diff --git a/.env.example b/.env.example index b218bdb84..6c5756fad 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ JWT_TOKEN_EXPIRES_IN=30d DATABASE_URL="postgresql://postgres:password@localhost:5432/docmost?schema=public" REDIS_URL=redis://127.0.0.1:6379 -# options: local | s3 +# options: local | s3 | azure STORAGE_DRIVER=local # S3 driver config @@ -21,6 +21,11 @@ AWS_S3_BUCKET= AWS_S3_ENDPOINT= AWS_S3_FORCE_PATH_STYLE= +# Azure Blob Storage driver config +AZURE_STORAGE_ACCOUNT_NAME= +AZURE_STORAGE_ACCOUNT_KEY= +AZURE_STORAGE_CONTAINER= + # default: 50mb FILE_UPLOAD_SIZE_LIMIT= @@ -48,6 +53,13 @@ GOTENBERG_URL= DISABLE_TELEMETRY=false +# Allow other sites to embed Docmost in an iframe. +IFRAME_EMBED_ALLOWED=false + +# Only used when IFRAME_EMBED_ALLOWED=true. When empty, any origin is allowed. +# Example: https://intranet.example.com,https://portal.example.com +IFRAME_ALLOWED_ORIGINS= + # Enable debug logging in production (default: false) DEBUG_MODE=false diff --git a/apps/client/package.json b/apps/client/package.json index 932b849cd..ccd0bac1c 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,92 +1,101 @@ { "name": "client", "private": true, - "version": "0.80.0", + "version": "0.90.0", "scripts": { "dev": "vite", "build": "tsc && vite build", "lint": "eslint .", "preview": "vite preview", - "format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\"" + "format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\"", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { - "@casl/react": "^5.0.1", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/modifiers": "^9.0.0", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", + "@atlaskit/pragmatic-drag-and-drop": "1.8.1", + "@atlaskit/pragmatic-drag-and-drop-auto-scroll": "2.1.5", + "@atlaskit/pragmatic-drag-and-drop-flourish": "2.0.15", + "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0", + "@atlaskit/pragmatic-drag-and-drop-live-region": "1.3.4", + "@casl/react": "5.0.1", + "@dnd-kit/core": "6.3.1", + "@dnd-kit/modifiers": "9.0.0", + "@dnd-kit/sortable": "10.0.0", + "@dnd-kit/utilities": "3.2.2", "@docmost/base-formula": "workspace:*", "@docmost/editor-ext": "workspace:*", - "@emoji-mart/data": "^1.2.1", - "@emoji-mart/react": "^1.1.1", "@excalidraw/excalidraw": "0.18.0-3a5ef40", - "@mantine/core": "^8.3.18", - "@mantine/dates": "^8.3.18", - "@mantine/form": "^8.3.18", - "@mantine/hooks": "^8.3.18", - "@mantine/modals": "^8.3.18", - "@mantine/notifications": "^8.3.18", - "@mantine/spotlight": "^8.3.18", - "@tabler/icons-react": "^3.40.0", - "@tanstack/react-query": "5.99.1", - "@tanstack/react-table": "^8.21.3", - "@tanstack/react-virtual": "^3.13.24", - "alfaaz": "^1.1.0", - "axios": "1.15.0", - "blueimp-load-image": "^5.16.0", - "clsx": "^2.1.1", - "emoji-mart": "^5.6.0", - "file-saver": "^2.0.5", - "highlightjs-sap-abap": "^0.3.0", - "i18next": "^25.10.1", - "i18next-http-backend": "^3.0.2", - "jotai": "^2.18.1", - "jotai-optics": "^0.4.0", - "js-cookie": "^3.0.5", - "jwt-decode": "^4.0.0", + "@mantine/core": "8.3.18", + "@mantine/dates": "8.3.18", + "@mantine/form": "8.3.18", + "@mantine/hooks": "8.3.18", + "@mantine/modals": "8.3.18", + "@mantine/notifications": "8.3.18", + "@mantine/spotlight": "8.3.18", + "@slidoapp/emoji-mart": "5.8.7", + "@slidoapp/emoji-mart-data": "1.2.4", + "@slidoapp/emoji-mart-react": "1.1.5", + "@tabler/icons-react": "3.40.0", + "@tanstack/react-query": "5.90.17", + "@tanstack/react-table": "8.21.3", + "@tanstack/react-virtual": "3.13.24", + "alfaaz": "1.1.0", + "axios": "1.16.0", + "blueimp-load-image": "5.16.0", + "clsx": "2.1.1", + "file-saver": "2.0.5", + "highlightjs-sap-abap": "0.3.0", + "i18next": "25.10.1", + "i18next-http-backend": "3.0.6", + "jotai": "2.18.1", + "jotai-optics": "0.4.0", + "js-cookie": "3.0.5", + "jwt-decode": "4.0.0", "katex": "0.16.40", - "lowlight": "^3.3.0", - "mantine-form-zod-resolver": "^1.3.0", - "mermaid": "^11.13.0", - "mitt": "^3.0.1", - "posthog-js": "1.363.1", - "react": "^18.3.1", - "react-arborist": "3.4.0", + "lowlight": "3.3.0", + "mantine-form-zod-resolver": "1.3.0", + "mermaid": "11.15.0", + "mitt": "3.0.1", + "posthog-js": "1.372.2", + "react": "18.3.1", "react-clear-modal": "^2.0.18", "react-dom": "^18.3.1", - "react-drawio": "^1.0.7", - "react-error-boundary": "^6.1.1", - "react-helmet-async": "^3.0.0", - "react-i18next": "^16.5.8", - "react-router-dom": "^7.13.1", - "semver": "^7.7.4", - "socket.io-client": "^4.8.3", - "tiptap-extension-global-drag-handle": "^0.1.18", - "zod": "^4.3.6" + "react-drawio": "1.0.7", + "react-error-boundary": "6.1.1", + "react-helmet-async": "3.0.0", + "react-i18next": "16.5.8", + "react-router-dom": "7.13.1", + "semver": "7.7.4", + "socket.io-client": "4.8.3", + "zod": "4.3.6" }, "devDependencies": { - "@eslint/js": "^9.28.0", - "@tanstack/eslint-plugin-query": "^5.94.4", - "@types/blueimp-load-image": "^5.16.6", - "@types/file-saver": "^2.0.7", - "@types/js-cookie": "^3.0.6", - "@types/katex": "^0.16.8", + "@eslint/js": "9.28.0", + "@tanstack/eslint-plugin-query": "5.94.4", + "@testing-library/jest-dom": "6.6.0", + "@testing-library/react": "16.1.0", + "@types/blueimp-load-image": "5.16.6", + "@types/file-saver": "2.0.7", + "@types/js-cookie": "3.0.6", + "@types/katex": "0.16.8", "@types/node": "22.19.1", - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^6.0.1", - "eslint": "^9.28.0", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.5.2", - "globals": "^15.13.0", - "optics-ts": "^2.4.1", - "postcss": "^8.5.8", - "postcss-preset-mantine": "^1.18.0", - "postcss-simple-vars": "^7.0.1", - "prettier": "^3.8.1", - "typescript": "^5.9.3", - "typescript-eslint": "^8.57.1", - "vite": "8.0.5" + "@types/react": "18.3.12", + "@types/react-dom": "18.3.1", + "@vitejs/plugin-react": "6.0.1", + "eslint": "9.28.0", + "eslint-plugin-react": "7.37.5", + "eslint-plugin-react-hooks": "7.0.1", + "eslint-plugin-react-refresh": "0.5.2", + "globals": "15.13.0", + "jsdom": "25.0.0", + "optics-ts": "2.4.1", + "postcss": "8.5.14", + "postcss-preset-mantine": "1.18.0", + "postcss-simple-vars": "7.0.1", + "prettier": "3.8.1", + "typescript": "5.9.3", + "typescript-eslint": "8.57.1", + "vite": "8.0.5", + "vitest": "4.1.6" } } diff --git a/apps/client/public/locales/de-DE/translation.json b/apps/client/public/locales/de-DE/translation.json index b6caf4d61..0368ad429 100644 --- a/apps/client/public/locales/de-DE/translation.json +++ b/apps/client/public/locales/de-DE/translation.json @@ -71,6 +71,7 @@ "Export": "Exportieren", "Failed to create page": "Erstellung der Seite fehlgeschlagen", "Failed to delete page": "Löschen der Seite fehlgeschlagen", + "Failed to restore page": "Seite konnte nicht wiederhergestellt werden", "Failed to fetch recent pages": "Fehler beim Abrufen der letzten Seiten", "Failed to import pages": "Import der Seiten fehlgeschlagen", "Failed to load page. An error occurred.": "Seite konnte nicht geladen werden. Es ist ein Fehler aufgetreten.", @@ -111,7 +112,7 @@ "Member": "Mitglied", "members": "Mitglieder", "Members": "Mitglieder", - "My preferences": "Meine Voreinstellungen", + "My preferences": "Meine Einstellungen", "My Profile": "Mein Profil", "My profile": "Mein Profil", "Name": "Name", @@ -139,7 +140,7 @@ "People": "Personen", "Pending": "Ausstehend", "Please confirm your action": "Bitte bestätigen Sie Ihre Aktion", - "Preferences": "Vorlieben", + "Preferences": "Einstellungen", "Print PDF": "PDF drucken", "Profile": "Profil", "Recently updated": "Kürzlich aktualisiert", @@ -276,6 +277,9 @@ "Align left": "Links ausrichten", "Align right": "Rechts ausrichten", "Align center": "Zentrieren", + "Alt text": "Alternativtext", + "Describe this for accessibility.": "Beschreiben Sie dies für die Barrierefreiheit.", + "Add a description": "Beschreibung hinzufügen", "Justify": "Blocksatz", "Merge cells": "Zellen zusammenführen", "Split cell": "Zelle teilen", @@ -286,6 +290,19 @@ "Add row above": "Zeile oben hinzufügen", "Add row below": "Zeile unten hinzufügen", "Delete table": "Tabelle löschen", + "Add column left": "Spalte links hinzufügen", + "Add column right": "Spalte rechts hinzufügen", + "Clear cell": "Zelle leeren", + "Clear cells": "Zellen leeren", + "Toggle header cell": "Kopfzelle umschalten", + "Toggle header column": "Kopfspalte umschalten", + "Toggle header row": "Kopfzeile umschalten", + "Move column left": "Spalte nach links verschieben", + "Move column right": "Spalte nach rechts verschieben", + "Move row down": "Zeile nach unten verschieben", + "Move row up": "Zeile nach oben verschieben", + "Sort A → Z": "A → Z sortieren", + "Sort Z → A": "Z → A sortieren", "Info": "Info", "Note": "Hinweis", "Success": "Erfolg", @@ -348,6 +365,8 @@ "Create block quote.": "Erstellen Sie ein Blockzitat.", "Insert code snippet.": "Code-Snippet einfügen.", "Insert horizontal rule divider": "Horizontale Trennlinie einfügen", + "Page break": "Seitenumbruch", + "Insert a page break for printing.": "Einen Seitenumbruch zum Drucken einfügen.", "Upload any image from your device.": "Laden Sie ein beliebiges Bild von Ihrem Gerät hoch.", "Upload any video from your device.": "Laden Sie ein beliebiges Video von Ihrem Gerät hoch.", "Upload any audio from your device.": "Laden Sie beliebige Audiodateien von Ihrem Gerät hoch.", @@ -391,7 +410,11 @@ "Write anything. Enter \"/\" for commands": "Schreiben Sie etwas. Geben Sie \"/\" für Befehle ein", "Write...": "\"Schreiben...\"", "Column count": "Spaltenanzahl", - "{{count}} Columns": "{count, plural, one {# Spalte} other {# Spalten}}", + "{{count}} Columns": "{{count}} Spalten", + "{{count}} command available_one": "1 Befehl verfügbar", + "{{count}} command available_other": "{{count}} Befehle verfügbar", + "{{count}} result available_one": "1 Ergebnis verfügbar", + "{{count}} result available_other": "{{count}} Ergebnisse verfügbar", "Equal columns": "Gleich breite Spalten", "Left sidebar": "Linke Seitenleiste", "Right sidebar": "Rechte Seitenleiste", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} ist verfügbar", "Default page edit mode": "Standard-Bearbeitungsmodus für Seiten", "Choose your preferred page edit mode. Avoid accidental edits.": "Wählen Sie Ihren bevorzugten Seitenbearbeitungsmodus. Vermeiden Sie versehentliche Bearbeitungen.", + "Choose {{format}} file": "{{format}}-Datei auswählen", "Reading": "Lesen", "Delete member": "Mitglied löschen", "Member deleted successfully": "Mitglied erfolgreich gelöscht", @@ -565,6 +589,8 @@ "Move to trash": "In den Papierkorb verschieben", "Move this page to trash?": "Diese Seite in den Papierkorb verschieben?", "Restore page": "Seite wiederherstellen", + "Permanently delete": "Endgültig löschen", + "{{name}} moved this page to Trash {{time}}.": "{{name}} hat diese Seite {{time}} in den Papierkorb verschoben.", "Page moved to trash": "Seite in den Papierkorb verschoben", "Page restored successfully": "Seite erfolgreich wiederhergestellt", "Deleted by": "Gelöscht von", @@ -608,25 +634,21 @@ "Image exceeds 10MB limit.": "Bild überschreitet das Limit von 10 MB.", "Image removed successfully": "Bild erfolgreich entfernt", "API key": "API-Schlüssel", - "API key created successfully": "API-Schlüssel erfolgreich erstellt", "API keys": "API-Schlüssel", "API management": "API-Verwaltung", - "Are you sure you want to revoke this API key": "Sind Sie sicher, dass Sie diesen API-Schlüssel widerrufen möchten?", - "Create API Key": "API-Schlüssel erstellen", "Custom expiration date": "Benutzerdefiniertes Ablaufdatum", "Enter a descriptive token name": "Geben Sie einen beschreibenden Token-Namen ein", "Expiration": "Ablauf", "Expired": "Abgelaufen", "Expires": "Läuft ab", - "I've saved my API key": "Ich habe meinen API-Schlüssel gespeichert", "Last use": "Zuletzt verwendet", "No API keys found": "Keine API-Schlüssel gefunden", "No expiration": "Kein Ablauf", - "Revoke API key": "API-Schlüssel widerrufen", "Revoked successfully": "Erfolgreich widerrufen", "Select expiration date": "Ablaufdatum wählen", "This action cannot be undone. Any applications using this API key will stop working.": "Diese Aktion kann nicht rückgängig gemacht werden. Alle Anwendungen, die diesen API-Schlüssel verwenden, werden nicht mehr funktionieren.", - "Update API key": "API-Schlüssel aktualisieren", + "Update": "Aktualisieren", + "Update {{credential}}": "{{credential}} aktualisieren", "Manage API keys for all users in the workspace": "Verwalten Sie API-Schlüssel für alle Benutzer im Arbeitsbereich", "Restrict API key creation to admins": "API-Schlüsselerstellung auf Administratoren beschränken", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Nur Administratoren und Eigentümer können neue API-Schlüssel erstellen. Bestehende Mitgliederschlüssel funktionieren weiterhin.", @@ -858,9 +880,12 @@ "AI Chat": "KI-Chat", "Analyze for insights": "Für Erkenntnisse analysieren", "Ask anything...": "Fragen Sie irgendetwas...", + "Assistant said:": "Assistent sagte:", "Chat history": "Chatverlauf", "Chat name": "Chatname", + "Chat transcript": "Chatprotokoll", "Close": "Schließen", + "Copy assistant response": "Antwort des Assistenten kopieren", "Docmost AI": "Docmost KI", "Failed to load chat. An error occurred.": "Chat konnte nicht geladen werden. Ein Fehler ist aufgetreten.", "Failed to render this message.": "Diese Nachricht konnte nicht dargestellt werden.", @@ -870,9 +895,17 @@ "No chats found": "Keine Chats gefunden", "No conversations yet": "Noch keine Unterhaltungen", "Open full page": "Ganze Seite öffnen", + "Scroll to bottom": "Nach unten scrollen", + "You said:": "Sie sagten:", "Previous 7 days": "Letzte 7 Tage", "Previous 30 days": "Letzte 30 Tage", "Search chats...": "Chats durchsuchen...", + "Search chats": "Chats durchsuchen", + "Ask anything... Use @ to mention pages": "Frag etwas ... Verwende @, um Seiten zu erwähnen", + "Ask anything or search your workspace": "Fragen Sie etwas oder durchsuchen Sie Ihren Workspace", + "Welcome to {{name}}": "Willkommen bei {{name}}", + "Add files": "Dateien hinzufügen", + "Mention a page": "Eine Seite einfügen", "Start a new chat to see it here.": "Starten Sie einen neuen Chat, damit er hier angezeigt wird.", "Summarize this page": "Diese Seite zusammenfassen", "Toggle AI Chat": "KI-Chat umschalten", @@ -880,5 +913,176 @@ "Try a different search term.": "Versuchen Sie einen anderen Suchbegriff.", "Try again": "Erneut versuchen", "Untitled chat": "Chat ohne Titel", - "What can I help you with?": "Womit kann ich Ihnen helfen?" + "What can I help you with?": "Womit kann ich Ihnen helfen?", + "Are you sure you want to revoke this {{credential}}": "Sind Sie sicher, dass Sie diese(n) {{credential}} widerrufen möchten?", + "Automatically provision users and groups from your identity provider via SCIM.": "Stellen Sie Benutzer und Gruppen automatisch über SCIM von Ihrem Identitätsanbieter bereit.", + "Configure your identity provider with this URL to provision users and groups.": "Konfigurieren Sie Ihren Identitätsanbieter mit dieser URL, um Benutzer und Gruppen bereitzustellen.", + "Create {{credential}}": "{{credential}} erstellen", + "{{credential}} created": "{{credential}} erstellt", + "{{credential}} created successfully": "{{credential}} erfolgreich erstellt", + "Created by": "Erstellt von", + "Custom": "Benutzerdefiniert", + "Enable SCIM": "SCIM aktivieren", + "Enter a descriptive name": "Geben Sie einen beschreibenden Namen ein", + "I've saved my {{credential}}": "Ich habe meine(n) {{credential}} gespeichert", + "Important": "Wichtig", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "Stellen Sie sicher, dass Sie Ihre(n) {{credential}} jetzt kopieren. Sie können sie/ihn später nicht erneut anzeigen!", + "Never": "Nie", + "Revoke {{credential}}": "{{credential}} widerrufen", + "SCIM endpoint URL": "SCIM-Endpunkt-URL", + "SCIM provisioning": "SCIM-Bereitstellung", + "SCIM takes precedence over SSO group sync while enabled.": "SCIM hat Vorrang vor der SSO-Gruppensynchronisierung, solange es aktiviert ist.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "Sie haben die maximale Anzahl von {{max}} SCIM-Token erreicht. Löschen Sie ein vorhandenes Token, um ein neues zu erstellen.", + "SCIM token": "SCIM-Token", + "SCIM tokens": "SCIM-Token", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "Diese Aktion kann nicht rückgängig gemacht werden. Ihr Identitätsanbieter wird die Synchronisierung sofort beenden.", + "Toggle SCIM provisioning": "SCIM-Bereitstellung umschalten", + "Token": "Token", + "Page menu": "Seitenmenü", + "Expand": "Erweitern", + "Collapse": "Reduzieren", + "Comment menu": "Kommentarmenü", + "Group menu": "Gruppenmenü", + "Show hidden breadcrumbs": "Ausgeblendete Breadcrumbs anzeigen", + "Breadcrumbs": "Navigationspfade", + "Page actions": "Seitenaktionen", + "Pick emoji": "Emoji auswählen", + "Template menu": "Vorlagenmenü", + "Use": "Verwenden", + "Use template": "Vorlage verwenden", + "Preview template: {{title}}": "Vorlage anzeigen: {{title}}", + "Use a template": "Eine Vorlage verwenden", + "Search templates...": "Vorlagen suchen...", + "Search spaces...": "Bereiche suchen...", + "No templates found": "Keine Vorlagen gefunden", + "No spaces found": "Keine Bereiche gefunden", + "Browse all templates": "Alle Vorlagen durchsuchen", + "This space": "Dieser Bereich", + "All templates": "Alle Vorlagen", + "Global": "Global", + "New template": "Neue Vorlage", + "Edit template": "Vorlage bearbeiten", + "Are you sure you want to delete this template?": "Sind Sie sicher, dass Sie diese Vorlage löschen möchten?", + "Template scope updated": "Vorlagenbereich aktualisiert", + "Choose which space this template belongs to": "Wählen Sie den Bereich aus, zu dem diese Vorlage gehört", + "Scope": "Bereich", + "Select scope": "Bereich auswählen", + "Title": "Titel", + "Saving...": "Wird gespeichert...", + "Saved": "Gespeichert", + "Save failed. Retry": "Speichern fehlgeschlagen. Erneut versuchen", + "By {{name}}": "Von {{name}}", + "Updated {{time}}": "Aktualisiert {{time}}", + "Choose destination": "Ziel auswählen", + "Search pages and spaces...": "Seiten und Bereiche suchen...", + "No results found": "Keine Ergebnisse gefunden", + "You don't have permission to create pages here": "Sie haben hier keine Berechtigung, Seiten zu erstellen", + "Chat menu": "Chatmenü", + "API key menu": "API-Schlüssel-Menü", + "Jump to comment selection": "Zur Kommentarauswahl springen", + "Slash commands": "Slash-Befehle", + "Mention suggestions": "Erwähnungsvorschläge", + "Link suggestions": "Linkvorschläge", + "Diagram editor": "Diagrammeditor", + "Add comment": "Kommentar hinzufügen", + "Find and replace": "Suchen und ersetzen", + "Main navigation": "Hauptnavigation", + "Space navigation": "Bereichsnavigation", + "Settings navigation": "Einstellungsnavigation", + "AI navigation": "KI-Navigation", + "Breadcrumb": "Navigationspfad", + "Synced block": "Synchronisierter Block", + "Create a block that stays in sync across pages.": "Erstellt einen Block der über mehrere Seiten synchronisiert wird", + "Editing original": "Original bearbeiten", + "Copy synced block": "Synchronisierten Block kopieren", + "Unsync": "Synchronisierung aufheben", + "Delete synced block": "Synchronisierten Block löschen", + "Synced to {{count}} other page_one": "Mit {{count}} anderer Seite synchronisiert", + "Synced to {{count}} other page_other": "Mit {{count}} anderen Seiten synchronisiert", + "ORIGINAL": "ORIGINAL", + "THIS PAGE": "DIESE SEITE", + "No pages": "Keine Seiten", + "The original synced block no longer exists": "Der originale synchronisierte Block existiert nicht mehr", + "You don't have access to this synced block": "Sie haben keinen Zugriff auf diesen synchronisierten Block", + "Failed to load this synced block": "Dieser synchronisierte Block konnte nicht geladen werden", + "Fixed editor toolbar": "Fixierte Editor-Symbolleiste", + "Show a formatting toolbar above the editor with quick access to common actions.": "Anzeige einer Formatierungs-Symbolleiste über dem Editor für schnellen Zugriff auf Aktionen.", + "Toggle fixed editor toolbar": "Fixierte Editor-Symbolleiste ein/aus", + "Normal text": "Normaler Text", + "More inline formatting": "Weitere Formatierung", + "Subscript": "Tiefgestellt", + "Superscript": "Hochgestellt", + "Inline code": "Inline-Code", + "Insert media": "Medien einfügen", + "Mention": "Erwähnung", + "Emoji": "Emoji", + "Columns": "Spalten", + "More inserts": "Weiteren Inhalt einfügen", + "Embeds": "Einbettungen", + "Diagrams": "Diagramme", + "Advanced": "Erweitert", + "Utility": "Dienstprogramme", + "Decrease indent": "Einzug verkleinern", + "Increase indent": "Einzug vergrößern", + "Clear formatting": "Formatierung zurücksetzen", + "Code block": "Codeblock", + "Experimental": "Experimentell", + "Strikethrough": "Durchgestrichen", + "Undo": "Rückgängig", + "Redo": "Wiederholen", + "Backlinks": "Rückverweise", + "Last updated by": "Zuletzt aktualisiert von", + "Last updated": "Zuletzt aktualisiert", + "Stats": "Statistiken", + "Word count": "Wörter", + "Characters": "Zeichen", + "Incoming links": "Eingehende Links", + "Outgoing links": "Ausgehende Links", + "Incoming links ({{count}})": "Eingehende Links ({{count}})", + "Outgoing links ({{count}})": "Ausgehende Links ({{count}})", + "No pages link here yet.": "Aktuell verlinken keine Seiten hierher.", + "This page doesn't link to other pages yet.": "Diese Seite verlinkt noch nicht auf andere Seiten.", + "Verified until {{date}}": "Verifiziert bis zum {{date}}", + "Labels": "Beschriftungen", + "Add label": "Beschriftung hinzufügen", + "No labels yet": "Noch keine Beschriftungen", + "Already added": "Bereits hinzugefügt", + "Invalid label name": "Ungültiger Beschriftungsname", + "No matches": "Keine Treffer", + "Search or create…": "Suchen oder erstellen…", + "Remove label {{name}}": "Beschriftung {{name}} entfernen", + "Failed to add label": "Beschriftung konnte nicht hinzugefügt werden", + "Failed to remove label": "Beschriftung konnte nicht entfernt werden", + "No pages with this label": "Keine Seiten mit dieser Beschriftung", + "Pages tagged with this label will appear here.": "Hier werden Seiten angezeigt, die mit dieser Beschriftung versehen sind.", + "No pages match your search.": "Es konnten keine Seiten gefunden werden, die mit Ihrer Suche übereinstimmen.", + "Updated {{date}}": "Aktualisiert am {{date}}", + "Cell actions": "Zellaktionen", + "Column actions": "Spaltenaktionen", + "Row actions": "Zeilenaktionen", + "Filter": "Filter", + "Page title": "Seitentitel", + "Page content": "Seiteninhalt", + "Member actions": "Mitgliederaktionen", + "Toggle password visibility": "Passwortsichtbarkeit umschalten", + "Send comment": "Kommentar senden", + "Token actions": "Token-Aktionen", + "Template settings": "Vorlageneinstellungen", + "Edit diagram": "Diagramm bearbeiten", + "Edit embed": "Einbettung bearbeiten", + "Edit drawing": "Zeichnung bearbeiten", + "Delete equation": "Gleichung löschen", + "Invite actions": "Einladungsaktionen", + "Get started": "Erste Schritte", + "* indicates required fields": "* kennzeichnet Pflichtfelder", + "List of spaces in this workspace": "Liste der Bereiche in diesem Workspace", + "Active sessions": "Aktive Sitzungen", + "Add {{name}} to favorites": "{{name}} zu Favoriten hinzufügen", + "Remove {{name}} from favorites": "{{name}} aus Favoriten entfernen", + "Added to favorites": "Zu Favoriten hinzugefügt", + "Removed from favorites": "Aus Favoriten entfernt", + "Added {{name}} to favorites": "{{name}} zu Favoriten hinzugefügt", + "Removed {{name}} from favorites": "{{name}} aus Favoriten entfernt", + "Page menu for {{name}}": "Seitenmenü für {{name}}", + "Create subpage of {{name}}": "Unterseite von {{name}} erstellen" } diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 3bfe7c9f0..ec40a1967 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -71,6 +71,7 @@ "Export": "Export", "Failed to create page": "Failed to create page", "Failed to delete page": "Failed to delete page", + "Failed to restore page": "Failed to restore page", "Failed to fetch recent pages": "Failed to fetch recent pages", "Failed to import pages": "Failed to import pages", "Failed to load page. An error occurred.": "Failed to load page. An error occurred.", @@ -276,6 +277,9 @@ "Align left": "Align left", "Align right": "Align right", "Align center": "Align center", + "Alt text": "Alt text", + "Describe this for accessibility.": "Describe this for accessibility.", + "Add a description": "Add a description", "Justify": "Justify", "Merge cells": "Merge cells", "Split cell": "Split cell", @@ -286,6 +290,19 @@ "Add row above": "Add row above", "Add row below": "Add row below", "Delete table": "Delete table", + "Add column left": "Add column left", + "Add column right": "Add column right", + "Clear cell": "Clear cell", + "Clear cells": "Clear cells", + "Toggle header cell": "Toggle header cell", + "Toggle header column": "Toggle header column", + "Toggle header row": "Toggle header row", + "Move column left": "Move column left", + "Move column right": "Move column right", + "Move row down": "Move row down", + "Move row up": "Move row up", + "Sort A → Z": "Sort A → Z", + "Sort Z → A": "Sort Z → A", "Info": "Info", "Note": "Note", "Success": "Success", @@ -348,6 +365,8 @@ "Create block quote.": "Create block quote.", "Insert code snippet.": "Insert code snippet.", "Insert horizontal rule divider": "Insert horizontal rule divider", + "Page break": "Page break", + "Insert a page break for printing.": "Insert a page break for printing.", "Upload any image from your device.": "Upload any image from your device.", "Upload any video from your device.": "Upload any video from your device.", "Upload any audio from your device.": "Upload any audio from your device.", @@ -392,6 +411,10 @@ "Write...": "Write...", "Column count": "Column count", "{{count}} Columns": "{{count}} Columns", + "{{count}} command available_one": "1 command available", + "{{count}} command available_other": "{{count}} commands available", + "{{count}} result available_one": "1 result available", + "{{count}} result available_other": "{{count}} results available", "Equal columns": "Equal columns", "Left sidebar": "Left sidebar", "Right sidebar": "Right sidebar", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} is available", "Default page edit mode": "Default page edit mode", "Choose your preferred page edit mode. Avoid accidental edits.": "Choose your preferred page edit mode. Avoid accidental edits.", + "Choose {{format}} file": "Choose {{format}} file", "Reading": "Reading", "Delete member": "Delete member", "Member deleted successfully": "Member deleted successfully", @@ -565,6 +589,8 @@ "Move to trash": "Move to trash", "Move this page to trash?": "Move this page to trash?", "Restore page": "Restore page", + "Permanently delete": "Permanently delete", + "{{name}} moved this page to Trash {{time}}.": "{{name}} moved this page to Trash {{time}}.", "Page moved to trash": "Page moved to trash", "Page restored successfully": "Page restored successfully", "Deleted by": "Deleted by", @@ -608,25 +634,21 @@ "Image exceeds 10MB limit.": "Image exceeds 10MB limit.", "Image removed successfully": "Image removed successfully", "API key": "API key", - "API key created successfully": "API key created successfully", "API keys": "API keys", "API management": "API management", - "Are you sure you want to revoke this API key": "Are you sure you want to revoke this API key", - "Create API Key": "Create API Key", "Custom expiration date": "Custom expiration date", "Enter a descriptive token name": "Enter a descriptive token name", "Expiration": "Expiration", "Expired": "Expired", "Expires": "Expires", - "I've saved my API key": "I've saved my API key", "Last use": "Last Used", "No API keys found": "No API keys found", "No expiration": "No expiration", - "Revoke API key": "Revoke API key", "Revoked successfully": "Revoked successfully", "Select expiration date": "Select expiration date", "This action cannot be undone. Any applications using this API key will stop working.": "This action cannot be undone. Any applications using this API key will stop working.", - "Update API key": "Update API key", + "Update": "Update", + "Update {{credential}}": "Update {{credential}}", "Manage API keys for all users in the workspace": "Manage API keys for all users in the workspace", "Restrict API key creation to admins": "Restrict API key creation to admins", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Only admins and owners can create new API keys. Existing member keys will continue to work.", @@ -858,9 +880,12 @@ "AI Chat": "AI Chat", "Analyze for insights": "Analyze for insights", "Ask anything...": "Ask anything...", + "Assistant said:": "Assistant said:", "Chat history": "Chat history", "Chat name": "Chat name", + "Chat transcript": "Chat transcript", "Close": "Close", + "Copy assistant response": "Copy assistant response", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "Failed to load chat. An error occurred.", "Failed to render this message.": "Failed to render this message.", @@ -870,9 +895,17 @@ "No chats found": "No chats found", "No conversations yet": "No conversations yet", "Open full page": "Open full page", + "Scroll to bottom": "Scroll to bottom", + "You said:": "You said:", "Previous 7 days": "Previous 7 days", "Previous 30 days": "Previous 30 days", "Search chats...": "Search chats...", + "Search chats": "Search chats", + "Ask anything... Use @ to mention pages": "Ask anything... Use @ to mention pages", + "Ask anything or search your workspace": "Ask anything or search your workspace", + "Welcome to {{name}}": "Welcome to {{name}}", + "Add files": "Add files", + "Mention a page": "Mention a page", "Start a new chat to see it here.": "Start a new chat to see it here.", "Summarize this page": "Summarize this page", "Toggle AI Chat": "Toggle AI Chat", @@ -880,5 +913,176 @@ "Try a different search term.": "Try a different search term.", "Try again": "Try again", "Untitled chat": "Untitled chat", - "What can I help you with?": "What can I help you with?" + "What can I help you with?": "What can I help you with?", + "Are you sure you want to revoke this {{credential}}": "Are you sure you want to revoke this {{credential}}", + "Automatically provision users and groups from your identity provider via SCIM.": "Automatically provision users and groups from your identity provider via SCIM.", + "Configure your identity provider with this URL to provision users and groups.": "Configure your identity provider with this URL to provision users and groups.", + "Create {{credential}}": "Create {{credential}}", + "{{credential}} created": "{{credential}} created", + "{{credential}} created successfully": "{{credential}} created successfully", + "Created by": "Created by", + "Custom": "Custom", + "Enable SCIM": "Enable SCIM", + "Enter a descriptive name": "Enter a descriptive name", + "I've saved my {{credential}}": "I've saved my {{credential}}", + "Important": "Important", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "Make sure to copy your {{credential}} now. You won't be able to see it again!", + "Never": "Never", + "Revoke {{credential}}": "Revoke {{credential}}", + "SCIM endpoint URL": "SCIM endpoint URL", + "SCIM provisioning": "SCIM provisioning", + "SCIM takes precedence over SSO group sync while enabled.": "SCIM takes precedence over SSO group sync while enabled.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.", + "SCIM token": "SCIM token", + "SCIM tokens": "SCIM tokens", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "This action cannot be undone. Your identity provider will stop syncing immediately.", + "Toggle SCIM provisioning": "Toggle SCIM provisioning", + "Token": "Token", + "Page menu": "Page menu", + "Expand": "Expand", + "Collapse": "Collapse", + "Comment menu": "Comment menu", + "Group menu": "Group menu", + "Show hidden breadcrumbs": "Show hidden breadcrumbs", + "Breadcrumbs": "Breadcrumbs", + "Page actions": "Page actions", + "Pick emoji": "Pick emoji", + "Template menu": "Template menu", + "Use": "Use", + "Use template": "Use template", + "Preview template: {{title}}": "Preview template: {{title}}", + "Use a template": "Use a template", + "Search templates...": "Search templates...", + "Search spaces...": "Search spaces...", + "No templates found": "No templates found", + "No spaces found": "No spaces found", + "Browse all templates": "Browse all templates", + "This space": "This space", + "All templates": "All templates", + "Global": "Global", + "New template": "New template", + "Edit template": "Edit template", + "Are you sure you want to delete this template?": "Are you sure you want to delete this template?", + "Template scope updated": "Template scope updated", + "Choose which space this template belongs to": "Choose which space this template belongs to", + "Scope": "Scope", + "Select scope": "Select scope", + "Title": "Title", + "Saving...": "Saving...", + "Saved": "Saved", + "Save failed. Retry": "Save failed. Retry", + "By {{name}}": "By {{name}}", + "Updated {{time}}": "Updated {{time}}", + "Choose destination": "Choose destination", + "Search pages and spaces...": "Search pages and spaces...", + "No results found": "No results found", + "You don't have permission to create pages here": "You don't have permission to create pages here", + "Chat menu": "Chat menu", + "API key menu": "API key menu", + "Jump to comment selection": "Jump to comment selection", + "Slash commands": "Slash commands", + "Mention suggestions": "Mention suggestions", + "Link suggestions": "Link suggestions", + "Diagram editor": "Diagram editor", + "Add comment": "Add comment", + "Find and replace": "Find and replace", + "Main navigation": "Main navigation", + "Space navigation": "Space navigation", + "Settings navigation": "Settings navigation", + "AI navigation": "AI navigation", + "Breadcrumb": "Breadcrumb", + "Synced block": "Synced block", + "Create a block that stays in sync across pages.": "Create a block that stays in sync across pages.", + "Editing original": "Editing original", + "Copy synced block": "Copy synced block", + "Unsync": "Unsync", + "Delete synced block": "Delete synced block", + "Synced to {{count}} other page_one": "Synced to {{count}} other page", + "Synced to {{count}} other page_other": "Synced to {{count}} other pages", + "ORIGINAL": "ORIGINAL", + "THIS PAGE": "THIS PAGE", + "No pages": "No pages", + "The original synced block no longer exists": "The original synced block no longer exists", + "You don't have access to this synced block": "You don't have access to this synced block", + "Failed to load this synced block": "Failed to load this synced block", + "Fixed editor toolbar": "Fixed editor toolbar", + "Show a formatting toolbar above the editor with quick access to common actions.": "Show a formatting toolbar above the editor with quick access to common actions.", + "Toggle fixed editor toolbar": "Toggle fixed editor toolbar", + "Normal text": "Normal text", + "More inline formatting": "More inline formatting", + "Subscript": "Subscript", + "Superscript": "Superscript", + "Inline code": "Inline code", + "Insert media": "Insert media", + "Mention": "Mention", + "Emoji": "Emoji", + "Columns": "Columns", + "More inserts": "More inserts", + "Embeds": "Embeds", + "Diagrams": "Diagrams", + "Advanced": "Advanced", + "Utility": "Utility", + "Decrease indent": "Decrease indent", + "Increase indent": "Increase indent", + "Clear formatting": "Clear formatting", + "Code block": "Code block", + "Experimental": "Experimental", + "Strikethrough": "Strikethrough", + "Undo": "Undo", + "Redo": "Redo", + "Backlinks": "Backlinks", + "Last updated by": "Last updated by", + "Last updated": "Last updated", + "Stats": "Stats", + "Word count": "Word count", + "Characters": "Characters", + "Incoming links": "Incoming links", + "Outgoing links": "Outgoing links", + "Incoming links ({{count}})": "Incoming links ({{count}})", + "Outgoing links ({{count}})": "Outgoing links ({{count}})", + "No pages link here yet.": "No pages link here yet.", + "This page doesn't link to other pages yet.": "This page doesn't link to other pages yet.", + "Verified until {{date}}": "Verified until {{date}}", + "Labels": "Labels", + "Add label": "Add label", + "No labels yet": "No labels yet", + "Already added": "Already added", + "Invalid label name": "Invalid label name", + "No matches": "No matches", + "Search or create…": "Search or create…", + "Remove label {{name}}": "Remove label {{name}}", + "Failed to add label": "Failed to add label", + "Failed to remove label": "Failed to remove label", + "No pages with this label": "No pages with this label", + "Pages tagged with this label will appear here.": "Pages tagged with this label will appear here.", + "No pages match your search.": "No pages match your search.", + "Updated {{date}}": "Updated {{date}}", + "Cell actions": "Cell actions", + "Column actions": "Column actions", + "Row actions": "Row actions", + "Filter": "Filter", + "Page title": "Page title", + "Page content": "Page content", + "Member actions": "Member actions", + "Toggle password visibility": "Toggle password visibility", + "Send comment": "Send comment", + "Token actions": "Token actions", + "Template settings": "Template settings", + "Edit diagram": "Edit diagram", + "Edit embed": "Edit embed", + "Edit drawing": "Edit drawing", + "Delete equation": "Delete equation", + "Invite actions": "Invite actions", + "Get started": "Get started", + "* indicates required fields": "* indicates required fields", + "List of spaces in this workspace": "List of spaces in this workspace", + "Active sessions": "Active sessions", + "Add {{name}} to favorites": "Add {{name}} to favorites", + "Remove {{name}} from favorites": "Remove {{name}} from favorites", + "Added to favorites": "Added to favorites", + "Removed from favorites": "Removed from favorites", + "Added {{name}} to favorites": "Added {{name}} to favorites", + "Removed {{name}} from favorites": "Removed {{name}} from favorites", + "Page menu for {{name}}": "Page menu for {{name}}", + "Create subpage of {{name}}": "Create subpage of {{name}}" } diff --git a/apps/client/public/locales/es-ES/translation.json b/apps/client/public/locales/es-ES/translation.json index e9ceacf98..131977738 100644 --- a/apps/client/public/locales/es-ES/translation.json +++ b/apps/client/public/locales/es-ES/translation.json @@ -71,6 +71,7 @@ "Export": "Exportar", "Failed to create page": "No se pudo crear la página", "Failed to delete page": "No se pudo eliminar la página", + "Failed to restore page": "No se pudo restaurar la página", "Failed to fetch recent pages": "Error al obtener las páginas recientes", "Failed to import pages": "No se pudieron importar las páginas", "Failed to load page. An error occurred.": "Error al cargar la página. Se produjo un error.", @@ -276,6 +277,9 @@ "Align left": "Alinear a la izquierda", "Align right": "Alinear a la derecha", "Align center": "Alinear al centro", + "Alt text": "Texto alternativo", + "Describe this for accessibility.": "Describe esto para la accesibilidad.", + "Add a description": "Agregar una descripción", "Justify": "Justificar", "Merge cells": "Combinar celdas", "Split cell": "Dividir celda", @@ -286,6 +290,19 @@ "Add row above": "Agregar fila arriba", "Add row below": "Agregar fila debajo", "Delete table": "Eliminar tabla", + "Add column left": "Agregar columna a la izquierda", + "Add column right": "Agregar columna a la derecha", + "Clear cell": "Borrar celda", + "Clear cells": "Borrar celdas", + "Toggle header cell": "Alternar celda de encabezado", + "Toggle header column": "Alternar columna de encabezado", + "Toggle header row": "Alternar fila de encabezado", + "Move column left": "Mover columna a la izquierda", + "Move column right": "Mover columna a la derecha", + "Move row down": "Mover fila hacia abajo", + "Move row up": "Mover fila hacia arriba", + "Sort A → Z": "Ordenar de A → Z", + "Sort Z → A": "Ordenar de Z → A", "Info": "Información", "Note": "Nota", "Success": "Satisfactorio", @@ -348,6 +365,8 @@ "Create block quote.": "Crear una cita en bloque.", "Insert code snippet.": "Insertar fragmento de código.", "Insert horizontal rule divider": "Insertar regla horizontal", + "Page break": "Salto de página", + "Insert a page break for printing.": "Inserta un salto de página para imprimir.", "Upload any image from your device.": "Sube cualquier imagen desde tu dispositivo.", "Upload any video from your device.": "Sube cualquier video desde tu dispositivo.", "Upload any audio from your device.": "Sube cualquier audio desde tu dispositivo.", @@ -392,6 +411,10 @@ "Write...": "Escribe...", "Column count": "Número de columnas", "{{count}} Columns": "{count, plural, one {# columna} other {# columnas}}", + "{{count}} command available_one": "1 comando disponible", + "{{count}} command available_other": "{{count}} comandos disponibles", + "{{count}} result available_one": "1 resultado disponible", + "{{count}} result available_other": "{{count}} resultados disponibles", "Equal columns": "Columnas iguales", "Left sidebar": "Barra lateral izquierda", "Right sidebar": "Barra lateral derecha", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} está disponible", "Default page edit mode": "Modo de edición predeterminado de la página", "Choose your preferred page edit mode. Avoid accidental edits.": "Elige tu modo de edición de página preferido. Evita ediciones accidentales.", + "Choose {{format}} file": "Elegir archivo {{format}}", "Reading": "Lectura", "Delete member": "Eliminar miembro", "Member deleted successfully": "Miembro eliminado correctamente", @@ -565,6 +589,8 @@ "Move to trash": "Mover a la papelera", "Move this page to trash?": "¿Mover esta página a la papelera?", "Restore page": "Restaurar página", + "Permanently delete": "Eliminar permanentemente", + "{{name}} moved this page to Trash {{time}}.": "{{name}} movió esta página a la Papelera {{time}}.", "Page moved to trash": "Página movida a la papelera", "Page restored successfully": "Página restaurada correctamente", "Deleted by": "Eliminado por", @@ -608,25 +634,21 @@ "Image exceeds 10MB limit.": "La imagen excede del límite de 10 MB", "Image removed successfully": "Imagen eliminada correctamente", "API key": "Clave API", - "API key created successfully": "Clave API creada correctamente", "API keys": "Claves API", "API management": "Gestión de API", - "Are you sure you want to revoke this API key": "¿Está seguro de que desea revocar esta clave API? ", - "Create API Key": "Crear clave API", "Custom expiration date": "Fecha de vencimiento personalizada", "Enter a descriptive token name": "Introduce un nombre descriptivo del token", "Expiration": "Vencimiento", "Expired": "Vencido", "Expires": "Vence", - "I've saved my API key": "He guardado mi clave API", "Last use": "Último uso", "No API keys found": "No se han encontrado claves API", "No expiration": "Sin vencimiento", - "Revoke API key": "Revocar clave API", "Revoked successfully": "Revocada correctamente", "Select expiration date": "Seleccionar fecha de vencimiento", "This action cannot be undone. Any applications using this API key will stop working.": "Esta acción no se puede deshacer. Las aplicaciones que utilicen esta clave API dejarán de funcionar.", - "Update API key": "Actualizar clave API", + "Update": "Actualizar", + "Update {{credential}}": "Actualizar {{credential}}", "Manage API keys for all users in the workspace": "Gestionar claves API para todos los usuarios en el espacio de trabajo", "Restrict API key creation to admins": "Restringir la creación de claves API a administradores", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Solo los administradores y propietarios pueden crear nuevas claves API. Las claves de miembros existentes seguirán funcionando.", @@ -858,9 +880,12 @@ "AI Chat": "Chat de IA", "Analyze for insights": "Analizar para obtener información", "Ask anything...": "Pregunta lo que quieras...", + "Assistant said:": "El asistente dijo:", "Chat history": "Historial de chat", "Chat name": "Nombre del chat", + "Chat transcript": "Transcripción del chat", "Close": "Cerrar", + "Copy assistant response": "Copiar respuesta del asistente", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "No se pudo cargar el chat. Se produjo un error.", "Failed to render this message.": "No se pudo mostrar este mensaje.", @@ -870,9 +895,17 @@ "No chats found": "No se encontraron chats", "No conversations yet": "Aún no hay conversaciones", "Open full page": "Abrir página completa", + "Scroll to bottom": "Desplazarse hasta abajo", + "You said:": "Dijiste:", "Previous 7 days": "Últimos 7 días", "Previous 30 days": "Últimos 30 días", "Search chats...": "Buscar chats...", + "Search chats": "Buscar chats", + "Ask anything... Use @ to mention pages": "Pregunta lo que sea... Usa @ para mencionar páginas", + "Ask anything or search your workspace": "Pregunta cualquier cosa o busca en tu espacio de trabajo", + "Welcome to {{name}}": "Te damos la bienvenida a {{name}}", + "Add files": "Agregar archivos", + "Mention a page": "Mencionar una página", "Start a new chat to see it here.": "Inicia un nuevo chat para verlo aquí.", "Summarize this page": "Resumir esta página", "Toggle AI Chat": "Alternar chat de IA", @@ -880,5 +913,176 @@ "Try a different search term.": "Prueba con otro término de búsqueda.", "Try again": "Intentar de nuevo", "Untitled chat": "Chat sin título", - "What can I help you with?": "¿En qué puedo ayudarte?" + "What can I help you with?": "¿En qué puedo ayudarte?", + "Are you sure you want to revoke this {{credential}}": "¿Está seguro de que desea revocar esta {{credential}}?", + "Automatically provision users and groups from your identity provider via SCIM.": "Aprovisione automáticamente usuarios y grupos desde su proveedor de identidad mediante SCIM.", + "Configure your identity provider with this URL to provision users and groups.": "Configure su proveedor de identidad con esta URL para aprovisionar usuarios y grupos.", + "Create {{credential}}": "Crear {{credential}}", + "{{credential}} created": "{{credential}} creada", + "{{credential}} created successfully": "{{credential}} creada con éxito", + "Created by": "Creado por", + "Custom": "Personalizado", + "Enable SCIM": "Habilitar SCIM", + "Enter a descriptive name": "Introduzca un nombre descriptivo", + "I've saved my {{credential}}": "He guardado mi {{credential}}", + "Important": "Importante", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "Asegúrese de copiar su {{credential}} ahora. ¡No podrá volver a verla!", + "Never": "Nunca", + "Revoke {{credential}}": "Revocar {{credential}}", + "SCIM endpoint URL": "URL del endpoint de SCIM", + "SCIM provisioning": "Aprovisionamiento SCIM", + "SCIM takes precedence over SSO group sync while enabled.": "SCIM tiene prioridad sobre la sincronización de grupos de SSO mientras esté habilitado.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "Ha alcanzado el máximo de {{max}} tokens SCIM. Elimine un token existente para crear uno nuevo.", + "SCIM token": "Token SCIM", + "SCIM tokens": "Tokens SCIM", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "Esta acción no se puede deshacer. Su proveedor de identidad dejará de sincronizarse inmediatamente.", + "Toggle SCIM provisioning": "Activar o desactivar el aprovisionamiento SCIM", + "Token": "Token", + "Page menu": "Menú de la página", + "Expand": "Expandir", + "Collapse": "Contraer", + "Comment menu": "Menú de comentarios", + "Group menu": "Menú del grupo", + "Show hidden breadcrumbs": "Mostrar rutas de navegación ocultas", + "Breadcrumbs": "Rutas de navegación", + "Page actions": "Acciones de la página", + "Pick emoji": "Elegir emoji", + "Template menu": "Menú de plantillas", + "Use": "Usar", + "Use template": "Usar plantilla", + "Preview template: {{title}}": "Vista previa de la plantilla: {{title}}", + "Use a template": "Usar una plantilla", + "Search templates...": "Buscar plantillas...", + "Search spaces...": "Buscar espacios...", + "No templates found": "No se encontraron plantillas", + "No spaces found": "No se encontraron espacios", + "Browse all templates": "Ver todas las plantillas", + "This space": "Este espacio", + "All templates": "Todas las plantillas", + "Global": "Global", + "New template": "Nueva plantilla", + "Edit template": "Editar plantilla", + "Are you sure you want to delete this template?": "¿Seguro que quieres eliminar esta plantilla?", + "Template scope updated": "Alcance de la plantilla actualizado", + "Choose which space this template belongs to": "Elige a qué espacio pertenece esta plantilla", + "Scope": "Alcance", + "Select scope": "Seleccionar alcance", + "Title": "Título", + "Saving...": "Guardando...", + "Saved": "Guardado", + "Save failed. Retry": "Error al guardar. Reintentar", + "By {{name}}": "Por {{name}}", + "Updated {{time}}": "Actualizado {{time}}", + "Choose destination": "Elegir destino", + "Search pages and spaces...": "Buscar páginas y espacios...", + "No results found": "No se encontraron resultados", + "You don't have permission to create pages here": "No tienes permiso para crear páginas aquí", + "Chat menu": "Menú del chat", + "API key menu": "Menú de la clave API", + "Jump to comment selection": "Ir a la selección de comentarios", + "Slash commands": "Comandos de barra", + "Mention suggestions": "Sugerencias de menciones", + "Link suggestions": "Sugerencias de enlaces", + "Diagram editor": "Editor de diagramas", + "Add comment": "Agregar comentario", + "Find and replace": "Buscar y reemplazar", + "Main navigation": "Navegación principal", + "Space navigation": "Navegación del espacio", + "Settings navigation": "Navegación de configuración", + "AI navigation": "Navegación de IA", + "Breadcrumb": "Ruta de navegación", + "Synced block": "Bloque sincronizado", + "Create a block that stays in sync across pages.": "Crea un bloque que se mantenga sincronizado entre páginas.", + "Editing original": "Editando original", + "Copy synced block": "Copiar bloque sincronizado", + "Unsync": "Desincronizar", + "Delete synced block": "Eliminar bloque sincronizado", + "Synced to {{count}} other page_one": "Sincronizado con {{count}} página más", + "Synced to {{count}} other page_other": "Sincronizado con {{count}} páginas más", + "ORIGINAL": "ORIGINAL", + "THIS PAGE": "ESTA PÁGINA", + "No pages": "No hay páginas", + "The original synced block no longer exists": "El bloque sincronizado original ya no existe", + "You don't have access to this synced block": "No tienes acceso a este bloque sincronizado", + "Failed to load this synced block": "No se pudo cargar este bloque sincronizado", + "Fixed editor toolbar": "Barra de herramientas fija del editor", + "Show a formatting toolbar above the editor with quick access to common actions.": "Muestra una barra de herramientas de formato sobre el editor con acceso rápido a acciones comunes.", + "Toggle fixed editor toolbar": "Alternar barra de herramientas fija del editor", + "Normal text": "Texto normal", + "More inline formatting": "Más formato en línea", + "Subscript": "Subíndice", + "Superscript": "Superíndice", + "Inline code": "Código en línea", + "Insert media": "Insertar contenido multimedia", + "Mention": "Mención", + "Emoji": "Emojis", + "Columns": "Columnas", + "More inserts": "Más inserciones", + "Embeds": "Integraciones", + "Diagrams": "Diagramas", + "Advanced": "Avanzado", + "Utility": "Utilidad", + "Decrease indent": "Disminuir sangría", + "Increase indent": "Aumentar sangría", + "Clear formatting": "Borrar formato", + "Code block": "Bloque de código", + "Experimental": "Experimental", + "Strikethrough": "Tachado", + "Undo": "Deshacer", + "Redo": "Rehacer", + "Backlinks": "Enlaces entrantes", + "Last updated by": "Última actualización por", + "Last updated": "Última actualización", + "Stats": "Estadísticas", + "Word count": "Recuento de palabras", + "Characters": "Caracteres", + "Incoming links": "Enlaces entrantes", + "Outgoing links": "Enlaces salientes", + "Incoming links ({{count}})": "Enlaces entrantes ({{count}})", + "Outgoing links ({{count}})": "Enlaces salientes ({{count}})", + "No pages link here yet.": "Todavía no hay páginas que enlacen aquí.", + "This page doesn't link to other pages yet.": "Esta página todavía no enlaza a otras páginas.", + "Verified until {{date}}": "Verificado hasta {{date}}", + "Labels": "Etiquetas", + "Add label": "Agregar etiqueta", + "No labels yet": "Todavía no hay etiquetas", + "Already added": "Ya agregado", + "Invalid label name": "Nombre de etiqueta no válido", + "No matches": "Sin coincidencias", + "Search or create…": "Buscar o crear…", + "Remove label {{name}}": "Eliminar etiqueta {{name}}", + "Failed to add label": "No se pudo agregar la etiqueta", + "Failed to remove label": "No se pudo eliminar la etiqueta", + "No pages with this label": "No hay páginas con esta etiqueta", + "Pages tagged with this label will appear here.": "Las páginas etiquetadas con esta etiqueta aparecerán aquí.", + "No pages match your search.": "Ninguna página coincide con tu búsqueda.", + "Updated {{date}}": "Actualizado el {{date}}", + "Cell actions": "Acciones de celda", + "Column actions": "Acciones de columna", + "Row actions": "Acciones de fila", + "Filter": "Filtrar", + "Page title": "Título de la página", + "Page content": "Contenido de la página", + "Member actions": "Acciones de miembro", + "Toggle password visibility": "Alternar visibilidad de la contraseña", + "Send comment": "Enviar comentario", + "Token actions": "Acciones de token", + "Template settings": "Configuración de la plantilla", + "Edit diagram": "Editar diagrama", + "Edit embed": "Editar contenido integrado", + "Edit drawing": "Editar dibujo", + "Delete equation": "Eliminar ecuación", + "Invite actions": "Acciones de invitación", + "Get started": "Comenzar", + "* indicates required fields": "* indica los campos obligatorios", + "List of spaces in this workspace": "Lista de espacios en este espacio de trabajo", + "Active sessions": "Sesiones activas", + "Add {{name}} to favorites": "Agregar {{name}} a favoritos", + "Remove {{name}} from favorites": "Quitar {{name}} de favoritos", + "Added to favorites": "Agregado a favoritos", + "Removed from favorites": "Quitado de favoritos", + "Added {{name}} to favorites": "Se agregó {{name}} a favoritos", + "Removed {{name}} from favorites": "Se quitó {{name}} de favoritos", + "Page menu for {{name}}": "Menú de página para {{name}}", + "Create subpage of {{name}}": "Crear subpágina de {{name}}" } diff --git a/apps/client/public/locales/fr-FR/translation.json b/apps/client/public/locales/fr-FR/translation.json index 90172476a..20f354421 100644 --- a/apps/client/public/locales/fr-FR/translation.json +++ b/apps/client/public/locales/fr-FR/translation.json @@ -71,6 +71,7 @@ "Export": "Exporter", "Failed to create page": "Échec de la création de la page", "Failed to delete page": "Échec de la suppression de la page", + "Failed to restore page": "Échec de la restauration de la page", "Failed to fetch recent pages": "Échec de la récupération des pages récentes", "Failed to import pages": "Échec de l'importation des pages", "Failed to load page. An error occurred.": "Échec du chargement de la page. Une erreur s'est produite.", @@ -276,6 +277,9 @@ "Align left": "Aligner à gauche", "Align right": "Aligner à droite", "Align center": "Aligner au centre", + "Alt text": "Texte alternatif", + "Describe this for accessibility.": "Décrivez ceci pour l’accessibilité.", + "Add a description": "Ajouter une description", "Justify": "Justifier", "Merge cells": "Fusionner les cellules", "Split cell": "Diviser la cellule", @@ -286,6 +290,19 @@ "Add row above": "Ajouter une ligne au-dessus", "Add row below": "Ajouter une ligne en dessous", "Delete table": "Supprimer le tableau", + "Add column left": "Ajouter une colonne à gauche", + "Add column right": "Ajouter une colonne à droite", + "Clear cell": "Effacer la cellule", + "Clear cells": "Effacer les cellules", + "Toggle header cell": "Activer/désactiver la cellule d’en-tête", + "Toggle header column": "Activer/désactiver la colonne d’en-tête", + "Toggle header row": "Activer/désactiver la ligne d’en-tête", + "Move column left": "Déplacer la colonne vers la gauche", + "Move column right": "Déplacer la colonne vers la droite", + "Move row down": "Déplacer la ligne vers le bas", + "Move row up": "Déplacer la ligne vers le haut", + "Sort A → Z": "Trier de A à Z", + "Sort Z → A": "Trier de Z à A", "Info": "Info", "Note": "Remarque", "Success": "Succès", @@ -348,6 +365,8 @@ "Create block quote.": "Créez un bloc de citation.", "Insert code snippet.": "Insérez un extrait de code.", "Insert horizontal rule divider": "Insérer un séparateur de règle horizontale", + "Page break": "Saut de page", + "Insert a page break for printing.": "Insérer un saut de page pour l’impression.", "Upload any image from your device.": "Téléchargez n'importe quelle image depuis votre appareil.", "Upload any video from your device.": "Téléchargez n'importe quelle vidéo depuis votre appareil.", "Upload any audio from your device.": "Téléchargez n'importe quel fichier audio depuis votre appareil.", @@ -392,6 +411,10 @@ "Write...": "Écrire...", "Column count": "Nombre de colonnes", "{{count}} Columns": "{count, plural, one {# colonne} other {# colonnes}}", + "{{count}} command available_one": "1 commande disponible", + "{{count}} command available_other": "{{count}} commandes disponibles", + "{{count}} result available_one": "1 résultat disponible", + "{{count}} result available_other": "{{count}} résultats disponibles", "Equal columns": "Colonnes égales", "Left sidebar": "Barre latérale gauche", "Right sidebar": "Barre latérale droite", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} est disponible", "Default page edit mode": "Mode d’édition par défaut de la page", "Choose your preferred page edit mode. Avoid accidental edits.": "Choisissez votre mode d'édition de page préféré. Évitez les modifications accidentelles.", + "Choose {{format}} file": "Choisir un fichier {{format}}", "Reading": "Lecture", "Delete member": "Supprimer le membre", "Member deleted successfully": "Membre supprimé avec succès", @@ -565,6 +589,8 @@ "Move to trash": "Déplacer vers la corbeille", "Move this page to trash?": "Déplacer cette page vers la corbeille ?", "Restore page": "Restaurer la page", + "Permanently delete": "Supprimer définitivement", + "{{name}} moved this page to Trash {{time}}.": "{{name}} a déplacé cette page vers la corbeille {{time}}.", "Page moved to trash": "Page déplacée vers la corbeille", "Page restored successfully": "Page restaurée avec succès", "Deleted by": "Supprimé par", @@ -608,25 +634,21 @@ "Image exceeds 10MB limit.": "L'image dépasse la limite de 10 Mo.", "Image removed successfully": "Image supprimée avec succès", "API key": "Clé API", - "API key created successfully": "Clé API créée avec succès", "API keys": "Clés API", "API management": "Gestion des API", - "Are you sure you want to revoke this API key": "Êtes-vous sûr de vouloir révoquer cette clé API", - "Create API Key": "Créer une clé API", "Custom expiration date": "Date d'expiration personnalisée", "Enter a descriptive token name": "Entrez un nom descriptif pour le jeton", "Expiration": "Expiration", "Expired": "Expiré(e)", "Expires": "Expire", - "I've saved my API key": "J'ai enregistré ma clé API", "Last use": "Dernière utilisation", "No API keys found": "Aucune clé API trouvée", "No expiration": "Pas d'expiration", - "Revoke API key": "Révoquer la clé API", "Revoked successfully": "Révoqué(e) avec succès", "Select expiration date": "Sélectionnez la date d'expiration", "This action cannot be undone. Any applications using this API key will stop working.": "Cette action ne peut pas être annulée. Toutes les applications utilisant cette clé API cesseront de fonctionner.", - "Update API key": "Mettre à jour la clé API", + "Update": "Mettre à jour", + "Update {{credential}}": "Mettre à jour {{credential}}", "Manage API keys for all users in the workspace": "Gérer les clés API pour tous les utilisateurs dans l'espace de travail", "Restrict API key creation to admins": "Restreindre la création de clés API aux administrateurs", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Seuls les administrateurs et les propriétaires peuvent créer de nouvelles clés API. Les clés des membres existants continueront de fonctionner.", @@ -858,9 +880,12 @@ "AI Chat": "Chat IA", "Analyze for insights": "Analyser pour obtenir des informations", "Ask anything...": "Posez n’importe quelle question...", + "Assistant said:": "L’assistant a dit :", "Chat history": "Historique des discussions", "Chat name": "Nom de la discussion", + "Chat transcript": "Transcription du chat", "Close": "Fermer", + "Copy assistant response": "Copier la réponse de l’assistant", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "Échec du chargement de la discussion. Une erreur s’est produite.", "Failed to render this message.": "Échec de l’affichage de ce message.", @@ -870,9 +895,17 @@ "No chats found": "Aucune discussion trouvée", "No conversations yet": "Aucune conversation pour le moment", "Open full page": "Ouvrir la page complète", + "Scroll to bottom": "Faire défiler jusqu’en bas", + "You said:": "Vous avez dit :", "Previous 7 days": "7 derniers jours", "Previous 30 days": "30 derniers jours", "Search chats...": "Rechercher des discussions...", + "Search chats": "Rechercher des discussions", + "Ask anything... Use @ to mention pages": "Demandez n’importe quoi… Utilisez @ pour mentionner des pages", + "Ask anything or search your workspace": "Posez n’importe quelle question ou recherchez dans votre espace de travail", + "Welcome to {{name}}": "Bienvenue sur {{name}}", + "Add files": "Ajouter des fichiers", + "Mention a page": "Mentionner une page", "Start a new chat to see it here.": "Commencez une nouvelle discussion pour la voir ici.", "Summarize this page": "Résumer cette page", "Toggle AI Chat": "Basculer le chat IA", @@ -880,5 +913,176 @@ "Try a different search term.": "Essayez un autre terme de recherche.", "Try again": "Réessayer", "Untitled chat": "Discussion sans titre", - "What can I help you with?": "Que puis-je faire pour vous aider ?" + "What can I help you with?": "Que puis-je faire pour vous aider ?", + "Are you sure you want to revoke this {{credential}}": "Êtes-vous sûr de vouloir révoquer ce/cette {{credential}}", + "Automatically provision users and groups from your identity provider via SCIM.": "Provisionnez automatiquement les utilisateurs et les groupes depuis votre fournisseur d’identité via SCIM.", + "Configure your identity provider with this URL to provision users and groups.": "Configurez votre fournisseur d’identité avec cette URL pour provisionner les utilisateurs et les groupes.", + "Create {{credential}}": "Créer {{credential}}", + "{{credential}} created": "{{credential}} créé", + "{{credential}} created successfully": "{{credential}} créé avec succès", + "Created by": "Créé par", + "Custom": "Personnalisé", + "Enable SCIM": "Activer SCIM", + "Enter a descriptive name": "Saisissez un nom descriptif", + "I've saved my {{credential}}": "J’ai enregistré mon/ma {{credential}}", + "Important": "Important", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "Assurez-vous de copier votre {{credential}} maintenant. Vous ne pourrez plus le/la voir ensuite !", + "Never": "Jamais", + "Revoke {{credential}}": "Révoquer {{credential}}", + "SCIM endpoint URL": "URL du point de terminaison SCIM", + "SCIM provisioning": "Provisionnement SCIM", + "SCIM takes precedence over SSO group sync while enabled.": "SCIM a priorité sur la synchronisation des groupes SSO lorsqu’il est activé.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "Vous avez atteint le maximum de {{max}} jetons SCIM. Supprimez un jeton existant pour en créer un nouveau.", + "SCIM token": "Jeton SCIM", + "SCIM tokens": "Jetons SCIM", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "Cette action est irréversible. Votre fournisseur d’identité cessera immédiatement la synchronisation.", + "Toggle SCIM provisioning": "Activer/désactiver le provisionnement SCIM", + "Token": "Jeton", + "Page menu": "Menu de la page", + "Expand": "Développer", + "Collapse": "Réduire", + "Comment menu": "Menu du commentaire", + "Group menu": "Menu du groupe", + "Show hidden breadcrumbs": "Afficher les fils d’Ariane masqués", + "Breadcrumbs": "Fils d’Ariane", + "Page actions": "Actions de la page", + "Pick emoji": "Choisir un emoji", + "Template menu": "Menu du modèle", + "Use": "Utiliser", + "Use template": "Utiliser le modèle", + "Preview template: {{title}}": "Aperçu du modèle : {{title}}", + "Use a template": "Utiliser un modèle", + "Search templates...": "Rechercher des modèles...", + "Search spaces...": "Rechercher des espaces...", + "No templates found": "Aucun modèle trouvé", + "No spaces found": "Aucun espace trouvé", + "Browse all templates": "Parcourir tous les modèles", + "This space": "Cet espace", + "All templates": "Tous les modèles", + "Global": "Global", + "New template": "Nouveau modèle", + "Edit template": "Modifier le modèle", + "Are you sure you want to delete this template?": "Êtes-vous sûr de vouloir supprimer ce modèle ?", + "Template scope updated": "Portée du modèle mise à jour", + "Choose which space this template belongs to": "Choisissez à quel espace appartient ce modèle", + "Scope": "Portée", + "Select scope": "Sélectionner la portée", + "Title": "Titre", + "Saving...": "Enregistrement...", + "Saved": "Enregistré", + "Save failed. Retry": "Échec de l’enregistrement. Réessayer", + "By {{name}}": "Par {{name}}", + "Updated {{time}}": "Mis à jour {{time}}", + "Choose destination": "Choisir la destination", + "Search pages and spaces...": "Rechercher des pages et des espaces...", + "No results found": "Aucun résultat trouvé", + "You don't have permission to create pages here": "Vous n’avez pas l’autorisation de créer des pages ici", + "Chat menu": "Menu du chat", + "API key menu": "Menu de la clé API", + "Jump to comment selection": "Aller à la sélection de commentaires", + "Slash commands": "Commandes slash", + "Mention suggestions": "Suggestions de mention", + "Link suggestions": "Suggestions de liens", + "Diagram editor": "Éditeur de diagrammes", + "Add comment": "Ajouter un commentaire", + "Find and replace": "Rechercher et remplacer", + "Main navigation": "Navigation principale", + "Space navigation": "Navigation de l’espace", + "Settings navigation": "Navigation des paramètres", + "AI navigation": "Navigation IA", + "Breadcrumb": "Fil d’Ariane", + "Synced block": "Bloc synchronisé", + "Create a block that stays in sync across pages.": "Créez un bloc qui reste synchronisé entre les pages.", + "Editing original": "Modification de l’original", + "Copy synced block": "Copier le bloc synchronisé", + "Unsync": "Désynchroniser", + "Delete synced block": "Supprimer le bloc synchronisé", + "Synced to {{count}} other page_one": "Synchronisée avec {{count}} autre page", + "Synced to {{count}} other page_other": "Synchronisée avec {{count}} autres pages", + "ORIGINAL": "ORIGINAL", + "THIS PAGE": "CETTE PAGE", + "No pages": "Aucune page", + "The original synced block no longer exists": "Le bloc synchronisé d’origine n’existe plus", + "You don't have access to this synced block": "Vous n’avez pas accès à ce bloc synchronisé", + "Failed to load this synced block": "Échec du chargement de ce bloc synchronisé", + "Fixed editor toolbar": "Barre d’outils de l’éditeur fixe", + "Show a formatting toolbar above the editor with quick access to common actions.": "Afficher une barre d’outils de mise en forme au-dessus de l’éditeur avec un accès rapide aux actions courantes.", + "Toggle fixed editor toolbar": "Activer/désactiver la barre d’outils de l’éditeur fixe", + "Normal text": "Texte normal", + "More inline formatting": "Plus de mise en forme en ligne", + "Subscript": "Indice", + "Superscript": "Exposant", + "Inline code": "Code en ligne", + "Insert media": "Insérer un média", + "Mention": "Mention", + "Emoji": "Emoji", + "Columns": "Colonnes", + "More inserts": "Plus d’insertions", + "Embeds": "Intégrations", + "Diagrams": "Diagrammes", + "Advanced": "Avancé", + "Utility": "Utilitaire", + "Decrease indent": "Réduire le retrait", + "Increase indent": "Augmenter le retrait", + "Clear formatting": "Effacer la mise en forme", + "Code block": "Bloc de code", + "Experimental": "Expérimental", + "Strikethrough": "Barré", + "Undo": "Annuler", + "Redo": "Rétablir", + "Backlinks": "Liens retour", + "Last updated by": "Dernière mise à jour par", + "Last updated": "Dernière mise à jour", + "Stats": "Statistiques", + "Word count": "Nombre de mots", + "Characters": "Caractères", + "Incoming links": "Liens entrants", + "Outgoing links": "Liens sortants", + "Incoming links ({{count}})": "Liens entrants ({{count}})", + "Outgoing links ({{count}})": "Liens sortants ({{count}})", + "No pages link here yet.": "Aucune page ne pointe encore ici.", + "This page doesn't link to other pages yet.": "Cette page ne renvoie pas encore vers d’autres pages.", + "Verified until {{date}}": "Vérifié jusqu’au {{date}}", + "Labels": "Étiquettes", + "Add label": "Ajouter une étiquette", + "No labels yet": "Aucune étiquette pour l’instant", + "Already added": "Déjà ajouté", + "Invalid label name": "Nom d’étiquette invalide", + "No matches": "Aucune correspondance", + "Search or create…": "Rechercher ou créer…", + "Remove label {{name}}": "Supprimer l’étiquette {{name}}", + "Failed to add label": "Échec de l’ajout de l’étiquette", + "Failed to remove label": "Échec de la suppression de l’étiquette", + "No pages with this label": "Aucune page avec cette étiquette", + "Pages tagged with this label will appear here.": "Les pages portant cette étiquette apparaîtront ici.", + "No pages match your search.": "Aucune page ne correspond à votre recherche.", + "Updated {{date}}": "Mis à jour le {{date}}", + "Cell actions": "Actions de cellule", + "Column actions": "Actions de colonne", + "Row actions": "Actions de ligne", + "Filter": "Filtrer", + "Page title": "Titre de la page", + "Page content": "Contenu de la page", + "Member actions": "Actions des membres", + "Toggle password visibility": "Afficher/masquer le mot de passe", + "Send comment": "Envoyer le commentaire", + "Token actions": "Actions du jeton", + "Template settings": "Paramètres du modèle", + "Edit diagram": "Modifier le diagramme", + "Edit embed": "Modifier l’intégration", + "Edit drawing": "Modifier le dessin", + "Delete equation": "Supprimer l’équation", + "Invite actions": "Actions d’invitation", + "Get started": "Commencer", + "* indicates required fields": "* indique les champs obligatoires", + "List of spaces in this workspace": "Liste des espaces de cet espace de travail", + "Active sessions": "Sessions actives", + "Add {{name}} to favorites": "Ajouter {{name}} aux favoris", + "Remove {{name}} from favorites": "Retirer {{name}} des favoris", + "Added to favorites": "Ajouté aux favoris", + "Removed from favorites": "Retiré des favoris", + "Added {{name}} to favorites": "{{name}} a été ajouté aux favoris", + "Removed {{name}} from favorites": "{{name}} a été retiré des favoris", + "Page menu for {{name}}": "Menu de la page pour {{name}}", + "Create subpage of {{name}}": "Créer une sous-page de {{name}}" } diff --git a/apps/client/public/locales/it-IT/translation.json b/apps/client/public/locales/it-IT/translation.json index 9ceec008a..441c90f69 100644 --- a/apps/client/public/locales/it-IT/translation.json +++ b/apps/client/public/locales/it-IT/translation.json @@ -71,6 +71,7 @@ "Export": "Esporta", "Failed to create page": "Impossibile creare la pagina", "Failed to delete page": "Impossibile eliminare la pagina", + "Failed to restore page": "Impossibile ripristinare la pagina", "Failed to fetch recent pages": "Impossibile recuperare le pagine recenti", "Failed to import pages": "Impossibile importare le pagine", "Failed to load page. An error occurred.": "Il caricamento della pagina è fallito. Si è verificato un errore.", @@ -276,6 +277,9 @@ "Align left": "Allinea a sinistra", "Align right": "Allinea a destra", "Align center": "Allinea al centro", + "Alt text": "Testo alternativo", + "Describe this for accessibility.": "Descrivi questo contenuto per l'accessibilità.", + "Add a description": "Aggiungi una descrizione", "Justify": "Giustifica", "Merge cells": "Unisci celle", "Split cell": "Dividi cella", @@ -286,6 +290,19 @@ "Add row above": "Aggiungi riga sopra", "Add row below": "Aggiungi riga sotto", "Delete table": "Elimina tabella", + "Add column left": "Aggiungi colonna a sinistra", + "Add column right": "Aggiungi colonna a destra", + "Clear cell": "Cancella cella", + "Clear cells": "Cancella celle", + "Toggle header cell": "Attiva/disattiva cella di intestazione", + "Toggle header column": "Attiva/disattiva colonna di intestazione", + "Toggle header row": "Attiva/disattiva riga di intestazione", + "Move column left": "Sposta colonna a sinistra", + "Move column right": "Sposta colonna a destra", + "Move row down": "Sposta riga in basso", + "Move row up": "Sposta riga in alto", + "Sort A → Z": "Ordina A → Z", + "Sort Z → A": "Ordina Z → A", "Info": "Informazioni", "Note": "Nota", "Success": "Successo", @@ -348,6 +365,8 @@ "Create block quote.": "Crea blocco citazione.", "Insert code snippet.": "Inserisci frammento di codice.", "Insert horizontal rule divider": "Inserisci divisore di regola orizzontale", + "Page break": "Interruzione di pagina", + "Insert a page break for printing.": "Inserisci un'interruzione di pagina per la stampa.", "Upload any image from your device.": "Carica un'immagine dal tuo dispositivo.", "Upload any video from your device.": "Carica qualsiasi video dal tuo dispositivo.", "Upload any audio from your device.": "Carica qualsiasi audio dal tuo dispositivo.", @@ -392,6 +411,10 @@ "Write...": "Scrivi...", "Column count": "Numero di colonne", "{{count}} Columns": "{{count}} colonne", + "{{count}} command available_one": "1 comando disponibile", + "{{count}} command available_other": "{{count}} comandi disponibili", + "{{count}} result available_one": "1 risultato disponibile", + "{{count}} result available_other": "{{count}} risultati disponibili", "Equal columns": "Colonne uguali", "Left sidebar": "Barra laterale sinistra", "Right sidebar": "Barra laterale destra", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} è disponibile", "Default page edit mode": "Modalità di modifica predefinita della pagina", "Choose your preferred page edit mode. Avoid accidental edits.": "Scegli la tua modalità di modifica della pagina preferita. Evita modifiche accidentali.", + "Choose {{format}} file": "Scegli file {{format}}", "Reading": "Lettura", "Delete member": "Elimina membro", "Member deleted successfully": "Membro eliminato con successo", @@ -565,6 +589,8 @@ "Move to trash": "Sposta nel cestino", "Move this page to trash?": "Spostare questa pagina nel cestino?", "Restore page": "Ripristina pagina", + "Permanently delete": "Elimina definitivamente", + "{{name}} moved this page to Trash {{time}}.": "{{name}} ha spostato questa pagina nel Cestino {{time}}.", "Page moved to trash": "Pagina spostata nel cestino", "Page restored successfully": "Pagina ripristinata con successo", "Deleted by": "Eliminato da", @@ -608,25 +634,21 @@ "Image exceeds 10MB limit.": "L'immagine supera il limite di 10MB.", "Image removed successfully": "Immagine rimossa con successo", "API key": "Chiave API", - "API key created successfully": "Chiave API creata con successo", "API keys": "Chiavi API", "API management": "Gestione API", - "Are you sure you want to revoke this API key": "Sei sicuro di voler revocare questa chiave API", - "Create API Key": "Crea Chiave API", "Custom expiration date": "Data di scadenza personalizzata", "Enter a descriptive token name": "Inserisci un nome descrittivo del token", "Expiration": "Scadenza", "Expired": "Scaduto", "Expires": "Scade", - "I've saved my API key": "Ho salvato la mia chiave API", "Last use": "Ultimo utilizzo", "No API keys found": "Nessuna chiave API trovata", "No expiration": "Nessuna scadenza", - "Revoke API key": "Revoca chiave API", "Revoked successfully": "Revocata con successo", "Select expiration date": "Seleziona la data di scadenza", "This action cannot be undone. Any applications using this API key will stop working.": "Questa azione non può essere annullata. Qualsiasi applicazione che utilizza questa chiave API smetterà di funzionare.", - "Update API key": "Aggiorna chiave API", + "Update": "Aggiorna", + "Update {{credential}}": "Aggiorna {{credential}}", "Manage API keys for all users in the workspace": "Gestisci le chiavi API per tutti gli utenti nell'area di lavoro", "Restrict API key creation to admins": "Limita la creazione delle chiavi API agli amministratori", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Solo gli amministratori e i proprietari possono creare nuove chiavi API. Le chiavi dei membri esistenti continueranno a funzionare.", @@ -858,9 +880,12 @@ "AI Chat": "Chat IA", "Analyze for insights": "Analizza per ottenere approfondimenti", "Ask anything...": "Chiedi qualsiasi cosa...", + "Assistant said:": "L'assistente ha detto:", "Chat history": "Cronologia chat", "Chat name": "Nome chat", + "Chat transcript": "Trascrizione della chat", "Close": "Chiudi", + "Copy assistant response": "Copia risposta dell'assistente", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "Caricamento della chat non riuscito. Si è verificato un errore.", "Failed to render this message.": "Impossibile visualizzare questo messaggio.", @@ -870,9 +895,17 @@ "No chats found": "Nessuna chat trovata", "No conversations yet": "Nessuna conversazione al momento", "Open full page": "Apri pagina completa", + "Scroll to bottom": "Scorri in basso", + "You said:": "Hai detto:", "Previous 7 days": "Ultimi 7 giorni", "Previous 30 days": "Ultimi 30 giorni", "Search chats...": "Cerca nelle chat...", + "Search chats": "Cerca nelle chat", + "Ask anything... Use @ to mention pages": "Chiedi qualsiasi cosa... Usa @ per menzionare le pagine", + "Ask anything or search your workspace": "Chiedi qualsiasi cosa o cerca nel tuo spazio di lavoro", + "Welcome to {{name}}": "Benvenuto in {{name}}", + "Add files": "Aggiungi file", + "Mention a page": "Menziona una pagina", "Start a new chat to see it here.": "Avvia una nuova chat per vederla qui.", "Summarize this page": "Riassumi questa pagina", "Toggle AI Chat": "Attiva/disattiva Chat IA", @@ -880,5 +913,176 @@ "Try a different search term.": "Prova un termine di ricerca diverso.", "Try again": "Riprova", "Untitled chat": "Chat senza titolo", - "What can I help you with?": "Con cosa posso aiutarti?" + "What can I help you with?": "Con cosa posso aiutarti?", + "Are you sure you want to revoke this {{credential}}": "Sei sicuro di voler revocare questa {{credential}}", + "Automatically provision users and groups from your identity provider via SCIM.": "Esegui automaticamente il provisioning di utenti e gruppi dal tuo provider di identità tramite SCIM.", + "Configure your identity provider with this URL to provision users and groups.": "Configura il tuo provider di identità con questo URL per eseguire il provisioning di utenti e gruppi.", + "Create {{credential}}": "Crea {{credential}}", + "{{credential}} created": "{{credential}} creata", + "{{credential}} created successfully": "{{credential}} creata con successo", + "Created by": "Creata da", + "Custom": "Personalizzato", + "Enable SCIM": "Abilita SCIM", + "Enter a descriptive name": "Inserisci un nome descrittivo", + "I've saved my {{credential}}": "Ho salvato la mia {{credential}}", + "Important": "Importante", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "Assicurati di copiare subito la tua {{credential}}. Non potrai più visualizzarla!", + "Never": "Mai", + "Revoke {{credential}}": "Revoca {{credential}}", + "SCIM endpoint URL": "URL dell'endpoint SCIM", + "SCIM provisioning": "Provisioning SCIM", + "SCIM takes precedence over SSO group sync while enabled.": "SCIM ha la precedenza sulla sincronizzazione dei gruppi SSO quando è abilitato.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "Hai raggiunto il numero massimo di {{max}} token SCIM. Elimina un token esistente per crearne uno nuovo.", + "SCIM token": "Token SCIM", + "SCIM tokens": "Token SCIM", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "Questa azione non può essere annullata. Il tuo provider di identità smetterà di sincronizzarsi immediatamente.", + "Toggle SCIM provisioning": "Attiva/disattiva il provisioning SCIM", + "Token": "Token", + "Page menu": "Menu della pagina", + "Expand": "Espandi", + "Collapse": "Comprimi", + "Comment menu": "Menu dei commenti", + "Group menu": "Menu del gruppo", + "Show hidden breadcrumbs": "Mostra breadcrumb nascosti", + "Breadcrumbs": "Breadcrumb", + "Page actions": "Azioni della pagina", + "Pick emoji": "Scegli emoji", + "Template menu": "Menu del modello", + "Use": "Usa", + "Use template": "Usa modello", + "Preview template: {{title}}": "Anteprima modello: {{title}}", + "Use a template": "Usa un modello", + "Search templates...": "Cerca modelli...", + "Search spaces...": "Cerca spazi...", + "No templates found": "Nessun modello trovato", + "No spaces found": "Nessuno spazio trovato", + "Browse all templates": "Sfoglia tutti i modelli", + "This space": "Questo spazio", + "All templates": "Tutti i modelli", + "Global": "Globale", + "New template": "Nuovo modello", + "Edit template": "Modifica modello", + "Are you sure you want to delete this template?": "Sei sicuro di voler eliminare questo modello?", + "Template scope updated": "Ambito del modello aggiornato", + "Choose which space this template belongs to": "Scegli a quale spazio appartiene questo modello", + "Scope": "Ambito", + "Select scope": "Seleziona ambito", + "Title": "Titolo", + "Saving...": "Salvataggio...", + "Saved": "Salvato", + "Save failed. Retry": "Salvataggio non riuscito. Riprova", + "By {{name}}": "Di {{name}}", + "Updated {{time}}": "Aggiornato {{time}}", + "Choose destination": "Scegli destinazione", + "Search pages and spaces...": "Cerca pagine e spazi...", + "No results found": "Nessun risultato trovato", + "You don't have permission to create pages here": "Non hai l'autorizzazione per creare pagine qui", + "Chat menu": "Menu della chat", + "API key menu": "Menu della chiave API", + "Jump to comment selection": "Vai alla selezione dei commenti", + "Slash commands": "Comandi slash", + "Mention suggestions": "Suggerimenti di menzione", + "Link suggestions": "Suggerimenti di link", + "Diagram editor": "Editor di diagrammi", + "Add comment": "Aggiungi commento", + "Find and replace": "Trova e sostituisci", + "Main navigation": "Navigazione principale", + "Space navigation": "Navigazione dello spazio", + "Settings navigation": "Navigazione delle impostazioni", + "AI navigation": "Navigazione AI", + "Breadcrumb": "Percorso di navigazione", + "Synced block": "Blocco sincronizzato", + "Create a block that stays in sync across pages.": "Crea un blocco che rimanga sincronizzato tra le pagine.", + "Editing original": "Modifica originale", + "Copy synced block": "Copia blocco sincronizzato", + "Unsync": "Annulla sincronizzazione", + "Delete synced block": "Elimina blocco sincronizzato", + "Synced to {{count}} other page_one": "Sincronizzato con {{count}} altra pagina", + "Synced to {{count}} other page_other": "Sincronizzato con {{count}} altre pagine", + "ORIGINAL": "ORIGINALE", + "THIS PAGE": "QUESTA PAGINA", + "No pages": "Nessuna pagina", + "The original synced block no longer exists": "Il blocco sincronizzato originale non esiste più", + "You don't have access to this synced block": "Non hai accesso a questo blocco sincronizzato", + "Failed to load this synced block": "Impossibile caricare questo blocco sincronizzato", + "Fixed editor toolbar": "Barra degli strumenti dell'editor fissa", + "Show a formatting toolbar above the editor with quick access to common actions.": "Mostra una barra degli strumenti di formattazione sopra l'editor con accesso rapido alle azioni comuni.", + "Toggle fixed editor toolbar": "Attiva/disattiva barra degli strumenti dell'editor fissa", + "Normal text": "Testo normale", + "More inline formatting": "Altra formattazione in linea", + "Subscript": "Pedice", + "Superscript": "Apice", + "Inline code": "Codice in linea", + "Insert media": "Inserisci contenuti multimediali", + "Mention": "Menzione", + "Emoji": "Emoji", + "Columns": "Colonne", + "More inserts": "Altri inserimenti", + "Embeds": "Incorporamenti", + "Diagrams": "Diagrammi", + "Advanced": "Avanzate", + "Utility": "Utilità", + "Decrease indent": "Riduci rientro", + "Increase indent": "Aumenta rientro", + "Clear formatting": "Cancella formattazione", + "Code block": "Blocco di codice", + "Experimental": "Sperimentale", + "Strikethrough": "Barrato", + "Undo": "Annulla", + "Redo": "Ripeti", + "Backlinks": "Backlink", + "Last updated by": "Ultimo aggiornamento di", + "Last updated": "Ultimo aggiornamento", + "Stats": "Statistiche", + "Word count": "Conteggio parole", + "Characters": "Caratteri", + "Incoming links": "Link in entrata", + "Outgoing links": "Link in uscita", + "Incoming links ({{count}})": "Link in entrata ({{count}})", + "Outgoing links ({{count}})": "Link in uscita ({{count}})", + "No pages link here yet.": "Nessuna pagina rimanda ancora qui.", + "This page doesn't link to other pages yet.": "Questa pagina non rimanda ancora ad altre pagine.", + "Verified until {{date}}": "Verificato fino al {{date}}", + "Labels": "Etichette", + "Add label": "Aggiungi etichetta", + "No labels yet": "Nessuna etichetta per ora", + "Already added": "Già aggiunto", + "Invalid label name": "Nome etichetta non valido", + "No matches": "Nessuna corrispondenza", + "Search or create…": "Cerca o crea…", + "Remove label {{name}}": "Rimuovi etichetta {{name}}", + "Failed to add label": "Impossibile aggiungere l'etichetta", + "Failed to remove label": "Impossibile rimuovere l'etichetta", + "No pages with this label": "Nessuna pagina con questa etichetta", + "Pages tagged with this label will appear here.": "Le pagine contrassegnate con questa etichetta appariranno qui.", + "No pages match your search.": "Nessuna pagina corrisponde alla tua ricerca.", + "Updated {{date}}": "Aggiornato il {{date}}", + "Cell actions": "Azioni cella", + "Column actions": "Azioni colonna", + "Row actions": "Azioni riga", + "Filter": "Filtro", + "Page title": "Titolo pagina", + "Page content": "Contenuto della pagina", + "Member actions": "Azioni membro", + "Toggle password visibility": "Attiva/disattiva visibilità password", + "Send comment": "Invia commento", + "Token actions": "Azioni token", + "Template settings": "Impostazioni modello", + "Edit diagram": "Modifica diagramma", + "Edit embed": "Modifica incorporamento", + "Edit drawing": "Modifica disegno", + "Delete equation": "Elimina equazione", + "Invite actions": "Azioni invito", + "Get started": "Inizia", + "* indicates required fields": "* indica i campi obbligatori", + "List of spaces in this workspace": "Elenco degli spazi in questo spazio di lavoro", + "Active sessions": "Sessioni attive", + "Add {{name}} to favorites": "Aggiungi {{name}} ai preferiti", + "Remove {{name}} from favorites": "Rimuovi {{name}} dai preferiti", + "Added to favorites": "Aggiunto ai preferiti", + "Removed from favorites": "Rimosso dai preferiti", + "Added {{name}} to favorites": "{{name}} aggiunto ai preferiti", + "Removed {{name}} from favorites": "{{name}} rimosso dai preferiti", + "Page menu for {{name}}": "Menu della pagina per {{name}}", + "Create subpage of {{name}}": "Crea sottopagina di {{name}}" } diff --git a/apps/client/public/locales/ja-JP/translation.json b/apps/client/public/locales/ja-JP/translation.json index 14285e238..492868cf2 100644 --- a/apps/client/public/locales/ja-JP/translation.json +++ b/apps/client/public/locales/ja-JP/translation.json @@ -71,6 +71,7 @@ "Export": "エクスポート", "Failed to create page": "ページの作成に失敗しました", "Failed to delete page": "ページの削除に失敗しました", + "Failed to restore page": "ページの復元に失敗しました", "Failed to fetch recent pages": "最近のページを取得できませんでした", "Failed to import pages": "ページのインポートに失敗しました", "Failed to load page. An error occurred.": "ページの読み込みに失敗しました。エラーが発生しました。", @@ -276,6 +277,9 @@ "Align left": "左揃え", "Align right": "右揃え", "Align center": "中央揃え", + "Alt text": "代替テキスト", + "Describe this for accessibility.": "アクセシビリティのために説明を追加してください。", + "Add a description": "説明を追加", "Justify": "両端揃え", "Merge cells": "セルを結合", "Split cell": "セルを分割", @@ -286,6 +290,19 @@ "Add row above": "上に行を追加", "Add row below": "下に行を追加", "Delete table": "テーブルを削除", + "Add column left": "左に列を追加", + "Add column right": "右に列を追加", + "Clear cell": "セルをクリア", + "Clear cells": "セルをクリア", + "Toggle header cell": "ヘッダーセルを切り替え", + "Toggle header column": "ヘッダー列を切り替え", + "Toggle header row": "ヘッダー行を切り替え", + "Move column left": "列を左に移動", + "Move column right": "列を右に移動", + "Move row down": "行を下に移動", + "Move row up": "行を上に移動", + "Sort A → Z": "A → Z で並べ替え", + "Sort Z → A": "Z → A で並べ替え", "Info": "情報", "Note": "ノート", "Success": "成功", @@ -348,6 +365,8 @@ "Create block quote.": "引用ブロックを作成します", "Insert code snippet.": "コードスニペットを挿入します", "Insert horizontal rule divider": "区切り線を挿入します", + "Page break": "改ページ", + "Insert a page break for printing.": "印刷用に改ページを挿入します。", "Upload any image from your device.": "デバイスから画像をアップロードします", "Upload any video from your device.": "デバイスから動画をアップロードします", "Upload any audio from your device.": "デバイスから音声ファイルをアップロードします。", @@ -392,6 +411,10 @@ "Write...": "ここに入力...", "Column count": "列数", "{{count}} Columns": "{{count}}列", + "{{count}} command available_one": "利用可能なコマンドが1件あります", + "{{count}} command available_other": "利用可能なコマンドが{{count}}件あります", + "{{count}} result available_one": "結果が1件あります", + "{{count}} result available_other": "結果が{{count}}件あります", "Equal columns": "均等な列", "Left sidebar": "左サイドバー", "Right sidebar": "右サイドバー", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} が利用可能です", "Default page edit mode": "デフォルトのページ編集モード", "Choose your preferred page edit mode. Avoid accidental edits.": "お好みのページ編集モードを選択してください(誤編集を防止します)", + "Choose {{format}} file": "{{format}} ファイルを選択", "Reading": "閲覧", "Delete member": "メンバーを削除", "Member deleted successfully": "メンバーが正常に削除されました", @@ -565,6 +589,8 @@ "Move to trash": "ゴミ箱に移動", "Move this page to trash?": "このページをごみ箱に移動しますか?", "Restore page": "ページを復元", + "Permanently delete": "完全に削除", + "{{name}} moved this page to Trash {{time}}.": "{{name}} が {{time}} にこのページをゴミ箱に移動しました。", "Page moved to trash": "ページをゴミ箱に移動しました", "Page restored successfully": "ページが正常に復元されました", "Deleted by": "削除者", @@ -608,25 +634,21 @@ "Image exceeds 10MB limit.": "画像が10MBの制限を超えています", "Image removed successfully": "画像を削除しました", "API key": "APIキー", - "API key created successfully": "APIキーを作成しました", "API keys": "APIキー", "API management": "API管理", - "Are you sure you want to revoke this API key": "このAPIキーを無効にしてもよろしいですか", - "Create API Key": "APIキーを作成", "Custom expiration date": "カスタム有効期限", "Enter a descriptive token name": "説明的なトークン名を入力してください", "Expiration": "有効期限", "Expired": "期限切れ", "Expires": "期限が切れます", - "I've saved my API key": "APIキーを保存しました", "Last use": "最終使用", "No API keys found": "APIキーが見つかりません", "No expiration": "期限なし", - "Revoke API key": "APIキーを無効にする", "Revoked successfully": "無効にしました", "Select expiration date": "有効期限を選択してください", "This action cannot be undone. Any applications using this API key will stop working.": "この操作は取り消せません。このAPIキーを使用しているアプリケーションは動作しなくなります", - "Update API key": "APIキーを更新", + "Update": "更新", + "Update {{credential}}": "{{credential}}を更新", "Manage API keys for all users in the workspace": "ワークスペース内のすべてのユーザーのAPIキーを管理", "Restrict API key creation to admins": "APIキーの作成を管理者のみに制限する", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "新しいAPIキーを作成できるのは管理者とオーナーのみです。既存のメンバーキーは引き続き有効です。", @@ -858,9 +880,12 @@ "AI Chat": "AI チャット", "Analyze for insights": "分析してインサイトを得る", "Ask anything...": "何でも聞いてください...", + "Assistant said:": "アシスタントの回答:", "Chat history": "チャット履歴", "Chat name": "チャット名", + "Chat transcript": "チャットの記録", "Close": "閉じる", + "Copy assistant response": "アシスタントの回答をコピー", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "チャットの読み込みに失敗しました。エラーが発生しました。", "Failed to render this message.": "このメッセージの表示に失敗しました。", @@ -870,9 +895,17 @@ "No chats found": "チャットが見つかりません", "No conversations yet": "会話はまだありません", "Open full page": "全ページで開く", + "Scroll to bottom": "一番下までスクロール", + "You said:": "あなたの発言:", "Previous 7 days": "過去 7 日間", "Previous 30 days": "過去 30 日間", "Search chats...": "チャットを検索...", + "Search chats": "チャットを検索", + "Ask anything... Use @ to mention pages": "何でも質問してください… @ を使ってページにメンションできます", + "Ask anything or search your workspace": "何でも質問するか、ワークスペースを検索", + "Welcome to {{name}}": "{{name}} へようこそ", + "Add files": "ファイルを追加", + "Mention a page": "ページにメンション", "Start a new chat to see it here.": "ここに表示するには新しいチャットを開始してください。", "Summarize this page": "このページを要約", "Toggle AI Chat": "AI チャットを切り替え", @@ -880,5 +913,176 @@ "Try a different search term.": "別の検索語を試してください。", "Try again": "再試行", "Untitled chat": "無題のチャット", - "What can I help you with?": "何をお手伝いしましょうか?" + "What can I help you with?": "何をお手伝いしましょうか?", + "Are you sure you want to revoke this {{credential}}": "この{{credential}}を無効にしてもよろしいですか", + "Automatically provision users and groups from your identity provider via SCIM.": "SCIM を介して、ID プロバイダーからユーザーとグループを自動的にプロビジョニングします。", + "Configure your identity provider with this URL to provision users and groups.": "この URL を使用して ID プロバイダーを設定し、ユーザーとグループをプロビジョニングします。", + "Create {{credential}}": "{{credential}}を作成", + "{{credential}} created": "{{credential}}を作成しました", + "{{credential}} created successfully": "{{credential}}を正常に作成しました", + "Created by": "作成者", + "Custom": "カスタム", + "Enable SCIM": "SCIM を有効にする", + "Enter a descriptive name": "説明的な名前を入力してください", + "I've saved my {{credential}}": "{{credential}}を保存しました", + "Important": "重要", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "今すぐ {{credential}} をコピーしてください。後でもう一度表示することはできません!", + "Never": "なし", + "Revoke {{credential}}": "{{credential}}を無効にする", + "SCIM endpoint URL": "SCIM エンドポイント URL", + "SCIM provisioning": "SCIM プロビジョニング", + "SCIM takes precedence over SSO group sync while enabled.": "有効になっている間は、SCIM が SSO グループ同期より優先されます。", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "SCIM トークンの上限 {{max}} に達しました。新しいトークンを作成するには、既存のトークンを削除してください。", + "SCIM token": "SCIM トークン", + "SCIM tokens": "SCIM トークン", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "この操作は元に戻せません。ID プロバイダーは直ちに同期を停止します。", + "Toggle SCIM provisioning": "SCIM プロビジョニングを切り替える", + "Token": "トークン", + "Page menu": "ページメニュー", + "Expand": "展開", + "Collapse": "折りたたむ", + "Comment menu": "コメントメニュー", + "Group menu": "グループメニュー", + "Show hidden breadcrumbs": "非表示のパンくずリストを表示", + "Breadcrumbs": "パンくずリスト", + "Page actions": "ページアクション", + "Pick emoji": "絵文字を選択", + "Template menu": "テンプレートメニュー", + "Use": "使用", + "Use template": "テンプレートを使用", + "Preview template: {{title}}": "テンプレートをプレビュー: {{title}}", + "Use a template": "テンプレートを使用", + "Search templates...": "テンプレートを検索…", + "Search spaces...": "スペースを検索…", + "No templates found": "テンプレートが見つかりません", + "No spaces found": "スペースが見つかりません", + "Browse all templates": "すべてのテンプレートを表示", + "This space": "このスペース", + "All templates": "すべてのテンプレート", + "Global": "グローバル", + "New template": "新しいテンプレート", + "Edit template": "テンプレートを編集", + "Are you sure you want to delete this template?": "このテンプレートを削除してもよろしいですか?", + "Template scope updated": "テンプレートのスコープを更新しました", + "Choose which space this template belongs to": "このテンプレートを所属させるスペースを選択", + "Scope": "スコープ", + "Select scope": "スコープを選択", + "Title": "タイトル", + "Saving...": "保存中…", + "Saved": "保存しました", + "Save failed. Retry": "保存に失敗しました。再試行", + "By {{name}}": "{{name}} 作成", + "Updated {{time}}": "{{time}} に更新", + "Choose destination": "保存先を選択", + "Search pages and spaces...": "ページとスペースを検索…", + "No results found": "結果が見つかりません", + "You don't have permission to create pages here": "ここにページを作成する権限がありません", + "Chat menu": "チャットメニュー", + "API key menu": "API キーメニュー", + "Jump to comment selection": "コメント選択に移動", + "Slash commands": "スラッシュコマンド", + "Mention suggestions": "メンション候補", + "Link suggestions": "リンク候補", + "Diagram editor": "ダイアグラムエディター", + "Add comment": "コメントを追加", + "Find and replace": "検索と置換", + "Main navigation": "メインナビゲーション", + "Space navigation": "スペースナビゲーション", + "Settings navigation": "設定ナビゲーション", + "AI navigation": "AI ナビゲーション", + "Breadcrumb": "パンくずリスト", + "Synced block": "同期ブロック", + "Create a block that stays in sync across pages.": "ページ間で同期されたままになるブロックを作成します。", + "Editing original": "オリジナルを編集中", + "Copy synced block": "同期ブロックをコピー", + "Unsync": "同期解除", + "Delete synced block": "同期ブロックを削除", + "Synced to {{count}} other page_one": "他の{{count}}ページに同期済みです", + "Synced to {{count}} other page_other": "他の{{count}}ページに同期済みです", + "ORIGINAL": "オリジナル", + "THIS PAGE": "このページ", + "No pages": "ページがありません", + "The original synced block no longer exists": "元の同期ブロックは存在しなくなりました", + "You don't have access to this synced block": "この同期ブロックにアクセスできません", + "Failed to load this synced block": "この同期ブロックの読み込みに失敗しました", + "Fixed editor toolbar": "固定エディターツールバー", + "Show a formatting toolbar above the editor with quick access to common actions.": "一般的な操作にすばやくアクセスできる書式設定ツールバーをエディターの上に表示します。", + "Toggle fixed editor toolbar": "固定エディターツールバーを切り替え", + "Normal text": "通常のテキスト", + "More inline formatting": "その他のインライン書式", + "Subscript": "下付き", + "Superscript": "上付き", + "Inline code": "インラインコード", + "Insert media": "メディアを挿入", + "Mention": "メンション", + "Emoji": "絵文字", + "Columns": "列", + "More inserts": "その他の挿入", + "Embeds": "埋め込み", + "Diagrams": "ダイアグラム", + "Advanced": "詳細", + "Utility": "ユーティリティ", + "Decrease indent": "インデントを減らす", + "Increase indent": "インデントを増やす", + "Clear formatting": "書式をクリア", + "Code block": "コードブロック", + "Experimental": "実験的", + "Strikethrough": "取り消し線", + "Undo": "元に戻す", + "Redo": "やり直す", + "Backlinks": "バックリンク", + "Last updated by": "最終更新者", + "Last updated": "最終更新", + "Stats": "統計", + "Word count": "単語数", + "Characters": "文字数", + "Incoming links": "被リンク", + "Outgoing links": "発リンク", + "Incoming links ({{count}})": "被リンク ({{count}})", + "Outgoing links ({{count}})": "発リンク ({{count}})", + "No pages link here yet.": "まだこのページにリンクしているページはありません。", + "This page doesn't link to other pages yet.": "このページはまだ他のページにリンクしていません。", + "Verified until {{date}}": "{{date}} まで検証済み", + "Labels": "ラベル", + "Add label": "ラベルを追加", + "No labels yet": "まだラベルはありません", + "Already added": "追加済み", + "Invalid label name": "無効なラベル名", + "No matches": "一致するものがありません", + "Search or create…": "検索または作成…", + "Remove label {{name}}": "ラベル {{name}} を削除", + "Failed to add label": "ラベルの追加に失敗しました", + "Failed to remove label": "ラベルの削除に失敗しました", + "No pages with this label": "このラベルが付いたページはありません", + "Pages tagged with this label will appear here.": "このラベルが付いたページがここに表示されます。", + "No pages match your search.": "検索に一致するページはありません。", + "Updated {{date}}": "{{date}} に更新", + "Cell actions": "セルの操作", + "Column actions": "列の操作", + "Row actions": "行の操作", + "Filter": "フィルター", + "Page title": "ページタイトル", + "Page content": "ページ内容", + "Member actions": "メンバーの操作", + "Toggle password visibility": "パスワードの表示を切り替え", + "Send comment": "コメントを送信", + "Token actions": "トークンの操作", + "Template settings": "テンプレート設定", + "Edit diagram": "ダイアグラムを編集", + "Edit embed": "埋め込みを編集", + "Edit drawing": "図を編集", + "Delete equation": "数式を削除", + "Invite actions": "招待の操作", + "Get started": "始める", + "* indicates required fields": "* は必須項目を示します", + "List of spaces in this workspace": "このワークスペース内のスペース一覧", + "Active sessions": "アクティブなセッション", + "Add {{name}} to favorites": "{{name}} をお気に入りに追加", + "Remove {{name}} from favorites": "{{name}} をお気に入りから削除", + "Added to favorites": "お気に入りに追加しました", + "Removed from favorites": "お気に入りから削除しました", + "Added {{name}} to favorites": "{{name}} をお気に入りに追加しました", + "Removed {{name}} from favorites": "{{name}} をお気に入りから削除しました", + "Page menu for {{name}}": "{{name}} のページメニュー", + "Create subpage of {{name}}": "{{name}} のサブページを作成" } diff --git a/apps/client/public/locales/ko-KR/translation.json b/apps/client/public/locales/ko-KR/translation.json index e02f16335..5847ab19a 100644 --- a/apps/client/public/locales/ko-KR/translation.json +++ b/apps/client/public/locales/ko-KR/translation.json @@ -71,6 +71,7 @@ "Export": "내보내기", "Failed to create page": "페이지 생성 실패", "Failed to delete page": "페이지 삭제 실패", + "Failed to restore page": "페이지를 복원하지 못했습니다", "Failed to fetch recent pages": "최근 페이지 불러오기 실패", "Failed to import pages": "페이지 가져오기 실패", "Failed to load page. An error occurred.": "페이지 불러오기 실패. 오류가 발생했습니다.", @@ -276,6 +277,9 @@ "Align left": "왼쪽 정렬", "Align right": "오른쪽 정렬", "Align center": "가운데 정렬", + "Alt text": "대체 텍스트", + "Describe this for accessibility.": "접근성을 위해 이를 설명하세요.", + "Add a description": "설명 추가", "Justify": "양쪽 정렬", "Merge cells": "셀 병합", "Split cell": "셀 분할", @@ -286,6 +290,19 @@ "Add row above": "위에 행 추가", "Add row below": "아래에 행 추가", "Delete table": "테이블 삭제", + "Add column left": "왼쪽에 열 추가", + "Add column right": "오른쪽에 열 추가", + "Clear cell": "셀 지우기", + "Clear cells": "셀 지우기", + "Toggle header cell": "헤더 셀 전환", + "Toggle header column": "헤더 열 전환", + "Toggle header row": "헤더 행 전환", + "Move column left": "열 왼쪽으로 이동", + "Move column right": "열 오른쪽으로 이동", + "Move row down": "행 아래로 이동", + "Move row up": "행 위로 이동", + "Sort A → Z": "A → Z 정렬", + "Sort Z → A": "Z → A 정렬", "Info": "정보", "Note": "참고", "Success": "완료", @@ -348,6 +365,8 @@ "Create block quote.": "인용구 만들기.", "Insert code snippet.": "코드 블록 삽입.", "Insert horizontal rule divider": "가로 구분선 삽입", + "Page break": "페이지 나누기", + "Insert a page break for printing.": "인쇄용 페이지 나누기를 삽입합니다.", "Upload any image from your device.": "기기에서 이미지를 업로드하세요.", "Upload any video from your device.": "기기에서 비디오를 업로드하세요.", "Upload any audio from your device.": "기기에서 오디오를 업로드하세요.", @@ -392,6 +411,10 @@ "Write...": "작성...", "Column count": "열 개수", "{{count}} Columns": "{{count}}열", + "{{count}} command available_one": "사용 가능한 명령 1개", + "{{count}} command available_other": "사용 가능한 명령 {{count}}개", + "{{count}} result available_one": "사용 가능한 결과 1개", + "{{count}} result available_other": "사용 가능한 결과 {{count}}개", "Equal columns": "열 너비 균등", "Left sidebar": "왼쪽 사이드바", "Right sidebar": "오른쪽 사이드바", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} 버전을 사용할 수 있습니다", "Default page edit mode": "기본 페이지 편집 모드", "Choose your preferred page edit mode. Avoid accidental edits.": "선호하는 페이지 편집 모드를 선택하세요. 실수로 인한 편집을 방지하세요.", + "Choose {{format}} file": "{{format}} 파일 선택", "Reading": "읽기", "Delete member": "멤버 삭제", "Member deleted successfully": "멤버가 성공적으로 삭제되었습니다", @@ -565,6 +589,8 @@ "Move to trash": "휴지통으로 이동", "Move this page to trash?": "이 페이지를 휴지통으로 이동하시겠습니까?", "Restore page": "페이지 복원", + "Permanently delete": "영구 삭제", + "{{name}} moved this page to Trash {{time}}.": "{{name}}님이 {{time}}에 이 페이지를 휴지통으로 이동했습니다.", "Page moved to trash": "페이지가 휴지통으로 이동되었습니다", "Page restored successfully": "페이지가 성공적으로 복원되었습니다", "Deleted by": "삭제한 사람", @@ -608,25 +634,21 @@ "Image exceeds 10MB limit.": "이미지가 10MB 용량 제한을 초과합니다.", "Image removed successfully": "이미지가 성공적으로 제거되었습니다", "API key": "API 키", - "API key created successfully": "API 키 생성 완료", "API keys": "API 키", "API management": "API 관리", - "Are you sure you want to revoke this API key": "이 API 키를 취소하시겠습니까?", - "Create API Key": "API 키 생성", "Custom expiration date": "사용자 정의 만료일", "Enter a descriptive token name": "토큰 이름을 입력하세요", "Expiration": "만료", "Expired": "만료됨", "Expires": "만료일", - "I've saved my API key": "API 키를 저장했습니다", "Last use": "최근 사용", "No API keys found": "API 키를 찾을 수 없습니다", "No expiration": "유효기간 없음", - "Revoke API key": "API 키 취소", "Revoked successfully": "성공적으로 취소되었습니다", "Select expiration date": "만료일 선택", "This action cannot be undone. Any applications using this API key will stop working.": "이 작업은 되돌릴 수 없습니다. 이 API 키를 사용하는 모든 응용 프로그램이 작동을 멈출 것입니다.", - "Update API key": "API 키 갱신", + "Update": "업데이트", + "Update {{credential}}": "{{credential}} 업데이트", "Manage API keys for all users in the workspace": "워크스페이스 내 모든 사용자의 API 키 관리", "Restrict API key creation to admins": "API 키 생성 권한을 관리자에게만 제한합니다", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "새로운 API 키는 관리자와 소유자만 생성할 수 있습니다. 기존 멤버 키는 계속 사용할 수 있습니다.", @@ -858,9 +880,12 @@ "AI Chat": "AI 채팅", "Analyze for insights": "인사이트 분석", "Ask anything...": "무엇이든 물어보세요...", + "Assistant said:": "어시스턴트의 답변:", "Chat history": "채팅 기록", "Chat name": "채팅 이름", + "Chat transcript": "채팅 기록", "Close": "닫기", + "Copy assistant response": "어시스턴트 응답 복사", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "채팅을 불러오지 못했습니다. 오류가 발생했습니다.", "Failed to render this message.": "이 메시지를 렌더링하지 못했습니다.", @@ -870,9 +895,17 @@ "No chats found": "채팅을 찾을 수 없습니다", "No conversations yet": "아직 대화가 없습니다", "Open full page": "전체 페이지 열기", + "Scroll to bottom": "맨 아래로 스크롤", + "You said:": "내가 한 말:", "Previous 7 days": "지난 7일", "Previous 30 days": "지난 30일", "Search chats...": "채팅 검색...", + "Search chats": "채팅 검색", + "Ask anything... Use @ to mention pages": "무엇이든 물어보세요... 페이지를 언급하려면 @를 사용하세요", + "Ask anything or search your workspace": "무엇이든 물어보거나 워크스페이스를 검색하세요", + "Welcome to {{name}}": "{{name}}에 오신 것을 환영합니다", + "Add files": "파일 추가", + "Mention a page": "페이지 멘션", "Start a new chat to see it here.": "여기에 표시하려면 새 채팅을 시작하세요.", "Summarize this page": "이 페이지 요약", "Toggle AI Chat": "AI 채팅 전환", @@ -880,5 +913,176 @@ "Try a different search term.": "다른 검색어를 사용해 보세요.", "Try again": "다시 시도", "Untitled chat": "제목 없는 채팅", - "What can I help you with?": "무엇을 도와드릴까요?" + "What can I help you with?": "무엇을 도와드릴까요?", + "Are you sure you want to revoke this {{credential}}": "이 {{credential}}을 취소하시겠습니까?", + "Automatically provision users and groups from your identity provider via SCIM.": "SCIM을 통해 ID 공급자에서 사용자와 그룹을 자동으로 프로비저닝합니다.", + "Configure your identity provider with this URL to provision users and groups.": "사용자와 그룹을 프로비저닝할 수 있도록 이 URL로 ID 공급자를 구성하세요.", + "Create {{credential}}": "{{credential}} 만들기", + "{{credential}} created": "{{credential}} 생성됨", + "{{credential}} created successfully": "{{credential}} 생성 완료", + "Created by": "생성한 사람", + "Custom": "사용자 지정", + "Enable SCIM": "SCIM 활성화", + "Enter a descriptive name": "설명적인 이름을 입력하세요", + "I've saved my {{credential}}": "내 {{credential}}를 저장했습니다", + "Important": "중요", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "지금 {{credential}}를 복사해 두세요. 다시는 볼 수 없습니다!", + "Never": "안 함", + "Revoke {{credential}}": "{{credential}} 취소", + "SCIM endpoint URL": "SCIM 엔드포인트 URL", + "SCIM provisioning": "SCIM 프로비저닝", + "SCIM takes precedence over SSO group sync while enabled.": "SCIM이 활성화되어 있는 동안에는 SSO 그룹 동기화보다 SCIM이 우선 적용됩니다.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "SCIM 토큰은 최대 {{max}}개까지 만들 수 있습니다. 새 토큰을 만들려면 기존 토큰을 삭제하세요.", + "SCIM token": "SCIM 토큰", + "SCIM tokens": "SCIM 토큰", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "이 작업은 되돌릴 수 없습니다. ID 공급자가 즉시 동기화를 중지합니다.", + "Toggle SCIM provisioning": "SCIM 프로비저닝 전환", + "Token": "토큰", + "Page menu": "페이지 메뉴", + "Expand": "펼치기", + "Collapse": "접기", + "Comment menu": "댓글 메뉴", + "Group menu": "그룹 메뉴", + "Show hidden breadcrumbs": "숨겨진 이동 경로 표시", + "Breadcrumbs": "이동 경로", + "Page actions": "페이지 작업", + "Pick emoji": "이모지 선택", + "Template menu": "템플릿 메뉴", + "Use": "사용", + "Use template": "템플릿 사용", + "Preview template: {{title}}": "템플릿 미리보기: {{title}}", + "Use a template": "템플릿 사용", + "Search templates...": "템플릿 검색...", + "Search spaces...": "스페이스 검색...", + "No templates found": "템플릿을 찾을 수 없습니다", + "No spaces found": "스페이스를 찾을 수 없습니다", + "Browse all templates": "모든 템플릿 보기", + "This space": "이 스페이스", + "All templates": "모든 템플릿", + "Global": "전역", + "New template": "새 템플릿", + "Edit template": "템플릿 편집", + "Are you sure you want to delete this template?": "이 템플릿을 삭제하시겠습니까?", + "Template scope updated": "템플릿 범위가 업데이트되었습니다", + "Choose which space this template belongs to": "이 템플릿이 속할 스페이스를 선택하세요", + "Scope": "범위", + "Select scope": "범위 선택", + "Title": "제목", + "Saving...": "저장 중...", + "Saved": "저장됨", + "Save failed. Retry": "저장에 실패했습니다. 다시 시도하세요", + "By {{name}}": "작성자 {{name}}", + "Updated {{time}}": "{{time}}에 업데이트됨", + "Choose destination": "대상 선택", + "Search pages and spaces...": "페이지와 스페이스 검색...", + "No results found": "결과를 찾을 수 없습니다", + "You don't have permission to create pages here": "여기에서 페이지를 만들 권한이 없습니다", + "Chat menu": "채팅 메뉴", + "API key menu": "API 키 메뉴", + "Jump to comment selection": "댓글 선택으로 이동", + "Slash commands": "슬래시 명령어", + "Mention suggestions": "멘션 추천", + "Link suggestions": "링크 추천", + "Diagram editor": "다이어그램 편집기", + "Add comment": "댓글 추가", + "Find and replace": "찾기 및 바꾸기", + "Main navigation": "기본 탐색", + "Space navigation": "스페이스 탐색", + "Settings navigation": "설정 탐색", + "AI navigation": "AI 탐색", + "Breadcrumb": "이동 경로", + "Synced block": "동기화된 블록", + "Create a block that stays in sync across pages.": "페이지 간에 동기화된 상태로 유지되는 블록을 만드세요.", + "Editing original": "원본 편집 중", + "Copy synced block": "동기화된 블록 복사", + "Unsync": "동기화 해제", + "Delete synced block": "동기화된 블록 삭제", + "Synced to {{count}} other page_one": "다른 페이지 {{count}}개에 동기화됨", + "Synced to {{count}} other page_other": "다른 페이지 {{count}}개에 동기화됨", + "ORIGINAL": "원본", + "THIS PAGE": "이 페이지", + "No pages": "페이지 없음", + "The original synced block no longer exists": "원본 동기화 블록이 더 이상 존재하지 않습니다", + "You don't have access to this synced block": "이 동기화된 블록에 접근할 수 없습니다", + "Failed to load this synced block": "이 동기화된 블록을 불러오지 못했습니다", + "Fixed editor toolbar": "고정된 편집기 도구 모음", + "Show a formatting toolbar above the editor with quick access to common actions.": "일반적인 작업에 빠르게 접근할 수 있도록 편집기 위에 서식 도구 모음을 표시합니다.", + "Toggle fixed editor toolbar": "고정된 편집기 도구 모음 전환", + "Normal text": "일반 텍스트", + "More inline formatting": "추가 인라인 서식", + "Subscript": "아래 첨자", + "Superscript": "위 첨자", + "Inline code": "인라인 코드", + "Insert media": "미디어 삽입", + "Mention": "멘션", + "Emoji": "이모지", + "Columns": "열", + "More inserts": "더 많은 삽입", + "Embeds": "임베드", + "Diagrams": "다이어그램", + "Advanced": "고급", + "Utility": "유틸리티", + "Decrease indent": "들여쓰기 줄이기", + "Increase indent": "들여쓰기 늘리기", + "Clear formatting": "서식 지우기", + "Code block": "코드 블록", + "Experimental": "실험 기능", + "Strikethrough": "취소선", + "Undo": "실행 취소", + "Redo": "다시 실행", + "Backlinks": "백링크", + "Last updated by": "마지막 업데이트한 사람", + "Last updated": "마지막 업데이트", + "Stats": "통계", + "Word count": "단어 수", + "Characters": "문자 수", + "Incoming links": "들어오는 링크", + "Outgoing links": "나가는 링크", + "Incoming links ({{count}})": "들어오는 링크 ({{count}})", + "Outgoing links ({{count}})": "나가는 링크 ({{count}})", + "No pages link here yet.": "아직 여기에 링크된 페이지가 없습니다.", + "This page doesn't link to other pages yet.": "이 페이지는 아직 다른 페이지에 링크되어 있지 않습니다.", + "Verified until {{date}}": "{{date}}까지 검증됨", + "Labels": "라벨", + "Add label": "라벨 추가", + "No labels yet": "아직 라벨이 없습니다", + "Already added": "이미 추가됨", + "Invalid label name": "유효하지 않은 라벨 이름", + "No matches": "일치하는 항목 없음", + "Search or create…": "검색하거나 만들기…", + "Remove label {{name}}": "라벨 {{name}} 제거", + "Failed to add label": "라벨 추가 실패", + "Failed to remove label": "라벨 제거 실패", + "No pages with this label": "이 라벨이 지정된 페이지가 없습니다", + "Pages tagged with this label will appear here.": "이 라벨이 지정된 페이지가 여기에 표시됩니다.", + "No pages match your search.": "검색과 일치하는 페이지가 없습니다.", + "Updated {{date}}": "{{date}}에 업데이트됨", + "Cell actions": "셀 작업", + "Column actions": "열 작업", + "Row actions": "행 작업", + "Filter": "필터", + "Page title": "페이지 제목", + "Page content": "페이지 내용", + "Member actions": "멤버 작업", + "Toggle password visibility": "비밀번호 표시 전환", + "Send comment": "댓글 보내기", + "Token actions": "토큰 작업", + "Template settings": "템플릿 설정", + "Edit diagram": "다이어그램 편집", + "Edit embed": "임베드 편집", + "Edit drawing": "드로잉 편집", + "Delete equation": "수식 삭제", + "Invite actions": "초대 작업", + "Get started": "시작하기", + "* indicates required fields": "* 는 필수 입력 항목을 나타냅니다", + "List of spaces in this workspace": "이 워크스페이스의 스페이스 목록", + "Active sessions": "활성 세션", + "Add {{name}} to favorites": "{{name}} 즐겨찾기에 추가", + "Remove {{name}} from favorites": "{{name}} 즐겨찾기에서 제거", + "Added to favorites": "즐겨찾기에 추가됨", + "Removed from favorites": "즐겨찾기에서 제거됨", + "Added {{name}} to favorites": "{{name}} 즐겨찾기에 추가됨", + "Removed {{name}} from favorites": "{{name}} 즐겨찾기에서 제거됨", + "Page menu for {{name}}": "{{name}}의 페이지 메뉴", + "Create subpage of {{name}}": "{{name}}의 하위 페이지 만들기" } diff --git a/apps/client/public/locales/nl-NL/translation.json b/apps/client/public/locales/nl-NL/translation.json index 0ef2a3ba2..591c96746 100644 --- a/apps/client/public/locales/nl-NL/translation.json +++ b/apps/client/public/locales/nl-NL/translation.json @@ -71,6 +71,7 @@ "Export": "Exporteer", "Failed to create page": "Pagina aanmaken mislukt", "Failed to delete page": "Verwijderen van pagina mislukt", + "Failed to restore page": "Pagina herstellen mislukt", "Failed to fetch recent pages": "Kan recente pagina's niet ophalen", "Failed to import pages": "Pagina's importeren mislukt", "Failed to load page. An error occurred.": "Laden van pagina mislukt. Er is een fout opgetreden.", @@ -276,6 +277,9 @@ "Align left": "Links uitlijnen", "Align right": "Rechts uitlijnen", "Align center": "Centreren", + "Alt text": "Alternatieve tekst", + "Describe this for accessibility.": "Beschrijf dit voor toegankelijkheid.", + "Add a description": "Een beschrijving toevoegen", "Justify": "Uitvullen", "Merge cells": "Cellen samenvoegen", "Split cell": "Cel splitsen", @@ -286,6 +290,19 @@ "Add row above": "Rij hierboven toevoegen", "Add row below": "Rij hieronder toevoegen", "Delete table": "Verwijder tabel", + "Add column left": "Kolom links toevoegen", + "Add column right": "Kolom rechts toevoegen", + "Clear cell": "Cel wissen", + "Clear cells": "Cellen wissen", + "Toggle header cell": "Kopcel in-/uitschakelen", + "Toggle header column": "Kopkolom in-/uitschakelen", + "Toggle header row": "Koprij in-/uitschakelen", + "Move column left": "Kolom naar links verplaatsen", + "Move column right": "Kolom naar rechts verplaatsen", + "Move row down": "Rij omlaag verplaatsen", + "Move row up": "Rij omhoog verplaatsen", + "Sort A → Z": "Sorteren A → Z", + "Sort Z → A": "Sorteren Z → A", "Info": "Info", "Note": "Opmerking", "Success": "Geslaagd", @@ -348,6 +365,8 @@ "Create block quote.": "Maak een block quote.", "Insert code snippet.": "Codefragment invoegen.", "Insert horizontal rule divider": "Horizontale lijn invoegen", + "Page break": "Pagina-einde", + "Insert a page break for printing.": "Voeg een pagina-einde in voor het afdrukken.", "Upload any image from your device.": "Upload een afbeelding vanaf uw apparaat.", "Upload any video from your device.": "Upload een video vanaf uw apparaat.", "Upload any audio from your device.": "Upload een audio vanaf uw apparaat.", @@ -392,6 +411,10 @@ "Write...": "Typ...", "Column count": "Aantal kolommen", "{{count}} Columns": "{{count}} kolommen", + "{{count}} command available_one": "1 opdracht beschikbaar", + "{{count}} command available_other": "{{count}} opdrachten beschikbaar", + "{{count}} result available_one": "1 resultaat beschikbaar", + "{{count}} result available_other": "{{count}} resultaten beschikbaar", "Equal columns": "Gelijke kolommen", "Left sidebar": "Linker zijbalk", "Right sidebar": "Rechter zijbalk", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} is beschikbaar", "Default page edit mode": "Standaard bewerkingsmodus voor pagina", "Choose your preferred page edit mode. Avoid accidental edits.": "Kies uw voorkeurs bewerkmodus voor pagina's. Vermijd per ongeluk bewerken.", + "Choose {{format}} file": "Kies {{format}}-bestand", "Reading": "Lezen", "Delete member": "Lid verwijderen", "Member deleted successfully": "Lid succesvol verwijderd", @@ -565,6 +589,8 @@ "Move to trash": "Verplaatsen naar prullenbak", "Move this page to trash?": "Deze pagina naar de prullenbak verplaatsen?", "Restore page": "Pagina herstellen", + "Permanently delete": "Permanent verwijderen", + "{{name}} moved this page to Trash {{time}}.": "{{name}} heeft deze pagina {{time}} naar de prullenbak verplaatst.", "Page moved to trash": "Pagina verplaatst naar prullenbak", "Page restored successfully": "Pagina succesvol hersteld", "Deleted by": "Verwijderd door", @@ -608,25 +634,21 @@ "Image exceeds 10MB limit.": "Afbeelding overschrijdt de limiet van 10MB.", "Image removed successfully": "Afbeelding succesvol verwijderd", "API key": "API-sleutel", - "API key created successfully": "API-sleutel succesvol aangemaakt", "API keys": "API-sleutels", "API management": "API-beheer", - "Are you sure you want to revoke this API key": "Weet u zeker dat u deze API-sleutel wilt intrekken", - "Create API Key": "API-sleutel aanmaken", "Custom expiration date": "Aangepaste vervaldatum", "Enter a descriptive token name": "Voer een beschrijvende tokennaam in", "Expiration": "Vervaldatum", "Expired": "Verlopen", "Expires": "Verloopt", - "I've saved my API key": "Ik heb mijn API-sleutel opgeslagen", "Last use": "Laatst gebruikt", "No API keys found": "Geen API-sleutels gevonden", "No expiration": "Geen vervaldatum", - "Revoke API key": "API-sleutel intrekken", "Revoked successfully": "Succesvol ingetrokken", "Select expiration date": "Selecteer vervaldatum", "This action cannot be undone. Any applications using this API key will stop working.": "Deze actie kan niet ongedaan worden gemaakt. Alle toepassingen die deze API-sleutel gebruiken, zullen niet meer werken.", - "Update API key": "API-sleutel bijwerken", + "Update": "Bijwerken", + "Update {{credential}}": "{{credential}} bijwerken", "Manage API keys for all users in the workspace": "Beheer API-sleutels voor alle gebruikers in de werkruimte", "Restrict API key creation to admins": "Beperk het aanmaken van API-sleutels tot beheerders.", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Alleen beheerders en eigenaren kunnen nieuwe API-sleutels aanmaken. Bestaande leden-sleutels blijven werken.", @@ -858,9 +880,12 @@ "AI Chat": "AI-chat", "Analyze for insights": "Analyseren voor inzichten", "Ask anything...": "Vraag iets...", + "Assistant said:": "Assistent zei:", "Chat history": "Chatgeschiedenis", "Chat name": "Chatnaam", + "Chat transcript": "Chattranscript", "Close": "Sluiten", + "Copy assistant response": "Reactie van assistent kopiëren", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "Chat laden mislukt. Er is een fout opgetreden.", "Failed to render this message.": "Dit bericht kon niet worden weergegeven.", @@ -870,9 +895,17 @@ "No chats found": "Geen chats gevonden", "No conversations yet": "Nog geen gesprekken", "Open full page": "Volledige pagina openen", + "Scroll to bottom": "Naar beneden scrollen", + "You said:": "Jij zei:", "Previous 7 days": "Afgelopen 7 dagen", "Previous 30 days": "Afgelopen 30 dagen", "Search chats...": "Chats zoeken...", + "Search chats": "Chats zoeken", + "Ask anything... Use @ to mention pages": "Vraag iets... Gebruik @ om pagina's te vermelden", + "Ask anything or search your workspace": "Vraag iets of doorzoek je werkruimte", + "Welcome to {{name}}": "Welkom bij {{name}}", + "Add files": "Bestanden toevoegen", + "Mention a page": "Een pagina vermelden", "Start a new chat to see it here.": "Start een nieuwe chat om die hier te zien.", "Summarize this page": "Vat deze pagina samen", "Toggle AI Chat": "AI-chat in-/uitschakelen", @@ -880,5 +913,176 @@ "Try a different search term.": "Probeer een andere zoekterm.", "Try again": "Probeer opnieuw", "Untitled chat": "Chat zonder titel", - "What can I help you with?": "Waar kan ik je mee helpen?" + "What can I help you with?": "Waar kan ik je mee helpen?", + "Are you sure you want to revoke this {{credential}}": "Weet u zeker dat u deze {{credential}} wilt intrekken", + "Automatically provision users and groups from your identity provider via SCIM.": "Voorzie gebruikers en groepen automatisch vanuit uw identiteitsprovider via SCIM.", + "Configure your identity provider with this URL to provision users and groups.": "Configureer uw identiteitsprovider met deze URL om gebruikers en groepen te provisioneren.", + "Create {{credential}}": "{{credential}} maken", + "{{credential}} created": "{{credential}} aangemaakt", + "{{credential}} created successfully": "{{credential}} succesvol aangemaakt", + "Created by": "Aangemaakt door", + "Custom": "Aangepast", + "Enable SCIM": "SCIM inschakelen", + "Enter a descriptive name": "Voer een beschrijvende naam in", + "I've saved my {{credential}}": "Ik heb mijn {{credential}} opgeslagen", + "Important": "Belangrijk", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "Zorg ervoor dat u uw {{credential}} nu kopieert. U kunt deze niet meer bekijken!", + "Never": "Nooit", + "Revoke {{credential}}": "{{credential}} intrekken", + "SCIM endpoint URL": "SCIM-eindpunt-URL", + "SCIM provisioning": "SCIM-provisioning", + "SCIM takes precedence over SSO group sync while enabled.": "SCIM heeft voorrang op SSO-groepssynchronisatie zolang het is ingeschakeld.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "U heeft het maximum van {{max}} SCIM-tokens bereikt. Verwijder een bestaand token om een nieuw token aan te maken.", + "SCIM token": "SCIM-token", + "SCIM tokens": "SCIM-tokens", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "Deze actie kan niet ongedaan worden gemaakt. Uw identiteitsprovider stopt onmiddellijk met synchroniseren.", + "Toggle SCIM provisioning": "SCIM-provisioning in-/uitschakelen", + "Token": "Token", + "Page menu": "Paginamenu", + "Expand": "Uitvouwen", + "Collapse": "Samenvouwen", + "Comment menu": "Reactiemenu", + "Group menu": "Groepsmenu", + "Show hidden breadcrumbs": "Verborgen broodkruimels weergeven", + "Breadcrumbs": "Broodkruimels", + "Page actions": "Pagina-acties", + "Pick emoji": "Emoji kiezen", + "Template menu": "Sjabloonmenu", + "Use": "Gebruiken", + "Use template": "Sjabloon gebruiken", + "Preview template: {{title}}": "Voorbeeld van sjabloon: {{title}}", + "Use a template": "Een sjabloon gebruiken", + "Search templates...": "Sjablonen zoeken...", + "Search spaces...": "Werkruimtes zoeken...", + "No templates found": "Geen sjablonen gevonden", + "No spaces found": "Geen werkruimtes gevonden", + "Browse all templates": "Alle sjablonen bekijken", + "This space": "Deze werkruimte", + "All templates": "Alle sjablonen", + "Global": "Globaal", + "New template": "Nieuw sjabloon", + "Edit template": "Sjabloon bewerken", + "Are you sure you want to delete this template?": "Weet je zeker dat je dit sjabloon wilt verwijderen?", + "Template scope updated": "Bereik van sjabloon bijgewerkt", + "Choose which space this template belongs to": "Kies bij welke werkruimte dit sjabloon hoort", + "Scope": "Bereik", + "Select scope": "Bereik selecteren", + "Title": "Titel", + "Saving...": "Opslaan...", + "Saved": "Opgeslagen", + "Save failed. Retry": "Opslaan mislukt. Opnieuw proberen", + "By {{name}}": "Door {{name}}", + "Updated {{time}}": "Bijgewerkt {{time}}", + "Choose destination": "Bestemming kiezen", + "Search pages and spaces...": "Pagina's en werkruimtes zoeken...", + "No results found": "Geen resultaten gevonden", + "You don't have permission to create pages here": "Je hebt geen toestemming om hier pagina's te maken", + "Chat menu": "Chatmenu", + "API key menu": "API-sleutelmenu", + "Jump to comment selection": "Naar reactieselectie springen", + "Slash commands": "Slash-opdrachten", + "Mention suggestions": "Vermeldingssuggesties", + "Link suggestions": "Linksuggesties", + "Diagram editor": "Diagrameditor", + "Add comment": "Reactie toevoegen", + "Find and replace": "Zoeken en vervangen", + "Main navigation": "Hoofdnavigatie", + "Space navigation": "Ruimtenavigatie", + "Settings navigation": "Instellingennavigatie", + "AI navigation": "AI-navigatie", + "Breadcrumb": "Broodkruimel", + "Synced block": "Gesynchroniseerd blok", + "Create a block that stays in sync across pages.": "Maak een blok dat gesynchroniseerd blijft op meerdere pagina's.", + "Editing original": "Origineel bewerken", + "Copy synced block": "Gesynchroniseerd blok kopiëren", + "Unsync": "Synchronisatie opheffen", + "Delete synced block": "Gesynchroniseerd blok verwijderen", + "Synced to {{count}} other page_one": "Gesynchroniseerd met {{count}} andere pagina", + "Synced to {{count}} other page_other": "Gesynchroniseerd met {{count}} andere pagina's", + "ORIGINAL": "ORIGINEEL", + "THIS PAGE": "DEZE PAGINA", + "No pages": "Geen pagina's", + "The original synced block no longer exists": "Het oorspronkelijke gesynchroniseerde blok bestaat niet meer", + "You don't have access to this synced block": "Je hebt geen toegang tot dit gesynchroniseerde blok", + "Failed to load this synced block": "Dit gesynchroniseerde blok kon niet worden geladen", + "Fixed editor toolbar": "Vaste editorwerkbalk", + "Show a formatting toolbar above the editor with quick access to common actions.": "Toon een opmaakwerkbalk boven de editor met snelle toegang tot veelgebruikte acties.", + "Toggle fixed editor toolbar": "Vaste editorwerkbalk in-/uitschakelen", + "Normal text": "Normale tekst", + "More inline formatting": "Meer inline-opmaak", + "Subscript": "Subscript", + "Superscript": "Superscript", + "Inline code": "Inline-code", + "Insert media": "Media invoegen", + "Mention": "Vermelding", + "Emoji": "Emoji", + "Columns": "Kolommen", + "More inserts": "Meer invoegingen", + "Embeds": "Insluitingen", + "Diagrams": "Diagrammen", + "Advanced": "Geavanceerd", + "Utility": "Hulpprogramma's", + "Decrease indent": "Inspringing verkleinen", + "Increase indent": "Inspringing vergroten", + "Clear formatting": "Opmaak wissen", + "Code block": "Codeblok", + "Experimental": "Experimenteel", + "Strikethrough": "Doorhalen", + "Undo": "Ongedaan maken", + "Redo": "Opnieuw", + "Backlinks": "Terugkoppelingen", + "Last updated by": "Laatst bijgewerkt door", + "Last updated": "Laatst bijgewerkt", + "Stats": "Statistieken", + "Word count": "Aantal woorden", + "Characters": "Tekens", + "Incoming links": "Inkomende links", + "Outgoing links": "Uitgaande links", + "Incoming links ({{count}})": "Inkomende links ({{count}})", + "Outgoing links ({{count}})": "Uitgaande links ({{count}})", + "No pages link here yet.": "Er linken nog geen pagina's hiernaartoe.", + "This page doesn't link to other pages yet.": "Deze pagina linkt nog niet naar andere pagina's.", + "Verified until {{date}}": "Geverifieerd tot {{date}}", + "Labels": "Labels", + "Add label": "Label toevoegen", + "No labels yet": "Nog geen labels", + "Already added": "Al toegevoegd", + "Invalid label name": "Ongeldige labelnaam", + "No matches": "Geen overeenkomsten", + "Search or create…": "Zoeken of maken…", + "Remove label {{name}}": "Label {{name}} verwijderen", + "Failed to add label": "Label toevoegen mislukt", + "Failed to remove label": "Label verwijderen mislukt", + "No pages with this label": "Geen pagina's met dit label", + "Pages tagged with this label will appear here.": "Pagina's met dit label worden hier weergegeven.", + "No pages match your search.": "Geen pagina's komen overeen met je zoekopdracht.", + "Updated {{date}}": "Bijgewerkt op {{date}}", + "Cell actions": "Celacties", + "Column actions": "Kolomacties", + "Row actions": "Rijacties", + "Filter": "Filter", + "Page title": "Paginatitel", + "Page content": "Pagina-inhoud", + "Member actions": "Lidacties", + "Toggle password visibility": "Wachtwoordzichtbaarheid in-/uitschakelen", + "Send comment": "Reactie verzenden", + "Token actions": "Tokenacties", + "Template settings": "Sjablooninstellingen", + "Edit diagram": "Diagram bewerken", + "Edit embed": "Insluiting bewerken", + "Edit drawing": "Tekening bewerken", + "Delete equation": "Vergelijking verwijderen", + "Invite actions": "Uitnodigingsacties", + "Get started": "Aan de slag", + "* indicates required fields": "* geeft verplichte velden aan", + "List of spaces in this workspace": "Lijst met werkruimtes in deze workspace", + "Active sessions": "Actieve sessies", + "Add {{name}} to favorites": "{{name}} aan favorieten toevoegen", + "Remove {{name}} from favorites": "{{name}} uit favorieten verwijderen", + "Added to favorites": "Toegevoegd aan favorieten", + "Removed from favorites": "Verwijderd uit favorieten", + "Added {{name}} to favorites": "{{name}} toegevoegd aan favorieten", + "Removed {{name}} from favorites": "{{name}} verwijderd uit favorieten", + "Page menu for {{name}}": "Paginamenu voor {{name}}", + "Create subpage of {{name}}": "Subpagina van {{name}} maken" } diff --git a/apps/client/public/locales/pt-BR/translation.json b/apps/client/public/locales/pt-BR/translation.json index f71b2e59b..e2788f1fe 100644 --- a/apps/client/public/locales/pt-BR/translation.json +++ b/apps/client/public/locales/pt-BR/translation.json @@ -71,6 +71,7 @@ "Export": "Exportar", "Failed to create page": "Falha ao criar página", "Failed to delete page": "Falha ao excluir página", + "Failed to restore page": "Falha ao restaurar página", "Failed to fetch recent pages": "Falha ao buscar páginas recentes", "Failed to import pages": "Falha ao importar páginas", "Failed to load page. An error occurred.": "Falha ao carregar página. Ocorreu um erro.", @@ -276,6 +277,9 @@ "Align left": "Alinhar à esquerda", "Align right": "Alinhar à direita", "Align center": "Alinhar ao centro", + "Alt text": "Texto alternativo", + "Describe this for accessibility.": "Descreva isto para acessibilidade.", + "Add a description": "Adicionar uma descrição", "Justify": "Justificar", "Merge cells": "Mesclar células", "Split cell": "Dividir célula", @@ -286,6 +290,19 @@ "Add row above": "Adicionar linha acima", "Add row below": "Adicionar linha abaixo", "Delete table": "Excluir tabela", + "Add column left": "Adicionar coluna à esquerda", + "Add column right": "Adicionar coluna à direita", + "Clear cell": "Limpar célula", + "Clear cells": "Limpar células", + "Toggle header cell": "Alternar célula de cabeçalho", + "Toggle header column": "Alternar coluna de cabeçalho", + "Toggle header row": "Alternar linha de cabeçalho", + "Move column left": "Mover coluna para a esquerda", + "Move column right": "Mover coluna para a direita", + "Move row down": "Mover linha para baixo", + "Move row up": "Mover linha para cima", + "Sort A → Z": "Ordenar A → Z", + "Sort Z → A": "Ordenar Z → A", "Info": "Informação", "Note": "Observação", "Success": "Sucesso", @@ -348,6 +365,8 @@ "Create block quote.": "Crie uma citação em bloco.", "Insert code snippet.": "Insira um trecho de código.", "Insert horizontal rule divider": "Insira um divisor horizontal", + "Page break": "Quebra de página", + "Insert a page break for printing.": "Insira uma quebra de página para impressão.", "Upload any image from your device.": "Envie qualquer imagem do seu dispositivo.", "Upload any video from your device.": "Envie qualquer vídeo do seu dispositivo.", "Upload any audio from your device.": "Envie qualquer áudio do seu dispositivo.", @@ -392,6 +411,10 @@ "Write...": "Escreva...", "Column count": "Número de colunas", "{{count}} Columns": "{{count}} colunas", + "{{count}} command available_one": "1 comando disponível", + "{{count}} command available_other": "{{count}} comandos disponíveis", + "{{count}} result available_one": "1 resultado disponível", + "{{count}} result available_other": "{{count}} resultados disponíveis", "Equal columns": "Colunas iguais", "Left sidebar": "Barra lateral esquerda", "Right sidebar": "Barra lateral direita", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} está disponível", "Default page edit mode": "Modo padrão de edição da página", "Choose your preferred page edit mode. Avoid accidental edits.": "Escolha o modo de edição de página preferido. Evite edições acidentais.", + "Choose {{format}} file": "Escolher arquivo {{format}}", "Reading": "Leitura", "Delete member": "Excluir membro", "Member deleted successfully": "Membro excluído com sucesso", @@ -565,6 +589,8 @@ "Move to trash": "Mover para a lixeira", "Move this page to trash?": "Mover esta página para a lixeira?", "Restore page": "Restaurar página", + "Permanently delete": "Excluir permanentemente", + "{{name}} moved this page to Trash {{time}}.": "{{name}} moveu esta página para a Lixeira {{time}}.", "Page moved to trash": "Página movida para a lixeira", "Page restored successfully": "Página restaurada com sucesso", "Deleted by": "Excluído por", @@ -608,25 +634,21 @@ "Image exceeds 10MB limit.": "A imagem excede o limite de 10MB.", "Image removed successfully": "Imagem removida com sucesso", "API key": "Chave API", - "API key created successfully": "Chave API criada com sucesso", "API keys": "Chaves API", "API management": "Gestão de API", - "Are you sure you want to revoke this API key": "Tem certeza de que deseja revogar esta chave API", - "Create API Key": "Criar Chave API", "Custom expiration date": "Data de expiração personalizada", "Enter a descriptive token name": "Insira um nome descritivo para o token", "Expiration": "Expiração", "Expired": "Expirado", "Expires": "Expira", - "I've saved my API key": "Salvei minha chave API", "Last use": "Último uso", "No API keys found": "Nenhuma chave API encontrada", "No expiration": "Sem expiração", - "Revoke API key": "Revogar chave API", "Revoked successfully": "Revogada com sucesso", "Select expiration date": "Selecionar data de expiração", "This action cannot be undone. Any applications using this API key will stop working.": "Esta ação não pode ser desfeita. Qualquer aplicação usando esta chave API deixará de funcionar.", - "Update API key": "Atualizar chave API", + "Update": "Atualizar", + "Update {{credential}}": "Atualizar {{credential}}", "Manage API keys for all users in the workspace": "Gerenciar chaves API para todos os usuários no espaço de trabalho", "Restrict API key creation to admins": "Restringir a criação de chave de API aos administradores", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Somente administradores e proprietários podem criar novas chaves de API. As chaves de membros já existentes continuarão funcionando.", @@ -858,9 +880,12 @@ "AI Chat": "Chat com IA", "Analyze for insights": "Analisar para obter insights", "Ask anything...": "Pergunte qualquer coisa...", + "Assistant said:": "O assistente disse:", "Chat history": "Histórico de chats", "Chat name": "Nome do chat", + "Chat transcript": "Transcrição do chat", "Close": "Fechar", + "Copy assistant response": "Copiar resposta do assistente", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "Falha ao carregar o chat. Ocorreu um erro.", "Failed to render this message.": "Falha ao renderizar esta mensagem.", @@ -870,9 +895,17 @@ "No chats found": "Nenhum chat encontrado", "No conversations yet": "Ainda não há conversas", "Open full page": "Abrir página inteira", + "Scroll to bottom": "Rolar até o fim", + "You said:": "Você disse:", "Previous 7 days": "Últimos 7 dias", "Previous 30 days": "Últimos 30 dias", "Search chats...": "Pesquisar chats...", + "Search chats": "Pesquisar chats", + "Ask anything... Use @ to mention pages": "Pergunte qualquer coisa... Use @ para mencionar páginas", + "Ask anything or search your workspace": "Pergunte qualquer coisa ou pesquise no seu workspace", + "Welcome to {{name}}": "Boas-vindas a {{name}}", + "Add files": "Adicionar arquivos", + "Mention a page": "Mencionar uma página", "Start a new chat to see it here.": "Inicie um novo chat para vê-lo aqui.", "Summarize this page": "Resumir esta página", "Toggle AI Chat": "Alternar chat com IA", @@ -880,5 +913,176 @@ "Try a different search term.": "Tente um termo de pesquisa diferente.", "Try again": "Tentar novamente", "Untitled chat": "Chat sem título", - "What can I help you with?": "Com o que posso ajudar você?" + "What can I help you with?": "Com o que posso ajudar você?", + "Are you sure you want to revoke this {{credential}}": "Tem certeza de que deseja revogar esta {{credential}}", + "Automatically provision users and groups from your identity provider via SCIM.": "Provisione automaticamente usuários e grupos do seu provedor de identidade via SCIM.", + "Configure your identity provider with this URL to provision users and groups.": "Configure seu provedor de identidade com esta URL para provisionar usuários e grupos.", + "Create {{credential}}": "Criar {{credential}}", + "{{credential}} created": "{{credential}} criada", + "{{credential}} created successfully": "{{credential}} criada com sucesso", + "Created by": "Criado por", + "Custom": "Personalizado", + "Enable SCIM": "Ativar SCIM", + "Enter a descriptive name": "Insira um nome descritivo", + "I've saved my {{credential}}": "Salvei minha {{credential}}", + "Important": "Importante", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "Copie sua {{credential}} agora. Você não poderá vê-la novamente!", + "Never": "Nunca", + "Revoke {{credential}}": "Revogar {{credential}}", + "SCIM endpoint URL": "URL do endpoint SCIM", + "SCIM provisioning": "Provisionamento SCIM", + "SCIM takes precedence over SSO group sync while enabled.": "O SCIM tem precedência sobre a sincronização de grupos por SSO enquanto estiver ativado.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "Você atingiu o máximo de {{max}} tokens SCIM. Exclua um token existente para criar um novo.", + "SCIM token": "Token SCIM", + "SCIM tokens": "Tokens SCIM", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "Esta ação não pode ser desfeita. Seu provedor de identidade deixará de sincronizar imediatamente.", + "Toggle SCIM provisioning": "Alternar provisionamento SCIM", + "Token": "Token", + "Page menu": "Menu da página", + "Expand": "Expandir", + "Collapse": "Recolher", + "Comment menu": "Menu de comentários", + "Group menu": "Menu do grupo", + "Show hidden breadcrumbs": "Mostrar breadcrumbs ocultos", + "Breadcrumbs": "Trilhas de navegação", + "Page actions": "Ações da página", + "Pick emoji": "Escolher emoji", + "Template menu": "Menu do modelo", + "Use": "Usar", + "Use template": "Usar modelo", + "Preview template: {{title}}": "Visualizar modelo: {{title}}", + "Use a template": "Usar um modelo", + "Search templates...": "Pesquisar modelos...", + "Search spaces...": "Pesquisar espaços...", + "No templates found": "Nenhum modelo encontrado", + "No spaces found": "Nenhum espaço encontrado", + "Browse all templates": "Ver todos os modelos", + "This space": "Este espaço", + "All templates": "Todos os modelos", + "Global": "Global", + "New template": "Novo modelo", + "Edit template": "Editar modelo", + "Are you sure you want to delete this template?": "Tem certeza de que deseja excluir este modelo?", + "Template scope updated": "Escopo do modelo atualizado", + "Choose which space this template belongs to": "Escolha a qual espaço este modelo pertence", + "Scope": "Escopo", + "Select scope": "Selecionar escopo", + "Title": "Título", + "Saving...": "Salvando...", + "Saved": "Salvo", + "Save failed. Retry": "Falha ao salvar. Tentar novamente", + "By {{name}}": "Por {{name}}", + "Updated {{time}}": "Atualizado {{time}}", + "Choose destination": "Escolher destino", + "Search pages and spaces...": "Pesquisar páginas e espaços...", + "No results found": "Nenhum resultado encontrado", + "You don't have permission to create pages here": "Você não tem permissão para criar páginas aqui", + "Chat menu": "Menu do chat", + "API key menu": "Menu da chave de API", + "Jump to comment selection": "Ir para a seleção de comentários", + "Slash commands": "Comandos de barra", + "Mention suggestions": "Sugestões de menção", + "Link suggestions": "Sugestões de links", + "Diagram editor": "Editor de diagramas", + "Add comment": "Adicionar comentário", + "Find and replace": "Localizar e substituir", + "Main navigation": "Navegação principal", + "Space navigation": "Navegação do espaço", + "Settings navigation": "Navegação de configurações", + "AI navigation": "Navegação de IA", + "Breadcrumb": "Trilha de navegação", + "Synced block": "Bloco sincronizado", + "Create a block that stays in sync across pages.": "Crie um bloco que permaneça sincronizado entre páginas.", + "Editing original": "Editando original", + "Copy synced block": "Copiar bloco sincronizado", + "Unsync": "Desfazer sincronização", + "Delete synced block": "Excluir bloco sincronizado", + "Synced to {{count}} other page_one": "Sincronizado com {{count}} outra página", + "Synced to {{count}} other page_other": "Sincronizado com {{count}} outras páginas", + "ORIGINAL": "ORIGINAL", + "THIS PAGE": "ESTA PÁGINA", + "No pages": "Nenhuma página", + "The original synced block no longer exists": "O bloco sincronizado original não existe mais", + "You don't have access to this synced block": "Você não tem acesso a este bloco sincronizado", + "Failed to load this synced block": "Falha ao carregar este bloco sincronizado", + "Fixed editor toolbar": "Barra de ferramentas fixa do editor", + "Show a formatting toolbar above the editor with quick access to common actions.": "Mostre uma barra de ferramentas de formatação acima do editor com acesso rápido a ações comuns.", + "Toggle fixed editor toolbar": "Alternar barra de ferramentas fixa do editor", + "Normal text": "Texto normal", + "More inline formatting": "Mais formatação em linha", + "Subscript": "Subscrito", + "Superscript": "Sobrescrito", + "Inline code": "Código em linha", + "Insert media": "Inserir mídia", + "Mention": "Menção", + "Emoji": "Emoji", + "Columns": "Colunas", + "More inserts": "Mais inserções", + "Embeds": "Incorporações", + "Diagrams": "Diagramas", + "Advanced": "Avançado", + "Utility": "Utilitário", + "Decrease indent": "Diminuir recuo", + "Increase indent": "Aumentar recuo", + "Clear formatting": "Limpar formatação", + "Code block": "Bloco de código", + "Experimental": "Experimental", + "Strikethrough": "Tachado", + "Undo": "Desfazer", + "Redo": "Refazer", + "Backlinks": "Links de retorno", + "Last updated by": "Última atualização por", + "Last updated": "Última atualização", + "Stats": "Estatísticas", + "Word count": "Contagem de palavras", + "Characters": "Caracteres", + "Incoming links": "Links recebidos", + "Outgoing links": "Links de saída", + "Incoming links ({{count}})": "Links recebidos ({{count}})", + "Outgoing links ({{count}})": "Links de saída ({{count}})", + "No pages link here yet.": "Nenhuma página tem link para cá ainda.", + "This page doesn't link to other pages yet.": "Esta página ainda não tem links para outras páginas.", + "Verified until {{date}}": "Verificado até {{date}}", + "Labels": "Rótulos", + "Add label": "Adicionar rótulo", + "No labels yet": "Ainda não há rótulos", + "Already added": "Já adicionado", + "Invalid label name": "Nome de rótulo inválido", + "No matches": "Sem correspondências", + "Search or create…": "Pesquisar ou criar…", + "Remove label {{name}}": "Remover rótulo {{name}}", + "Failed to add label": "Falha ao adicionar rótulo", + "Failed to remove label": "Falha ao remover rótulo", + "No pages with this label": "Nenhuma página com este rótulo", + "Pages tagged with this label will appear here.": "As páginas marcadas com este rótulo aparecerão aqui.", + "No pages match your search.": "Nenhuma página corresponde à sua pesquisa.", + "Updated {{date}}": "Atualizado em {{date}}", + "Cell actions": "Ações da célula", + "Column actions": "Ações da coluna", + "Row actions": "Ações da linha", + "Filter": "Filtrar", + "Page title": "Título da página", + "Page content": "Conteúdo da página", + "Member actions": "Ações do membro", + "Toggle password visibility": "Alternar visibilidade da senha", + "Send comment": "Enviar comentário", + "Token actions": "Ações do token", + "Template settings": "Configurações do modelo", + "Edit diagram": "Editar diagrama", + "Edit embed": "Editar incorporação", + "Edit drawing": "Editar desenho", + "Delete equation": "Excluir equação", + "Invite actions": "Ações do convite", + "Get started": "Começar", + "* indicates required fields": "* indica campos obrigatórios", + "List of spaces in this workspace": "Lista de espaços neste workspace", + "Active sessions": "Sessões ativas", + "Add {{name}} to favorites": "Adicionar {{name}} aos favoritos", + "Remove {{name}} from favorites": "Remover {{name}} dos favoritos", + "Added to favorites": "Adicionado aos favoritos", + "Removed from favorites": "Removido dos favoritos", + "Added {{name}} to favorites": "{{name}} adicionado aos favoritos", + "Removed {{name}} from favorites": "{{name}} removido dos favoritos", + "Page menu for {{name}}": "Menu da página de {{name}}", + "Create subpage of {{name}}": "Criar subpágina de {{name}}" } diff --git a/apps/client/public/locales/ru-RU/translation.json b/apps/client/public/locales/ru-RU/translation.json index e5ac8fe85..3d93ec67d 100644 --- a/apps/client/public/locales/ru-RU/translation.json +++ b/apps/client/public/locales/ru-RU/translation.json @@ -71,6 +71,7 @@ "Export": "Экспорт", "Failed to create page": "Не удалось создать страницу", "Failed to delete page": "Не удалось удалить страницу", + "Failed to restore page": "Не удалось восстановить страницу", "Failed to fetch recent pages": "Не удалось получить недавние страницы", "Failed to import pages": "Не удалось импортировать страницы", "Failed to load page. An error occurred.": "Не удалось загрузить страницу. Произошла ошибка.", @@ -276,6 +277,9 @@ "Align left": "По левому краю", "Align right": "По правому краю", "Align center": "По центру", + "Alt text": "Альтернативный текст", + "Describe this for accessibility.": "Опишите это для специальных возможностей.", + "Add a description": "Добавить описание", "Justify": "По ширине", "Merge cells": "Объединить ячейки", "Split cell": "Разделить ячейку", @@ -286,6 +290,19 @@ "Add row above": "Добавить строку выше", "Add row below": "Добавить строку ниже", "Delete table": "Удалить таблицу", + "Add column left": "Добавить столбец слева", + "Add column right": "Добавить столбец справа", + "Clear cell": "Очистить ячейку", + "Clear cells": "Очистить ячейки", + "Toggle header cell": "Переключить ячейку заголовка", + "Toggle header column": "Переключить столбец заголовка", + "Toggle header row": "Переключить строку заголовка", + "Move column left": "Переместить столбец влево", + "Move column right": "Переместить столбец вправо", + "Move row down": "Переместить строку вниз", + "Move row up": "Переместить строку вверх", + "Sort A → Z": "Сортировать A → Я", + "Sort Z → A": "Сортировать Я → A", "Info": "Информация", "Note": "Примечание", "Success": "Успешно", @@ -348,6 +365,8 @@ "Create block quote.": "Создать блок цитирования.", "Insert code snippet.": "Вставить фрагмент кода.", "Insert horizontal rule divider": "Вставить горизонтальный разделитель", + "Page break": "Разрыв страницы", + "Insert a page break for printing.": "Вставить разрыв страницы для печати.", "Upload any image from your device.": "Загрузить любое изображение с вашего устройства.", "Upload any video from your device.": "Загрузить любое видео с вашего устройства.", "Upload any audio from your device.": "Загрузите любой аудиофайл с вашего устройства.", @@ -392,6 +411,10 @@ "Write...": "Напишите...", "Column count": "Количество столбцов", "{{count}} Columns": "{count, plural, one{# столбец} few{# столбца} many{# столбцов} other{# столбца}}", + "{{count}} command available_one": "Доступна 1 команда", + "{{count}} command available_other": "Доступно {{count}} команд", + "{{count}} result available_one": "Доступен 1 результат", + "{{count}} result available_other": "Доступно {{count}} результатов", "Equal columns": "Равные столбцы", "Left sidebar": "Левая боковая панель", "Right sidebar": "Правая боковая панель", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "Доступна версия {{latestVersion}}", "Default page edit mode": "Режим редактирования страницы по умолчанию", "Choose your preferred page edit mode. Avoid accidental edits.": "Выберите предпочитаемый режим редактирования страницы. Избегайте случайных изменений.", + "Choose {{format}} file": "Выберите файл {{format}}", "Reading": "Чтение", "Delete member": "Удалить участника", "Member deleted successfully": "Участник успешно удалён", @@ -565,6 +589,8 @@ "Move to trash": "Переместить в корзину", "Move this page to trash?": "Переместить эту страницу в корзину?", "Restore page": "Восстановить страницу", + "Permanently delete": "Удалить навсегда", + "{{name}} moved this page to Trash {{time}}.": "{{name}} переместил(а) эту страницу в корзину {{time}}.", "Page moved to trash": "Страница перемещена в корзину", "Page restored successfully": "Страница успешно восстановлена", "Deleted by": "Удалено пользователем", @@ -608,25 +634,21 @@ "Image exceeds 10MB limit.": "Изображение превышает предел 10MB.", "Image removed successfully": "Изображение успешно удалено", "API key": "API ключ", - "API key created successfully": "API ключ успешно создан", "API keys": "API ключи", "API management": "Управление API", - "Are you sure you want to revoke this API key": "Вы уверены, что хотите отозвать этот API ключ", - "Create API Key": "Создать API ключ", "Custom expiration date": "Пользовательская дата срока действия", "Enter a descriptive token name": "Введите понятное имя токена", "Expiration": "Срок действия", "Expired": "Истек", "Expires": "Истекает", - "I've saved my API key": "Я сохранил мой API ключ", "Last use": "Последнее использование", "No API keys found": "API ключи не найдены", "No expiration": "Не истекает", - "Revoke API key": "Отозвать API ключ", "Revoked successfully": "Отозван успешно", "Select expiration date": "Выберете срок действия", "This action cannot be undone. Any applications using this API key will stop working.": "Это действие необратимо. Любые приложения, использующие этот API ключ, перестанут работать.", - "Update API key": "Обновить API ключ", + "Update": "Обновить", + "Update {{credential}}": "Обновить {{credential}}", "Manage API keys for all users in the workspace": "Управлять API ключами для всех пользователей в рабочей области", "Restrict API key creation to admins": "Ограничить создание API-ключей только администраторами.", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Только администраторы и владельцы могут создавать новые API-ключи. Существующие ключи участников продолжат работать.", @@ -858,9 +880,12 @@ "AI Chat": "Чат с ИИ", "Analyze for insights": "Проанализировать и получить выводы", "Ask anything...": "Спросите что угодно...", + "Assistant said:": "Ассистент ответил:", "Chat history": "История чатов", "Chat name": "Название чата", + "Chat transcript": "Расшифровка чата", "Close": "Закрыть", + "Copy assistant response": "Скопировать ответ ассистента", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "Не удалось загрузить чат. Произошла ошибка.", "Failed to render this message.": "Не удалось отобразить это сообщение.", @@ -870,9 +895,17 @@ "No chats found": "Чаты не найдены", "No conversations yet": "Пока нет разговоров", "Open full page": "Открыть полную страницу", + "Scroll to bottom": "Прокрутить вниз", + "You said:": "Вы сказали:", "Previous 7 days": "Предыдущие 7 дней", "Previous 30 days": "Предыдущие 30 дней", "Search chats...": "Поиск чатов...", + "Search chats": "Поиск чатов", + "Ask anything... Use @ to mention pages": "Спросите что угодно... Используйте @, чтобы упомянуть страницы", + "Ask anything or search your workspace": "Спросите что угодно или выполните поиск по рабочему пространству", + "Welcome to {{name}}": "Добро пожаловать в {{name}}", + "Add files": "Добавить файлы", + "Mention a page": "Упомянуть страницу", "Start a new chat to see it here.": "Начните новый чат, чтобы увидеть его здесь.", "Summarize this page": "Суммировать эту страницу", "Toggle AI Chat": "Переключить чат с ИИ", @@ -880,5 +913,176 @@ "Try a different search term.": "Попробуйте другой поисковый запрос.", "Try again": "Попробовать снова", "Untitled chat": "Чат без названия", - "What can I help you with?": "Чем я могу вам помочь?" + "What can I help you with?": "Чем я могу вам помочь?", + "Are you sure you want to revoke this {{credential}}": "Вы уверены, что хотите отозвать этот {{credential}}", + "Automatically provision users and groups from your identity provider via SCIM.": "Автоматически предоставляйте доступ пользователям и группам из вашего провайдера удостоверений через SCIM.", + "Configure your identity provider with this URL to provision users and groups.": "Настройте ваш провайдер удостоверений с этим URL для предоставления доступа пользователям и группам.", + "Create {{credential}}": "Создать {{credential}}", + "{{credential}} created": "{{credential}} создан", + "{{credential}} created successfully": "{{credential}} успешно создан", + "Created by": "Создан", + "Custom": "Пользовательский", + "Enable SCIM": "Включить SCIM", + "Enter a descriptive name": "Введите понятное имя", + "I've saved my {{credential}}": "Я сохранил свой {{credential}}", + "Important": "Важно", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "Обязательно скопируйте ваш {{credential}} сейчас. Позже вы не сможете увидеть его снова!", + "Never": "Никогда", + "Revoke {{credential}}": "Отозвать {{credential}}", + "SCIM endpoint URL": "URL конечной точки SCIM", + "SCIM provisioning": "SCIM-подготовка учетных записей", + "SCIM takes precedence over SSO group sync while enabled.": "Пока SCIM включен, он имеет приоритет над синхронизацией групп через SSO.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "Вы достигли максимального количества токенов SCIM: {{max}}. Удалите существующий токен, чтобы создать новый.", + "SCIM token": "Токен SCIM", + "SCIM tokens": "Токены SCIM", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "Это действие нельзя отменить. Ваш провайдер удостоверений немедленно прекратит синхронизацию.", + "Toggle SCIM provisioning": "Переключить подготовку учетных записей SCIM", + "Token": "Токен", + "Page menu": "Меню страницы", + "Expand": "Развернуть", + "Collapse": "Свернуть", + "Comment menu": "Меню комментария", + "Group menu": "Меню группы", + "Show hidden breadcrumbs": "Показать скрытые хлебные крошки", + "Breadcrumbs": "Хлебные крошки", + "Page actions": "Действия со страницей", + "Pick emoji": "Выбрать эмодзи", + "Template menu": "Меню шаблона", + "Use": "Использовать", + "Use template": "Использовать шаблон", + "Preview template: {{title}}": "Предпросмотр шаблона: {{title}}", + "Use a template": "Использовать шаблон", + "Search templates...": "Поиск шаблонов...", + "Search spaces...": "Поиск пространств...", + "No templates found": "Шаблоны не найдены", + "No spaces found": "Пространства не найдены", + "Browse all templates": "Просмотреть все шаблоны", + "This space": "Это пространство", + "All templates": "Все шаблоны", + "Global": "Глобально", + "New template": "Новый шаблон", + "Edit template": "Редактировать шаблон", + "Are you sure you want to delete this template?": "Вы уверены, что хотите удалить этот шаблон?", + "Template scope updated": "Область действия шаблона обновлена", + "Choose which space this template belongs to": "Выберите, к какому пространству относится этот шаблон", + "Scope": "Область действия", + "Select scope": "Выберите область действия", + "Title": "Заголовок", + "Saving...": "Сохранение...", + "Saved": "Сохранено", + "Save failed. Retry": "Не удалось сохранить. Повторите попытку", + "By {{name}}": "От {{name}}", + "Updated {{time}}": "Обновлено {{time}}", + "Choose destination": "Выберите место назначения", + "Search pages and spaces...": "Поиск страниц и пространств...", + "No results found": "Результаты не найдены", + "You don't have permission to create pages here": "У вас нет прав на создание страниц здесь", + "Chat menu": "Меню чата", + "API key menu": "Меню API-ключа", + "Jump to comment selection": "Перейти к выбору комментария", + "Slash commands": "Команды со слешем", + "Mention suggestions": "Подсказки упоминаний", + "Link suggestions": "Подсказки ссылок", + "Diagram editor": "Редактор диаграмм", + "Add comment": "Добавить комментарий", + "Find and replace": "Найти и заменить", + "Main navigation": "Основная навигация", + "Space navigation": "Навигация по пространству", + "Settings navigation": "Навигация по настройкам", + "AI navigation": "Навигация ИИ", + "Breadcrumb": "Хлебная крошка", + "Synced block": "Синхронизированный блок", + "Create a block that stays in sync across pages.": "Создайте блок, который будет синхронизироваться между страницами.", + "Editing original": "Редактирование оригинала", + "Copy synced block": "Скопировать синхронизированный блок", + "Unsync": "Не синхронизировать", + "Delete synced block": "Удалить синхронизированный блок", + "Synced to {{count}} other page_one": "Синхронизировано с {{count}} с другой страницей", + "Synced to {{count}} other page_other": "Синхронизировано с {{count}} с другими страницами", + "ORIGINAL": "ОРИГИНАЛ", + "THIS PAGE": "ЭТА СТРАНИЦА", + "No pages": "Нет страниц", + "The original synced block no longer exists": "Исходный синхронизированный блок больше не существует", + "You don't have access to this synced block": "У вас нет доступа к этому синхронизированному блоку", + "Failed to load this synced block": "Не удалось загрузить этот синхронизированный блок", + "Fixed editor toolbar": "Закреплённая панель инструментов редактора", + "Show a formatting toolbar above the editor with quick access to common actions.": "Показывать панель форматирования над редактором для быстрого доступа к часто используемым действиям.", + "Toggle fixed editor toolbar": "Переключить закреплённую панель инструментов редактора", + "Normal text": "Обычный текст", + "More inline formatting": "Больше вариантов встроенного форматирования", + "Subscript": "Подстрочный", + "Superscript": "Надстрочный", + "Inline code": "Встроенный код", + "Insert media": "Вставить медиа", + "Mention": "Упоминание", + "Emoji": "Эмодзи", + "Columns": "Столбцы", + "More inserts": "Больше вариантов вставки", + "Embeds": "Встраивания", + "Diagrams": "Диаграммы", + "Advanced": "Дополнительно", + "Utility": "Служебное", + "Decrease indent": "Уменьшить отступ", + "Increase indent": "Увеличить отступ", + "Clear formatting": "Очистить форматирование", + "Code block": "Блок кода", + "Experimental": "Экспериментальное", + "Strikethrough": "Зачеркивание", + "Undo": "Отменить", + "Redo": "Повторить", + "Backlinks": "Обратные ссылки", + "Last updated by": "Последний изменивший", + "Last updated": "Последнее обновление", + "Stats": "Статистика", + "Word count": "Количество слов", + "Characters": "Символы", + "Incoming links": "Входящие ссылки", + "Outgoing links": "Исходящие ссылки", + "Incoming links ({{count}})": "Входящие ссылки ({{count}})", + "Outgoing links ({{count}})": "Исходящие ссылки ({{count}})", + "No pages link here yet.": "На эту страницу пока что нет ссылок.", + "This page doesn't link to other pages yet.": "Эта страница пока не содержит ссылок на другие страницы.", + "Verified until {{date}}": "Подтверждено до: {{date}}", + "Labels": "Метки", + "Add label": "Добавить метку", + "No labels yet": "Меток пока нет", + "Already added": "Уже добавлено", + "Invalid label name": "Недопустимое имя метки", + "No matches": "Совпадений нет", + "Search or create…": "Найти или создать…", + "Remove label {{name}}": "Удалить метку {{name}}", + "Failed to add label": "Не удалось добавить метку", + "Failed to remove label": "Не удалось удалить метку", + "No pages with this label": "Нет страниц с этой меткой", + "Pages tagged with this label will appear here.": "Здесь будут отображаться страницы с этой меткой.", + "No pages match your search.": "Нет страниц, соответствующих вашему запросу.", + "Updated {{date}}": "Обновлено {{date}}", + "Cell actions": "Действия с ячейкой", + "Column actions": "Действия со столбцом", + "Row actions": "Действия со строкой", + "Filter": "Фильтр", + "Page title": "Заголовок страницы", + "Page content": "Содержимое страницы", + "Member actions": "Действия с участником", + "Toggle password visibility": "Переключить видимость пароля", + "Send comment": "Отправить комментарий", + "Token actions": "Действия с токеном", + "Template settings": "Настройки шаблона", + "Edit diagram": "Редактировать диаграмму", + "Edit embed": "Редактировать встраивание", + "Edit drawing": "Редактировать рисунок", + "Delete equation": "Удалить уравнение", + "Invite actions": "Действия с приглашением", + "Get started": "Начать", + "* indicates required fields": "* обозначает обязательные поля", + "List of spaces in this workspace": "Список пространств в этом рабочем пространстве", + "Active sessions": "Активные сеансы", + "Add {{name}} to favorites": "Добавить {{name}} в избранное", + "Remove {{name}} from favorites": "Удалить {{name}} из избранного", + "Added to favorites": "Добавлено в избранное", + "Removed from favorites": "Удалено из избранного", + "Added {{name}} to favorites": "{{name}} добавлено в избранное", + "Removed {{name}} from favorites": "{{name}} удалено из избранного", + "Page menu for {{name}}": "Меню страницы для {{name}}", + "Create subpage of {{name}}": "Создать подстраницу для {{name}}" } diff --git a/apps/client/public/locales/uk-UA/translation.json b/apps/client/public/locales/uk-UA/translation.json index aff25b4ae..6144df469 100644 --- a/apps/client/public/locales/uk-UA/translation.json +++ b/apps/client/public/locales/uk-UA/translation.json @@ -71,6 +71,7 @@ "Export": "Експорт", "Failed to create page": "Не вдалося створити сторінку", "Failed to delete page": "Не вдалося видалити сторінку", + "Failed to restore page": "Не вдалося відновити сторінку", "Failed to fetch recent pages": "Не вдалося отримати нещодавні сторінки", "Failed to import pages": "Не вдалося імпортувати сторінки", "Failed to load page. An error occurred.": "Не вдалося завантажити сторінку. Сталася помилка.", @@ -276,6 +277,9 @@ "Align left": "По лівому краю", "Align right": "По правому краю", "Align center": "По центру", + "Alt text": "Альтернативний текст", + "Describe this for accessibility.": "Опишіть це для доступності.", + "Add a description": "Додати опис", "Justify": "По ширині", "Merge cells": "Об'єднати комірки", "Split cell": "Розділити комірку", @@ -286,6 +290,19 @@ "Add row above": "Додати рядок вище", "Add row below": "Додати рядок нижче", "Delete table": "Видалити таблицю", + "Add column left": "Додати стовпець ліворуч", + "Add column right": "Додати стовпець праворуч", + "Clear cell": "Очистити комірку", + "Clear cells": "Очистити комірки", + "Toggle header cell": "Перемкнути комірку заголовка", + "Toggle header column": "Перемкнути стовпець заголовка", + "Toggle header row": "Перемкнути рядок заголовка", + "Move column left": "Перемістити стовпець ліворуч", + "Move column right": "Перемістити стовпець праворуч", + "Move row down": "Перемістити рядок вниз", + "Move row up": "Перемістити рядок вгору", + "Sort A → Z": "Сортувати A → Z", + "Sort Z → A": "Сортувати Z → A", "Info": "Інформація", "Note": "Примітка", "Success": "Успішно", @@ -348,6 +365,8 @@ "Create block quote.": "Створити блок цитування.", "Insert code snippet.": "Вставити фрагмент коду.", "Insert horizontal rule divider": "Вставити горизонтальний роздільник", + "Page break": "Розрив сторінки", + "Insert a page break for printing.": "Вставте розрив сторінки для друку.", "Upload any image from your device.": "Завантажити будь-яке зображення з вашого пристрою.", "Upload any video from your device.": "Завантажити будь-яке відео з вашого пристрою.", "Upload any audio from your device.": "Завантажте будь-який аудіофайл зі свого пристрою.", @@ -392,6 +411,10 @@ "Write...": "Напишіть...", "Column count": "Кількість колонок", "{{count}} Columns": "{count, plural, one{# колонка} few{# колонки} many{# колонок} other{# колонки}}", + "{{count}} command available_one": "Доступна 1 команда", + "{{count}} command available_other": "Доступно {{count}} команд", + "{{count}} result available_one": "Доступний 1 результат", + "{{count}} result available_other": "Доступно {{count}} результатів", "Equal columns": "Рівні колонки", "Left sidebar": "Ліва бічна панель", "Right sidebar": "Права бічна панель", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "Доступна версія {{latestVersion}}", "Default page edit mode": "Режим редагування сторінки за замовчуванням", "Choose your preferred page edit mode. Avoid accidental edits.": "Виберіть бажаний режим редагування сторінки. Уникайте випадкових редагувань.", + "Choose {{format}} file": "Виберіть файл {{format}}", "Reading": "Читання", "Delete member": "Видалити учасника", "Member deleted successfully": "Учасника успішно видалено", @@ -565,6 +589,8 @@ "Move to trash": "Перемістити в кошик", "Move this page to trash?": "Перемістити цю сторінку до кошика?", "Restore page": "Відновити сторінку", + "Permanently delete": "Видалити назавжди", + "{{name}} moved this page to Trash {{time}}.": "{{name}} перемістив цю сторінку до кошика {{time}}.", "Page moved to trash": "Сторінку переміщено в кошик", "Page restored successfully": "Сторінку успішно відновлено", "Deleted by": "Видалив", @@ -608,25 +634,21 @@ "Image exceeds 10MB limit.": "Зображення має займати менше, ніж 10 МБ.", "Image removed successfully": "Зображення видалено", "API key": "Ключ API", - "API key created successfully": "Ключ API успішно створено", "API keys": "Ключі API", "API management": "Управління API", - "Are you sure you want to revoke this API key": "Ви впевнені, що хочете відкликати цей ключ API", - "Create API Key": "Створити ключ API", "Custom expiration date": "Користувацька дата закінчення", "Enter a descriptive token name": "Введіть описову назву токена", "Expiration": "Термін дії", "Expired": "Закінчився", "Expires": "Закінчується", - "I've saved my API key": "Я зберіг свій ключ API", "Last use": "Останнє використання", "No API keys found": "Ключі API не знайдено", "No expiration": "Без терміну дії", - "Revoke API key": "Відкликати ключ API", "Revoked successfully": "Успішно відкликано", "Select expiration date": "Виберіть дату закінчення", "This action cannot be undone. Any applications using this API key will stop working.": "Цю дію не можна скасувати. Будь-які додатки, що використовують цей ключ API, перестануть працювати.", - "Update API key": "Оновити ключ API", + "Update": "Оновити", + "Update {{credential}}": "Оновити {{credential}}", "Manage API keys for all users in the workspace": "Керувати ключами API для всіх користувачів у робочій області", "Restrict API key creation to admins": "Обмежити створення API-ключів лише для адміністраторів", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "Тільки адміністратори та власники можуть створювати нові API-ключі. Існуючі ключі учасників і надалі працюватимуть.", @@ -858,9 +880,12 @@ "AI Chat": "AI-чат", "Analyze for insights": "Проаналізувати для отримання висновків", "Ask anything...": "Запитайте що завгодно...", + "Assistant said:": "Помічник сказав:", "Chat history": "Історія чатів", "Chat name": "Назва чату", + "Chat transcript": "Стенограма чату", "Close": "Закрити", + "Copy assistant response": "Копіювати відповідь помічника", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "Не вдалося завантажити чат. Сталася помилка.", "Failed to render this message.": "Не вдалося відобразити це повідомлення.", @@ -870,9 +895,17 @@ "No chats found": "Чатів не знайдено", "No conversations yet": "Розмов поки немає", "Open full page": "Відкрити повну сторінку", + "Scroll to bottom": "Прокрутити вниз", + "You said:": "Ви сказали:", "Previous 7 days": "Попередні 7 днів", "Previous 30 days": "Попередні 30 днів", "Search chats...": "Шукати чати...", + "Search chats": "Шукати чати", + "Ask anything... Use @ to mention pages": "Запитайте будь-що... Використовуйте @, щоб згадувати сторінки", + "Ask anything or search your workspace": "Запитайте будь-що або шукайте у своєму робочому просторі", + "Welcome to {{name}}": "Ласкаво просимо до {{name}}", + "Add files": "Додати файли", + "Mention a page": "Згадати сторінку", "Start a new chat to see it here.": "Почніть новий чат, щоб побачити його тут.", "Summarize this page": "Підсумувати цю сторінку", "Toggle AI Chat": "Перемкнути AI-чат", @@ -880,5 +913,176 @@ "Try a different search term.": "Спробуйте інший пошуковий запит.", "Try again": "Спробувати ще раз", "Untitled chat": "Чат без назви", - "What can I help you with?": "Чим я можу вам допомогти?" + "What can I help you with?": "Чим я можу вам допомогти?", + "Are you sure you want to revoke this {{credential}}": "Ви впевнені, що хочете відкликати цей {{credential}}", + "Automatically provision users and groups from your identity provider via SCIM.": "Автоматично надавайте користувачів і групи від вашого постачальника ідентифікації через SCIM.", + "Configure your identity provider with this URL to provision users and groups.": "Налаштуйте свого постачальника ідентифікації за допомогою цієї URL-адреси для надання користувачів і груп.", + "Create {{credential}}": "Створити {{credential}}", + "{{credential}} created": "{{credential}} створено", + "{{credential}} created successfully": "{{credential}} успішно створено", + "Created by": "Створено", + "Custom": "Користувацький", + "Enable SCIM": "Увімкнути SCIM", + "Enter a descriptive name": "Введіть описову назву", + "I've saved my {{credential}}": "Я зберіг(ла) свій {{credential}}", + "Important": "Важливо", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "Обов’язково скопіюйте свій {{credential}} зараз. Ви більше не зможете побачити його знову!", + "Never": "Ніколи", + "Revoke {{credential}}": "Відкликати {{credential}}", + "SCIM endpoint URL": "URL-адреса кінцевої точки SCIM", + "SCIM provisioning": "Надання SCIM", + "SCIM takes precedence over SSO group sync while enabled.": "SCIM має пріоритет над синхронізацією груп SSO, коли його ввімкнено.", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "Ви досягли максимальної кількості токенів SCIM: {{max}}. Видаліть наявний токен, щоб створити новий.", + "SCIM token": "Токен SCIM", + "SCIM tokens": "Токени SCIM", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "Цю дію не можна скасувати. Ваш постачальник ідентифікації негайно припинить синхронізацію.", + "Toggle SCIM provisioning": "Перемкнути надання SCIM", + "Token": "Токен", + "Page menu": "Меню сторінки", + "Expand": "Розгорнути", + "Collapse": "Згорнути", + "Comment menu": "Меню коментаря", + "Group menu": "Меню групи", + "Show hidden breadcrumbs": "Показати приховані \"хлібні крихти\"", + "Breadcrumbs": "\"Хлібні крихти\"", + "Page actions": "Дії сторінки", + "Pick emoji": "Вибрати емодзі", + "Template menu": "Меню шаблону", + "Use": "Використати", + "Use template": "Використати шаблон", + "Preview template: {{title}}": "Попередній перегляд шаблону: {{title}}", + "Use a template": "Використати шаблон", + "Search templates...": "Шукати шаблони...", + "Search spaces...": "Шукати простори...", + "No templates found": "Шаблони не знайдено", + "No spaces found": "Простори не знайдено", + "Browse all templates": "Переглянути всі шаблони", + "This space": "Цей простір", + "All templates": "Усі шаблони", + "Global": "Глобальний", + "New template": "Новий шаблон", + "Edit template": "Редагувати шаблон", + "Are you sure you want to delete this template?": "Ви впевнені, що хочете видалити цей шаблон?", + "Template scope updated": "Область дії шаблону оновлено", + "Choose which space this template belongs to": "Виберіть, до якого простору належить цей шаблон", + "Scope": "Область дії", + "Select scope": "Вибрати область дії", + "Title": "Назва", + "Saving...": "Збереження...", + "Saved": "Збережено", + "Save failed. Retry": "Не вдалося зберегти. Повторіть спробу", + "By {{name}}": "Від {{name}}", + "Updated {{time}}": "Оновлено {{time}}", + "Choose destination": "Вибрати місце призначення", + "Search pages and spaces...": "Шукати сторінки та простори...", + "No results found": "Результати не знайдено", + "You don't have permission to create pages here": "У вас немає дозволу на створення сторінок тут", + "Chat menu": "Меню чату", + "API key menu": "Меню ключа API", + "Jump to comment selection": "Перейти до вибору коментаря", + "Slash commands": "Слеш-команди", + "Mention suggestions": "Підказки згадок", + "Link suggestions": "Підказки посилань", + "Diagram editor": "Редактор діаграм", + "Add comment": "Додати коментар", + "Find and replace": "Знайти й замінити", + "Main navigation": "Основна навігація", + "Space navigation": "Навігація простору", + "Settings navigation": "Навігація налаштувань", + "AI navigation": "Навігація AI", + "Breadcrumb": "Хлібна крихта", + "Synced block": "Синхронізований блок", + "Create a block that stays in sync across pages.": "Створіть блок, який синхронізується між сторінками.", + "Editing original": "Редагування оригіналу", + "Copy synced block": "Скопіювати синхронізований блок", + "Unsync": "Скасувати синхронізацію", + "Delete synced block": "Видалити синхронізований блок", + "Synced to {{count}} other page_one": "Синхронізовано з {{count}} іншою сторінкою", + "Synced to {{count}} other page_other": "Синхронізовано з {{count}} іншими сторінками", + "ORIGINAL": "ОРИГІНАЛ", + "THIS PAGE": "ЦЯ СТОРІНКА", + "No pages": "Немає сторінок", + "The original synced block no longer exists": "Оригінальний синхронізований блок більше не існує", + "You don't have access to this synced block": "У вас немає доступу до цього синхронізованого блоку", + "Failed to load this synced block": "Не вдалося завантажити цей синхронізований блок", + "Fixed editor toolbar": "Закріплена панель інструментів редактора", + "Show a formatting toolbar above the editor with quick access to common actions.": "Показувати панель форматування над редактором для швидкого доступу до поширених дій.", + "Toggle fixed editor toolbar": "Перемкнути закріплену панель інструментів редактора", + "Normal text": "Звичайний текст", + "More inline formatting": "Більше вбудованого форматування", + "Subscript": "Нижній індекс", + "Superscript": "Верхній індекс", + "Inline code": "Вбудований код", + "Insert media": "Вставити медіа", + "Mention": "Згадка", + "Emoji": "Емодзі", + "Columns": "Стовпці", + "More inserts": "Більше вставок", + "Embeds": "Вбудовування", + "Diagrams": "Діаграми", + "Advanced": "Додатково", + "Utility": "Службові", + "Decrease indent": "Зменшити відступ", + "Increase indent": "Збільшити відступ", + "Clear formatting": "Очистити форматування", + "Code block": "Блок коду", + "Experimental": "Експериментальне", + "Strikethrough": "Закреслення", + "Undo": "Скасувати", + "Redo": "Повторити", + "Backlinks": "Зворотні посилання", + "Last updated by": "Востаннє оновив", + "Last updated": "Останнє оновлення", + "Stats": "Статистика", + "Word count": "Кількість слів", + "Characters": "Символи", + "Incoming links": "Вхідні посилання", + "Outgoing links": "Вихідні посилання", + "Incoming links ({{count}})": "Вхідні посилання ({{count}})", + "Outgoing links ({{count}})": "Вихідні посилання ({{count}})", + "No pages link here yet.": "Поки що жодна сторінка не посилається сюди.", + "This page doesn't link to other pages yet.": "Ця сторінка ще не містить посилань на інші сторінки.", + "Verified until {{date}}": "Перевірено до {{date}}", + "Labels": "Мітки", + "Add label": "Додати мітку", + "No labels yet": "Міток поки немає", + "Already added": "Уже додано", + "Invalid label name": "Некоректна назва мітки", + "No matches": "Немає збігів", + "Search or create…": "Шукати або створити…", + "Remove label {{name}}": "Видалити мітку {{name}}", + "Failed to add label": "Не вдалося додати мітку", + "Failed to remove label": "Не вдалося видалити мітку", + "No pages with this label": "Немає сторінок із цією міткою", + "Pages tagged with this label will appear here.": "Тут з’являться сторінки, позначені цією міткою.", + "No pages match your search.": "Немає сторінок, що відповідають вашому запиту.", + "Updated {{date}}": "Оновлено {{date}}", + "Cell actions": "Дії з коміркою", + "Column actions": "Дії зі стовпцем", + "Row actions": "Дії з рядком", + "Filter": "Фільтр", + "Page title": "Назва сторінки", + "Page content": "Вміст сторінки", + "Member actions": "Дії з учасником", + "Toggle password visibility": "Перемкнути видимість пароля", + "Send comment": "Надіслати коментар", + "Token actions": "Дії з токеном", + "Template settings": "Налаштування шаблону", + "Edit diagram": "Редагувати діаграму", + "Edit embed": "Редагувати вбудований елемент", + "Edit drawing": "Редагувати рисунок", + "Delete equation": "Видалити рівняння", + "Invite actions": "Дії із запрошенням", + "Get started": "Почати", + "* indicates required fields": "* позначає обов’язкові поля", + "List of spaces in this workspace": "Список просторів у цьому робочому просторі", + "Active sessions": "Активні сеанси", + "Add {{name}} to favorites": "Додати {{name}} до обраного", + "Remove {{name}} from favorites": "Видалити {{name}} з обраного", + "Added to favorites": "Додано до обраного", + "Removed from favorites": "Видалено з обраного", + "Added {{name}} to favorites": "{{name}} додано до обраного", + "Removed {{name}} from favorites": "{{name}} видалено з обраного", + "Page menu for {{name}}": "Меню сторінки для {{name}}", + "Create subpage of {{name}}": "Створити підсторінку для {{name}}" } diff --git a/apps/client/public/locales/zh-CN/translation.json b/apps/client/public/locales/zh-CN/translation.json index d8fd340d3..40cd5c1ba 100644 --- a/apps/client/public/locales/zh-CN/translation.json +++ b/apps/client/public/locales/zh-CN/translation.json @@ -71,6 +71,7 @@ "Export": "导出", "Failed to create page": "创建页面失败", "Failed to delete page": "删除页面失败", + "Failed to restore page": "恢复页面失败", "Failed to fetch recent pages": "获取最近页面失败", "Failed to import pages": "导入页面失败", "Failed to load page. An error occurred.": "页面加载失败。发生了一个错误。", @@ -276,6 +277,9 @@ "Align left": "靠左对齐", "Align right": "靠右对齐", "Align center": "居中对齐", + "Alt text": "替代文本", + "Describe this for accessibility.": "为无障碍访问添加描述。", + "Add a description": "添加描述", "Justify": "两端对齐", "Merge cells": "合并单元格", "Split cell": "分割单元格", @@ -286,6 +290,19 @@ "Add row above": "在上方添加行", "Add row below": "在下方插入行", "Delete table": "删除表格", + "Add column left": "在左侧添加列", + "Add column right": "在右侧添加列", + "Clear cell": "清空单元格", + "Clear cells": "清空单元格", + "Toggle header cell": "切换标题单元格", + "Toggle header column": "切换标题列", + "Toggle header row": "切换标题行", + "Move column left": "左移列", + "Move column right": "右移列", + "Move row down": "下移行", + "Move row up": "上移行", + "Sort A → Z": "按 A → Z 排序", + "Sort Z → A": "按 Z → A 排序", "Info": "信息", "Note": "注意", "Success": "成功", @@ -348,6 +365,8 @@ "Create block quote.": "创建引用块", "Insert code snippet.": "插入代码片段", "Insert horizontal rule divider": "插入水平分割线", + "Page break": "分页符", + "Insert a page break for printing.": "插入一个用于打印的分页符。", "Upload any image from your device.": "从设备上传任何图像", "Upload any video from your device.": "从设备上传任何视频", "Upload any audio from your device.": "从您的设备上传任意音频文件。", @@ -392,6 +411,10 @@ "Write...": "写点内容...", "Column count": "列数", "{{count}} Columns": "{{count}} 列", + "{{count}} command available_one": "有 1 个可用命令", + "{{count}} command available_other": "有 {{count}} 个可用命令", + "{{count}} result available_one": "有 1 个可用结果", + "{{count}} result available_other": "有 {{count}} 个可用结果", "Equal columns": "等宽列", "Left sidebar": "左侧边栏", "Right sidebar": "右侧边栏", @@ -416,6 +439,7 @@ "{{latestVersion}} is available": "{{latestVersion}} 可用", "Default page edit mode": "默认页面编辑模式", "Choose your preferred page edit mode. Avoid accidental edits.": "选择您偏好的页面编辑模式。避免意外编辑。", + "Choose {{format}} file": "选择 {{format}} 文件", "Reading": "阅读", "Delete member": "删除成员", "Member deleted successfully": "成员删除成功", @@ -565,6 +589,8 @@ "Move to trash": "移至回收站", "Move this page to trash?": "将此页面移至垃圾箱?", "Restore page": "恢复页面", + "Permanently delete": "永久删除", + "{{name}} moved this page to Trash {{time}}.": "{{name}} 于 {{time}} 将此页面移至回收站。", "Page moved to trash": "页面已移至回收站", "Page restored successfully": "页面恢复成功", "Deleted by": "删除者", @@ -608,25 +634,21 @@ "Image exceeds 10MB limit.": "图片超过10MB限制。", "Image removed successfully": "图片删除成功", "API key": "API密钥", - "API key created successfully": "API密钥创建成功", "API keys": "API密钥", "API management": "API管理", - "Are you sure you want to revoke this API key": "确定要撤销此API密钥吗", - "Create API Key": "创建API密钥", "Custom expiration date": "自定义到期日期", "Enter a descriptive token name": "输入描述性令牌名称", "Expiration": "到期", "Expired": "已过期", "Expires": "到期", - "I've saved my API key": "我已保存我的API密钥", "Last use": "上次使用", "No API keys found": "找不到API密钥", "No expiration": "无到期", - "Revoke API key": "撤销API密钥", "Revoked successfully": "撤销成功", "Select expiration date": "选择到期日期", "This action cannot be undone. Any applications using this API key will stop working.": "此操作无法撤销。使用此API密钥的任何应用程序将停止工作。", - "Update API key": "更新API密钥", + "Update": "更新", + "Update {{credential}}": "更新{{credential}}", "Manage API keys for all users in the workspace": "管理工作空间中所有用户的API密钥", "Restrict API key creation to admins": "仅限管理员创建 API 密钥。", "Only admins and owners can create new API keys. Existing member keys will continue to work.": "只有管理员和所有者可以创建新的 API 密钥。现有成员密钥将继续有效。", @@ -858,9 +880,12 @@ "AI Chat": "AI 聊天", "Analyze for insights": "分析并获取洞察", "Ask anything...": "随便问点什么...", + "Assistant said:": "助手说:", "Chat history": "聊天记录", "Chat name": "聊天名称", + "Chat transcript": "聊天记录", "Close": "关闭", + "Copy assistant response": "复制助手回复", "Docmost AI": "Docmost AI", "Failed to load chat. An error occurred.": "加载聊天失败。发生错误。", "Failed to render this message.": "渲染此消息失败。", @@ -870,9 +895,17 @@ "No chats found": "未找到聊天", "No conversations yet": "暂无对话", "Open full page": "打开完整页面", + "Scroll to bottom": "滚动到底部", + "You said:": "你说:", "Previous 7 days": "前 7 天", "Previous 30 days": "前 30 天", "Search chats...": "搜索聊天...", + "Search chats": "搜索聊天", + "Ask anything... Use @ to mention pages": "询问任何内容……使用 @ 提及页面", + "Ask anything or search your workspace": "询问任何问题或搜索你的工作区", + "Welcome to {{name}}": "欢迎使用 {{name}}", + "Add files": "添加文件", + "Mention a page": "提及页面", "Start a new chat to see it here.": "开始新的聊天后会显示在这里。", "Summarize this page": "总结此页面", "Toggle AI Chat": "切换 AI 聊天", @@ -880,5 +913,176 @@ "Try a different search term.": "请尝试其他搜索词。", "Try again": "重试", "Untitled chat": "未命名聊天", - "What can I help you with?": "我能帮您做什么?" + "What can I help you with?": "我能帮您做什么?", + "Are you sure you want to revoke this {{credential}}": "确定要撤销此{{credential}}吗", + "Automatically provision users and groups from your identity provider via SCIM.": "通过 SCIM 从您的身份提供商自动预配用户和群组。", + "Configure your identity provider with this URL to provision users and groups.": "使用此 URL 配置您的身份提供商以预配用户和群组。", + "Create {{credential}}": "创建{{credential}}", + "{{credential}} created": "已创建{{credential}}", + "{{credential}} created successfully": "已成功创建{{credential}}", + "Created by": "创建者", + "Custom": "自定义", + "Enable SCIM": "启用 SCIM", + "Enter a descriptive name": "输入描述性名称", + "I've saved my {{credential}}": "我已保存我的{{credential}}", + "Important": "重要", + "Make sure to copy your {{credential}} now. You won't be able to see it again!": "请务必立即复制您的{{credential}}。之后您将无法再次查看!", + "Never": "从不", + "Revoke {{credential}}": "撤销{{credential}}", + "SCIM endpoint URL": "SCIM 端点 URL", + "SCIM provisioning": "SCIM 预配", + "SCIM takes precedence over SSO group sync while enabled.": "启用后,SCIM 的优先级高于 SSO 群组同步。", + "You have reached the maximum of {{max}} SCIM tokens. Delete an existing token to create a new one.": "您已达到 {{max}} 个 SCIM 令牌的上限。请删除一个现有令牌以创建新令牌。", + "SCIM token": "SCIM 令牌", + "SCIM tokens": "SCIM 令牌", + "This action cannot be undone. Your identity provider will stop syncing immediately.": "此操作无法撤销。您的身份提供商将立即停止同步。", + "Toggle SCIM provisioning": "切换 SCIM 预配", + "Token": "令牌", + "Page menu": "页面菜单", + "Expand": "展开", + "Collapse": "折叠", + "Comment menu": "评论菜单", + "Group menu": "群组菜单", + "Show hidden breadcrumbs": "显示隐藏的面包屑", + "Breadcrumbs": "面包屑", + "Page actions": "页面操作", + "Pick emoji": "选择表情符号", + "Template menu": "模板菜单", + "Use": "使用", + "Use template": "使用模板", + "Preview template: {{title}}": "预览模板:{{title}}", + "Use a template": "使用模板", + "Search templates...": "搜索模板……", + "Search spaces...": "搜索空间……", + "No templates found": "未找到模板", + "No spaces found": "未找到空间", + "Browse all templates": "浏览所有模板", + "This space": "此空间", + "All templates": "所有模板", + "Global": "全局", + "New template": "新建模板", + "Edit template": "编辑模板", + "Are you sure you want to delete this template?": "你确定要删除此模板吗?", + "Template scope updated": "模板范围已更新", + "Choose which space this template belongs to": "选择此模板所属的空间", + "Scope": "范围", + "Select scope": "选择范围", + "Title": "标题", + "Saving...": "正在保存……", + "Saved": "已保存", + "Save failed. Retry": "保存失败。重试", + "By {{name}}": "作者:{{name}}", + "Updated {{time}}": "更新于 {{time}}", + "Choose destination": "选择目标位置", + "Search pages and spaces...": "搜索页面和空间……", + "No results found": "未找到结果", + "You don't have permission to create pages here": "你无权在此处创建页面", + "Chat menu": "聊天菜单", + "API key menu": "API 密钥菜单", + "Jump to comment selection": "跳转到评论选择", + "Slash commands": "斜杠命令", + "Mention suggestions": "提及建议", + "Link suggestions": "链接建议", + "Diagram editor": "图表编辑器", + "Add comment": "添加评论", + "Find and replace": "查找和替换", + "Main navigation": "主导航", + "Space navigation": "空间导航", + "Settings navigation": "设置导航", + "AI navigation": "AI 导航", + "Breadcrumb": "面包屑", + "Synced block": "同步块", + "Create a block that stays in sync across pages.": "创建一个可在多个页面间保持同步的块。", + "Editing original": "正在编辑原始内容", + "Copy synced block": "复制同步块", + "Unsync": "取消同步", + "Delete synced block": "删除同步块", + "Synced to {{count}} other page_one": "已与另外 {{count}} 个页面同步", + "Synced to {{count}} other page_other": "已与另外 {{count}} 个页面同步", + "ORIGINAL": "原始内容", + "THIS PAGE": "此页面", + "No pages": "没有页面", + "The original synced block no longer exists": "原始同步块已不存在", + "You don't have access to this synced block": "你无权访问此同步块", + "Failed to load this synced block": "加载此同步块失败", + "Fixed editor toolbar": "固定编辑器工具栏", + "Show a formatting toolbar above the editor with quick access to common actions.": "在编辑器上方显示格式工具栏,便于快速访问常用操作。", + "Toggle fixed editor toolbar": "切换固定编辑器工具栏", + "Normal text": "普通文本", + "More inline formatting": "更多内联格式", + "Subscript": "下标", + "Superscript": "上标", + "Inline code": "行内代码", + "Insert media": "插入媒体", + "Mention": "提及", + "Emoji": "表情符号", + "Columns": "分栏", + "More inserts": "更多插入项", + "Embeds": "嵌入内容", + "Diagrams": "图表", + "Advanced": "高级", + "Utility": "实用工具", + "Decrease indent": "减少缩进", + "Increase indent": "增加缩进", + "Clear formatting": "清除格式", + "Code block": "代码块", + "Experimental": "实验性", + "Strikethrough": "删除线", + "Undo": "撤销", + "Redo": "重做", + "Backlinks": "反向链接", + "Last updated by": "最后更新者", + "Last updated": "最后更新", + "Stats": "统计", + "Word count": "字数", + "Characters": "字符数", + "Incoming links": "传入链接", + "Outgoing links": "传出链接", + "Incoming links ({{count}})": "传入链接({{count}})", + "Outgoing links ({{count}})": "传出链接({{count}})", + "No pages link here yet.": "还没有页面链接到这里。", + "This page doesn't link to other pages yet.": "此页面尚未链接到其他页面。", + "Verified until {{date}}": "验证有效期至 {{date}}", + "Labels": "标签", + "Add label": "添加标签", + "No labels yet": "还没有标签", + "Already added": "已添加", + "Invalid label name": "标签名称无效", + "No matches": "无匹配结果", + "Search or create…": "搜索或创建…", + "Remove label {{name}}": "移除标签 {{name}}", + "Failed to add label": "添加标签失败", + "Failed to remove label": "移除标签失败", + "No pages with this label": "没有带有此标签的页面", + "Pages tagged with this label will appear here.": "带有此标签的页面将显示在这里。", + "No pages match your search.": "没有页面匹配你的搜索。", + "Updated {{date}}": "更新于 {{date}}", + "Cell actions": "单元格操作", + "Column actions": "列操作", + "Row actions": "行操作", + "Filter": "筛选", + "Page title": "页面标题", + "Page content": "页面内容", + "Member actions": "成员操作", + "Toggle password visibility": "切换密码可见性", + "Send comment": "发送评论", + "Token actions": "令牌操作", + "Template settings": "模板设置", + "Edit diagram": "编辑图表", + "Edit embed": "编辑嵌入内容", + "Edit drawing": "编辑绘图", + "Delete equation": "删除公式", + "Invite actions": "邀请操作", + "Get started": "开始使用", + "* indicates required fields": "* 表示必填字段", + "List of spaces in this workspace": "此工作区中的空间列表", + "Active sessions": "活动会话", + "Add {{name}} to favorites": "将 {{name}} 添加到收藏", + "Remove {{name}} from favorites": "将 {{name}} 从收藏中移除", + "Added to favorites": "已添加到收藏", + "Removed from favorites": "已从收藏中移除", + "Added {{name}} to favorites": "已将 {{name}} 添加到收藏", + "Removed {{name}} from favorites": "已将 {{name}} 从收藏中移除", + "Page menu for {{name}}": "{{name}} 的页面菜单", + "Create subpage of {{name}}": "创建 {{name}} 的子页面" } diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index 0c8b72f24..0285c977e 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -46,6 +46,7 @@ import TemplateEditor from "@/ee/template/pages/template-editor"; import FavoritesPage from "@/pages/favorites/favorites-page"; import AiChat from "@/ee/ai-chat/pages/ai-chat.tsx"; import VerifyEmail from "@/ee/pages/verify-email.tsx"; +import LabelPage from "@/pages/label/label-page"; export default function App() { const { t } = useTranslation(); @@ -93,6 +94,7 @@ export default function App() { } /> } /> } /> + } /> } /> { if (disabled) return; @@ -104,6 +118,8 @@ export default function AvatarUploader({ ref={fileInputRef} onChange={handleFileInputChange} accept="image/png,image/jpeg,image/jpg" + aria-label={ariaLabel} + tabIndex={-1} style={{ display: "none" }} /> @@ -115,6 +131,8 @@ export default function AvatarUploader({ size={size} avatarUrl={currentImageUrl} name={fallbackName} + aria-label={ariaLabel} + aria-haspopup="menu" style={{ cursor: disabled || isLoading ? "default" : "pointer", opacity: isLoading ? 0.6 : 1, diff --git a/apps/client/src/components/common/copy.tsx b/apps/client/src/components/common/copy.tsx index 745fc4ba6..8bf4ec938 100644 --- a/apps/client/src/components/common/copy.tsx +++ b/apps/client/src/components/common/copy.tsx @@ -8,15 +8,19 @@ interface CopyProps { text: string; size?: MantineSize; color?: MantineColor; + /** Override the accessible name (and tooltip) when not yet copied. Lets callers disambiguate adjacent copy buttons for screen readers. */ + label?: string; } -export default function CopyTextButton({ text, size }: CopyProps) { +export default function CopyTextButton({ text, size, label }: CopyProps) { const { t } = useTranslation(); + const copyLabel = label ?? t("Copy"); + return ( {({ copied, copy }) => ( @@ -25,6 +29,7 @@ export default function CopyTextButton({ text, size }: CopyProps) { variant="subtle" onClick={copy} size={size} + aria-label={copied ? t("Copied") : copyLabel} > {copied ? : } diff --git a/apps/client/src/components/common/export-modal.tsx b/apps/client/src/components/common/export-modal.tsx index 53de82467..2a83debf9 100644 --- a/apps/client/src/components/common/export-modal.tsx +++ b/apps/client/src/components/common/export-modal.tsx @@ -81,7 +81,7 @@ export default function ExportModal({ {t(`Export ${type}`)} - + diff --git a/apps/client/src/components/common/recent-changes.tsx b/apps/client/src/components/common/recent-changes.tsx index 277ceb811..f37b26f67 100644 --- a/apps/client/src/components/common/recent-changes.tsx +++ b/apps/client/src/components/common/recent-changes.tsx @@ -4,7 +4,7 @@ import { UnstyledButton, Badge, Table, - ActionIcon, + ThemeIcon, Button, } from "@mantine/core"; import { Link } from "react-router-dom"; @@ -17,6 +17,7 @@ import { EmptyState } from "@/components/ui/empty-state.tsx"; import { getSpaceUrl } from "@/lib/config.ts"; import { useTranslation } from "react-i18next"; import { getInitialsColor } from "@/lib/get-initials-color.ts"; +import rowClasses from "@/components/ui/clickable-table-row.module.css"; interface Props { spaceId?: string; @@ -41,17 +42,18 @@ export default function RecentChanges({ spaceId }: Props) { {pages.map((page) => ( - + {page.icon || ( - + - + )} diff --git a/apps/client/src/components/common/search-input.tsx b/apps/client/src/components/common/search-input.tsx index 08cbbee06..27e50fd4e 100644 --- a/apps/client/src/components/common/search-input.tsx +++ b/apps/client/src/components/common/search-input.tsx @@ -6,12 +6,14 @@ import { useTranslation } from "react-i18next"; export interface SearchInputProps { placeholder?: string; + ariaLabel?: string; debounceDelay?: number; onSearch: (value: string) => void; } export function SearchInput({ placeholder, + ariaLabel, debounceDelay = 500, onSearch, }: SearchInputProps) { @@ -28,6 +30,7 @@ export function SearchInput({ } value={value} onChange={(e) => setValue(e.currentTarget.value)} diff --git a/apps/client/src/components/icons/icon-people-circle.tsx b/apps/client/src/components/icons/icon-people-circle.tsx index 996958965..1a2daf73d 100644 --- a/apps/client/src/components/icons/icon-people-circle.tsx +++ b/apps/client/src/components/icons/icon-people-circle.tsx @@ -1,11 +1,11 @@ -import { ActionIcon, rem } from "@mantine/core"; +import { ThemeIcon } from "@mantine/core"; import React from "react"; import { IconUsersGroup } from "@tabler/icons-react"; export function IconGroupCircle() { return ( - + - + ); } diff --git a/apps/client/src/components/layouts/global/app-shell.module.css b/apps/client/src/components/layouts/global/app-shell.module.css index ed369612c..dd3b72e45 100644 --- a/apps/client/src/components/layouts/global/app-shell.module.css +++ b/apps/client/src/components/layouts/global/app-shell.module.css @@ -27,5 +27,3 @@ background: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-5)) } } - - diff --git a/apps/client/src/components/layouts/global/aside.tsx b/apps/client/src/components/layouts/global/aside.tsx index 4f2cf5927..556adbf17 100644 --- a/apps/client/src/components/layouts/global/aside.tsx +++ b/apps/client/src/components/layouts/global/aside.tsx @@ -1,18 +1,27 @@ -import { Box, ScrollArea, Text } from "@mantine/core"; +import { ActionIcon, Box, Group, ScrollArea, Title, Tooltip } from "@mantine/core"; +import { IconX } from "@tabler/icons-react"; import CommentListWithTabs from "@/features/comment/components/comment-list-with-tabs.tsx"; import { useAtom } from "jotai"; import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts"; -import React, { ReactNode } from "react"; +import React, { ReactNode, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { TableOfContents } from "@/features/editor/components/table-of-contents/table-of-contents.tsx"; import { useAtomValue } from "jotai"; import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms.ts"; import AsideChatPanel from "@/ee/ai-chat/components/aside-chat-panel"; +import { PageDetailsAside } from "@/features/page-details/components/page-details-aside.tsx"; +import { ASIDE_PANEL_ID } from "@/hooks/use-toggle-aside.tsx"; export default function Aside() { - const [{ tab }] = useAtom(asideStateAtom); + const [{ tab, isAsideOpen }, setAsideState] = useAtom(asideStateAtom); const { t } = useTranslation(); const pageEditor = useAtomValue(pageEditorAtom); + const closeAside = () => setAsideState((s) => ({ ...s, isAsideOpen: false })); + + useEffect(() => { + if (!isAsideOpen) return; + document.getElementById(ASIDE_PANEL_ID)?.focus(); + }, [isAsideOpen, tab]); let title: string; let component: ReactNode; @@ -30,6 +39,10 @@ export default function Aside() { component = ; title = "AI Chat"; break; + case "details": + component = ; + title = "Details"; + break; default: component = null; title = null; @@ -40,9 +53,19 @@ export default function Aside() { {component && ( <> {tab !== "chat" && ( - - {t(title)} - + + {t(title)} + + + + + + )} {tab === "comments" || tab === "chat" ? ( diff --git a/apps/client/src/components/layouts/global/global-app-shell.tsx b/apps/client/src/components/layouts/global/global-app-shell.tsx index 64bd3dde7..f35029d40 100644 --- a/apps/client/src/components/layouts/global/global-app-shell.tsx +++ b/apps/client/src/components/layouts/global/global-app-shell.tsx @@ -1,6 +1,7 @@ import { AppShell, Container } from "@mantine/core"; import React, { useEffect, useRef, useState } from "react"; import { useLocation } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import SettingsSidebar from "@/components/settings/settings-sidebar.tsx"; import { useAtom } from "jotai"; import { @@ -17,17 +18,20 @@ import classes from "./app-shell.module.css"; import { useTrialEndAction } from "@/ee/hooks/use-trial-end-action.tsx"; import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts"; import GlobalSidebar from "@/components/layouts/global/global-sidebar.tsx"; +import { ASIDE_PANEL_ID } from "@/hooks/use-toggle-aside.tsx"; +import { MAIN_CONTENT_ID, SkipToMain } from "@/components/ui/skip-to-main.tsx"; export default function GlobalAppShell({ children, }: { children: React.ReactNode; }) { + const { t } = useTranslation(); useTrialEndAction(); const [mobileOpened] = useAtom(mobileSidebarAtom); const toggleMobile = useToggleSidebar(mobileSidebarAtom); const [desktopOpened] = useAtom(desktopSidebarAtom); - const [{ isAsideOpen }] = useAtom(asideStateAtom); + const [{ isAsideOpen, tab: asideTab }] = useAtom(asideStateAtom); const [sidebarWidth, setSidebarWidth] = useAtom(sidebarWidthAtom); const [isResizing, setIsResizing] = useState(false); const sidebarRef = useRef(null); @@ -79,7 +83,9 @@ export default function GlobalAppShell({ const showGlobalSidebar = !isSpaceRoute && !isSettingsRoute && !isAiRoute; return ( - + + {isSpaceRoute && (
@@ -114,19 +129,39 @@ export default function GlobalAppShell({ {isAiRoute && } {showGlobalSidebar && } - + {isSettingsRoute ? ( - {children} + + {children} + ) : ( children )} {isPageRoute && ( - +
+ + + {t("Name")} + {t("Token")} + {t("Created by")} + {t("Last used")} + {t("Created")} + + + + + + {tokens && tokens.length > 0 ? ( + tokens.map((token) => ( + + + + {token.name} + + + + + + ••••{token.tokenLastFour} + + + + {token.creator ? ( + + + + + {token.creator.name} + + + + ) : ( + + + — + + + )} + + + + {formatDate(token.lastUsedAt)} + + + + + + {formatDate(token.createdAt)} + + + + + + + + + + + + {onUpdate && ( + } + onClick={() => onUpdate(token)} + > + {t("Rename")} + + )} + {onRevoke && ( + } + color="red" + onClick={() => onRevoke(token)} + > + {t("Revoke")} + + )} + + + + + )) + ) : ( + + )} + +
+ + ); +} diff --git a/apps/client/src/ee/scim/components/scim-url-panel.tsx b/apps/client/src/ee/scim/components/scim-url-panel.tsx new file mode 100644 index 000000000..6aa78820c --- /dev/null +++ b/apps/client/src/ee/scim/components/scim-url-panel.tsx @@ -0,0 +1,30 @@ +import { Group, Stack, Text, TextInput } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import CopyTextButton from "@/components/common/copy.tsx"; + +export function ScimUrlPanel() { + const { t } = useTranslation(); + const scimUrl = `${window.location.origin}/api/scim/v2`; + + return ( + + + {t("SCIM endpoint URL")} + + + {t( + "Configure your identity provider with this URL to provision users and groups.", + )} + + + + + + + ); +} diff --git a/apps/client/src/ee/scim/components/update-scim-token-modal.tsx b/apps/client/src/ee/scim/components/update-scim-token-modal.tsx new file mode 100644 index 000000000..45ffd782c --- /dev/null +++ b/apps/client/src/ee/scim/components/update-scim-token-modal.tsx @@ -0,0 +1,78 @@ +import { Modal, TextInput, Button, Group, Stack } from "@mantine/core"; +import { useForm } from "@mantine/form"; +import { zod4Resolver } from "mantine-form-zod-resolver"; +import { z } from "zod/v4"; +import { useTranslation } from "react-i18next"; +import { useEffect } from "react"; +import { useUpdateScimTokenMutation } from "@/ee/scim/queries/scim-token-query"; +import { IScimToken } from "@/ee/scim/types/scim-token.types"; + +const formSchema = z.object({ + name: z.string().min(1, "Name is required"), +}); +type FormValues = z.infer; + +interface UpdateScimTokenModalProps { + opened: boolean; + onClose: () => void; + scimToken: IScimToken | null; +} + +export function UpdateScimTokenModal({ + opened, + onClose, + scimToken, +}: UpdateScimTokenModalProps) { + const { t } = useTranslation(); + const updateMutation = useUpdateScimTokenMutation(); + + const form = useForm({ + validate: zod4Resolver(formSchema), + initialValues: { name: "" }, + }); + + useEffect(() => { + if (opened && scimToken) { + form.setValues({ name: scimToken.name }); + } + }, [opened, scimToken]); + + const handleSubmit = async (data: FormValues) => { + if (!scimToken) return; + await updateMutation.mutateAsync({ + tokenId: scimToken.id, + name: data.name, + }); + onClose(); + }; + + return ( + +
handleSubmit(values))}> + + + + + + + + +
+
+ ); +} diff --git a/apps/client/src/ee/scim/index.ts b/apps/client/src/ee/scim/index.ts new file mode 100644 index 000000000..e246200d0 --- /dev/null +++ b/apps/client/src/ee/scim/index.ts @@ -0,0 +1,2 @@ +export * from "./types/scim-token.types"; +export * from "./services/scim-token-service"; diff --git a/apps/client/src/ee/scim/queries/scim-token-query.ts b/apps/client/src/ee/scim/queries/scim-token-query.ts new file mode 100644 index 000000000..999f4d200 --- /dev/null +++ b/apps/client/src/ee/scim/queries/scim-token-query.ts @@ -0,0 +1,96 @@ +import { IPagination, QueryParams } from "@/lib/types.ts"; +import { + keepPreviousData, + useMutation, + useQuery, + useQueryClient, + UseQueryResult, +} from "@tanstack/react-query"; +import { + createScimToken, + getScimTokens, + revokeScimToken, + updateScimToken, +} from "@/ee/scim/services/scim-token-service"; +import { + IScimToken, + ICreateScimTokenRequest, + IRevokeScimTokenRequest, + IUpdateScimTokenRequest, +} from "@/ee/scim/types/scim-token.types"; +import { notifications } from "@mantine/notifications"; +import { useTranslation } from "react-i18next"; + +export function useGetScimTokensQuery( + params?: QueryParams, +): UseQueryResult, Error> { + return useQuery({ + queryKey: ["scim-token-list", params], + queryFn: () => getScimTokens(params), + placeholderData: keepPreviousData, + }); +} + +export function useCreateScimTokenMutation() { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: (data) => createScimToken(data), + onSuccess: () => { + notifications.show({ + message: t("{{credential}} created successfully", { + credential: t("SCIM token"), + }), + }); + queryClient.invalidateQueries({ + predicate: (item) => + ["scim-token-list"].includes(item.queryKey[0] as string), + }); + }, + onError: (error) => { + const errorMessage = error["response"]?.data?.message; + notifications.show({ message: errorMessage, color: "red" }); + }, + }); +} + +export function useUpdateScimTokenMutation() { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: (data) => updateScimToken(data), + onSuccess: () => { + notifications.show({ message: t("Updated successfully") }); + queryClient.invalidateQueries({ + predicate: (item) => + ["scim-token-list"].includes(item.queryKey[0] as string), + }); + }, + onError: (error) => { + const errorMessage = error["response"]?.data?.message; + notifications.show({ message: errorMessage, color: "red" }); + }, + }); +} + +export function useRevokeScimTokenMutation() { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: (data) => revokeScimToken(data), + onSuccess: () => { + notifications.show({ message: t("Revoked successfully") }); + queryClient.invalidateQueries({ + predicate: (item) => + ["scim-token-list"].includes(item.queryKey[0] as string), + }); + }, + onError: (error) => { + const errorMessage = error["response"]?.data?.message; + notifications.show({ message: errorMessage, color: "red" }); + }, + }); +} diff --git a/apps/client/src/ee/scim/services/scim-token-service.ts b/apps/client/src/ee/scim/services/scim-token-service.ts new file mode 100644 index 000000000..27e730358 --- /dev/null +++ b/apps/client/src/ee/scim/services/scim-token-service.ts @@ -0,0 +1,34 @@ +import api from "@/lib/api-client"; +import { + IScimToken, + ICreateScimTokenRequest, + IRevokeScimTokenRequest, + IUpdateScimTokenRequest, +} from "@/ee/scim/types/scim-token.types"; +import { IPagination, QueryParams } from "@/lib/types.ts"; + +export async function getScimTokens( + params?: QueryParams, +): Promise> { + const req = await api.post("/scim-tokens", { ...params }); + return req.data; +} + +export async function createScimToken( + data: ICreateScimTokenRequest, +): Promise { + const req = await api.post("/scim-tokens/create", data); + return req.data; +} + +export async function updateScimToken( + data: IUpdateScimTokenRequest, +): Promise { + await api.post("/scim-tokens/update", data); +} + +export async function revokeScimToken( + data: IRevokeScimTokenRequest, +): Promise { + await api.post("/scim-tokens/revoke", data); +} diff --git a/apps/client/src/ee/scim/types/scim-token.types.ts b/apps/client/src/ee/scim/types/scim-token.types.ts new file mode 100644 index 000000000..076501291 --- /dev/null +++ b/apps/client/src/ee/scim/types/scim-token.types.ts @@ -0,0 +1,27 @@ +import { IUser } from "@/features/user/types/user.types.ts"; + +export interface IScimToken { + id: string; + name: string; + token?: string; + tokenLastFour: string; + isEnabled: boolean; + creatorId: string; + workspaceId: string; + lastUsedAt: string | null; + createdAt: string; + creator?: Partial; +} + +export interface ICreateScimTokenRequest { + name: string; +} + +export interface IUpdateScimTokenRequest { + tokenId: string; + name: string; +} + +export interface IRevokeScimTokenRequest { + tokenId: string; +} diff --git a/apps/client/src/ee/security/components/allow-member-templates.tsx b/apps/client/src/ee/security/components/allow-member-templates.tsx index f547d1644..8acc8ba6d 100644 --- a/apps/client/src/ee/security/components/allow-member-templates.tsx +++ b/apps/client/src/ee/security/components/allow-member-templates.tsx @@ -34,7 +34,7 @@ function AllowMemberTemplatesToggle() { const [checked, setChecked] = useState( workspace?.settings?.templates?.allowMemberTemplates === true, ); - const hasSecuritySettings = useHasFeature(Feature.SECURITY_SETTINGS); + const hasTemplates = useHasFeature(Feature.TEMPLATES); const upgradeLabel = useUpgradeLabel(); const handleChange = async (event: React.ChangeEvent) => { @@ -54,15 +54,11 @@ function AllowMemberTemplatesToggle() { }; return ( - + diff --git a/apps/client/src/ee/security/components/sso-provider-list.tsx b/apps/client/src/ee/security/components/sso-provider-list.tsx index 7aac7d117..7f74df8c2 100644 --- a/apps/client/src/ee/security/components/sso-provider-list.tsx +++ b/apps/client/src/ee/security/components/sso-provider-list.tsx @@ -69,8 +69,8 @@ export default function SsoProviderList() { return ( <> - - + +
{t("Name")} @@ -141,6 +141,7 @@ export default function SsoProviderList() { handleEdit(provider)} > @@ -152,7 +153,13 @@ export default function SsoProviderList() { withinPortal > - + diff --git a/apps/client/src/ee/security/components/sso-provider-modal.tsx b/apps/client/src/ee/security/components/sso-provider-modal.tsx index 6bc900178..8f9cc47cf 100644 --- a/apps/client/src/ee/security/components/sso-provider-modal.tsx +++ b/apps/client/src/ee/security/components/sso-provider-modal.tsx @@ -32,6 +32,7 @@ export default function SsoProviderModal({ ssoProviderType: provider.type.toUpperCase(), })} onClose={onClose} + closeButtonProps={{ "aria-label": t("Close") }} > {provider.type === SSO_PROVIDER.SAML && ( diff --git a/apps/client/src/ee/security/pages/security.tsx b/apps/client/src/ee/security/pages/security.tsx index 254809c74..2ff3670be 100644 --- a/apps/client/src/ee/security/pages/security.tsx +++ b/apps/client/src/ee/security/pages/security.tsx @@ -1,8 +1,18 @@ import { Helmet } from "react-helmet-async"; import { getAppName, isCloud } from "@/lib/config.ts"; import SettingsTitle from "@/components/settings/settings-title.tsx"; -import { Divider, Title } from "@mantine/core"; -import React from "react"; +import { + Alert, + Button, + Card, + Divider, + Group, + Space, + Title, + Tooltip, +} from "@mantine/core"; +import { IconInfoCircle } from "@tabler/icons-react"; +import React, { useState } from "react"; import useUserRole from "@/hooks/use-user-role.tsx"; import SsoProviderList from "@/ee/security/components/sso-provider-list.tsx"; import CreateSsoProvider from "@/ee/security/components/create-sso-provider.tsx"; @@ -12,16 +22,41 @@ import { useTranslation } from "react-i18next"; import EnforceMfa from "@/ee/security/components/enforce-mfa.tsx"; import DisablePublicSharing from "@/ee/security/components/disable-public-sharing.tsx"; import TrashRetention from "@/ee/security/components/trash-retention.tsx"; - +import { useAtom } from "jotai"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; import { useHasFeature } from "@/ee/hooks/use-feature"; import { Feature } from "@/ee/features"; +import { useGetScimTokensQuery } from "@/ee/scim/queries/scim-token-query"; +import { ScimUrlPanel } from "@/ee/scim/components/scim-url-panel"; +import { ScimTokenTable } from "@/ee/scim/components/scim-token-table"; +import { CreateScimTokenModal } from "@/ee/scim/components/create-scim-token-modal"; +import { ScimTokenCreatedModal } from "@/ee/scim/components/scim-token-created-modal"; +import { RevokeScimTokenModal } from "@/ee/scim/components/revoke-scim-token-modal"; +import { UpdateScimTokenModal } from "@/ee/scim/components/update-scim-token-modal"; +import EnableScim from "@/ee/scim/components/enable-scim"; +import { useCursorPaginate } from "@/hooks/use-cursor-paginate"; +import Paginate from "@/components/common/paginate"; +import { IScimToken } from "@/ee/scim/types/scim-token.types"; + +const SCIM_TOKEN_LIMIT = 5; export default function Security() { const { t } = useTranslation(); const { isAdmin } = useUserRole(); const hasCustomSso = useHasFeature(Feature.SSO_CUSTOM); - const hasRetention = useHasFeature(Feature.RETENTION); - const hasSharingControls = useHasFeature(Feature.SHARING_CONTROLS); + const hasScim = useHasFeature(Feature.SCIM); + const [workspace] = useAtom(workspaceAtom); + const isScimEnabled = workspace?.isScimEnabled ?? false; + + const { cursor, goNext, goPrev } = useCursorPaginate(); + const { data: scimData, isLoading: scimLoading } = useGetScimTokensQuery( + hasScim && isScimEnabled ? { cursor } : undefined, + ); + + const [createOpen, setCreateOpen] = useState(false); + const [createdToken, setCreatedToken] = useState(null); + const [updateTarget, setUpdateTarget] = useState(null); + const [revokeTarget, setRevokeTarget] = useState(null); if (!isAdmin) { return null; @@ -45,7 +80,7 @@ export default function Security() { - Single sign-on (SSO) + {t("Single sign-on (SSO)")} @@ -66,6 +101,101 @@ export default function Security() { )} + + {hasScim && ( + <> + + + + {t("SCIM provisioning")} + + + } + color="blue" + variant="light" + mb="md" + > + {t("SCIM takes precedence over SSO group sync while enabled.")} + + + + + + + + + {isScimEnabled && ( + <> + + + + {t("SCIM tokens")} + + + + + + + + + + + + {scimData?.items.length > 0 && ( + goNext(scimData?.meta?.nextCursor)} + onPrev={goPrev} + /> + )} + + setCreateOpen(false)} + onSuccess={setCreatedToken} + /> + + setCreatedToken(null)} + scimToken={createdToken} + /> + + setUpdateTarget(null)} + scimToken={updateTarget} + /> + + setRevokeTarget(null)} + scimToken={revokeTarget} + /> + + )} + + )} ); } diff --git a/apps/client/src/ee/security/sso.utils.ts b/apps/client/src/ee/security/sso.utils.ts index 4a4665c16..bfeb7c062 100644 --- a/apps/client/src/ee/security/sso.utils.ts +++ b/apps/client/src/ee/security/sso.utils.ts @@ -18,14 +18,21 @@ export function buildSsoLoginUrl(opts: { providerId: string; type: SSO_PROVIDER; workspaceId?: string; + redirect?: string; }): string { - const { providerId, type, workspaceId } = opts; + const { providerId, type, workspaceId, redirect } = opts; const domain = getAppUrl(); + const params = new URLSearchParams(); + if (redirect) params.set("redirect", redirect); + if (type === SSO_PROVIDER.GOOGLE) { - return `${getServerAppUrl()}/api/sso/${type}/login?workspaceId=${workspaceId}`; + if (workspaceId) params.set("workspaceId", workspaceId); + return `${getServerAppUrl()}/api/sso/${type}/login?${params.toString()}`; } - return `${domain}/api/sso/${type}/${providerId}/login`; + const query = params.toString(); + const base = `${domain}/api/sso/${type}/${providerId}/login`; + return query ? `${base}?${query}` : base; } export function getGoogleSignupUrl(): string { diff --git a/apps/client/src/ee/template/components/template-card.module.css b/apps/client/src/ee/template/components/template-card.module.css index 3e041e2b4..025453b44 100644 --- a/apps/client/src/ee/template/components/template-card.module.css +++ b/apps/client/src/ee/template/components/template-card.module.css @@ -8,6 +8,11 @@ @mixin hover { transform: scale(1.02); } + + &:focus-visible { + outline: 2px solid var(--mantine-primary-color-filled); + outline-offset: 2px; + } } .cardBody { @@ -50,18 +55,27 @@ .footer { display: flex; align-items: center; - justify-content: space-between; - gap: var(--mantine-spacing-xs); + gap: 6px; padding-top: var(--mantine-spacing-sm); margin-top: var(--mantine-spacing-lg); border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); } +.scopeDot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; + background-color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3)); +} + .menuTarget { opacity: 0; transition: opacity 100ms ease; - .card:hover & { + .card:hover &, + .card:focus-within & { opacity: 1; } } diff --git a/apps/client/src/ee/template/components/template-card.tsx b/apps/client/src/ee/template/components/template-card.tsx index 347140f11..ef18fc313 100644 --- a/apps/client/src/ee/template/components/template-card.tsx +++ b/apps/client/src/ee/template/components/template-card.tsx @@ -1,4 +1,4 @@ -import { Card, Text, ActionIcon, Menu, Group } from "@mantine/core"; +import { Button, Card, Text, ActionIcon, Menu, Group } from "@mantine/core"; import { IconDots, IconEdit, @@ -12,6 +12,7 @@ import classes from "./template-card.module.css"; type TemplateCardProps = { template: ITemplate; spaceName?: string; + onPreview: (template: ITemplate) => void; onUse: (template: ITemplate) => void; onEdit?: (template: ITemplate) => void; onDelete?: (template: ITemplate) => void; @@ -21,6 +22,7 @@ type TemplateCardProps = { export default function TemplateCard({ template, spaceName, + onPreview, onUse, onEdit, onDelete, @@ -34,7 +36,17 @@ export default function TemplateCard({ padding="lg" className={classes.card} style={{ cursor: "pointer" }} - onClick={() => onUse(template)} + role="button" + tabIndex={0} + aria-label={t("Preview template: {{title}}", { title: template.title })} + onClick={() => onPreview(template)} + onKeyDown={(e) => { + if (e.target !== e.currentTarget) return; + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onPreview(template); + } + }} >
@@ -47,6 +59,17 @@ export default function TemplateCard({ )} + {canManage && ( @@ -56,6 +79,7 @@ export default function TemplateCard({ color="gray" className={classes.menuTarget} onClick={(e) => e.stopPropagation()} + aria-label={t("Template menu")} > @@ -90,6 +114,7 @@ export default function TemplateCard({
{template.title}
+
@@ -391,6 +392,7 @@ const PageCommentInput = ({ onSave, isLoading }) => { variant="filled" radius="xl" size="sm" + aria-label={t("Send comment")} onClick={handleSave} onMouseDown={(e) => e.preventDefault()} loading={isLoading} diff --git a/apps/client/src/features/comment/components/comment-menu.tsx b/apps/client/src/features/comment/components/comment-menu.tsx index fe047232b..690815e62 100644 --- a/apps/client/src/features/comment/components/comment-menu.tsx +++ b/apps/client/src/features/comment/components/comment-menu.tsx @@ -46,7 +46,11 @@ function CommentMenu({ return ( - + diff --git a/apps/client/src/features/comment/components/comment.module.css b/apps/client/src/features/comment/components/comment.module.css index 590324990..dfa61b790 100644 --- a/apps/client/src/features/comment/components/comment.module.css +++ b/apps/client/src/features/comment/components/comment.module.css @@ -22,6 +22,11 @@ .commentEditor { + &[data-editable][data-surface="muted"] .ProseMirror:not(.focused) { + border-radius: var(--mantine-radius-sm); + box-shadow: 0 0 0 1px light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-4)); + } + .focused { border-radius: var(--mantine-radius-sm); box-shadow: 0 0 0 2px var(--mantine-color-blue-3); diff --git a/apps/client/src/features/editor/atoms/editor-atoms.ts b/apps/client/src/features/editor/atoms/editor-atoms.ts index 8982765e4..c0873adfa 100644 --- a/apps/client/src/features/editor/atoms/editor-atoms.ts +++ b/apps/client/src/features/editor/atoms/editor-atoms.ts @@ -1,5 +1,6 @@ import { atom } from "jotai"; import { Editor } from "@tiptap/core"; +import { PageEditMode } from "@/features/user/types/user.types.ts"; export const pageEditorAtom = atom(null); @@ -12,3 +13,7 @@ export const yjsConnectionStatusAtom = atom(""); export const showAiMenuAtom = atom(false); export const showLinkMenuAtom = atom(false); + +// Current page's edit mode — initialized from the user's saved preference on +// first load, can be toggled locally without persisting to the server. +export const currentPageEditModeAtom = atom(PageEditMode.Edit); diff --git a/apps/client/src/features/editor/components/attachment/upload-attachment-action.tsx b/apps/client/src/features/editor/components/attachment/upload-attachment-action.tsx index 9f668963f..e9408891b 100644 --- a/apps/client/src/features/editor/components/attachment/upload-attachment-action.tsx +++ b/apps/client/src/features/editor/components/attachment/upload-attachment-action.tsx @@ -19,7 +19,9 @@ export const uploadAttachmentAction = handleAttachmentUpload({ }, validateFn: (file, allowMedia: boolean) => { if ( - (file.type.includes("image/") || file.type.includes("video/")) && + (file.type.includes("image/") || + file.type.includes("video/") || + file.type === "application/pdf") && !allowMedia ) { return false; diff --git a/apps/client/src/features/editor/components/audio/audio-menu.tsx b/apps/client/src/features/editor/components/audio/audio-menu.tsx index 3ca1950da..eadc1afe5 100644 --- a/apps/client/src/features/editor/components/audio/audio-menu.tsx +++ b/apps/client/src/features/editor/components/audio/audio-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { useCallback } from "react"; import { Node as PMNode } from "@tiptap/pm/model"; +import { isEditorReady } from "@docmost/editor-ext"; import { EditorMenuProps, ShouldShowProps, @@ -46,7 +47,7 @@ export function AudioMenu({ editor }: EditorMenuProps) { ); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "audio"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/audio/audio-view.tsx b/apps/client/src/features/editor/components/audio/audio-view.tsx index 9e5f619f2..3dc0739d6 100644 --- a/apps/client/src/features/editor/components/audio/audio-view.tsx +++ b/apps/client/src/features/editor/components/audio/audio-view.tsx @@ -36,6 +36,7 @@ export default function AudioView(props: NodeViewProps) { preload="metadata" controls src={safeSrc} + aria-label={placeholder?.name || t("Audio")} /> )} {!safeSrc && previewSrc && ( @@ -45,6 +46,7 @@ export default function AudioView(props: NodeViewProps) { preload="metadata" controls src={previewSrc} + aria-label={placeholder?.name || t("Audio")} /> @@ -60,7 +62,7 @@ export default function AudioView(props: NodeViewProps) { )} {!safeSrc && !previewSrc && !placeholder && ( -
diff --git a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.module.css b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.module.css index facaf7ffd..00e019f29 100644 --- a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.module.css +++ b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.module.css @@ -28,6 +28,18 @@ } } +.colorSwatch:focus-visible { + outline: none; + box-shadow: 0 0 0 2px var(--mantine-color-blue-6); + position: relative; + z-index: 1; +} + +.removeColor:focus-visible { + outline: none; + box-shadow: inset 0 0 0 2px var(--mantine-color-blue-6); +} + .buttonRoot { height: 34px; padding-left: rem(8); diff --git a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx index ec7e7baa6..27c4b3854 100644 --- a/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/bubble-menu.tsx @@ -27,7 +27,7 @@ import { isCellSelection, isTextSelected } from "@docmost/editor-ext"; import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx"; import { useTranslation } from "react-i18next"; import { showAiMenuAtom, showLinkMenuAtom } from "@/features/editor/atoms/editor-atoms"; -import { workspaceAtom } from "@/features/user/atoms/current-user-atom"; +import { userAtom, workspaceAtom } from "@/features/user/atoms/current-user-atom"; export interface BubbleMenuItem { name: string; @@ -46,6 +46,9 @@ export const EditorBubbleMenu: FC = (props) => { const [showCommentPopup, setShowCommentPopup] = useAtom(showCommentPopupAtom); const workspace = useAtomValue(workspaceAtom); const isGenerativeAiEnabled = workspace?.settings?.ai?.generative === true; + const user = useAtomValue(userAtom); + const editorToolbarEnabled = + user?.settings?.preferences?.editorToolbar ?? false; const [, setDraftCommentId] = useAtom(draftCommentIdAtom); const showCommentPopupRef = useRef(showCommentPopup); const showAiMenuRef = useRef(showAiMenu); @@ -149,7 +152,7 @@ export const EditorBubbleMenu: FC = (props) => { return isTextSelected(editor); }, options: { - placement: "top", + placement: editorToolbarEnabled ? "bottom" : "top", offset: 8, onHide: () => { setIsNodeSelectorOpen(false); @@ -188,56 +191,60 @@ export const EditorBubbleMenu: FC = (props) => {
)} - { - setIsNodeSelectorOpen(!isNodeSelectorOpen); - setIsTextAlignmentOpen(false); - setIsColorSelectorOpen(false); - }} - /> + {!editorToolbarEnabled && ( + <> + { + setIsNodeSelectorOpen(!isNodeSelectorOpen); + setIsTextAlignmentOpen(false); + setIsColorSelectorOpen(false); + }} + /> - { - setIsTextAlignmentOpen(!isTextAlignmentSelectorOpen); - setIsNodeSelectorOpen(false); - setIsColorSelectorOpen(false); - }} - /> + { + setIsTextAlignmentOpen(!isTextAlignmentSelectorOpen); + setIsNodeSelectorOpen(false); + setIsColorSelectorOpen(false); + }} + /> - - {items.map((item, index) => ( - - - - - - ))} - + + {items.map((item, index) => ( + + + + + + ))} + - + - { - setIsColorSelectorOpen(!isColorSelectorOpen); - setIsNodeSelectorOpen(false); - setIsTextAlignmentOpen(false); - }} - /> + { + setIsColorSelectorOpen(!isColorSelectorOpen); + setIsNodeSelectorOpen(false); + setIsTextAlignmentOpen(false); + }} + /> + + )} ( + `[data-color-grid="${grid}"][data-color-index="${index}"]`, + ); + el?.focus(); +} + +function handleColorKeyNav( + e: React.KeyboardEvent, + index: number, + grid: "text" | "highlight", +) { + const cols = COLOR_GRID_COLS; + const total = + grid === "text" ? TEXT_COLORS.length : HIGHLIGHT_COLORS.length; + const col = index % cols; + + if (e.key === "ArrowRight") { + e.preventDefault(); + if (index < total - 1) focusSwatch(grid, index + 1); + return; + } + if (e.key === "ArrowLeft") { + e.preventDefault(); + if (index > 0) focusSwatch(grid, index - 1); + return; + } + if (e.key === "ArrowDown") { + e.preventDefault(); + const next = index + cols; + if (next < total) { + focusSwatch(grid, next); + } else if (grid === "text") { + focusSwatch("highlight", Math.min(col, HIGHLIGHT_COLORS.length - 1)); + } else if (grid === "highlight") { + document + .querySelector('[data-color-grid="remove"]') + ?.focus(); + } + return; + } + if (e.key === "ArrowUp") { + e.preventDefault(); + const prev = index - cols; + if (prev >= 0) { + focusSwatch(grid, prev); + } else if (grid === "highlight") { + const lastRowStart = + Math.floor((TEXT_COLORS.length - 1) / cols) * cols; + focusSwatch("text", Math.min(lastRowStart + col, TEXT_COLORS.length - 1)); + } + return; + } +} + export const ColorSelector: FC = ({ editor, isOpen, @@ -157,13 +213,20 @@ export const ColorSelector: FC = ({ ); return ( - + - - - + e.preventDefault()}> + {t("Text color")} - {TEXT_COLORS.map(({ name, color }, index) => ( + {TEXT_COLORS.map(({ name, color }, index) => { + const applyTextColor = () => { + if (name === "Default") { + editor.commands.unsetColor(); + } else { + editor + .chain() + .focus() + .setColor(color || "") + .run(); + } + setIsOpen(false); + }; + return ( { - if (name === "Default") { - editor.commands.unsetColor(); - } else { - editor - .chain() - .focus() - .setColor(color || "") - .run(); + role="button" + tabIndex={0} + data-autofocus={index === 0 ? true : undefined} + data-color-grid="text" + data-color-index={index} + className={classes.colorSwatch} + aria-label={t(name)} + aria-pressed={!!editorState[`text_${color}`]} + onClick={applyTextColor} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + applyTextColor(); + return; } - setIsOpen(false); + handleColorKeyNav(e, index, "text"); }} style={{ width: rem(28), @@ -221,7 +304,8 @@ export const ColorSelector: FC = ({ A - ))} + ); + })} @@ -230,23 +314,40 @@ export const ColorSelector: FC = ({ {t("Highlight color")} - {HIGHLIGHT_COLORS.map(({ name, color }, index) => ( + {HIGHLIGHT_COLORS.map(({ name, color }, index) => { + const applyHighlight = () => { + if (name === "Default") { + editor.commands.unsetHighlight(); + } else { + editor + .chain() + .focus() + .toggleMark("highlight", { + color: color || "", + colorName: name.toLowerCase() || "", + }) + .run(); + } + setIsOpen(false); + }; + return ( { - if (name === "Default") { - editor.commands.unsetHighlight(); - } else { - editor - .chain() - .focus() - .toggleMark("highlight", { - color: color || "", - colorName: name.toLowerCase() || "", - }) - .run(); + role="button" + tabIndex={0} + data-color-grid="highlight" + data-color-index={index} + className={classes.colorSwatch} + aria-label={t(name)} + aria-pressed={!!editorState[`highlight_${color}`]} + onClick={applyHighlight} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + applyHighlight(); + return; } - setIsOpen(false); + handleColorKeyNav(e, index, "highlight"); }} style={{ width: rem(28), @@ -274,23 +375,35 @@ export const ColorSelector: FC = ({ )} - ))} + ); + })} - ); diff --git a/apps/client/src/features/editor/components/bubble-menu/node-selector.tsx b/apps/client/src/features/editor/components/bubble-menu/node-selector.tsx index 7fecff9e9..73fe722ad 100644 --- a/apps/client/src/features/editor/components/bubble-menu/node-selector.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/node-selector.tsx @@ -12,6 +12,7 @@ import { IconInfoCircle, IconList, IconListNumbers, + IconQuote, IconTypography, } from "@tabler/icons-react"; import { Popover, Button, ScrollArea, Tooltip } from "@mantine/core"; @@ -59,6 +60,7 @@ export const NodeSelector: FC = ({ isCodeBlock: ctx.editor.isActive("codeBlock"), isCallout: ctx.editor.isActive("callout"), isDetails: ctx.editor.isActive("details"), + isTransclusionSource: ctx.editor.isActive("transclusionSource"), }; }, }); @@ -122,6 +124,12 @@ export const NodeSelector: FC = ({ .run(), isActive: () => editorState?.isBlockquote, }, + { + name: "Synced block", + icon: IconQuote, + command: () => editor.chain().focus().toggleTransclusionSource().run(), + isActive: () => editorState?.isTransclusionSource, + }, { name: "Code", icon: IconCode, @@ -147,9 +155,14 @@ export const NodeSelector: FC = ({ }; return ( - + - + diff --git a/apps/client/src/features/editor/components/bubble-menu/text-alignment-selector.tsx b/apps/client/src/features/editor/components/bubble-menu/text-alignment-selector.tsx index f8dba1c99..ccfc48382 100644 --- a/apps/client/src/features/editor/components/bubble-menu/text-alignment-selector.tsx +++ b/apps/client/src/features/editor/components/bubble-menu/text-alignment-selector.tsx @@ -7,7 +7,7 @@ import { IconCheck, IconChevronDown, } from "@tabler/icons-react"; -import { Popover, Button, ScrollArea, Tooltip, rem } from "@mantine/core"; +import { Menu, Button, Tooltip, rem } from "@mantine/core"; import type { Editor } from "@tiptap/react"; import { useEditorState } from "@tiptap/react"; import { useTranslation } from "react-i18next"; @@ -82,47 +82,49 @@ export const TextAlignmentSelector: FC = ({ const activeItem = items.filter((item) => item.isActive()).pop() ?? items[0]; return ( - - - + + + - + - - - - {items.map((item, index) => ( - - ))} - - - - + + {items.map((item, index) => ( + } + rightSection={ + activeItem.name === item.name ? : null + } + onClick={() => { + item.command(); + setIsOpen(false); + }} + > + {t(item.name)} + + ))} + + ); }; diff --git a/apps/client/src/features/editor/components/callout/callout-menu.tsx b/apps/client/src/features/editor/components/callout/callout-menu.tsx index 69c836934..3ce022dae 100644 --- a/apps/client/src/features/editor/components/callout/callout-menu.tsx +++ b/apps/client/src/features/editor/components/callout/callout-menu.tsx @@ -16,7 +16,7 @@ import { IconMoodSmile, IconNotes, } from "@tabler/icons-react"; -import { CalloutType, isTextSelected } from "@docmost/editor-ext"; +import { CalloutType, isEditorReady, isTextSelected } from "@docmost/editor-ext"; import { useTranslation } from "react-i18next"; import EmojiPicker from "@/components/ui/emoji-picker.tsx"; import classes from "../common/toolbar-menu.module.css"; @@ -55,7 +55,7 @@ export function CalloutMenu({ editor }: EditorMenuProps) { }); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "callout"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/columns/columns-menu.tsx b/apps/client/src/features/editor/components/columns/columns-menu.tsx index 0ee99508c..4a1f041eb 100644 --- a/apps/client/src/features/editor/components/columns/columns-menu.tsx +++ b/apps/client/src/features/editor/components/columns/columns-menu.tsx @@ -19,7 +19,7 @@ import { IconCopy, IconTrash, } from "@tabler/icons-react"; -import { isTextSelected } from "@docmost/editor-ext"; +import { isEditorReady, isTextSelected } from "@docmost/editor-ext"; import type { WidthMode, ColumnsLayout } from "@docmost/editor-ext"; import { useTranslation } from "react-i18next"; import classes from "../common/toolbar-menu.module.css"; @@ -82,7 +82,7 @@ export function ColumnsMenu({ editor }: EditorMenuProps) { const shouldShow = useCallback( ({ state }: ShouldShowProps) => { - if (!state) return false; + if (!state || !isEditorReady(editor)) return false; if (!editor.isActive("columns")) return false; if (isTextSelected(editor)) return false; if (nodesWithMenus.some((name) => editor.isActive(name))) return false; @@ -121,7 +121,7 @@ export function ColumnsMenu({ editor }: EditorMenuProps) { }); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "columns"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/common/use-alt-text-control.tsx b/apps/client/src/features/editor/components/common/use-alt-text-control.tsx new file mode 100644 index 000000000..1a43f9d79 --- /dev/null +++ b/apps/client/src/features/editor/components/common/use-alt-text-control.tsx @@ -0,0 +1,139 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { Editor } from "@tiptap/react"; +import { + ActionIcon, + Button, + Group, + Paper, + Text, + Textarea, + Tooltip, +} from "@mantine/core"; +import { IconAlt } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; + +const ALT_MAX_LENGTH = 300; + +function sanitizeAlt(value: string): string { + return value + .replace(/[\\\[\]!]/g, "") + .replace(/\s+/g, " ") + .trim(); +} + +type UseAltTextControlArgs = { + editor: Editor; + nodeName: string; + currentAlt: string; +}; + +export function useAltTextControl({ + editor, + nodeName, + currentAlt, +}: UseAltTextControlArgs) { + const { t } = useTranslation(); + const [showInput, setShowInput] = useState(false); + const [draft, setDraft] = useState(""); + + const open = useCallback(() => { + setDraft(currentAlt || ""); + setShowInput(true); + }, [currentAlt]); + + useEffect(() => { + const handler = () => { + if (!editor.isActive(nodeName)) { + setShowInput(false); + } + }; + editor.on("selectionUpdate", handler); + return () => { + editor.off("selectionUpdate", handler); + }; + }, [editor, nodeName]); + + const cancel = useCallback(() => { + setShowInput(false); + }, []); + + const save = useCallback(() => { + editor + .chain() + .focus(undefined, { scrollIntoView: false }) + .updateAttributes(nodeName, { alt: sanitizeAlt(draft) || undefined }) + .run(); + setShowInput(false); + }, [editor, nodeName, draft]); + + const onKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + save(); + } else if (e.key === "Escape") { + e.preventDefault(); + cancel(); + } + }, + [save, cancel], + ); + + const button = ( + + + + + + ); + + const panel = showInput ? ( + + + {t("Alt text")} + + + {t("Describe this for accessibility.")} + + - - props.deleteNode()} /> + props.deleteNode()} + > + diff --git a/apps/client/src/features/editor/components/mention/mention-list.tsx b/apps/client/src/features/editor/components/mention/mention-list.tsx index 3ddf39762..a098a1afe 100644 --- a/apps/client/src/features/editor/components/mention/mention-list.tsx +++ b/apps/client/src/features/editor/components/mention/mention-list.tsx @@ -16,6 +16,7 @@ import { ScrollArea, Text, UnstyledButton, + VisuallyHidden, } from "@mantine/core"; import clsx from "clsx"; import classes from "./mention.module.css"; @@ -36,7 +37,7 @@ import { usePageQuery, } from "@/features/page/queries/page-query"; import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom"; -import { SimpleTree } from "react-arborist"; +import { treeModel } from "@/features/page/tree/model/tree-model"; import { SpaceTreeNode } from "@/features/page/tree/types"; import { useTranslation } from "react-i18next"; import { useQueryEmit } from "@/features/websocket/use-query-emit"; @@ -46,6 +47,8 @@ import { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx"; const MentionList = forwardRef((props, ref) => { const [selectedIndex, setSelectedIndex] = useState(1); const viewportRef = useRef(null); + const [countAnnouncement, setCountAnnouncement] = useState(""); + const [selectionAnnouncement, setSelectionAnnouncement] = useState(""); const { pageSlug, spaceSlug } = useParams(); const { data: page } = usePageQuery({ pageId: extractPageSlugId(pageSlug) }); const { data: space } = useSpaceQuery(spaceSlug); @@ -53,7 +56,6 @@ const MentionList = forwardRef((props, ref) => { const [renderItems, setRenderItems] = useState([]); const { t } = useTranslation(); const [data, setData] = useAtom(treeDataAtom); - const tree = useMemo(() => new SimpleTree(data), [data]); const createPageMutation = useCreatePageMutation(); const emit = useQueryEmit(); const isInCommentContext = props.isInCommentContext ?? false; @@ -184,6 +186,45 @@ const MentionList = forwardRef((props, ref) => { setSelectedIndex(1); }, [suggestion]); + const selectableCount = useMemo( + () => renderItems.filter((item) => item.entityType !== "header").length, + [renderItems], + ); + + useEffect(() => { + if (renderItems.length === 0) { + setCountAnnouncement(t("No results")); + return; + } + setCountAnnouncement( + t("{{count}} result available", { count: selectableCount }), + ); + }, [renderItems.length, selectableCount, t]); + + useEffect(() => { + const item = renderItems[selectedIndex]; + if (!item || item.entityType === "header") { + setSelectionAnnouncement(""); + return; + } + if (item.entityType === "user") { + setSelectionAnnouncement(`${t("People")}: ${item.label}`); + return; + } + if (item.entityType === "page") { + if (item.id === null) { + setSelectionAnnouncement(`${t("Create page")}: ${item.label}`); + return; + } + const pageLabel = item.label || t("Untitled"); + setSelectionAnnouncement( + item.spaceName + ? `${t("Pages")}: ${pageLabel}, ${item.spaceName}` + : `${t("Pages")}: ${pageLabel}`, + ); + } + }, [selectedIndex, renderItems, t]); + useImperativeHandle(ref, () => ({ onKeyDown: ({ event }) => { if (event.key === "ArrowUp") { @@ -220,20 +261,20 @@ const MentionList = forwardRef((props, ref) => { try { createdPage = await createPageMutation.mutateAsync(payload); const parentId = page.id || null; - const data = { + const newNode: SpaceTreeNode = { id: createdPage.id, slugId: createdPage.slugId, name: createdPage.title, position: createdPage.position, spaceId: createdPage.spaceId, parentPageId: createdPage.parentPageId, + hasChildren: false, children: [], - } as any; + }; - const lastIndex = tree.data.length; + const lastIndex = data.length; - tree.create({ parentId, index: lastIndex, data }); - setData(tree.data); + setData(treeModel.insert(data, parentId, newNode, lastIndex)); props.command({ id: uuid7(), @@ -251,7 +292,7 @@ const MentionList = forwardRef((props, ref) => { payload: { parentId, index: lastIndex, - data, + data: newNode, }, }); }, 50); @@ -271,6 +312,9 @@ const MentionList = forwardRef((props, ref) => { if (renderItems.length === 0) { return ( + + {countAnnouncement} + {t("No results")} @@ -287,7 +331,22 @@ const MentionList = forwardRef((props, ref) => { ); return ( - + + + {countAnnouncement} + + + {selectionAnnouncement} + ((props, ref) => { if (item.entityType === "header") { const isFirst = index === 0; return ( -
+
{!isFirst && } ((props, ref) => { selectItem(index)} className={clsx(classes.menuBtn, { [classes.selectedItem]: index === selectedIndex, @@ -348,6 +410,9 @@ const MentionList = forwardRef((props, ref) => { selectItem(index)} className={clsx(classes.menuBtn, { [classes.selectedItem]: index === selectedIndex, @@ -358,7 +423,7 @@ const MentionList = forwardRef((props, ref) => { diff --git a/apps/client/src/features/editor/components/pdf/pdf-menu.tsx b/apps/client/src/features/editor/components/pdf/pdf-menu.tsx index 2104bfbc6..3fc8b6fd1 100644 --- a/apps/client/src/features/editor/components/pdf/pdf-menu.tsx +++ b/apps/client/src/features/editor/components/pdf/pdf-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { useCallback } from "react"; import { Node as PMNode } from "@tiptap/pm/model"; +import { isEditorReady } from "@docmost/editor-ext"; import { EditorMenuProps, ShouldShowProps, @@ -37,9 +38,8 @@ export function PdfMenu({ editor }: EditorMenuProps) { const shouldShow = useCallback( ({ state }: ShouldShowProps) => { - if (!state || !editor.isActive("pdf")) { - return false; - } + if (!state || !isEditorReady(editor)) return false; + if (!editor.isActive("pdf")) return false; const { selection } = state; const dom = editor.view.nodeDOM(selection.from) as HTMLElement | null; @@ -51,7 +51,7 @@ export function PdfMenu({ editor }: EditorMenuProps) { ); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "pdf"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/pdf/pdf-view.tsx b/apps/client/src/features/editor/components/pdf/pdf-view.tsx index 4d06402b5..693d37557 100644 --- a/apps/client/src/features/editor/components/pdf/pdf-view.tsx +++ b/apps/client/src/features/editor/components/pdf/pdf-view.tsx @@ -92,7 +92,20 @@ export default function PdfView(props: NodeViewProps) { if (hasError) { return ( -
+
{ + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleSelect(); + } + }} + role="button" + tabIndex={0} + aria-label={t("Failed to load PDF")} + > {t("Failed to load PDF")} diff --git a/apps/client/src/features/editor/components/search-and-replace/search-and-replace-dialog.tsx b/apps/client/src/features/editor/components/search-and-replace/search-and-replace-dialog.tsx index f5c176613..a6054d222 100644 --- a/apps/client/src/features/editor/components/search-and-replace/search-and-replace-dialog.tsx +++ b/apps/client/src/features/editor/components/search-and-replace/search-and-replace-dialog.tsx @@ -187,12 +187,14 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo position={{ top: 90, right: 50 }} withBorder transitionProps={{ transition: "slide-down" }} + aria-label={t("Find and replace")} > } rightSection={ @@ -217,7 +219,12 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo - + - + caseSensitiveToggle()} + aria-label={t("Match case (Alt+C)")} + aria-pressed={caseSensitive.isCaseSensitive} > replaceButtonToggle()} + aria-label={t("Replace")} + aria-pressed={replaceButton.isReplaceShow} > )} - + @@ -269,6 +290,7 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo } rightSection={
} rightSectionPointerEvents="all" diff --git a/apps/client/src/features/editor/components/slash-menu/command-list.tsx b/apps/client/src/features/editor/components/slash-menu/command-list.tsx index 54d6cd17a..2113d09ab 100644 --- a/apps/client/src/features/editor/components/slash-menu/command-list.tsx +++ b/apps/client/src/features/editor/components/slash-menu/command-list.tsx @@ -10,6 +10,7 @@ import { ScrollArea, Text, UnstyledButton, + VisuallyHidden, } from "@mantine/core"; import classes from "./slash-menu.module.css"; import clsx from "clsx"; @@ -29,6 +30,8 @@ const CommandList = ({ const { t } = useTranslation(); const [selectedIndex, setSelectedIndex] = useState(0); const viewportRef = useRef(null); + const [countAnnouncement, setCountAnnouncement] = useState(""); + const [selectionAnnouncement, setSelectionAnnouncement] = useState(""); const flatItems = useMemo(() => { return Object.values(items).flat(); @@ -79,6 +82,25 @@ const CommandList = ({ setSelectedIndex(0); }, [flatItems]); + useEffect(() => { + if (flatItems.length === 0) { + setCountAnnouncement(""); + return; + } + setCountAnnouncement( + t("{{count}} command available", { count: flatItems.length }), + ); + }, [flatItems.length, t]); + + useEffect(() => { + const item = flatItems[selectedIndex]; + if (!item) { + setSelectionAnnouncement(""); + return; + } + setSelectionAnnouncement(`${t(item.title)}, ${t(item.description)}`); + }, [selectedIndex, flatItems, t]); + useEffect(() => { viewportRef.current ?.querySelector(`[data-item-index="${selectedIndex}"]`) @@ -86,7 +108,21 @@ const CommandList = ({ }, [selectedIndex]); return flatItems.length > 0 ? ( - + + + {countAnnouncement} + + + {selectionAnnouncement} + - {Object.entries(items).map(([category, categoryItems]) => ( -
+ {(() => { + let flatIndex = -1; + return Object.entries(items).map(([category, categoryItems]) => ( +
{category} - {categoryItems.map((item: SlashMenuItemType, index: number) => ( + {categoryItems.map((item: SlashMenuItemType) => { + flatIndex += 1; + const itemIndex = flatIndex; + return ( selectItem(index)} + data-item-index={itemIndex} + key={itemIndex} + id={`slash-command-option-${itemIndex}`} + role="option" + aria-selected={itemIndex === selectedIndex} + onClick={() => selectItem(itemIndex)} className={clsx(classes.menuBtn, { - [classes.selectedItem]: index === selectedIndex, + [classes.selectedItem]: itemIndex === selectedIndex, })} > - + @@ -124,9 +168,11 @@ const CommandList = ({
- ))} + ); + })}
- ))} + )); + })()}
) : null; diff --git a/apps/client/src/features/editor/components/slash-menu/menu-items.ts b/apps/client/src/features/editor/components/slash-menu/menu-items.ts index 31a788004..2ec8aa734 100644 --- a/apps/client/src/features/editor/components/slash-menu/menu-items.ts +++ b/apps/client/src/features/editor/components/slash-menu/menu-items.ts @@ -19,12 +19,15 @@ import { IconTable, IconTypography, IconMenu4, + IconPageBreak, IconCalendar, IconAppWindow, IconSitemap, IconColumns3, IconColumns2, IconTag, + IconMoodSmile, + IconRotate2, } from "@tabler/icons-react"; import { CommandProps, @@ -157,7 +160,7 @@ const CommandGroups: SlashMenuGroupedItemsType = { { title: "Numbered list", description: "Create a list with numbering.", - searchTerms: ["numbered", "ordered", "list"], + searchTerms: ["numbered", "ordered", "list", "ol"], icon: IconListNumbers, command: ({ editor, range }: CommandProps) => { editor.chain().focus().deleteRange(range).toggleOrderedList().run(); @@ -187,6 +190,14 @@ const CommandGroups: SlashMenuGroupedItemsType = { command: ({ editor, range }: CommandProps) => editor.chain().focus().deleteRange(range).setHorizontalRule().run(), }, + { + title: "Page break", + description: "Insert a page break for printing.", + searchTerms: ["page", "break", "pagebreak", "print"], + icon: IconPageBreak, + command: ({ editor, range }: CommandProps) => + editor.chain().focus().deleteRange(range).setPageBreak().run(), + }, { title: "Image", description: "Upload any image from your device.", @@ -256,7 +267,15 @@ const CommandGroups: SlashMenuGroupedItemsType = { { title: "Audio", description: "Upload any audio from your device.", - searchTerms: ["audio", "music", "sound", "mp3", "media", "file", "attachment"], + searchTerms: [ + "audio", + "music", + "sound", + "mp3", + "media", + "file", + "attachment", + ], icon: IconMusic, command: ({ editor, range }) => { editor.chain().focus().deleteRange(range).run(); @@ -493,15 +512,53 @@ const CommandGroups: SlashMenuGroupedItemsType = { .run(); }, }, + { + title: "Emoji", + description: "Insert emoji.", + searchTerms: ["emoji", "icon", "smiley", "emoticon", "symbol", "reaction"], + icon: IconMoodSmile, + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).insertContent(":").run(); + }, + }, { title: "Subpages (Child pages)", description: "List all subpages of the current page", - searchTerms: ["subpages", "child", "children", "nested", "hierarchy"], + searchTerms: [ + "subpages", + "child", + "children", + "nested", + "hierarchy", + "toc", + ], icon: IconSitemap, command: ({ editor, range }: CommandProps) => { editor.chain().focus().deleteRange(range).insertSubpages().run(); }, }, + { + title: "Synced block", + description: "Create a block that stays in sync across pages.", + searchTerms: [ + "sync", + "synced", + "synced block", + "excerpt", + "transclusion", + "reusable", + "snippet", + ], + icon: IconRotate2, + command: ({ editor, range }: CommandProps) => { + editor + .chain() + .focus() + .deleteRange(range) + .insertTransclusionSource() + .run(); + }, + }, { title: "Database", description: "Insert an inline database on this page", diff --git a/apps/client/src/features/editor/components/status/status-view.tsx b/apps/client/src/features/editor/components/status/status-view.tsx index d8f10d794..3cca4b597 100644 --- a/apps/client/src/features/editor/components/status/status-view.tsx +++ b/apps/client/src/features/editor/components/status/status-view.tsx @@ -92,8 +92,17 @@ export default function StatusView(props: NodeViewProps) { colorClassMap[color], )} onClick={() => isEditable && setOpened(true)} + onKeyDown={(e) => { + if (isEditable && (e.key === "Enter" || e.key === " ")) { + e.preventDefault(); + setOpened(true); + } + }} role="button" tabIndex={0} + aria-label={text || "SET STATUS"} + aria-haspopup="dialog" + aria-expanded={opened} > {text || "SET STATUS"} @@ -127,6 +136,16 @@ export default function StatusView(props: NodeViewProps) { )} style={{ backgroundColor: bg }} onClick={() => handleColorChange(name)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + handleColorChange(name); + } + }} + role="button" + tabIndex={0} + aria-label={name} + aria-pressed={color === name} > {color === name && } diff --git a/apps/client/src/features/editor/components/subpages/subpages-menu.tsx b/apps/client/src/features/editor/components/subpages/subpages-menu.tsx index 9f0544e67..a626e1ee2 100644 --- a/apps/client/src/features/editor/components/subpages/subpages-menu.tsx +++ b/apps/client/src/features/editor/components/subpages/subpages-menu.tsx @@ -6,6 +6,7 @@ import { ActionIcon, Tooltip } from "@mantine/core"; import { IconTrash } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { Editor } from "@tiptap/core"; +import { isEditorReady } from "@docmost/editor-ext"; interface SubpagesMenuProps { editor: Editor; @@ -33,6 +34,7 @@ export const SubpagesMenu = React.memo( ); const getReferenceClientRect = useCallback(() => { + if (!isEditorReady(editor)) return new DOMRect(); const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "subpages"; const parent = findParentNode(predicate)(selection); diff --git a/apps/client/src/features/editor/components/table-of-contents/table-of-contents.tsx b/apps/client/src/features/editor/components/table-of-contents/table-of-contents.tsx index 123889f32..4b76e08dd 100644 --- a/apps/client/src/features/editor/components/table-of-contents/table-of-contents.tsx +++ b/apps/client/src/features/editor/components/table-of-contents/table-of-contents.tsx @@ -3,7 +3,7 @@ import { TextSelection } from "@tiptap/pm/state"; import React, { FC, useEffect, useRef, useState } from "react"; import classes from "./table-of-contents.module.css"; import clsx from "clsx"; -import { Box, Text } from "@mantine/core"; +import { Box, Text, Title } from "@mantine/core"; import { useTranslation } from "react-i18next"; type TableOfContentsProps = { @@ -25,7 +25,7 @@ const recalculateLinks = (nodePos: NodePos[]) => { (acc, item) => { const label = item.node.textContent; const level = Number(item.node.attrs.level); - if (label.length && level <= 3) { + if (label.length && level <= 4) { acc.push({ label, level, @@ -156,9 +156,9 @@ export const TableOfContents: FC = (props) => { return ( <> {props.isShare && ( - + {t("Table of contents")} - </Text> + )}
{links.map((item, idx) => ( diff --git a/apps/client/src/features/editor/components/table/handle/cell-chevron.tsx b/apps/client/src/features/editor/components/table/handle/cell-chevron.tsx new file mode 100644 index 000000000..db79844e8 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/cell-chevron.tsx @@ -0,0 +1,126 @@ +import React, { useCallback, useEffect } from "react"; +import type { Editor } from "@tiptap/react"; +import { useEditorState } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { TextSelection } from "@tiptap/pm/state"; +import { columnResizingPluginKey } from "@tiptap/pm/tables"; +import { useFloating, offset, autoUpdate, hide } from "@floating-ui/react"; +import { Menu, UnstyledButton } from "@mantine/core"; +import { IconChevronDown } from "@tabler/icons-react"; +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; +import { isCellSelection } from "@docmost/editor-ext"; +import { CellChevronMenu } from "./menus/cell-chevron-menu"; +import classes from "./handle.module.css"; + +interface CellChevronProps { + editor: Editor; + cellPos: number; + tableNode: ProseMirrorNode; + tablePos: number; +} + +export const CellChevron = React.memo(function CellChevron({ + editor, + cellPos, + tableNode, + tablePos, +}: CellChevronProps) { + const { t } = useTranslation(); + const cellDom = editor.view.nodeDOM(cellPos) as HTMLElement | null; + + const { refs, floatingStyles, middlewareData } = useFloating({ + placement: "top-end", + // crossAxis pulls the chevron INWARD from the cell's right edge. We need + // enough inset that we don't overlap PM-tables' column-resize hot zone + // (~5px wide around the column boundary). Without this, hovering near the + // column edge picks up the chevron's `cursor: pointer` instead of + // `col-resize`, and a drag near the edge clicks the chevron. + middleware: [offset({ mainAxis: -22, crossAxis: -10 }), hide()], + whileElementsMounted: autoUpdate, + strategy: "absolute", + }); + const isReferenceHidden = !!middlewareData.hide?.referenceHidden; + + useEffect(() => { + refs.setReference(cellDom); + }, [cellDom, refs]); + + // Hide the chevron while the user is resizing a column. PM-tables sets + // `activeHandle > -1` whenever the mouse is near a column boundary OR + // actively dragging it. Either way we don't want the chevron in the way. + const isResizingColumn = useEditorState({ + editor, + selector: (ctx) => { + if (!ctx.editor) return false; + const state = columnResizingPluginKey.getState(ctx.editor.state) as + | { activeHandle: number } + | undefined; + return !!state && state.activeHandle > -1; + }, + }); + + const onOpen = useCallback(() => { + const current = editor.state.selection; + + // Preserve an existing multi-cell CellSelection that already covers + // this cell so merge etc. operate on the user's whole range. + let preserveExisting = false; + if (isCellSelection(current)) { + current.forEachCell((_node, pos) => { + if (pos === cellPos) preserveExisting = true; + }); + } + + if (!preserveExisting) { + // Drop a collapsed cursor inside the cell rather than a single-cell + // CellSelection — PM-tables paints the latter as a text-range + // highlight on the cell content. + try { + const $inside = editor.state.doc.resolve(cellPos + 1); + const sel = TextSelection.near($inside, 1); + editor.view.dispatch(editor.state.tr.setSelection(sel)); + } catch {} + } + editor.commands.freezeHandles(); + }, [editor, cellPos]); + + const onClose = useCallback(() => { + editor.commands.unfreezeHandles(); + }, [editor]); + + if (!cellDom) return null; + if (isResizingColumn) return null; + + return ( + + + + + + + + + + + ); +}); diff --git a/apps/client/src/features/editor/components/table/handle/column-handle.tsx b/apps/client/src/features/editor/components/table/handle/column-handle.tsx new file mode 100644 index 000000000..a46ac50d5 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/column-handle.tsx @@ -0,0 +1,132 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { useFloating, offset, autoUpdate, hide } from "@floating-ui/react"; +import { Menu } from "@mantine/core"; +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; +import { useTableHandleDrag } from "./hooks/use-table-handle-drag"; +import { useColumnRowMenuLifecycle } from "./hooks/use-column-row-menu-lifecycle"; +import { ColumnHandleMenu } from "./menus/column-handle-menu"; +import classes from "./handle.module.css"; + +interface ColumnHandleProps { + editor: Editor; + index: number; + anchorPos: number; + tableNode: ProseMirrorNode; + tablePos: number; +} + +export const ColumnHandle = React.memo(function ColumnHandle({ + editor, + index, + anchorPos, + tableNode, + tablePos, +}: ColumnHandleProps) { + const { t } = useTranslation(); + // Hold the cell DOM in a ref-backed state so we never unmount the handle + // mid-drag. A remote edit can transiently flip `nodeDOM(anchorPos)` to null + // (the plugin re-emits `hoveringCell` with the mapped pos a tick later); + // unmounting the source element here would make pragmatic-dnd silently + // abort the active drag. + // `nodeDOM` is typed as `Node | null` — when `anchorPos` goes stale (e.g. + // an external drop reflows the doc before the plugin re-emits + // hoveringCell), it can resolve to a Text node, on which `.closest` is + // undefined. Filter to HTMLElement so downstream consumers stay safe. + const lookupDom = editor.view.nodeDOM(anchorPos); + const lookupCellDom = lookupDom instanceof HTMLElement ? lookupDom : null; + const [cellDom, setCellDom] = useState(lookupCellDom); + const lastCellDomRef = useRef(lookupCellDom); + useEffect(() => { + if (lookupCellDom && lookupCellDom !== lastCellDomRef.current) { + lastCellDomRef.current = lookupCellDom; + setCellDom(lookupCellDom); + } + }, [lookupCellDom]); + + const [handleEl, setHandleEl] = useState(null); + + const { refs, floatingStyles, middlewareData } = useFloating({ + placement: "top", + middleware: [offset(-4), hide()], + whileElementsMounted: autoUpdate, + }); + const isReferenceHidden = !!middlewareData.hide?.referenceHidden; + + useEffect(() => { + refs.setReference(cellDom); + }, [cellDom, refs]); + + // `cellDom` is inside the table, so `closest('.tableWrapper')` finds the + // wrapper for this drag's auto-scroll. The handle itself lives in a + // floating layer outside the editor DOM, so we can't walk up from it. + const wrapper = cellDom?.closest(".tableWrapper") ?? null; + + const [menuOpened, setMenuOpened] = useState(false); + const closeMenu = useCallback(() => setMenuOpened(false), []); + useTableHandleDrag(editor, "col", handleEl, wrapper, closeMenu); + + const { onOpen, onClose } = useColumnRowMenuLifecycle({ + editor, + orientation: "col", + index, + tableNode, + tablePos, + }); + + if (!cellDom) return null; + + return ( + + +
{ + refs.setFloating(node); + setHandleEl(node); + }} + style={{ + ...floatingStyles, + ...(isReferenceHidden ? { visibility: "hidden" as const } : {}), + }} + className={clsx(classes.handle, classes.columnHandle)} + role="button" + tabIndex={0} + aria-label={t("Column actions")} + > + + + +
+
+ + + +
+ ); +}); + +function GripIcon() { + return ( + + + + ); +} diff --git a/apps/client/src/features/editor/components/table/handle/handle.module.css b/apps/client/src/features/editor/components/table/handle/handle.module.css new file mode 100644 index 000000000..e7d9ac124 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/handle.module.css @@ -0,0 +1,108 @@ +.handle { + position: absolute; + z-index: 50; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + color: rgba(55, 53, 47, 0.45); + background: var(--mantine-color-body); + border: 1px solid rgba(55, 53, 47, 0.12); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + cursor: grab; + padding: 0; + transition: background-color 120ms ease, color 120ms ease; + + @mixin dark { + color: rgba(255, 255, 255, 0.55); + background: var(--mantine-color-dark-7); + border-color: rgba(255, 255, 255, 0.12); + } +} + +.handle:hover { + background: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-dark-5) + ); + color: light-dark( + var(--mantine-color-gray-7), + var(--mantine-color-dark-0) + ); +} + +.handle:active { + cursor: grabbing; +} + +.columnHandle { + width: 28px; + height: 16px; +} + +.columnHandle svg { + transform: rotate(90deg); +} + +.rowHandle { + width: 16px; + height: 28px; +} + +@media (max-width: 600px) { + .handle { + display: none; + } +} + +.cellChevron { + position: absolute; + z-index: 50; + width: 18px; + height: 18px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + color: light-dark( + var(--mantine-color-gray-7), + var(--mantine-color-dark-1) + ); + background: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-dark-5) + ); + border: 1px solid rgba(55, 53, 47, 0.12); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + cursor: pointer; + padding: 0; + transition: background-color 120ms ease, color 120ms ease; + + @mixin dark { + border-color: rgba(255, 255, 255, 0.12); + } +} + +.cellChevron:hover { + background: light-dark( + var(--mantine-color-gray-2), + var(--mantine-color-dark-4) + ); + color: light-dark( + var(--mantine-color-gray-8), + var(--mantine-color-dark-0) + ); +} + +@media (max-width: 600px) { + .cellChevron { + display: none; + } +} + +@media print { + .handle, + .cellChevron { + display: none !important; + } +} diff --git a/apps/client/src/features/editor/components/table/handle/hooks/use-column-row-menu-lifecycle.ts b/apps/client/src/features/editor/components/table/handle/hooks/use-column-row-menu-lifecycle.ts new file mode 100644 index 000000000..a30595597 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/hooks/use-column-row-menu-lifecycle.ts @@ -0,0 +1,40 @@ +import { useCallback } from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { buildRowOrColumnSelection, Orientation } from "../lib/select-row-column"; + +interface Args { + editor: Editor; + orientation: Orientation; + index: number; + tableNode: ProseMirrorNode; + tablePos: number; +} + +export function useColumnRowMenuLifecycle({ + editor, + orientation, + index, + tableNode, + tablePos, +}: Args) { + const onOpen = useCallback(() => { + const selection = buildRowOrColumnSelection( + editor.state, + tableNode, + tablePos, + orientation, + index, + ); + const tr = editor.state.tr; + if (selection) tr.setSelection(selection); + editor.view.dispatch(tr); + editor.commands.freezeHandles(); + }, [editor, orientation, index, tableNode, tablePos]); + + const onClose = useCallback(() => { + editor.commands.unfreezeHandles(); + }, [editor]); + + return { onOpen, onClose }; +} diff --git a/apps/client/src/features/editor/components/table/handle/hooks/use-table-clear.ts b/apps/client/src/features/editor/components/table/handle/hooks/use-table-clear.ts new file mode 100644 index 000000000..1bd4cb209 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/hooks/use-table-clear.ts @@ -0,0 +1,54 @@ +import { useCallback } from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { TableMap } from "@tiptap/pm/tables"; + +type Scope = + | { kind: "col"; index: number } + | { kind: "row"; index: number } + | { kind: "cell"; cellPos: number }; + +export function useTableClear( + editor: Editor, + tableNode: ProseMirrorNode, + tablePos: number, + scope: Scope, +) { + return useCallback(() => { + const tr = editor.state.tr; + const tableStart = tablePos + 1; + const map = TableMap.get(tableNode); + const paragraph = editor.schema.nodes.paragraph; + if (!paragraph) return; + + const cellOffsets: number[] = []; + + if (scope.kind === "col") { + for (let row = 0; row < map.height; row++) { + cellOffsets.push(map.map[row * map.width + scope.index]); + } + } else if (scope.kind === "row") { + for (let col = 0; col < map.width; col++) { + cellOffsets.push(map.map[scope.index * map.width + col]); + } + } + + const targets = + scope.kind === "cell" + ? [scope.cellPos] + : Array.from(new Set(cellOffsets)).map((o) => tableStart + o); + + // Process in reverse position order so earlier replacements don't shift later ones. + targets.sort((a, b) => b - a); + + for (const cellPos of targets) { + const node = tr.doc.nodeAt(cellPos); + if (!node) continue; + const start = cellPos + 1; + const end = cellPos + node.nodeSize - 1; + tr.replaceWith(start, end, paragraph.create()); + } + + if (tr.docChanged) editor.view.dispatch(tr); + }, [editor, tableNode, tablePos, scope]); +} diff --git a/apps/client/src/features/editor/components/table/handle/hooks/use-table-handle-drag.ts b/apps/client/src/features/editor/components/table/handle/hooks/use-table-handle-drag.ts new file mode 100644 index 000000000..30b179689 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/hooks/use-table-handle-drag.ts @@ -0,0 +1,79 @@ +import { useEffect } from "react"; +import type { Editor } from "@tiptap/react"; +import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; +import { draggable } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; +import { disableNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview"; +import { + autoScrollForElements, + autoScrollWindowForElements, +} from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; +import { getTableHandlePluginSpec } from "@docmost/editor-ext"; + +// Uses pragmatic-drag-and-drop instead of native HTML5 DnD because the native +// dragstart→dragover→drop lifecycle was being silently cancelled +export function useTableHandleDrag( + editor: Editor, + orientation: "col" | "row", + element: HTMLElement | null, + wrapper: HTMLElement | null, + onDragStart?: () => void, +) { + useEffect(() => { + if (!element) return; + + return combine( + draggable({ + element, + getInitialData: () => ({ type: `table-${orientation}` }), + onGenerateDragPreview: ({ nativeSetDragImage }) => { + // We render our own floating preview via PreviewController, so hide + // the native drag image entirely. + disableNativeDragPreview({ nativeSetDragImage }); + }, + onDragStart: ({ location }) => { + // The menu (if open from a prior click on the handle) won't dismiss + // on its own — pragmatic-dnd swallows the events Mantine listens for. + onDragStart?.(); + const spec = getTableHandlePluginSpec(editor); + if (!spec) return; + const { clientX, clientY } = location.initial.input; + spec.startDragFromHandle(orientation, clientX, clientY); + }, + onDrag: ({ location }) => { + const spec = getTableHandlePluginSpec(editor); + if (!spec) return; + const { clientX, clientY } = location.current.input; + spec.updateDragPosition(clientX, clientY); + }, + onDrop: ({ location }) => { + const spec = getTableHandlePluginSpec(editor); + if (!spec) return; + const { clientX, clientY } = location.current.input; + // Make sure the final position is recorded before committing the drop. + spec.updateDragPosition(clientX, clientY); + spec.commitDrop(); + spec.endDrag(); + }, + }), + // Wrapper owns horizontal auto-scroll (it has `overflow-x: auto`); + // window owns vertical. Locking each axis prevents the window's + // horizontal auto-scroll from running when the cursor approaches + // the viewport edge — without the cap, the preview's `left` follows + // the cursor past the viewport, the page widens to contain it, the + // plugin scrolls the now-wider page further, and the loop never + // ends. + // Only the column handle registers wrapper auto-scroll (rows can't + // scroll horizontally) — registering twice on the same wrapper + // triggers a dev-mode warning from pragmatic-dnd-auto-scroll. + orientation === "col" && + wrapper && + !wrapper.classList.contains("tableWrapperNoOverflow") + ? autoScrollForElements({ + element: wrapper, + getAllowedAxis: () => "horizontal", + }) + : () => {}, + autoScrollWindowForElements({ getAllowedAxis: () => "vertical" }), + ); + }, [editor, orientation, element, wrapper, onDragStart]); +} diff --git a/apps/client/src/features/editor/components/table/handle/hooks/use-table-handle-state.ts b/apps/client/src/features/editor/components/table/handle/hooks/use-table-handle-state.ts new file mode 100644 index 000000000..ab8893566 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/hooks/use-table-handle-state.ts @@ -0,0 +1,23 @@ +import type { Editor } from "@tiptap/react"; +import { useEditorState } from "@tiptap/react"; +import { TableDndKey, TableHandleState } from "@docmost/editor-ext"; + +const FALLBACK: TableHandleState = { + hoveringCell: null, + tableNode: null, + tablePos: null, + dragging: null, + frozen: false, +}; + +export function useTableHandleState(editor: Editor | null): TableHandleState { + const state = useEditorState({ + editor, + selector: (ctx) => { + if (!ctx.editor) return null; + return TableDndKey.getState(ctx.editor.state) ?? null; + }, + }); + + return state ?? FALLBACK; +} diff --git a/apps/client/src/features/editor/components/table/handle/hooks/use-table-move-row-column.ts b/apps/client/src/features/editor/components/table/handle/hooks/use-table-move-row-column.ts new file mode 100644 index 000000000..476c68f8d --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/hooks/use-table-move-row-column.ts @@ -0,0 +1,50 @@ +import { useCallback, useMemo } from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { TableMap } from "@tiptap/pm/tables"; +import { moveColumn, moveRow } from "@docmost/editor-ext"; + +export type MoveDirection = "left" | "right" | "up" | "down"; + +export function useTableMoveRowColumn( + editor: Editor, + orientation: "col" | "row", + index: number, + direction: MoveDirection, + tableNode: ProseMirrorNode, + tablePos: number, +) { + const target = + direction === "left" || direction === "up" ? index - 1 : index + 1; + + const maxIndex = useMemo(() => { + const map = TableMap.get(tableNode); + return orientation === "col" ? map.width - 1 : map.height - 1; + }, [tableNode, orientation]); + + const canMove = target >= 0 && target <= maxIndex; + + const handleMove = useCallback(() => { + if (!canMove) return; + const tr = editor.state.tr; + const moved = + orientation === "col" + ? moveColumn({ + tr, + originIndex: index, + targetIndex: target, + select: true, + pos: tablePos + 1, + }) + : moveRow({ + tr, + originIndex: index, + targetIndex: target, + select: true, + pos: tablePos + 1, + }); + if (moved) editor.view.dispatch(tr); + }, [editor, orientation, index, target, tablePos, canMove]); + + return { canMove, handleMove }; +} diff --git a/apps/client/src/features/editor/components/table/handle/hooks/use-table-sort.ts b/apps/client/src/features/editor/components/table/handle/hooks/use-table-sort.ts new file mode 100644 index 000000000..afc6a2774 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/hooks/use-table-sort.ts @@ -0,0 +1,100 @@ +import { useCallback, useMemo } from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { + convertArrayOfRowsToTableNode, + convertTableNodeToArrayOfRows, + transpose, +} from "@docmost/editor-ext"; +import { + getCellSortText, + isCellEmpty, + isHeaderCell, + type SortDirection, + type SortableItem, + sortItems, + weaveItems, +} from "../lib/sort-cells"; + +interface Args { + editor: Editor; + orientation: "col" | "row"; + index: number; + tableNode: ProseMirrorNode; + tablePos: number; + direction: SortDirection; +} + +function tableHasMergedCells(tableNode: ProseMirrorNode): boolean { + for (let r = 0; r < tableNode.childCount; r++) { + const row = tableNode.child(r); + for (let c = 0; c < row.childCount; c++) { + const { colspan = 1, rowspan = 1 } = row.child(c).attrs; + if (colspan > 1 || rowspan > 1) return true; + } + } + return false; +} + +function isAllHeader(cells: (ProseMirrorNode | null)[]): boolean { + return cells.every((c) => c !== null && isHeaderCell(c)); +} + +export function useTableSort({ + editor, + orientation, + index, + tableNode, + tablePos, + direction, +}: Args) { + const canSort = useMemo(() => { + if (tableHasMergedCells(tableNode)) return false; + + const rows = convertTableNodeToArrayOfRows(tableNode); + const axes = orientation === "col" ? rows : transpose(rows); + if (axes.length < 2) return false; + + return axes.some((cells) => { + if (isAllHeader(cells)) return false; + const sortCell = cells[index]; + return !!sortCell && !isCellEmpty(sortCell); + }); + }, [tableNode, orientation, index]); + + const handleSort = useCallback(() => { + if (!canSort) return; + + const rows = convertTableNodeToArrayOfRows(tableNode); + const axes = orientation === "col" ? rows : transpose(rows); + + const items: SortableItem<(ProseMirrorNode | null)[]>[] = axes.map( + (cells, originalOrder) => { + const sortCell = cells[index]; + return { + payload: cells, + text: sortCell ? getCellSortText(sortCell) : "", + isHeader: isAllHeader(cells), + isEmpty: !sortCell || isCellEmpty(sortCell), + originalOrder, + }; + }, + ); + + const dataItems = items.filter((it) => !it.isHeader); + const sortedData = sortItems(dataItems, direction); + const woven = weaveItems(items, sortedData); + + const newAxes = woven.map((it) => it.payload); + const newRows = orientation === "col" ? newAxes : transpose(newAxes); + + const newTable = convertArrayOfRowsToTableNode(tableNode, newRows); + + const tr = editor.state.tr; + tr.replaceWith(tablePos, tablePos + tableNode.nodeSize, newTable); + + if (tr.docChanged) editor.view.dispatch(tr); + }, [editor, tableNode, tablePos, orientation, index, direction, canSort]); + + return { canSort, handleSort }; +} diff --git a/apps/client/src/features/editor/components/table/handle/lib/select-row-column.ts b/apps/client/src/features/editor/components/table/handle/lib/select-row-column.ts new file mode 100644 index 000000000..5ef315cf1 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/lib/select-row-column.ts @@ -0,0 +1,34 @@ +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import type { EditorState } from "@tiptap/pm/state"; +import { CellSelection, TableMap } from "@tiptap/pm/tables"; + +export type Orientation = "col" | "row"; + +export function buildRowOrColumnSelection( + state: EditorState, + tableNode: ProseMirrorNode, + tablePos: number, + orientation: Orientation, + index: number, +): CellSelection | null { + const map = TableMap.get(tableNode); + const tableStart = tablePos + 1; + + if (orientation === "col") { + if (index < 0 || index >= map.width) return null; + const firstCellPos = tableStart + map.map[index]; + const lastCellPos = + tableStart + map.map[(map.height - 1) * map.width + index]; + const $first = state.doc.resolve(firstCellPos); + const $last = state.doc.resolve(lastCellPos); + return CellSelection.colSelection($first, $last); + } + + if (index < 0 || index >= map.height) return null; + const firstCellPos = tableStart + map.map[index * map.width]; + const lastCellPos = + tableStart + map.map[index * map.width + (map.width - 1)]; + const $first = state.doc.resolve(firstCellPos); + const $last = state.doc.resolve(lastCellPos); + return CellSelection.rowSelection($first, $last); +} diff --git a/apps/client/src/features/editor/components/table/handle/lib/sort-cells.ts b/apps/client/src/features/editor/components/table/handle/lib/sort-cells.ts new file mode 100644 index 000000000..ffd039c2b --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/lib/sort-cells.ts @@ -0,0 +1,57 @@ +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; + +export type SortDirection = "asc" | "desc"; + +export interface SortableItem { + payload: T; + text: string; + isHeader: boolean; + isEmpty: boolean; + originalOrder: number; +} + +const HEADER_TYPE_NAMES = new Set(["tableHeader", "table_header"]); + +export function isHeaderCell(node: ProseMirrorNode): boolean { + if (HEADER_TYPE_NAMES.has(node.type.name)) return true; + return node.attrs?.header === true; +} + +export function getCellSortText(node: ProseMirrorNode): string { + let text = ""; + node.descendants((child) => { + if (child.isText) text += child.text ?? ""; + return true; + }); + return text.trim().toLowerCase(); +} + +export function isCellEmpty(node: ProseMirrorNode): boolean { + return getCellSortText(node) === ""; +} + +export const collator = new Intl.Collator(undefined, { + sensitivity: "base", + numeric: true, +}); + +export function sortItems( + data: SortableItem[], + direction: SortDirection, +): SortableItem[] { + return [...data].sort((a, b) => { + if (a.isEmpty && !b.isEmpty) return 1; + if (!a.isEmpty && b.isEmpty) return -1; + if (a.isEmpty && b.isEmpty) return a.originalOrder - b.originalOrder; + const cmp = collator.compare(a.text, b.text); + return direction === "asc" ? cmp : -cmp; + }); +} + +export function weaveItems( + all: SortableItem[], + sortedData: SortableItem[], +): SortableItem[] { + const dataQueue = [...sortedData]; + return all.map((item) => (item.isHeader ? item : dataQueue.shift()!)); +} diff --git a/apps/client/src/features/editor/components/table/handle/menus/alignment-submenu.tsx b/apps/client/src/features/editor/components/table/handle/menus/alignment-submenu.tsx new file mode 100644 index 000000000..c58f5a967 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/menus/alignment-submenu.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import type { Editor } from "@tiptap/react"; +import { Menu } from "@mantine/core"; +import { + IconAlignCenter, + IconAlignLeft, + IconAlignRight, +} from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; + +interface AlignmentSubmenuProps { + editor: Editor; +} + +export const AlignmentSubmenu = React.memo(function AlignmentSubmenu({ + editor, +}: AlignmentSubmenuProps) { + const { t } = useTranslation(); + + return ( + + + }> + {t("Text alignment")} + + + + } + onClick={() => editor.chain().focus().setTextAlign("left").run()} + > + {t("Align left")} + + } + onClick={() => editor.chain().focus().setTextAlign("center").run()} + > + {t("Align center")} + + } + onClick={() => editor.chain().focus().setTextAlign("right").run()} + > + {t("Align right")} + + + + ); +}); diff --git a/apps/client/src/features/editor/components/table/handle/menus/cell-chevron-menu.tsx b/apps/client/src/features/editor/components/table/handle/menus/cell-chevron-menu.tsx new file mode 100644 index 000000000..84f904ca7 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/menus/cell-chevron-menu.tsx @@ -0,0 +1,154 @@ +import React from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { ColorSwatch, Menu } from "@mantine/core"; +import { + IconBoxMargin, + IconColumnInsertRight, + IconColumnRemove, + IconEraser, + IconPalette, + IconRowInsertBottom, + IconRowRemove, + IconSquareToggle, + IconTableRow, +} from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { useTableClear } from "../hooks/use-table-clear"; +import { TABLE_COLORS } from "../../table-background-color"; +import { AlignmentSubmenu } from "./alignment-submenu"; + +interface CellChevronMenuProps { + editor: Editor; + cellPos: number; + tableNode: ProseMirrorNode; + tablePos: number; +} + +export const CellChevronMenu = React.memo(function CellChevronMenu({ + editor, + cellPos, + tableNode, + tablePos, +}: CellChevronMenuProps) { + const { t } = useTranslation(); + + const clearCell = useTableClear(editor, tableNode, tablePos, { + kind: "cell", + cellPos, + }); + + const setBackground = (color: string, name: string) => { + editor + .chain() + .focus() + .updateAttributes("tableCell", { + backgroundColor: color || null, + backgroundColorName: color ? name : null, + }) + .updateAttributes("tableHeader", { + backgroundColor: color || null, + backgroundColorName: color ? name : null, + }) + .run(); + }; + + return ( + <> + + + }> + {t("Background color")} + + + +
+ {TABLE_COLORS.map((c) => ( + + ))} +
+
+
+ + + + } + onClick={() => editor.chain().focus().mergeCells().run()} + disabled={!editor.can().mergeCells()} + > + {t("Merge cells")} + + } + onClick={() => editor.chain().focus().splitCell().run()} + disabled={!editor.can().splitCell()} + > + {t("Split cell")} + + } + onClick={() => editor.chain().focus().toggleHeaderCell().run()} + > + {t("Toggle header cell")} + + + + + } + onClick={() => editor.chain().focus().addColumnAfter().run()} + > + {t("Add column right")} + + } + onClick={() => editor.chain().focus().addRowAfter().run()} + > + {t("Add row below")} + + + } onClick={clearCell}> + {t("Clear cell")} + + } + onClick={() => editor.chain().focus().deleteColumn().run()} + > + {t("Delete column")} + + } + onClick={() => editor.chain().focus().deleteRow().run()} + > + {t("Delete row")} + + + ); +}); diff --git a/apps/client/src/features/editor/components/table/handle/menus/column-handle-menu.tsx b/apps/client/src/features/editor/components/table/handle/menus/column-handle-menu.tsx new file mode 100644 index 000000000..8dbe9d326 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/menus/column-handle-menu.tsx @@ -0,0 +1,177 @@ +import React from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { ColorSwatch, Menu } from "@mantine/core"; +import { TABLE_COLORS } from "../../table-background-color"; +import { + IconArrowLeft, + IconArrowRight, + IconColumnInsertLeft, + IconColumnInsertRight, + IconColumnRemove, + IconEraser, + IconPalette, + IconSortAscendingLetters, + IconSortDescendingLetters, +} from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { useTableMoveRowColumn } from "../hooks/use-table-move-row-column"; +import { useTableClear } from "../hooks/use-table-clear"; +import { useTableSort } from "../hooks/use-table-sort"; +import { AlignmentSubmenu } from "./alignment-submenu"; + +interface ColumnHandleMenuProps { + editor: Editor; + index: number; + tableNode: ProseMirrorNode; + tablePos: number; +} + +export const ColumnHandleMenu = React.memo(function ColumnHandleMenu({ + editor, + index, + tableNode, + tablePos, +}: ColumnHandleMenuProps) { + const { t } = useTranslation(); + + const moveLeft = useTableMoveRowColumn(editor, "col", index, "left", tableNode, tablePos); + const moveRight = useTableMoveRowColumn(editor, "col", index, "right", tableNode, tablePos); + const clearCol = useTableClear(editor, tableNode, tablePos, { + kind: "col", + index, + }); + + const setBackground = (color: string, name: string) => { + editor + .chain() + .focus() + .updateAttributes("tableCell", { + backgroundColor: color || null, + backgroundColorName: color ? name : null, + }) + .updateAttributes("tableHeader", { + backgroundColor: color || null, + backgroundColorName: color ? name : null, + }) + .run(); + }; + + const sortAsc = useTableSort({ + editor, + orientation: "col", + index, + tableNode, + tablePos, + direction: "asc", + }); + const sortDesc = useTableSort({ + editor, + orientation: "col", + index, + tableNode, + tablePos, + direction: "desc", + }); + + return ( + <> + } + onClick={sortAsc.handleSort} + disabled={!sortAsc.canSort} + > + {t("Sort A → Z")} + + } + onClick={sortDesc.handleSort} + disabled={!sortDesc.canSort} + > + {t("Sort Z → A")} + + + + + + }> + {t("Background color")} + + + +
+ {TABLE_COLORS.map((c) => ( + + ))} +
+
+
+ + + + + + } + onClick={() => editor.chain().focus().addColumnBefore().run()} + > + {t("Add column left")} + + } + onClick={() => editor.chain().focus().addColumnAfter().run()} + > + {t("Add column right")} + + + + + } + onClick={clearCol} + > + {t("Clear cells")} + + } + onClick={() => editor.chain().focus().deleteColumn().run()} + > + {t("Delete column")} + + + + + } + onClick={moveLeft.handleMove} + disabled={!moveLeft.canMove} + > + {t("Move column left")} + + } + onClick={moveRight.handleMove} + disabled={!moveRight.canMove} + > + {t("Move column right")} + + + ); +}); diff --git a/apps/client/src/features/editor/components/table/handle/menus/row-handle-menu.tsx b/apps/client/src/features/editor/components/table/handle/menus/row-handle-menu.tsx new file mode 100644 index 000000000..13b968b76 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/menus/row-handle-menu.tsx @@ -0,0 +1,138 @@ +import React from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { ColorSwatch, Menu } from "@mantine/core"; +import { TABLE_COLORS } from "../../table-background-color"; +import { + IconArrowDown, + IconArrowUp, + IconEraser, + IconPalette, + IconRowInsertBottom, + IconRowInsertTop, + IconRowRemove, +} from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { useTableMoveRowColumn } from "../hooks/use-table-move-row-column"; +import { useTableClear } from "../hooks/use-table-clear"; +import { AlignmentSubmenu } from "./alignment-submenu"; + +interface RowHandleMenuProps { + editor: Editor; + index: number; + tableNode: ProseMirrorNode; + tablePos: number; +} + +export const RowHandleMenu = React.memo(function RowHandleMenu({ + editor, + index, + tableNode, + tablePos, +}: RowHandleMenuProps) { + const { t } = useTranslation(); + + const setBackground = (color: string, name: string) => { + editor + .chain() + .focus() + .updateAttributes("tableCell", { + backgroundColor: color || null, + backgroundColorName: color ? name : null, + }) + .updateAttributes("tableHeader", { + backgroundColor: color || null, + backgroundColorName: color ? name : null, + }) + .run(); + }; + + const moveUp = useTableMoveRowColumn(editor, "row", index, "up", tableNode, tablePos); + const moveDown = useTableMoveRowColumn(editor, "row", index, "down", tableNode, tablePos); + const clearRow = useTableClear(editor, tableNode, tablePos, { + kind: "row", + index, + }); + + return ( + <> + + + }> + {t("Background color")} + + + +
+ {TABLE_COLORS.map((c) => ( + + ))} +
+
+
+ + + + + + } + onClick={() => editor.chain().focus().addRowBefore().run()} + > + {t("Add row above")} + + } + onClick={() => editor.chain().focus().addRowAfter().run()} + > + {t("Add row below")} + + + + + } onClick={clearRow}> + {t("Clear cells")} + + } + onClick={() => editor.chain().focus().deleteRow().run()} + > + {t("Delete row")} + + + + + } + onClick={moveUp.handleMove} + disabled={!moveUp.canMove} + > + {t("Move row up")} + + } + onClick={moveDown.handleMove} + disabled={!moveDown.canMove} + > + {t("Move row down")} + + + ); +}); diff --git a/apps/client/src/features/editor/components/table/handle/row-handle.tsx b/apps/client/src/features/editor/components/table/handle/row-handle.tsx new file mode 100644 index 000000000..1f3e3cc51 --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/row-handle.tsx @@ -0,0 +1,127 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import type { Editor } from "@tiptap/react"; +import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { useFloating, offset, autoUpdate, hide } from "@floating-ui/react"; +import { Menu } from "@mantine/core"; +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; +import { useTableHandleDrag } from "./hooks/use-table-handle-drag"; +import { useColumnRowMenuLifecycle } from "./hooks/use-column-row-menu-lifecycle"; +import { RowHandleMenu } from "./menus/row-handle-menu"; +import classes from "./handle.module.css"; + +interface RowHandleProps { + editor: Editor; + index: number; + anchorPos: number; + tableNode: ProseMirrorNode; + tablePos: number; +} + +export const RowHandle = React.memo(function RowHandle({ + editor, + index, + anchorPos, + tableNode, + tablePos, +}: RowHandleProps) { + const { t } = useTranslation(); + // See ColumnHandle for the rationale: keep the last valid cell DOM cached + // so the handle div stays mounted across stale-anchor renders, otherwise + // pragmatic-dnd silently aborts an in-flight drag. + // `nodeDOM` is typed as `Node | null` — when `anchorPos` goes stale (e.g. + // an external drop reflows the doc before the plugin re-emits + // hoveringCell), it can resolve to a Text node, on which `.closest` is + // undefined. Filter to HTMLElement so downstream consumers stay safe. + const lookupDom = editor.view.nodeDOM(anchorPos); + const lookupCellDom = lookupDom instanceof HTMLElement ? lookupDom : null; + const [cellDom, setCellDom] = useState(lookupCellDom); + const lastCellDomRef = useRef(lookupCellDom); + useEffect(() => { + if (lookupCellDom && lookupCellDom !== lastCellDomRef.current) { + lastCellDomRef.current = lookupCellDom; + setCellDom(lookupCellDom); + } + }, [lookupCellDom]); + + const [handleEl, setHandleEl] = useState(null); + + const { refs, floatingStyles, middlewareData } = useFloating({ + placement: "left", + middleware: [offset(-4), hide()], + whileElementsMounted: autoUpdate, + }); + const isReferenceHidden = !!middlewareData.hide?.referenceHidden; + + useEffect(() => { + refs.setReference(cellDom); + }, [cellDom, refs]); + + const wrapper = cellDom?.closest(".tableWrapper") ?? null; + + const [menuOpened, setMenuOpened] = useState(false); + const closeMenu = useCallback(() => setMenuOpened(false), []); + useTableHandleDrag(editor, "row", handleEl, wrapper, closeMenu); + + const { onOpen, onClose } = useColumnRowMenuLifecycle({ + editor, + orientation: "row", + index, + tableNode, + tablePos, + }); + + if (!cellDom) return null; + + return ( + + +
{ + refs.setFloating(node); + setHandleEl(node); + }} + style={{ + ...floatingStyles, + ...(isReferenceHidden ? { visibility: "hidden" as const } : {}), + }} + className={clsx(classes.handle, classes.rowHandle)} + role="button" + tabIndex={0} + aria-label={t("Row actions")} + > + + + +
+
+ + + +
+ ); +}); + +function GripIcon() { + return ( + + + + ); +} diff --git a/apps/client/src/features/editor/components/table/handle/table-handles-layer.tsx b/apps/client/src/features/editor/components/table/handle/table-handles-layer.tsx new file mode 100644 index 000000000..e40c7baac --- /dev/null +++ b/apps/client/src/features/editor/components/table/handle/table-handles-layer.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import type { Editor } from "@tiptap/react"; +import { useTableHandleState } from "./hooks/use-table-handle-state"; +import { ColumnHandle } from "./column-handle"; +import { RowHandle } from "./row-handle"; +import { CellChevron } from "./cell-chevron"; + +interface TableHandlesLayerProps { + editor: Editor | null; +} + +export const TableHandlesLayer = React.memo(function TableHandlesLayer({ + editor, +}: TableHandlesLayerProps) { + const state = useTableHandleState(editor); + + if (!editor || !editor.isEditable) return null; + if (!state.hoveringCell || !state.tableNode || state.tablePos == null) return null; + + return ( + <> + + + + + ); +}); diff --git a/apps/client/src/features/editor/components/table/table-background-color.tsx b/apps/client/src/features/editor/components/table/table-background-color.tsx index 3e4ce6168..c0df52d81 100644 --- a/apps/client/src/features/editor/components/table/table-background-color.tsx +++ b/apps/client/src/features/editor/components/table/table-background-color.tsx @@ -22,7 +22,7 @@ interface TableBackgroundColorProps { editor: Editor | null; } -const TABLE_COLORS: TableColorItem[] = [ +export const TABLE_COLORS: TableColorItem[] = [ { name: "Default", color: "" }, { name: "Blue", color: "#b4d5ff" }, { name: "Green", color: "#acf5d2" }, diff --git a/apps/client/src/features/editor/components/table/table-menu.tsx b/apps/client/src/features/editor/components/table/table-menu.tsx index 4adafb206..92cc318e9 100644 --- a/apps/client/src/features/editor/components/table/table-menu.tsx +++ b/apps/client/src/features/editor/components/table/table-menu.tsx @@ -18,7 +18,7 @@ import { IconTrashX, } from "@tabler/icons-react"; import { BubbleMenu } from "@tiptap/react/menus"; -import { isCellSelection, isTextSelected } from "@docmost/editor-ext"; +import { isCellSelection, isEditorReady, isTextSelected } from "@docmost/editor-ext"; import { useTranslation } from "react-i18next"; import classes from "../common/toolbar-menu.module.css"; @@ -38,6 +38,7 @@ export const TableMenu = React.memo( ); const getReferencedVirtualElement = useCallback(() => { + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "table"; const parent = findParentNode(predicate)(selection); @@ -104,12 +105,12 @@ export const TableMenu = React.memo( element.style.zIndex = "99"; }} options={{ - placement: "top", + placement: "bottom", offset: { mainAxis: 15, }, flip: { - fallbackPlacements: ["top", "bottom"], + fallbackPlacements: ["bottom", "top"], padding: { top: 35 + 15, left: 8, right: 8, bottom: -Infinity }, boundary: editor.options.element as HTMLElement, }, diff --git a/apps/client/src/features/editor/components/table/table-text-alignment.tsx b/apps/client/src/features/editor/components/table/table-text-alignment.tsx index 4d4646cf5..17ef7c42e 100644 --- a/apps/client/src/features/editor/components/table/table-text-alignment.tsx +++ b/apps/client/src/features/editor/components/table/table-text-alignment.tsx @@ -86,11 +86,11 @@ export const TableTextAlignment: FC = ({ editor }) => { transitionProps={{ transition: "pop" }} > - + setOpened(!opened)} > diff --git a/apps/client/src/features/editor/components/transclusion/error-placeholder.tsx b/apps/client/src/features/editor/components/transclusion/error-placeholder.tsx new file mode 100644 index 000000000..a7f99321c --- /dev/null +++ b/apps/client/src/features/editor/components/transclusion/error-placeholder.tsx @@ -0,0 +1,17 @@ +import { IconAlertTriangle } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import classes from "./transclusion.module.css"; + +export default function ErrorPlaceholder() { + const { t } = useTranslation(); + return ( +
+ + {t("Failed to load this synced block")} +
+ ); +} diff --git a/apps/client/src/features/editor/components/transclusion/no-access-placeholder.tsx b/apps/client/src/features/editor/components/transclusion/no-access-placeholder.tsx new file mode 100644 index 000000000..5212e169c --- /dev/null +++ b/apps/client/src/features/editor/components/transclusion/no-access-placeholder.tsx @@ -0,0 +1,13 @@ +import { IconEyeOff } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import classes from "./transclusion.module.css"; + +export default function NoAccessPlaceholder() { + const { t } = useTranslation(); + return ( +
+ + {t("You don't have access to this synced block")} +
+ ); +} diff --git a/apps/client/src/features/editor/components/transclusion/not-found-placeholder.tsx b/apps/client/src/features/editor/components/transclusion/not-found-placeholder.tsx new file mode 100644 index 000000000..db18d998c --- /dev/null +++ b/apps/client/src/features/editor/components/transclusion/not-found-placeholder.tsx @@ -0,0 +1,17 @@ +import { IconInfoCircle } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import classes from "./transclusion.module.css"; + +export default function NotFoundPlaceholder() { + const { t } = useTranslation(); + return ( +
+ + {t("The original synced block no longer exists")} +
+ ); +} diff --git a/apps/client/src/features/editor/components/transclusion/transclusion-content.tsx b/apps/client/src/features/editor/components/transclusion/transclusion-content.tsx new file mode 100644 index 000000000..946291225 --- /dev/null +++ b/apps/client/src/features/editor/components/transclusion/transclusion-content.tsx @@ -0,0 +1,48 @@ +import { EditorProvider } from "@tiptap/react"; +import { useMemo } from "react"; +import { mainExtensions } from "@/features/editor/extensions/extensions"; +import { UniqueID } from "@docmost/editor-ext"; + +type Props = { + content: unknown; +}; + +export default function TransclusionContent({ content }: Props) { + const extensions = useMemo(() => { + const filtered = mainExtensions.filter( + (e: any) => e.name !== "uniqueID" && e.name !== "globalDragHandle", + ); + return [ + ...filtered, + UniqueID.configure({ + types: ["heading", "paragraph", "transclusionSource"], + updateDocument: false, + }), + ]; + }, []); + + // Isolate the nested read-only editor's events from the host editor: + // - mousedown/click would otherwise make the host node-select the atom + // wrapper, blocking native text selection inside. + // - dragstart/dragover/drop would otherwise let the host treat events + // inside the nested view as drops on the host, duplicating dropped + // files at the transclusion's position. + const stop = (e: React.SyntheticEvent) => e.stopPropagation(); + + return ( +
+ +
+ ); +} diff --git a/apps/client/src/features/editor/components/transclusion/transclusion-lookup-context.tsx b/apps/client/src/features/editor/components/transclusion/transclusion-lookup-context.tsx new file mode 100644 index 000000000..ec44a5f20 --- /dev/null +++ b/apps/client/src/features/editor/components/transclusion/transclusion-lookup-context.tsx @@ -0,0 +1,213 @@ +import React, { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + lookupTransclusion, + lookupTransclusionForShare, +} from "@/features/transclusion/services/transclusion-api"; +import type { TransclusionLookup } from "@/features/transclusion/types/transclusion.types"; + +type LookupKey = string; // `${sourcePageId}::${transclusionId}` + +type Subscriber = { + key: LookupKey; + sourcePageId: string; + transclusionId: string; + setResult: (r: TransclusionLookup) => void; +}; + +type ContextValue = { + /** Register a subscriber. Returns an unsubscribe function. */ + subscribe: (s: Subscriber) => () => void; + /** + * Force a re-fetch of `key` and resolve when the response arrives (or the + * request fails). Bypasses the cache and any in-flight de-dup so the user + * always sees a fresh server read. + */ + refresh: (key: LookupKey) => Promise; +}; + +const TransclusionLookupContext = createContext(null); + +export function TransclusionLookupProvider({ + children, + shareId, +}: { + children: React.ReactNode; + /** + * When set, lookups go through the share-scoped public endpoint and are + * gated by the share graph (source page must have its own share or inherit + * one). Used by the public share viewer; left undefined in the authenticated + * app, where personal permissions gate access. + */ + shareId?: string; +}) { + const subscribersRef = useRef(new Map()); + const queueRef = useRef(new Set()); + const tickRef = useRef | null>(null); + // Read inside flush() via ref so changing share context doesn't churn the + // memoized callbacks (and thus doesn't re-render every consumer). + const shareIdRef = useRef(shareId); + shareIdRef.current = shareId; + // Last looked-up value for each key. Re-subscribers (e.g. when the editor + // remounts after switching from static to live) get this immediately + // instead of triggering a duplicate fetch. + const resultCacheRef = useRef(new Map()); + // Keys that are currently in flight in a batch request. A second subscribe + // for the same key while the first request is pending is a no-op; the + // subscriber is added to subscribersRef and will be notified when the + // pending request completes. + const inFlightRef = useRef(new Set()); + // Resolvers waiting on the next response for a key. Populated by refresh() + // so callers can await the fetch round-trip; resolved on success and on + // network error so the UI never hangs in a loading state. + const pendingRef = useRef(new Map void>>()); + + const flush = useCallback(async () => { + tickRef.current = null; + const keys = Array.from(queueRef.current); + queueRef.current.clear(); + if (keys.length === 0) return; + + for (const k of keys) inFlightRef.current.add(k); + + const references = keys.map((k) => { + const [sourcePageId, transclusionId] = k.split("::"); + return { sourcePageId, transclusionId }; + }); + + const resolveWaiters = (key: LookupKey) => { + const waiters = pendingRef.current.get(key); + if (!waiters) return; + pendingRef.current.delete(key); + for (const w of waiters) w(); + }; + + try { + const activeShareId = shareIdRef.current; + const { items } = activeShareId + ? await lookupTransclusionForShare({ + shareId: activeShareId, + references, + }) + : await lookupTransclusion({ references }); + for (const r of items) { + const key = `${r.sourcePageId}::${r.transclusionId}`; + resultCacheRef.current.set(key, r); + inFlightRef.current.delete(key); + const subs = subscribersRef.current.get(key); + if (subs) { + for (const s of subs) s.setResult(r); + } + resolveWaiters(key); + } + } catch { + // Network error — leave subscribers in pending state and clear the + // in-flight flag so a future subscribe can retry. + for (const k of keys) { + inFlightRef.current.delete(k); + resolveWaiters(k); + } + } + }, []); + + const enqueue = useCallback( + (key: LookupKey) => { + queueRef.current.add(key); + if (tickRef.current === null) { + tickRef.current = setTimeout(flush, 10); + } + }, + [flush], + ); + + const subscribe = useCallback( + (s) => { + const list = subscribersRef.current.get(s.key) ?? []; + list.push(s); + subscribersRef.current.set(s.key, list); + + const cached = resultCacheRef.current.get(s.key); + if (cached) { + s.setResult(cached); + } else if (!inFlightRef.current.has(s.key)) { + enqueue(s.key); + } + + return () => { + const cur = subscribersRef.current.get(s.key) ?? []; + const next = cur.filter((x) => x !== s); + if (next.length === 0) subscribersRef.current.delete(s.key); + else subscribersRef.current.set(s.key, next); + }; + }, + [enqueue], + ); + + const refresh = useCallback( + (key) => + new Promise((resolve) => { + resultCacheRef.current.delete(key); + inFlightRef.current.delete(key); + const waiters = pendingRef.current.get(key) ?? []; + waiters.push(resolve); + pendingRef.current.set(key, waiters); + enqueue(key); + }), + [enqueue], + ); + + useEffect( + () => () => { + if (tickRef.current) clearTimeout(tickRef.current); + }, + [], + ); + + const value = useMemo( + () => ({ subscribe, refresh }), + [subscribe, refresh], + ); + + return ( + + {children} + + ); +} + +export function useTransclusionLookup( + sourcePageId: string | null | undefined, + transclusionId: string | null | undefined, +): { + result: TransclusionLookup | null; + refresh: () => Promise; +} { + const ctx = useContext(TransclusionLookupContext); + const [result, setResult] = useState(null); + + useEffect(() => { + if (!ctx || !sourcePageId || !transclusionId) return; + const key = `${sourcePageId}::${transclusionId}`; + const unsubscribe = ctx.subscribe({ + key, + sourcePageId, + transclusionId, + setResult, + }); + return unsubscribe; + }, [ctx, sourcePageId, transclusionId]); + + const refresh = useCallback(async () => { + if (!ctx || !sourcePageId || !transclusionId) return; + await ctx.refresh(`${sourcePageId}::${transclusionId}`); + }, [ctx, sourcePageId, transclusionId]); + + return { result, refresh }; +} diff --git a/apps/client/src/features/editor/components/transclusion/transclusion-reference-view.tsx b/apps/client/src/features/editor/components/transclusion/transclusion-reference-view.tsx new file mode 100644 index 000000000..e50793149 --- /dev/null +++ b/apps/client/src/features/editor/components/transclusion/transclusion-reference-view.tsx @@ -0,0 +1,213 @@ +import { NodeViewProps, NodeViewWrapper } from "@tiptap/react"; +import { ActionIcon, Menu, Tooltip } from "@mantine/core"; +import { + IconDots, + IconLinkOff, + IconPencil, + IconRefresh, + IconTrash, +} from "@tabler/icons-react"; +import { useState } from "react"; +import { Link } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { ErrorBoundary } from "react-error-boundary"; +import { useTransclusionLookup } from "./transclusion-lookup-context"; +import TransclusionContent from "./transclusion-content"; +import NoAccessPlaceholder from "./no-access-placeholder"; +import NotFoundPlaceholder from "./not-found-placeholder"; +import ErrorPlaceholder from "./error-placeholder"; +import classes from "./transclusion.module.css"; +import SyncBlockReferencesDropdown from "@/features/transclusion/components/sync-block-references-dropdown"; +import { + useReferencesQuery, + useUnsyncReferenceMutation, +} from "@/features/transclusion/queries/transclusion-query"; +import { buildPageUrl } from "@/features/page/page.utils"; + +export default function TransclusionReferenceView(props: NodeViewProps) { + const isEditable = props.editor.isEditable; + const sourcePageId: string | null = props.node.attrs.sourcePageId ?? null; + const transclusionId: string | null = props.node.attrs.transclusionId ?? null; + const [openMenus, setOpenMenus] = useState(0); + const trackOpen = (open: boolean) => + setOpenMenus((n) => Math.max(0, n + (open ? 1 : -1))); + + return ( + 0 ? "true" : "false"} + contentEditable={false} + > + } + > + + + + ); +} + +function TransclusionReferenceBody({ + editor, + node, + deleteNode, + getPos, + trackOpen, +}: NodeViewProps & { trackOpen: (open: boolean) => void }) { + const { t } = useTranslation(); + const sourcePageId: string | null = node.attrs.sourcePageId ?? null; + const transclusionId: string | null = node.attrs.transclusionId ?? null; + const isEditable = editor.isEditable; + + const { result, refresh } = useTransclusionLookup( + sourcePageId, + transclusionId, + ); + const [refreshing, setRefreshing] = useState(false); + const handleRefresh = async () => { + setRefreshing(true); + try { + await refresh(); + } finally { + setRefreshing(false); + } + }; + // @ts-ignore - editor.storage.pageId is set by the host editor + const hostPageId: string | undefined = editor.storage?.pageId; + const unsyncMutation = useUnsyncReferenceMutation(); + // Cached against the dropdown's identical query so the source link target + // is ready as soon as the controls fade in on hover, without a second + // fetch. + const referencesQuery = useReferencesQuery( + sourcePageId, + transclusionId, + isEditable, + ); + const sourcePageHref = (() => { + const source = referencesQuery.data?.source; + const base = source?.spaceSlug + ? buildPageUrl(source.spaceSlug, source.slugId, source.title) + : sourcePageId + ? `/p/${sourcePageId}` + : null; + if (!base) return null; + return transclusionId ? `${base}#${transclusionId}` : base; + })(); + + const handleUnsync = async () => { + if (!hostPageId || !sourcePageId || !transclusionId) return; + try { + const { content } = await unsyncMutation.mutateAsync({ + referencePageId: hostPageId, + sourcePageId, + transclusionId, + }); + const pos = getPos(); + if (typeof pos !== "number") return; + const from = pos; + const to = pos + node.nodeSize; + editor + .chain() + .focus() + .insertContentAt({ from, to }, content as any) + .run(); + } catch { + // mutation surfaces errors via React Query; node stays as-is + } + }; + + return ( + <> + {isEditable && ( +
e.preventDefault()} + > + {sourcePageId && transclusionId && hostPageId && ( + + )} + + + + + + + {sourcePageHref && ( + + + + + + )} + + + + + + + + } + onClick={handleUnsync} + disabled={ + unsyncMutation.isPending || + !hostPageId || + !sourcePageId || + !transclusionId + } + > + {t("Unsync")} + + } + onClick={() => deleteNode()} + > + {t("Remove from page")} + + + +
+ )} + + {!sourcePageId || !transclusionId ? ( + + ) : !result ? ( +
+ ) : !("status" in result) ? ( + + ) : result.status === "no_access" ? ( + + ) : ( + + )} + + ); +} diff --git a/apps/client/src/features/editor/components/transclusion/transclusion-view.tsx b/apps/client/src/features/editor/components/transclusion/transclusion-view.tsx new file mode 100644 index 000000000..82997f5d5 --- /dev/null +++ b/apps/client/src/features/editor/components/transclusion/transclusion-view.tsx @@ -0,0 +1,127 @@ +import { + NodeViewContent, + NodeViewProps, + NodeViewWrapper, +} from "@tiptap/react"; +import { ActionIcon, Menu, Tooltip } from "@mantine/core"; +import { notifications } from "@mantine/notifications"; +import { + IconCheck, + IconCopy, + IconDots, + IconLinkOff, + IconTrash, +} from "@tabler/icons-react"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import classes from "./transclusion.module.css"; +import SyncBlockReferencesDropdown from "@/features/transclusion/components/sync-block-references-dropdown"; + +export default function TransclusionView(props: NodeViewProps) { + const { editor, node, deleteNode } = props; + const { t } = useTranslation(); + const [openMenus, setOpenMenus] = useState(0); + const trackOpen = (open: boolean) => + setOpenMenus((n) => Math.max(0, n + (open ? 1 : -1))); + + const isEditable = editor.isEditable; + // @ts-ignore - editor.storage.pageId is set by the host editor (page-editor.tsx onCreate) + const sourcePageId: string | undefined = editor.storage?.pageId; + const transclusionId: string | null = node.attrs.id ?? null; + + const [copied, setCopied] = useState(false); + const handleCopy = async () => { + if (!sourcePageId || !transclusionId) return; + const html = `
`; + try { + await navigator.clipboard.write([ + new ClipboardItem({ + "text/html": new Blob([html], { type: "text/html" }), + "text/plain": new Blob([html], { type: "text/plain" }), + }), + ]); + } catch { + // Fallback for browsers without ClipboardItem write support + try { + await navigator.clipboard.writeText(html); + } catch { + return; + } + } + setCopied(true); + window.setTimeout(() => setCopied(false), 2000); + notifications.show({ + message: t("Copied. Paste on any page to embed this synced block."), + }); + }; + + const handleUnsync = () => { + editor.chain().focus().unsyncTransclusionSource().run(); + }; + + return ( + 0 ? "true" : "false"} + data-id={transclusionId ?? undefined} + > + {isEditable && ( +
e.preventDefault()} + > + {sourcePageId && transclusionId && ( + + )} + + + + + + {copied ? : } + + + + + + + + + + + } + onClick={handleUnsync} + > + {t("Unsync")} + + } + onClick={() => deleteNode()} + > + {t("Delete synced block")} + + + +
+ )} + + +
+ ); +} diff --git a/apps/client/src/features/editor/components/transclusion/transclusion.module.css b/apps/client/src/features/editor/components/transclusion/transclusion.module.css new file mode 100644 index 000000000..4d8d321a1 --- /dev/null +++ b/apps/client/src/features/editor/components/transclusion/transclusion.module.css @@ -0,0 +1,230 @@ +.placeholder { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: var(--mantine-radius-md); + background: light-dark( + var(--mantine-color-gray-0), + var(--mantine-color-dark-6) + ); + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + font-size: var(--mantine-font-size-sm); + user-select: none; +} + +.placeholderIcon { + flex: none; + color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); +} + +.transclusionBadge { + display: inline-block; + padding: 2px 8px; + font-size: var(--mantine-font-size-xs); + font-weight: 600; + color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-1)); + background: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-dark-5) + ); + border-radius: var(--mantine-radius-sm); + margin-bottom: 4px; +} + +.transclusionWrap { + position: relative; + margin-left: -3rem; + margin-right: -3rem; + width: calc(100% + 6rem); + padding: 0.5em 3rem; + border-radius: 8px; + border: 2px solid transparent; + transition: border 0.3s; +} + +.transclusionWrap[data-editable="false"], +.includeWrap[data-editable="false"] { + margin-left: 0; + margin-right: 0; + width: 100%; + padding: 0; +} + +/* Cancel the wrapping .react-renderer's vertical spacing in read-only mode + so the synced block sits flush with surrounding paragraphs (whose own + margins already provide the right rhythm). */ +:global(.react-renderer.node-transclusionSource):has( + .transclusionWrap[data-editable="false"] + ), +:global(.react-renderer.node-transclusionReference):has( + .includeWrap[data-editable="false"] + ) { + margin-top: 0; + margin-bottom: 0; +} + +.transclusionWrap[data-editable="true"]:hover, +.transclusionWrap[data-editable="true"]:focus-within { + border: 2px solid + light-dark( + var(--mantine-color-orange-2), + color-mix(in srgb, var(--mantine-color-orange-9), transparent 55%) + ); +} + +.transclusionControls { + position: absolute; + bottom: calc(100% + 8px); + right: 4px; + display: flex; + align-items: center; + gap: 6px; + background: var(--mantine-color-body); + border: 1px solid var(--mantine-color-default-border); + border-radius: 6px; + padding: 4px 6px; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; + z-index: 2; +} + +/* Hover bridge: keeps :hover on the wrap while the cursor crosses the + 8px gap between wrap and floating chrome, so the menu doesn't fade out + on the way to it. */ +.transclusionControls::after { + content: ""; + position: absolute; + top: 100%; + left: 0; + right: 0; + height: 8px; +} + +.transclusionWrap:hover .transclusionControls, +.transclusionWrap:focus-within .transclusionControls, +.transclusionWrap[data-menu-open="true"] .transclusionControls { + opacity: 1; + pointer-events: auto; +} + +.controlsDivider { + display: inline-block; + width: 1px; + height: 16px; + background: var(--mantine-color-default-border); +} + +.transclusionControls a[href], +.includeControls a[href] { + color: var(--ai-color); + border-bottom: none; + font-weight: inherit; +} + +.includeWrap { + position: relative; + margin-left: -3rem; + margin-right: -3rem; + width: calc(100% + 6rem); + padding: 0.5em 0; + border-radius: 8px; + border: 2px solid transparent; + transition: border 0.3s; +} + +.includeWrap[data-editable="true"]:hover, +.includeWrap[data-editable="true"][data-focused="true"], +.includeWrap[data-editable="true"][data-menu-open="true"] { + border: 2px solid + light-dark( + var(--mantine-color-orange-2), + color-mix(in srgb, var(--mantine-color-orange-9), transparent 55%) + ); +} + +.includeControls { + position: absolute; + bottom: calc(100% + 8px); + right: 4px; + display: flex; + align-items: center; + gap: 6px; + background: var(--mantine-color-body); + border: 1px solid var(--mantine-color-default-border); + border-radius: 6px; + padding: 4px 6px; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; + z-index: 2; +} + +/* Hover bridge: keeps :hover on the wrap while the cursor crosses the + 8px gap between wrap and floating chrome, so the menu doesn't fade out + on the way to it. */ +.includeControls::after { + content: ""; + position: absolute; + top: 100%; + left: 0; + right: 0; + height: 8px; +} + +.includeWrap:hover .includeControls, +.includeWrap:focus-within .includeControls, +.includeWrap[data-focused="true"] .includeControls, +.includeWrap[data-menu-open="true"] .includeControls { + opacity: 1; + pointer-events: auto; +} + +:global(.react-renderer.node-transclusionSource.ProseMirror-selectednode), +:global(.react-renderer.node-transclusionReference.ProseMirror-selectednode) { + outline: none; +} + +@media (max-width: 48em) { + .transclusionWrap, + .includeWrap { + margin-left: -1rem; + margin-right: -1rem; + width: calc(100% + 2rem); + } + + .transclusionWrap { + padding-left: 1rem; + padding-right: 1rem; + } +} + +@media print { + .transclusionControls, + .includeControls { + display: none !important; + } + .transclusionWrap, + .includeWrap { + border: none !important; + margin-left: 0 !important; + margin-right: 0 !important; + width: 100% !important; + padding: 0 !important; + } +} + +.editingOriginalTag { + display: inline-block; + padding: 0 6px; + font-size: var(--mantine-font-size-xs); + font-weight: 500; + color: var(--mantine-color-blue-7); + background: light-dark( + var(--mantine-color-blue-0), + var(--mantine-color-blue-9) + ); + border-radius: var(--mantine-radius-sm); +} diff --git a/apps/client/src/features/editor/components/video/video-menu.tsx b/apps/client/src/features/editor/components/video/video-menu.tsx index 3f232625f..bfbaf27ad 100644 --- a/apps/client/src/features/editor/components/video/video-menu.tsx +++ b/apps/client/src/features/editor/components/video/video-menu.tsx @@ -2,6 +2,7 @@ import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react/menus"; import { findParentNode, posToDOMRect, useEditorState } from "@tiptap/react"; import { useCallback } from "react"; import { Node as PMNode } from "@tiptap/pm/model"; +import { isEditorReady } from "@docmost/editor-ext"; import { EditorMenuProps, ShouldShowProps, @@ -17,6 +18,7 @@ import { } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { getFileUrl } from "@/lib/config.ts"; +import { useAltTextControl } from "@/features/editor/components/common/use-alt-text-control.tsx"; import classes from "../common/toolbar-menu.module.css"; export function VideoMenu({ editor }: EditorMenuProps) { @@ -37,6 +39,7 @@ export function VideoMenu({ editor }: EditorMenuProps) { isAlignCenter: ctx.editor.isActive("video", { align: "center" }), isAlignRight: ctx.editor.isActive("video", { align: "right" }), src: videoAttrs?.src || null, + alt: videoAttrs?.alt || "", }; }, }); @@ -53,7 +56,7 @@ export function VideoMenu({ editor }: EditorMenuProps) { ); const getReferencedVirtualElement = useCallback(() => { - if (!editor) return; + if (!isEditorReady(editor)) return; const { selection } = editor.state; const predicate = (node: PMNode) => node.type.name === "video"; const parent = findParentNode(predicate)(selection); @@ -111,6 +114,16 @@ export function VideoMenu({ editor }: EditorMenuProps) { editor.commands.deleteSelection(); }, [editor]); + const { + button: altTextButton, + panel: altTextPanel, + isEditing: isEditingAlt, + } = useAltTextControl({ + editor, + nodeName: "video", + currentAlt: editorState?.alt || "", + }); + return ( -
+ {isEditingAlt ? ( + altTextPanel + ) : ( +
+ {altTextButton} + +
+ -
+
+ )} ); } diff --git a/apps/client/src/features/editor/components/video/video-view.tsx b/apps/client/src/features/editor/components/video/video-view.tsx index 46ff79084..d6c37a0cd 100644 --- a/apps/client/src/features/editor/components/video/video-view.tsx +++ b/apps/client/src/features/editor/components/video/video-view.tsx @@ -9,7 +9,7 @@ import { useTranslation } from "react-i18next"; export default function VideoView(props: NodeViewProps) { const { t } = useTranslation(); const { editor, node, selected } = props; - const { src, width, align, aspectRatio, placeholder } = node.attrs; + const { src, width, align, alt, aspectRatio, placeholder } = node.attrs; const alignClass = useMemo(() => { if (align === "left") return "alignLeft"; if (align === "right") return "alignRight"; @@ -47,6 +47,7 @@ export default function VideoView(props: NodeViewProps) { preload="metadata" controls src={getFileUrl(src)} + aria-label={alt || undefined} /> )} {!src && previewSrc && ( @@ -56,6 +57,7 @@ export default function VideoView(props: NodeViewProps) { preload="metadata" controls src={previewSrc} + aria-label={placeholder?.name || t("Video")} /> @@ -71,7 +73,7 @@ export default function VideoView(props: NodeViewProps) { )} {!src && !previewSrc && !placeholder && ( -
diff --git a/apps/client/src/features/editor/extensions/clean-styles.ts b/apps/client/src/features/editor/extensions/clean-styles.ts new file mode 100644 index 000000000..f36e9f500 --- /dev/null +++ b/apps/client/src/features/editor/extensions/clean-styles.ts @@ -0,0 +1,20 @@ +import { Extension } from "@tiptap/core"; +import { Plugin, PluginKey } from "@tiptap/pm/state"; + +export const CleanStyles = Extension.create({ + name: "cleanStyles", + priority: 80, + + addProseMirrorPlugins() { + return [ + new Plugin({ + key: new PluginKey("cleanStyles"), + props: { + transformPastedHTML(html) { + return html.replace(/\s+style="[^"]*"/gi, ""); + }, + }, + }), + ]; + }, +}); diff --git a/apps/client/src/features/editor/extensions/drag-handle.ts b/apps/client/src/features/editor/extensions/drag-handle.ts new file mode 100644 index 000000000..6b10678a1 --- /dev/null +++ b/apps/client/src/features/editor/extensions/drag-handle.ts @@ -0,0 +1,517 @@ +import { Extension } from "@tiptap/core"; +import { + NodeSelection, + Plugin, + PluginKey, + TextSelection, +} from "@tiptap/pm/state"; +import { Fragment, Slice, Node } from "@tiptap/pm/model"; +import { EditorView } from "@tiptap/pm/view"; + +export interface GlobalDragHandleOptions { + /** + * The width of the drag handle + */ + dragHandleWidth: number; + + /** + * The treshold for scrolling + */ + scrollThreshold: number; + + /* + * The css selector to query for the drag handle. (eg: '.custom-handle'). + * If handle element is found, that element will be used as drag handle. If not, a default handle will be created + */ + dragHandleSelector?: string; + + /** + * Tags to be excluded for drag handle + */ + excludedTags: string[]; + + /** + * Custom nodes to be included for drag handle + */ + customNodes: string[]; +} +function absoluteRect(node: Element) { + const data = node.getBoundingClientRect(); + const modal = node.closest('[role="dialog"]'); + + if (modal && window.getComputedStyle(modal).transform !== "none") { + const modalRect = modal.getBoundingClientRect(); + + return { + top: data.top - modalRect.top, + left: data.left - modalRect.left, + width: data.width, + }; + } + return { + top: data.top, + left: data.left, + width: data.width, + }; +} + +function nodeDOMAtCoords( + coords: { x: number; y: number }, + options: GlobalDragHandleOptions, + view: EditorView, +) { + // Custom nodes (transclusion, …) render via tiptap's React node-view + // renderer, which emits `class="react-renderer node-${name}"` on the + // live wrapper — the `data-type` attribute is for static HTML + // serialization only. Match both so we cover live and parsed DOM. + // Inside a custom node, also match plain `p` so the first paragraph + // (which doesn't match `:not(:first-child)`) still gets its own + // handle; only hovers on the custom node's padding/border fall + // through to the wrapper. + const customSelectors = options.customNodes.flatMap((node) => [ + `[data-type=${node}]`, + `.node-${node}`, + ]); + const customParagraphSelectors = options.customNodes.flatMap((node) => [ + `[data-type=${node}] p`, + `.node-${node} p`, + ]); + + const selectors = [ + "li", + "p:not(:first-child)", + "pre", + "blockquote", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + // Tables nested in another block (toggle, transclusion, …) have a + // wrapper that isn't a direct child of .ProseMirror, so the + // parent-check below skips it. Match the wrapper explicitly so the + // handle shows up even with empty cells. + ".tableWrapper", + ...customParagraphSelectors, + ...customSelectors, + ].join(", "); + return document + .elementsFromPoint(coords.x, coords.y) + .find((elem: Element) => { + // Skip elements that belong to a nested editor (e.g. transclusion + // references render their own ProseMirror instance). Only consider + // elements whose closest editor is this host view. + if (elem.closest(".ProseMirror") !== view.dom) return false; + return ( + elem.parentElement?.matches?.(".ProseMirror") || + elem.matches(selectors) + ); + }); +} +function nodePosAtDOM( + node: Element, + view: EditorView, + options: GlobalDragHandleOptions, +) { + const boundingRect = node.getBoundingClientRect(); + + return view.posAtCoords({ + left: boundingRect.left + 50 + options.dragHandleWidth, + top: boundingRect.top + 1, + })?.inside; +} + +function isCustomNodeDOM( + elem: Element | null | undefined, + options: GlobalDragHandleOptions, +): boolean { + if (!elem) return false; + for (const name of options.customNodes) { + if ( + elem.getAttribute("data-type") === name || + elem.classList.contains(`node-${name}`) + ) { + return true; + } + } + return false; +} + +function calcNodePos(pos: number, view: EditorView) { + const $pos = view.state.doc.resolve(pos); + if ($pos.depth > 1) return $pos.before($pos.depth); + return pos; +} + +export function DragHandlePlugin( + options: GlobalDragHandleOptions & { pluginKey: string }, +) { + let listType = ""; + function handleDragStart(event: DragEvent, view: EditorView) { + view.focus(); + + if (!event.dataTransfer) return; + + const node = nodeDOMAtCoords( + { + x: event.clientX + 50 + options.dragHandleWidth, + y: event.clientY, + }, + options, + view, + ); + + if (!(node instanceof Element)) return; + + let draggedNodePos = nodePosAtDOM(node, view, options); + if (draggedNodePos == null || draggedNodePos < 0) return; + draggedNodePos = calcNodePos(draggedNodePos, view); + + const { from, to } = view.state.selection; + const diff = from - to; + + const fromSelectionPos = calcNodePos(from, view); + let differentNodeSelected = false; + + const nodePos = view.state.doc.resolve(fromSelectionPos); + + if (nodePos.node().type.name === "doc") differentNodeSelected = true; + else { + const nodeSelection = NodeSelection.create( + view.state.doc, + nodePos.before(), + ); + + // Check if the node where the drag event started is part of the current selection + differentNodeSelected = !( + draggedNodePos + 1 >= nodeSelection.$from.pos && + draggedNodePos <= nodeSelection.$to.pos + ); + } + let selection = view.state.selection; + if ( + !differentNodeSelected && + diff !== 0 && + !(view.state.selection instanceof NodeSelection) + ) { + const endSelection = NodeSelection.create(view.state.doc, to - 1); + selection = TextSelection.create( + view.state.doc, + draggedNodePos, + endSelection.$to.pos, + ); + } else { + selection = NodeSelection.create(view.state.doc, draggedNodePos); + + const $sel = view.state.doc.resolve(selection.from); + + if (isCustomNodeDOM(node, options)) { + // The drag landed on a custom-node container (transclusion etc.). + // Walk up to the matching node so the drag moves the whole + // container, not whatever inner element the click landed on. + const customTypes = new Set(options.customNodes); + for (let d = $sel.depth; d > 0; d--) { + if (customTypes.has($sel.node(d).type.name)) { + selection = NodeSelection.create( + view.state.doc, + $sel.before(d), + ); + break; + } + } + } else { + // If the selected node lives inside a table (at any nesting + // depth), promote to the whole table — the global drag handle is + // meant to move the table as a single block, not a row/cell. The + // earlier tableRow-only check only worked when the table sat at + // the doc root; once wrapped in another node (toggle, layout, + // etc.) the selection lands on a cell/paragraph and that check + // never fired. + let tableDepth = -1; + for (let d = $sel.depth; d > 0; d--) { + if ($sel.node(d).type.name === "table") { + tableDepth = d; + break; + } + } + if (tableDepth > 0) { + selection = NodeSelection.create( + view.state.doc, + $sel.before(tableDepth), + ); + } else if ((selection as NodeSelection).node.type.isInline) { + // Inline node (e.g. mention): walk up to the parent block. + selection = NodeSelection.create(view.state.doc, $sel.before()); + } + } + } + view.dispatch(view.state.tr.setSelection(selection)); + + // If the selected node is a list item, we need to save the type of the wrapping list e.g. OL or UL + if ( + view.state.selection instanceof NodeSelection && + view.state.selection.node.type.name === "listItem" + ) { + listType = node.parentElement!.tagName; + } + + const slice = view.state.selection.content(); + const { dom, text } = view.serializeForClipboard(slice); + + event.dataTransfer.clearData(); + event.dataTransfer.setData("text/html", dom.innerHTML); + event.dataTransfer.setData("text/plain", text); + event.dataTransfer.effectAllowed = "move"; + + event.dataTransfer.setDragImage(node, 0, 0); + + view.dragging = { slice, move: event.ctrlKey }; + } + + let dragHandleElement: HTMLElement | null = null; + + function hideDragHandle() { + if (dragHandleElement) { + dragHandleElement.classList.add("hide"); + } + } + + function showDragHandle() { + if (dragHandleElement) { + dragHandleElement.classList.remove("hide"); + } + } + + function hideHandleOnEditorOut(event: MouseEvent) { + if (event.target instanceof Element) { + // Check if the relatedTarget class is still inside the editor + const relatedTarget = event.relatedTarget as HTMLElement; + const isInsideEditor = + relatedTarget?.classList.contains("tiptap") || + relatedTarget?.classList.contains("drag-handle"); + + if (isInsideEditor) return; + } + hideDragHandle(); + } + + return new Plugin({ + key: new PluginKey(options.pluginKey), + view: (view) => { + const handleBySelector = options.dragHandleSelector + ? document.querySelector(options.dragHandleSelector) + : null; + dragHandleElement = handleBySelector ?? document.createElement("div"); + dragHandleElement.draggable = true; + dragHandleElement.dataset.dragHandle = ""; + dragHandleElement.classList.add("drag-handle"); + + function onDragHandleDragStart(e: DragEvent) { + handleDragStart(e, view); + } + + dragHandleElement.addEventListener("dragstart", onDragHandleDragStart); + + function onDragHandleDrag(e: DragEvent) { + hideDragHandle(); + let scrollY = window.scrollY; + if (e.clientY < options.scrollThreshold) { + window.scrollTo({ top: scrollY - 30, behavior: "smooth" }); + } else if (window.innerHeight - e.clientY < options.scrollThreshold) { + window.scrollTo({ top: scrollY + 30, behavior: "smooth" }); + } + } + + dragHandleElement.addEventListener("drag", onDragHandleDrag); + + hideDragHandle(); + + if (!handleBySelector) { + view?.dom?.parentElement?.appendChild(dragHandleElement); + } + view?.dom?.parentElement?.addEventListener( + "mouseout", + hideHandleOnEditorOut, + ); + + return { + destroy: () => { + if (!handleBySelector) { + dragHandleElement?.remove?.(); + } + dragHandleElement?.removeEventListener("drag", onDragHandleDrag); + dragHandleElement?.removeEventListener( + "dragstart", + onDragHandleDragStart, + ); + dragHandleElement = null; + view?.dom?.parentElement?.removeEventListener( + "mouseout", + hideHandleOnEditorOut, + ); + }, + }; + }, + props: { + handleDOMEvents: { + mousemove: (view, event) => { + if (!view.editable) { + return; + } + + const node = nodeDOMAtCoords( + { + x: event.clientX + 50 + options.dragHandleWidth, + y: event.clientY, + }, + options, + view, + ); + + const notDragging = node?.closest(".not-draggable"); + const excludedTagList = options.excludedTags + .concat(["ol", "ul"]) + .join(", "); + + if ( + !(node instanceof Element) || + node.matches(excludedTagList) || + notDragging + ) { + hideDragHandle(); + return; + } + + const isCustomNode = isCustomNodeDOM(node, options); + + // Custom nodes pin the handle to the inner NodeViewWrapper's top-left: + // the natural anchor sits in transient/empty space outside the visible block. + if (isCustomNode) { + // tiptap React node-views emit an outer `.react-renderer` whose first + // child is the visible NodeViewWrapper; walk to that outer first since + // `node` may be either the outer or an inner element with data-type. + const rendererOuter = + (node.closest(".react-renderer") as HTMLElement | null) ?? node; + const inner = + (rendererOuter.firstElementChild as HTMLElement | null) ?? + rendererOuter; + const innerRect = absoluteRect(inner); + if (!dragHandleElement) return; + dragHandleElement.style.left = `${innerRect.left + 4}px`; + dragHandleElement.style.top = `${innerRect.top + 4}px`; + showDragHandle(); + return; + } + + const compStyle = window.getComputedStyle(node); + const parsedLineHeight = parseInt(compStyle.lineHeight, 10); + const lineHeight = isNaN(parsedLineHeight) + ? parseInt(compStyle.fontSize) * 1.2 + : parsedLineHeight; + const paddingTop = parseInt(compStyle.paddingTop, 10); + + const rect = absoluteRect(node); + + rect.top += (lineHeight - 24) / 2; + rect.top += paddingTop; + // Li markers + if (node.matches("ul:not([data-type=taskList]) li, ol li")) { + rect.left -= options.dragHandleWidth; + } + // Tables: clear the table's own row-drag handle so the two + // grips don't stack on each other. `nodeDOMAtCoords` returns + // the wrapper for top-level hovers (wrapper is direct child of + // .ProseMirror) and a descendant for deeper hovers — cover both. + if (node.closest(".tableWrapper")) { + rect.left -= options.dragHandleWidth; + } + rect.width = options.dragHandleWidth; + + if (!dragHandleElement) return; + + dragHandleElement.style.left = `${rect.left - rect.width}px`; + dragHandleElement.style.top = `${rect.top}px`; + showDragHandle(); + }, + keydown: () => { + hideDragHandle(); + }, + mousewheel: () => { + hideDragHandle(); + }, + // dragging class is used for CSS + dragstart: (view) => { + view.dom.classList.add("dragging"); + }, + drop: (view, event) => { + view.dom.classList.remove("dragging"); + hideDragHandle(); + let droppedNode: Node | null = null; + const dropPos = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + + if (!dropPos) return; + + if (view.state.selection instanceof NodeSelection) { + droppedNode = view.state.selection.node; + } + if (!droppedNode) return; + + const resolvedPos = view.state.doc.resolve(dropPos.pos); + + const isDroppedInsideList = + resolvedPos.parent.type.name === "listItem"; + + // If the selected node is a list item and is not dropped inside a list, we need to wrap it inside
    tag otherwise ol list items will be transformed into ul list item when dropped + if ( + view.state.selection instanceof NodeSelection && + view.state.selection.node.type.name === "listItem" && + !isDroppedInsideList && + listType == "OL" + ) { + const newList = view.state.schema.nodes.orderedList?.createAndFill( + null, + droppedNode, + ); + const slice = new Slice(Fragment.from(newList), 0, 0); + view.dragging = { slice, move: event.ctrlKey }; + } + }, + dragend: (view) => { + view.dom.classList.remove("dragging"); + }, + }, + }, + }); +} + +const GlobalDragHandle = Extension.create({ + name: "globalDragHandle", + + addOptions() { + return { + dragHandleWidth: 20, + scrollThreshold: 100, + excludedTags: [], + customNodes: [], + }; + }, + + addProseMirrorPlugins() { + return [ + DragHandlePlugin({ + pluginKey: "globalDragHandle", + dragHandleWidth: this.options.dragHandleWidth, + scrollThreshold: this.options.scrollThreshold, + dragHandleSelector: this.options.dragHandleSelector, + excludedTags: this.options.excludedTags, + customNodes: this.options.customNodes, + }), + ]; + }, +}); + +export default GlobalDragHandle; diff --git a/apps/client/src/features/editor/extensions/extensions.ts b/apps/client/src/features/editor/extensions/extensions.ts index ecf68afea..c7e1eda62 100644 --- a/apps/client/src/features/editor/extensions/extensions.ts +++ b/apps/client/src/features/editor/extensions/extensions.ts @@ -9,9 +9,10 @@ import SubScript from "@tiptap/extension-subscript"; import { Typography } from "@tiptap/extension-typography"; import { TextStyle } from "@tiptap/extension-text-style"; import { Color } from "@tiptap/extension-color"; -import GlobalDragHandle from "tiptap-extension-global-drag-handle"; import { Youtube } from "@tiptap/extension-youtube"; -import SlashCommand, { SlashCommandExtension as Command } from "@/features/editor/extensions/slash-command"; +import SlashCommand, { + SlashCommandExtension as Command, +} from "@/features/editor/extensions/slash-command"; import renderItems from "@/features/editor/components/slash-menu/render-items"; import getSuggestionItems from "@/features/editor/components/slash-menu/menu-items"; import { Collaboration, isChangeOrigin } from "@tiptap/extension-collaboration"; @@ -41,17 +42,25 @@ import { Excalidraw, Embed, TiptapPdf, + PageBreak, SearchAndReplace, Mention, TableDndExtension, + TableHandleCommandsExtension, + TableHeaderPin, + TableReadonlySort, Subpages, Heading, Highlight, + Indent, UniqueID, SharedStorage, Columns, Column, Status, + TransclusionSource, + TransclusionReference, + TableView, BaseEmbed as BaseEmbedNode, } from "@docmost/editor-ext"; import { @@ -77,10 +86,12 @@ import AudioView from "@/features/editor/components/audio/audio-view.tsx"; import AttachmentView from "@/features/editor/components/attachment/attachment-view.tsx"; import CodeBlockView from "@/features/editor/components/code-block/code-block-view.tsx"; import DrawioView from "../components/drawio/drawio-view"; -import ExcalidrawView from "@/features/editor/components/excalidraw/excalidraw-view.tsx"; +import ExcalidrawView from "@/features/editor/components/excalidraw/excalidraw-view-lazy.tsx"; import EmbedView from "@/features/editor/components/embed/embed-view.tsx"; import PdfView from "@/features/editor/components/pdf/pdf-view.tsx"; import SubpagesView from "@/features/editor/components/subpages/subpages-view.tsx"; +import TransclusionView from "@/features/editor/components/transclusion/transclusion-view.tsx"; +import TransclusionReferenceView from "@/features/editor/components/transclusion/transclusion-reference-view.tsx"; import { BaseEmbedView } from "@/features/editor/components/base-embed/base-embed-view.tsx"; import { common, createLowlight } from "lowlight"; import plaintext from "highlight.js/lib/languages/plaintext"; @@ -102,6 +113,8 @@ import { MarkdownClipboard } from "@/features/editor/extensions/markdown-clipboa import EmojiCommand from "./emoji-command"; import { countWords } from "alfaaz"; import AutoJoiner from "@/features/editor/extensions/autojoiner.ts"; +import GlobalDragHandle from "@/features/editor/extensions/drag-handle.ts"; +import { CleanStyles } from "@/features/editor/extensions/clean-styles.ts"; const lowlight = createLowlight(common); lowlight.register("mermaid", plaintext); @@ -169,7 +182,7 @@ export const mainExtensions = [ SharedStorage, Heading, UniqueID.configure({ - types: ["heading", "paragraph"], + types: ["heading", "paragraph", "transclusionSource"], filterTransaction: (transaction) => !isChangeOrigin(transaction), }), Placeholder.configure({ @@ -199,6 +212,7 @@ export const mainExtensions = [ showOnlyWhenEditable: true, }), TextAlign.configure({ types: ["heading", "paragraph"] }), + Indent, TaskList, TaskItem.configure({ nested: true, @@ -217,7 +231,9 @@ export const mainExtensions = [ }), Typography, TrailingNode, - GlobalDragHandle, + GlobalDragHandle.configure({ + customNodes: ["transclusionSource", "transclusionReference"], + }), TextStyle, Color, SlashCommand, @@ -251,11 +267,16 @@ export const mainExtensions = [ resizable: true, lastColumnResizable: true, allowTableNodeSelection: true, + cellMinWidth: 49, + View: TableView, }), TableRow, TableCell, TableHeader, TableDndExtension, + TableHandleCommandsExtension, + TableHeaderPin, + TableReadonlySort, MathInline.configure({ view: MathInlineView, }), @@ -307,6 +328,8 @@ export const mainExtensions = [ view: CodeBlockView, //@ts-ignore lowlight, + enableTabIndentation: true, + tabSize: 2, HTMLAttributes: { spellcheck: false, }, @@ -347,12 +370,19 @@ export const mainExtensions = [ TiptapPdf.configure({ view: PdfView, }), + PageBreak, Subpages.configure({ view: SubpagesView, }), Status.configure({ view: StatusView, }), + TransclusionSource.configure({ + view: TransclusionView, + }), + TransclusionReference.configure({ + view: TransclusionReferenceView, + }), BaseEmbedNode.extend({ addNodeView() { return ReactNodeViewRenderer(BaseEmbedView); @@ -361,6 +391,7 @@ export const mainExtensions = [ MarkdownClipboard.configure({ transformPastedText: true, }), + CleanStyles, CharacterCount.configure({ wordCounter: (text) => countWords(text), }), @@ -394,13 +425,18 @@ const TEMPLATE_EXCLUDED_SLASH_ITEMS = new Set([ "Video", "File attachment", "Draw.io (diagrams.net)", - "Excalidraw diagram", + "Excalidraw (Whiteboard)", + "Audio", + "Synced block" ]); const TemplateSlashCommand = Command.configure({ suggestion: { items: ({ query }: { query: string }) => - getSuggestionItems({ query, excludeItems: TEMPLATE_EXCLUDED_SLASH_ITEMS }), + getSuggestionItems({ + query, + excludeItems: TEMPLATE_EXCLUDED_SLASH_ITEMS, + }), render: renderItems, }, }); diff --git a/apps/client/src/features/editor/extensions/markdown-clipboard.ts b/apps/client/src/features/editor/extensions/markdown-clipboard.ts index 230798c50..bebb567ab 100644 --- a/apps/client/src/features/editor/extensions/markdown-clipboard.ts +++ b/apps/client/src/features/editor/extensions/markdown-clipboard.ts @@ -80,10 +80,12 @@ export const MarkdownClipboard = Extension.create({ const { from, to } = view.state.selection; const parsed = markdownToHtml(text.replace(/\n+$/, "")); + const body = elementFromString(parsed); + normalizeTableColumnWidths(body); const contentNodes = DOMParser.fromSchema( this.editor.schema, - ).parseSlice(elementFromString(parsed), { + ).parseSlice(body, { preserveWhitespace: true, }); @@ -137,3 +139,92 @@ function elementFromString(value) { return new window.DOMParser().parseFromString(wrappedValue, "text/html").body; } + +const DEFAULT_PASTE_COL_WIDTH_PX = 150; + +function parsePixelWidth(el: Element): number | null { + const attr = el.getAttribute("width"); + if (attr) { + const n = parseInt(attr, 10); + if (Number.isFinite(n) && n > 0) return n; + } + const style = el.getAttribute("style") || ""; + const m = style.match(/(?:^|;)\s*width\s*:\s*([\d.]+)\s*px/i); + if (m) { + const n = parseInt(m[1], 10); + if (Number.isFinite(n) && n > 0) return n; + } + return null; +} + +function getFirstRow(table: Element): Element | null { + const tbodyRow = table.querySelector(":scope > tbody > tr"); + if (tbodyRow) return tbodyRow; + const theadRow = table.querySelector(":scope > thead > tr"); + if (theadRow) return theadRow; + return table.querySelector(":scope > tr"); +} + +function deriveColumnWidths(table: Element): (number | null)[] | null { + const cols = table.querySelectorAll(":scope > colgroup > col"); + if (cols.length > 0) { + const widths: (number | null)[] = []; + cols.forEach((col) => widths.push(parsePixelWidth(col))); + if (widths.some((w) => w !== null)) return widths; + } + + const firstRow = getFirstRow(table); + if (!firstRow) return null; + + const widths: (number | null)[] = []; + Array.from(firstRow.children) + .filter((c) => c.tagName === "TD" || c.tagName === "TH") + .forEach((cell) => { + const colspan = parseInt(cell.getAttribute("colspan") || "1", 10) || 1; + const w = parsePixelWidth(cell); + for (let i = 0; i < colspan; i++) { + widths.push(w !== null ? Math.round(w / colspan) : null); + } + }); + if (widths.length === 0 || widths.every((w) => w === null)) return null; + return widths; +} + +// Mirror of server normalizeTableColumnWidths (see import/utils/table-utils.ts): +// markdown source has no widths, so without this every pasted table renders +// at table-layout:fixed/100% and squashes columns to fit the editor instead of +// letting .tableWrapper's overflow-x: auto scroll. +export function normalizeTableColumnWidths(root: Element): void { + root.querySelectorAll("table").forEach((table) => { + const firstRow = getFirstRow(table); + if (!firstRow) return; + + let colWidths = deriveColumnWidths(table); + if (!colWidths) { + let count = 0; + Array.from(firstRow.children) + .filter((c) => c.tagName === "TD" || c.tagName === "TH") + .forEach((cell) => { + count += parseInt(cell.getAttribute("colspan") || "1", 10) || 1; + }); + if (count === 0) return; + colWidths = new Array(count).fill(DEFAULT_PASTE_COL_WIDTH_PX); + } + + let col = 0; + Array.from(firstRow.children) + .filter((c) => c.tagName === "TD" || c.tagName === "TH") + .forEach((cell) => { + if (cell.getAttribute("colwidth")) { + col += parseInt(cell.getAttribute("colspan") || "1", 10) || 1; + return; + } + const colspan = parseInt(cell.getAttribute("colspan") || "1", 10) || 1; + const slice = colWidths!.slice(col, col + colspan); + col += colspan; + if (slice.length === 0 || slice.every((w) => w === null)) return; + const values = slice.map((w) => (w == null ? 100 : w)); + cell.setAttribute("colwidth", values.join(",")); + }); + }); +} diff --git a/apps/client/src/features/editor/full-editor.tsx b/apps/client/src/features/editor/full-editor.tsx index 6ebb8669c..98930db28 100644 --- a/apps/client/src/features/editor/full-editor.tsx +++ b/apps/client/src/features/editor/full-editor.tsx @@ -1,32 +1,47 @@ import classes from "@/features/editor/styles/editor.module.css"; -import React from "react"; +import React, { useEffect } from "react"; import { TitleEditor } from "@/features/editor/title-editor"; import PageEditor from "@/features/editor/page-editor"; import { + ActionIcon, Container, Divider, Group, Popover, Stack, Text, + Tooltip, UnstyledButton, } from "@mantine/core"; +import { IconInfoCircle } from "@tabler/icons-react"; import { useAtom } from "jotai"; import { userAtom } from "@/features/user/atoms/current-user-atom.ts"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; import { PageVerificationBadge } from "@/ee/page-verification"; import { useTranslation } from "react-i18next"; import { IContributor } from "@/features/page/types/page.types.ts"; +import { FixedToolbar } from "@/features/editor/components/fixed-toolbar/fixed-toolbar"; +import { PageEditMode } from "@/features/user/types/user.types.ts"; +import { useAsideTriggerProps } from "@/hooks/use-toggle-aside.tsx"; +import { DeletedPageBanner } from "@/features/page/trash/components/deleted-page-banner.tsx"; +import clsx from "clsx"; +import { currentPageEditModeAtom } from "@/features/editor/atoms/editor-atoms.ts"; const MemoizedTitleEditor = React.memo(TitleEditor); const MemoizedPageEditor = React.memo(PageEditor); +const MemoizedFixedToolbar = React.memo(FixedToolbar); +const MemoizedDeletedPageBanner = React.memo(DeletedPageBanner); -type PageCreator = { +type PageUser = { id: string; name: string; avatarUrl: string; }; +// Module-level flag: survives component unmount/remount on page navigation, +// reset only on full page reload (i.e. a new app session). +let defaultEditModeApplied = false; + export interface FullEditorProps { pageId: string; slugId: string; @@ -34,7 +49,7 @@ export interface FullEditorProps { content: string; spaceSlug: string; editable: boolean; - creator?: PageCreator; + creator?: PageUser; contributors?: IContributor[]; canComment?: boolean; } @@ -52,6 +67,23 @@ export function FullEditor({ }: FullEditorProps) { const [user] = useAtom(userAtom); const fullPageWidth = user.settings?.preferences?.fullPageWidth; + const editorToolbarEnabled = + user.settings?.preferences?.editorToolbar ?? false; + const [currentPageEditMode, setCurrentPageEditMode] = useAtom( + currentPageEditModeAtom, + ); + const userPageEditMode = + user.settings?.preferences?.pageEditMode ?? PageEditMode.Edit; + const isEditMode = currentPageEditMode === PageEditMode.Edit; + + // Apply the user's saved preference only once on initial load, not on every + // page navigation — so the mode sticks across navigations within a session. + useEffect(() => { + if (!defaultEditModeApplied) { + setCurrentPageEditMode(userPageEditMode as PageEditMode); + defaultEditModeApplied = true; + } + }, [userPageEditMode, setCurrentPageEditMode]); return ( + {editorToolbarEnabled && editable && isEditMode && ( + + )} + c.id !== creator?.id, @@ -102,13 +135,15 @@ function PageByline({ {creator && ( - + )} + + + + + + ); diff --git a/apps/client/src/features/editor/page-editor.tsx b/apps/client/src/features/editor/page-editor.tsx index 7cc4723ac..a703561f5 100644 --- a/apps/client/src/features/editor/page-editor.tsx +++ b/apps/client/src/features/editor/page-editor.tsx @@ -26,10 +26,11 @@ import { collabExtensions, mainExtensions, } from "@/features/editor/extensions/extensions"; -import { useAtom } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import useCollaborationUrl from "@/features/editor/hooks/use-collaboration-url"; import { currentUserAtom } from "@/features/user/atoms/current-user-atom"; import { + currentPageEditModeAtom, pageEditorAtom, yjsConnectionStatusAtom, } from "@/features/editor/atoms/editor-atoms"; @@ -42,8 +43,8 @@ import { import CommentDialog from "@/features/comment/components/comment-dialog"; import { EditorBubbleMenu } from "@/features/editor/components/bubble-menu/bubble-menu"; import { ReadonlyBubbleMenu } from "@/features/editor/components/bubble-menu/readonly-bubble-menu"; -import TableCellMenu from "@/features/editor/components/table/table-cell-menu.tsx"; import TableMenu from "@/features/editor/components/table/table-menu.tsx"; +import { TableHandlesLayer } from "@/features/editor/components/table/handle/table-handles-layer"; import ImageMenu from "@/features/editor/components/image/image-menu.tsx"; import CalloutMenu from "@/features/editor/components/callout/callout-menu.tsx"; import VideoMenu from "@/features/editor/components/video/video-menu.tsx"; @@ -53,7 +54,7 @@ import { handleFileDrop, handlePaste, } from "@/features/editor/components/common/editor-paste-handler.tsx"; -import ExcalidrawMenu from "./components/excalidraw/excalidraw-menu"; +import ExcalidrawMenu from "./components/excalidraw/excalidraw-menu-lazy"; import DrawioMenu from "./components/drawio/drawio-menu"; import { useCollabToken } from "@/features/auth/queries/auth-query.tsx"; import SearchAndReplaceDialog from "@/features/editor/components/search-and-replace/search-and-replace-dialog.tsx"; @@ -62,7 +63,7 @@ import { useIdle } from "@/hooks/use-idle.ts"; import { queryClient } from "@/main.tsx"; import { IPage } from "@/features/page/types/page.types.ts"; import { useParams } from "react-router-dom"; -import { extractPageSlugId } from "@/lib"; +import { extractPageSlugId, platformModifierKey } from "@/lib"; import { FIVE_MINUTES } from "@/lib/constants.ts"; import { PageEditMode } from "@/features/user/types/user.types.ts"; import { jwtDecode } from "jwt-decode"; @@ -71,6 +72,8 @@ import { useEditorScroll } from "./hooks/use-editor-scroll"; import { EditorAiMenu } from "@/ee/ai/components/editor/ai-menu/ai-menu"; import { EditorLinkMenu } from "@/features/editor/components/link/link-menu"; import ColumnsMenu from "@/features/editor/components/columns/columns-menu.tsx"; +import { TransclusionLookupProvider } from "@/features/editor/components/transclusion/transclusion-lookup-context"; +import { useTranslation } from "react-i18next"; interface PageEditorProps { pageId: string; @@ -85,6 +88,7 @@ export default function PageEditor({ content, canComment, }: PageEditorProps) { + const { t } = useTranslation(); const collaborationURL = useCollaborationUrl(); const isComponentMounted = useRef(false); const editorRef = useRef(null); @@ -110,8 +114,7 @@ export default function PageEditor({ const documentState = useDocumentVisibility(); const { pageSlug } = useParams(); const slugId = extractPageSlugId(pageSlug); - const userPageEditMode = - currentUser?.user?.settings?.preferences?.pageEditMode ?? PageEditMode.Edit; + const currentPageEditMode = useAtomValue(currentPageEditModeAtom); const canScroll = useCallback( () => Boolean(isComponentMounted.current && editorRef.current), [isComponentMounted], @@ -230,13 +233,16 @@ export default function PageEditor({ editorProps: { scrollThreshold: 80, scrollMargin: 80, + attributes: { + "aria-label": t("Page content"), + }, handleDOMEvents: { keydown: (_view, event) => { - if ((event.ctrlKey || event.metaKey) && event.code === "KeyS") { + if (platformModifierKey(event) && event.code === "KeyS") { event.preventDefault(); return true; } - if ((event.ctrlKey || event.metaKey) && event.code === "KeyK") { + if (platformModifierKey(event) && event.code === "KeyK") { searchSpotlight.open(); return true; } @@ -363,19 +369,9 @@ export default function PageEditor({ return () => clearTimeout(timeout); }, [yjsConnectionStatus, isSynced]); useEffect(() => { - // Only honor user default page edit mode preference and permissions - if (editor) { - if (userPageEditMode && editable) { - if (userPageEditMode === PageEditMode.Edit) { - editor.setEditable(true); - } else if (userPageEditMode === PageEditMode.Read) { - editor.setEditable(false); - } - } else { - editor.setEditable(false); - } - } - }, [userPageEditMode, editor, editable]); + if (!editor) return; + editor.setEditable(editable && currentPageEditMode === PageEditMode.Edit); + }, [currentPageEditMode, editor, editable]); const hasConnectedOnceRef = useRef(false); const [showStatic, setShowStatic] = useState(true); @@ -391,55 +387,63 @@ export default function PageEditor({ } }, [yjsConnectionStatus, isSynced]); - if (showStatic) { - return ( - - ); - } - return ( -
    -
    - + + {showStatic ? ( + + ) : ( +
    +
    + - {editor && ( - - )} + {editor && ( + + )} - {editor && editorIsEditable && ( -
    - - - - - - - - - - - - - + {editor && editorIsEditable && ( +
    + + + + + + + + + + + + + +
    + )} + {editor && + !editorIsEditable && + (editable || canComment) && + providersRef.current && } + {showCommentPopup && ( + + )} + {showReadOnlyCommentPopup && ( + + )}
    - )} - {editor && !editorIsEditable && (editable || canComment) && providersRef.current && ( - - )} - {showCommentPopup && } - {showReadOnlyCommentPopup && ( - - )} -
    -
    editor.commands.focus("end")} - style={{ paddingBottom: "20vh" }} - >
    -
    +
    editor.commands.focus("end")} + style={{ paddingBottom: "20vh" }} + >
    +
    + )} + ); } diff --git a/apps/client/src/features/editor/readonly-page-editor.tsx b/apps/client/src/features/editor/readonly-page-editor.tsx index a27c7bfb3..cd4878a9b 100644 --- a/apps/client/src/features/editor/readonly-page-editor.tsx +++ b/apps/client/src/features/editor/readonly-page-editor.tsx @@ -9,17 +9,26 @@ import { Placeholder } from "@tiptap/extension-placeholder"; import { useAtom } from "jotai"; import { readOnlyEditorAtom } from "@/features/editor/atoms/editor-atoms.ts"; import { useEditorScroll } from "./hooks/use-editor-scroll"; +import { TransclusionLookupProvider } from "@/features/editor/components/transclusion/transclusion-lookup-context"; interface PageEditorProps { title: string; content: any; pageId?: string; + /** + * When rendering inside a public share, pass the share's id (or key). Lookups + * for transclusion content then resolve against the share graph instead of + * the viewer's personal permissions, so a share never leaks source content + * that isn't itself shared. + */ + shareId?: string; } export default function ReadonlyPageEditor({ title, content, pageId, + shareId, }: PageEditorProps) { const [, setReadOnlyEditor] = useAtom(readOnlyEditorAtom); const isComponentMounted = useRef(false); @@ -65,7 +74,7 @@ export default function ReadonlyPageEditor({ ]; return ( - <> +
    - + ); } diff --git a/apps/client/src/features/editor/styles/core.css b/apps/client/src/features/editor/styles/core.css index a3b4f4285..ab3d23676 100644 --- a/apps/client/src/features/editor/styles/core.css +++ b/apps/client/src/features/editor/styles/core.css @@ -211,7 +211,8 @@ } } - .resize-cursor { + &.resize-cursor, + &.resize-cursor * { cursor: ew-resize; cursor: col-resize; } diff --git a/apps/client/src/features/editor/styles/editor.module.css b/apps/client/src/features/editor/styles/editor.module.css index dfe7393f8..ed5f86432 100644 --- a/apps/client/src/features/editor/styles/editor.module.css +++ b/apps/client/src/features/editor/styles/editor.module.css @@ -9,3 +9,15 @@ } } +.byline { + padding-left: 3rem; + + @media (max-width: $mantine-breakpoint-sm) { + padding-left: 1rem; + } + + @media print { + padding-left: 0; + } +} + diff --git a/apps/client/src/features/editor/styles/indent.css b/apps/client/src/features/editor/styles/indent.css new file mode 100644 index 000000000..cd2bd5857 --- /dev/null +++ b/apps/client/src/features/editor/styles/indent.css @@ -0,0 +1,14 @@ +.ProseMirror { + --indent-step: 2rem; +} + +.ProseMirror [data-indent="1"] { padding-inline-start: calc(var(--indent-step) * 1); } +.ProseMirror [data-indent="2"] { padding-inline-start: calc(var(--indent-step) * 2); } +.ProseMirror [data-indent="3"] { padding-inline-start: calc(var(--indent-step) * 3); } +.ProseMirror [data-indent="4"] { padding-inline-start: calc(var(--indent-step) * 4); } +.ProseMirror [data-indent="5"] { padding-inline-start: calc(var(--indent-step) * 5); } +.ProseMirror [data-indent="6"] { padding-inline-start: calc(var(--indent-step) * 6); } +.ProseMirror [data-indent="7"] { padding-inline-start: calc(var(--indent-step) * 7); } +.ProseMirror [data-indent="8"] { padding-inline-start: calc(var(--indent-step) * 8); } +.ProseMirror [data-indent="9"] { padding-inline-start: calc(var(--indent-step) * 9); } +.ProseMirror [data-indent="10"] { padding-inline-start: calc(var(--indent-step) * 10); } diff --git a/apps/client/src/features/editor/styles/index.css b/apps/client/src/features/editor/styles/index.css index ffec33f3d..7b1ce93e8 100644 --- a/apps/client/src/features/editor/styles/index.css +++ b/apps/client/src/features/editor/styles/index.css @@ -9,10 +9,12 @@ @import "./media.css"; @import "./code.css"; @import "./print.css"; +@import "./page-break.css"; @import "./find.css"; @import "./mention.css"; @import "./ordered-list.css"; @import "./highlight.css"; +@import "./indent.css"; @import "./columns.css"; @import "./status.css"; @import "./base-embed.css"; diff --git a/apps/client/src/features/editor/styles/page-break.css b/apps/client/src/features/editor/styles/page-break.css new file mode 100644 index 000000000..6dc97c738 --- /dev/null +++ b/apps/client/src/features/editor/styles/page-break.css @@ -0,0 +1,50 @@ +.ProseMirror .page-break { + position: relative; + margin: 1.5rem 0; + border-top: 1px dashed var(--mantine-color-default-border); + height: 0; + user-select: none; +} + +.ProseMirror[contenteditable="false"] .page-break { + margin: 0; + border: none; + height: 0; +} + +.ProseMirror[contenteditable="false"] .page-break::after { + content: none; +} + +.ProseMirror .page-break::after { + content: "Page break"; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + padding: 0 0.5rem; + background: var(--mantine-color-body); + color: var(--mantine-color-dimmed); + font-size: 0.75rem; + line-height: 1; + letter-spacing: 0.02em; + text-transform: uppercase; +} + +.ProseMirror .page-break.ProseMirror-selectednode { + border-top-color: var(--mantine-primary-color-filled); +} + +@media print { + .ProseMirror .page-break { + break-before: always; + page-break-before: always; + visibility: hidden; + border: none; + margin: 0; + } + + .ProseMirror .page-break::after { + content: none; + } +} diff --git a/apps/client/src/features/editor/styles/placeholder.css b/apps/client/src/features/editor/styles/placeholder.css index be5225bf6..9d42f22b0 100644 --- a/apps/client/src/features/editor/styles/placeholder.css +++ b/apps/client/src/features/editor/styles/placeholder.css @@ -1,7 +1,7 @@ .ProseMirror .is-editor-empty:first-child::before { content: attr(data-placeholder); float: left; - color: #adb5bd; + color: var(--mantine-color-placeholder); pointer-events: none; height: 0; @@ -13,7 +13,7 @@ .ProseMirror .is-empty::before { content: attr(data-placeholder); float: left; - color: #adb5bd; + color: var(--mantine-color-placeholder); pointer-events: none; height: 0; diff --git a/apps/client/src/features/editor/styles/table.css b/apps/client/src/features/editor/styles/table.css index 9926d0bc0..5d802e4ab 100644 --- a/apps/client/src/features/editor/styles/table.css +++ b/apps/client/src/features/editor/styles/table.css @@ -15,7 +15,8 @@ } .table-dnd-drop-indicator { - background-color: #adf; + background-color: var(--mantine-color-blue-5); + z-index: 3; } .ProseMirror { @@ -57,13 +58,14 @@ } .column-resize-handle { - background-color: #adf; + background-color: var(--mantine-color-blue-5); bottom: -1px; position: absolute; - right: -2px; + right: -1px; pointer-events: none; top: 0; - width: 4px; + width: 2px; + z-index: 3; } .selectedCell:after { @@ -129,6 +131,139 @@ } } + +/* Header-row pinning. Two CSS paths, picked by `header-pin/controller.ts`: + - native sticky (preferred): wrapper drops its overflow constraint so + `position: sticky` on the row can resolve against the document scroll. + - transform fallback: wrapper keeps `overflow-x: auto` for horizontal + scrolling; the row is positioned imperatively per scroll frame. + + `--editor-pin-offset` is published to :root by `pinOffsetWatcher` in + `header-pin/offset.ts`, measured against the lowest fixed surface above + the editor (app shell header, page header, fixed toolbar). */ + +.tableWrapper.tableWrapperNoOverflow, +.tableWrapper.tableWrapperNoOverflow table { + overflow: visible; +} + +.tableWrapper.tableHeaderPinned table tr:first-child { + z-index: 2; +} + +.tableWrapper.tableWrapperNoOverflow.tableHeaderPinned table tr:first-child { + position: sticky; + top: var(--editor-pin-offset, 90px); +} + +.tableWrapper.tableHeaderPinned:not(.tableWrapperNoOverflow) table tr:first-child { + position: relative; + transform: translateY(var(--table-pin-offset, 0px)); +} + +@media print { + .tableWrapper.tableHeaderPinned table tr:first-child { + position: static; + transform: none; + } +} + +.tableReadonlySortChevron { + /* Anchor to the cell's right edge, vertically centered with the cell + content. The cell content (a

    ) is block-level so an inline chevron + would wrap to a new line; absolute positioning takes it out of flow. */ + position: absolute; + top: 50%; + right: 6px; + transform: translateY(-50%); + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 4px; + background: light-dark( + rgba(55, 53, 47, 0.08), + rgba(255, 255, 255, 0.08) + ); + color: light-dark( + rgba(55, 53, 47, 0.55), + rgba(255, 255, 255, 0.55) + ); + user-select: none; + cursor: pointer; + z-index: 1; + /* Hidden by default; revealed on header-cell hover or when this column is + the active sort (see selectors below). */ + opacity: 0; + transition: opacity 120ms ease, background-color 120ms ease, color 120ms ease; +} + +.ProseMirror table th:hover .tableReadonlySortChevron, +.tableReadonlySortChevron[data-sort] { + opacity: 1; +} + +.ProseMirror table th:has(.tableReadonlySortChevron) { + padding-right: 30px; +} + +.tableReadonlySortChevron:hover { + background: light-dark( + rgba(55, 53, 47, 0.16), + rgba(255, 255, 255, 0.16) + ); +} + +/* Immediate tooltip on the chevron — same style language as the rest of the + app (small, dark, rounded), unlike the native `title` tooltip which only + appears after a long delay. */ +.tableReadonlySortChevron::after { + content: attr(data-tooltip); + position: absolute; + /* Below the chevron — placing it above the cell hits the table's + overflow clipping (the wrapper has `overflow-x: auto` which forces + `overflow-y: auto` per spec). */ + top: calc(100% + 6px); + right: 0; + padding: 4px 8px; + border-radius: 4px; + background: var(--mantine-color-dark-7); + color: var(--mantine-color-white); + font-size: 12px; + font-weight: 400; + line-height: 1.4; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 120ms ease; + z-index: 10; +} + +.tableReadonlySortChevron:hover::after { + opacity: 1; +} + +.tableReadonlySortChevron svg { + display: block; +} + +.tableReadonlySortChevron[data-sort="asc"], +.tableReadonlySortChevron[data-sort="desc"] { + background: light-dark( + var(--mantine-color-blue-1), + var(--mantine-color-blue-9) + ); + color: light-dark( + var(--mantine-color-blue-7), + var(--mantine-color-blue-2) + ); +} + +.tableReadonlySortChevron[data-sort="asc"] svg { + transform: rotate(180deg); +} + .editor-container:has(.table-dnd-drop-indicator[data-dragging="true"]) { .prosemirror-dropcursor-block { display: none; diff --git a/apps/client/src/features/editor/title-editor.tsx b/apps/client/src/features/editor/title-editor.tsx index 15c3ff028..fefe9f330 100644 --- a/apps/client/src/features/editor/title-editor.tsx +++ b/apps/client/src/features/editor/title-editor.tsx @@ -7,6 +7,7 @@ import { Text } from "@tiptap/extension-text"; import { Placeholder } from "@tiptap/extension-placeholder"; import { useAtomValue } from "jotai"; import { + currentPageEditModeAtom, pageEditorAtom, titleEditorAtom, } from "@/features/editor/atoms/editor-atoms"; @@ -24,9 +25,9 @@ import { useTranslation } from "react-i18next"; import EmojiCommand from "@/features/editor/extensions/emoji-command.ts"; import { UpdateEvent } from "@/features/websocket/types"; import localEmitter from "@/lib/local-emitter.ts"; -import { currentUserAtom } from "@/features/user/atoms/current-user-atom.ts"; import { PageEditMode } from "@/features/user/types/user.types.ts"; import { searchSpotlight } from "@/features/search/constants.ts"; +import { platformModifierKey } from "@/lib"; export interface TitleEditorProps { pageId: string; @@ -51,9 +52,7 @@ export function TitleEditor({ const emit = useQueryEmit(); const navigate = useNavigate(); const [activePageId, setActivePageId] = useState(pageId); - const [currentUser] = useAtom(currentUserAtom); - const userPageEditMode = - currentUser?.user?.settings?.preferences?.pageEditMode ?? PageEditMode.Edit; + const currentPageEditMode = useAtomValue(currentPageEditModeAtom); const titleEditor = useEditor({ extensions: [ @@ -88,13 +87,16 @@ export function TitleEditor({ immediatelyRender: true, shouldRerenderOnTransaction: false, editorProps: { + attributes: { + "aria-label": t("Page title"), + }, handleDOMEvents: { keydown: (_view, event) => { - if ((event.ctrlKey || event.metaKey) && event.code === "KeyS") { + if (platformModifierKey(event) && event.code === "KeyS") { event.preventDefault(); return true; } - if ((event.ctrlKey || event.metaKey) && event.code === "KeyK") { + if (platformModifierKey(event) && event.code === "KeyK") { searchSpotlight.open(); return true; } @@ -171,18 +173,9 @@ export function TitleEditor({ }, [pageId]); useEffect(() => { - if (titleEditor) { - if (userPageEditMode && editable) { - if (userPageEditMode === PageEditMode.Edit) { - titleEditor.setEditable(true); - } else if (userPageEditMode === PageEditMode.Read) { - titleEditor.setEditable(false); - } - } else { - titleEditor.setEditable(false); - } - } - }, [userPageEditMode, titleEditor, editable]); + if (!titleEditor) return; + titleEditor.setEditable(editable && currentPageEditMode === PageEditMode.Edit); + }, [currentPageEditMode, titleEditor, editable]); const openSearchDialog = () => { const event = new CustomEvent("openFindDialogFromEditor", {}); diff --git a/apps/client/src/features/favorite/components/star-button.tsx b/apps/client/src/features/favorite/components/star-button.tsx index 6a99e01e9..2b341c736 100644 --- a/apps/client/src/features/favorite/components/star-button.tsx +++ b/apps/client/src/features/favorite/components/star-button.tsx @@ -1,4 +1,5 @@ import { ActionIcon, Tooltip } from "@mantine/core"; +import { notifications } from "@mantine/notifications"; import { IconStar, IconStarFilled } from "@tabler/icons-react"; import { useFavoriteIds, @@ -14,6 +15,8 @@ type StarButtonProps = { pageId?: string; spaceId?: string; templateId?: string; + /** Name of the item being favorited, used to make the button's accessible name descriptive. */ + name?: string; size?: number; }; @@ -25,7 +28,7 @@ function getEntityId(props: StarButtonProps): string | undefined { } export default function StarButton(props: StarButtonProps) { - const { type, size = 18 } = props; + const { type, name, size = 18 } = props; const { t } = useTranslation(); const favoriteIds = useFavoriteIds(type); const addMutation = useAddFavoriteMutation(); @@ -47,21 +50,47 @@ export default function StarButton(props: StarButtonProps) { }; if (isFavorited) { - removeMutation.mutate(params); + removeMutation.mutate(params, { + onSuccess: () => { + notifications.show({ + message: name + ? t("Removed {{name}} from favorites", { name }) + : t("Removed from favorites"), + }); + }, + }); } else { - addMutation.mutate(params); + addMutation.mutate(params, { + onSuccess: () => { + notifications.show({ + message: name + ? t("Added {{name}} to favorites", { name }) + : t("Added to favorites"), + }); + }, + }); } }; + // Tooltip label stays short. Accessible name expands to include the item + // so screen reader users can distinguish stars on different rows. + const tooltipLabel = isFavorited + ? t("Remove from favorites") + : t("Add to favorites"); + + const ariaLabel = name + ? isFavorited + ? t("Remove {{name}} from favorites", { name }) + : t("Add {{name}} to favorites", { name }) + : tooltipLabel; + return ( - + diff --git a/apps/client/src/features/group/components/add-group-member-modal.tsx b/apps/client/src/features/group/components/add-group-member-modal.tsx index a5abaa70f..0cf81f5a0 100644 --- a/apps/client/src/features/group/components/add-group-member-modal.tsx +++ b/apps/client/src/features/group/components/add-group-member-modal.tsx @@ -31,7 +31,12 @@ export default function AddGroupMemberModal() { <> - + diff --git a/apps/client/src/features/group/components/create-group-modal.tsx b/apps/client/src/features/group/components/create-group-modal.tsx index 1cff53fbd..307a38f3c 100644 --- a/apps/client/src/features/group/components/create-group-modal.tsx +++ b/apps/client/src/features/group/components/create-group-modal.tsx @@ -11,7 +11,12 @@ export default function CreateGroupModal() { <> - + diff --git a/apps/client/src/features/group/components/edit-group-form.tsx b/apps/client/src/features/group/components/edit-group-form.tsx index f8b1671cd..68b6ba60c 100644 --- a/apps/client/src/features/group/components/edit-group-form.tsx +++ b/apps/client/src/features/group/components/edit-group-form.tsx @@ -9,6 +9,7 @@ import { z } from "zod/v4"; import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { zod4Resolver } from "mantine-form-zod-resolver"; +import { IGroup } from "@/features/group/types/group.types.ts"; const formSchema = z.object({ name: z.string().min(2).max(100), @@ -18,13 +19,16 @@ const formSchema = z.object({ type FormValues = z.infer; interface EditGroupFormProps { onClose?: () => void; + group?: IGroup; } -export function EditGroupForm({ onClose }: EditGroupFormProps) { +export function EditGroupForm({ onClose, group: groupProp }: EditGroupFormProps) { const { t } = useTranslation(); const updateGroupMutation = useUpdateGroupMutation(); const { isSuccess } = updateGroupMutation; - const { groupId } = useParams(); - const { data: group } = useGroupQuery(groupId); + const { groupId: routeGroupId } = useParams(); + const groupId = groupProp?.id ?? routeGroupId; + const { data: queriedGroup } = useGroupQuery(groupProp ? undefined : groupId); + const group = groupProp ?? queriedGroup; useEffect(() => { if (isSuccess) { @@ -66,6 +70,7 @@ export function EditGroupForm({ onClose }: EditGroupFormProps) { label={t("Group name")} placeholder={t("e.g Developers")} variant="filled" + data-autofocus {...form.getInputProps("name")} /> diff --git a/apps/client/src/features/group/components/edit-group-modal.tsx b/apps/client/src/features/group/components/edit-group-modal.tsx index 4da933f25..a2fc28f2f 100644 --- a/apps/client/src/features/group/components/edit-group-modal.tsx +++ b/apps/client/src/features/group/components/edit-group-modal.tsx @@ -1,23 +1,31 @@ import { Divider, Modal } from "@mantine/core"; import { EditGroupForm } from "@/features/group/components/edit-group-form.tsx"; import { useTranslation } from "react-i18next"; +import { IGroup } from "@/features/group/types/group.types.ts"; interface EditGroupModalProps { opened: boolean; onClose: () => void; + group?: IGroup; } export default function EditGroupModal({ opened, onClose, + group, }: EditGroupModalProps) { const { t } = useTranslation(); return ( <> - + - + ); diff --git a/apps/client/src/features/group/components/group-action-menu.tsx b/apps/client/src/features/group/components/group-action-menu.tsx index 331ea3dea..cb50f4bfa 100644 --- a/apps/client/src/features/group/components/group-action-menu.tsx +++ b/apps/client/src/features/group/components/group-action-menu.tsx @@ -10,18 +10,28 @@ import { useDisclosure } from "@mantine/hooks"; import EditGroupModal from "@/features/group/components/edit-group-modal.tsx"; import { modals } from "@mantine/modals"; import { useTranslation } from "react-i18next"; +import { IGroup } from "@/features/group/types/group.types.ts"; -export default function GroupActionMenu() { +interface GroupActionMenuProps { + group?: IGroup; +} + +export default function GroupActionMenu(props: GroupActionMenuProps = {}) { const { t } = useTranslation(); - const { groupId } = useParams(); - const { data: group, isLoading } = useGroupQuery(groupId); + const { groupId: routeGroupId } = useParams(); + const groupId = props.group?.id ?? routeGroupId; + const { data: queriedGroup } = useGroupQuery(props.group ? undefined : groupId); + const group = props.group ?? queriedGroup; const deleteGroupMutation = useDeleteGroupMutation(); const navigate = useNavigate(); const [opened, { open, close }] = useDisclosure(false); const onDelete = async () => { await deleteGroupMutation.mutateAsync(groupId); - navigate("/settings/groups"); + // Only navigate away if we're currently viewing this group's detail page. + if (routeGroupId === groupId) { + navigate("/settings/groups"); + } }; const openDeleteModal = () => @@ -53,7 +63,11 @@ export default function GroupActionMenu() { arrowPosition="center" > - + @@ -76,7 +90,7 @@ export default function GroupActionMenu() { )} - + ); } diff --git a/apps/client/src/features/group/components/group-list.tsx b/apps/client/src/features/group/components/group-list.tsx index d88e1ec02..5f5fed472 100644 --- a/apps/client/src/features/group/components/group-list.tsx +++ b/apps/client/src/features/group/components/group-list.tsx @@ -1,4 +1,4 @@ -import { Table, Group, Text, Anchor } from "@mantine/core"; +import { Table, Group, Text, Anchor, VisuallyHidden } from "@mantine/core"; import { useGetGroupsQuery } from "@/features/group/queries/group-query"; import { Link } from "react-router-dom"; import { IconGroupCircle } from "@/components/icons/icon-people-circle.tsx"; @@ -12,6 +12,8 @@ import { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx"; import { SearchInput } from "@/components/common/search-input.tsx"; import NoTableResults from "@/components/common/no-table-results.tsx"; import { usePaginateAndSearch } from "@/hooks/use-paginate-and-search.tsx"; +import rowClasses from "@/components/ui/clickable-table-row.module.css"; +import GroupActionMenu from "@/features/group/components/group-action-menu.tsx"; export default function GroupList() { const { t } = useTranslation(); @@ -34,13 +36,16 @@ export default function GroupList() { {t("Group")} {t("Members")} + + {t("Actions")} + {data?.items.length > 0 ? ( data?.items.map((group: IGroup, index: number) => ( - + prefetchGroupMembers(group.id)}> @@ -80,10 +86,13 @@ export default function GroupList() { {formatMemberCount(group.memberCount, t)} + + + )) ) : ( - + )}

diff --git a/apps/client/src/features/group/components/group-members.tsx b/apps/client/src/features/group/components/group-members.tsx index 9b5bbd7b2..14c5903a7 100644 --- a/apps/client/src/features/group/components/group-members.tsx +++ b/apps/client/src/features/group/components/group-members.tsx @@ -54,7 +54,7 @@ export default function GroupMembersList() { {t("User")} {t("Status")} - + @@ -88,7 +88,11 @@ export default function GroupMembersList() { arrowPosition="center" > - + diff --git a/apps/client/src/features/home/components/created-by-me.tsx b/apps/client/src/features/home/components/created-by-me.tsx index 70137b105..bc79da994 100644 --- a/apps/client/src/features/home/components/created-by-me.tsx +++ b/apps/client/src/features/home/components/created-by-me.tsx @@ -4,7 +4,7 @@ import { UnstyledButton, Badge, Table, - ActionIcon, + ThemeIcon, Button, } from "@mantine/core"; import { Link } from "react-router-dom"; @@ -17,6 +17,7 @@ import { EmptyState } from "@/components/ui/empty-state"; import { getSpaceUrl } from "@/lib/config"; import { useTranslation } from "react-i18next"; import { getInitialsColor } from "@/lib/get-initials-color"; +import rowClasses from "@/components/ui/clickable-table-row.module.css"; type Props = { spaceId?: string; @@ -49,9 +50,10 @@ export default function CreatedByMe({ spaceId }: Props) { {pages.map((page) => ( - + {page.icon || ( - - + )} {page.title || t("Untitled")} diff --git a/apps/client/src/features/home/components/favorites-pages.tsx b/apps/client/src/features/home/components/favorites-pages.tsx index eb87216e0..9aada2141 100644 --- a/apps/client/src/features/home/components/favorites-pages.tsx +++ b/apps/client/src/features/home/components/favorites-pages.tsx @@ -4,7 +4,7 @@ import { UnstyledButton, Badge, Table, - ActionIcon, + ThemeIcon, Button, } from "@mantine/core"; import { Link } from "react-router-dom"; @@ -17,6 +17,7 @@ import { EmptyState } from "@/components/ui/empty-state"; import { getSpaceUrl } from "@/lib/config"; import { useTranslation } from "react-i18next"; import { getInitialsColor } from "@/lib/get-initials-color"; +import rowClasses from "@/components/ui/clickable-table-row.module.css"; interface Props { spaceId?: string; @@ -50,9 +51,10 @@ export default function FavoritesPages({ spaceId }: Props) { {favorites.map((fav) => fav.page ? ( - + {fav.page.icon || ( - - + )} {fav.page.title || t("Untitled")} diff --git a/apps/client/src/features/home/components/home-ai-prompt.module.css b/apps/client/src/features/home/components/home-ai-prompt.module.css index e6d816067..8a6d57e11 100644 --- a/apps/client/src/features/home/components/home-ai-prompt.module.css +++ b/apps/client/src/features/home/components/home-ai-prompt.module.css @@ -16,7 +16,7 @@ .subtitle { font-size: var(--mantine-font-size-sm); - color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2)); + color: var(--mantine-color-dimmed); text-align: center; margin-top: 6px; margin-bottom: var(--mantine-spacing-lg); diff --git a/apps/client/src/features/label/components/label-chip.tsx b/apps/client/src/features/label/components/label-chip.tsx new file mode 100644 index 000000000..ad15ff4a3 --- /dev/null +++ b/apps/client/src/features/label/components/label-chip.tsx @@ -0,0 +1,51 @@ +import { Link } from "react-router-dom"; +import { useComputedColorScheme } from "@mantine/core"; +import { IconX } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { ILabel } from "@/features/label/types/label.types.ts"; +import { getLabelColor } from "@/features/label/utils/label-colors.ts"; +import classes from "@/features/label/label.module.css"; + +type LabelChipProps = { + label: Pick; + onRemove?: () => void; + asLink?: boolean; +}; + +export function LabelChip({ label, onRemove, asLink }: LabelChipProps) { + const { t } = useTranslation(); + const scheme = useComputedColorScheme("light"); + const c = getLabelColor(label.name, scheme); + + const nameNode = asLink ? ( + e.stopPropagation()} + > + {label.name} + + ) : ( + {label.name} + ); + + return ( + + {nameNode} + {onRemove && ( + + )} + + ); +} diff --git a/apps/client/src/features/label/components/label-page-row-skeleton.tsx b/apps/client/src/features/label/components/label-page-row-skeleton.tsx new file mode 100644 index 000000000..bc4a69915 --- /dev/null +++ b/apps/client/src/features/label/components/label-page-row-skeleton.tsx @@ -0,0 +1,29 @@ +import { Skeleton } from "@mantine/core"; +import classes from "@/features/label/label.module.css"; + +type LabelPageRowSkeletonProps = { + titleWidth?: number; + metaWidth?: number; +}; + +export function LabelPageRowSkeleton({ + titleWidth = 220, + metaWidth = 180, +}: LabelPageRowSkeletonProps) { + return ( + + ); +} diff --git a/apps/client/src/features/label/components/label-page-row.tsx b/apps/client/src/features/label/components/label-page-row.tsx new file mode 100644 index 000000000..7ca54c324 --- /dev/null +++ b/apps/client/src/features/label/components/label-page-row.tsx @@ -0,0 +1,91 @@ +import { Link } from "react-router-dom"; +import { ThemeIcon, Tooltip } from "@mantine/core"; +import { IconFileDescription } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { ILabelPageItem } from "@/features/label/types/label.types.ts"; +import { LabelChip } from "@/features/label/components/label-chip.tsx"; +import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; +import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts"; +import { buildPageUrl } from "@/features/page/page.utils"; +import { formatLabelListDate } from "@/features/label/utils/format-label-date.ts"; +import classes from "@/features/label/label.module.css"; + +type LabelPageRowProps = { + page: ILabelPageItem; + currentLabelName: string; +}; + +const MAX_VISIBLE_CHIPS = 3; + +export function LabelPageRow({ page, currentLabelName }: LabelPageRowProps) { + const { t } = useTranslation(); + + const otherLabels = page.labels.filter((l) => l.name !== currentLabelName); + const visibleLabels = otherLabels.slice(0, MAX_VISIBLE_CHIPS); + const hiddenLabels = otherLabels.slice(MAX_VISIBLE_CHIPS); + + return ( + +
+
+ {page.icon ? ( + {page.icon} + ) : ( + + + + )} +
+
+
+ {page.title || t("Untitled")} +
+
+ {page.space && ( + <> + + {page.space.name} + + + )} + + {t("Updated {{date}}", { + date: formatLabelListDate(new Date(page.updatedAt)), + })} + +
+ {/* {otherLabels.length > 0 && ( +
+ {visibleLabels.map((label) => ( + + ))} + {hiddenLabels.length > 0 && ( + l.name).join(", ")} + withArrow + openDelay={200} + > + + +{hiddenLabels.length} + + + )} +
+ )} */} +
+
+ + ); +} diff --git a/apps/client/src/features/label/components/label-picker.tsx b/apps/client/src/features/label/components/label-picker.tsx new file mode 100644 index 000000000..0bd5e6ada --- /dev/null +++ b/apps/client/src/features/label/components/label-picker.tsx @@ -0,0 +1,160 @@ +import { useMemo, useRef, useState, KeyboardEvent } from "react"; +import clsx from "clsx"; +import { IconPlus } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { useComputedColorScheme } from "@mantine/core"; +import { ILabel } from "@/features/label/types/label.types.ts"; +import { useWorkspaceLabelsQuery } from "@/features/label/queries/label-query.ts"; +import { getLabelColor } from "@/features/label/utils/label-colors.ts"; +import { normalizeLabelName } from "@/features/label/utils/normalize-label.ts"; +import classes from "@/features/label/label.module.css"; + +type LabelPickerProps = { + applied: ILabel[]; + enabled: boolean; + onAdd: (name: string) => void; + onClose: () => void; +}; + +const NAME_PATTERN = /^[a-z0-9_-][a-z0-9_~-]*$/; +const MAX_LABEL_NAME_LENGTH = 100; + +function isValidLabelName(name: string): boolean { + return ( + name.length > 0 && + name.length <= MAX_LABEL_NAME_LENGTH && + NAME_PATTERN.test(name) + ); +} + +export function LabelPicker({ + applied, + enabled, + onAdd, + onClose, +}: LabelPickerProps) { + const { t } = useTranslation(); + const scheme = useComputedColorScheme("light"); + const [query, setQuery] = useState(""); + const [hover, setHover] = useState(0); + const inputRef = useRef(null); + + const normalized = normalizeLabelName(query); + const { data } = useWorkspaceLabelsQuery(normalized, enabled); + + const appliedNames = useMemo( + () => new Set(applied.map((l) => l.name.toLowerCase())), + [applied], + ); + + const suggestions = useMemo(() => { + const items = data?.items ?? []; + return items.filter((l) => !appliedNames.has(l.name.toLowerCase())); + }, [data, appliedNames]); + + const exact = suggestions.find((l) => l.name === normalized); + const canCreate = + !exact && !appliedNames.has(normalized) && isValidLabelName(normalized); + + const total = suggestions.length + (canCreate ? 1 : 0); + + const select = (idx: number) => { + if (idx < suggestions.length) { + onAdd(suggestions[idx].name); + } else if (canCreate) { + onAdd(normalized); + } + setQuery(""); + setHover(0); + inputRef.current?.focus(); + }; + + const onKey = (e: KeyboardEvent) => { + if (e.key === "ArrowDown") { + e.preventDefault(); + setHover((h) => Math.min(Math.max(total - 1, 0), h + 1)); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setHover((h) => Math.max(0, h - 1)); + } else if (e.key === "Enter") { + e.preventDefault(); + if (total === 0) return; + select(hover); + } else if (e.key === "Escape") { + e.preventDefault(); + onClose(); + } + }; + + return ( +
+
+ { + setQuery(e.target.value); + setHover(0); + }} + onKeyDown={onKey} + /> +
+
+ {total === 0 && ( +
+ {normalized.length === 0 + ? t("No labels yet") + : appliedNames.has(normalized) + ? t("Already added") + : !isValidLabelName(normalized) + ? t("Invalid label name") + : t("No matches")} +
+ )} + {suggestions.map((s, i) => { + const c = getLabelColor(s.name, scheme); + return ( + + ); + })} + {canCreate && ( + + )} +
+
+ ); +} diff --git a/apps/client/src/features/label/components/labels-section.tsx b/apps/client/src/features/label/components/labels-section.tsx new file mode 100644 index 000000000..f44472b03 --- /dev/null +++ b/apps/client/src/features/label/components/labels-section.tsx @@ -0,0 +1,93 @@ +import { useState } from "react"; +import clsx from "clsx"; +import { Divider, Popover, Stack, Text } from "@mantine/core"; +import { IconPlus } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { LabelChip } from "@/features/label/components/label-chip.tsx"; +import { LabelPicker } from "@/features/label/components/label-picker.tsx"; +import { + useAddLabelsMutation, + usePageLabelsQuery, + useRemoveLabelMutation, +} from "@/features/label/queries/label-query.ts"; +import classes from "@/features/label/label.module.css"; + +type LabelsSectionProps = { + pageId: string; + canEdit: boolean; +}; + +export function LabelsSection({ pageId, canEdit }: LabelsSectionProps) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + + const { data } = usePageLabelsQuery(pageId); + const addMutation = useAddLabelsMutation(pageId); + const removeMutation = useRemoveLabelMutation(pageId); + + const labels = data?.items ?? []; + + if (!canEdit && labels.length === 0) { + return null; + } + + const handleAdd = (name: string) => { + addMutation.mutate({ pageId, names: [name] }); + }; + + const handleRemove = (labelId: string) => { + removeMutation.mutate({ pageId, labelId }); + }; + + return ( + <> + + + + {t("Labels")} + +
+ {labels.map((label) => ( + handleRemove(label.id) : undefined} + /> + ))} + {canEdit && ( + + + + + + handleAdd(name)} + onClose={() => setOpen(false)} + /> + + + )} +
+
+ + ); +} diff --git a/apps/client/src/features/label/label.module.css b/apps/client/src/features/label/label.module.css new file mode 100644 index 000000000..266a9c26f --- /dev/null +++ b/apps/client/src/features/label/label.module.css @@ -0,0 +1,325 @@ +.labelsWrap { + display: flex; + flex-wrap: wrap; + gap: 6px; + align-items: center; + margin-top: 4px; +} + +.chip { + display: inline-flex; + align-items: center; + gap: 6px; + height: 24px; + padding: 0 8px; + border-radius: 4px; + font-size: 12.5px; + font-weight: 500; + line-height: 1; + user-select: none; + white-space: nowrap; +} + +.chipName { + letter-spacing: 0.005em; +} + +.chipX { + appearance: none; + border: 0; + background: transparent; + color: currentColor; + width: 18px; + height: 18px; + border-radius: 4px; + margin-right: -4px; + margin-left: 0; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + padding: 0; + opacity: 0.6; +} + +.chipX:hover { + opacity: 1; + background: light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.12)); +} + +.addBtn { + appearance: none; + border: 1px dashed + light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-3)); + background: transparent; + color: var(--mantine-color-dimmed); + height: 24px; + padding: 0 8px; + border-radius: 4px; + font: inherit; + font-size: 12.5px; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 4px; + cursor: pointer; + transition: + background 100ms ease, + border-color 100ms ease, + color 100ms ease; +} + +.addBtn:hover { + background: light-dark(rgba(0, 0, 0, 0.03), rgba(255, 255, 255, 0.04)); + color: var(--mantine-color-text); + border-color: light-dark( + var(--mantine-color-gray-5), + var(--mantine-color-dark-2) + ); +} + +.addBtnOpen { + background: var(--mantine-color-body); + border-style: solid; + border-color: light-dark( + var(--mantine-color-gray-5), + var(--mantine-color-dark-2) + ); + color: var(--mantine-color-text); +} + +.popover { + width: 240px; + padding: 0; + overflow: hidden; +} + +.popoverSearch { + padding: 8px 8px 4px; + border-bottom: 1px solid + light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); +} + +.popoverSearch input { + width: 100%; + border: 0; + background: transparent; + font: inherit; + font-size: 13px; + padding: 4px 4px; + color: var(--mantine-color-text); + outline: none; +} + +.popoverSearch input::placeholder { + color: var(--mantine-color-placeholder); +} + +.popoverList { + max-height: 240px; + overflow-y: auto; + padding: 4px; +} + +.popoverEmpty { + padding: 12px 8px; + color: var(--mantine-color-dimmed); + font-size: 12.5px; + text-align: center; +} + +.popoverItem { + appearance: none; + width: 100%; + border: 0; + background: transparent; + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + border-radius: 4px; + font: inherit; + font-size: 13px; + color: var(--mantine-color-text); + cursor: pointer; + text-align: left; +} + +.popoverItemHover { + background: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-dark-5) + ); +} + +.popoverItemDot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} + +.popoverItemName { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.popoverCreatePlus { + width: 14px; + height: 14px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--mantine-color-dimmed); +} + +.headerChip { + display: inline-flex; + align-items: center; + gap: 8px; + height: 36px; + padding: 0 14px; + border-radius: 8px; + font-size: 22px; + font-weight: 600; + line-height: 1; + letter-spacing: -0.005em; + text-decoration: none; + user-select: none; + transition: filter 100ms ease; +} + +.headerChip:hover { + filter: brightness(0.97); +} + +.headerDot { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; +} + +.row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + padding: 14px 12px; + margin: 0 -12px; + border-radius: 8px; + text-decoration: none; + color: inherit; + cursor: pointer; + transition: background-color 80ms ease; +} + +.row + .row { + border-top: 1px solid + light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); +} + +.row:hover { + background-color: light-dark( + var(--mantine-color-gray-0), + var(--mantine-color-dark-6) + ); +} + +.row:hover + .row, +.row:has(+ .row:hover) { + border-top-color: transparent; +} + +.rowMain { + display: flex; + gap: 12px; + min-width: 0; + flex: 1; +} + +.rowIcon { + width: 20px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: var(--mantine-color-dimmed); + margin-top: 2px; +} + +.rowBody { + min-width: 0; + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.rowTitle { + font-size: 15px; + font-weight: 500; + color: var(--mantine-color-text); + line-height: 1.3; + word-break: break-word; +} + +.rowChips { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.chipMore { + display: inline-flex; + align-items: center; + height: 24px; + padding: 0 8px; + border-radius: 4px; + font-size: 12.5px; + font-weight: 500; + line-height: 1; + color: var(--mantine-color-dimmed); + background: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-dark-5) + ); + user-select: none; + white-space: nowrap; +} + +.rowMeta { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; + color: var(--mantine-color-dimmed); + font-size: 13px; +} + +.rowDate { + color: var(--mantine-color-dimmed); + font-size: 13px; + white-space: nowrap; + flex-shrink: 0; +} + +.metaDot { + font-size: 14px; + line-height: 1; + color: var(--mantine-color-dimmed); +} + +.chipLink { + text-decoration: none; + color: inherit; + display: inline-flex; +} + +.chipLink:hover { + filter: brightness(0.97); +} diff --git a/apps/client/src/features/label/queries/label-query.ts b/apps/client/src/features/label/queries/label-query.ts new file mode 100644 index 000000000..6b06c4e30 --- /dev/null +++ b/apps/client/src/features/label/queries/label-query.ts @@ -0,0 +1,157 @@ +import { + keepPreviousData, + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; +import { + addLabelsToPage, + findPagesByLabel, + getLabelInfo, + getPageLabels, + getWorkspaceLabels, + removeLabelFromPage, +} from "@/features/label/services/label-service.ts"; +import { + IAddLabels, + ILabel, + IRemoveLabel, +} from "@/features/label/types/label.types.ts"; +import { IPagination } from "@/lib/types.ts"; +import { notifications } from "@mantine/notifications"; +import { useTranslation } from "react-i18next"; + +const PAGE_LABELS_KEY = (pageId: string) => ["page-labels", pageId]; +const WORKSPACE_LABELS_KEY = (query?: string) => ["workspace-labels", query ?? ""]; + +export function usePageLabelsQuery(pageId: string | undefined) { + return useQuery({ + queryKey: PAGE_LABELS_KEY(pageId ?? ""), + queryFn: () => getPageLabels({ pageId: pageId as string, limit: 100 }), + enabled: !!pageId, + }); +} + +export function useWorkspaceLabelsQuery(query: string, enabled: boolean) { + return useQuery({ + queryKey: WORKSPACE_LABELS_KEY(query), + queryFn: () => getWorkspaceLabels({ type: "page", query, limit: 50 }), + enabled, + staleTime: 30 * 1000, + }); +} + +export function useAddLabelsMutation(pageId: string | undefined) { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: (data) => addLabelsToPage(data), + onSuccess: (added) => { + queryClient.setQueryData>( + PAGE_LABELS_KEY(pageId ?? ""), + (cache) => { + if (!cache) return cache; + const existing = new Set(cache.items.map((l) => l.id)); + const additions = added.filter((l) => !existing.has(l.id)); + if (additions.length === 0) return cache; + return { ...cache, items: [...cache.items, ...additions] }; + }, + ); + + queryClient.setQueriesData>( + { queryKey: ["workspace-labels"] }, + (cache) => { + if (!cache) return cache; + const existing = new Set(cache.items.map((l) => l.id)); + const additions = added.filter((l) => !existing.has(l.id)); + if (additions.length === 0) return cache; + return { + ...cache, + items: [...cache.items, ...additions].sort((a, b) => + a.name.localeCompare(b.name), + ), + }; + }, + ); + + queryClient.invalidateQueries({ queryKey: ["label-pages"] }); + queryClient.invalidateQueries({ queryKey: ["label-info"] }); + }, + onError: (error: any) => { + notifications.show({ + message: error?.response?.data?.message ?? t("Failed to add label"), + color: "red", + }); + }, + }); +} + +export function useRemoveLabelMutation(pageId: string | undefined) { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: (data) => removeLabelFromPage(data), + onSuccess: (_data, variables) => { + const cache = queryClient.getQueryData>( + PAGE_LABELS_KEY(pageId ?? ""), + ); + if (cache) { + queryClient.setQueryData>( + PAGE_LABELS_KEY(pageId ?? ""), + { + ...cache, + items: cache.items.filter((l) => l.id !== variables.labelId), + }, + ); + } + queryClient.invalidateQueries({ queryKey: ["workspace-labels"] }); + queryClient.invalidateQueries({ queryKey: ["label-pages"] }); + queryClient.invalidateQueries({ queryKey: ["label-info"] }); + }, + onError: () => { + notifications.show({ + message: t("Failed to remove label"), + color: "red", + }); + }, + }); +} + +export function useLabelInfoQuery(name: string, spaceId?: string) { + return useQuery({ + queryKey: ["label-info", name, spaceId ?? ""], + queryFn: () => getLabelInfo({ name, type: "page", spaceId }), + enabled: !!name, + placeholderData: keepPreviousData, + }); +} + +const LABEL_PAGES_LIMIT = 25; + +export function useLabelPagesQuery( + name: string, + query: string, + spaceId?: string, +) { + return useInfiniteQuery({ + queryKey: ["label-pages", name, query, spaceId ?? ""], + queryFn: ({ pageParam }) => + findPagesByLabel({ + name, + query, + spaceId, + cursor: pageParam, + limit: LABEL_PAGES_LIMIT, + }), + enabled: !!name, + initialPageParam: undefined as string | undefined, + getNextPageParam: (lastPage) => + lastPage.meta.hasNextPage + ? (lastPage.meta.nextCursor ?? undefined) + : undefined, + placeholderData: keepPreviousData, + }); +} diff --git a/apps/client/src/features/label/services/label-service.ts b/apps/client/src/features/label/services/label-service.ts new file mode 100644 index 000000000..b7180c1ad --- /dev/null +++ b/apps/client/src/features/label/services/label-service.ts @@ -0,0 +1,55 @@ +import api from "@/lib/api-client"; +import { IPagination } from "@/lib/types.ts"; +import { + IAddLabels, + IFindPagesByLabelParams, + ILabel, + ILabelInfo, + ILabelInfoParams, + ILabelPageItem, + IListLabelsParams, + IPageLabelsParams, + IRemoveLabel, +} from "@/features/label/types/label.types.ts"; + +export async function getPageLabels( + params: IPageLabelsParams, +): Promise> { + const req = await api.post>("/pages/labels", params); + return req.data; +} + +export async function getWorkspaceLabels( + params: IListLabelsParams, +): Promise> { + const req = await api.post>("/labels", params); + return req.data; +} + +export async function addLabelsToPage( + data: IAddLabels, +): Promise { + const req = await api.post("/pages/labels/add", data); + return req.data; +} + +export async function removeLabelFromPage(data: IRemoveLabel): Promise { + await api.post("/pages/labels/remove", data); +} + +export async function getLabelInfo( + params: ILabelInfoParams, +): Promise { + const req = await api.post("/labels/info", params); + return req.data; +} + +export async function findPagesByLabel( + params: IFindPagesByLabelParams, +): Promise> { + const req = await api.post>( + "/labels/pages", + params, + ); + return req.data; +} diff --git a/apps/client/src/features/label/types/label.types.ts b/apps/client/src/features/label/types/label.types.ts new file mode 100644 index 000000000..a1152d5d4 --- /dev/null +++ b/apps/client/src/features/label/types/label.types.ts @@ -0,0 +1,70 @@ +import { QueryParams } from "@/lib/types.ts"; + +export type LabelType = "page" | "space"; + +export interface ILabel { + id: string; + name: string; + type: LabelType; + workspaceId: string; + createdAt: string; + updatedAt: string; +} + +export interface IAddLabels { + pageId: string; + names: string[]; +} + +export interface IRemoveLabel { + pageId: string; + labelId: string; +} + +export interface IPageLabelsParams { + pageId: string; + cursor?: string; + limit?: number; +} + +export interface IListLabelsParams { + type: LabelType; + query?: string; + cursor?: string; + limit?: number; +} + +export interface ILabelInfo { + name: string; + usageCount: number; +} + +export interface ILabelPageItem { + id: string; + slugId: string; + title: string | null; + icon: string | null; + spaceId: string; + createdAt: string; + updatedAt: string; + space: { + id: string; + name: string; + slug: string; + logo: string | null; + } | null; + creator: { id: string; name: string; avatarUrl: string | null } | null; + labels: { id: string; name: string }[]; +} + +export interface IFindPagesByLabelParams extends QueryParams { + labelId?: string; + name?: string; + spaceId?: string; +} + +export interface ILabelInfoParams { + name: string; + type: LabelType; + spaceId?: string; +} diff --git a/apps/client/src/features/label/utils/format-label-date.ts b/apps/client/src/features/label/utils/format-label-date.ts new file mode 100644 index 000000000..1221c8ad8 --- /dev/null +++ b/apps/client/src/features/label/utils/format-label-date.ts @@ -0,0 +1,15 @@ +import { format, isThisYear, isToday, isYesterday } from "date-fns"; +import i18n from "@/i18n.ts"; + +export function formatLabelListDate(date: Date): string { + if (isToday(date)) { + return i18n.t("Today, {{time}}", { time: format(date, "h:mma") }); + } + if (isYesterday(date)) { + return i18n.t("Yesterday, {{time}}", { time: format(date, "h:mma") }); + } + if (isThisYear(date)) { + return format(date, "MMM dd"); + } + return format(date, "MMM dd, yyyy"); +} diff --git a/apps/client/src/features/label/utils/label-colors.ts b/apps/client/src/features/label/utils/label-colors.ts new file mode 100644 index 000000000..b9da2858f --- /dev/null +++ b/apps/client/src/features/label/utils/label-colors.ts @@ -0,0 +1,55 @@ +type LabelColor = { + bg: string; + fg: string; + dot: string; +}; + +const LABEL_PALETTE: Record = { + slate: { bg: "#eef1f5", fg: "#3b475a", dot: "#6b7a90" }, + blue: { bg: "#e6f0ff", fg: "#1e4fbf", dot: "#3b82f6" }, + green: { bg: "#e3f5ea", fg: "#1f7a47", dot: "#22a05a" }, + amber: { bg: "#fbf0d9", fg: "#8a5a00", dot: "#d99c1f" }, + red: { bg: "#fde6e6", fg: "#a02b2b", dot: "#dc4a4a" }, + purple: { bg: "#efe9fb", fg: "#5a3aa8", dot: "#8b6bd9" }, + pink: { bg: "#fce6ee", fg: "#a8336d", dot: "#dc6699" }, + teal: { bg: "#daf1ee", fg: "#1f6f6a", dot: "#2fa39a" }, +}; + +const PALETTE_KEYS = Object.keys(LABEL_PALETTE); + +const DARK_PALETTE: Record = { + slate: { bg: "#2a3140", fg: "#c8d3e3", dot: "#7e8da8" }, + blue: { bg: "#152a52", fg: "#a9c4ff", dot: "#5b9aff" }, + green: { bg: "#143b27", fg: "#9ce3b8", dot: "#3ec97c" }, + amber: { bg: "#3d2c0e", fg: "#f5cf85", dot: "#e6b34a" }, + red: { bg: "#401a1a", fg: "#f1a8a8", dot: "#e26565" }, + purple: { bg: "#2a1f4d", fg: "#c8b4f4", dot: "#a48ce6" }, + pink: { bg: "#3c1a2a", fg: "#f3a9c9", dot: "#e07ab0" }, + teal: { bg: "#103633", fg: "#92d5cf", dot: "#48b8af" }, +}; + +function hashName(name: string): number { + // Per-char accumulation with 31. Note: 31 ≡ -1 (mod 8), so the low bits of + // this hash are highly correlated across short strings — `% 8` would cluster. + let h = 0; + for (let i = 0; i < name.length; i++) { + h = (Math.imul(h, 31) + name.charCodeAt(i)) | 0; + } + // Murmur3 fmix32 finalizer — avalanches high bits into low bits so the + // subsequent `% palette.length` (small power of two) is well-distributed. + h ^= h >>> 16; + h = Math.imul(h, 0x85ebca6b); + h ^= h >>> 13; + h = Math.imul(h, 0xc2b2ae35); + h ^= h >>> 16; + return h >>> 0; +} + +export function getLabelColor( + name: string, + scheme: "light" | "dark" = "light", +): LabelColor { + const key = PALETTE_KEYS[hashName(name) % PALETTE_KEYS.length]; + const palette = scheme === "dark" ? DARK_PALETTE : LABEL_PALETTE; + return palette[key]; +} diff --git a/apps/client/src/features/label/utils/normalize-label.ts b/apps/client/src/features/label/utils/normalize-label.ts new file mode 100644 index 000000000..cb170d93b --- /dev/null +++ b/apps/client/src/features/label/utils/normalize-label.ts @@ -0,0 +1,3 @@ +export function normalizeLabelName(name: string): string { + return name.trim().replace(/\s+/g, "-").toLowerCase(); +} diff --git a/apps/client/src/features/notification/components/notification-popover.tsx b/apps/client/src/features/notification/components/notification-popover.tsx index 161ac1e6c..3c5286c48 100644 --- a/apps/client/src/features/notification/components/notification-popover.tsx +++ b/apps/client/src/features/notification/components/notification-popover.tsx @@ -7,7 +7,7 @@ import { Popover, ScrollArea, Tabs, - Text, + Title, Tooltip, } from "@mantine/core"; import { @@ -34,11 +34,14 @@ export function NotificationPopover() { const [opened, setOpened] = useState(false); const [tab, setTab] = useState("direct"); const [filter, setFilter] = useState("all"); + const [filterMenuOpened, setFilterMenuOpened] = useState(false); + const [moreMenuOpened, setMoreMenuOpened] = useState(false); const { data: unreadData } = useUnreadCountQuery(); const markAllRead = useMarkAllReadMutation(); const unreadCount = unreadData?.count ?? 0; + const isSubMenuOpen = filterMenuOpened || moreMenuOpened; const handleMarkAllRead = () => { markAllRead.mutate(); @@ -51,6 +54,9 @@ export function NotificationPopover() { opened={opened} onChange={setOpened} withArrow + trapFocus + returnFocus + closeOnEscape={!isSubMenuOpen} > @@ -58,6 +64,9 @@ export function NotificationPopover() { variant="subtle" color="dark" size="sm" + aria-label={t("Notifications")} + aria-haspopup="dialog" + aria-expanded={opened} onClick={() => setOpened((o) => !o)} > - + {t("Notifications")} - </Text> + - + - + @@ -110,10 +130,21 @@ export function NotificationPopover() { - + - + diff --git a/apps/client/src/features/page-details/components/backlinks-list.tsx b/apps/client/src/features/page-details/components/backlinks-list.tsx new file mode 100644 index 000000000..90547f2ca --- /dev/null +++ b/apps/client/src/features/page-details/components/backlinks-list.tsx @@ -0,0 +1,113 @@ +import { + Button, + Center, + Group, + Loader, + Stack, + Text, + UnstyledButton, +} from "@mantine/core"; +import { Link } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { useBacklinksQuery } from "@/features/page-details/queries/backlinks-query.ts"; +import { + BacklinkDirection, + IBacklinkPageItem, +} from "@/features/page-details/types/backlink.types.ts"; +import { buildPageUrl } from "@/features/page/page.utils.ts"; +import { getPageIcon } from "@/lib"; + +interface BacklinksListProps { + pageId: string; + direction: BacklinkDirection; + enabled: boolean; + onItemClick: () => void; +} + +export function BacklinksList({ + pageId, + direction, + enabled, + onItemClick, +}: BacklinksListProps) { + const { t } = useTranslation(); + const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = + useBacklinksQuery(pageId, direction, enabled); + + if (!enabled) return null; + + if (isLoading) { + return ( +
+ +
+ ); + } + + const items: IBacklinkPageItem[] = + data?.pages.flatMap((page) => page.items) ?? []; + + if (items.length === 0) { + return ( + + {direction === "incoming" + ? t("No pages link here yet.") + : t("This page doesn't link to other pages yet.")} + + ); + } + + const handleClick = (e: React.MouseEvent) => { + if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) { + return; + } + onItemClick(); + }; + + return ( + + {items.map((item) => ( + + + {getPageIcon(item.icon ?? "")} + + + {item.title || t("Untitled")} + + {item.space?.name && ( + + {item.space.name} + + )} + + + + ))} + {hasNextPage && ( + + )} + + ); +} diff --git a/apps/client/src/features/page-details/components/backlinks-modal.tsx b/apps/client/src/features/page-details/components/backlinks-modal.tsx new file mode 100644 index 000000000..1fbd771a3 --- /dev/null +++ b/apps/client/src/features/page-details/components/backlinks-modal.tsx @@ -0,0 +1,62 @@ +import { Modal, Stack, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { useBacklinksCountQuery } from "@/features/page-details/queries/backlinks-query.ts"; +import { BacklinksList } from "./backlinks-list"; + +interface BacklinksModalProps { + pageId: string; + opened: boolean; + onClose: () => void; +} + +export function BacklinksModal({ + pageId, + opened, + onClose, +}: BacklinksModalProps) { + const { t } = useTranslation(); + const { data: counts } = useBacklinksCountQuery(pageId); + + return ( + + + + + {t("Backlinks")} + + + + + + + {t("Incoming links ({{count}})", { + count: counts?.incoming ?? 0, + })} + + + + + + + {t("Outgoing links ({{count}})", { + count: counts?.outgoing ?? 0, + })} + + + + + + + + ); +} diff --git a/apps/client/src/features/page-details/components/page-details-aside.tsx b/apps/client/src/features/page-details/components/page-details-aside.tsx new file mode 100644 index 000000000..84209d7a6 --- /dev/null +++ b/apps/client/src/features/page-details/components/page-details-aside.tsx @@ -0,0 +1,239 @@ +import { + Divider, + Group, + Skeleton, + Stack, + Text, + UnstyledButton, +} from "@mantine/core"; +import { IconChevronRight } from "@tabler/icons-react"; +import { useDisclosure } from "@mantine/hooks"; +import { useAtomValue } from "jotai"; +import { useParams } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { extractPageSlugId } from "@/lib"; +import { usePageQuery } from "@/features/page/queries/page-query.ts"; +import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms.ts"; +import { useBacklinksCountQuery } from "@/features/page-details/queries/backlinks-query.ts"; +import { BacklinksModal } from "./backlinks-modal"; +import { formattedDate, timeAgo } from "@/lib/time.ts"; +import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; +import { LabelsSection } from "@/features/label/components/labels-section.tsx"; + +export function PageDetailsAside() { + const { pageSlug } = useParams(); + const { data: page } = usePageQuery({ + pageId: extractPageSlugId(pageSlug), + }); + const pageEditor = useAtomValue(pageEditorAtom); + const { data: counts, isLoading: countsLoading } = useBacklinksCountQuery(page?.id); + const [modalOpened, { open: openModal, close: closeModal }] = + useDisclosure(false); + + if (!page) return null; + + const wordCount: number = + pageEditor?.storage?.characterCount?.words?.() ?? 0; + const characterCount: number = + pageEditor?.storage?.characterCount?.characters?.() ?? 0; + + return ( + <> + + + + + + + + + + + + + + + + + ); +} + +function PeopleSection({ + creator, + lastUpdatedBy, +}: { + creator: { id: string; name: string; avatarUrl: string } | null; + lastUpdatedBy: { id: string; name: string; avatarUrl: string } | null; +}) { + const { t } = useTranslation(); + return ( + + + + + ); +} + +function PersonRow({ + label, + person, +}: { + label: string; + person: { id: string; name: string; avatarUrl: string } | null; +}) { + return ( + + + {label} + + {person ? ( + + + + {person.name} + + + ) : ( + + — + + )} + + ); +} + +function StatsSection({ + wordCount, + characterCount, + createdAt, + updatedAt, +}: { + wordCount: number; + characterCount: number; + createdAt: Date | string; + updatedAt: Date | string; +}) { + const { t } = useTranslation(); + return ( + + + {t("Stats")} + + + + + + + ); +} + +function StatRow({ label, value }: { label: string; value: string }) { + return ( + + + {label} + + {value} + + ); +} + +function BacklinksSection({ + incomingCount, + outgoingCount, + isLoading, + onClick, +}: { + incomingCount: number; + outgoingCount: number; + isLoading: boolean; + onClick: () => void; +}) { + const { t } = useTranslation(); + return ( + + + {t("Backlinks")} + + + + + ); +} + +function BacklinksRow({ + label, + count, + isLoading, + onClick, +}: { + label: string; + count: number; + isLoading: boolean; + onClick: () => void; +}) { + return ( + + + + {label} + + + {isLoading ? ( + + ) : ( + {count} + )} + + + + + ); +} diff --git a/apps/client/src/features/page-details/queries/backlinks-query.ts b/apps/client/src/features/page-details/queries/backlinks-query.ts new file mode 100644 index 000000000..a5e4619ee --- /dev/null +++ b/apps/client/src/features/page-details/queries/backlinks-query.ts @@ -0,0 +1,45 @@ +import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; +import { + getBacklinks, + getBacklinksCount, +} from "@/features/page-details/services/backlinks-service.ts"; +import { + BacklinkDirection, + IBacklinkCount, +} from "@/features/page-details/types/backlink.types.ts"; + +const BACKLINKS_STALE_TIME = 30 * 1000; +const BACKLINKS_PAGE_LIMIT = 100; + +export function useBacklinksCountQuery(pageId: string | undefined) { + return useQuery({ + queryKey: ["backlinks-count", pageId], + queryFn: () => getBacklinksCount(pageId as string), + enabled: !!pageId, + staleTime: BACKLINKS_STALE_TIME, + }); +} + +export function useBacklinksQuery( + pageId: string | undefined, + direction: BacklinkDirection, + enabled: boolean, +) { + return useInfiniteQuery({ + queryKey: ["backlinks", pageId, direction], + queryFn: ({ pageParam }) => + getBacklinks({ + pageId: pageId as string, + direction, + cursor: pageParam, + limit: BACKLINKS_PAGE_LIMIT, + }), + enabled: enabled && !!pageId, + initialPageParam: undefined as string | undefined, + getNextPageParam: (lastPage) => + lastPage.meta.hasNextPage + ? (lastPage.meta.nextCursor ?? undefined) + : undefined, + staleTime: BACKLINKS_STALE_TIME, + }); +} diff --git a/apps/client/src/features/page-details/services/backlinks-service.ts b/apps/client/src/features/page-details/services/backlinks-service.ts new file mode 100644 index 000000000..779911eec --- /dev/null +++ b/apps/client/src/features/page-details/services/backlinks-service.ts @@ -0,0 +1,26 @@ +import api from "@/lib/api-client"; +import { IPagination } from "@/lib/types.ts"; +import { + IBacklinkCount, + IBacklinkPageItem, + IBacklinksListParams, +} from "@/features/page-details/types/backlink.types.ts"; + +export async function getBacklinksCount( + pageId: string, +): Promise { + const req = await api.post("/pages/backlinks-count", { + pageId, + }); + return req.data; +} + +export async function getBacklinks( + params: IBacklinksListParams, +): Promise> { + const req = await api.post>( + "/pages/backlinks", + params, + ); + return req.data; +} diff --git a/apps/client/src/features/page-details/types/backlink.types.ts b/apps/client/src/features/page-details/types/backlink.types.ts new file mode 100644 index 000000000..f45874e5d --- /dev/null +++ b/apps/client/src/features/page-details/types/backlink.types.ts @@ -0,0 +1,23 @@ +export type BacklinkDirection = "incoming" | "outgoing"; + +export interface IBacklinkCount { + incoming: number; + outgoing: number; +} + +export interface IBacklinkPageItem { + id: string; + slugId: string; + title: string | null; + icon: string | null; + spaceId: string; + space: { id: string; slug: string; name: string } | null; + updatedAt: string; +} + +export interface IBacklinksListParams { + pageId: string; + direction: BacklinkDirection; + cursor?: string; + limit?: number; +} diff --git a/apps/client/src/features/page-history/components/history-modal.tsx b/apps/client/src/features/page-history/components/history-modal.tsx index dfd43cf1f..05768638a 100644 --- a/apps/client/src/features/page-history/components/history-modal.tsx +++ b/apps/client/src/features/page-history/components/history-modal.tsx @@ -22,6 +22,7 @@ export default function HistoryModal({ pageId, pageTitle }: Props) { opened={isModalOpen} onClose={() => setModalOpen(false)} fullScreen + aria-label={t("Page history")} > @@ -31,7 +32,7 @@ export default function HistoryModal({ pageId, pageTitle }: Props) { {t("Page history")} - + setModalOpen(false)} + aria-label={t("Page history")} > @@ -58,7 +60,7 @@ export default function HistoryModal({ pageId, pageTitle }: Props) { {t("Page history")} - + diff --git a/apps/client/src/features/page/components/breadcrumbs/breadcrumb.tsx b/apps/client/src/features/page/components/breadcrumbs/breadcrumb.tsx index 11507e404..d02ba6e91 100644 --- a/apps/client/src/features/page/components/breadcrumbs/breadcrumb.tsx +++ b/apps/client/src/features/page/components/breadcrumbs/breadcrumb.tsx @@ -19,6 +19,7 @@ import { buildPageUrl } from "@/features/page/page.utils.ts"; import { usePageQuery } from "@/features/page/queries/page-query.ts"; import { extractPageSlugId } from "@/lib"; import { useMediaQuery } from "@mantine/hooks"; +import { useTranslation } from "react-i18next"; function getTitle(name: string, icon: string) { if (icon) { @@ -28,6 +29,7 @@ function getTitle(name: string, icon: string) { } export default function Breadcrumb() { + const { t } = useTranslation(); const treeData = useAtomValue(treeDataAtom); const [breadcrumbNodes, setBreadcrumbNodes] = useState< SpaceTreeNode[] | null @@ -80,7 +82,7 @@ export default function Breadcrumb() { )); const renderAnchor = useCallback( - (node: SpaceTreeNode) => ( + (node: SpaceTreeNode, isCurrent = false) => ( {getTitle(node.name, node.icon)} @@ -115,7 +118,11 @@ export default function Breadcrumb() { key="hidden-nodes" > - + @@ -124,11 +131,13 @@ export default function Breadcrumb() { , //renderAnchor(secondLastNode), - renderAnchor(lastNode), + renderAnchor(lastNode, true), ]; } - return breadcrumbNodes.map(renderAnchor); + return breadcrumbNodes.map((node, i) => + renderAnchor(node, i === breadcrumbNodes.length - 1), + ); }; const getMobileBreadcrumbItems = () => { @@ -144,8 +153,12 @@ export default function Breadcrumb() { key="mobile-hidden-nodes" > - - + + @@ -157,16 +170,18 @@ export default function Breadcrumb() { ]; } - return breadcrumbNodes.map(renderAnchor); + return breadcrumbNodes.map((node, i) => + renderAnchor(node, i === breadcrumbNodes.length - 1), + ); }; return ( -
+
+ ); } diff --git a/apps/client/src/features/page/components/copy-page-modal.tsx b/apps/client/src/features/page/components/copy-page-modal.tsx index 4745f731c..b03bd1e5e 100644 --- a/apps/client/src/features/page/components/copy-page-modal.tsx +++ b/apps/client/src/features/page/components/copy-page-modal.tsx @@ -80,7 +80,7 @@ export default function CopyPageModal({ {t("Copy page")} - + diff --git a/apps/client/src/features/page/components/header/page-header-menu.tsx b/apps/client/src/features/page/components/header/page-header-menu.tsx index 9e4b72096..d82cb494c 100644 --- a/apps/client/src/features/page/components/header/page-header-menu.tsx +++ b/apps/client/src/features/page/components/header/page-header-menu.tsx @@ -1,4 +1,4 @@ -import { ActionIcon, Group, Menu, Text, Tooltip } from "@mantine/core"; +import { ActionIcon, Group, Menu, Text, ThemeIcon, Tooltip } from "@mantine/core"; import { IconArrowRight, IconArrowsHorizontal, @@ -18,7 +18,7 @@ import { IconWifiOff, } from "@tabler/icons-react"; import React, { useEffect, useRef, useState } from "react"; -import useToggleAside from "@/hooks/use-toggle-aside.tsx"; +import { useAsideTriggerProps } from "@/hooks/use-toggle-aside.tsx"; import { useAtom, useAtomValue } from "jotai"; import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts"; import { useDisclosure, useHotkeys } from "@mantine/hooks"; @@ -29,7 +29,7 @@ import { buildPageUrl } from "@/features/page/page.utils.ts"; import { notifications } from "@mantine/notifications"; import { getAppUrl } from "@/lib/config.ts"; import { extractPageSlugId } from "@/lib"; -import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts"; +import { useTreeMutation } from "@/features/page/tree/hooks/use-tree-mutation.ts"; import { useDeletePageModal } from "@/features/page/hooks/use-delete-page-modal.tsx"; import { PageWidthToggle } from "@/features/user/components/page-width-pref.tsx"; import { Trans, useTranslation } from "react-i18next"; @@ -40,7 +40,7 @@ import { yjsConnectionStatusAtom, } from "@/features/editor/atoms/editor-atoms.ts"; import { formattedDate } from "@/lib/time.ts"; -import { PageStateSegmentedControl } from "@/features/user/components/page-state-pref.tsx"; +import { PageEditModeToggle } from "@/features/user/components/page-state-pref.tsx"; import MovePageModal from "@/features/page/components/move-page-modal.tsx"; import { useTimeAgo } from "@/hooks/use-time-ago.tsx"; import { PageShareModal } from "@/ee/page-permission"; @@ -64,7 +64,13 @@ interface PageHeaderMenuProps { } export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { const { t } = useTranslation(); - const toggleAside = useToggleAside(); + const commentsTriggerProps = useAsideTriggerProps("comments"); + const tocTriggerProps = useAsideTriggerProps("toc"); + const { pageSlug } = useParams(); + const { data: page } = usePageQuery({ + pageId: extractPageSlugId(pageSlug), + }); + const isDeleted = !!page?.deletedAt; useHotkeys( [ @@ -87,11 +93,15 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { [], ); + if (isDeleted) { + return null; + } + return ( <> - {!readOnly && } + {!readOnly && } @@ -99,7 +109,8 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { toggleAside("comments")} + aria-label={t("Comments")} + {...commentsTriggerProps} > @@ -109,7 +120,8 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) { toggleAside("toc")} + aria-label={t("Table of contents")} + {...tocTriggerProps} > @@ -132,7 +144,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) { pageId: extractPageSlugId(pageSlug), }); const { openDeleteModal } = useDeletePageModal(); - const [tree] = useAtom(treeApiAtom); + const { handleDelete } = useTreeMutation(page?.spaceId ?? ""); const [exportOpened, { open: openExportModal, close: closeExportModal }] = useDisclosure(false); const [ @@ -181,7 +193,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) { }; const handleDeletePage = () => { - openDeleteModal({ onConfirm: () => tree?.delete(page.id) }); + openDeleteModal({ onConfirm: () => handleDelete(page.id) }); }; const handleToggleFavorite = () => { @@ -205,7 +217,11 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) { arrowPosition="center" > - + @@ -416,9 +432,15 @@ function ConnectionWarning() { openDelay={250} withArrow > - + - +
); } diff --git a/apps/client/src/features/page/components/header/page-header.tsx b/apps/client/src/features/page/components/header/page-header.tsx index 12f131b8d..0614cf0bd 100644 --- a/apps/client/src/features/page/components/header/page-header.tsx +++ b/apps/client/src/features/page/components/header/page-header.tsx @@ -8,7 +8,7 @@ interface Props { } export default function PageHeader({ readOnly }: Props) { return ( -
+
diff --git a/apps/client/src/features/page/components/move-page-modal.tsx b/apps/client/src/features/page/components/move-page-modal.tsx index 74880084b..907bea2f2 100644 --- a/apps/client/src/features/page/components/move-page-modal.tsx +++ b/apps/client/src/features/page/components/move-page-modal.tsx @@ -75,7 +75,7 @@ export default function MovePageModal({ {t("Move page")} - + diff --git a/apps/client/src/features/page/components/page-import-modal.tsx b/apps/client/src/features/page/components/page-import-modal.tsx index df6691d55..3283ff794 100644 --- a/apps/client/src/features/page/components/page-import-modal.tsx +++ b/apps/client/src/features/page/components/page-import-modal.tsx @@ -12,6 +12,7 @@ import { IconCheck, IconFileCode, IconFileTypeDocx, + IconFileTypePdf, IconFileTypeZip, IconMarkdown, IconX, @@ -66,7 +67,7 @@ export default function PageImportModal({ {t("Import pages")} - + @@ -90,12 +91,14 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) { const markdownFileRef = useRef<() => void>(null); const htmlFileRef = useRef<() => void>(null); const docxFileRef = useRef<() => void>(null); + const pdfFileRef = useRef<() => void>(null); const notionFileRef = useRef<() => void>(null); const confluenceFileRef = useRef<() => void>(null); const zipFileRef = useRef<() => void>(null); const canUseConfluence = useHasFeature(Feature.CONFLUENCE_IMPORT); const canUseDocx = useHasFeature(Feature.DOCX_IMPORT); + const canUsePdf = useHasFeature(Feature.PDF_IMPORT); const upgradeLabel = useUpgradeLabel(); const handleZipUpload = async (selectedFile: File, source: string) => { @@ -244,7 +247,7 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) { }, 3000); }, [fileTaskId]); - const maxSingleFileSize = bytes("20mb"); + const maxSingleFileSize = bytes("30mb"); const handleFileUpload = async (selectedFiles: File[]) => { if (!selectedFiles) { @@ -298,6 +301,7 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) { if (markdownFileRef.current) markdownFileRef.current(); if (htmlFileRef.current) htmlFileRef.current(); if (docxFileRef.current) docxFileRef.current(); + if (pdfFileRef.current) pdfFileRef.current(); const pageCountText = pageCount === 1 ? `1 ${t("page")}` : `${pageCount} ${t("pages")}`; @@ -328,7 +332,15 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) { return ( <> - + {(props) => ( + + )} + + handleZipUpload(file, "notion")} accept="application/zip" resetRef={notionFileRef} + inputProps={{ + "aria-label": t("Choose {{format}} file", { format: "Notion" }), + }} > {(props) => ( + )} + {canPermanentlyDelete && ( + + )} + + + {canRestore && ( + + + + + + )} + {canPermanentlyDelete && ( + + + + + + )} + + + )} + + + ); +} diff --git a/apps/client/src/features/page/trash/components/trash-banner.tsx b/apps/client/src/features/page/trash/components/trash-banner.tsx new file mode 100644 index 000000000..90ec4a066 --- /dev/null +++ b/apps/client/src/features/page/trash/components/trash-banner.tsx @@ -0,0 +1,21 @@ +import { Alert, Text } from "@mantine/core"; +import { IconInfoCircle } from "@tabler/icons-react"; +import { useAtomValue } from "jotai"; +import { useTranslation } from "react-i18next"; +import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; + +export function TrashBanner() { + const { t } = useTranslation(); + const workspace = useAtomValue(workspaceAtom); + const retentionDays = workspace?.trashRetentionDays ?? 30; + + return ( + } variant="light" color="red"> + + {t("Pages in trash will be permanently deleted after {{count}} days.", { + count: retentionDays, + })} + + + ); +} diff --git a/apps/client/src/features/page/trash/components/trash-page-content-modal.tsx b/apps/client/src/features/page/trash/components/trash-page-content-modal.tsx index ef35b02db..842c4257c 100644 --- a/apps/client/src/features/page/trash/components/trash-page-content-modal.tsx +++ b/apps/client/src/features/page/trash/components/trash-page-content-modal.tsx @@ -19,7 +19,7 @@ export default function TrashPageContentModal({ const title = pageTitle || t("Untitled"); return ( - + @@ -28,7 +28,7 @@ export default function TrashPageContentModal({ {t("Preview")} - + diff --git a/apps/client/src/features/page/trash/components/trash.tsx b/apps/client/src/features/page/trash/components/trash.tsx index ad2ba5002..2b5ca9649 100644 --- a/apps/client/src/features/page/trash/components/trash.tsx +++ b/apps/client/src/features/page/trash/components/trash.tsx @@ -7,17 +7,16 @@ import { Group, ActionIcon, Text, - Alert, Stack, Menu, } from "@mantine/core"; import { - IconInfoCircle, IconDots, IconRestore, IconTrash, IconFileDescription, } from "@tabler/icons-react"; +import { TrashBanner } from "@/features/page/trash/components/trash-banner.tsx"; import { useDeletedPagesQuery, useRestorePageMutation, @@ -31,12 +30,10 @@ import TrashPageContentModal from "@/features/page/trash/components/trash-page-c import { UserInfo } from "@/components/common/user-info.tsx"; import Paginate from "@/components/common/paginate.tsx"; import { useCursorPaginate } from "@/hooks/use-cursor-paginate"; -import { useAtom } from "jotai"; -import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts"; +import { useRestorePageModal } from "@/features/page/hooks/use-restore-page-modal.tsx"; export default function Trash() { const { t } = useTranslation(); - const [workspace] = useAtom(workspaceAtom); const { spaceSlug } = useParams(); const { cursor, goNext, goPrev } = useCursorPaginate(); const { data: space } = useGetSpaceBySlugQuery(spaceSlug); @@ -45,6 +42,7 @@ export default function Trash() { }); const restorePageMutation = useRestorePageMutation(); const deletePageMutation = useDeletePageMutation(); + const { openRestoreModal } = useRestorePageModal(); const [selectedPage, setSelectedPage] = useState<{ title: string; @@ -78,23 +76,6 @@ export default function Trash() { }); }; - const openRestoreModal = (pageId: string, pageTitle: string) => { - modals.openConfirmModal({ - title: t("Restore page"), - children: ( - - {t("Restore '{{title}}' and its sub-pages?", { - title: pageTitle || "Untitled", - })} - - ), - centered: true, - labels: { confirm: t("Restore"), cancel: t("Cancel") }, - confirmProps: { color: "blue" }, - onConfirm: () => handleRestorePage(pageId), - }); - }; - const hasPages = deletedPages && deletedPages.items.length > 0; const handlePageClick = (page: any) => { @@ -109,11 +90,7 @@ export default function Trash() { {t("Trash")} - } variant="light" color="red"> - - {t("Pages in trash will be permanently deleted after {{count}} days.", { count: workspace?.trashRetentionDays ?? 30 })} - - + {isLoading || !deletedPages ? ( <> @@ -129,7 +106,7 @@ export default function Trash() { {t("Deleted at")} - + @@ -173,7 +150,11 @@ export default function Trash() { - + @@ -181,7 +162,10 @@ export default function Trash() { } onClick={() => - openRestoreModal(page.id, page.title) + openRestoreModal({ + title: page.title, + onConfirm: () => handleRestorePage(page.id), + }) } > {t("Restore")} diff --git a/apps/client/src/features/page/tree/atoms/open-tree-nodes-atom.ts b/apps/client/src/features/page/tree/atoms/open-tree-nodes-atom.ts new file mode 100644 index 000000000..3dd2d98bc --- /dev/null +++ b/apps/client/src/features/page/tree/atoms/open-tree-nodes-atom.ts @@ -0,0 +1,5 @@ +import { atom } from "jotai"; + +export type OpenMap = Record; + +export const openTreeNodesAtom = atom({}); diff --git a/apps/client/src/features/page/tree/atoms/tree-api-atom.ts b/apps/client/src/features/page/tree/atoms/tree-api-atom.ts deleted file mode 100644 index f12106f99..000000000 --- a/apps/client/src/features/page/tree/atoms/tree-api-atom.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { atom } from "jotai"; -import { TreeApi } from "react-arborist"; -import { SpaceTreeNode } from "../types"; - -export const treeApiAtom = atom | null>(null); diff --git a/apps/client/src/features/page/tree/components/doc-tree-drag-preview.module.css b/apps/client/src/features/page/tree/components/doc-tree-drag-preview.module.css new file mode 100644 index 000000000..acd0da016 --- /dev/null +++ b/apps/client/src/features/page/tree/components/doc-tree-drag-preview.module.css @@ -0,0 +1,26 @@ +.preview { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + background-color: light-dark( + var(--mantine-color-white), + var(--mantine-color-dark-6) + ); + color: light-dark( + var(--mantine-color-gray-9), + var(--mantine-color-dark-0) + ); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.18); + border: 1px solid light-dark( + var(--mantine-color-gray-3), + var(--mantine-color-dark-4) + ); + max-width: 260px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/apps/client/src/features/page/tree/components/doc-tree-drag-preview.tsx b/apps/client/src/features/page/tree/components/doc-tree-drag-preview.tsx new file mode 100644 index 000000000..f8a8a88b1 --- /dev/null +++ b/apps/client/src/features/page/tree/components/doc-tree-drag-preview.tsx @@ -0,0 +1,9 @@ +import styles from './doc-tree-drag-preview.module.css'; + +type Props = { + label: string; +}; + +export function DocTreeDragPreview({ label }: Props) { + return
{label || 'Untitled'}
; +} diff --git a/apps/client/src/features/page/tree/components/doc-tree-drop-indicator.tsx b/apps/client/src/features/page/tree/components/doc-tree-drop-indicator.tsx new file mode 100644 index 000000000..9d6352ab5 --- /dev/null +++ b/apps/client/src/features/page/tree/components/doc-tree-drop-indicator.tsx @@ -0,0 +1,39 @@ +import type { Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item'; +import styles from '../styles/tree.module.css'; + +type Props = { + instruction: Instruction; + indentPx: number; +}; + +export function DocTreeDropIndicator({ instruction, indentPx }: Props) { + const blocked = instruction.type === 'instruction-blocked'; + const inst = blocked ? instruction.desired : instruction; + + const style = { + ['--drop-line-indent' as never]: `${indentPx}px`, + } as React.CSSProperties; + + if (inst.type === 'reorder-above') { + return ( +
+ ); + } + if (inst.type === 'reorder-below') { + return ( +
+ ); + } + // 'combine' (make-child) is rendered via [data-receiving-drop] on the row itself. + return null; +} diff --git a/apps/client/src/features/page/tree/components/doc-tree-row.tsx b/apps/client/src/features/page/tree/components/doc-tree-row.tsx new file mode 100644 index 000000000..347f1f3e7 --- /dev/null +++ b/apps/client/src/features/page/tree/components/doc-tree-row.tsx @@ -0,0 +1,398 @@ +import { + memo, + useCallback, + useEffect, + useRef, + useState, + type ReactNode, +} from 'react'; +import { createRoot } from 'react-dom/client'; +import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; +import { + draggable, + dropTargetForElements, +} from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; +import { pointerOutsideOfPreview } from '@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview'; +import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview'; +import { + attachInstruction, + extractInstruction, + type Instruction, + type ItemMode, +} from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item'; +import { triggerPostMoveFlash } from '@atlaskit/pragmatic-drag-and-drop-flourish/trigger-post-move-flash'; +import * as liveRegion from '@atlaskit/pragmatic-drag-and-drop-live-region'; + +import type { TreeNode, DropOp } from '../model/tree-model.types'; +import { treeModel } from '../model/tree-model'; +import { DocTreeDropIndicator } from './doc-tree-drop-indicator'; +import { DocTreeDragPreview } from './doc-tree-drag-preview'; +import type { RenderRowProps } from './doc-tree'; +import styles from '../styles/tree.module.css'; + +type Props = { + node: TreeNode; + level: number; + isLastSibling: boolean; + openIds: ReadonlySet; + selectedId?: string; + // Roving tabindex: the single row that currently carries tabIndex={0}. + activeId?: string; + renderRow: (props: RenderRowProps) => ReactNode; + indentPerLevel: number; + onMove: (sourceId: string, op: DropOp) => void | Promise; + onToggle: (id: string, isOpen: boolean) => void; + readOnly: boolean; + disableDrag?: (node: TreeNode) => boolean; + disableDrop?: (node: TreeNode) => boolean; + getDragLabel: (node: TreeNode) => string; + contextId: symbol; + registerRowElement: (id: string, el: HTMLElement | null) => void; + // Stable accessor — calling it returns the latest tree. Avoids passing the + // tree itself as a prop (which would break memo and re-run every row's DnD + // useEffect on every mutation). + getRootData: () => TreeNode[]; +}; + +const DRAG_TYPE = 'doc-tree-item'; +const AUTO_EXPAND_MS = 500; + +function DocTreeRowInner(props: Props) { + const { + node, + level, + isLastSibling, + openIds, + selectedId, + activeId, + renderRow, + indentPerLevel, + onMove, + onToggle, + readOnly, + disableDrag, + disableDrop, + getDragLabel, + contextId, + registerRowElement, + getRootData, + } = props; + + const isOpen = openIds.has(node.id); + // "Has children" includes both already-loaded children AND the consumer's + // own server-side flag (`hasChildren` is a docmost convention on + // SpaceTreeNode / SharedPageTreeNode). The flag lets the chevron and the + // auto-expand timer recognize unloaded subtrees so the consumer's lazy-load + // (via onToggle) can populate them on demand. + const hasLoadedChildren = !!node.children && node.children.length > 0; + const declaredHasChildren = + (node as { hasChildren?: boolean }).hasChildren === true; + const hasChildren = hasLoadedChildren || declaredHasChildren; + const isSelected = selectedId === node.id; + + const rowRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const [instruction, setInstruction] = useState(null); + const autoExpandTimerRef = useRef | null>(null); + + const cancelAutoExpand = useCallback(() => { + if (autoExpandTimerRef.current) { + clearTimeout(autoExpandTimerRef.current); + autoExpandTimerRef.current = null; + } + }, []); + + const toggleOpen = useCallback(() => { + onToggle(node.id, !isOpen); + }, [onToggle, node.id, isOpen]); + + useEffect(() => { + registerRowElement(node.id, rowRef.current); + return () => registerRowElement(node.id, null); + }, [registerRowElement, node.id]); + + // Restore lazy-loaded children when the row mounts open but its children + // aren't loaded (e.g. cross-space page move drops a node into a new tree + // that still has its id in openIds). Calling onToggle(id, true) is + // idempotent for open state and triggers the consumer's lazy-load. + useEffect(() => { + if (isOpen && declaredHasChildren && !hasLoadedChildren) { + onToggle(node.id, true); + } + }, [isOpen, declaredHasChildren, hasLoadedChildren, node.id, onToggle]); + + useEffect(() => { + const el = rowRef.current; + if (!el || readOnly) return; + const dragDisabled = disableDrag?.(node) ?? false; + const dropDisabled = disableDrop?.(node) ?? false; + + const cleanups: Array<() => void> = []; + + if (!dragDisabled) { + cleanups.push( + draggable({ + element: el, + getInitialData: () => ({ + id: node.id, + type: DRAG_TYPE, + uniqueContextId: contextId, + isOpenOnDragStart: isOpen, + }), + onGenerateDragPreview: ({ nativeSetDragImage }) => { + setCustomNativeDragPreview({ + nativeSetDragImage, + getOffset: pointerOutsideOfPreview({ x: '16px', y: '8px' }), + render: ({ container }) => { + const root = createRoot(container); + root.render(); + return () => root.unmount(); + }, + }); + }, + onDragStart: () => setIsDragging(true), + onDrop: () => setIsDragging(false), + }), + ); + } + + if (!dropDisabled) { + const mode: ItemMode = + isOpen && hasChildren + ? 'expanded' + : isLastSibling + ? 'last-in-group' + : 'standard'; + // Always block 'reparent' (out of scope per spec). + // Block 'reorder-below' when the row is open with children — ambiguous gesture, + // force users to drop into the folder via 'make-child' instead. + const block: Instruction['type'][] = ['reparent']; + if (isOpen && hasChildren) block.push('reorder-below'); + + cleanups.push( + dropTargetForElements({ + element: el, + canDrop: ({ source }) => + source.data.type === DRAG_TYPE && + source.data.uniqueContextId === contextId && + source.data.id !== node.id && + !treeModel.isDescendant( + getRootData(), + source.data.id as string, + node.id, + ), + getData: ({ input, element }) => + attachInstruction( + { id: node.id, type: DRAG_TYPE }, + { + input, + element, + currentLevel: level, + indentPerLevel, + mode, + block, + }, + ), + onDrag: ({ self }) => { + const inst = extractInstruction(self.data); + setInstruction(inst); + // Auto-expand on hover over any collapsed row that has children, + // regardless of the specific instruction type. Reorder-before and + // reorder-after also benefit: once expanded, the user can see the + // children and refine their drop target. + if ( + inst && + hasChildren && + !isOpen && + !autoExpandTimerRef.current + ) { + autoExpandTimerRef.current = setTimeout(() => { + onToggle(node.id, true); + autoExpandTimerRef.current = null; + }, AUTO_EXPAND_MS); + } + }, + onDragLeave: () => { + setInstruction(null); + cancelAutoExpand(); + }, + onDrop: ({ source, self }) => { + setInstruction(null); + cancelAutoExpand(); + const inst = extractInstruction(self.data); + if (!inst || inst.type === 'instruction-blocked') return; + const sourceId = source.data.id as string; + const op: DropOp = + inst.type === 'reorder-above' + ? { kind: 'reorder-before', targetId: node.id } + : inst.type === 'reorder-below' + ? { kind: 'reorder-after', targetId: node.id } + : inst.type === 'make-child' + ? { kind: 'make-child', targetId: node.id } + : null!; + if (!op) return; + onMove(sourceId, op); + triggerPostMoveFlash(el); + const liveTree = getRootData(); + const parentName = + op.kind === 'make-child' + ? getDragLabel(node) + : (() => { + const sib = treeModel.siblingsOf(liveTree, op.targetId); + const parent = sib?.parentId + ? treeModel.find(liveTree, sib.parentId) + : null; + return parent ? getDragLabel(parent) : 'root'; + })(); + const sourceNode = treeModel.find(liveTree, sourceId); + const sourceLabel = sourceNode + ? getDragLabel(sourceNode) + : 'item'; + liveRegion.announce(`Moved ${sourceLabel} under ${parentName}.`); + // After a make-child drop, expand this row so the user sees the + // just-dropped child — especially important when the row had no + // children before (chevron just appeared) so the drop would + // otherwise be invisible. + if (op.kind === 'make-child') onToggle(node.id, true); + if (source.data.isOpenOnDragStart) onToggle(sourceId, true); + }, + }), + ); + } + + return combine(...cleanups); + }, [ + node, + level, + isOpen, + hasChildren, + isLastSibling, + readOnly, + disableDrag, + disableDrop, + contextId, + indentPerLevel, + getDragLabel, + onMove, + onToggle, + getRootData, + cancelAutoExpand, + ]); + + useEffect(() => () => cancelAutoExpand(), [cancelAutoExpand]); + + const effectiveInst = + instruction?.type === 'instruction-blocked' + ? instruction.desired + : instruction; + const blocked = instruction?.type === 'instruction-blocked'; + const receivingDrop: 'before' | 'after' | 'make-child' | null = (() => { + if (!effectiveInst) return null; + if (effectiveInst.type === 'reorder-above') return 'before'; + if (effectiveInst.type === 'reorder-below') return 'after'; + if (effectiveInst.type === 'make-child') return 'make-child'; + return null; + })(); + + // Treeitem semantics ride on the row's focusable element (the consumer's + // ). The outer
  • is presentational layout. aria-label uses the row's + // label so the SR's accessible name is just the page title, not the + // concatenation of inner action-button aria-labels. + const treeItemProps = { + role: 'treeitem' as const, + 'aria-level': level + 1, + 'aria-expanded': hasChildren ? isOpen : undefined, + 'aria-selected': isSelected ? (true as const) : undefined, + 'aria-current': isSelected ? ('page' as const) : undefined, + 'aria-label': getDragLabel(node), + 'data-row-id': node.id, + }; + + return ( +
    +
    + {renderRow({ + node, + level, + isOpen, + hasChildren, + isSelected, + isDragging, + isReceivingDrop: receivingDrop, + rowRef, + tabIndex: activeId === node.id ? 0 : -1, + treeItemProps, + toggleOpen, + })} +
    + {instruction && ( + + )} +
    + ); +} + +// Custom memo comparator. The default shallow compare re-renders every row +// when `openIds` (a Set) or `selectedId` (a string) on the parent changes, +// because all rows receive the same reference via {...props} spread. With 1K +// rows that's a perceptible stall on every expand and every navigate. +// +// Resolve openIds / selectedId per-row: only re-render if THIS row's own +// open-state or selected-state actually flipped. Everything else uses +// reference equality (callbacks are useCallback-stable from the parent). +function arePropsEqual( + prev: Props, + next: Props, +): boolean { + if (prev.node !== next.node) return false; + if (prev.level !== next.level) return false; + if (prev.isLastSibling !== next.isLastSibling) return false; + if (prev.readOnly !== next.readOnly) return false; + if (prev.contextId !== next.contextId) return false; + if (prev.indentPerLevel !== next.indentPerLevel) return false; + if (prev.renderRow !== next.renderRow) return false; + if (prev.onMove !== next.onMove) return false; + if (prev.onToggle !== next.onToggle) return false; + if (prev.disableDrag !== next.disableDrag) return false; + if (prev.disableDrop !== next.disableDrop) return false; + if (prev.getDragLabel !== next.getDragLabel) return false; + if (prev.registerRowElement !== next.registerRowElement) return false; + if (prev.getRootData !== next.getRootData) return false; + + const id = next.node.id; + // openIds: only this row's own membership matters. + if (prev.openIds.has(id) !== next.openIds.has(id)) return false; + // selectedId: re-render only the rows whose isSelected actually flipped. + const wasSelected = prev.selectedId === id; + const isSelected = next.selectedId === id; + if (wasSelected !== isSelected) return false; + // activeId: same trick — only the outgoing and incoming active rows + // re-render when the user moves focus through the tree. + const wasActive = prev.activeId === id; + const isActive = next.activeId === id; + if (wasActive !== isActive) return false; + + return true; +} + +export const DocTreeRow = memo( + DocTreeRowInner, + arePropsEqual, +) as typeof DocTreeRowInner; diff --git a/apps/client/src/features/page/tree/components/doc-tree.tsx b/apps/client/src/features/page/tree/components/doc-tree.tsx new file mode 100644 index 000000000..3dbbfda16 --- /dev/null +++ b/apps/client/src/features/page/tree/components/doc-tree.tsx @@ -0,0 +1,541 @@ +import { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, + type ReactNode, + type Ref, +} from 'react'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element'; +import type { TreeNode, DropOp } from '../model/tree-model.types'; +import { treeModel } from '../model/tree-model'; +import { DocTreeRow } from './doc-tree-row'; +import styles from '../styles/tree.module.css'; + +export type RenderRowProps = { + node: TreeNode; + level: number; + isOpen: boolean; + hasChildren: boolean; + isSelected: boolean; + isDragging: boolean; + isReceivingDrop: 'before' | 'after' | 'make-child' | null; + + rowRef: Ref; + // Roving tabindex: exactly one row in the tree carries tabIndex={0} (the + // active row); every other row gets tabIndex={-1}. Consumers must spread + // this onto the same element they wire rowRef to. + tabIndex: 0 | -1; + // Treeitem semantics for the row's focusable element. Consumers MUST spread + // these onto the same element rowRef points at, so the focused element IS + // the treeitem. This makes screen readers announce "treeitem" (not "link") + // and replaces the descendant-text accname with the row's label, so action + // button labels inside the row don't get concatenated. + treeItemProps: { + role: 'treeitem'; + 'aria-level': number; + 'aria-expanded'?: boolean; + 'aria-selected'?: true; + 'aria-current'?: 'page'; + 'aria-label': string; + 'data-row-id': string; + }; + toggleOpen: () => void; +}; + +export type DocTreeProps = { + data: TreeNode[]; + openIds: ReadonlySet; + selectedId?: string; + + renderRow: (props: RenderRowProps) => ReactNode; + indentPerLevel?: number; + rowHeight?: number; + emptyState?: ReactNode; + + onMove: (sourceId: string, op: DropOp) => void | Promise; + onToggle: (id: string, isOpen: boolean) => void; + onSelect?: (id: string) => void; + + readOnly?: boolean; + disableDrag?: (node: TreeNode) => boolean; + disableDrop?: (node: TreeNode) => boolean; + + getDragLabel: (node: TreeNode) => string; + uniqueContextId?: symbol; + + // Accessible name for the tree itself (e.g. "Pages"). Rendered as + // aria-label on the
  • + + ( !option.disabled && contentType !== option.value && @@ -283,7 +202,7 @@ export function SearchSpotlightFilters({ )}
    - {contentType === option.value && } + {contentType === option.value && } ))} diff --git a/apps/client/src/features/search/components/search-spotlight.tsx b/apps/client/src/features/search/components/search-spotlight.tsx index 5a2980a38..4c5269f15 100644 --- a/apps/client/src/features/search/components/search-spotlight.tsx +++ b/apps/client/src/features/search/components/search-spotlight.tsx @@ -1,6 +1,6 @@ import { Spotlight } from "@mantine/spotlight"; import { IconSearch, IconSparkles } from "@tabler/icons-react"; -import { Group, Button } from "@mantine/core"; +import { Group, Button, VisuallyHidden } from "@mantine/core"; import React, { useState, useMemo, useEffect } from "react"; import { useDebouncedValue } from "@mantine/hooks"; import { useTranslation } from "react-i18next"; @@ -126,6 +126,7 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) { } style={{ flex: 1 }} onKeyDown={(e) => { @@ -161,6 +162,18 @@ export function SearchSpotlight({ spaceId }: SearchSpotlightProps) { />
    + + {isAiMode + ? query.length > 0 && !isAiLoading && !aiSearchResult + ? t("No answer available") + : "" + : query.length > 0 && !isLoading + ? resultItems.length === 0 + ? t("No results found") + : t("{{count}} results found", { count: resultItems.length }) + : ""} + + {isAiMode ? ( <> diff --git a/apps/client/src/features/search/components/share-search-spotlight.tsx b/apps/client/src/features/search/components/share-search-spotlight.tsx index dd0d5181f..eccaeb4db 100644 --- a/apps/client/src/features/search/components/share-search-spotlight.tsx +++ b/apps/client/src/features/search/components/share-search-spotlight.tsx @@ -74,6 +74,7 @@ export function ShareSearchSpotlight({ shareId }: ShareSearchSpotlightProps) { > } /> diff --git a/apps/client/src/features/session/components/session-list.tsx b/apps/client/src/features/session/components/session-list.tsx index 18a304586..9e519942c 100644 --- a/apps/client/src/features/session/components/session-list.tsx +++ b/apps/client/src/features/session/components/session-list.tsx @@ -7,6 +7,7 @@ import { Stack, Table, Text, + VisuallyHidden, } from "@mantine/core"; import { IconDevices } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; @@ -33,11 +34,16 @@ export default function SessionList() { if (isLoading) { return (
    + + {t("Active sessions")} + {t("Device Name")} {t("Last Active")} - + + {t("Action")} + @@ -90,11 +96,18 @@ export default function SessionList() { )}
    + + {t("Active sessions")} + {t("Device Name")} {t("Last Active")} - {otherSessions.length > 0 && } + {otherSessions.length > 0 && ( + + {t("Action")} + + )} diff --git a/apps/client/src/features/share/atoms/open-shared-tree-nodes-atom.ts b/apps/client/src/features/share/atoms/open-shared-tree-nodes-atom.ts new file mode 100644 index 000000000..47882e5e9 --- /dev/null +++ b/apps/client/src/features/share/atoms/open-shared-tree-nodes-atom.ts @@ -0,0 +1,3 @@ +import { atom } from "jotai"; + +export const openSharedTreeNodesAtom = atom>({}); diff --git a/apps/client/src/features/share/atoms/shared-page-atom.ts b/apps/client/src/features/share/atoms/shared-page-atom.ts index 813f5e61b..bf8929942 100644 --- a/apps/client/src/features/share/atoms/shared-page-atom.ts +++ b/apps/client/src/features/share/atoms/shared-page-atom.ts @@ -1,6 +1,11 @@ import { atom } from "jotai"; +import { atomWithStorage } from "jotai/utils"; import { ISharedPageTree } from "@/features/share/types/share.types"; import { SharedPageTreeNode } from "@/features/share/utils"; export const sharedPageTreeAtom = atom(null); -export const sharedTreeDataAtom = atom(null); \ No newline at end of file +export const sharedTreeDataAtom = atom(null); +export const sharedPageFullWidthAtom = atomWithStorage( + "sharedPageFullWidth", + false, +); \ No newline at end of file diff --git a/apps/client/src/features/share/components/share-action-menu.tsx b/apps/client/src/features/share/components/share-action-menu.tsx index 52dad0dab..059951289 100644 --- a/apps/client/src/features/share/components/share-action-menu.tsx +++ b/apps/client/src/features/share/components/share-action-menu.tsx @@ -75,7 +75,7 @@ export default function ShareActionMenu({ share }: Props) { arrowPosition="center" > - + diff --git a/apps/client/src/features/share/components/share-list.tsx b/apps/client/src/features/share/components/share-list.tsx index 147c8bb90..37ea4abf6 100644 --- a/apps/client/src/features/share/components/share-list.tsx +++ b/apps/client/src/features/share/components/share-list.tsx @@ -14,6 +14,7 @@ import { getPageIcon } from "@/lib"; import { CustomAvatar } from "@/components/ui/custom-avatar.tsx"; import { EmptyState } from "@/components/ui/empty-state.tsx"; import classes from "./share.module.css"; +import rowClasses from "@/components/ui/clickable-table-row.module.css"; export default function ShareList() { const { t } = useTranslation(); @@ -38,7 +39,7 @@ export default function ShareList() { {data?.items.map((share: ISharedItem, index: number) => ( - + (null); + + const startResizing = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + setIsResizing(true); + }, []); + + const stopResizing = useCallback(() => { + setIsResizing(false); + }, []); + + const resize = useCallback( + (e: MouseEvent) => { + if (!isResizing || !sidebarRef.current) return; + const newWidth = + e.clientX - sidebarRef.current.getBoundingClientRect().left; + if (newWidth < 220) { + setSidebarWidth(220); + return; + } + if (newWidth > 600) { + setSidebarWidth(600); + return; + } + setSidebarWidth(newWidth); + }, + [isResizing, setSidebarWidth], + ); + + useEffect(() => { + window.addEventListener("mousemove", resize); + window.addEventListener("mouseup", stopResizing); + return () => { + window.removeEventListener("mousemove", resize); + window.removeEventListener("mouseup", stopResizing); + }; + }, [resize, stopResizing]); const { shareId } = useParams(); const { data } = useGetSharedPageTreeQuery(shareId); @@ -77,11 +123,13 @@ export default function ShareShell({ }, [data, treeData, setSharedPageTree, setSharedTreeData]); return ( - + + 1 && { navbar: { - width: 300, + width: sidebarWidth, breakpoint: "sm", collapsed: { mobile: !mobileOpened, @@ -148,6 +196,7 @@ export default function ShareShell({ onClick={toggleTocMobile} hiddenFrom="sm" size="sm" + aria-label={t("Table of contents")} > @@ -157,6 +206,7 @@ export default function ShareShell({ + + + setFullWidth((v) => !v)} + visibleFrom="sm" + size="sm" + > + + + @@ -172,12 +236,16 @@ export default function ShareShell({ {data?.pageTree?.length > 1 && ( - + +
    )} - + {children} {data && shareId && !(data.features?.length > 0) && } @@ -199,5 +267,6 @@ export default function ShareShell({ + ); } diff --git a/apps/client/src/features/share/components/share.module.css b/apps/client/src/features/share/components/share.module.css index 617768ff1..ebf1e74cb 100644 --- a/apps/client/src/features/share/components/share.module.css +++ b/apps/client/src/features/share/components/share.module.css @@ -10,6 +10,7 @@ .treeNode { text-decoration: none; user-select: none; + padding-bottom: 0; } .navbar, @@ -18,3 +19,26 @@ width: 350px; } } + +.resizeHandle { + width: 3px; + cursor: col-resize; + position: absolute; + right: 0; + top: 0; + bottom: 0; + z-index: 1; + + &:hover, + &:active { + width: 5px; + background: light-dark( + var(--mantine-color-gray-4), + var(--mantine-color-dark-5) + ); + } + + @media (max-width: $mantine-breakpoint-sm) { + display: none; + } +} diff --git a/apps/client/src/features/share/components/shared-tree.tsx b/apps/client/src/features/share/components/shared-tree.tsx index 486127b98..370c59e70 100644 --- a/apps/client/src/features/share/components/shared-tree.tsx +++ b/apps/client/src/features/share/components/shared-tree.tsx @@ -1,14 +1,11 @@ import { ISharedPageTree } from "@/features/share/types/share.types.ts"; -import { NodeApi, NodeRendererProps, Tree, TreeApi } from "react-arborist"; import { buildSharedPageTree, SharedPageTreeNode, } from "@/features/share/utils.ts"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { useElementSize, useMergedRef } from "@mantine/hooks"; -import { SpaceTreeNode } from "@/features/page/tree/types.ts"; +import { useCallback, useEffect, useMemo, useRef } from "react"; import { Link, useParams } from "react-router-dom"; -import { atom, useAtom } from "jotai/index"; +import { useAtom } from "jotai"; import { useTranslation } from "react-i18next"; import { buildSharedPageUrl } from "@/features/page/page.utils.ts"; import clsx from "clsx"; @@ -20,176 +17,204 @@ import { } from "@tabler/icons-react"; import { ActionIcon, Box } from "@mantine/core"; import { extractPageSlugId } from "@/lib"; -import { OpenMap } from "react-arborist/dist/main/state/open-slice"; import classes from "@/features/page/tree/styles/tree.module.css"; import styles from "./share.module.css"; import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts"; import EmojiPicker from "@/components/ui/emoji-picker.tsx"; +import { + DocTree, + type DocTreeApi, + type RenderRowProps, +} from "@/features/page/tree/components/doc-tree"; +import { openSharedTreeNodesAtom } from "@/features/share/atoms/open-shared-tree-nodes-atom"; -interface SharedTree { +interface SharedTreeProps { sharedPageTree: ISharedPageTree; } -const openSharedTreeNodesAtom = atom({}); - -export default function SharedTree({ sharedPageTree }: SharedTree) { - const [tree, setTree] = useState< - TreeApi | null | undefined - >(null); - const rootElement = useRef(); - const { ref: sizeRef, width, height } = useElementSize(); - const mergedRef = useMergedRef(rootElement, sizeRef); +export default function SharedTree({ sharedPageTree }: SharedTreeProps) { + const { t } = useTranslation(); + const treeRef = useRef(null); const { pageSlug } = useParams(); - const [openTreeNodes, setOpenTreeNodes] = useAtom( - openSharedTreeNodesAtom, - ); + const [openTreeNodes, setOpenTreeNodes] = useAtom(openSharedTreeNodesAtom); const currentNodeId = extractPageSlugId(pageSlug); const treeData: SharedPageTreeNode[] = useMemo(() => { - if (!sharedPageTree?.pageTree) return; + if (!sharedPageTree?.pageTree) return [] as SharedPageTreeNode[]; return buildSharedPageTree(sharedPageTree.pageTree); }, [sharedPageTree?.pageTree]); - useEffect(() => { - const parentNodeId = treeData?.[0]?.slugId; - - if (parentNodeId && tree) { - const parentNode = tree.get(parentNodeId); - - setTimeout(() => { - if (parentNode) { - tree.openSiblings(parentNode); - } - }); - - // open direct children of parent node - parentNode?.children.forEach((node) => { - tree.openSiblings(node); - }); - } - }, [treeData, tree]); + const openIds = useMemo( + () => + new Set( + Object.keys(openTreeNodes).filter((k) => openTreeNodes[k]), + ), + [openTreeNodes], + ); useEffect(() => { - if (currentNodeId && tree) { - setTimeout(() => { - // focus on node and open all parents - tree?.select(currentNodeId, { align: "auto" }); - }, 200); - } else { - tree?.deselectAll(); + // Auto-open the first level of the shared tree on initial load. + const root = treeData?.[0]; + if (!root) return; + setOpenTreeNodes((prev) => { + if (prev[root.slugId]) return prev; + const next = { ...prev, [root.slugId]: true }; + for (const child of root.children ?? []) { + next[child.slugId] = true; + } + return next; + }); + }, [treeData, setOpenTreeNodes]); + + useEffect(() => { + if (currentNodeId) { + treeRef.current?.select(currentNodeId, { scrollIntoView: true }); } - }, [currentNodeId, tree]); + }, [currentNodeId, treeData]); + + // Stable callbacks so memo(DocTreeRow) actually saves work — see I2 in the + // post-implementation code review. + const handleToggle = useCallback( + (id: string, isOpen: boolean) => + setOpenTreeNodes((prev) => ({ ...prev, [id]: isOpen })), + [setOpenTreeNodes], + ); + const getDragLabel = useCallback( + (n: SharedPageTreeNode) => n.name || "untitled", + [], + ); if (!sharedPageTree || !sharedPageTree?.pageTree) { return null; } return ( -
    - {rootElement.current && ( - setTree(t)} - openByDefault={false} - disableMultiSelection={true} - className={classes.tree} - rowClassName={classes.row} - rowHeight={30} - overscanCount={10} - dndRootElement={rootElement.current} - onToggle={() => { - setOpenTreeNodes(tree?.openState); - }} - initialOpenState={openTreeNodes} - onClick={(e) => { - if (tree && tree.focusedNode) { - tree.select(tree.focusedNode); - } - }} - > - {Node} - - )} +
    + + readOnly + ref={treeRef} + data={treeData} + openIds={openIds} + selectedId={currentNodeId} + renderRow={SharedTreeRow} + onMove={noopMove} + onToggle={handleToggle} + getDragLabel={getDragLabel} + aria-label={t("Pages")} + />
    ); } -function Node({ node, style, tree }: NodeRendererProps) { +// Module-scope noop so it's a stable reference across renders. +const noopMove = () => {}; + +function SharedTreeRow({ + node, + isOpen, + hasChildren, + isSelected, + rowRef, + tabIndex, + treeItemProps, + toggleOpen, +}: RenderRowProps) { const { shareId } = useParams(); const { t } = useTranslation(); const [, setMobileSidebarState] = useAtom(mobileSidebarAtom); const pageUrl = buildSharedPageUrl({ shareId: shareId, - pageSlugId: node.data.slugId, - pageTitle: node.data.name, + pageSlugId: node.slugId, + pageTitle: node.name, }); return ( - <> - { - setMobileSidebarState(false); - }} - > - -
    - {}} - icon={ - node.data.icon ? ( - node.data.icon - ) : ( - - ) - } - readOnly={true} - removeEmojiAction={() => {}} - /> -
    - {node.data.name || t("untitled")} -
    - + } + tabIndex={tabIndex} + {...treeItemProps} + data-selected={isSelected || undefined} + className={clsx(classes.node, styles.treeNode)} + component={Link} + to={pageUrl} + onClick={() => { + setMobileSidebarState(false); + }} + > + +
    + {}} + icon={ + node.icon ? ( + node.icon + ) : ( + + ) + } + readOnly={true} + removeEmojiAction={() => {}} + actionIconProps={{ tabIndex: -1 }} + /> +
    + {node.name || t("untitled")} +
    ); } -interface PageArrowProps { - node: NodeApi; +interface SharedPageArrowProps { + isOpen: boolean; + hasChildren: boolean; + onToggle: () => void; } -function PageArrow({ node }: PageArrowProps) { +function SharedPageArrow({ + isOpen, + hasChildren, + onToggle, +}: SharedPageArrowProps) { + if (!hasChildren) { + return ( + + + + ); + } + return ( { e.preventDefault(); e.stopPropagation(); - node.toggle(); + onToggle(); }} > - {node.isInternal ? ( - node.children && (node.children.length > 0 || node.data.hasChildren) ? ( - node.isOpen ? ( - - ) : ( - - ) - ) : ( - - ) - ) : null} + {isOpen ? ( + + ) : ( + + )} ); } diff --git a/apps/client/src/features/space/components/add-space-members-modal.tsx b/apps/client/src/features/space/components/add-space-members-modal.tsx index 5efd32f6b..79910ae3a 100644 --- a/apps/client/src/features/space/components/add-space-members-modal.tsx +++ b/apps/client/src/features/space/components/add-space-members-modal.tsx @@ -1,6 +1,6 @@ import { Button, Divider, Group, Modal, Stack } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; -import React, { useState } from "react"; +import React, { useId, useState } from "react"; import { useAddSpaceMemberMutation } from "@/features/space/queries/space-query.ts"; import { MultiMemberSelect } from "@/features/space/components/multi-member-select.tsx"; import { SpaceMemberRole } from "@/features/space/components/space-member-role.tsx"; @@ -14,6 +14,7 @@ export default function AddSpaceMembersModal({ spaceId, }: AddSpaceMemberModalProps) { const { t } = useTranslation(); + const titleId = useId(); const [opened, { open, close }] = useDisclosure(false); const [memberIds, setMemberIds] = useState([]); const [role, setRole] = useState(SpaceRole.WRITER); @@ -51,24 +52,33 @@ export default function AddSpaceMembersModal({ return ( <> - - + + + + + {t("Add space members")} + + + + - - - - + + + + - - - - + + + + + + ); } diff --git a/apps/client/src/features/space/components/create-space-form.tsx b/apps/client/src/features/space/components/create-space-form.tsx index 896b599b3..d5d26d57b 100644 --- a/apps/client/src/features/space/components/create-space-form.tsx +++ b/apps/client/src/features/space/components/create-space-form.tsx @@ -1,4 +1,4 @@ -import { Group, Box, Button, TextInput, Stack, Textarea } from "@mantine/core"; +import { Group, Box, Button, TextInput, Stack, Textarea, Text } from "@mantine/core"; import React, { useEffect } from "react"; import { useForm } from "@mantine/form"; import { zod4Resolver } from "mantine-form-zod-resolver"; @@ -69,10 +69,25 @@ export function CreateSpaceForm() { navigate(getSpaceUrl(createdSpace.slug)); }; + function handleValidationFailure(errors: Record) { + const firstInvalidId = Object.keys(errors)[0]; + if (firstInvalidId) { + document.getElementById(firstInvalidId)?.focus(); + } + } + return ( <> - handleSubmit(values))}> + handleSubmit(values), + handleValidationFailure, + )} + > + + {t("* indicates required fields")} + @@ -89,6 +106,7 @@ export function CreateSpaceForm() { label={t("Space slug")} placeholder={t("e.g product")} variant="filled" + errorProps={{ role: "alert" }} {...form.getInputProps("slug")} /> @@ -100,6 +118,7 @@ export function CreateSpaceForm() { autosize minRows={2} maxRows={8} + errorProps={{ role: "alert" }} {...form.getInputProps("description")} /> diff --git a/apps/client/src/features/space/components/create-space-modal.tsx b/apps/client/src/features/space/components/create-space-modal.tsx index 152e1950c..22f3e40c3 100644 --- a/apps/client/src/features/space/components/create-space-modal.tsx +++ b/apps/client/src/features/space/components/create-space-modal.tsx @@ -11,7 +11,12 @@ export default function CreateSpaceModal() { <> - + diff --git a/apps/client/src/features/space/components/settings-modal.tsx b/apps/client/src/features/space/components/settings-modal.tsx index fb8d24f63..3d24d100c 100644 --- a/apps/client/src/features/space/components/settings-modal.tsx +++ b/apps/client/src/features/space/components/settings-modal.tsx @@ -48,7 +48,7 @@ export default function SpaceSettingsModal({ {space?.name} - +
    diff --git a/apps/client/src/features/space/components/sidebar/space-sidebar.tsx b/apps/client/src/features/space/components/sidebar/space-sidebar.tsx index 0ad0094e5..f5c6ea0e7 100644 --- a/apps/client/src/features/space/components/sidebar/space-sidebar.tsx +++ b/apps/client/src/features/space/components/sidebar/space-sidebar.tsx @@ -18,6 +18,7 @@ import { IconSettings, IconStar, IconStarFilled, + IconTemplate, IconTrash, } from "@tabler/icons-react"; import { @@ -28,7 +29,7 @@ import { import classes from "./space-sidebar.module.css"; import React from "react"; import { useAtom } from "jotai"; -import { treeApiAtom } from "@/features/page/tree/atoms/tree-api-atom.ts"; +import { useTreeMutation } from "@/features/page/tree/hooks/use-tree-mutation.ts"; import { Link, useLocation, useParams } from "react-router-dom"; import clsx from "clsx"; import { useDisclosure } from "@mantine/hooks"; @@ -53,10 +54,14 @@ import { import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts"; import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts"; import { searchSpotlight } from "@/features/search/constants"; +import TemplatePickerModal from "@/ee/template/components/template-picker-modal"; +import { useHasFeature } from "@/ee/hooks/use-feature"; +import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label"; +import { Feature } from "@/ee/features"; +import { ErrorBoundary } from "react-error-boundary"; export function SpaceSidebar() { const { t } = useTranslation(); - const [tree] = useAtom(treeApiAtom); const location = useLocation(); const [opened, { open: openSettings, close: closeSettings }] = useDisclosure(false); @@ -68,13 +73,14 @@ export function SpaceSidebar() { const spaceRules = space?.membership?.permissions; const spaceAbility = useSpaceAbility(spaceRules); + const { handleCreate } = useTreeMutation(space?.id ?? ""); if (!space) { return <>; } function handleCreatePage() { - tree?.create({ parentId: null, type: "internal", index: 0 }); + handleCreate(null); } return ( @@ -246,6 +252,12 @@ function SpaceMenu({ useDisclosure(false); const [exportOpened, { open: openExportModal, close: closeExportModal }] = useDisclosure(false); + const [ + templatePickerOpened, + { open: openTemplatePicker, close: closeTemplatePicker }, + ] = useDisclosure(false); + const hasTemplates = useHasFeature(Feature.TEMPLATES); + const upgradeLabel = useUpgradeLabel(); const { data: watchStatus } = useSpaceWatchStatusQuery(spaceId); const watchMutation = useWatchSpaceMutation(); @@ -315,6 +327,27 @@ function SpaceMenu({ {isWatching ? t("Stop watching space") : t("Watch space")} + {canManagePages && ( + <> + + + } + data-disabled={!hasTemplates || undefined} + aria-disabled={!hasTemplates || undefined} + > + {t("Templates")} + + + + )} + {canManagePages && ( <> @@ -370,6 +403,16 @@ function SpaceMenu({ /> )} + + {hasTemplates && templatePickerOpened && ( + null}> + + + )} ); } diff --git a/apps/client/src/features/space/components/sidebar/switch-space.tsx b/apps/client/src/features/space/components/sidebar/switch-space.tsx index 89e0f64ee..7531a5926 100644 --- a/apps/client/src/features/space/components/sidebar/switch-space.tsx +++ b/apps/client/src/features/space/components/sidebar/switch-space.tsx @@ -38,6 +38,8 @@ export function SwitchSpace({ shadow="md" opened={opened} onChange={toggle} + trapFocus + returnFocus >
    + + + {t("List of spaces in this workspace")} + + {t("Space")} {t("Members")} - + + {t("Action")} + {spaces.length > 0 ? ( spaces.map((space) => ( - + prefetchSpace(space.slug, space.id)} > - + - + diff --git a/apps/client/src/features/space/components/spaces-page/favorite-spaces-grid.tsx b/apps/client/src/features/space/components/spaces-page/favorite-spaces-grid.tsx index 131de2995..a2a2ec263 100644 --- a/apps/client/src/features/space/components/spaces-page/favorite-spaces-grid.tsx +++ b/apps/client/src/features/space/components/spaces-page/favorite-spaces-grid.tsx @@ -1,4 +1,4 @@ -import { Text, SimpleGrid, Card, rem, Group, Box, Button } from "@mantine/core"; +import { Text, SimpleGrid, Card, rem, Group, Box, Button, Title } from "@mantine/core"; import { useState } from "react"; import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; @@ -30,9 +30,9 @@ export default function FavoriteSpacesGrid() { return ( - + {t("Favorite spaces")} - </Text> + {visibleSpaces.map((fav) => ( @@ -53,6 +53,7 @@ export default function FavoriteSpacesGrid() { diff --git a/apps/client/src/features/transclusion/components/sync-block-references-dropdown.module.css b/apps/client/src/features/transclusion/components/sync-block-references-dropdown.module.css new file mode 100644 index 000000000..a39da374b --- /dev/null +++ b/apps/client/src/features/transclusion/components/sync-block-references-dropdown.module.css @@ -0,0 +1,205 @@ +.trigger { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 6px 3px 6px; + border-radius: 999px; + background: transparent; + border: 0; + cursor: pointer; + font: inherit; + font-size: var(--mantine-font-size-xs); + color: light-dark( + var(--mantine-color-gray-7), + var(--mantine-color-dark-1) + ); + transition: background 120ms ease; + user-select: none; +} + +.trigger:hover { + background: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-dark-5) + ); +} + +.triggerIcon { + color: light-dark( + var(--mantine-color-gray-5), + var(--mantine-color-dark-2) + ); + display: inline-flex; +} + +.triggerChev { + color: light-dark( + var(--mantine-color-gray-5), + var(--mantine-color-dark-2) + ); + display: inline-flex; + margin-left: 2px; +} + +.dropdown { + padding: 0; +} + +.banner { + display: flex; + gap: 10px; + padding: 10px 14px; + background: light-dark( + var(--mantine-color-gray-0), + var(--mantine-color-dark-6) + ); + border-bottom: 1px solid + light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4)); + color: light-dark( + var(--mantine-color-gray-7), + var(--mantine-color-dark-1) + ); + font-size: var(--mantine-font-size-xs); + line-height: 1.5; +} + +.bannerIcon { + color: light-dark( + var(--mantine-color-gray-6), + var(--mantine-color-dark-2) + ); + flex: none; + display: inline-flex; + margin-top: 1px; +} + +.bannerLink { + color: light-dark( + var(--mantine-color-gray-9), + var(--mantine-color-dark-0) + ); + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-thickness: 1px; + font-weight: 500; +} + +.bannerLink:hover { + text-decoration-thickness: 2px; +} + +.section { + padding: 12px; +} + +.sectionLabel { + font-size: var(--mantine-font-size-xs); + font-weight: 500; + color: light-dark( + var(--mantine-color-gray-6), + var(--mantine-color-dark-2) + ); + margin: 0 0 6px; + padding: 0 4px; +} + +.list { + list-style: none; + margin: 0; + padding: 0; +} + +.row { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 8px; + border-radius: 6px; + cursor: pointer; + color: light-dark( + var(--mantine-color-gray-9), + var(--mantine-color-dark-0) + ); + font-size: var(--mantine-font-size-sm); + text-decoration: none; + transition: background 100ms ease; +} + +.row:hover { + background: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-dark-5) + ); +} + +.rowIcon { + color: light-dark( + var(--mantine-color-gray-5), + var(--mantine-color-dark-2) + ); + flex: none; + display: inline-flex; +} + +.rowEmoji { + font-size: 14px; + line-height: 1; + flex: none; + display: inline-flex; + width: 18px; + justify-content: center; +} + +.rowTitle { + font-weight: 500; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.badge { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.05em; + padding: 2px 7px; + border-radius: 999px; + background: light-dark( + var(--mantine-color-gray-1), + var(--mantine-color-dark-5) + ); + color: light-dark( + var(--mantine-color-gray-7), + var(--mantine-color-dark-1) + ); + text-transform: uppercase; + flex: none; +} + +.badgeAccent { + background: light-dark( + var(--mantine-color-blue-0), + var(--mantine-color-blue-9) + ); + color: light-dark( + var(--mantine-color-blue-7), + var(--mantine-color-blue-2) + ); +} + +.empty { + padding: 18px 14px; + text-align: center; + color: light-dark( + var(--mantine-color-gray-6), + var(--mantine-color-dark-2) + ); + font-size: var(--mantine-font-size-xs); +} + +.loading { + display: flex; + justify-content: center; + padding: 18px; +} diff --git a/apps/client/src/features/transclusion/components/sync-block-references-dropdown.tsx b/apps/client/src/features/transclusion/components/sync-block-references-dropdown.tsx new file mode 100644 index 000000000..966282a8c --- /dev/null +++ b/apps/client/src/features/transclusion/components/sync-block-references-dropdown.tsx @@ -0,0 +1,184 @@ +import { useState } from "react"; +import { Loader, Popover } from "@mantine/core"; +import { + IconChevronDown, + IconCornerDownLeft, + IconFile, + IconInfoCircle, +} from "@tabler/icons-react"; +import { Link } from "react-router-dom"; +import { Trans, useTranslation } from "react-i18next"; +import { useReferencesQuery } from "@/features/transclusion/queries/transclusion-query"; +import type { ReferencingPage } from "@/features/transclusion/types/transclusion.types"; +import { buildPageUrl } from "@/features/page/page.utils"; +import classes from "./sync-block-references-dropdown.module.css"; + +type Props = { + sourcePageId: string | null; + transclusionId: string | null; + /** The page currently being viewed - used to mark the "THIS PAGE" badge. */ + currentPageId: string; + /** + * Source: trigger reads "Editing original". + * Reference: trigger reads "Synced to N other pages". + */ + mode: "source" | "reference"; + /** Notified whenever the dropdown opens/closes (for keep-chrome-visible). */ + onOpenChange?: (open: boolean) => void; +}; + +export default function SyncBlockReferencesDropdown({ + sourcePageId, + transclusionId, + currentPageId, + mode, + onOpenChange, +}: Props) { + const { t } = useTranslation(); + const [opened, setOpened] = useState(false); + + const handleOpenChange = (next: boolean) => { + setOpened(next); + onOpenChange?.(next); + }; + + // Fetch eagerly so the "Synced to N other pages" count is correct even + // before the dropdown is opened. The cache is keyed on (sourcePageId, + // transclusionId), so two views (source + reference) share one fetch. + const enabled = !!sourcePageId && !!transclusionId; + const { data, isLoading } = useReferencesQuery( + sourcePageId, + transclusionId, + enabled, + ); + + const allPages: Array<{ page: ReferencingPage; isOriginal: boolean }> = []; + if (data?.source) { + allPages.push({ page: data.source, isOriginal: true }); + } + for (const ref of data?.references ?? []) { + allPages.push({ page: ref, isOriginal: false }); + } + + const otherCount = allPages.filter((p) => p.page.id !== currentPageId).length; + const label = + mode === "source" + ? t("Editing original") + : t("Synced to {{count}} other page", { + count: otherCount, + defaultValue_one: "Synced to {{count}} other page", + defaultValue_other: "Synced to {{count}} other pages", + }); + + return ( + + + + + + + {mode === "reference" && data?.source && ( +
    + + + +
    + handleOpenChange(false)} + /> + ), + }} + /> +
    +
    + )} + + {isLoading ? ( +
    + +
    + ) : allPages.length === 0 ? ( +
    {t("No pages")}
    + ) : ( +
    +
    {t("Synced to")}
    +
      + {allPages.map(({ page, isOriginal }) => { + const isCurrent = page.id === currentPageId; + const href = page.spaceSlug + ? buildPageUrl(page.spaceSlug, page.slugId, page.title) + : `/p/${page.id}`; + const title = page.title?.length ? page.title : t("Untitled"); + return ( +
    • + handleOpenChange(false)} + > + {page.icon ? ( + {page.icon} + ) : ( + + + + )} + + {title} + + {isCurrent ? ( + + {t("THIS PAGE")} + + ) : isOriginal ? ( + {t("ORIGINAL")} + ) : null} + +
    • + ); + })} +
    +
    + )} +
    +
    + ); +} diff --git a/apps/client/src/features/transclusion/queries/transclusion-query.ts b/apps/client/src/features/transclusion/queries/transclusion-query.ts new file mode 100644 index 000000000..5cee0ad27 --- /dev/null +++ b/apps/client/src/features/transclusion/queries/transclusion-query.ts @@ -0,0 +1,32 @@ +import { useMutation, useQuery } from "@tanstack/react-query"; +import { + listReferences, + unsyncReference, +} from "../services/transclusion-api"; + +export function useReferencesQuery( + sourcePageId: string | null, + transclusionId: string | null, + enabled: boolean, +) { + return useQuery({ + queryKey: ["transclusion-references", sourcePageId, transclusionId], + queryFn: () => + listReferences({ + sourcePageId: sourcePageId!, + transclusionId: transclusionId!, + }), + enabled: enabled && !!sourcePageId && !!transclusionId, + staleTime: 10 * 1000, + }); +} + +export function useUnsyncReferenceMutation() { + return useMutation({ + mutationFn: (params: { + referencePageId: string; + sourcePageId: string; + transclusionId: string; + }) => unsyncReference(params), + }); +} diff --git a/apps/client/src/features/transclusion/services/transclusion-api.ts b/apps/client/src/features/transclusion/services/transclusion-api.ts new file mode 100644 index 000000000..9aa695221 --- /dev/null +++ b/apps/client/src/features/transclusion/services/transclusion-api.ts @@ -0,0 +1,37 @@ +import api from "@/lib/api-client"; +import type { + ReferencingPagesResponse, + TransclusionLookup, +} from "../types/transclusion.types"; + +export async function lookupTransclusion(params: { + references: Array<{ sourcePageId: string; transclusionId: string }>; +}): Promise<{ items: TransclusionLookup[] }> { + const r = await api.post("/pages/transclusion/lookup", params); + return r.data; +} + +export async function lookupTransclusionForShare(params: { + shareId: string; + references: Array<{ sourcePageId: string; transclusionId: string }>; +}): Promise<{ items: TransclusionLookup[] }> { + const r = await api.post("/shares/transclusion/lookup", params); + return r.data; +} + +export async function listReferences(params: { + sourcePageId: string; + transclusionId: string; +}): Promise { + const r = await api.post("/pages/transclusion/references", params); + return r.data; +} + +export async function unsyncReference(params: { + referencePageId: string; + sourcePageId: string; + transclusionId: string; +}): Promise<{ content: unknown }> { + const r = await api.post("/pages/transclusion/unsync-reference", params); + return r.data; +} diff --git a/apps/client/src/features/transclusion/types/transclusion.types.ts b/apps/client/src/features/transclusion/types/transclusion.types.ts new file mode 100644 index 000000000..3be6968c9 --- /dev/null +++ b/apps/client/src/features/transclusion/types/transclusion.types.ts @@ -0,0 +1,23 @@ +export type TransclusionLookup = + | { + sourcePageId: string; + transclusionId: string; + content: unknown; + sourceUpdatedAt: string; + } + | { sourcePageId: string; transclusionId: string; status: "not_found" } + | { sourcePageId: string; transclusionId: string; status: "no_access" }; + +export type ReferencingPage = { + id: string; + slugId: string; + title: string | null; + icon: string | null; + spaceId: string; + spaceSlug: string | null; +}; + +export type ReferencingPagesResponse = { + source: ReferencingPage | null; + references: ReferencingPage[]; +}; diff --git a/apps/client/src/features/user/components/change-email.tsx b/apps/client/src/features/user/components/change-email.tsx index 16f74b427..8c841b401 100644 --- a/apps/client/src/features/user/components/change-email.tsx +++ b/apps/client/src/features/user/components/change-email.tsx @@ -36,7 +36,13 @@ export default function ChangeEmail() { */} - + {t( "To change your email, you have to enter your password and new email.", @@ -80,6 +86,11 @@ function ChangeEmailForm() { placeholder={t("Enter your password")} variant="filled" mb="md" + visibilityToggleButtonProps={{ + "aria-label": t("Toggle password visibility"), + "aria-hidden": false, + tabIndex: 0, + }} {...form.getInputProps("password")} /> diff --git a/apps/client/src/features/user/components/change-password.tsx b/apps/client/src/features/user/components/change-password.tsx index 0925cdc48..9c0b4cb3d 100644 --- a/apps/client/src/features/user/components/change-password.tsx +++ b/apps/client/src/features/user/components/change-password.tsx @@ -95,6 +95,11 @@ function ChangePasswordForm({ onClose }: ChangePasswordFormProps) { variant="filled" mb="md" data-autofocus + visibilityToggleButtonProps={{ + "aria-label": t("Toggle password visibility"), + "aria-hidden": false, + tabIndex: 0, + }} {...form.getInputProps("oldPassword")} /> @@ -103,6 +108,11 @@ function ChangePasswordForm({ onClose }: ChangePasswordFormProps) { placeholder={t("Enter your new password")} variant="filled" mb="md" + visibilityToggleButtonProps={{ + "aria-label": t("Toggle password visibility"), + "aria-hidden": false, + tabIndex: 0, + }} {...form.getInputProps("newPassword")} /> diff --git a/apps/client/src/features/user/components/fixed-toolbar-pref.tsx b/apps/client/src/features/user/components/fixed-toolbar-pref.tsx new file mode 100644 index 000000000..05b28a7d6 --- /dev/null +++ b/apps/client/src/features/user/components/fixed-toolbar-pref.tsx @@ -0,0 +1,57 @@ +import { userAtom } from "@/features/user/atoms/current-user-atom"; +import { updateUser } from "@/features/user/services/user-service"; +import { Badge, Group, Switch, Text } from "@mantine/core"; +import { useAtom } from "jotai"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + ResponsiveSettingsRow, + ResponsiveSettingsContent, + ResponsiveSettingsControl, +} from "@/components/ui/responsive-settings-row"; + +export default function FixedToolbarPref() { + const { t } = useTranslation(); + const [user, setUser] = useAtom(userAtom); + const [checked, setChecked] = useState( + user.settings?.preferences?.editorToolbar ?? false, + ); + + const handleChange = async (event: React.ChangeEvent) => { + const value = event.currentTarget.checked; + setChecked(value); + try { + const updatedUser = await updateUser({ editorToolbar: value }); + setUser(updatedUser); + } catch { + setChecked(!value); + } + }; + + return ( + + + + {t("Fixed editor toolbar")} + + {t("Experimental")} + + + + {t( + "Show a formatting toolbar above the editor with quick access to common actions.", + )} + + + + + + + + ); +} diff --git a/apps/client/src/features/user/components/notification-pref.tsx b/apps/client/src/features/user/components/notification-pref.tsx index e8a983ede..cce519f91 100644 --- a/apps/client/src/features/user/components/notification-pref.tsx +++ b/apps/client/src/features/user/components/notification-pref.tsx @@ -3,7 +3,7 @@ import { updateUser } from "@/features/user/services/user-service.ts"; import { IUser, IUserSettings } from "@/features/user/types/user.types.ts"; import { Switch, Text, Title, Stack } from "@mantine/core"; import { useAtom } from "jotai"; -import React, { useState } from "react"; +import React, { useId, useState } from "react"; import { useTranslation } from "react-i18next"; import { ResponsiveSettingsRow, @@ -64,6 +64,8 @@ function NotificationToggle({ description: string; }) { const { t } = useTranslation(); + const switchId = useId(); + const descriptionId = useId(); const [user, setUser] = useAtom(userAtom); const [checked, setChecked] = useState( user.settings?.notifications?.[settingKey] !== false, @@ -83,14 +85,21 @@ function NotificationToggle({ return ( - {t(label)} - + + {t(label)} + + {t(description)} - + ); @@ -101,7 +110,7 @@ export default function NotificationPref() { return ( - {t("Email notifications")} + {t("Email notifications")} {notificationItems.map((item) => ( ); } + +// Header variant: updates the current page's mode locally without persisting +// the preference to the server. +export function PageEditModeToggle({ size }: { size?: MantineSize }) { + const { t } = useTranslation(); + const [currentPageEditMode, setCurrentPageEditMode] = useAtom( + currentPageEditModeAtom, + ); + + return ( + setCurrentPageEditMode(v as PageEditMode)} + data={[ + { label: t("Edit"), value: PageEditMode.Edit }, + { label: t("Read"), value: PageEditMode.Read }, + ]} + /> + ); +} diff --git a/apps/client/src/features/user/types/user.types.ts b/apps/client/src/features/user/types/user.types.ts index 75d45bfd9..f51546360 100644 --- a/apps/client/src/features/user/types/user.types.ts +++ b/apps/client/src/features/user/types/user.types.ts @@ -20,6 +20,7 @@ export interface IUser { deletedAt: Date; fullPageWidth: boolean; // used for update pageEditMode: string; // used for update + editorToolbar: boolean; // used for update notificationPageUpdates: boolean; // used for update notificationPageUserMention: boolean; // used for update notificationCommentUserMention: boolean; // used for update @@ -37,6 +38,7 @@ export interface IUserSettings { preferences: { fullPageWidth: boolean; pageEditMode: string; + editorToolbar: boolean; }; notifications?: { "page.updated"?: boolean; diff --git a/apps/client/src/features/user/user-provider.tsx b/apps/client/src/features/user/user-provider.tsx index 4f640e135..870a2d4f7 100644 --- a/apps/client/src/features/user/user-provider.tsx +++ b/apps/client/src/features/user/user-provider.tsx @@ -60,6 +60,10 @@ export function UserProvider({ children }: React.PropsWithChildren) { } }, [data, isLoading]); + useEffect(() => { + document.documentElement.lang = i18n.resolvedLanguage || i18n.language || "en-US"; + }, [i18n.language, i18n.resolvedLanguage]); + useEffect(() => { if (entitlements) { setEntitlements(entitlements); diff --git a/apps/client/src/features/websocket/use-tree-socket.ts b/apps/client/src/features/websocket/use-tree-socket.ts index df160ec8c..c4cd083b4 100644 --- a/apps/client/src/features/websocket/use-tree-socket.ts +++ b/apps/client/src/features/websocket/use-tree-socket.ts @@ -1,37 +1,27 @@ -import { useEffect, useRef } from "react"; +import { useEffect } from "react"; import { socketAtom } from "@/features/websocket/atoms/socket-atom.ts"; import { useAtom } from "jotai"; import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom.ts"; import { WebSocketEvent } from "@/features/websocket/types"; import { SpaceTreeNode } from "@/features/page/tree/types.ts"; import { useQueryClient } from "@tanstack/react-query"; -import { SimpleTree } from "react-arborist"; +import { treeModel } from "@/features/page/tree/model/tree-model"; import localEmitter from "@/lib/local-emitter.ts"; export const useTreeSocket = () => { const [socket] = useAtom(socketAtom); - const [treeData, setTreeData] = useAtom(treeDataAtom); + const [, setTreeData] = useAtom(treeDataAtom); const queryClient = useQueryClient(); - const initialTreeData = useRef(treeData); - - useEffect(() => { - initialTreeData.current = treeData; - }, [treeData]); useEffect(() => { const updateNodeName = (event) => { - const initialData = initialTreeData.current; - const treeApi = new SimpleTree(initialData); - - if (treeApi.find(event?.id)) { - if (event.payload?.title !== undefined) { - treeApi.update({ - id: event.id, - changes: { name: event.payload.title }, - }); - setTreeData(treeApi.data); - } - } + if (event.payload?.title === undefined) return; + setTreeData((prev) => { + if (!treeModel.find(prev, event?.id)) return prev; + return treeModel.update(prev, event.id, { + name: event.payload.title, + } as Partial); + }); }; localEmitter.on("message", updateNodeName); @@ -42,70 +32,110 @@ export const useTreeSocket = () => { useEffect(() => { socket?.on("message", (event: WebSocketEvent) => { - const initialData = initialTreeData.current; - const treeApi = new SimpleTree(initialData); - switch (event.operation) { case "updateOne": if (event.entity[0] === "pages") { - if (treeApi.find(event.id)) { + setTreeData((prev) => { + if (!treeModel.find(prev, event.id)) return prev; + let next = prev; if (event.payload?.title !== undefined) { - treeApi.update({ - id: event.id, - changes: { name: event.payload.title }, - }); + next = treeModel.update(next, event.id, { + name: event.payload.title, + } as Partial); } if (event.payload?.icon !== undefined) { - treeApi.update({ - id: event.id, - changes: { icon: event.payload.icon }, - }); + next = treeModel.update(next, event.id, { + icon: event.payload.icon, + } as Partial); } - setTreeData(treeApi.data); - } + return next; + }); } break; case "addTreeNode": - if (treeApi.find(event.payload.data.id)) return; - - treeApi.create({ - parentId: event.payload.parentId, - index: event.payload.index, - data: event.payload.data, + setTreeData((prev) => { + if (treeModel.find(prev, event.payload.data.id)) return prev; + const newParentId = event.payload.parentId as string | null; + let next = treeModel.insert( + prev, + newParentId, + event.payload.data, + event.payload.index, + ); + // Mirror the emitter: flip new parent's hasChildren to true so + // the chevron renders on the receiver. + if (newParentId) { + next = treeModel.update(next, newParentId, { + hasChildren: true, + } as Partial); + } + return next; }); - setTreeData(treeApi.data); - break; case "moveTreeNode": - // move node - if (treeApi.find(event.payload.id)) { - treeApi.move({ - id: event.payload.id, - parentId: event.payload.parentId, + setTreeData((prev) => { + const sourceBefore = treeModel.find(prev, event.payload.id); + if (!sourceBefore) return prev; + const oldParentId = + (sourceBefore as SpaceTreeNode).parentPageId ?? null; + const newParentId = event.payload.parentId as string | null; + + const placed = treeModel.place(prev, event.payload.id, { + parentId: newParentId, index: event.payload.index, }); + // `place` silently returns the same reference if the destination + // parent isn't loaded on this client. Falling back to removing the + // source keeps the UI consistent (the source will reappear when + // the user expands the new parent and lazy-load fetches it). + if (placed === prev) { + return treeModel.remove(prev, event.payload.id); + } - // update node position - treeApi.update({ - id: event.payload.id, - changes: { - position: event.payload.position, - }, - }); + let next = treeModel.update(placed, event.payload.id, { + position: event.payload.position, + parentPageId: newParentId, + } as Partial); - setTreeData(treeApi.data); - } + // Mirror the emitter's hasChildren bookkeeping so both clients + // converge to the same chevron state. + if (oldParentId) { + const oldParent = treeModel.find(next, oldParentId); + if (!oldParent?.children?.length) { + next = treeModel.update(next, oldParentId, { + hasChildren: false, + } as Partial); + } + } + if (newParentId) { + next = treeModel.update(next, newParentId, { + hasChildren: true, + } as Partial); + } + return next; + }); break; case "deleteTreeNode": - if (treeApi.find(event.payload.node.id)) { - treeApi.drop({ id: event.payload.node.id }); - setTreeData(treeApi.data); - + setTreeData((prev) => { + if (!treeModel.find(prev, event.payload.node.id)) return prev; queryClient.invalidateQueries({ queryKey: ["pages", event.payload.node.slugId].filter(Boolean), }); - } + let next = treeModel.remove(prev, event.payload.node.id); + // Mirror the emitter's hasChildren bookkeeping so both clients + // converge to the same chevron state when the last child is deleted. + const parentPageId = event.payload.node.parentPageId; + if (parentPageId) { + const parent = treeModel.find(next, parentPageId); + if (!parent?.children?.length) { + next = treeModel.update(next, parentPageId, { + hasChildren: false, + } as Partial); + } + } + return next; + }); break; } }); diff --git a/apps/client/src/features/workspace/components/members/components/invite-action-menu.tsx b/apps/client/src/features/workspace/components/members/components/invite-action-menu.tsx index 5569f8e89..e87e95ee9 100644 --- a/apps/client/src/features/workspace/components/members/components/invite-action-menu.tsx +++ b/apps/client/src/features/workspace/components/members/components/invite-action-menu.tsx @@ -73,7 +73,11 @@ export default function InviteActionMenu({ invitationId }: Props) { arrowPosition="center" > - + diff --git a/apps/client/src/features/workspace/components/members/components/members-action-menu.tsx b/apps/client/src/features/workspace/components/members/components/members-action-menu.tsx index e9376ddad..ce1c588a0 100644 --- a/apps/client/src/features/workspace/components/members/components/members-action-menu.tsx +++ b/apps/client/src/features/workspace/components/members/components/members-action-menu.tsx @@ -83,7 +83,11 @@ export default function MemberActionMenu({ userId, deactivatedAt }: Props) { arrowPosition="center" > - + diff --git a/apps/client/src/features/workspace/components/members/components/workspace-invite-form.tsx b/apps/client/src/features/workspace/components/members/components/workspace-invite-form.tsx index aa9d3d90a..8deddc7d5 100644 --- a/apps/client/src/features/workspace/components/members/components/workspace-invite-form.tsx +++ b/apps/client/src/features/workspace/components/members/components/workspace-invite-form.tsx @@ -56,6 +56,12 @@ export function WorkspaceInviteForm({ onClose }: Props) { maxDropdownHeight={200} maxTags={50} onChange={setEmails} + data-autofocus + autoComplete="off" + data-1p-ignore + data-lpignore="true" + data-bwignore + data-form-type="other" />
    → first-row cells' own width style. + * Returns an array of length = number of columns, with null entries + * for columns whose width couldn't be determined. + */ +function deriveColumnWidths( + $: CheerioAPI, + table: Cheerio, +): (number | null)[] | null { + const cols = table.find('> colgroup > col'); + if (cols.length > 0) { + const widths: (number | null)[] = []; + cols.each(function () { + widths.push(parsePixelWidth($(this))); + }); + if (widths.some((w) => w !== null)) return widths; + } + + // Fallback: first row's cells. + const firstRow = table.find('> tbody > tr, > thead > tr, > tr').first(); + if (!firstRow.length) return null; + + const widths: (number | null)[] = []; + firstRow.children('td, th').each(function () { + const cell = $(this); + const colspan = parseInt(cell.attr('colspan') || '1', 10) || 1; + const w = parsePixelWidth(cell); + for (let i = 0; i < colspan; i++) { + widths.push(w !== null ? Math.round(w / colspan) : null); + } + }); + if (widths.every((w) => w === null)) return null; + return widths; +} + +/** + * Apply colwidth attributes to the first row of each table based on + * derived column widths. Accounts for colspan. Idempotent — re-running + * on already-normalized markup is a no-op. + * + * This lives upstream of tiptap's generateJSON: tiptap reads + * `colwidth="N[,N...]"` on . + */ +export function normalizeTableColumnWidths( + $: CheerioAPI, + $root: Cheerio, +): void { + $root.find('table').each(function () { + const table = $(this); + const firstRow = table.find('> tbody > tr, > thead > tr, > tr').first(); + if (!firstRow.length) return; + + let colWidths = deriveColumnWidths($, table); + if (!colWidths) { + // No widths anywhere (e.g. markdown-sourced tables). Apply a default + // per-column width so the table's intrinsic width can exceed the + // editor container, letting .tableWrapper's overflow-x: auto scroll + // instead of cramming columns into the available width. + let count = 0; + firstRow.children('td, th').each(function () { + count += parseInt($(this).attr('colspan') || '1', 10) || 1; + }); + if (count === 0) return; + colWidths = new Array(count).fill(DEFAULT_IMPORT_COL_WIDTH_PX); + } + + let col = 0; + firstRow.children('td, th').each(function () { + const cell = $(this); + if (cell.attr('colwidth')) { + col += parseInt(cell.attr('colspan') || '1', 10) || 1; + return; + } + const colspan = parseInt(cell.attr('colspan') || '1', 10) || 1; + const slice = colWidths.slice(col, col + colspan); + col += colspan; + if (slice.length === 0 || slice.every((w) => w === null)) return; + const values = slice.map((w) => (w == null ? 100 : w)); + cell.attr('colwidth', values.join(',')); + }); + }); +} diff --git a/apps/server/src/integrations/mail/mail.service.ts b/apps/server/src/integrations/mail/mail.service.ts index 035b27062..22f6916f5 100644 --- a/apps/server/src/integrations/mail/mail.service.ts +++ b/apps/server/src/integrations/mail/mail.service.ts @@ -6,7 +6,7 @@ import { EnvironmentService } from '../environment/environment.service'; import { InjectQueue } from '@nestjs/bullmq'; import { QueueName, QueueJob } from '../queue/constants'; import { Queue } from 'bullmq'; -import { render } from '@react-email/render'; +import { render } from 'react-email'; @Injectable() export class MailService { @@ -17,6 +17,10 @@ export class MailService { ) {} async sendEmail(message: MailMessage): Promise { + if (this.isRecipientBlocked(message.to)) { + return; + } + if (message.template) { // in case this method is used directly. we do not send the tsx template from queue message.html = await render(message.template, { @@ -35,6 +39,10 @@ export class MailService { } async sendToQueue(message: MailMessage): Promise { + if (this.isRecipientBlocked(message.to)) { + return; + } + if (message.template) { // transform the React object because it gets lost when sent via the queue message.html = await render(message.template, { @@ -47,4 +55,11 @@ export class MailService { } await this.emailQueue.add(QueueJob.SEND_EMAIL, message); } + + private isRecipientBlocked(to: string): boolean { + const blocked = this.environmentService.getMailBlockedRecipientDomains(); + if (blocked.length === 0) return false; + const domain = to?.split('@')[1]?.toLowerCase(); + return !!domain && blocked.includes(domain); + } } diff --git a/apps/server/src/integrations/storage/drivers/azure.driver.ts b/apps/server/src/integrations/storage/drivers/azure.driver.ts new file mode 100644 index 000000000..034c46032 --- /dev/null +++ b/apps/server/src/integrations/storage/drivers/azure.driver.ts @@ -0,0 +1,192 @@ +import { Readable } from 'stream'; +import { + AzureStorageConfig, + StorageDriver, + StorageOption, +} from '../interfaces'; +import { + BlobSASPermissions, + BlobServiceClient, + BlockBlobClient, + ContainerClient, + generateBlobSASQueryParameters, + SASProtocol, + StorageSharedKeyCredential, +} from '@azure/storage-blob'; +import { Logger } from '@nestjs/common'; +import { getMimeType } from '../../../common/helpers'; + +export class AzureDriver implements StorageDriver { + private readonly config: AzureStorageConfig; + private readonly blobServiceClient: BlobServiceClient; + private readonly containerClient: ContainerClient; + private readonly sharedKeyCredential: StorageSharedKeyCredential; + private readonly accountUrl: string; + + constructor(config: AzureStorageConfig) { + this.config = config; + + if (!config.accountName) { + throw new Error('AzureDriver: accountName is required'); + } + if (!config.container) { + throw new Error('AzureDriver: container is required'); + } + if (!config.accountKey) { + throw new Error('AzureDriver: accountKey is required'); + } + + this.accountUrl = + config.endpoint ?? + `https://${config.accountName}.blob.core.windows.net`; + + this.sharedKeyCredential = new StorageSharedKeyCredential( + config.accountName, + config.accountKey, + ); + + this.blobServiceClient = this.createBlobServiceClient(); + this.containerClient = this.blobServiceClient.getContainerClient( + config.container, + ); + } + + private blockBlob(filePath: string): BlockBlobClient { + return this.containerClient.getBlockBlobClient(filePath); + } + + async upload(filePath: string, file: Buffer | Readable): Promise { + const stream: Readable = Buffer.isBuffer(file) ? Readable.from(file) : file; + await this.uploadStream(filePath, stream); + } + + async uploadStream( + filePath: string, + file: Readable, + options?: { recreateClient?: boolean }, + ): Promise { + const clientToUse = options?.recreateClient + ? this.createBlobServiceClient() + .getContainerClient(this.config.container) + .getBlockBlobClient(filePath) + : this.blockBlob(filePath); + + try { + const contentType = getMimeType(filePath); + await clientToUse.uploadStream(file, undefined, undefined, { + blobHTTPHeaders: { blobContentType: contentType }, + }); + } catch (err) { + Logger.error(err); + throw new Error(`Failed to upload file: ${(err as Error).message}`); + } + } + + async copy(fromFilePath: string, toFilePath: string): Promise { + try { + if (!(await this.exists(fromFilePath))) { + return; + } + const sourceUrl = await this.getSignedUrl(fromFilePath, 60); + const dest = this.blockBlob(toFilePath); + await dest.syncCopyFromURL(sourceUrl); + } catch (err) { + throw new Error(`Failed to copy file: ${(err as Error).message}`); + } + } + + async read(filePath: string): Promise { + try { + return await this.blockBlob(filePath).downloadToBuffer(); + } catch (err) { + throw new Error( + `Failed to read file from Azure: ${(err as Error).message}`, + ); + } + } + + async readStream(filePath: string): Promise { + try { + const response = await this.blockBlob(filePath).download(); + return response.readableStreamBody as Readable; + } catch (err) { + throw new Error( + `Failed to read file from Azure: ${(err as Error).message}`, + ); + } + } + + async readRangeStream( + filePath: string, + range: { start: number; end: number }, + ): Promise { + try { + const count = range.end - range.start + 1; + const response = await this.blockBlob(filePath).download( + range.start, + count, + ); + return response.readableStreamBody as Readable; + } catch (err) { + throw new Error( + `Failed to read file from Azure: ${(err as Error).message}`, + ); + } + } + + async exists(filePath: string): Promise { + try { + return await this.blockBlob(filePath).exists(); + } catch (err) { + throw new Error( + `Failed to check existence in Azure: ${(err as Error).message}`, + ); + } + } + + getUrl(filePath: string): string { + const base = this.config.baseUrl ?? this.accountUrl; + return `${base}/${this.config.container}/${filePath}`; + } + + async getSignedUrl(filePath: string, expiresIn: number): Promise { + const expiresOn = new Date(Date.now() + expiresIn * 1000); + const sas = generateBlobSASQueryParameters( + { + containerName: this.config.container, + blobName: filePath, + permissions: BlobSASPermissions.parse('r'), + expiresOn, + protocol: SASProtocol.HttpsAndHttp, + }, + this.sharedKeyCredential, + ).toString(); + return `${this.accountUrl}/${this.config.container}/${filePath}?${sas}`; + } + + async delete(filePath: string): Promise { + try { + await this.blockBlob(filePath).delete(); + } catch (err) { + throw new Error( + `Error deleting file ${filePath} from Azure: ${(err as Error).message}`, + ); + } + } + + getDriver(): BlobServiceClient { + return this.blobServiceClient; + } + + getDriverName(): string { + return StorageOption.AZURE; + } + + getConfig(): Record { + return this.config; + } + + private createBlobServiceClient(): BlobServiceClient { + return new BlobServiceClient(this.accountUrl, this.sharedKeyCredential); + } +} diff --git a/apps/server/src/integrations/storage/drivers/index.ts b/apps/server/src/integrations/storage/drivers/index.ts index 02ab4b30c..5b7a326ea 100644 --- a/apps/server/src/integrations/storage/drivers/index.ts +++ b/apps/server/src/integrations/storage/drivers/index.ts @@ -1,2 +1,3 @@ export { LocalDriver } from './local.driver'; export { S3Driver } from './s3.driver'; +export { AzureDriver } from './azure.driver'; diff --git a/apps/server/src/integrations/storage/drivers/local.driver.spec.ts b/apps/server/src/integrations/storage/drivers/local.driver.spec.ts new file mode 100644 index 000000000..ccdddcc5d --- /dev/null +++ b/apps/server/src/integrations/storage/drivers/local.driver.spec.ts @@ -0,0 +1,67 @@ +import { resolve, sep } from 'path'; +import { LocalDriver } from './local.driver'; + +type FullPath = (filePath: string) => string; + +describe('LocalDriver._fullPath', () => { + const ROOT = resolve('/data/storage'); + const driver = new LocalDriver({ storagePath: ROOT }); + const fullPath = ((driver as any)._fullPath as FullPath).bind(driver); + + describe('legitimate inputs (behavior preserved)', () => { + it.each([ + ['workspace-id/avatars/uuid.png', `${ROOT}${sep}workspace-id${sep}avatars${sep}uuid.png`], + ['workspace-id/files/uuid/file.pdf', `${ROOT}${sep}workspace-id${sep}files${sep}uuid${sep}file.pdf`], + ['a/b/c/d/e.bin', `${ROOT}${sep}a${sep}b${sep}c${sep}d${sep}e.bin`], + ['', ROOT], + ['.', ROOT], + ['./x/y.png', `${ROOT}${sep}x${sep}y.png`], + ['a//b', `${ROOT}${sep}a${sep}b`], + ['a/b/../c', `${ROOT}${sep}a${sep}c`], + ])('resolves %j to %j', (input, expected) => { + expect(fullPath(input)).toBe(expected); + }); + }); + + describe('traversal rejected', () => { + it.each([ + '../etc/passwd', + '../../../etc/passwd', + 'workspace/../../../etc/passwd', + '..', + '../..', + 'a/../../..', + ])('throws for %j', (input) => { + expect(() => fullPath(input)).toThrow('Invalid file path'); + }); + }); + + describe('absolute path rejected', () => { + it.each([ + '/etc/passwd', + '/root/.ssh/id_rsa', + sep + 'absolute', + ])('throws for %j', (input) => { + expect(() => fullPath(input)).toThrow('Invalid file path'); + }); + }); + + describe('prefix-confusion rejected', () => { + it('rejects a sibling directory whose name starts with the storage root', () => { + const siblingDriver = new LocalDriver({ storagePath: '/data/storage' }); + const siblingFullPath = ((siblingDriver as any)._fullPath as FullPath).bind(siblingDriver); + // Attempt to reach /data/storage-evil/secret by traversal: + // resolve('/data/storage', '../storage-evil/secret') === '/data/storage-evil/secret' + // Without the `+ sep` guard, a startsWith check would match. + expect(() => siblingFullPath('../storage-evil/secret')).toThrow('Invalid file path'); + }); + }); + + describe('storage root itself', () => { + it('accepts the root when input resolves to it', () => { + expect(fullPath('')).toBe(ROOT); + expect(fullPath('.')).toBe(ROOT); + expect(fullPath('a/..')).toBe(ROOT); + }); + }); +}); diff --git a/apps/server/src/integrations/storage/drivers/local.driver.ts b/apps/server/src/integrations/storage/drivers/local.driver.ts index 39342c619..60be7c5df 100644 --- a/apps/server/src/integrations/storage/drivers/local.driver.ts +++ b/apps/server/src/integrations/storage/drivers/local.driver.ts @@ -3,7 +3,7 @@ import { LocalStorageConfig, StorageOption, } from '../interfaces'; -import { join, dirname } from 'path'; +import { dirname, resolve, sep } from 'path'; import * as fs from 'fs-extra'; import { Readable } from 'stream'; import { createReadStream, createWriteStream } from 'node:fs'; @@ -17,7 +17,12 @@ export class LocalDriver implements StorageDriver { } private _fullPath(filePath: string): string { - return join(this.config.storagePath, filePath); + const storageRoot = resolve(this.config.storagePath); + const fullPath = resolve(storageRoot, filePath); + if (fullPath !== storageRoot && !fullPath.startsWith(storageRoot + sep)) { + throw new Error('Invalid file path'); + } + return fullPath; } async upload(filePath: string, file: Buffer | Readable): Promise { diff --git a/apps/server/src/integrations/storage/interfaces/storage.interface.ts b/apps/server/src/integrations/storage/interfaces/storage.interface.ts index 48c684919..ef37f43c0 100644 --- a/apps/server/src/integrations/storage/interfaces/storage.interface.ts +++ b/apps/server/src/integrations/storage/interfaces/storage.interface.ts @@ -3,11 +3,13 @@ import { S3ClientConfig } from '@aws-sdk/client-s3'; export enum StorageOption { LOCAL = 'local', S3 = 's3', + AZURE = 'azure', } export type StorageConfig = | { driver: StorageOption.LOCAL; config: LocalStorageConfig } - | { driver: StorageOption.S3; config: S3StorageConfig }; + | { driver: StorageOption.S3; config: S3StorageConfig } + | { driver: StorageOption.AZURE; config: AzureStorageConfig }; export interface LocalStorageConfig { storagePath: string; @@ -20,6 +22,14 @@ export interface S3StorageConfig baseUrl?: string; // Optional CDN URL for assets } +export interface AzureStorageConfig { + accountName: string; + container: string; + accountKey: string; + endpoint?: string; + baseUrl?: string; +} + export interface StorageOptions { disk: StorageConfig; } diff --git a/apps/server/src/integrations/storage/providers/storage.provider.ts b/apps/server/src/integrations/storage/providers/storage.provider.ts index 114896667..91967f8ec 100644 --- a/apps/server/src/integrations/storage/providers/storage.provider.ts +++ b/apps/server/src/integrations/storage/providers/storage.provider.ts @@ -4,13 +4,14 @@ import { } from '../constants/storage.constants'; import { EnvironmentService } from '../../environment/environment.service'; import { + AzureStorageConfig, LocalStorageConfig, S3StorageConfig, StorageConfig, StorageDriver, StorageOption, } from '../interfaces'; -import { LocalDriver, S3Driver } from '../drivers'; +import { AzureDriver, LocalDriver, S3Driver } from '../drivers'; import * as process from 'node:process'; import { LOCAL_STORAGE_PATH } from '../../../common/helpers'; import path from 'path'; @@ -21,6 +22,8 @@ function createStorageDriver(disk: StorageConfig): StorageDriver { return new LocalDriver(disk.config as LocalStorageConfig); case StorageOption.S3: return new S3Driver(disk.config as S3StorageConfig); + case StorageOption.AZURE: + return new AzureDriver(disk.config as AzureStorageConfig); default: throw new Error(`Unknown storage driver`); } @@ -70,6 +73,18 @@ export const storageDriverConfigProvider = { return s3Config; } + case StorageOption.AZURE: + return { + driver, + config: { + accountName: environmentService.getAzureStorageAccountName(), + container: environmentService.getAzureStorageContainer(), + accountKey: environmentService.getAzureStorageAccountKey(), + endpoint: environmentService.getAzureStorageEndpoint() || undefined, + baseUrl: environmentService.getAzureStorageUrl() || undefined, + }, + }; + default: throw new Error(`Unknown storage driver: ${driver}`); } diff --git a/apps/server/src/integrations/transactional/emails/approval-rejected-email.tsx b/apps/server/src/integrations/transactional/emails/approval-rejected-email.tsx index c647ef8d4..4da0c910d 100644 --- a/apps/server/src/integrations/transactional/emails/approval-rejected-email.tsx +++ b/apps/server/src/integrations/transactional/emails/approval-rejected-email.tsx @@ -1,4 +1,4 @@ -import { Section, Text } from '@react-email/components'; +import { Section, Text } from 'react-email'; import * as React from 'react'; import { content, paragraph } from '../css/styles'; import { EmailButton, MailBody } from '../partials/partials'; diff --git a/apps/server/src/integrations/transactional/emails/approval-requested-email.tsx b/apps/server/src/integrations/transactional/emails/approval-requested-email.tsx index f3c276599..f500f850c 100644 --- a/apps/server/src/integrations/transactional/emails/approval-requested-email.tsx +++ b/apps/server/src/integrations/transactional/emails/approval-requested-email.tsx @@ -1,4 +1,4 @@ -import { Section, Text } from '@react-email/components'; +import { Section, Text } from 'react-email'; import * as React from 'react'; import { content, paragraph } from '../css/styles'; import { EmailButton, MailBody } from '../partials/partials'; diff --git a/apps/server/src/integrations/transactional/emails/change-password-email.tsx b/apps/server/src/integrations/transactional/emails/change-password-email.tsx index de3566c9b..b6754c552 100644 --- a/apps/server/src/integrations/transactional/emails/change-password-email.tsx +++ b/apps/server/src/integrations/transactional/emails/change-password-email.tsx @@ -1,4 +1,4 @@ -import { Section, Text } from '@react-email/components'; +import { Section, Text } from 'react-email'; import * as React from 'react'; import { content, paragraph } from '../css/styles'; import { MailBody } from '../partials/partials'; diff --git a/apps/server/src/integrations/transactional/emails/comment-created-email.tsx b/apps/server/src/integrations/transactional/emails/comment-created-email.tsx index 7fee6dc90..ddf70963b 100644 --- a/apps/server/src/integrations/transactional/emails/comment-created-email.tsx +++ b/apps/server/src/integrations/transactional/emails/comment-created-email.tsx @@ -1,4 +1,4 @@ -import { Section, Text } from '@react-email/components'; +import { Section, Text } from 'react-email'; import * as React from 'react'; import { content, paragraph } from '../css/styles'; import { EmailButton, MailBody } from '../partials/partials'; diff --git a/apps/server/src/integrations/transactional/emails/comment-mention-email.tsx b/apps/server/src/integrations/transactional/emails/comment-mention-email.tsx index 079912a08..72a1d4a75 100644 --- a/apps/server/src/integrations/transactional/emails/comment-mention-email.tsx +++ b/apps/server/src/integrations/transactional/emails/comment-mention-email.tsx @@ -1,4 +1,4 @@ -import { Section, Text } from '@react-email/components'; +import { Section, Text } from 'react-email'; import * as React from 'react'; import { content, paragraph } from '../css/styles'; import { EmailButton, MailBody } from '../partials/partials'; diff --git a/apps/server/src/integrations/transactional/emails/comment-resolved-email.tsx b/apps/server/src/integrations/transactional/emails/comment-resolved-email.tsx index 4555acb6d..303375cd4 100644 --- a/apps/server/src/integrations/transactional/emails/comment-resolved-email.tsx +++ b/apps/server/src/integrations/transactional/emails/comment-resolved-email.tsx @@ -1,4 +1,4 @@ -import { Section, Text } from '@react-email/components'; +import { Section, Text } from 'react-email'; import * as React from 'react'; import { content, paragraph } from '../css/styles'; import { EmailButton, MailBody } from '../partials/partials'; diff --git a/apps/server/src/integrations/transactional/emails/forgot-password-email.tsx b/apps/server/src/integrations/transactional/emails/forgot-password-email.tsx index 59270e5e6..4ce79345c 100644 --- a/apps/server/src/integrations/transactional/emails/forgot-password-email.tsx +++ b/apps/server/src/integrations/transactional/emails/forgot-password-email.tsx @@ -1,4 +1,4 @@ -import { Button, Link, Section, Text } from '@react-email/components'; +import { Button, Link, Section, Text } from 'react-email'; import * as React from 'react'; import { button, content, paragraph } from '../css/styles'; import { MailBody } from '../partials/partials'; diff --git a/apps/server/src/integrations/transactional/emails/invitation-accepted-email.tsx b/apps/server/src/integrations/transactional/emails/invitation-accepted-email.tsx index 33256d4d4..a1754e84a 100644 --- a/apps/server/src/integrations/transactional/emails/invitation-accepted-email.tsx +++ b/apps/server/src/integrations/transactional/emails/invitation-accepted-email.tsx @@ -1,4 +1,4 @@ -import { Section, Text } from '@react-email/components'; +import { Section, Text } from 'react-email'; import * as React from 'react'; import { content, paragraph } from '../css/styles'; import { MailBody } from '../partials/partials'; diff --git a/apps/server/src/integrations/transactional/emails/invitation-email.tsx b/apps/server/src/integrations/transactional/emails/invitation-email.tsx index ac8ce32ab..7a1cde3ab 100644 --- a/apps/server/src/integrations/transactional/emails/invitation-email.tsx +++ b/apps/server/src/integrations/transactional/emails/invitation-email.tsx @@ -1,4 +1,4 @@ -import { Section, Text } from '@react-email/components'; +import { Section, Text } from 'react-email'; import * as React from 'react'; import { content, paragraph } from '../css/styles'; import { EmailButton, MailBody } from '../partials/partials'; diff --git a/apps/server/src/integrations/transactional/emails/page-mention-email.tsx b/apps/server/src/integrations/transactional/emails/page-mention-email.tsx index d78866411..a112ec65d 100644 --- a/apps/server/src/integrations/transactional/emails/page-mention-email.tsx +++ b/apps/server/src/integrations/transactional/emails/page-mention-email.tsx @@ -1,4 +1,4 @@ -import { Section, Text } from '@react-email/components'; +import { Section, Text } from 'react-email'; import * as React from 'react'; import { content, paragraph } from '../css/styles'; import { EmailButton, MailBody } from '../partials/partials'; diff --git a/apps/server/src/integrations/transactional/emails/page-update-digest-email.tsx b/apps/server/src/integrations/transactional/emails/page-update-digest-email.tsx index f847291d8..259ecfb62 100644 --- a/apps/server/src/integrations/transactional/emails/page-update-digest-email.tsx +++ b/apps/server/src/integrations/transactional/emails/page-update-digest-email.tsx @@ -1,4 +1,4 @@ -import { Link, Section, Text } from '@react-email/components'; +import { Link, Section, Text } from 'react-email'; import * as React from 'react'; import { content, link, paragraph } from '../css/styles'; import { getGreetingName, MailBody } from '../partials/partials'; diff --git a/apps/server/src/integrations/transactional/emails/page-update-email.tsx b/apps/server/src/integrations/transactional/emails/page-update-email.tsx index 876b4f31b..bb9ceb498 100644 --- a/apps/server/src/integrations/transactional/emails/page-update-email.tsx +++ b/apps/server/src/integrations/transactional/emails/page-update-email.tsx @@ -1,4 +1,4 @@ -import { Link, Section, Text } from '@react-email/components'; +import { Link, Section, Text } from 'react-email'; import * as React from 'react'; import { content, link, paragraph } from '../css/styles'; import { EmailButton, getGreetingName, MailBody } from '../partials/partials'; diff --git a/apps/server/src/integrations/transactional/emails/permission-granted-email.tsx b/apps/server/src/integrations/transactional/emails/permission-granted-email.tsx index bd4529b7d..356a89d79 100644 --- a/apps/server/src/integrations/transactional/emails/permission-granted-email.tsx +++ b/apps/server/src/integrations/transactional/emails/permission-granted-email.tsx @@ -1,4 +1,4 @@ -import { Section, Text } from '@react-email/components'; +import { Section, Text } from 'react-email'; import * as React from 'react'; import { content, paragraph } from '../css/styles'; import { EmailButton, MailBody } from '../partials/partials'; diff --git a/apps/server/src/integrations/transactional/emails/verification-expired-email.tsx b/apps/server/src/integrations/transactional/emails/verification-expired-email.tsx index be808333d..c53721372 100644 --- a/apps/server/src/integrations/transactional/emails/verification-expired-email.tsx +++ b/apps/server/src/integrations/transactional/emails/verification-expired-email.tsx @@ -1,4 +1,4 @@ -import { Section, Text } from '@react-email/components'; +import { Section, Text } from 'react-email'; import * as React from 'react'; import { content, paragraph } from '../css/styles'; import { EmailButton, MailBody } from '../partials/partials'; diff --git a/apps/server/src/integrations/transactional/emails/verification-expiring-email.tsx b/apps/server/src/integrations/transactional/emails/verification-expiring-email.tsx index 16bb49772..670a85467 100644 --- a/apps/server/src/integrations/transactional/emails/verification-expiring-email.tsx +++ b/apps/server/src/integrations/transactional/emails/verification-expiring-email.tsx @@ -1,4 +1,4 @@ -import { Section, Text } from '@react-email/components'; +import { Section, Text } from 'react-email'; import * as React from 'react'; import { content, paragraph } from '../css/styles'; import { EmailButton, MailBody } from '../partials/partials'; diff --git a/apps/server/src/integrations/transactional/partials/partials.tsx b/apps/server/src/integrations/transactional/partials/partials.tsx index c5b30e6be..0c0e58b0a 100644 --- a/apps/server/src/integrations/transactional/partials/partials.tsx +++ b/apps/server/src/integrations/transactional/partials/partials.tsx @@ -7,7 +7,7 @@ import { Row, Section, Text, -} from '@react-email/components'; +} from 'react-email'; import * as React from 'react'; interface MailBodyProps { diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index d47bf547e..1c2ccebf1 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -12,6 +12,8 @@ import fastifyMultipart from '@fastify/multipart'; import fastifyCookie from '@fastify/cookie'; import fastifyIp from 'fastify-ip'; import { InternalLogFilter } from './common/logger/internal-log-filter'; +import { EnvironmentService } from './integrations/environment/environment.service'; +import { resolveFrameHeader } from './common/helpers'; async function bootstrap() { const app = await NestFactory.create( @@ -50,6 +52,52 @@ async function bootstrap() { await app.register(fastifyMultipart); await app.register(fastifyCookie); + const environmentService = app.get(EnvironmentService); + const frameHeader = resolveFrameHeader( + environmentService.isIframeEmbedAllowed(), + environmentService.getIframeAllowedOrigins(), + ); + if (frameHeader) { + // Skipped routes: + // /api/files/ - attachment controller sets its own CSP we'd overwrite + // /share/ 0 public share pages are safe to embed + const frameHeaderSkippedPrefixes = ['/api/files/', '/share/']; + app + .getHttpAdapter() + .getInstance() + .addHook('onSend', (req, reply, payload, done) => { + if (frameHeaderSkippedPrefixes.some((p) => req.url.startsWith(p))) { + return done(null, payload); + } + reply.header(frameHeader.name, frameHeader.value); + done(null, payload); + }); + } + + app + .getHttpAdapter() + .getInstance() + .addHook('onRequest', (request, _reply, done) => { + (request.raw as any).ip = request.ip; + done(); + }); + + app + .getHttpAdapter() + .getInstance() + .addContentTypeParser( + 'application/scim+json', + { parseAs: 'string' }, + (_, body, done) => { + try { + const json = JSON.parse(body.toString()); + done(null, json); + } catch (err: any) { + done(err); + } + }, + ); + app .getHttpAdapter() .getInstance() diff --git a/apps/server/src/ws/ws.service.ts b/apps/server/src/ws/ws.service.ts index 23d41909d..3278f72cb 100644 --- a/apps/server/src/ws/ws.service.ts +++ b/apps/server/src/ws/ws.service.ts @@ -54,7 +54,7 @@ export class WsService { return; } - await this.broadcastToAuthorizedUsers(room, client.data.userId, pageId, data); + await this.broadcastToAuthorizedUsers(room, client.id, pageId, data); } async invalidateSpaceRestrictionCache(spaceId: string): Promise { @@ -115,14 +115,17 @@ export class WsService { private async broadcastToAuthorizedUsers( room: string, - excludeUserId: string | null, + excludeSocketId: string | null, pageId: string, data: any, ): Promise { const sockets = await this.server.in(room).fetchSockets(); - const otherSockets = excludeUserId - ? sockets.filter((s) => s.data.userId !== excludeUserId) + // Exclude only the originating socket, not every socket of the originating + // user. Excluding by userId silently dropped the originator's other tabs + // from receiving restricted-space tree events. + const otherSockets = excludeSocketId + ? sockets.filter((s) => s.id !== excludeSocketId) : sockets; if (otherSockets.length === 0) return; diff --git a/crowdin.yml b/crowdin.yml index bddcbc3cf..c90d092ff 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,3 +1,4 @@ files: - source: /apps/client/public/locales/en-US/translation.json translation: /apps/client/public/locales/%locale%/%original_file_name% + type: i18next_json diff --git a/package.json b/package.json index bf7b27618..8b497eaf1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "docmost", "homepage": "https://docmost.com", - "version": "0.80.0", + "version": "0.90.0", "private": true, "scripts": { "build": "nx run-many -t build", @@ -62,7 +62,7 @@ "cross-env": "^10.1.0", "date-fns": "^4.1.0", "diff": "8.0.3", - "dompurify": "^3.3.3", + "dompurify": "3.4.1", "fractional-indexing-jittered": "^1.0.0", "highlight.js": "^11.11.1", "image-dimensions": "^2.5.0", @@ -72,7 +72,7 @@ "ms": "3.0.0-canary.1", "qrcode": "^1.5.4", "rfc6902": "5.2.0", - "uuid": "^13.0.0", + "uuid": "^14.0.0", "y-indexeddb": "^9.0.12", "y-prosemirror": "1.3.7", "yjs": "^13.6.30" @@ -95,17 +95,17 @@ "packageManager": "pnpm@10.4.0", "pnpm": { "patchedDependencies": { - "react-arborist@3.4.0": "patches/react-arborist@3.4.0.patch" + "scimmy@1.3.5": "patches/scimmy@1.3.5.patch" }, "overrides": { "prosemirror-changeset": "2.4.0", "y-prosemirror": "1.3.7", "glob": "13.0.6", - "ws": "8.20.0", - "dompurify": "3.3.3", + "ws": "8.20.1", + "dompurify": "3.4.1", "tmp": "0.2.5", - "hono": "4.12.12", - "mermaid": "11.13.0", + "hono": "4.12.18", + "mermaid": "11.15.0", "nanoid@^3": "3.3.8", "socket.io-parser": "4.2.6", "serialize-javascript": "7.0.3", @@ -123,16 +123,18 @@ "flatted": "3.4.2", "picomatch@<2.3.2": "2.3.2", "picomatch@>=4.0.0 <4.0.4": "4.0.4", - "fastify": "5.8.3", + "fastify": "5.8.5", "yaml@>=1.0.0 <1.10.3": "1.10.3", "yaml@>=2.0.0 <2.8.3": "2.8.3", "path-to-regexp@^8": "8.4.0", - "brace-expansion@^5": "5.0.5", - "@xmldom/xmldom": "0.8.12", + "brace-expansion@^5": "5.0.6", + "@xmldom/xmldom": "0.8.13", "handlebars": "4.7.9", - "axios": "1.15.0", - "langsmith": "0.5.18", - "follow-redirects": "1.16.0" + "axios": "1.16.0", + "langsmith": "0.7.0", + "follow-redirects": "1.16.0", + "protobufjs": "7.5.8", + "ip-address": "10.1.1" }, "neverBuiltDependencies": [] } diff --git a/packages/editor-ext/src/index.ts b/packages/editor-ext/src/index.ts index f64de6539..800e830fe 100644 --- a/packages/editor-ext/src/index.ts +++ b/packages/editor-ext/src/index.ts @@ -21,7 +21,9 @@ export * from "./lib/markdown"; export * from "./lib/search-and-replace"; export * from "./lib/embed-provider"; export * from "./lib/subpages"; +export * from "./lib/transclusion"; export * from "./lib/highlight"; +export * from "./lib/indent"; export * from "./lib/heading/heading"; export * from "./lib/unique-id"; export * from "./lib/shared-storage"; @@ -29,5 +31,6 @@ export * from "./lib/recreate-transform"; export * from "./lib/columns"; export * from "./lib/status"; export * from "./lib/pdf"; +export * from "./lib/page-break"; export * from "./lib/resizable-nodeview"; export * from "./lib/base-embed"; diff --git a/packages/editor-ext/src/lib/callout/callout.ts b/packages/editor-ext/src/lib/callout/callout.ts index 898fc4152..a07a4a96c 100644 --- a/packages/editor-ext/src/lib/callout/callout.ts +++ b/packages/editor-ext/src/lib/callout/callout.ts @@ -162,6 +162,28 @@ export const Callout = Node.create({ return false; } + // Empty callout: delete the whole node so Backspace inside it isn't + // a no-op (isolating: true blocks the default join with the block + // above). + const calloutDepth = $from.depth - 1; + if (calloutDepth >= 0) { + const calloutNode = $from.node(calloutDepth); + if ( + calloutNode.type === this.type && + calloutNode.childCount === 1 && + calloutNode.firstChild?.content.size === 0 + ) { + const calloutPos = $from.before(calloutDepth); + const { tr } = state; + tr.delete(calloutPos, calloutPos + calloutNode.nodeSize); + tr.setSelection( + TextSelection.near(tr.doc.resolve(calloutPos), -1), + ); + view.dispatch(tr); + return true; + } + } + const previousPosition = $from.before($from.depth) - 1; // If nothing above to join with @@ -207,6 +229,56 @@ export const Callout = Node.create({ } return false; }, + + // Exit the callout into a fresh paragraph below when the cursor sits + // in an empty trailing child. An empty callout (single empty + // paragraph) exits on the first Enter and keeps the empty callout + // intact; a callout with content needs the double-Enter pattern + // (first Enter splits, second Enter on the new trailing empty exits + // and removes that trailing paragraph). + Enter: ({ editor }) => { + const { state, view } = editor; + const { selection } = state; + if (!selection.empty) return false; + + const { $from } = selection; + const calloutDepth = $from.depth - 1; + if (calloutDepth < 0) return false; + + const calloutNode = $from.node(calloutDepth); + if (calloutNode.type !== this.type) return false; + if ($from.parent.content.size !== 0) return false; + if ($from.index(calloutDepth) !== calloutNode.childCount - 1) { + return false; + } + + const paragraphType = state.schema.nodes.paragraph; + const containerDepth = calloutDepth - 1; + const container = $from.node(containerDepth); + const indexAfter = $from.indexAfter(containerDepth); + if ( + !container.canReplaceWith(indexAfter, indexAfter, paragraphType) + ) { + return false; + } + + const calloutEnd = $from.after(calloutDepth); + const paragraph = paragraphType.create(); + const { tr } = state; + + if (calloutNode.childCount === 1) { + tr.insert(calloutEnd, paragraph); + tr.setSelection(TextSelection.create(tr.doc, calloutEnd + 1)); + } else { + tr.delete($from.before(), $from.after()); + const insertPos = tr.mapping.map(calloutEnd); + tr.insert(insertPos, paragraph); + tr.setSelection(TextSelection.create(tr.doc, insertPos + 1)); + } + + view.dispatch(tr); + return true; + }, }; }, diff --git a/packages/editor-ext/src/lib/custom-code-block/custom-code-block.ts b/packages/editor-ext/src/lib/custom-code-block/custom-code-block.ts index ba9fe9c18..5d67188a3 100644 --- a/packages/editor-ext/src/lib/custom-code-block/custom-code-block.ts +++ b/packages/editor-ext/src/lib/custom-code-block/custom-code-block.ts @@ -1,8 +1,10 @@ -import type { CodeBlockOptions } from "@tiptap/extension-code-block"; -import CodeBlock from "@tiptap/extension-code-block"; +import type { CodeBlockOptions } from '@tiptap/extension-code-block'; +import CodeBlock from '@tiptap/extension-code-block'; +import { Plugin, Selection, TextSelection } from '@tiptap/pm/state'; +import { GapCursor } from '@tiptap/pm/gapcursor'; -import { LowlightPlugin } from "./lowlight-plugin.js"; -import { ReactNodeViewRenderer } from "@tiptap/react"; +import { LowlightPlugin } from './lowlight-plugin.js'; +import { ReactNodeViewRenderer } from '@tiptap/react'; export interface CodeBlockLowlightOptions extends CodeBlockOptions { /** @@ -12,20 +14,24 @@ export interface CodeBlockLowlightOptions extends CodeBlockOptions { view: any; } -const TAB_CHAR = "\u00A0\u00A0"; +const TAB_CHAR = '\u00A0\u00A0'; /** * This extension allows you to highlight code blocks with lowlight. * @see https://tiptap.dev/api/nodes/code-block-lowlight */ export const CustomCodeBlock = CodeBlock.extend({ + // Run ahead of Gapcursor (100) so the mermaid arrow-into-source plugin + // can intercept before gapcursor takes over. + priority: 101, selectable: true, + isolating: true, addOptions() { return { ...this.parent?.(), lowlight: {}, - languageClassPrefix: "language-", + languageClassPrefix: 'language-', exitOnTripleEnter: true, exitOnArrowDown: true, defaultLanguage: null, @@ -35,22 +41,88 @@ export const CustomCodeBlock = CodeBlock.extend({ }, addKeyboardShortcuts() { + const isMermaid = (node: any) => + node?.type === this.type && node.attrs.language === 'mermaid'; + return { ...this.parent?.(), - Tab: () => { - if (this.editor.isActive("codeBlock")) { - this.editor - .chain() - .command(({ tr }) => { - tr.insertText(TAB_CHAR); - return true; - }) - .run(); - return true; + // Stop at the gap (or enter mermaid source) instead of jumping + // straight into the next block, so the user can place a cursor + // between two adjacent isolating blocks. + ArrowDown: ({ editor }) => { + const { state } = editor; + const { selection, doc } = state; + const { $from, empty } = selection; + + if (!empty || $from.parent.type !== this.type) return false; + if ($from.parentOffset !== $from.parent.nodeSize - 2) return false; + + const after = $from.after(); + if (after >= doc.content.size) { + return editor.commands.exitCode(); } + + const $after = doc.resolve(after); + const nodeAfter = $after.nodeAfter; + + if (isMermaid(nodeAfter)) { + return editor.commands.command(({ tr }) => { + tr.setSelection(TextSelection.create(tr.doc, after + 1)); + return true; + }); + } + + if ( + nodeAfter?.type.spec.isolating && + !nodeAfter.type.spec.atom + ) { + return editor.commands.command(({ tr }) => { + tr.setSelection(new GapCursor(tr.doc.resolve(after))); + return true; + }); + } + + return editor.commands.command(({ tr }) => { + tr.setSelection(Selection.near(tr.doc.resolve(after))); + return true; + }); }, - "Mod-a": () => { - if (this.editor.isActive("codeBlock")) { + // Mirror of ArrowDown; upstream has no ArrowUp handler. + ArrowUp: ({ editor }) => { + const { state } = editor; + const { selection, doc } = state; + const { $from, empty } = selection; + + if (!empty || $from.parent.type !== this.type) return false; + if ($from.parentOffset !== 0) return false; + + const before = $from.before(); + if (before <= 0) return false; + + const $before = doc.resolve(before); + const nodeBefore = $before.nodeBefore; + + if (isMermaid(nodeBefore)) { + return editor.commands.command(({ tr }) => { + tr.setSelection(TextSelection.create(tr.doc, before - 1)); + return true; + }); + } + + if ( + nodeBefore?.type.spec.isolating && + !nodeBefore.type.spec.atom + ) { + return editor.commands.command(({ tr }) => { + tr.setSelection(new GapCursor(tr.doc.resolve(before))); + return true; + }); + } + + return false; + }, + 'Mod-a': () => { + if (this.editor.isActive('codeBlock')) { const { state } = this.editor; const { $from } = state.selection; @@ -60,7 +132,7 @@ export const CustomCodeBlock = CodeBlock.extend({ for (depth = $from.depth; depth > 0; depth--) { const node = $from.node(depth); - if (node.type.name === "codeBlock") { + if (node.type.name === 'codeBlock') { codeBlockNode = node; codeBlockPos = $from.start(depth) - 1; break; @@ -96,6 +168,7 @@ export const CustomCodeBlock = CodeBlock.extend({ }, addProseMirrorPlugins() { + const codeBlockType = this.type; return [ ...(this.parent?.() || []), LowlightPlugin({ @@ -103,6 +176,60 @@ export const CustomCodeBlock = CodeBlock.extend({ lowlight: this.options.lowlight, defaultLanguage: this.options.defaultLanguage, }), + // Mermaid hides its
     when unselected, so the browser's native
    +      // vertical caret movement skips past it. Land the cursor inside the
    +      // source explicitly.
    +      new Plugin({
    +        props: {
    +          handleKeyDown: (view, event) => {
    +            if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
    +              return false;
    +            }
    +            const { state } = view;
    +            const { selection } = state;
    +            if (
    +              !selection.empty ||
    +              !(selection instanceof TextSelection)
    +            ) {
    +              return false;
    +            }
    +            const { $from } = selection;
    +            if ($from.depth === 0 || $from.parent.type === codeBlockType) {
    +              return false;
    +            }
    +            const dir = event.key === 'ArrowUp' ? 'up' : 'down';
    +            if (!view.endOfTextblock(dir)) return false;
    +
    +            const isMermaid = (node: any) =>
    +              node?.type === codeBlockType && node.attrs.language === 'mermaid';
    +
    +            if (event.key === 'ArrowUp') {
    +              if ($from.parentOffset !== 0) return false;
    +              const beforePos = $from.before();
    +              const prev = state.doc.resolve(beforePos).nodeBefore;
    +              if (!isMermaid(prev)) return false;
    +              const endPos = beforePos - 1;
    +              view.dispatch(
    +                state.tr.setSelection(
    +                  TextSelection.create(state.doc, endPos),
    +                ),
    +              );
    +              return true;
    +            }
    +            if ($from.parentOffset !== $from.parent.nodeSize - 2) return false;
    +            const afterPos = $from.after();
    +            const next = state.doc.resolve(afterPos).nodeAfter;
    +            if (!isMermaid(next)) return false;
    +            const startPos = afterPos + 1;
    +            view.dispatch(
    +              state.tr.setSelection(
    +                TextSelection.create(state.doc, startPos),
    +              ),
    +            );
    +            return true;
    +          },
    +        },
    +      }),
         ];
       },
     });
    diff --git a/packages/editor-ext/src/lib/drawio.ts b/packages/editor-ext/src/lib/drawio.ts
    index 4968912be..14d70f394 100644
    --- a/packages/editor-ext/src/lib/drawio.ts
    +++ b/packages/editor-ext/src/lib/drawio.ts
    @@ -28,6 +28,7 @@ export interface DrawioOptions {
     export interface DrawioAttributes {
       src?: string;
       title?: string;
    +  alt?: string;
       size?: number;
       width?: number | string;
       height?: number;
    @@ -79,6 +80,13 @@ export const Drawio = Node.create({
               "data-title": attributes.title,
             }),
           },
    +      alt: {
    +        default: undefined,
    +        parseHTML: (element) => element.getAttribute("data-alt"),
    +        renderHTML: (attributes: DrawioAttributes) => ({
    +          "data-alt": attributes.alt,
    +        }),
    +      },
           width: {
             default: null,
             parseHTML: (element) => {
    @@ -155,7 +163,7 @@ export const Drawio = Node.create({
             "img",
             {
               src: HTMLAttributes["data-src"],
    -          alt: HTMLAttributes["data-title"],
    +          alt: HTMLAttributes["data-alt"] || HTMLAttributes["data-title"],
               width: HTMLAttributes["data-width"],
             },
           ],
    @@ -226,7 +234,7 @@ export const Drawio = Node.create({
     
           const el = document.createElement("img");
           el.src = normalizeFileUrl(node.attrs.src);
    -      el.alt = node.attrs.title || "";
    +      el.alt = node.attrs.alt || node.attrs.title || "";
           el.style.display = "block";
           el.style.maxWidth = "100%";
           el.style.borderRadius = "8px";
    @@ -264,6 +272,14 @@ export const Drawio = Node.create({
                 el.src = normalizeFileUrl(updatedNode.attrs.src);
               }
     
    +          if (
    +            updatedNode.attrs.alt !== currentNode.attrs.alt ||
    +            updatedNode.attrs.title !== currentNode.attrs.title
    +          ) {
    +            el.alt =
    +              updatedNode.attrs.alt || updatedNode.attrs.title || "";
    +          }
    +
               const w = updatedNode.attrs.width;
               const h = updatedNode.attrs.height;
               if (w != null) {
    diff --git a/packages/editor-ext/src/lib/excalidraw.ts b/packages/editor-ext/src/lib/excalidraw.ts
    index 71c881f17..59b28f7fa 100644
    --- a/packages/editor-ext/src/lib/excalidraw.ts
    +++ b/packages/editor-ext/src/lib/excalidraw.ts
    @@ -28,6 +28,7 @@ export interface ExcalidrawOptions {
     export interface ExcalidrawAttributes {
       src?: string;
       title?: string;
    +  alt?: string;
       size?: number;
       width?: number | string;
       height?: number;
    @@ -79,6 +80,13 @@ export const Excalidraw = Node.create({
               "data-title": attributes.title,
             }),
           },
    +      alt: {
    +        default: undefined,
    +        parseHTML: (element) => element.getAttribute("data-alt"),
    +        renderHTML: (attributes: ExcalidrawAttributes) => ({
    +          "data-alt": attributes.alt,
    +        }),
    +      },
           width: {
             default: null,
             parseHTML: (element) => {
    @@ -155,7 +163,7 @@ export const Excalidraw = Node.create({
             "img",
             {
               src: HTMLAttributes["data-src"],
    -          alt: HTMLAttributes["data-title"],
    +          alt: HTMLAttributes["data-alt"] || HTMLAttributes["data-title"],
               width: HTMLAttributes["data-width"],
             },
           ],
    @@ -226,7 +234,7 @@ export const Excalidraw = Node.create({
     
           const el = document.createElement("img");
           el.src = normalizeFileUrl(node.attrs.src);
    -      el.alt = node.attrs.title || "";
    +      el.alt = node.attrs.alt || node.attrs.title || "";
           el.style.display = "block";
           el.style.maxWidth = "100%";
           el.style.borderRadius = "8px";
    @@ -264,6 +272,14 @@ export const Excalidraw = Node.create({
                 el.src = normalizeFileUrl(updatedNode.attrs.src);
               }
     
    +          if (
    +            updatedNode.attrs.alt !== currentNode.attrs.alt ||
    +            updatedNode.attrs.title !== currentNode.attrs.title
    +          ) {
    +            el.alt =
    +              updatedNode.attrs.alt || updatedNode.attrs.title || "";
    +          }
    +
               const w = updatedNode.attrs.width;
               const h = updatedNode.attrs.height;
               if (w != null) {
    diff --git a/packages/editor-ext/src/lib/indent.ts b/packages/editor-ext/src/lib/indent.ts
    new file mode 100644
    index 000000000..6e4ad243f
    --- /dev/null
    +++ b/packages/editor-ext/src/lib/indent.ts
    @@ -0,0 +1,225 @@
    +import { Extension } from '@tiptap/core';
    +import {
    +  Plugin,
    +  PluginKey,
    +  type EditorState,
    +  type Transaction,
    +} from '@tiptap/pm/state';
    +
    +export type IndentOptions = {
    +  types: string[];
    +  min: number;
    +  max: number;
    +};
    +
    +declare module '@tiptap/core' {
    +  interface Commands {
    +    indent: {
    +      indent: () => ReturnType;
    +      outdent: () => ReturnType;
    +    };
    +  }
    +}
    +
    +// Containers whose descendants must never carry an `indent` attribute. These
    +// nodes own their own Tab semantics (list nesting, cell navigation, literal
    +// tab) and visually conflict with our indent padding, so paragraphs and
    +// headings inside them stay flat
    +const NON_INDENTABLE_ANCESTORS = new Set([
    +  'listItem',
    +  'taskItem',
    +  'tableCell',
    +  'tableHeader',
    +  'codeBlock',
    +]);
    +
    +const clampIndent = (value: number, min: number, max: number): number => {
    +  if (!Number.isFinite(value)) return min;
    +  return Math.max(min, Math.min(max, Math.trunc(value)));
    +};
    +
    +const hasNonIndentableAncestor = (
    +  doc: EditorState['doc'],
    +  pos: number,
    +): boolean => {
    +  const $pos = doc.resolve(pos);
    +  for (let depth = $pos.depth; depth >= 0; depth--) {
    +    if (NON_INDENTABLE_ANCESTORS.has($pos.node(depth).type.name)) {
    +      return true;
    +    }
    +  }
    +  return false;
    +};
    +
    +export const Indent = Extension.create({
    +  name: 'indent',
    +
    +  priority: 1000,
    +
    +  addOptions() {
    +    return {
    +      types: ['paragraph', 'heading'],
    +      min: 0,
    +      max: 8,
    +    };
    +  },
    +
    +  addGlobalAttributes() {
    +    return [
    +      {
    +        types: this.options.types,
    +        attributes: {
    +          indent: {
    +            default: this.options.min,
    +            keepOnSplit: true,
    +            parseHTML: (element) => {
    +              const raw = element.getAttribute('data-indent');
    +              if (raw === null) return this.options.min;
    +              return clampIndent(
    +                parseInt(raw, 10),
    +                this.options.min,
    +                this.options.max,
    +              );
    +            },
    +            renderHTML: (attributes) => {
    +              const value = attributes.indent;
    +              if (value <= this.options.min) return {};
    +              return { 'data-indent': String(value) };
    +            },
    +          },
    +        },
    +      },
    +    ];
    +  },
    +
    +  addCommands() {
    +    return {
    +      indent:
    +        () =>
    +        ({ state, tr, dispatch }) => {
    +          return updateIndent(state, tr, dispatch, this.options, +1);
    +        },
    +      outdent:
    +        () =>
    +        ({ state, tr, dispatch }) => {
    +          return updateIndent(state, tr, dispatch, this.options, -1);
    +        },
    +    };
    +  },
    +
    +  addKeyboardShortcuts() {
    +    const isInIndentableBlock = (): boolean => {
    +      const { $from } = this.editor.state.selection;
    +      if (!this.options.types.includes($from.parent.type.name)) return false;
    +      for (let depth = $from.depth - 1; depth >= 0; depth--) {
    +        if (NON_INDENTABLE_ANCESTORS.has($from.node(depth).type.name)) {
    +          return false;
    +        }
    +      }
    +      return true;
    +    };
    +
    +    return {
    +      // Return the command's result so Tab falls through to the browser
    +      // (moving focus out of the editor) once the user has reached max
    +      // indent. Without this Tab stays trapped at max depth, failing
    +      // WCAG 2.1.2.
    +      Tab: () => {
    +        if (!isInIndentableBlock()) return false;
    +        return this.editor.commands.indent();
    +      },
    +      'Shift-Tab': () => {
    +        if (!isInIndentableBlock()) return false;
    +        return this.editor.commands.outdent();
    +      },
    +      Backspace: () => {
    +        const { $from, empty } = this.editor.state.selection;
    +        if (!empty) return false;
    +        if ($from.parentOffset !== 0) return false;
    +        if (!isInIndentableBlock()) return false;
    +        if ($from.parent.attrs.indent <= this.options.min) return false;
    +        this.editor.commands.outdent();
    +        return true;
    +      },
    +    };
    +  },
    +
    +  addProseMirrorPlugins() {
    +    const types = new Set(this.options.types);
    +    const min = this.options.min;
    +
    +    return [
    +      new Plugin({
    +        key: new PluginKey('indentNormalizer'),
    +        appendTransaction: (transactions, _oldState, newState) => {
    +          if (!transactions.some((tr) => tr.docChanged)) return null;
    +
    +          const tr = newState.tr;
    +          let modified = false;
    +
    +          newState.doc.descendants((node, pos) => {
    +            // Containers: descend so we can find paragraph/heading children.
    +            if (!types.has(node.type.name)) return true;
    +
    +            if (node.attrs.indent <= min) return false;
    +
    +            if (hasNonIndentableAncestor(newState.doc, pos)) {
    +              tr.setNodeMarkup(
    +                pos,
    +                undefined,
    +                { ...node.attrs, indent: min },
    +                node.marks,
    +              );
    +              modified = true;
    +            }
    +
    +            // paragraph/heading don't contain other paragraphs/headings —
    +            // never descend into their inline content.
    +            return false;
    +          });
    +
    +          if (!modified) return null;
    +          // Normalisation must not show up as a separate undo step;
    +          // otherwise undo would re-introduce the illegal indent.
    +          return tr.setMeta('addToHistory', false);
    +        },
    +      }),
    +    ];
    +  },
    +});
    +
    +function updateIndent(
    +  state: EditorState,
    +  tr: Transaction,
    +  dispatch: ((tr: Transaction) => void) | undefined,
    +  options: IndentOptions,
    +  delta: number,
    +): boolean {
    +  const { selection } = state;
    +  const { from, to } = selection;
    +  const types = new Set(options.types);
    +  let updated = false;
    +
    +  state.doc.nodesBetween(from, to, (node, pos) => {
    +    // Skip non-block nodes (text, inline atoms) up front.
    +    if (!node.type.isBlock) return false;
    +
    +    // Don't descend into containers whose children must stay flat — handles
    +    // multi-block selections that span across e.g. a list-item or table-cell.
    +    if (NON_INDENTABLE_ANCESTORS.has(node.type.name)) return false;
    +
    +    if (!types.has(node.type.name)) return true;
    +
    +    const current = node.attrs.indent as number;
    +    const next = clampIndent(current + delta, options.min, options.max);
    +    if (next === current) return false;
    +
    +    tr.setNodeMarkup(pos, undefined, { ...node.attrs, indent: next });
    +    updated = true;
    +    return false;
    +  });
    +
    +  if (!updated) return false;
    +  if (dispatch) dispatch(tr);
    +  return true;
    +}
    diff --git a/packages/editor-ext/src/lib/markdown/utils/turndown.utils.ts b/packages/editor-ext/src/lib/markdown/utils/turndown.utils.ts
    index 635983df9..ebfc3423e 100644
    --- a/packages/editor-ext/src/lib/markdown/utils/turndown.utils.ts
    +++ b/packages/editor-ext/src/lib/markdown/utils/turndown.utils.ts
    @@ -5,6 +5,13 @@ import { getBasename } from './basename';
     // CJS/ESM interop: .default exists in Vite, not in NestJS
     const TurndownService = (_TurndownService as any).default || _TurndownService;
     
    +function sanitizeMdLinkText(value: string): string {
    +  return value
    +    .replace(/\\/g, '\\\\')
    +    .replace(/([\[\]!])/g, '\\$1')
    +    .replace(/[\r\n]+/g, ' ');
    +}
    +
     export function htmlToMarkdown(html: string): string {
       const turndownService = new TurndownService({
         headingStyle: 'atx',
    @@ -25,6 +32,7 @@ export function htmlToMarkdown(html: string): string {
         mathInline,
         mathBlock,
         iframeEmbed,
    +    image,
         video,
       ]);
       return turndownService.turndown(html).replaceAll('
    ', ' '); @@ -181,6 +189,20 @@ function iframeEmbed(turndownService: _TurndownService) { }); } +function image(turndownService: _TurndownService) { + turndownService.addRule('image', { + filter: 'img', + replacement: function (_content: string, node: HTMLInputElement) { + const src = node.getAttribute('src') || ''; + if (!src) return ''; + const alt = sanitizeMdLinkText(node.getAttribute('alt') || ''); + const title = node.getAttribute('title') || ''; + const titlePart = title ? ' "' + title.replace(/"/g, '\\"') + '"' : ''; + return '![' + alt + '](' + src + titlePart + ')'; + }, + }); +} + function video(turndownService: _TurndownService) { turndownService.addRule('video', { filter: function (node: HTMLInputElement) { @@ -188,7 +210,10 @@ function video(turndownService: _TurndownService) { }, replacement: function (_content: string, node: HTMLInputElement) { const src = node.getAttribute('src') || ''; - const name = getBasename(src) || src; + const ariaLabel = node.getAttribute('aria-label'); + const name = sanitizeMdLinkText( + ariaLabel || getBasename(src) || src, + ); return '[' + name + '](' + src + ')'; }, }); diff --git a/packages/editor-ext/src/lib/page-break/index.ts b/packages/editor-ext/src/lib/page-break/index.ts new file mode 100644 index 000000000..701b20b78 --- /dev/null +++ b/packages/editor-ext/src/lib/page-break/index.ts @@ -0,0 +1 @@ +export * from "./page-break"; diff --git a/packages/editor-ext/src/lib/page-break/page-break.ts b/packages/editor-ext/src/lib/page-break/page-break.ts new file mode 100644 index 000000000..f8991b266 --- /dev/null +++ b/packages/editor-ext/src/lib/page-break/page-break.ts @@ -0,0 +1,60 @@ +import { mergeAttributes, Node } from "@tiptap/core"; + +export interface PageBreakOptions { + HTMLAttributes: Record; +} + +declare module "@tiptap/core" { + interface Commands { + pageBreak: { + setPageBreak: () => ReturnType; + }; + } +} + +export const PageBreak = Node.create({ + name: "pageBreak", + + group: "block", + + atom: true, + + selectable: true, + + addOptions() { + return { + HTMLAttributes: {}, + }; + }, + + parseHTML() { + return [ + { + tag: `div[data-type="${this.name}"]`, + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + "div", + mergeAttributes( + { "data-type": this.name, class: "page-break" }, + this.options.HTMLAttributes, + HTMLAttributes, + ), + ]; + }, + + addCommands() { + return { + setPageBreak: + () => + ({ chain }) => + chain() + .insertContent({ type: this.name }) + .focus() + .run(), + }; + }, +}); diff --git a/packages/editor-ext/src/lib/table/cell.ts b/packages/editor-ext/src/lib/table/cell.ts index 2f693573f..9101e1614 100644 --- a/packages/editor-ext/src/lib/table/cell.ts +++ b/packages/editor-ext/src/lib/table/cell.ts @@ -3,7 +3,7 @@ import { TableCell as TiptapTableCell } from "@tiptap/extension-table"; export const TableCell = TiptapTableCell.extend({ name: "tableCell", content: - "(paragraph | heading | bulletList | orderedList | taskList | blockquote | callout | image | video | attachment | mathBlock | details | codeBlock)+", + "(paragraph | heading | bulletList | orderedList | taskList | blockquote | callout | image | video | audio | subpages | attachment | mathBlock | details | codeBlock)+", addAttributes() { return { diff --git a/packages/editor-ext/src/lib/table/dnd/auto-scroll-controller.ts b/packages/editor-ext/src/lib/table/dnd/auto-scroll-controller.ts deleted file mode 100644 index 9b8304d54..000000000 --- a/packages/editor-ext/src/lib/table/dnd/auto-scroll-controller.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { DraggingDOMs } from "./utils"; - -const EDGE_THRESHOLD = 100; -const SCROLL_SPEED = 10; - -export class AutoScrollController { - private _autoScrollInterval?: number; - - checkYAutoScroll = (clientY: number) => { - const scrollContainer = document.documentElement; - - if (clientY < 0 + EDGE_THRESHOLD) { - this._startYAutoScroll(scrollContainer!, -1 * SCROLL_SPEED); - } else if (clientY > window.innerHeight - EDGE_THRESHOLD) { - this._startYAutoScroll(scrollContainer!, SCROLL_SPEED); - } else { - this._stopYAutoScroll(); - } - } - - checkXAutoScroll = (clientX: number, draggingDOMs: DraggingDOMs) => { - const table = draggingDOMs?.table; - if (!table) return; - - const scrollContainer = table.closest('.tableWrapper'); - const editorRect = scrollContainer.getBoundingClientRect(); - if (!scrollContainer) return; - - if (clientX < editorRect.left + EDGE_THRESHOLD) { - this._startXAutoScroll(scrollContainer!, -1 * SCROLL_SPEED); - } else if (clientX > editorRect.right - EDGE_THRESHOLD) { - this._startXAutoScroll(scrollContainer!, SCROLL_SPEED); - } else { - this._stopXAutoScroll(); - } - } - - stop = () => { - this._stopXAutoScroll(); - this._stopYAutoScroll(); - } - - private _startXAutoScroll = (scrollContainer: HTMLElement, speed: number) => { - if (this._autoScrollInterval) { - clearInterval(this._autoScrollInterval); - } - - this._autoScrollInterval = window.setInterval(() => { - scrollContainer.scrollLeft += speed; - }, 16); - } - - private _stopXAutoScroll = () => { - if (this._autoScrollInterval) { - clearInterval(this._autoScrollInterval); - this._autoScrollInterval = undefined; - } - } - - private _startYAutoScroll = (scrollContainer: HTMLElement, speed: number) => { - if (this._autoScrollInterval) { - clearInterval(this._autoScrollInterval); - } - - this._autoScrollInterval = window.setInterval(() => { - scrollContainer.scrollTop += speed; - }, 16); - } - - private _stopYAutoScroll = () => { - if (this._autoScrollInterval) { - clearInterval(this._autoScrollInterval); - this._autoScrollInterval = undefined; - } - } -} \ No newline at end of file diff --git a/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts b/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts index 1ad57ec1f..b4ca516ae 100644 --- a/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts +++ b/packages/editor-ext/src/lib/table/dnd/dnd-extension.ts @@ -1,316 +1,393 @@ import { Editor, Extension } from "@tiptap/core"; -import { PluginKey, Plugin, PluginSpec } from "@tiptap/pm/state"; +import { PluginKey, Plugin, PluginSpec, TextSelection, Transaction } from "@tiptap/pm/state"; +import { Node as ProseMirrorNode } from "@tiptap/pm/model"; import { EditorProps, EditorView } from "@tiptap/pm/view"; +import { columnResizingPluginKey } from "@tiptap/pm/tables"; +import { cellAround } from "@tiptap/pm/tables"; import { + cellInfoFromResolvedCell, DraggingDOMs, getDndRelatedDOMs, getHoveringCell, HoveringCellInfo, } from "./utils"; import { getDragOverColumn, getDragOverRow } from "./calc-drag-over"; +import { findTable } from "../utils/query"; import { moveColumn, moveRow } from "../utils"; import { PreviewController } from "./preview/preview-controller"; import { DropIndicatorController } from "./preview/drop-indicator-controller"; -import { DragHandleController } from "./handle/drag-handle-controller"; -import { EmptyImageController } from "./handle/empty-image-controller"; -import { AutoScrollController } from "./auto-scroll-controller"; -export const TableDndKey = new PluginKey("table-drag-and-drop"); +export interface TableHandleState { + hoveringCell: HoveringCellInfo | null; + tableNode: ProseMirrorNode | null; + tablePos: number | null; + dragging: { orientation: "col" | "row"; index: number } | null; + frozen: boolean; +} -class TableDragHandlePluginSpec implements PluginSpec { +const INITIAL_STATE: TableHandleState = { + hoveringCell: null, + tableNode: null, + tablePos: null, + dragging: null, + frozen: false, +}; + +export const TableDndKey = new PluginKey("table-handles"); + +class TableHandlePluginSpec implements PluginSpec { key = TableDndKey; - props: EditorProps>; + props: EditorProps>; + + private _previewController: PreviewController; + private _dropIndicatorController: DropIndicatorController; - private _colDragHandle: HTMLElement; - private _rowDragHandle: HTMLElement; private _hoveringCell?: HoveringCellInfo; private _disposables: (() => void)[] = []; - private _draggingCoords: { x: number; y: number } = { x: 0, y: 0 }; - private _dragging = false; private _draggingDirection: "col" | "row" = "col"; private _draggingIndex = -1; private _droppingIndex = -1; - private _draggingDOMs?: DraggingDOMs | undefined; - private _startCoords: { x: number; y: number } = { x: 0, y: 0 }; - private _previewController: PreviewController; - private _dropIndicatorController: DropIndicatorController; - private _dragHandleController: DragHandleController; - private _emptyImageController: EmptyImageController; - private _autoScrollController: AutoScrollController; + private _draggingDOMs?: DraggingDOMs; + private _startCoords = { x: 0, y: 0 }; + private _dragging = false; + + state = { + init: (): TableHandleState => INITIAL_STATE, + apply: (tr: Transaction, prev: TableHandleState): TableHandleState => { + const meta = tr.getMeta(TableDndKey) as Partial | null; + if (!meta) return prev; + let changed = false; + for (const key in meta) { + if (!Object.is(prev[key as keyof TableHandleState], meta[key as keyof TableHandleState])) { + changed = true; + break; + } + } + return changed ? { ...prev, ...meta } : prev; + }, + }; constructor(public editor: Editor) { this.props = { handleDOMEvents: { - pointerover: this._pointerOver, + pointermove: this._pointerMove, + // Force-unfreeze on any pointerdown that lands on the editor. + // Mantine's `Menu.onClose` doesn't always fire on outside click + // (the dropdown vanishes visually but the callback is skipped), + // which would otherwise leave `frozen=true` permanently. + pointerdown: this._pointerDown, }, }; - this._dragHandleController = new DragHandleController(); - this._colDragHandle = this._dragHandleController.colDragHandle; - this._rowDragHandle = this._dragHandleController.rowDragHandle; - this._previewController = new PreviewController(); this._dropIndicatorController = new DropIndicatorController(); - this._emptyImageController = new EmptyImageController(); - - this._autoScrollController = new AutoScrollController(); - - this._bindDragEvents(); } view = () => { const wrapper = this.editor.options.element; - //@ts-ignore - wrapper.appendChild(this._colDragHandle); - //@ts-ignore - wrapper.appendChild(this._rowDragHandle); - //@ts-ignore + // @ts-ignore wrapper.appendChild(this._previewController.previewRoot); - //@ts-ignore + // @ts-ignore wrapper.appendChild(this._dropIndicatorController.dropIndicatorRoot); + // Track the cursor cell so handles follow keyboard nav and clicks too. + this.editor.on("selectionUpdate", this._onSelectionUpdate); + this._disposables.push(() => + this.editor.off("selectionUpdate", this._onSelectionUpdate), + ); + return { - update: this.update, destroy: this.destroy, }; }; - update = () => {}; - destroy = () => { - if (!this.editor.isDestroyed) return; - this._dragHandleController.destroy(); - this._emptyImageController.destroy(); this._previewController.destroy(); this._dropIndicatorController.destroy(); - this._autoScrollController.stop(); - - this._disposables.forEach((disposable) => disposable()); + this._disposables.forEach((d) => d()); }; - private _pointerOver = (view: EditorView, event: PointerEvent) => { - if (this._dragging) return; + private _pointerDown = (view: EditorView, _event: PointerEvent): boolean => { + const current = TableDndKey.getState(view.state); + if (current?.frozen) this.editor.commands.unfreezeHandles(); + return false; + }; + + private _pointerMove = (view: EditorView, event: PointerEvent) => { + const current = TableDndKey.getState(view.state); + if (current?.frozen || current?.dragging) return; + + const resizeState = columnResizingPluginKey.getState(view.state); + if (resizeState?.dragging) return; - // Don't show drag handles in readonly mode if (!this.editor.isEditable) { - this._dragHandleController.hide(); + if (current?.hoveringCell == null && current?.tableNode == null && current?.tablePos == null) return; + this._dispatchMeta({ hoveringCell: null, tableNode: null, tablePos: null }); return; } const hoveringCell = getHoveringCell(view, event); - this._hoveringCell = hoveringCell; - if (!hoveringCell) { - this._dragHandleController.hide(); - } else { - this._dragHandleController.show(this.editor, hoveringCell); + if (hoveringCell) { + if (current?.hoveringCell?.cellPos === hoveringCell.cellPos) return; + this._hoveringCell = hoveringCell; + const $cell = view.state.doc.resolve(hoveringCell.cellPos); + const tableInfo = findTable($cell); + this._dispatchMeta({ + hoveringCell, + tableNode: tableInfo?.node ?? null, + tablePos: tableInfo?.pos ?? null, + }); + return; } + + // Pointer isn't over a cell but may be transiting toward a handle that + // floats outside the cell — fall back to the selection's cell so the + // handles stay visible. + const $cellPos = cellAround(view.state.selection.$head); + if ($cellPos) { + const cellInfo = cellInfoFromResolvedCell($cellPos); + if (current?.hoveringCell?.cellPos === cellInfo.cellPos) return; + this._hoveringCell = cellInfo; + const tableInfo = findTable($cellPos); + this._dispatchMeta({ + hoveringCell: cellInfo, + tableNode: tableInfo?.node ?? null, + tablePos: tableInfo?.pos ?? null, + }); + return; + } + + this._hoveringCell = undefined; + if (current?.hoveringCell == null && current?.tableNode == null && current?.tablePos == null) return; + this._dispatchMeta({ hoveringCell: null, tableNode: null, tablePos: null }); }; - private _onDragColStart = (event: DragEvent) => { - this._onDragStart(event, "col"); + private _onSelectionUpdate = () => { + if (!this.editor.isEditable) return; + + const current = TableDndKey.getState(this.editor.state); + if (current?.frozen || current?.dragging) return; + + const $cellPos = cellAround(this.editor.state.selection.$head); + if (!$cellPos) return; + + const cellInfo = cellInfoFromResolvedCell($cellPos); + if (current?.hoveringCell?.cellPos === cellInfo.cellPos) return; + + this._hoveringCell = cellInfo; + const tableInfo = findTable($cellPos); + this._dispatchMeta({ + hoveringCell: cellInfo, + tableNode: tableInfo?.node ?? null, + tablePos: tableInfo?.pos ?? null, + }); }; - private _onDraggingCol = (event: DragEvent) => { + private _dispatchMeta = (patch: Partial) => { + const tr = this.editor.state.tr.setMeta(TableDndKey, patch); + tr.setMeta("addToHistory", false); + this.editor.view.dispatch(tr); + }; + + // ---- Public API for the React handle layer ---- + + // Returns true if the drag was set up successfully. + startDragFromHandle = ( + orientation: "col" | "row", + clientX: number, + clientY: number, + ): boolean => { + if (!this._hoveringCell) return false; + this._dragging = true; + this._draggingDirection = orientation; + this._startCoords = { x: clientX, y: clientY }; + + const draggingIndex = + (orientation === "col" + ? this._hoveringCell.colIndex + : this._hoveringCell.rowIndex) ?? 0; + this._draggingIndex = draggingIndex; + + const relatedDoms = getDndRelatedDOMs( + this.editor.view, + this._hoveringCell.cellPos, + draggingIndex, + orientation, + ); + if (!relatedDoms) { + this._dragging = false; + return false; + } + this._draggingDOMs = relatedDoms; + + this._previewController.onDragStart(relatedDoms, draggingIndex, orientation); + this._dropIndicatorController.onDragStart(relatedDoms, orientation); + + // Park the selection inside the dragged cell unless it's already in the + // same table. PM auto-maps `selection.from` through concurrent remote + // transactions, so commitDrop can resolve the table even if the doc + // shifted mid-drag — same trick the pre-pragmatic-dnd implementation + // relied on. + const state = this.editor.state; + const currentTable = findTable(state.selection.$from); + const hoverTable = (() => { + try { + return findTable(state.doc.resolve(this._hoveringCell.cellPos)); + } catch { + return undefined; + } + })(); + const tr = state.tr; + if ( + hoverTable && + (!currentTable || currentTable.pos !== hoverTable.pos) + ) { + try { + const $inside = state.doc.resolve(this._hoveringCell.cellPos + 1); + tr.setSelection(TextSelection.near($inside, 1)); + } catch {} + } + tr.setMeta(TableDndKey, { + dragging: { orientation, index: draggingIndex }, + }); + tr.setMeta("addToHistory", false); + this.editor.view.dispatch(tr); + return true; + }; + + updateDragPosition = (clientX: number, clientY: number) => { const draggingDOMs = this._draggingDOMs; - if (!draggingDOMs) return; + if (!draggingDOMs || !this._dragging) return; - this._draggingCoords = { x: event.clientX, y: event.clientY }; - this._previewController.onDragging( - draggingDOMs, - this._draggingCoords.x, - this._draggingCoords.y, - "col", - ); + if (this._draggingDirection === "col") { + this._previewController.onDragging( + draggingDOMs, + clientX, + clientY, + "col", + ); + const direction = this._startCoords.x > clientX ? "left" : "right"; + const dragOverColumn = getDragOverColumn(draggingDOMs.table, clientX); + if (!dragOverColumn) return; + const [col, index] = dragOverColumn; + this._droppingIndex = index; + this._dropIndicatorController.onDragging(col, direction, "col"); + return; + } - this._autoScrollController.checkXAutoScroll(event.clientX, draggingDOMs); - - const direction = - this._startCoords.x > this._draggingCoords.x ? "left" : "right"; - const dragOverColumn = getDragOverColumn( - draggingDOMs.table, - this._draggingCoords.x, - ); - if (!dragOverColumn) return; - - const [col, index] = dragOverColumn; - this._droppingIndex = index; - this._dropIndicatorController.onDragging(col, direction, "col"); - }; - - private _onDragRowStart = (event: DragEvent) => { - this._onDragStart(event, "row"); - }; - - private _onDraggingRow = (event: DragEvent) => { - const draggingDOMs = this._draggingDOMs; - if (!draggingDOMs) return; - - this._draggingCoords = { x: event.clientX, y: event.clientY }; - this._previewController.onDragging( - draggingDOMs, - this._draggingCoords.x, - this._draggingCoords.y, - "row", - ); - - this._autoScrollController.checkYAutoScroll(event.clientY); - - const direction = - this._startCoords.y > this._draggingCoords.y ? "up" : "down"; - const dragOverRow = getDragOverRow( - draggingDOMs.table, - this._draggingCoords.y, - ); + this._previewController.onDragging(draggingDOMs, clientX, clientY, "row"); + const direction = this._startCoords.y > clientY ? "up" : "down"; + const dragOverRow = getDragOverRow(draggingDOMs.table, clientY); if (!dragOverRow) return; - const [row, index] = dragOverRow; this._droppingIndex = index; this._dropIndicatorController.onDragging(row, direction, "row"); }; - private _onDragEnd = () => { - this._dragging = false; - this._draggingIndex = -1; - this._droppingIndex = -1; - this._startCoords = { x: 0, y: 0 }; - this._autoScrollController.stop(); - this._dropIndicatorController.onDragEnd(); - this._previewController.onDragEnd(); - }; - - private _bindDragEvents = () => { - this._colDragHandle.addEventListener("dragstart", this._onDragColStart); - this._disposables.push(() => { - this._colDragHandle.removeEventListener( - "dragstart", - this._onDragColStart, - ); - }); - - this._colDragHandle.addEventListener("dragend", this._onDragEnd); - this._disposables.push(() => { - this._colDragHandle.removeEventListener("dragend", this._onDragEnd); - }); - - this._rowDragHandle.addEventListener("dragstart", this._onDragRowStart); - this._disposables.push(() => { - this._rowDragHandle.removeEventListener( - "dragstart", - this._onDragRowStart, - ); - }); - - this._rowDragHandle.addEventListener("dragend", this._onDragEnd); - this._disposables.push(() => { - this._rowDragHandle.removeEventListener("dragend", this._onDragEnd); - }); - - const ownerDocument = this.editor.view.dom?.ownerDocument; - if (ownerDocument) { - // To make `drop` event work, we need to prevent the default behavior of the - // `dragover` event for drop zone. Here we set the whole document as the - // drop zone so that even the mouse moves outside the editor, the `drop` - // event will still be triggered. - ownerDocument.addEventListener("drop", this._onDrop); - ownerDocument.addEventListener("dragover", this._onDrag); - this._disposables.push(() => { - ownerDocument.removeEventListener("drop", this._onDrop); - ownerDocument.removeEventListener("dragover", this._onDrag); - }); - } - }; - - private _onDragStart = (event: DragEvent, type: "col" | "row") => { - const dataTransfer = event.dataTransfer; - if (dataTransfer) { - dataTransfer.effectAllowed = "move"; - this._emptyImageController.hideDragImage(dataTransfer); - } - this._dragging = true; - this._draggingDirection = type; - this._startCoords = { x: event.clientX, y: event.clientY }; - const draggingIndex = - (type === "col" - ? this._hoveringCell?.colIndex - : this._hoveringCell?.rowIndex) ?? 0; - - this._draggingIndex = draggingIndex; - - const relatedDoms = getDndRelatedDOMs( - this.editor.view, - this._hoveringCell?.cellPos, - draggingIndex, - type, - ); - this._draggingDOMs = relatedDoms; - - const index = - type === "col" - ? this._hoveringCell?.colIndex - : this._hoveringCell?.rowIndex; - - this._previewController.onDragStart(relatedDoms, index, type); - this._dropIndicatorController.onDragStart(relatedDoms, type); - }; - - private _onDrag = (event: DragEvent) => { - event.preventDefault(); - if (!this._dragging) return; - if (this._draggingDirection === "col") { - this._onDraggingCol(event); - } else { - this._onDraggingRow(event); - } - }; - - private _onDrop = () => { + commitDrop = () => { if (!this._dragging) return; const direction = this._draggingDirection; const from = this._draggingIndex; const to = this._droppingIndex; + + if (from < 0 || to < 0 || from === to) return; + + // Use the live (auto-mapped) selection as the table anchor — PM has + // already mapped it through any concurrent remote transactions, so + // it's safe to resolve even if the doc shifted mid-drag. const tr = this.editor.state.tr; const pos = this.editor.state.selection.from; if (direction === "col") { - const canMove = moveColumn({ - tr, - originIndex: from, - targetIndex: to, - select: true, - pos, - }); - if (canMove) { + if (moveColumn({ tr, originIndex: from, targetIndex: to, select: true, pos })) { this.editor.view.dispatch(tr); } - return; } - - if (direction === "row") { - const canMove = moveRow({ - tr, - originIndex: from, - targetIndex: to, - select: true, - pos, - }); - if (canMove) { - this.editor.view.dispatch(tr); - } - - return; + if (moveRow({ tr, originIndex: from, targetIndex: to, select: true, pos })) { + this.editor.view.dispatch(tr); } }; + + endDrag = () => { + this._dragging = false; + this._draggingIndex = -1; + this._droppingIndex = -1; + this._startCoords = { x: 0, y: 0 }; + this._draggingDOMs = undefined; + this._dropIndicatorController.onDragEnd(); + this._previewController.onDragEnd(); + this._dispatchMeta({ dragging: null }); + }; +} + +export type { TableHandlePluginSpec }; + +// Resolve via plugin key, not a module singleton — survives StrictMode / HMR. +export function getTableHandlePluginSpec( + editor: Editor, +): TableHandlePluginSpec | null { + const plugin = TableDndKey.get(editor.state); + if (!plugin) return null; + return plugin.spec as unknown as TableHandlePluginSpec; } export const TableDndExtension = Extension.create({ name: "table-drag-and-drop", addProseMirrorPlugins() { const editor = this.editor; - - const dragHandlePluginSpec = new TableDragHandlePluginSpec(editor); - const dragHandlePlugin = new Plugin(dragHandlePluginSpec); - - return [dragHandlePlugin]; + const spec = new TableHandlePluginSpec(editor); + return [new Plugin(spec)]; }, }); + +export const TableHandleCommandsExtension = Extension.create({ + name: "table-handle-commands", + addCommands() { + return { + freezeHandles: + () => + ({ tr, dispatch }) => { + if (dispatch) { + tr.setMeta(TableDndKey, { frozen: true }); + tr.setMeta("addToHistory", false); + } + return true; + }, + unfreezeHandles: + () => + ({ tr, state, dispatch }) => { + if (dispatch) { + // Re-sync `hoveringCell` to the cursor's cell as we unfreeze: + // `selectionUpdate` was gated while frozen, so the stored + // hoveringCell may be stale. + const patch: Partial = { frozen: false }; + const $cellPos = cellAround(state.selection.$head); + if ($cellPos) { + const cellInfo = cellInfoFromResolvedCell($cellPos); + const tableInfo = findTable($cellPos); + patch.hoveringCell = cellInfo; + patch.tableNode = tableInfo?.node ?? null; + patch.tablePos = tableInfo?.pos ?? null; + } else { + patch.hoveringCell = null; + patch.tableNode = null; + patch.tablePos = null; + } + tr.setMeta(TableDndKey, patch); + tr.setMeta("addToHistory", false); + } + return true; + }, + }; + }, +}); + +declare module "@tiptap/core" { + interface Commands { + tableHandleCommands: { + freezeHandles: () => ReturnType; + unfreezeHandles: () => ReturnType; + }; + } +} diff --git a/packages/editor-ext/src/lib/table/dnd/handle/drag-handle-controller.ts b/packages/editor-ext/src/lib/table/dnd/handle/drag-handle-controller.ts deleted file mode 100644 index 33137e91f..000000000 --- a/packages/editor-ext/src/lib/table/dnd/handle/drag-handle-controller.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Editor } from "@tiptap/core"; -import { HoveringCellInfo } from "../utils"; -import { computePosition, offset } from "@floating-ui/dom"; - -export class DragHandleController { - private _colDragHandle: HTMLElement; - private _rowDragHandle: HTMLElement; - - constructor() { - this._colDragHandle = this._createDragHandleDom('col'); - this._rowDragHandle = this._createDragHandleDom('row'); - } - - get colDragHandle() { - return this._colDragHandle; - } - - get rowDragHandle() { - return this._rowDragHandle; - } - - show = (editor: Editor, hoveringCell: HoveringCellInfo) => { - this._showColDragHandle(editor, hoveringCell); - this._showRowDragHandle(editor, hoveringCell); - } - - hide = () => { - Object.assign(this._colDragHandle.style, { - display: 'none', - left: '-999px', - top: '-999px', - }); - Object.assign(this._rowDragHandle.style, { - display: 'none', - left: '-999px', - top: '-999px', - }); - } - - destroy = () => { - this._colDragHandle.remove() - this._rowDragHandle.remove() - } - - private _createDragHandleDom = (type: 'col' | 'row') => { - const dragHandle = document.createElement('div') - dragHandle.classList.add('drag-handle') - dragHandle.setAttribute('draggable', 'true') - dragHandle.setAttribute('data-direction', type === 'col' ? 'horizontal' : 'vertical') - dragHandle.setAttribute('data-drag-handle', '') - Object.assign(dragHandle.style, { - position: 'absolute', - top: '-999px', - left: '-999px', - display: 'none', - }) - return dragHandle; - } - - private _showColDragHandle(editor: Editor, hoveringCell: HoveringCellInfo) { - const referenceCell = editor.view.nodeDOM(hoveringCell.colFirstCellPos); - if (!referenceCell) return; - - const yOffset = -1 * parseInt(getComputedStyle(this._colDragHandle).height) / 2; - - computePosition( - referenceCell as HTMLElement, - this._colDragHandle, - { - placement: 'top', - middleware: [offset(yOffset)] - } - ) - .then(({ x, y }) => { - Object.assign(this._colDragHandle.style, { - display: 'block', - top: `${y}px`, - left: `${x}px`, - }); - }) - } - - private _showRowDragHandle(editor: Editor, hoveringCell: HoveringCellInfo) { - const referenceCell = editor.view.nodeDOM(hoveringCell.rowFirstCellPos); - if (!referenceCell) return; - - const xOffset = -1 * parseInt(getComputedStyle(this._rowDragHandle).width) / 2; - - computePosition( - referenceCell as HTMLElement, - this._rowDragHandle, - { - middleware: [offset(xOffset)], - placement: 'left' - } - ) - .then(({ x, y}) => { - Object.assign(this._rowDragHandle.style, { - display: 'block', - top: `${y}px`, - left: `${x}px`, - }); - }) - } -} \ No newline at end of file diff --git a/packages/editor-ext/src/lib/table/dnd/handle/empty-image-controller.ts b/packages/editor-ext/src/lib/table/dnd/handle/empty-image-controller.ts deleted file mode 100644 index 8848a6b04..000000000 --- a/packages/editor-ext/src/lib/table/dnd/handle/empty-image-controller.ts +++ /dev/null @@ -1,21 +0,0 @@ -export class EmptyImageController { - private _emptyImage: HTMLImageElement; - - constructor() { - this._emptyImage = new Image(1, 1); - this._emptyImage.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; - } - - get emptyImage() { - return this._emptyImage; - } - - hideDragImage = (dataTransfer: DataTransfer) => { - dataTransfer.effectAllowed = 'move'; - dataTransfer.setDragImage(this._emptyImage, 0, 0); - } - - destroy = () => { - this._emptyImage.remove(); - } -} \ No newline at end of file diff --git a/packages/editor-ext/src/lib/table/dnd/index.ts b/packages/editor-ext/src/lib/table/dnd/index.ts index cb21bec14..eaeade987 100644 --- a/packages/editor-ext/src/lib/table/dnd/index.ts +++ b/packages/editor-ext/src/lib/table/dnd/index.ts @@ -1 +1,7 @@ -export * from './dnd-extension' \ No newline at end of file +export { + TableDndExtension, + TableHandleCommandsExtension, + TableDndKey, + getTableHandlePluginSpec, +} from "./dnd-extension"; +export type { TableHandleState, TableHandlePluginSpec } from "./dnd-extension"; diff --git a/packages/editor-ext/src/lib/table/dnd/preview/drop-indicator-controller.ts b/packages/editor-ext/src/lib/table/dnd/preview/drop-indicator-controller.ts index 0f0798282..a42c632f7 100644 --- a/packages/editor-ext/src/lib/table/dnd/preview/drop-indicator-controller.ts +++ b/packages/editor-ext/src/lib/table/dnd/preview/drop-indicator-controller.ts @@ -99,4 +99,4 @@ export class DropIndicatorController { }); } -} \ No newline at end of file +} diff --git a/packages/editor-ext/src/lib/table/dnd/preview/preview-controller.ts b/packages/editor-ext/src/lib/table/dnd/preview/preview-controller.ts index b7a0ea40d..9884f00f6 100644 --- a/packages/editor-ext/src/lib/table/dnd/preview/preview-controller.ts +++ b/packages/editor-ext/src/lib/table/dnd/preview/preview-controller.ts @@ -1,4 +1,4 @@ -import { computePosition, offset, ReferenceElement } from "@floating-ui/dom"; +import { computePosition, offset, shift, ReferenceElement } from "@floating-ui/dom"; import { DraggingDOMs } from "../utils"; import { clearPreviewDOM, createPreviewDOM } from "./render-preview"; @@ -23,7 +23,7 @@ export class PreviewController { onDragStart = (relatedDoms: DraggingDOMs, index: number | undefined, type: 'col' | 'row') => { this._initPreviewStyle(relatedDoms.table, relatedDoms.cell, type); createPreviewDOM(relatedDoms.table, this._preview, index, type) - this._initPreviewPosition(relatedDoms.cell, type); + this._initPreviewPosition(relatedDoms.table, relatedDoms.cell, type); } onDragEnd = () => { @@ -32,7 +32,7 @@ export class PreviewController { } onDragging = (relatedDoms: DraggingDOMs, x: number, y: number, type: 'col' | 'row') => { - this._updatePreviewPosition(x, y, relatedDoms.cell, type); + this._updatePreviewPosition(x, y, relatedDoms.table, relatedDoms.cell, type); } destroy = () => { @@ -60,7 +60,7 @@ export class PreviewController { } } - private _initPreviewPosition(cell: HTMLElement, type: 'col' | 'row') { + private _initPreviewPosition(table: HTMLElement, cell: HTMLElement, type: 'col' | 'row') { void computePosition(cell, this._preview, { placement: type === 'row' ? 'right' : 'bottom', middleware: [ @@ -70,6 +70,7 @@ export class PreviewController { } return -rects.reference.width }), + shift({ boundary: table, padding: 0 }), ], }).then(({ x, y }) => { Object.assign(this._preview.style, { @@ -79,11 +80,20 @@ export class PreviewController { }); } - private _updatePreviewPosition(x: number, y: number, cell: HTMLElement, type: 'col' | 'row') { + // Clamp the preview to within the table's bounds via `shift({ boundary })` + // so it can't track the cursor past the table edge. Without the clamp, + // dragging near the viewport edge pushes the preview's `left` (or `top`) + // beyond the document's natural width/height, the browser extends the + // page to contain it, and the auto-scroll plugin then has a wider area + // to keep scrolling into — a feedback loop that grows the page forever. + private _updatePreviewPosition(x: number, y: number, table: HTMLElement, cell: HTMLElement, type: 'col' | 'row') { computePosition( getVirtualElement(cell, x, y), this._preview, - { placement: type === 'row' ? 'right' : 'bottom' }, + { + placement: type === 'row' ? 'right' : 'bottom', + middleware: [shift({ boundary: table, padding: 0 })], + }, ).then(({ x, y }) => { if (type === 'row') { Object.assign(this._preview.style, { diff --git a/packages/editor-ext/src/lib/table/dnd/utils.ts b/packages/editor-ext/src/lib/table/dnd/utils.ts index d184368f4..9b00769d3 100644 --- a/packages/editor-ext/src/lib/table/dnd/utils.ts +++ b/packages/editor-ext/src/lib/table/dnd/utils.ts @@ -1,4 +1,5 @@ import { cellAround, TableMap } from "@tiptap/pm/tables" +import { ResolvedPos } from "@tiptap/pm/model" import { EditorView } from "@tiptap/pm/view" export function getHoveringCell( @@ -8,19 +9,30 @@ export function getHoveringCell( const domCell = domCellAround(event.target as HTMLElement | null) if (!domCell) return - const { left, top, width, height } = domCell.getBoundingClientRect() - const eventPos = view.posAtCoords({ - // Use the center coordinates of the cell to ensure we're within the - // selected cell. This prevents potential issues when the mouse is on the - // border of two cells. - left: left + width / 2, - top: top + height / 2, - }) - if (!eventPos) return - - const $cellPos = cellAround(view.state.doc.resolve(eventPos.pos)) + // Resolve directly from the cell DOM rather than via coords. The previous + // center-coords approach broke on tall merged cells — their visual center + // can land in empty space whose closest PM position resolves to an + // adjacent cell. `posAtDOM(td, 0)` is always inside this cell, regardless + // of rowspan/colspan. + let pos: number + try { + pos = view.posAtDOM(domCell, 0) + } catch { + return + } + const $cellPos = cellAround(view.state.doc.resolve(pos)) if (!$cellPos) return + return cellInfoFromResolvedCell($cellPos) +} + +/** + * Build HoveringCellInfo from a resolved position whose parent is a + * table cell (i.e. the result of `cellAround` on some inner position). + */ +export function cellInfoFromResolvedCell( + $cellPos: ResolvedPos, +): HoveringCellInfo { const map = TableMap.get($cellPos.node(-1)) const tableStart = $cellPos.start(-1) const cellRect = map.findCell($cellPos.pos - tableStart) diff --git a/packages/editor-ext/src/lib/table/header-pin/controller.ts b/packages/editor-ext/src/lib/table/header-pin/controller.ts new file mode 100644 index 000000000..318d4145d --- /dev/null +++ b/packages/editor-ext/src/lib/table/header-pin/controller.ts @@ -0,0 +1,186 @@ +// Per-table header-pin controller: native sticky when table fits its wrapper, transform fallback when it doesn't. + +import { computePinTop, pinOffsetWatcher } from './offset'; + +const WRAPPER_NO_OVERFLOW = 'tableWrapperNoOverflow'; +const HEADER_PINNED = 'tableHeaderPinned'; +const PIN_OFFSET_VAR = '--table-pin-offset'; + +type PinMode = 'off' | 'native' | 'fallback'; + +function firstRowIsAllHeaders(row: HTMLTableRowElement | null): boolean { + if (!row) return false; + const cells = Array.from(row.cells); + return cells.length > 0 && cells.every((c) => c.tagName === 'TH'); +} + +function isNestedTable(wrapper: HTMLElement): boolean { + return wrapper.closest('table .tableWrapper') !== null; +} + +function isLayoutInert(rect: DOMRectReadOnly): boolean { + return rect.width === 0 && rect.height === 0; +} + +const fallbackControllers = new Set(); +let fallbackScrollListener: (() => void) | null = null; +let fallbackRafPending = false; + +function ensureFallbackListener() { + if (fallbackScrollListener) return; + fallbackScrollListener = () => { + if (fallbackRafPending) return; + fallbackRafPending = true; + requestAnimationFrame(() => { + fallbackRafPending = false; + for (const ctrl of fallbackControllers) ctrl.updateFallbackOffset(); + }); + }; + document.addEventListener('scroll', fallbackScrollListener, { + passive: true, + capture: true, + }); +} + +function maybeTeardownFallbackListener() { + if (!fallbackScrollListener || fallbackControllers.size > 0) return; + document.removeEventListener('scroll', fallbackScrollListener, { + capture: true, + }); + fallbackScrollListener = null; + fallbackRafPending = false; +} + +export class TablePinController { + private wrapper: HTMLElement; + private table: HTMLTableElement; + private fitsObserver?: IntersectionObserver; + private mode: PinMode = 'off'; + private cachedHeaderRow: HTMLTableRowElement | null = null; + + constructor(wrapper: HTMLElement, table: HTMLTableElement) { + this.wrapper = wrapper; + this.table = table; + pinOffsetWatcher.acquire(); + this.fitsObserver = new IntersectionObserver( + (entries) => { + for (const entry of entries) this.evaluateFit(entry); + }, + { root: this.wrapper, threshold: 1 }, + ); + this.fitsObserver.observe(this.table); + } + + private getHeaderRow(): HTMLTableRowElement | null { + if (this.cachedHeaderRow && this.table.contains(this.cachedHeaderRow)) { + return this.cachedHeaderRow; + } + this.cachedHeaderRow = this.table.querySelector('tr'); + return this.cachedHeaderRow; + } + + private evaluateFit(entry: IntersectionObserverEntry) { + if (!this.isEligible()) { + this.apply('off'); + return; + } + if (isLayoutInert(entry.boundingClientRect)) return; + this.apply(entry.isIntersecting ? 'native' : 'fallback'); + } + + private isEligible(): boolean { + return ( + !isNestedTable(this.wrapper) && firstRowIsAllHeaders(this.getHeaderRow()) + ); + } + + private apply(next: PinMode) { + if (next === this.mode) return; + + if (this.mode === 'fallback' && next !== 'fallback') { + fallbackControllers.delete(this); + maybeTeardownFallbackListener(); + } + + this.mode = next; + const cls = this.wrapper.classList; + + if (next === 'off') { + cls.remove(HEADER_PINNED); + cls.remove(WRAPPER_NO_OVERFLOW); + this.wrapper.style.removeProperty(PIN_OFFSET_VAR); + } else if (next === 'native') { + cls.add(HEADER_PINNED); + cls.add(WRAPPER_NO_OVERFLOW); + // Native mode reads --editor-pin-offset from :root; clear stale per-wrapper var from fallback. + this.wrapper.style.removeProperty(PIN_OFFSET_VAR); + } else if (next === 'fallback') { + cls.add(HEADER_PINNED); + cls.remove(WRAPPER_NO_OVERFLOW); + fallbackControllers.add(this); + ensureFallbackListener(); + // Avoid one stale-frame paint under translateY. + this.updateFallbackOffset(); + } + } + + updateFallbackOffset() { + const pinTop = computePinTop(); + const tableRect = this.table.getBoundingClientRect(); + const headerRow = this.getHeaderRow(); + if (!headerRow) return; + const rowHeight = headerRow.getBoundingClientRect().height; + + const active = tableRect.top < pinTop && tableRect.bottom > pinTop + rowHeight; + + if (active) { + const offset = Math.min(pinTop - tableRect.top, tableRect.height - rowHeight); + this.wrapper.style.setProperty(PIN_OFFSET_VAR, `${offset}px`); + } else { + this.wrapper.style.removeProperty(PIN_OFFSET_VAR); + } + } + + refresh() { + // The header
    may have been replaced by a PM transaction; drop + // the cached reference before checking eligibility. + this.cachedHeaderRow = null; + if (!this.isEligible()) { + this.apply('off'); + return; + } + if (this.mode === 'off') { + // Eligibility just flipped back on; re-trigger the observer so it + // emits the current intersection state. + this.fitsObserver?.unobserve(this.table); + this.fitsObserver?.observe(this.table); + } + } + + destroy() { + this.fitsObserver?.disconnect(); + this.fitsObserver = undefined; + this.apply('off'); + pinOffsetWatcher.release(); + } +} + +const controllers = new WeakMap(); + +export function attach(wrapper: HTMLElement) { + if (controllers.has(wrapper)) return; + const table = wrapper.querySelector(':scope > table') as HTMLTableElement | null; + if (!table) return; + controllers.set(wrapper, new TablePinController(wrapper, table)); +} + +export function detach(wrapper: HTMLElement) { + const ctrl = controllers.get(wrapper); + if (!ctrl) return; + ctrl.destroy(); + controllers.delete(wrapper); +} + +export function getController(wrapper: HTMLElement): TablePinController | undefined { + return controllers.get(wrapper); +} diff --git a/packages/editor-ext/src/lib/table/header-pin/extension.ts b/packages/editor-ext/src/lib/table/header-pin/extension.ts new file mode 100644 index 000000000..8e5157ede --- /dev/null +++ b/packages/editor-ext/src/lib/table/header-pin/extension.ts @@ -0,0 +1,78 @@ +import { Extension } from '@tiptap/core'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; + +import { attach, detach, getController } from './controller'; + +const tableHeaderPinKey = new PluginKey('tableHeaderPin'); + +export const TableHeaderPin = Extension.create({ + name: 'tableHeaderPin', + + addProseMirrorPlugins() { + let editorRoot: HTMLElement | null = null; + let domObserver: MutationObserver | null = null; + const tracked = new Set(); + let rafHandle: number | null = null; + + const reconcile = () => { + rafHandle = null; + if (!editorRoot) return; + const current = new Set( + editorRoot.querySelectorAll('.tableWrapper'), + ); + for (const w of tracked) { + if (!current.has(w)) { + detach(w); + tracked.delete(w); + } + } + for (const w of current) { + if (!tracked.has(w)) { + attach(w); + tracked.add(w); + } + } + }; + + const schedule = () => { + if (rafHandle !== null) return; + rafHandle = requestAnimationFrame(reconcile); + }; + + return [ + new Plugin({ + key: tableHeaderPinKey, + + view(editorView) { + editorRoot = editorView.dom as HTMLElement; + + schedule(); + + domObserver = new MutationObserver(schedule); + domObserver.observe(editorRoot, { subtree: true, childList: true }); + + return { + update(view, prevState) { + if (!editorRoot) return; + if (view.state.doc === prevState.doc) return; + editorRoot + .querySelectorAll('.tableWrapper') + .forEach((w) => getController(w)?.refresh()); + }, + destroy() { + if (rafHandle !== null) { + cancelAnimationFrame(rafHandle); + rafHandle = null; + } + domObserver?.disconnect(); + domObserver = null; + for (const w of tracked) detach(w); + tracked.clear(); + editorRoot = null; + }, + }; + }, + }), + ]; + }, +}); diff --git a/packages/editor-ext/src/lib/table/header-pin/index.ts b/packages/editor-ext/src/lib/table/header-pin/index.ts new file mode 100644 index 000000000..b45e01aee --- /dev/null +++ b/packages/editor-ext/src/lib/table/header-pin/index.ts @@ -0,0 +1 @@ +export { TableHeaderPin } from './extension'; diff --git a/packages/editor-ext/src/lib/table/header-pin/offset.ts b/packages/editor-ext/src/lib/table/header-pin/offset.ts new file mode 100644 index 000000000..89cc6bf9e --- /dev/null +++ b/packages/editor-ext/src/lib/table/header-pin/offset.ts @@ -0,0 +1,65 @@ +// Pin-offset measurement and watcher used by the table header-pin controller. + +// Fallback app-bar height (px) when no fixed surface is mounted; matches global-app-shell.tsx. +const APP_BAR_FALLBACK_HEIGHT = 45; + +export const EDITOR_PIN_OFFSET_VAR = '--editor-pin-offset'; + +// Selectors for fixed surfaces between viewport top and editor content. Use data attributes — +// CSS module classes are build-time hashed and won't match. +const PIN_ANCHOR_SELECTORS = [ + '[data-page-header]', + '[data-fixed-toolbar]', +] as const; + +export function computePinTop(): number { + let bottom = APP_BAR_FALLBACK_HEIGHT; + for (const sel of PIN_ANCHOR_SELECTORS) { + const el = document.querySelector(sel) as HTMLElement | null; + if (!el) continue; + const rect = el.getBoundingClientRect(); + if (rect.height > 0 && rect.bottom > bottom) bottom = rect.bottom; + } + return bottom; +} + +// Reference-counted watcher that publishes the editor's top offset to a CSS custom property. +export const pinOffsetWatcher = { + refs: 0, + resizeObserver: null as ResizeObserver | null, + rafPending: false, + lastValue: -1, + + acquire() { + if (this.refs++ > 0) return; + this.publish(); + const schedule = () => { + if (this.rafPending) return; + this.rafPending = true; + requestAnimationFrame(() => { + this.rafPending = false; + this.publish(); + }); + }; + this.resizeObserver = new ResizeObserver(schedule); + this.resizeObserver.observe(document.body); + }, + + release() { + if (--this.refs > 0) return; + this.resizeObserver?.disconnect(); + this.resizeObserver = null; + document.documentElement.style.removeProperty(EDITOR_PIN_OFFSET_VAR); + this.lastValue = -1; + }, + + publish() { + const top = computePinTop(); + if (top === this.lastValue) return; + this.lastValue = top; + document.documentElement.style.setProperty( + EDITOR_PIN_OFFSET_VAR, + `${top}px`, + ); + }, +}; diff --git a/packages/editor-ext/src/lib/table/header.ts b/packages/editor-ext/src/lib/table/header.ts index 77ab02f10..7f288dde9 100644 --- a/packages/editor-ext/src/lib/table/header.ts +++ b/packages/editor-ext/src/lib/table/header.ts @@ -3,7 +3,7 @@ import { TableHeader as TiptapTableHeader } from "@tiptap/extension-table"; export const TableHeader = TiptapTableHeader.extend({ name: "tableHeader", content: - "(paragraph | heading | bulletList | orderedList | taskList | blockquote | callout | image | video | attachment | mathBlock | details | codeBlock)+", + "(paragraph | heading | bulletList | orderedList | taskList | blockquote | callout | image | video | audio | subpages | attachment | mathBlock | details | codeBlock)+", addAttributes() { return { diff --git a/packages/editor-ext/src/lib/table/index.ts b/packages/editor-ext/src/lib/table/index.ts index 9e5a92651..ed06582e3 100644 --- a/packages/editor-ext/src/lib/table/index.ts +++ b/packages/editor-ext/src/lib/table/index.ts @@ -2,4 +2,14 @@ export * from "./row"; export * from "./cell"; export * from "./header"; export * from "./table"; -export * from "./dnd"; \ No newline at end of file +export * from "./dnd"; +export * from "./table-view"; +export * from "./header-pin"; +export * from "./table-readonly-sort"; +export { moveColumn } from "./utils/move-column"; +export type { MoveColumnParams } from "./utils/move-column"; +export { moveRow } from "./utils/move-row"; +export type { MoveRowParams } from "./utils/move-row"; +export { convertTableNodeToArrayOfRows } from "./utils/convert-table-node-to-array-of-rows"; +export { convertArrayOfRowsToTableNode } from "./utils/convert-array-of-rows-to-table-node"; +export { transpose } from "./utils/transpose"; diff --git a/packages/editor-ext/src/lib/table/table-readonly-sort.ts b/packages/editor-ext/src/lib/table/table-readonly-sort.ts new file mode 100644 index 000000000..3e246a411 --- /dev/null +++ b/packages/editor-ext/src/lib/table/table-readonly-sort.ts @@ -0,0 +1,233 @@ +import { Extension } from '@tiptap/core'; +import { Plugin, PluginKey } from '@tiptap/pm/state'; + +type SortDirection = 'asc' | 'desc'; + +type SortState = { + col: number; + direction: SortDirection; +}; + +const CHEVRON_CLASS = 'tableReadonlySortChevron'; + +const tableReadonlySortKey = new PluginKey('tableReadonlySort'); + +const sortStates = new WeakMap(); +const originalOrders = new WeakMap(); + +const collator = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true }); + +function getColumnIndex(th: HTMLTableCellElement): number { + const row = th.parentElement as HTMLTableRowElement; + if (!row) return -1; + let col = 0; + for (let i = 0; i < row.cells.length; i++) { + if (row.cells[i] === th) return col; + col += row.cells[i].colSpan ?? 1; + } + return -1; +} + +function getHeaderTh(target: EventTarget | null): HTMLTableCellElement | null { + if (!(target instanceof Element)) return null; + const th = target.closest('th') as HTMLTableCellElement | null; + if (!th) return null; + const row = th.parentElement; + if (!row) return null; + const tbody = row.parentElement; + if (!tbody) return null; + const table = tbody.closest('table'); + if (!table) return null; + + // th must be in the first row of the table (could be in thead or tbody) + const firstRow = table.querySelector('tr'); + if (firstRow !== row) return null; + + return th; +} + +function getCellText(row: HTMLTableRowElement, colIndex: number): string { + let col = 0; + for (let i = 0; i < row.cells.length; i++) { + if (col === colIndex) return row.cells[i].textContent?.trim() ?? ''; + col += row.cells[i].colSpan ?? 1; + } + return ''; +} + +function getOrSaveOriginalOrder( + table: HTMLTableElement, + dataRows: HTMLTableRowElement[], +): HTMLTableRowElement[] { + if (!originalOrders.has(table)) { + originalOrders.set(table, [...dataRows]); + } + return originalOrders.get(table)!; +} + +function sortDataRows( + dataRows: HTMLTableRowElement[], + colIndex: number, + direction: SortDirection, +): HTMLTableRowElement[] { + return [...dataRows].sort((a, b) => { + const textA = getCellText(a, colIndex); + const textB = getCellText(b, colIndex); + const emptyA = textA === ''; + const emptyB = textB === ''; + if (emptyA && emptyB) return 0; + if (emptyA) return 1; + if (emptyB) return -1; + const cmp = collator.compare(textA, textB); + return direction === 'asc' ? cmp : -cmp; + }); +} + +function applySort(table: HTMLTableElement, colIndex: number): void { + const tbody = table.querySelector('tbody'); + if (!tbody) return; + + const allRows = Array.from(tbody.querySelectorAll(':scope > tr')); + if (allRows.length === 0) return; + + const headerRow = allRows[0]; + const dataRows = allRows.slice(1); + if (dataRows.length === 0) return; + + const current = sortStates.get(table) ?? null; + const saved = getOrSaveOriginalOrder(table, dataRows); + + let next: SortState | null; + if (!current || current.col !== colIndex) { + next = { col: colIndex, direction: 'asc' }; + } else if (current.direction === 'asc') { + next = { col: colIndex, direction: 'desc' }; + } else { + next = null; + } + + if (next === null) { + sortStates.delete(table); + tbody.append(headerRow, ...saved); + } else { + sortStates.set(table, next); + const sorted = sortDataRows(saved, next.col, next.direction); + tbody.append(headerRow, ...sorted); + } + + updateChevrons(table); +} + +const CHEVRON_SVG = + ''; + +function ensureChevron(th: HTMLTableCellElement): HTMLSpanElement { + let chevron = th.querySelector(`.${CHEVRON_CLASS}`); + if (!chevron) { + chevron = document.createElement('span'); + chevron.className = CHEVRON_CLASS; + chevron.setAttribute('aria-hidden', 'true'); + chevron.innerHTML = CHEVRON_SVG; + th.appendChild(chevron); + } + return chevron; +} + +function updateChevrons(table: HTMLTableElement): void { + const firstRow = table.querySelector('tr'); + if (!firstRow) return; + + const state = sortStates.get(table) ?? null; + let col = 0; + for (let i = 0; i < firstRow.cells.length; i++) { + const cell = firstRow.cells[i]; + if (cell.tagName !== 'TH') { + col += cell.colSpan ?? 1; + continue; + } + const chevron = ensureChevron(cell as HTMLTableCellElement); + let label: string; + if (state && state.col === col) { + chevron.setAttribute('data-sort', state.direction); + label = state.direction === 'asc' ? 'Sort descending' : 'Clear sort'; + } else { + chevron.removeAttribute('data-sort'); + label = 'Sort ascending'; + } + chevron.setAttribute('data-tooltip', label); + chevron.setAttribute('aria-label', label); + chevron.title = label; + col += cell.colSpan ?? 1; + } +} + +function addChevronsToAllTables(editorRoot: HTMLElement): void { + const tables = editorRoot.querySelectorAll('table'); + tables.forEach((table) => updateChevrons(table)); +} + +function removeAllChevrons(editorRoot: HTMLElement): void { + editorRoot + .querySelectorAll(`.${CHEVRON_CLASS}`) + .forEach((el) => el.remove()); +} + +export const TableReadonlySort = Extension.create({ + name: 'tableReadonlySort', + + addProseMirrorPlugins() { + const editor = this.editor; + let editorRoot: HTMLElement | null = null; + + const onClick = (event: MouseEvent) => { + if (editor.isEditable) return; + // Only react to clicks on the chevron, not anywhere else in the header + // cell. This lets the user click into a header to select text without + // accidentally triggering a sort. + if (!(event.target instanceof Element)) return; + const chevron = event.target.closest(`.${CHEVRON_CLASS}`); + if (!chevron) return; + const th = getHeaderTh(chevron); + if (!th) return; + const table = th.closest('table') as HTMLTableElement | null; + if (!table) return; + const colIndex = getColumnIndex(th); + if (colIndex < 0) return; + applySort(table, colIndex); + }; + + return [ + new Plugin({ + key: tableReadonlySortKey, + + view(editorView) { + editorRoot = editorView.dom as HTMLElement; + editorRoot.addEventListener('click', onClick); + + if (!editor.isEditable) { + addChevronsToAllTables(editorRoot); + } + + return { + update(view) { + const root = view.dom as HTMLElement; + if (!editor.isEditable) { + addChevronsToAllTables(root); + } else { + removeAllChevrons(root); + } + }, + destroy() { + if (editorRoot) { + editorRoot.removeEventListener('click', onClick); + removeAllChevrons(editorRoot); + } + }, + }; + }, + }), + ]; + }, +}); diff --git a/packages/editor-ext/src/lib/table/table-view.ts b/packages/editor-ext/src/lib/table/table-view.ts new file mode 100644 index 000000000..7e410918e --- /dev/null +++ b/packages/editor-ext/src/lib/table/table-view.ts @@ -0,0 +1,158 @@ +import type { Node as ProseMirrorNode } from '@tiptap/pm/model'; +import type { NodeView, ViewMutationRecord } from '@tiptap/pm/view'; +import { getColStyleDeclaration } from './utils/col-style'; + +export function updateColumns( + node: ProseMirrorNode, + colgroup: HTMLElement, + table: HTMLTableElement, + cellMinWidth: number, + overrideCol?: number, + overrideValue?: number, +) { + let totalWidth = 0; + let fixedWidth = true; + let nextDOM = colgroup.firstChild; + const row = node.firstChild; + + if (row !== null) { + for (let i = 0, col = 0; i < row.childCount; i += 1) { + const { colspan, colwidth } = row.child(i).attrs; + + for (let j = 0; j < colspan; j += 1, col += 1) { + const hasWidth = + overrideCol === col + ? overrideValue + : ((colwidth && colwidth[j]) as number | undefined); + const cssWidth = hasWidth ? `${hasWidth}px` : ''; + + totalWidth += hasWidth || cellMinWidth; + + if (!hasWidth) { + fixedWidth = false; + } + + if (!nextDOM) { + const colElement = document.createElement('col'); + + const [propertyKey, propertyValue] = getColStyleDeclaration( + cellMinWidth, + hasWidth, + ); + + colElement.style.setProperty(propertyKey, propertyValue); + + colgroup.appendChild(colElement); + } else { + if ((nextDOM as HTMLTableColElement).style.width !== cssWidth) { + const [propertyKey, propertyValue] = getColStyleDeclaration( + cellMinWidth, + hasWidth, + ); + + (nextDOM as HTMLTableColElement).style.setProperty( + propertyKey, + propertyValue, + ); + } + + nextDOM = nextDOM.nextSibling; + } + } + } + } + + while (nextDOM) { + const after = nextDOM.nextSibling; + + nextDOM.parentNode?.removeChild(nextDOM); + nextDOM = after; + } + + const hasUserWidth = + node.attrs.style && + typeof node.attrs.style === 'string' && + /\bwidth\s*:/i.test(node.attrs.style); + + if (fixedWidth && !hasUserWidth) { + table.style.width = `${totalWidth}px`; + table.style.minWidth = ''; + } else { + table.style.width = ''; + table.style.minWidth = `${totalWidth}px`; + } +} + +export class TableView implements NodeView { + node: ProseMirrorNode; + + cellMinWidth: number; + + dom: HTMLDivElement; + + table: HTMLTableElement; + + colgroup: HTMLTableColElement; + + contentDOM: HTMLTableSectionElement; + + constructor(node: ProseMirrorNode, cellMinWidth: number) { + this.node = node; + this.cellMinWidth = cellMinWidth; + this.dom = document.createElement('div'); + this.dom.className = 'tableWrapper'; + this.table = this.dom.appendChild(document.createElement('table')); + + if (node.attrs.style) { + this.table.style.cssText = node.attrs.style; + } + + this.colgroup = this.table.appendChild(document.createElement('colgroup')); + updateColumns(node, this.colgroup, this.table, cellMinWidth); + this.contentDOM = this.table.appendChild(document.createElement('tbody')); + } + + update(node: ProseMirrorNode) { + if (node.type !== this.node.type) return false; + + this.node = node; + updateColumns(node, this.colgroup, this.table, this.cellMinWidth); + + return true; + } + + ignoreMutation(mutation: ViewMutationRecord) { + const target = mutation.target as Node; + const isInsideWrapper = this.dom.contains(target); + const isInsideContent = this.contentDOM.contains(target); + + if (isInsideWrapper && !isInsideContent) { + if ( + mutation.type === 'attributes' || + mutation.type === 'childList' || + mutation.type === 'characterData' + ) { + return true; + } + } + + // Chevron span (.tableReadonlySortChevron) added/removed by sort plugin. + if (mutation.type === 'childList') { + const nodes = [ + ...Array.from(mutation.addedNodes), + ...Array.from(mutation.removedNodes), + ]; + if ( + nodes.some( + (n) => + n instanceof Element && + n.classList.contains('tableReadonlySortChevron'), + ) + ) { + return true; + } + } + + return false; + } +} diff --git a/packages/editor-ext/src/lib/table/table.ts b/packages/editor-ext/src/lib/table/table.ts index f1436c28d..e87048a46 100644 --- a/packages/editor-ext/src/lib/table/table.ts +++ b/packages/editor-ext/src/lib/table/table.ts @@ -1,6 +1,8 @@ import { Table } from "@tiptap/extension-table"; import { Editor } from "@tiptap/core"; import { DOMOutputSpec } from "@tiptap/pm/model"; +import { TextSelection } from "@tiptap/pm/state"; +import { cellAround } from "@tiptap/pm/tables"; const LIST_TYPES = ["bulletList", "orderedList", "taskList"]; @@ -32,9 +34,36 @@ function handleListOutdent(editor: Editor): boolean { } export const CustomTable = Table.extend({ + addKeyboardShortcuts() { return { ...this.parent?.(), + "Mod-a": () => { + const { state, view } = this.editor; + const { selection, doc } = state; + + const $cellPos = cellAround(selection.$anchor); + if (!$cellPos) return false; + + const cellNode = doc.nodeAt($cellPos.pos); + // Empty cells have nothing useful to scope to — let the default + // Mod-a fall through and select the whole doc. + if (!cellNode || !cellNode.textContent) return false; + + const from = $cellPos.pos + 1; + const to = $cellPos.pos + cellNode.nodeSize - 1; + if (from >= to) return true; + + const nextSel = TextSelection.between( + doc.resolve(from), + doc.resolve(to), + 1, + ); + if (!nextSel || selection.eq(nextSel)) return true; + + view.dispatch(state.tr.setSelection(nextSel)); + return true; + }, Tab: () => { // If we're in a list within a table, handle list indentation if (isInList(this.editor) && this.editor.isActive("table")) { diff --git a/packages/editor-ext/src/lib/table/utils/col-style.ts b/packages/editor-ext/src/lib/table/utils/col-style.ts new file mode 100644 index 000000000..8060962fd --- /dev/null +++ b/packages/editor-ext/src/lib/table/utils/col-style.ts @@ -0,0 +1,7 @@ +export function getColStyleDeclaration(minWidth: number, width: number | undefined): [string, string] { + if (width) { + return ['width', `${Math.max(width, minWidth)}px`] + } + + return ['min-width', `${minWidth}px`] +} diff --git a/packages/editor-ext/src/lib/transclusion/constants.ts b/packages/editor-ext/src/lib/transclusion/constants.ts new file mode 100644 index 000000000..762addeae --- /dev/null +++ b/packages/editor-ext/src/lib/transclusion/constants.ts @@ -0,0 +1,41 @@ +/** + * Top-level block node types allowed inside a `transclusionSource`. + * Notably excludes: + * - `transclusionSource` — sync blocks cannot wrap other sync blocks (sources are leaves). + * - `transclusionReference` — sync blocks cannot transclude other sync blocks, + * which keeps the transclusion graph acyclic and lets the renderer skip + * cycle-aware traversal entirely. + * + * Also excludes child-only nodes (`listItem`, `tableRow`, `column`, etc.) + * — they're already constrained by their parent containers. + */ +export const TRANSCLUSION_SOURCE_ALLOWED_NODE_TYPES = [ + 'paragraph', + 'heading', + 'blockquote', + 'codeBlock', + 'horizontalRule', + 'bulletList', + 'orderedList', + 'taskList', + 'image', + 'video', + 'audio', + 'attachment', + 'callout', + 'details', + 'embed', + 'mathBlock', + 'table', + 'drawio', + 'excalidraw', + 'pdf', + 'subpages', + 'columns', + 'youtube', +] as const; + +export type TransclusionSourceAllowedNodeType = + (typeof TRANSCLUSION_SOURCE_ALLOWED_NODE_TYPES)[number]; + +export const TRANSCLUSION_SOURCE_CONTENT_EXPRESSION = `(${TRANSCLUSION_SOURCE_ALLOWED_NODE_TYPES.join(' | ')})+`; diff --git a/packages/editor-ext/src/lib/transclusion/index.ts b/packages/editor-ext/src/lib/transclusion/index.ts new file mode 100644 index 000000000..619bc9221 --- /dev/null +++ b/packages/editor-ext/src/lib/transclusion/index.ts @@ -0,0 +1,3 @@ +export * from "./constants"; +export * from "./transclusion-source"; +export * from "./transclusion-reference"; diff --git a/packages/editor-ext/src/lib/transclusion/transclusion-reference.ts b/packages/editor-ext/src/lib/transclusion/transclusion-reference.ts new file mode 100644 index 000000000..a220e89a3 --- /dev/null +++ b/packages/editor-ext/src/lib/transclusion/transclusion-reference.ts @@ -0,0 +1,92 @@ +import { mergeAttributes, Node } from "@tiptap/core"; +import { ReactNodeViewRenderer } from "@tiptap/react"; + +export interface TransclusionReferenceOptions { + HTMLAttributes: Record; + view: any; +} + +export interface TransclusionReferenceAttributes { + sourcePageId?: string | null; + transclusionId?: string | null; +} + +declare module "@tiptap/core" { + interface Commands { + transclusionReference: { + insertTransclusionReference: ( + attributes: TransclusionReferenceAttributes, + ) => ReturnType; + }; + } +} + +export const TransclusionReference = Node.create({ + name: "transclusionReference", + + addOptions() { + return { + HTMLAttributes: {}, + view: null, + }; + }, + + group: "block", + atom: true, + selectable: true, + draggable: false, + + addAttributes() { + return { + sourcePageId: { + default: null, + parseHTML: (el) => el.getAttribute("data-source-page-id"), + renderHTML: (attrs) => + attrs.sourcePageId + ? { "data-source-page-id": attrs.sourcePageId } + : {}, + }, + transclusionId: { + default: null, + parseHTML: (el) => el.getAttribute("data-transclusion-id"), + renderHTML: (attrs) => + attrs.transclusionId + ? { "data-transclusion-id": attrs.transclusionId } + : {}, + }, + }; + }, + + parseHTML() { + return [{ tag: `div[data-type="${this.name}"]` }]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + "div", + mergeAttributes( + { "data-type": this.name }, + this.options.HTMLAttributes, + HTMLAttributes, + ), + ]; + }, + + addCommands() { + return { + insertTransclusionReference: + (attributes) => + ({ commands }) => + commands.insertContent({ + type: this.name, + attrs: attributes, + }), + }; + }, + + addNodeView() { + if (!this.options.view) return null; + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); + }, +}); diff --git a/packages/editor-ext/src/lib/transclusion/transclusion-source.ts b/packages/editor-ext/src/lib/transclusion/transclusion-source.ts new file mode 100644 index 000000000..9e1b61657 --- /dev/null +++ b/packages/editor-ext/src/lib/transclusion/transclusion-source.ts @@ -0,0 +1,134 @@ +import { mergeAttributes, Node } from "@tiptap/core"; +import { ReactNodeViewRenderer } from "@tiptap/react"; +import { TRANSCLUSION_SOURCE_CONTENT_EXPRESSION } from "./constants"; + +export interface TransclusionSourceOptions { + HTMLAttributes: Record; + view: any; +} + +export interface TransclusionSourceAttributes { + id?: string | null; +} + +declare module "@tiptap/core" { + interface Commands { + transclusionSource: { + insertTransclusionSource: ( + attributes?: TransclusionSourceAttributes, + ) => ReturnType; + toggleTransclusionSource: () => ReturnType; + unsyncTransclusionSource: () => ReturnType; + }; + } +} + +export const TransclusionSource = Node.create({ + name: "transclusionSource", + + addOptions() { + return { + HTMLAttributes: {}, + view: null, + }; + }, + + group: "block", + // Schema-enforced allow-list. Excludes `transclusionSource` (no nesting) + content: TRANSCLUSION_SOURCE_CONTENT_EXPRESSION, + defining: true, + isolating: true, + + addAttributes() { + return { + id: { + default: null, + parseHTML: (el) => el.getAttribute("data-id"), + renderHTML: (attrs) => + attrs.id ? { "data-id": attrs.id } : {}, + }, + }; + }, + + parseHTML() { + return [{ tag: `div[data-type="${this.name}"]` }]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + "div", + mergeAttributes( + { "data-type": this.name }, + this.options.HTMLAttributes, + HTMLAttributes, + ), + 0, + ]; + }, + + addCommands() { + return { + insertTransclusionSource: + (attributes) => + ({ commands, state, chain }) => { + const { $from } = state.selection; + for (let depth = $from.depth; depth > 0; depth -= 1) { + if ($from.node(depth).type.name === this.name) return false; + } + + const node = { + type: this.name, + attrs: attributes ?? {}, + content: [{ type: "paragraph" }], + }; + + const parent = $from.parent; + const isEmptyParagraph = + parent.type.name === "paragraph" && parent.content.size === 0; + + if (isEmptyParagraph) { + return chain() + .insertContentAt( + { from: $from.before(), to: $from.after() }, + node, + ) + .run(); + } + + return commands.insertContent(node); + }, + toggleTransclusionSource: + () => + ({ commands }) => + commands.toggleWrap(this.name), + unsyncTransclusionSource: + () => + ({ state, tr, dispatch }) => { + const { $from } = state.selection; + // Walk up to the nearest source wrapper. + let depth = $from.depth; + while (depth > 0 && $from.node(depth).type.name !== this.name) { + depth -= 1; + } + if (depth === 0) return false; + + const node = $from.node(depth); + const start = $from.before(depth); + const end = start + node.nodeSize; + + if (dispatch) { + tr.replaceWith(start, end, node.content); + dispatch(tr); + } + return true; + }, + }; + }, + + addNodeView() { + if (!this.options.view) return null; + // Force the react node view to render immediately using flush sync + this.editor.isInitialized = true; + return ReactNodeViewRenderer(this.options.view); + }, +}); diff --git a/packages/editor-ext/src/lib/utils.ts b/packages/editor-ext/src/lib/utils.ts index 3bfd01778..8d03577c7 100644 --- a/packages/editor-ext/src/lib/utils.ts +++ b/packages/editor-ext/src/lib/utils.ts @@ -338,6 +338,15 @@ export const isRowGripSelected = ({ return !!gripRow; }; +// TipTap's `editor.view` proxy throws if accessed before mount or after destroy. +// Guard floating-menu callbacks (getReferencedVirtualElement, shouldShow) with +// this before touching `editor.view.nodeDOM(...)`. +export function isEditorReady( + editor: Editor | null | undefined, +): editor is Editor { + return !!editor && editor.isInitialized; +} + export function isTextSelected(editor: Editor) { const { state: { diff --git a/packages/editor-ext/src/lib/video/video.ts b/packages/editor-ext/src/lib/video/video.ts index c6d7356b0..5bf248cd7 100644 --- a/packages/editor-ext/src/lib/video/video.ts +++ b/packages/editor-ext/src/lib/video/video.ts @@ -27,6 +27,7 @@ export interface VideoOptions { export interface VideoAttributes { src?: string; + alt?: string; align?: string; attachmentId?: string; size?: number; @@ -79,6 +80,13 @@ export const TiptapVideo = Node.create({ src: attributes.src, }), }, + alt: { + default: undefined, + parseHTML: (element) => element.getAttribute("aria-label"), + renderHTML: (attributes: VideoAttributes) => ({ + "aria-label": attributes.alt, + }), + }, attachmentId: { default: undefined, parseHTML: (element) => element.getAttribute("data-attachment-id"), @@ -228,6 +236,9 @@ export const TiptapVideo = Node.create({ el.src = normalizeFileUrl(node.attrs.src); el.controls = true; el.preload = "metadata"; + if (node.attrs.alt) { + el.setAttribute("aria-label", node.attrs.alt); + } el.style.display = "block"; el.style.maxWidth = "100%"; el.style.borderRadius = "8px"; @@ -272,6 +283,14 @@ export const TiptapVideo = Node.create({ el.src = normalizeFileUrl(updatedNode.attrs.src); } + if (updatedNode.attrs.alt !== currentNode.attrs.alt) { + if (updatedNode.attrs.alt) { + el.setAttribute("aria-label", updatedNode.attrs.alt); + } else { + el.removeAttribute("aria-label"); + } + } + const w = updatedNode.attrs.width; const h = updatedNode.attrs.height; if (w != null) { diff --git a/patches/@tiptap__core.patch b/patches/@tiptap__core.patch deleted file mode 100644 index 58f580c8a..000000000 --- a/patches/@tiptap__core.patch +++ /dev/null @@ -1,105 +0,0 @@ -diff --git a/dist/index.cjs b/dist/index.cjs -index 01d6999642c5ae990083798a1bf0ef87068e4192..891b13c6901f28a6ab413c6dbae0ea726a76a196 100644 ---- a/dist/index.cjs -+++ b/dist/index.cjs -@@ -5463,7 +5463,10 @@ var ResizableNodeView = class { - this.container.classList.remove(this.classNames.resizing); - } - document.removeEventListener("mousemove", this.handleMouseMove); -+ document.removeEventListener("touchmove", this.handleTouchMove); - document.removeEventListener("mouseup", this.handleMouseUp); -+ document.removeEventListener("touchend", this.handleMouseUp); -+ window.removeEventListener("blur", this.handleMouseUp); - document.removeEventListener("keydown", this.handleKeyDown); - document.removeEventListener("keyup", this.handleKeyUp); - }; -@@ -5593,7 +5596,10 @@ var ResizableNodeView = class { - this.container.classList.remove(this.classNames.resizing); - } - document.removeEventListener("mousemove", this.handleMouseMove); -+ document.removeEventListener("touchmove", this.handleTouchMove); - document.removeEventListener("mouseup", this.handleMouseUp); -+ document.removeEventListener("touchend", this.handleMouseUp); -+ window.removeEventListener("blur", this.handleMouseUp); - document.removeEventListener("keydown", this.handleKeyDown); - document.removeEventListener("keyup", this.handleKeyUp); - this.isResizing = false; -@@ -5796,6 +5802,8 @@ var ResizableNodeView = class { - document.addEventListener("mousemove", this.handleMouseMove); - document.addEventListener("touchmove", this.handleTouchMove); - document.addEventListener("mouseup", this.handleMouseUp); -+ document.addEventListener("touchend", this.handleMouseUp); -+ window.addEventListener("blur", this.handleMouseUp); - document.addEventListener("keydown", this.handleKeyDown); - document.addEventListener("keyup", this.handleKeyUp); - } -diff --git a/dist/index.js b/dist/index.js -index 6f357a03b038abeb5ed86967b7fc7c3e5eb1d2d6..2d2742532860821984e1ba82625821504538ebbe 100644 ---- a/dist/index.js -+++ b/dist/index.js -@@ -5330,7 +5330,10 @@ var ResizableNodeView = class { - this.container.classList.remove(this.classNames.resizing); - } - document.removeEventListener("mousemove", this.handleMouseMove); -+ document.removeEventListener("touchmove", this.handleTouchMove); - document.removeEventListener("mouseup", this.handleMouseUp); -+ document.removeEventListener("touchend", this.handleMouseUp); -+ window.removeEventListener("blur", this.handleMouseUp); - document.removeEventListener("keydown", this.handleKeyDown); - document.removeEventListener("keyup", this.handleKeyUp); - }; -@@ -5460,7 +5463,10 @@ var ResizableNodeView = class { - this.container.classList.remove(this.classNames.resizing); - } - document.removeEventListener("mousemove", this.handleMouseMove); -+ document.removeEventListener("touchmove", this.handleTouchMove); - document.removeEventListener("mouseup", this.handleMouseUp); -+ document.removeEventListener("touchend", this.handleMouseUp); -+ window.removeEventListener("blur", this.handleMouseUp); - document.removeEventListener("keydown", this.handleKeyDown); - document.removeEventListener("keyup", this.handleKeyUp); - this.isResizing = false; -@@ -5663,6 +5669,8 @@ var ResizableNodeView = class { - document.addEventListener("mousemove", this.handleMouseMove); - document.addEventListener("touchmove", this.handleTouchMove); - document.addEventListener("mouseup", this.handleMouseUp); -+ document.addEventListener("touchend", this.handleMouseUp); -+ window.addEventListener("blur", this.handleMouseUp); - document.addEventListener("keydown", this.handleKeyDown); - document.addEventListener("keyup", this.handleKeyUp); - } -diff --git a/src/lib/ResizableNodeView.ts b/src/lib/ResizableNodeView.ts -index f13e210b0aa46aefe7c31105deee3d2aa8a26cd5..9bac138dbf17c6ae6c3c129cbedb3a81bd39b60c 100644 ---- a/src/lib/ResizableNodeView.ts -+++ b/src/lib/ResizableNodeView.ts -@@ -523,7 +523,10 @@ export class ResizableNodeView { - } - - document.removeEventListener('mousemove', this.handleMouseMove) -+ document.removeEventListener('touchmove', this.handleTouchMove) - document.removeEventListener('mouseup', this.handleMouseUp) -+ document.removeEventListener('touchend', this.handleMouseUp) -+ window.removeEventListener('blur', this.handleMouseUp) - document.removeEventListener('keydown', this.handleKeyDown) - document.removeEventListener('keyup', this.handleKeyUp) - this.isResizing = false -@@ -774,6 +777,8 @@ export class ResizableNodeView { - document.addEventListener('mousemove', this.handleMouseMove) - document.addEventListener('touchmove', this.handleTouchMove) - document.addEventListener('mouseup', this.handleMouseUp) -+ document.addEventListener('touchend', this.handleMouseUp) -+ window.addEventListener('blur', this.handleMouseUp) - document.addEventListener('keydown', this.handleKeyDown) - document.addEventListener('keyup', this.handleKeyUp) - } -@@ -859,7 +864,10 @@ export class ResizableNodeView { - - // Clean up document-level listeners - document.removeEventListener('mousemove', this.handleMouseMove) -+ document.removeEventListener('touchmove', this.handleTouchMove) - document.removeEventListener('mouseup', this.handleMouseUp) -+ document.removeEventListener('touchend', this.handleMouseUp) -+ window.removeEventListener('blur', this.handleMouseUp) - document.removeEventListener('keydown', this.handleKeyDown) - document.removeEventListener('keyup', this.handleKeyUp) - } diff --git a/patches/react-arborist@3.4.0.patch b/patches/react-arborist@3.4.0.patch deleted file mode 100644 index 0d8c1ae0f..000000000 --- a/patches/react-arborist@3.4.0.patch +++ /dev/null @@ -1,33 +0,0 @@ -diff --git a/dist/module/components/default-container.js b/dist/module/components/default-container.js -index 47724f59b482454fe3144dbb98bd16d3df6a9c17..2285e35ea0073a773b7b74e22758056fd3514c1a 100644 ---- a/dist/module/components/default-container.js -+++ b/dist/module/components/default-container.js -@@ -34,28 +34,6 @@ export function DefaultContainer() { - return; - } - if (e.key === "Backspace") { -- if (!tree.props.onDelete) -- return; -- const ids = Array.from(tree.selectedIds); -- if (ids.length > 1) { -- let nextFocus = tree.mostRecentNode; -- while (nextFocus && nextFocus.isSelected) { -- nextFocus = nextFocus.nextSibling; -- } -- if (!nextFocus) -- nextFocus = tree.lastNode; -- tree.focus(nextFocus, { scroll: false }); -- tree.delete(Array.from(ids)); -- } -- else { -- const node = tree.focusedNode; -- if (node) { -- const sib = node.nextSibling; -- const parent = node.parent; -- tree.focus(sib || parent, { scroll: false }); -- tree.delete(node); -- } -- } - return; - } - if (e.key === "Tab" && !e.shiftKey) { diff --git a/patches/scimmy@1.3.5.patch b/patches/scimmy@1.3.5.patch new file mode 100644 index 000000000..fd2060346 --- /dev/null +++ b/patches/scimmy@1.3.5.patch @@ -0,0 +1,23 @@ +diff --git a/dist/cjs/lib/messages.cjs b/dist/cjs/lib/messages.cjs +index e74b8f52137e3267f3d065c4210a1114c4f32dd1..5740606b18851c0ac4f55cfa333152359e0ad135 100644 +--- a/dist/cjs/lib/messages.cjs ++++ b/dist/cjs/lib/messages.cjs +@@ -502,10 +502,15 @@ class PatchOp { + } + } + } +- ++ ++ /** Reason: Commented out to avoid failing patch requests when filters don't match. ++ * Some IdPs send patch paths like `addresses[type eq "work"].country` even if no such address exists. We can't always decide what the end user IdPs send. ++ * Since we manually control patch application, we safely ignore these cases. ++ * example error: "noTarget","detail":"Filter 'addresses[type eq \"work\"].country' does not match any values for 'add' op of operation 5 in PatchOp request body ++ */ + // No targets, bail out! +- if (targets.length === 0 && op !== "remove") +- throw new lib_types.default.Error(400, "noTarget", `Filter '${path}' does not match any values for '${op}' op of operation ${index} in PatchOp request body`); ++ // if (targets.length === 0 && op !== "remove") ++ // throw new lib_types.default.Error(400, "noTarget", `Filter '${path}' does not match any values for '${op}' op of operation ${index} in PatchOp request body`); + + /** + * @typedef {Object} PatchOpDetails diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7143cb856..cc9edd641 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,11 +8,11 @@ overrides: prosemirror-changeset: 2.4.0 y-prosemirror: 1.3.7 glob: 13.0.6 - ws: 8.20.0 - dompurify: 3.3.3 + ws: 8.20.1 + dompurify: 3.4.1 tmp: 0.2.5 - hono: 4.12.12 - mermaid: 11.13.0 + hono: 4.12.18 + mermaid: 11.15.0 nanoid@^3: 3.3.8 socket.io-parser: 4.2.6 serialize-javascript: 7.0.3 @@ -30,21 +30,23 @@ overrides: flatted: 3.4.2 picomatch@<2.3.2: 2.3.2 picomatch@>=4.0.0 <4.0.4: 4.0.4 - fastify: 5.8.3 + fastify: 5.8.5 yaml@>=1.0.0 <1.10.3: 1.10.3 yaml@>=2.0.0 <2.8.3: 2.8.3 path-to-regexp@^8: 8.4.0 - brace-expansion@^5: 5.0.5 - '@xmldom/xmldom': 0.8.12 + brace-expansion@^5: 5.0.6 + '@xmldom/xmldom': 0.8.13 handlebars: 4.7.9 - axios: 1.15.0 - langsmith: 0.5.18 + axios: 1.16.0 + langsmith: 0.7.0 follow-redirects: 1.16.0 + protobufjs: 7.5.8 + ip-address: 10.1.1 patchedDependencies: - react-arborist@3.4.0: - hash: 419b3b02e24afe928cc006a006f6e906666aff19aa6fd7daaa788ccc2202678a - path: patches/react-arborist@3.4.0.patch + scimmy@1.3.5: + hash: 775d80f86830b2c5dd1a250c9802c10f8fc3da3c7898373de5aa0c23993d1673 + path: patches/scimmy@1.3.5.patch importers: @@ -180,8 +182,8 @@ importers: specifier: 8.0.3 version: 8.0.3 dompurify: - specifier: 3.3.3 - version: 3.3.3 + specifier: 3.4.1 + version: 3.4.1 fractional-indexing-jittered: specifier: ^1.0.0 version: 1.0.0 @@ -210,8 +212,8 @@ importers: specifier: 5.2.0 version: 5.2.0 uuid: - specifier: ^13.0.0 - version: 13.0.0 + specifier: ^14.0.0 + version: 14.0.0 y-indexeddb: specifier: ^9.0.12 version: 9.0.12(yjs@13.6.30) @@ -246,20 +248,35 @@ importers: apps/client: dependencies: + '@atlaskit/pragmatic-drag-and-drop': + specifier: 1.8.1 + version: 1.8.1 + '@atlaskit/pragmatic-drag-and-drop-auto-scroll': + specifier: 2.1.5 + version: 2.1.5 + '@atlaskit/pragmatic-drag-and-drop-flourish': + specifier: 2.0.15 + version: 2.0.15(react@18.3.1) + '@atlaskit/pragmatic-drag-and-drop-hitbox': + specifier: 1.1.0 + version: 1.1.0 + '@atlaskit/pragmatic-drag-and-drop-live-region': + specifier: 1.3.4 + version: 1.3.4 '@casl/react': - specifier: ^5.0.1 + specifier: 5.0.1 version: 5.0.1(@casl/ability@6.8.0)(react@18.3.1) '@dnd-kit/core': - specifier: ^6.3.1 + specifier: 6.3.1 version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@dnd-kit/modifiers': - specifier: ^9.0.0 + specifier: 9.0.0 version: 9.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@dnd-kit/sortable': - specifier: ^10.0.0 + specifier: 10.0.0 version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@dnd-kit/utilities': - specifier: ^3.2.2 + specifier: 3.2.2 version: 3.2.2(react@18.3.1) '@docmost/base-formula': specifier: workspace:* @@ -267,111 +284,108 @@ importers: '@docmost/editor-ext': specifier: workspace:* version: link:../../packages/editor-ext - '@emoji-mart/data': - specifier: ^1.2.1 - version: 1.2.1 - '@emoji-mart/react': - specifier: ^1.1.1 - version: 1.1.1(emoji-mart@5.6.0)(react@18.3.1) '@excalidraw/excalidraw': specifier: 0.18.0-3a5ef40 version: 0.18.0-3a5ef40(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/core': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(@mantine/hooks@8.3.18(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/dates': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@8.3.18(react@18.3.1))(dayjs@1.11.19)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/form': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(react@18.3.1) '@mantine/hooks': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(react@18.3.1) '@mantine/modals': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@8.3.18(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/notifications': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@8.3.18(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mantine/spotlight': - specifier: ^8.3.18 + specifier: 8.3.18 version: 8.3.18(@mantine/core@8.3.18(@mantine/hooks@8.3.18(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mantine/hooks@8.3.18(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@slidoapp/emoji-mart': + specifier: 5.8.7 + version: 5.8.7 + '@slidoapp/emoji-mart-data': + specifier: 1.2.4 + version: 1.2.4 + '@slidoapp/emoji-mart-react': + specifier: 1.1.5 + version: 1.1.5(@slidoapp/emoji-mart@5.8.7)(react@18.3.1) '@tabler/icons-react': - specifier: ^3.40.0 + specifier: 3.40.0 version: 3.40.0(react@18.3.1) '@tanstack/react-query': - specifier: 5.99.1 - version: 5.99.1(react@18.3.1) + specifier: 5.90.17 + version: 5.90.17(react@18.3.1) '@tanstack/react-table': - specifier: ^8.21.3 + specifier: 8.21.3 version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-virtual': - specifier: ^3.13.24 + specifier: 3.13.24 version: 3.13.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) alfaaz: - specifier: ^1.1.0 + specifier: 1.1.0 version: 1.1.0 axios: - specifier: 1.15.0 - version: 1.15.0 + specifier: 1.16.0 + version: 1.16.0 blueimp-load-image: - specifier: ^5.16.0 + specifier: 5.16.0 version: 5.16.0 clsx: - specifier: ^2.1.1 + specifier: 2.1.1 version: 2.1.1 - emoji-mart: - specifier: ^5.6.0 - version: 5.6.0 file-saver: - specifier: ^2.0.5 + specifier: 2.0.5 version: 2.0.5 highlightjs-sap-abap: - specifier: ^0.3.0 + specifier: 0.3.0 version: 0.3.0 i18next: - specifier: ^25.10.1 + specifier: 25.10.1 version: 25.10.1(typescript@5.9.3) i18next-http-backend: - specifier: ^3.0.2 - version: 3.0.2 + specifier: 3.0.6 + version: 3.0.6 jotai: - specifier: ^2.18.1 + specifier: 2.18.1 version: 2.18.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@18.3.12)(react@18.3.1) jotai-optics: - specifier: ^0.4.0 + specifier: 0.4.0 version: 0.4.0(jotai@2.18.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@18.3.12)(react@18.3.1))(optics-ts@2.4.1) js-cookie: - specifier: ^3.0.5 + specifier: 3.0.5 version: 3.0.5 jwt-decode: - specifier: ^4.0.0 + specifier: 4.0.0 version: 4.0.0 katex: specifier: 0.16.40 version: 0.16.40 lowlight: - specifier: ^3.3.0 + specifier: 3.3.0 version: 3.3.0 mantine-form-zod-resolver: - specifier: ^1.3.0 + specifier: 1.3.0 version: 1.3.0(@mantine/form@8.3.18(react@18.3.1))(zod@4.3.6) mermaid: - specifier: 11.13.0 - version: 11.13.0 + specifier: 11.15.0 + version: 11.15.0 mitt: - specifier: ^3.0.1 + specifier: 3.0.1 version: 3.0.1 posthog-js: - specifier: 1.363.1 - version: 1.363.1 + specifier: 1.372.2 + version: 1.372.2 react: - specifier: ^18.3.1 + specifier: 18.3.1 version: 18.3.1 - react-arborist: - specifier: 3.4.0 - version: 3.4.0(patch_hash=419b3b02e24afe928cc006a006f6e906666aff19aa6fd7daaa788ccc2202678a)(@types/node@22.19.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-clear-modal: specifier: ^2.0.18 version: 2.0.18(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -379,102 +393,111 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) react-drawio: - specifier: ^1.0.7 + specifier: 1.0.7 version: 1.0.7(react@18.3.1) react-error-boundary: - specifier: ^6.1.1 + specifier: 6.1.1 version: 6.1.1(react@18.3.1) react-helmet-async: - specifier: ^3.0.0 + specifier: 3.0.0 version: 3.0.0(react@18.3.1) react-i18next: - specifier: ^16.5.8 + specifier: 16.5.8 version: 16.5.8(i18next@25.10.1(typescript@5.9.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.3) react-router-dom: - specifier: ^7.13.1 + specifier: 7.13.1 version: 7.13.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) semver: - specifier: ^7.7.4 + specifier: 7.7.4 version: 7.7.4 socket.io-client: - specifier: ^4.8.3 + specifier: 4.8.3 version: 4.8.3 - tiptap-extension-global-drag-handle: - specifier: ^0.1.18 - version: 0.1.18 zod: - specifier: ^4.3.6 + specifier: 4.3.6 version: 4.3.6 devDependencies: '@eslint/js': - specifier: ^9.28.0 - version: 9.39.4 + specifier: 9.28.0 + version: 9.28.0 '@tanstack/eslint-plugin-query': - specifier: ^5.94.4 - version: 5.94.4(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3) + specifier: 5.94.4 + version: 5.94.4(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + '@testing-library/jest-dom': + specifier: 6.6.0 + version: 6.6.0 + '@testing-library/react': + specifier: 16.1.0 + version: 16.1.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/blueimp-load-image': - specifier: ^5.16.6 + specifier: 5.16.6 version: 5.16.6 '@types/file-saver': - specifier: ^2.0.7 + specifier: 2.0.7 version: 2.0.7 '@types/js-cookie': - specifier: ^3.0.6 + specifier: 3.0.6 version: 3.0.6 '@types/katex': - specifier: ^0.16.8 + specifier: 0.16.8 version: 0.16.8 '@types/node': specifier: 22.19.1 version: 22.19.1 '@types/react': - specifier: ^18.3.12 + specifier: 18.3.12 version: 18.3.12 '@types/react-dom': - specifier: ^18.3.1 + specifier: 18.3.1 version: 18.3.1 '@vitejs/plugin-react': - specifier: ^6.0.1 - version: 6.0.1(vite@8.0.5(@types/node@22.19.1)(esbuild@0.27.4)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.8))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) + specifier: 6.0.1 + version: 6.0.1(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) eslint: - specifier: ^9.28.0 - version: 9.39.4(jiti@2.4.2) + specifier: 9.28.0 + version: 9.28.0(jiti@2.4.2) eslint-plugin-react: - specifier: ^7.37.5 - version: 7.37.5(eslint@9.39.4(jiti@2.4.2)) + specifier: 7.37.5 + version: 7.37.5(eslint@9.28.0(jiti@2.4.2)) eslint-plugin-react-hooks: - specifier: ^7.0.1 - version: 7.0.1(eslint@9.39.4(jiti@2.4.2)) + specifier: 7.0.1 + version: 7.0.1(eslint@9.28.0(jiti@2.4.2)) eslint-plugin-react-refresh: - specifier: ^0.5.2 - version: 0.5.2(eslint@9.39.4(jiti@2.4.2)) + specifier: 0.5.2 + version: 0.5.2(eslint@9.28.0(jiti@2.4.2)) globals: - specifier: ^15.13.0 + specifier: 15.13.0 version: 15.13.0 + jsdom: + specifier: 25.0.0 + version: 25.0.0 optics-ts: - specifier: ^2.4.1 + specifier: 2.4.1 version: 2.4.1 postcss: - specifier: ^8.5.8 - version: 8.5.8 + specifier: 8.5.14 + version: 8.5.14 postcss-preset-mantine: - specifier: ^1.18.0 - version: 1.18.0(postcss@8.5.8) + specifier: 1.18.0 + version: 1.18.0(postcss@8.5.14) postcss-simple-vars: - specifier: ^7.0.1 - version: 7.0.1(postcss@8.5.8) + specifier: 7.0.1 + version: 7.0.1(postcss@8.5.14) prettier: - specifier: ^3.8.1 + specifier: 3.8.1 version: 3.8.1 typescript: - specifier: ^5.9.3 + specifier: 5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.57.1 - version: 8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3) + specifier: 8.57.1 + version: 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) vite: specifier: 8.0.5 - version: 8.0.5(@types/node@22.19.1)(esbuild@0.27.4)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.8))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + version: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vitest: + specifier: 4.1.6 + version: 4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(happy-dom@20.8.9)(jsdom@25.0.0)(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) apps/server: dependencies: @@ -488,98 +511,98 @@ importers: specifier: ^2.0.37 version: 2.0.37(zod@4.3.6) '@aws-sdk/client-s3': - specifier: 3.1014.0 - version: 3.1014.0 + specifier: 3.1050.0 + version: 3.1050.0 '@aws-sdk/lib-storage': - specifier: 3.1014.0 - version: 3.1014.0(@aws-sdk/client-s3@3.1014.0) + specifier: 3.1050.0 + version: 3.1050.0(@aws-sdk/client-s3@3.1050.0) '@aws-sdk/s3-request-presigner': - specifier: 3.1014.0 - version: 3.1014.0 + specifier: 3.1050.0 + version: 3.1050.0 + '@azure/storage-blob': + specifier: 12.31.0 + version: 12.31.0 '@clickhouse/client': specifier: ^1.18.2 version: 1.18.2 + '@docmost/pdf-inspector': + specifier: 1.9.6 + version: 1.9.6 '@fastify/cookie': specifier: ^11.0.2 version: 11.0.2 '@fastify/multipart': - specifier: ^9.4.0 - version: 9.4.0 + specifier: ^10.0.0 + version: 10.0.0 '@fastify/static': - specifier: ^9.0.0 - version: 9.0.0 + specifier: ^9.1.3 + version: 9.1.3 '@keyv/redis': specifier: ^5.1.6 version: 5.1.6(keyv@5.6.0) '@langchain/core': - specifier: 1.1.39 - version: 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) + specifier: 1.1.46 + version: 1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.1)(zod@4.3.6))(ws@8.20.1) '@langchain/textsplitters': specifier: 1.0.1 - version: 1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)) + version: 1.0.1(@langchain/core@1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.1)(zod@4.3.6))(ws@8.20.1)) '@modelcontextprotocol/sdk': - specifier: ^1.29.0 + specifier: 1.29.0 version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) '@nest-lab/throttler-storage-redis': specifier: ^1.2.0 - version: 1.2.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/throttler@6.5.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2))(ioredis@5.10.1)(reflect-metadata@0.2.2) + version: 1.2.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/throttler@6.5.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.2.2))(ioredis@5.10.1)(reflect-metadata@0.2.2) '@nestjs-labs/nestjs-ioredis': specifier: ^11.0.4 - version: 11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(ioredis@5.10.1) + version: 11.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(ioredis@5.10.1) '@nestjs/bullmq': specifier: ^11.0.4 - version: 11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(bullmq@5.71.0) + version: 11.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(bullmq@5.76.10) '@nestjs/cache-manager': specifier: ^3.1.0 - version: 3.1.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2) + version: 3.1.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2) '@nestjs/common': - specifier: ^11.1.18 - version: 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.19 + version: 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/config': - specifier: ^4.0.3 - version: 4.0.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + specifier: ^4.0.4 + version: 4.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': - specifier: ^11.1.18 - version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.19 + version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/event-emitter': specifier: ^3.0.1 - version: 3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + version: 3.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19) '@nestjs/jwt': specifier: 11.0.2 - version: 11.0.2(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + version: 11.0.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/mapped-types': specifier: ^2.1.1 - version: 2.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) + version: 2.1.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) '@nestjs/passport': specifier: ^11.0.5 - version: 11.0.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) + version: 11.0.5(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) '@nestjs/platform-fastify': - specifier: ^11.1.18 - version: 11.1.18(@fastify/static@9.0.0)(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + specifier: ^11.1.19 + version: 11.1.19(@fastify/static@9.1.3)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19) '@nestjs/platform-socket.io': - specifier: ^11.1.18 - version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(rxjs@7.8.2) + specifier: ^11.1.19 + version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(rxjs@7.8.2) '@nestjs/schedule': - specifier: ^6.1.1 - version: 6.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + specifier: ^6.1.3 + version: 6.1.3(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19) '@nestjs/terminus': specifier: ^11.1.1 - version: 11.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/throttler': specifier: ^6.5.0 - version: 6.5.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2) + version: 6.5.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.2.2) '@nestjs/websockets': - specifier: ^11.1.18 - version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/platform-socket.io@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.19 + version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-socket.io@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@node-saml/passport-saml': specifier: ^5.1.0 version: 5.1.0 - '@react-email/components': - specifier: 1.0.10 - version: 1.0.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-email/render': - specifier: 2.0.4 - version: 2.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@socket.io/redis-adapter': specifier: ^8.3.0 version: 8.3.0(socket.io-adapter@2.5.4) @@ -596,8 +619,8 @@ importers: specifier: ^2.14.1 version: 2.14.1 bullmq: - specifier: ^5.71.0 - version: 5.71.0 + specifier: ^5.76.10 + version: 5.76.10 cache-manager: specifier: ^7.2.8 version: 7.2.8 @@ -638,14 +661,14 @@ importers: specifier: ^9.0.3 version: 9.0.3 kysely: - specifier: ^0.28.14 - version: 0.28.14 + specifier: ^0.28.17 + version: 0.28.17 kysely-migration-cli: specifier: ^0.4.2 version: 0.4.2 kysely-postgres-js: specifier: ^3.0.0 - version: 3.0.0(kysely@0.28.14)(postgres@3.4.8) + version: 3.0.0(kysely@0.28.17)(postgres@3.4.8) ldapts: specifier: ^8.1.7 version: 8.1.7 @@ -666,13 +689,13 @@ importers: version: 5.1.7 nestjs-cls: specifier: ^6.2.0 - version: 6.2.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 6.2.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) nestjs-kysely: specifier: ^3.1.2 - version: 3.1.2(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(kysely@0.28.14)(reflect-metadata@0.2.2) + version: 3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(kysely@0.28.17)(reflect-metadata@0.2.2) nestjs-pino: specifier: ^4.6.1 - version: 4.6.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2) + version: 4.6.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2) nodemailer: specifier: ^8.0.5 version: 8.0.5 @@ -691,9 +714,6 @@ importers: passport-jwt: specifier: ^4.0.1 version: 4.0.1 - pdfjs-dist: - specifier: ^5.5.207 - version: 5.5.207 pg-tsquery: specifier: ^8.4.2 version: 8.4.2 @@ -715,15 +735,21 @@ importers: react: specifier: ^18.3.1 version: 18.3.1 + react-email: + specifier: 6.0.8 + version: 6.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) reflect-metadata: specifier: ^0.2.2 version: 0.2.2 rxjs: specifier: ^7.8.2 version: 7.8.2 - sanitize-filename-ts: - specifier: 1.0.2 - version: 1.0.2 + sanitize-filename: + specifier: 1.6.3 + version: 1.6.3 + scimmy: + specifier: 1.3.5 + version: 1.3.5(patch_hash=775d80f86830b2c5dd1a250c9802c10f8fc3da3c7898373de5aa0c23993d1673) socket.io: specifier: ^4.8.3 version: 4.8.3 @@ -742,9 +768,12 @@ importers: typesense: specifier: ^3.0.5 version: 3.0.5(@babel/runtime@7.29.2) + undici: + specifier: 7.24.0 + version: 7.24.0 ws: - specifier: 8.20.0 - version: 8.20.0 + specifier: 8.20.1 + version: 8.20.1 yauzl: specifier: ^3.2.1 version: 3.2.1 @@ -756,14 +785,14 @@ importers: specifier: ^9.28.0 version: 9.39.4 '@nestjs/cli': - specifier: ^11.0.18 - version: 11.0.18(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@25.5.0) + specifier: ^11.0.21 + version: 11.0.21(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@25.5.0) '@nestjs/schematics': specifier: ^11.0.10 version: 11.0.10(chokidar@4.0.3)(typescript@5.9.3) '@nestjs/testing': - specifier: ^11.1.18 - version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + specifier: ^11.1.19 + version: 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19) '@types/bcrypt': specifier: ^6.0.0 version: 6.0.0 @@ -814,13 +843,10 @@ importers: version: 30.3.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@25.5.0)(typescript@5.9.3)) kysely-codegen: specifier: ^0.20.0 - version: 0.20.0(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3) + version: 0.20.0(kysely@0.28.17)(pg@8.16.3)(typescript@5.9.3) prettier: specifier: ^3.8.1 version: 3.8.1 - react-email: - specifier: 5.2.10 - version: 5.2.10 source-map-support: specifier: ^0.5.21 version: 0.5.21 @@ -832,7 +858,7 @@ importers: version: 29.4.6(@babel/core@7.28.5)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.28.5))(jest-util@30.3.0)(jest@30.3.0(@types/node@25.5.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@25.5.0)(typescript@5.9.3)))(typescript@5.9.3) ts-loader: specifier: ^9.5.7 - version: 9.5.7(typescript@5.9.3)(webpack@5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5))) + version: 9.5.7(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.5.25(@swc/helpers@0.5.5))) ts-node: specifier: ^10.9.2 version: 10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@25.5.0)(typescript@5.9.3) @@ -906,8 +932,17 @@ packages: chokidar: optional: true - '@angular-devkit/schematics-cli@19.2.23': - resolution: {integrity: sha512-M8g7Gu3Lc5bbzijd2QLcQhfdpfMVE32YXQ6FIkA8x91Kmd2gb8aVvGYPLYUN5619P+ABWhN5Dn2PKuk01zz3vg==} + '@angular-devkit/core@19.2.24': + resolution: {integrity: sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/schematics-cli@19.2.24': + resolution: {integrity: sha512-bsStZQG67J1HBqTmWxtIcobvgrn32L4UOdL7hGyOru5VxDWPNA8pRnDYavT3hnJeBkJYPoQIw8u7Dm0ecoQprw==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true @@ -915,12 +950,67 @@ packages: resolution: {integrity: sha512-Jzs7YM4X6azmHU7Mw5tQSPMuvaqYS8SLnZOJbtiXCy1JyuW9bm/WBBecNHMiuZ8LHXKhvQ6AVX1tKrzF6uiDmw==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@angular-devkit/schematics@19.2.24': + resolution: {integrity: sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} '@asamuzakjp/css-color@2.8.3': resolution: {integrity: sha512-GIc76d9UI1hCvOATjZPyHFmE5qhRccp3/zGfMPapK3jBi+yocEzp6BBB0UnfRYP9NP4FANqUZYb0hnfs3TM3hw==} + '@atlaskit/atlassian-context@0.8.0': + resolution: {integrity: sha512-I4Rpupz2sfHumiLRMHG+emYM0dTSisuL0665lfou6k55jjhrLDDbfH1eBfCZV3qts/xIWnBxoSCaNpV2Vgg9ew==} + peerDependencies: + react: ^18.2.0 || ^19.0.0 + + '@atlaskit/browser-apis@0.0.1': + resolution: {integrity: sha512-XKy1lSkezw6o6D4WBuZrpezwwSHhzwMpiGAkVvaLUZF1WJ+nleQaQk3VEZwEOdmUXvIfIJnTbE1QxqsmsehUbQ==} + peerDependencies: + react: ^18.2.0 + + '@atlaskit/css@0.19.4': + resolution: {integrity: sha512-OBacSDD/XBkuu4R2FFMgOoVifSbs7E4LRwqUJFolPurxnsc88/cDrwf9C3obMwDh+64O3a5OyzuUPLVQTfMXYQ==} + peerDependencies: + react: ^18.2.0 + + '@atlaskit/ds-lib@7.0.0': + resolution: {integrity: sha512-Uqywr6YMi6uBpD0BtbRmG8f81fteRT7NBf62J8zzWPSs7u3Jq9G3Oruwm3y+57Dxv6IqfRwrS10WJARlCJdBLw==} + peerDependencies: + react: ^18.2.0 || ^19.0.0 + + '@atlaskit/feature-gate-js-client@5.5.11': + resolution: {integrity: sha512-kD+qCh2lIKxXB9IemmKiB+QUI+YKSoDmJ+dTGLehwmav0mcpzpZjrFRc/sHIZ3XefXxob761t8svrakO0hqfPw==} + + '@atlaskit/motion@6.2.2': + resolution: {integrity: sha512-8NmUFbTPDnc2yfRrkVVKCsIrI0AHPk5gU+UyDBSPlUQL6AdWr+zclpii1bAIJjvRHD3aUZwUyt9sa7WBbg2y7A==} + peerDependencies: + react: ^18.2.0 + + '@atlaskit/platform-feature-flags@1.1.3': + resolution: {integrity: sha512-dRn6UvVmMF5+WXnv7ZlxKTV2rVQncI2TgM0KTGSqpHdD9LnogITWC/rFjzUuF/W8MBCEvpouGilukqhq6rASnw==} + + '@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.5': + resolution: {integrity: sha512-InLvVhZAHPBfv3CxuG4AfOQuhNJjaFy69YBfodPMWtRFQNQAKa9Yb3vL9Ho6qsD9qKUBuJa4A5k7QddaXQ4Eyw==} + + '@atlaskit/pragmatic-drag-and-drop-flourish@2.0.15': + resolution: {integrity: sha512-jtrseeLce58/5jXHklwVUo+tzHpGtQyAH63b7AIneyphISFk5gFt9kmTEHaHRd6Q8bfdeOaZjpUbafYVZUAlAQ==} + + '@atlaskit/pragmatic-drag-and-drop-hitbox@1.1.0': + resolution: {integrity: sha512-JWt6eVp6Br2FPHRM8s0dUIHQk/jFInGP1f3ti5CdtM1Ji5/pt8Akm44wDC063Gv2i5RGseixtbW0z/t6RYtbdg==} + + '@atlaskit/pragmatic-drag-and-drop-live-region@1.3.4': + resolution: {integrity: sha512-HWIbmO4MojXGagbl9lsRTETdLSmkx+FSEzH4n5VK2MDXu/t2sYEK9vq+K9oSrMHh+S6ZkkVchjqGvD94uBlSeg==} + + '@atlaskit/pragmatic-drag-and-drop@1.8.1': + resolution: {integrity: sha512-uXWNPpL8n4OmTVbduH7nq8pk8htqGo/prR5cYEE8sVCPJGAUMWn6lzvWTfI+4VCeQvHiDRODVz4YzH06OVAxhw==} + + '@atlaskit/tokens@13.0.4': + resolution: {integrity: sha512-DTUMD/NzxtIBb31DZh3xvmcjQPsAafoT109UlcQHcwT20xue9KxiJqAWvh7OSlkI7YrhV11drqbO+p8HugStbg==} + peerDependencies: + react: ^18.2.0 || ^19.0.0 + '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -944,160 +1034,175 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-sdk/client-s3@3.1014.0': - resolution: {integrity: sha512-0XLrOT4Cm3NEhhiME7l/8LbTXS4KdsbR4dSrY207KNKTcHLLTZ9EXt4ZpgnTfLvWQF3pGP2us4Zi1fYLo0N+Ow==} + '@aws-sdk/client-s3@3.1050.0': + resolution: {integrity: sha512-9kgtv+bXZQrOIJT2INPPBCezrJu1FlgGrzEat/ut4A4V53IT00LynsBZgp12eFKbjJuNCeTo7iPSKjPsX8ub+A==} engines: {node: '>=20.0.0'} - '@aws-sdk/core@3.973.26': - resolution: {integrity: sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ==} + '@aws-sdk/core@3.974.12': + resolution: {integrity: sha512-qrqgioqYFjwR6LatVNS1L2Vk++EwRIxqSQXPKNv5Ofux2D8UNgqMQ1znnMyEImXquVPTtbf71fc128pvmU6y9A==} engines: {node: '>=20.0.0'} - '@aws-sdk/crc64-nvme@3.972.5': - resolution: {integrity: sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg==} + '@aws-sdk/crc64-nvme@3.972.8': + resolution: {integrity: sha512-fVfUCL/Xh2zINYMPZvj+iBn6XWouQf0DAnjaWCI9MkmqXzL2Iy5FoQB8O7syFe6gN6AH1ecDDU58T51Ou0kFkA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.972.24': - resolution: {integrity: sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w==} + '@aws-sdk/credential-provider-env@3.972.38': + resolution: {integrity: sha512-m3WjZEgPtioMhPmwqUt+DhlTJ2i9ufR6DhfkyXojb9puEvfR+ur2U5shavu5/Cc9WHHsDCvALi6UFHgcqjhQ5w==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.972.26': - resolution: {integrity: sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA==} + '@aws-sdk/credential-provider-http@3.972.40': + resolution: {integrity: sha512-D78L/m2Dr6cJnnSvWoAudPhQmCwmJ7j6APXsPYmFpPaKfQTfCSu0rdm8j14Np+VmXF9z8Aj8HE3xFpsrwtfgeg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.972.28': - resolution: {integrity: sha512-wXYvq3+uQcZV7k+bE4yDXCTBdzWTU9x/nMiKBfzInmv6yYK1veMK0AKvRfRBd72nGWYKcL6AxwiPg9z/pYlgpw==} + '@aws-sdk/credential-provider-ini@3.972.42': + resolution: {integrity: sha512-Mu5ESvFXeinafVM8jTIvRqcvK2Ehj4kz3auT39yUcHwu1Vfxo6xRlmUafdKLW4tusjAJukQwK09sCSMgOm7OKg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.972.28': - resolution: {integrity: sha512-ZSTfO6jqUTCysbdBPtEX5OUR//3rbD0lN7jO3sQeS2Gjr/Y+DT6SbIJ0oT2cemNw3UzKu97sNONd1CwNMthuZQ==} + '@aws-sdk/credential-provider-login@3.972.42': + resolution: {integrity: sha512-O6WkZga3kf0yqyJYd1dbeJqVhEgJx/x1UaLgtbR+XuL/YP+K5y6QTxQKL7ka9z3jnQASESKGAPnRyt4D5hQrxA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.972.29': - resolution: {integrity: sha512-clSzDcvndpFJAggLDnDb36sPdlZYyEs5Zm6zgZjjUhwsJgSWiWKwFIXUVBcbruidNyBdbpOv2tNDL9sX8y3/0g==} + '@aws-sdk/credential-provider-node@3.972.43': + resolution: {integrity: sha512-D/DJmbrWRP5BXEO3FH+ar4el+2n6OlGofiud7dQun2jES+AQEJjczenp1jBb4MBN7CpGpS8nsWGQLtuzc9tQbA==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.972.24': - resolution: {integrity: sha512-Q2k/XLrFXhEztPHqj4SLCNID3hEPdlhh1CDLBpNnM+1L8fq7P+yON9/9M1IGN/dA5W45v44ylERfXtDAlmMNmw==} + '@aws-sdk/credential-provider-process@3.972.38': + resolution: {integrity: sha512-EnbYVajGgbkb24s0K1eo4VNAPV5mHIET7LSvirTaFCwkfrfaOJxtSE+wY/tJdKDS21cEYkZs2ruCaAm+W4iblg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.972.28': - resolution: {integrity: sha512-IoUlmKMLEITFn1SiCTjPfR6KrE799FBo5baWyk/5Ppar2yXZoUdaRqZzJzK6TcJxx450M8m8DbpddRVYlp5R/A==} + '@aws-sdk/credential-provider-sso@3.972.42': + resolution: {integrity: sha512-RVV/9NbFwI8ZHEH5dn39lGyFmSbSVj1+orZdr6QsOe1mW9DCglmlen0cFaNZmCcqkqc7erNRHNBduxbeZuHAnw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.972.28': - resolution: {integrity: sha512-d+6h0SD8GGERzKe27v5rOzNGKOl0D+l0bWJdqrxH8WSQzHzjsQFIAPgIeOTUwBHVsKKwtSxc91K/SWax6XgswQ==} + '@aws-sdk/credential-provider-web-identity@3.972.42': + resolution: {integrity: sha512-/67fXX0ddllD4u2Nujc5PvT4byHgpMUfz6+RxIKi/0nFIckeorm7JvXgzBuDyVKw0s58EbofmETDWUf9vTEuHQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/lib-storage@3.1014.0': - resolution: {integrity: sha512-mM0/YpIEKKQ1hM9n1Q2HhN1ztjk3oNKqFMYP+cZ57FR3XVU4UOw9Npu7gnjfzkNrS7WTDZMe0wCMuDvvtq1Oeg==} + '@aws-sdk/lib-storage@3.1050.0': + resolution: {integrity: sha512-+PyWDxWPRSEi0l4fU1EmhLK9XvRe70cXlfUPdJqHfJ3E3DKWrihj314i7K5Z69bv015qjorIyDeHLKMHcGGp+g==} engines: {node: '>=20.0.0'} peerDependencies: - '@aws-sdk/client-s3': ^3.1014.0 + '@aws-sdk/client-s3': ^3.1050.0 - '@aws-sdk/middleware-bucket-endpoint@3.972.8': - resolution: {integrity: sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw==} + '@aws-sdk/middleware-bucket-endpoint@3.972.14': + resolution: {integrity: sha512-Aaj0d+xbo1jJquBWJP0/9V/XZRYukO3LWIRp3dOLHmoFrYKb4YZ0aLefgVHfGcNOVBS2ZTq7L/n5JcrE7DaC+Q==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-expect-continue@3.972.8': - resolution: {integrity: sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ==} + '@aws-sdk/middleware-expect-continue@3.972.12': + resolution: {integrity: sha512-dA5pKTom/Ls9mgeyeaRBNQrRIVOLVjv4AmKOB0/e4yaiXEUy0gSz2d3liP8JHtYoCAEWySU1jWnyzwLOREN+4g==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-flexible-checksums@3.974.6': - resolution: {integrity: sha512-YckB8k1ejbyCg/g36gUMFLNzE4W5cERIa4MtsdO+wpTmJEP0+TB7okWIt7d8TDOvnb7SwvxJ21E4TGOBxFpSWQ==} + '@aws-sdk/middleware-flexible-checksums@3.974.20': + resolution: {integrity: sha512-NdnMVQCR1YjIcqFAiNLdBiOwr2DyQDB2IiXQrBhzolKOv32ae4d4Ll7IzLMi04eMHiq/o/Y/GjFuVjF9HuG0QA==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-host-header@3.972.8': - resolution: {integrity: sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==} + '@aws-sdk/middleware-location-constraint@3.972.10': + resolution: {integrity: sha512-rI3NZvJcEvjoD0+0PI0iUAwlPw2IlSlhyvgBK/3WkKJQE/YiKFedd9dMN2lVacdNxPNhxL/jzQaKQdrGtQagjQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-location-constraint@3.972.8': - resolution: {integrity: sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw==} + '@aws-sdk/middleware-sdk-s3@3.972.41': + resolution: {integrity: sha512-M4T2I2WPuH5WQpU8Tsp+u2bcO29zGRkU14ATzuqb9I4xh8tzsLqtp4hzaJM5aO2dhMZnHDzyQwSFVgc3XbnoGg==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-logger@3.972.8': - resolution: {integrity: sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==} + '@aws-sdk/middleware-ssec@3.972.10': + resolution: {integrity: sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-recursion-detection@3.972.9': - resolution: {integrity: sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ==} + '@aws-sdk/nested-clients@3.997.10': + resolution: {integrity: sha512-FtQ/Bt327peZJuyo4WZSOLVUTw9ujRxntepiC7L65FxA2P82Xlq0g14T22BuqBUeMjDoxa9nvwiMHjLIfP3eUg==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-sdk-s3@3.972.27': - resolution: {integrity: sha512-gomO6DZwx+1D/9mbCpcqO5tPBqYBK7DtdgjTIjZ4yvfh/S7ETwAPS0XbJgP2JD8Ycr5CwVrEkV1sFtu3ShXeOw==} + '@aws-sdk/s3-request-presigner@3.1050.0': + resolution: {integrity: sha512-tTQ+MYyQehtA5SMXnLumFpOMrVBcn88f1k6oyzs4sOQ1Upq72T/BYkKZ+Yn7ejncSsCp3exIVcYwZHFEGKAugQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-ssec@3.972.8': - resolution: {integrity: sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw==} + '@aws-sdk/signature-v4-multi-region@3.996.27': + resolution: {integrity: sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-user-agent@3.972.28': - resolution: {integrity: sha512-cfWZFlVh7Va9lRay4PN2A9ARFzaBYcA097InT5M2CdRS05ECF5yaz86jET8Wsl2WcyKYEvVr/QNmKtYtafUHtQ==} + '@aws-sdk/token-providers@3.1049.0': + resolution: {integrity: sha512-r7+d0lQMTHKypkmaF5jRTBYLYHCUHzt3gaVoN9SidLhQeWhCmHk3AKrboDTpPF5b7Pt7vKu3+oeMjznM2Eu1ow==} engines: {node: '>=20.0.0'} - '@aws-sdk/nested-clients@3.996.18': - resolution: {integrity: sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/region-config-resolver@3.972.10': - resolution: {integrity: sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/s3-request-presigner@3.1014.0': - resolution: {integrity: sha512-XEcK50lToSoLPrQztKQhONYQW45613H8oEL00mBUd/+OZgk0+3zJ8kSNDsIJioZ3H7Be+yC3CL6a22dZFIKUXQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/signature-v4-multi-region@3.996.15': - resolution: {integrity: sha512-Ukw2RpqvaL96CjfH/FgfBmy/ZosHBqoHBCFsN61qGg99F33vpntIVii8aNeh65XuOja73arSduskoa4OJea9RQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/token-providers@3.1021.0': - resolution: {integrity: sha512-TKY6h9spUk3OLs5v1oAgW9mAeBE3LAGNBwJokLy96wwmd4W2v/tYlXseProyed9ValDj2u1jK/4Rg1T+1NXyJA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/types@3.973.6': - resolution: {integrity: sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-arn-parser@3.972.3': - resolution: {integrity: sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-endpoints@3.996.5': - resolution: {integrity: sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-format-url@3.972.8': - resolution: {integrity: sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==} + '@aws-sdk/types@3.973.8': + resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==} engines: {node: '>=20.0.0'} '@aws-sdk/util-locate-window@3.535.0': resolution: {integrity: sha512-PHJ3SL6d2jpcgbqdgiPxkXpu7Drc2PYViwxSIqvvMKhDwzSB1W3mMvtpzwKM4IE7zLFodZo0GKjJ9AsoXndXhA==} engines: {node: '>=14.0.0'} - '@aws-sdk/util-user-agent-browser@3.972.8': - resolution: {integrity: sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==} - - '@aws-sdk/util-user-agent-node@3.973.14': - resolution: {integrity: sha512-vNSB/DYaPOyujVZBg/zUznH9QC142MaTHVmaFlF7uzzfg3CgT9f/l4C0Yi+vU/tbBhxVcXVB90Oohk5+o+ZbWw==} - engines: {node: '>=20.0.0'} - peerDependencies: - aws-crt: '>=1.0.0' - peerDependenciesMeta: - aws-crt: - optional: true - - '@aws-sdk/xml-builder@3.972.16': - resolution: {integrity: sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==} + '@aws-sdk/xml-builder@3.972.24': + resolution: {integrity: sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==} engines: {node: '>=20.0.0'} '@aws/lambda-invoke-store@0.2.3': resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==} engines: {node: '>=18.0.0'} + '@azure/abort-controller@2.1.2': + resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} + engines: {node: '>=18.0.0'} + + '@azure/core-auth@1.10.1': + resolution: {integrity: sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==} + engines: {node: '>=20.0.0'} + + '@azure/core-client@1.10.1': + resolution: {integrity: sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==} + engines: {node: '>=20.0.0'} + + '@azure/core-http-compat@2.4.0': + resolution: {integrity: sha512-f1P96IB399YiN2ARYHP7EpZi3Bf3wH4SN2lGzrw7JVwm7bbsVYtf2iKSBwTywD2P62NOPZGHFSZi+6jjb75JuA==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@azure/core-client': ^1.10.0 + '@azure/core-rest-pipeline': ^1.22.0 + + '@azure/core-lro@2.7.2': + resolution: {integrity: sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==} + engines: {node: '>=18.0.0'} + + '@azure/core-paging@1.6.2': + resolution: {integrity: sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==} + engines: {node: '>=18.0.0'} + + '@azure/core-rest-pipeline@1.23.0': + resolution: {integrity: sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-tracing@1.3.1': + resolution: {integrity: sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-util@1.13.1': + resolution: {integrity: sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==} + engines: {node: '>=20.0.0'} + + '@azure/core-xml@1.5.1': + resolution: {integrity: sha512-xcNRHqCoSp4AunOALEae6A8f3qATb83gSrm31Iqb01OzblvC3/W/bfXozcq78EzIdzZzuH1bZ2NvRR0TdX709w==} + engines: {node: '>=20.0.0'} + + '@azure/logger@1.3.0': + resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} + engines: {node: '>=20.0.0'} + + '@azure/storage-blob@12.31.0': + resolution: {integrity: sha512-DBgNv10aCSxopt92DkTDD0o9xScXeBqPKGmR50FPZQaEcH4JLQ+GEOGEDv19V5BMkB7kxr+m4h6il/cCDPvmHg==} + engines: {node: '>=20.0.0'} + + '@azure/storage-common@12.3.0': + resolution: {integrity: sha512-/OFHhy86aG5Pe8dP5tsp+BuJ25JOAl9yaMU3WZbkeoiFMHFtJ7tu5ili7qEdBXNW9G5lDB19trwyI6V49F/8iQ==} + engines: {node: '>=20.0.0'} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.28.5': resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} engines: {node: '>=6.9.0'} @@ -1700,10 +1805,6 @@ packages: '@babel/regjsgen@0.8.0': resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} - '@babel/runtime@7.26.10': - resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} @@ -1754,21 +1855,12 @@ packages: '@chevrotain/cst-dts-gen@11.0.3': resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} - '@chevrotain/cst-dts-gen@11.1.2': - resolution: {integrity: sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==} - '@chevrotain/gast@11.0.3': resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} - '@chevrotain/gast@11.1.2': - resolution: {integrity: sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g==} - '@chevrotain/regexp-to-ast@11.0.3': resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} - '@chevrotain/regexp-to-ast@11.1.2': - resolution: {integrity: sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw==} - '@chevrotain/types@11.0.3': resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} @@ -1778,9 +1870,6 @@ packages: '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} - '@chevrotain/utils@11.1.2': - resolution: {integrity: sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==} - '@clickhouse/client-common@1.18.2': resolution: {integrity: sha512-J0SG6q9V31ydxonglpj9xhNRsUxCsF71iEZ784yldqMYwsHixj/9xHFDgBDX3DuMiDx/kPDfXnf+pimp08wIBA==} @@ -1797,6 +1886,11 @@ packages: peerDependencies: commander: 11.1.x + '@compiled/react@0.20.0': + resolution: {integrity: sha512-mEJuYGFxIDST1H7CpksyE6a3HRVRQmeDal26O+bCHTEZlPp7iKvs5KD1FOmd2palng+S60dPFFG+UuoZDRILwA==} + peerDependencies: + react: '>=18.0.0' + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -1857,6 +1951,9 @@ packages: peerDependencies: react: '>=16.8.0' + '@docmost/pdf-inspector@1.9.6': + resolution: {integrity: sha512-8k8N8Mwu9xbpRC1jLcz4sFv88ev2oBnW56a/2WLbrOBkfXzyZV2Tml5PikUwEWT4cUXfYfk2dGnJpWQYgCESCQ==} + '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} @@ -1869,35 +1966,20 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} - '@emoji-mart/data@1.2.1': - resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==} - - '@emoji-mart/react@1.1.1': - resolution: {integrity: sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==} - peerDependencies: - emoji-mart: ^5.2 - react: ^16.8 || ^17 || ^18 - '@epic-web/invariant@1.0.0': resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.27.4': resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} engines: {node: '>=18'} - cpu: [arm64] - os: [android] + cpu: [ppc64] + os: [aix] '@esbuild/android-arm64@0.27.4': resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} @@ -1905,10 +1987,10 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} engines: {node: '>=18'} - cpu: [arm] + cpu: [arm64] os: [android] '@esbuild/android-arm@0.27.4': @@ -1917,10 +1999,10 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} engines: {node: '>=18'} - cpu: [x64] + cpu: [arm] os: [android] '@esbuild/android-x64@0.27.4': @@ -1929,11 +2011,11 @@ packages: cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] + cpu: [x64] + os: [android] '@esbuild/darwin-arm64@0.27.4': resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} @@ -1941,10 +2023,10 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} engines: {node: '>=18'} - cpu: [x64] + cpu: [arm64] os: [darwin] '@esbuild/darwin-x64@0.27.4': @@ -1953,11 +2035,11 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] + cpu: [x64] + os: [darwin] '@esbuild/freebsd-arm64@0.27.4': resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} @@ -1965,10 +2047,10 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} engines: {node: '>=18'} - cpu: [x64] + cpu: [arm64] os: [freebsd] '@esbuild/freebsd-x64@0.27.4': @@ -1977,11 +2059,11 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} engines: {node: '>=18'} - cpu: [arm64] - os: [linux] + cpu: [x64] + os: [freebsd] '@esbuild/linux-arm64@0.27.4': resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} @@ -1989,10 +2071,10 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} engines: {node: '>=18'} - cpu: [arm] + cpu: [arm64] os: [linux] '@esbuild/linux-arm@0.27.4': @@ -2001,10 +2083,10 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} engines: {node: '>=18'} - cpu: [ia32] + cpu: [arm] os: [linux] '@esbuild/linux-ia32@0.27.4': @@ -2013,10 +2095,10 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} engines: {node: '>=18'} - cpu: [loong64] + cpu: [ia32] os: [linux] '@esbuild/linux-loong64@0.27.4': @@ -2025,10 +2107,10 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} engines: {node: '>=18'} - cpu: [mips64el] + cpu: [loong64] os: [linux] '@esbuild/linux-mips64el@0.27.4': @@ -2037,10 +2119,10 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} engines: {node: '>=18'} - cpu: [ppc64] + cpu: [mips64el] os: [linux] '@esbuild/linux-ppc64@0.27.4': @@ -2049,10 +2131,10 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} engines: {node: '>=18'} - cpu: [riscv64] + cpu: [ppc64] os: [linux] '@esbuild/linux-riscv64@0.27.4': @@ -2061,10 +2143,10 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} engines: {node: '>=18'} - cpu: [s390x] + cpu: [riscv64] os: [linux] '@esbuild/linux-s390x@0.27.4': @@ -2073,10 +2155,10 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} engines: {node: '>=18'} - cpu: [x64] + cpu: [s390x] os: [linux] '@esbuild/linux-x64@0.27.4': @@ -2085,11 +2167,11 @@ packages: cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] + cpu: [x64] + os: [linux] '@esbuild/netbsd-arm64@0.27.4': resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} @@ -2097,10 +2179,10 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} engines: {node: '>=18'} - cpu: [x64] + cpu: [arm64] os: [netbsd] '@esbuild/netbsd-x64@0.27.4': @@ -2109,11 +2191,11 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] + cpu: [x64] + os: [netbsd] '@esbuild/openbsd-arm64@0.27.4': resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} @@ -2121,10 +2203,10 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} engines: {node: '>=18'} - cpu: [x64] + cpu: [arm64] os: [openbsd] '@esbuild/openbsd-x64@0.27.4': @@ -2133,11 +2215,11 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] + cpu: [x64] + os: [openbsd] '@esbuild/openharmony-arm64@0.27.4': resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} @@ -2145,11 +2227,11 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} engines: {node: '>=18'} - cpu: [x64] - os: [sunos] + cpu: [arm64] + os: [openharmony] '@esbuild/sunos-x64@0.27.4': resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} @@ -2157,11 +2239,11 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} engines: {node: '>=18'} - cpu: [arm64] - os: [win32] + cpu: [x64] + os: [sunos] '@esbuild/win32-arm64@0.27.4': resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} @@ -2169,10 +2251,10 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} engines: {node: '>=18'} - cpu: [ia32] + cpu: [arm64] os: [win32] '@esbuild/win32-ia32@0.27.4': @@ -2181,10 +2263,10 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} engines: {node: '>=18'} - cpu: [x64] + cpu: [ia32] os: [win32] '@esbuild/win32-x64@0.27.4': @@ -2193,6 +2275,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -2203,14 +2291,30 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@eslint/config-array@0.20.1': + resolution: {integrity: sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-array@0.21.2': resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.2.3': + resolution: {integrity: sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.4.2': resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.14.0': + resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.17.0': resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2219,6 +2323,10 @@ packages: resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.28.0': + resolution: {integrity: sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@9.39.4': resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2227,6 +2335,10 @@ packages: resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/plugin-kit@0.4.1': resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2292,8 +2404,8 @@ packages: '@fastify/merge-json-schemas@0.2.1': resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} - '@fastify/multipart@9.4.0': - resolution: {integrity: sha512-Z404bzZeLSXTBmp/trCBuoVFX28pM7rhv849Q5TsbTFZHuk1lc4QjQITTPK92DKVpXmNtJXeHSSc7GYvqFpxAQ==} + '@fastify/multipart@10.0.0': + resolution: {integrity: sha512-pUx3Z1QStY7E7kwvDTIvB6P+rF5lzP+iqPgZyJyG3yBJVPvQaZxzDHYbQD89rbY0ciXrMOyGi8ezHDVexLvJDA==} '@fastify/proxy-addr@5.0.0': resolution: {integrity: sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==} @@ -2301,8 +2413,8 @@ packages: '@fastify/send@4.1.0': resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==} - '@fastify/static@9.0.0': - resolution: {integrity: sha512-r64H8Woe/vfilg5RTy7lwWlE8ZZcTrc3kebYFMEUBrMqlydhQyoiExQXdYAy2REVpST/G35+stAM8WYp1WGmMA==} + '@fastify/static@9.1.3': + resolution: {integrity: sha512-aXrYtsiryLhRxRNaxNqsn7FUISeb7rB9q4eHUPIot5aeQBLNahnz1m6thzm7JWC1poSGXS9XrX8DvuMivp2hkQ==} '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -2355,7 +2467,7 @@ packages: resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==} engines: {node: '>=18.14.1'} peerDependencies: - hono: 4.12.12 + hono: 4.12.18 '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} @@ -2526,9 +2638,6 @@ packages: '@types/node': optional: true - '@ioredis/commands@1.5.0': - resolution: {integrity: sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==} - '@ioredis/commands@1.5.1': resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} @@ -2656,6 +2765,9 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} @@ -2671,8 +2783,8 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} - '@langchain/core@1.1.39': - resolution: {integrity: sha512-DP9c7TREy6iA7HnywstmUAsNyJNYTFpRg2yBfQ+6H0l1HnvQzei9GsQ36GeOLxgRaD3vm9K8urCcawSC7yQpCw==} + '@langchain/core@1.1.46': + resolution: {integrity: sha512-i8rDC83BpItxChCw4Lf+6tAr+k+OUcbirc5ZkrhI9ywYWmvxegUljLGOGYvtJNTbEAIFkhYIODPE5QRqyjF6sA==} engines: {node: '>=20'} '@langchain/textsplitters@1.0.1': @@ -2753,8 +2865,8 @@ packages: '@mermaid-js/parser@0.6.3': resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} - '@mermaid-js/parser@1.0.1': - resolution: {integrity: sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ==} + '@mermaid-js/parser@1.1.1': + resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} '@modelcontextprotocol/sdk@1.29.0': resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} @@ -2796,76 +2908,6 @@ packages: cpu: [x64] os: [win32] - '@napi-rs/canvas-android-arm64@0.1.97': - resolution: {integrity: sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - - '@napi-rs/canvas-darwin-arm64@0.1.97': - resolution: {integrity: sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@napi-rs/canvas-darwin-x64@0.1.97': - resolution: {integrity: sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.97': - resolution: {integrity: sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - - '@napi-rs/canvas-linux-arm64-gnu@0.1.97': - resolution: {integrity: sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@napi-rs/canvas-linux-arm64-musl@0.1.97': - resolution: {integrity: sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@napi-rs/canvas-linux-riscv64-gnu@0.1.97': - resolution: {integrity: sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==} - engines: {node: '>= 10'} - cpu: [riscv64] - os: [linux] - - '@napi-rs/canvas-linux-x64-gnu@0.1.97': - resolution: {integrity: sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@napi-rs/canvas-linux-x64-musl@0.1.97': - resolution: {integrity: sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@napi-rs/canvas-win32-arm64-msvc@0.1.97': - resolution: {integrity: sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@napi-rs/canvas-win32-x64-msvc@0.1.97': - resolution: {integrity: sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@napi-rs/canvas@0.1.97': - resolution: {integrity: sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==} - engines: {node: '>= 10'} - '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -2914,8 +2956,8 @@ packages: keyv: '>=5' rxjs: ^7.8.1 - '@nestjs/cli@11.0.18': - resolution: {integrity: sha512-z72OS+sFrDgIkNu/e/vUhbnjHZwAYQS8fBJKXLiFyz8059IVuY2FKebV2YMxyhY+920d4LX1hBIAGL5qQNdR7g==} + '@nestjs/cli@11.0.21': + resolution: {integrity: sha512-F8mV0Sj/zVEouzR3NxBuJy08YHTUOmC5Xdcx3qIIaJWzrm8Vw86CHkhkaPBJ5ewRMHPDCShPmhsfwhpCcjts3A==} engines: {node: '>= 20.11'} hasBin: true peerDependencies: @@ -2927,8 +2969,8 @@ packages: '@swc/core': optional: true - '@nestjs/common@11.1.18': - resolution: {integrity: sha512-0sLq8Z+TIjLnz1Tqp0C/x9BpLbqpt1qEu0VcH4/fkE0y3F5JxhfK1AdKQ/SPbKhKgwqVDoY4gS8GQr2G6ujaWg==} + '@nestjs/common@11.1.19': + resolution: {integrity: sha512-qeiTt2tv+e5QyDKqG8HlVZb2wx64FEaSGFJouqTSRs+kG44iTfl3xlz1XqVped+rihx4hmjWgL5gkhtdK3E6+Q==} peerDependencies: class-transformer: '>=0.4.1' class-validator: '>=0.13.2' @@ -2940,14 +2982,14 @@ packages: class-validator: optional: true - '@nestjs/config@4.0.3': - resolution: {integrity: sha512-FQ3M3Ohqfl+nHAn5tp7++wUQw0f2nAk+SFKe8EpNRnIifPqvfJP6JQxPKtFLMOHbyer4X646prFG4zSRYEssQQ==} + '@nestjs/config@4.0.4': + resolution: {integrity: sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==} peerDependencies: '@nestjs/common': ^10.0.0 || ^11.0.0 rxjs: ^7.1.0 - '@nestjs/core@11.1.18': - resolution: {integrity: sha512-wR3DtGyk/LUAiPtbXDuWJJwVkWElKBY0sqnTzf9d4uM3+X18FRZhK7WFc47czsIGOdWuRsMeLYV+1Z9dO4zDEQ==} + '@nestjs/core@11.1.19': + resolution: {integrity: sha512-6nJkWa2efrYi+XlU686J9y5L7OvxpLVjT0T/sxRKE7Jvpffiihelup4WSvLvRhdHDjj/5SuoWEwqReXAaaeHmw==} engines: {node: '>= 20'} peerDependencies: '@nestjs/common': ^11.0.0 @@ -2994,8 +3036,8 @@ packages: '@nestjs/common': ^10.0.0 || ^11.0.0 passport: ^0.5.0 || ^0.6.0 || ^0.7.0 - '@nestjs/platform-fastify@11.1.18': - resolution: {integrity: sha512-iJtbqQz51k7Z1vOTUEHO1mU8PsDO1WdgPSJ/6CuXBnazkrkePXoszhefFaPwJreBVn35GE3WTd/6ou7bFwnhmA==} + '@nestjs/platform-fastify@11.1.19': + resolution: {integrity: sha512-PdldJPw+xu8JM7VNE2FY+ty+qoxDMW7326h/z0MtfZvKj84FE6zuqpcSXen1CYjtyP8og+x/5XrJbKKKDNabtQ==} peerDependencies: '@fastify/static': ^8.0.0 || ^9.0.0 '@fastify/view': ^10.0.0 || ^11.0.0 @@ -3007,15 +3049,15 @@ packages: '@fastify/view': optional: true - '@nestjs/platform-socket.io@11.1.18': - resolution: {integrity: sha512-DiFRpMIdFaHqZQFwqLqGHMdNurrKVkRkMHxIrecjooPHJNNIMgrbpYZ+oJW8hpwifUyZUL4r4uXXRy4+yFEMjA==} + '@nestjs/platform-socket.io@11.1.19': + resolution: {integrity: sha512-gu1nPIEaP5Qjjg/Cl8wXyvwGpdZGzgbtK4KcH65YRAA+GTKUkIHb4BNpLJ27Ymq/wqLJKNEbCjajfzD0BEjMGA==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/websockets': ^11.0.0 rxjs: ^7.1.0 - '@nestjs/schedule@6.1.1': - resolution: {integrity: sha512-kQl1RRgi02GJ0uaUGCrXHCcwISsCsJDciCKe38ykJZgnAeeoeVWs8luWtBo4AqAAXm4nS5K8RlV0smHUJ4+2FA==} + '@nestjs/schedule@6.1.3': + resolution: {integrity: sha512-RflMFOpR16Dwd1jAUbeB4mfGTCh65fvEdL4mSjQPJChpkRGRjIXjb+6YQcK2faQrVT60c9DmLmoVR7/ONCtuYQ==} peerDependencies: '@nestjs/common': ^10.0.0 || ^11.0.0 '@nestjs/core': ^10.0.0 || ^11.0.0 @@ -3073,8 +3115,8 @@ packages: typeorm: optional: true - '@nestjs/testing@11.1.18': - resolution: {integrity: sha512-frzwNlpBgtAzI3hp/qo57DZoRO4RMTH1wST3QUYEhRTHyfPkLpzkWz3jV/mhApXjD0yT56Ptlzn6zuYPLh87Lw==} + '@nestjs/testing@11.1.19': + resolution: {integrity: sha512-/UFNWXvPEdu4v4DlC5oWLbGKmD27LehLK06b8oLzs6D6lf4vAQTdST8LRAXBadyMUQnVEQWMuBo3CtAVtlfXtQ==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -3093,8 +3135,8 @@ packages: '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 reflect-metadata: ^0.1.13 || ^0.2.0 - '@nestjs/websockets@11.1.18': - resolution: {integrity: sha512-HLO/QGlJoJaMMDphgjbcIwW7/8EA8nnFQjf+qPvA+wWLLfPKoiFPUezc5m9YgN2A7jmzwo6YmEAXeHyqO9tvTw==} + '@nestjs/websockets@11.1.19': + resolution: {integrity: sha512-2qo8jtIwwwgkqAI1BtnJ02EaFLrRkKA39eYXS8IhZCHilhBHCWdjnJ5cLcFq4oF+s+KZ7LcLGD/3stxJy8ijzg==} peerDependencies: '@nestjs/common': ^11.0.0 '@nestjs/core': ^11.0.0 @@ -3113,6 +3155,9 @@ packages: resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} engines: {node: '>= 20.19.0'} + '@nodable/entities@2.1.0': + resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} + '@node-saml/node-saml@5.1.0': resolution: {integrity: sha512-t3cJnZ4aC7HhPZ6MGylGZULvUtBOZ6FzuUndaHGXjmIZHXnLfC/7L8a57O9Q9V7AxJGKAiRM5zu2wNm9EsvQpw==} engines: {node: '>= 18'} @@ -3277,11 +3322,11 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@posthog/core@1.24.1': - resolution: {integrity: sha512-e8AciAnc6MRFws89ux8lJKFAaI03yEon0ASDoUO7yS91FVqbUGXYekObUUR3LHplcg+pmyiJBI0jolY0SFbGRA==} + '@posthog/core@1.27.6': + resolution: {integrity: sha512-FjvgPdORywAjgjtgkZJ2/x9ED52jtOJym/RVldY4Oa7wzmlY49rxZm8gvOlocEnjP90bSbj3ko7qVjXNhftFvA==} - '@posthog/types@1.363.1': - resolution: {integrity: sha512-bFYk5XHgYEfVhQU0AwkG9MbMqq9QRbKDDJxOtYWGJ6Uw+/nLRNs/ZydXy3aMt0ldIdkNzZq+qaJ/p2Jg0+mP8g==} + '@posthog/types@1.372.2': + resolution: {integrity: sha512-dx+WImdDg2NDqaDowTmW+BMNalUfPKngR+g1Iom8ULSav+fGacxexv6fSOl0uSVBwYZsDFe7qNUu0NB/rwGjEw==} '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -3289,8 +3334,8 @@ packages: '@protobufjs/base64@1.1.2': resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - '@protobufjs/codegen@2.0.4': - resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} '@protobufjs/eventemitter@1.1.0': resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} @@ -3301,8 +3346,8 @@ packages: '@protobufjs/float@1.0.2': resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - '@protobufjs/inquire@1.1.0': - resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + '@protobufjs/inquire@1.1.1': + resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==} '@protobufjs/path@1.1.2': resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} @@ -3310,8 +3355,8 @@ packages: '@protobufjs/pool@1.1.0': resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - '@protobufjs/utf8@1.1.0': - resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} '@radix-ui/number@1.1.1': resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} @@ -4003,174 +4048,13 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@react-dnd/asap@4.0.1': - resolution: {integrity: sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==} - - '@react-dnd/invariant@2.0.0': - resolution: {integrity: sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==} - - '@react-dnd/shallowequal@2.0.0': - resolution: {integrity: sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==} - - '@react-email/body@0.3.0': - resolution: {integrity: sha512-uGo0BOOzjbMUo3lu+BIDWayvn5o6Xyfmnlla5VGf05n8gHMvO1ll7U4FtzWe3hxMLwt53pmc4iE0M+B5slG+Ug==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/button@0.2.1': - resolution: {integrity: sha512-qXyj7RZLE7POy9BMKSoqQ00tOXThjOZSUnI2Yu9i29IHngPlmrNayIWBoVKtElES7OWwypUcpiajwi1mUWx6/A==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/code-block@0.2.1': - resolution: {integrity: sha512-M3B7JpVH4ytgn83/ujRR1k1DQHvTeABiDM61OvAbjLRPhC/5KLHU5KkzIbbuGIrjWwxAbL1kSQzU8MhLEtSxyw==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/code-inline@0.0.6': - resolution: {integrity: sha512-jfhebvv3dVsp3OdPgKXnk8+e2pBiDVZejDOBFzBa/IblrAJ9cQDkN6rBD5IyEg8hTOxwbw3iaI/yZFmDmIguIA==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/column@0.0.14': - resolution: {integrity: sha512-f+W+Bk2AjNO77zynE33rHuQhyqVICx4RYtGX9NKsGUg0wWjdGP0qAuIkhx9Rnmk4/hFMo1fUrtYNqca9fwJdHg==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/components@1.0.10': - resolution: {integrity: sha512-r/BnqfAjr3apcvn/NDx2DqNRD5BP5wZLRdjn2IVHXjt4KmQ5RHWSCAvFiXAzRHys1BWQ2zgIc7cpWePUcAl+nw==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/container@0.0.16': - resolution: {integrity: sha512-QWBB56RkkU0AJ9h+qy33gfT5iuZknPC7Un/IjZv9B0QmMIK+WWacc0cH6y2SV5Cv/b99hU94fjEMOOO4enpkbQ==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/font@0.0.10': - resolution: {integrity: sha512-0urVSgCmQIfx5r7Xc586miBnQUVnGp3OTYUm8m5pwtQRdTRO5XrTtEfNJ3JhYhSOruV0nD8fd+dXtKXobum6tA==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/head@0.0.13': - resolution: {integrity: sha512-AJg6le/08Gz4tm+6MtKXqtNNyKHzmooOCdmtqmWxD7FxoAdU1eVcizhtQ0gcnVaY6ethEyE/hnEzQxt1zu5Kog==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/heading@0.0.16': - resolution: {integrity: sha512-jmsKnQm1ykpBzw4hCYHwBkt5pW2jScXffPeEH5ZRF5tZeF5b1pvlFTO9han7C0pCkZYo1kEvWiRtx69yfCIwuw==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/hr@0.0.12': - resolution: {integrity: sha512-TwmOmBDibavUQpXBxpmZYi2Iks/yeZOzFYh+di9EltMSnEabH8dMZXrl+pxNXzCgZ2XE8HY7VmUL65Lenfu5PA==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/html@0.0.12': - resolution: {integrity: sha512-KTShZesan+UsreU7PDUV90afrZwU5TLwYlALuCSU0OT+/U8lULNNbAUekg+tGwCnOfIKYtpDPKkAMRdYlqUznw==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/img@0.0.12': - resolution: {integrity: sha512-sRCpEARNVTf3FQhZOC+JTvu5r6ubiYWkT0ucYXg8ctkyi4G8QG+jgYPiNUqVeTLA2STOfmPM/nrk1nb84y6CPQ==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/link@0.0.13': - resolution: {integrity: sha512-lkWc/NjOcefRZMkQoSDDbuKBEBDES9aXnFEOuPH845wD3TxPwh+QTf0fStuzjoRLUZWpHnio4z7qGGRYusn/sw==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/markdown@0.0.18': - resolution: {integrity: sha512-gSuYK5fsMbGk87jDebqQ6fa2fKcWlkf2Dkva8kMONqLgGCq8/0d+ZQYMEJsdidIeBo3kmsnHZPrwdFB4HgjUXg==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/preview@0.0.14': - resolution: {integrity: sha512-aYK8q0IPkBXyMsbpMXgxazwHxYJxTrXrV95GFuu2HbEiIToMwSyUgb8HDFYwPqqfV03/jbwqlsXmFxsOd+VNaw==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/render@2.0.4': - resolution: {integrity: sha512-kht2oTFQ1SwrLpd882ahTvUtNa9s53CERHstiTbzhm6aR2Hbykp/mQ4tpPvsBGkKAEvKRlDEoooh60Uk6nHK1g==} + '@react-email/render@2.0.8': + resolution: {integrity: sha512-5udvVr3U/WuGJZfLdLBOhkzrqRWd2Q5ZYmF7ppcy7FzWcwgshdqLMNqJOXcVzAXJXg/2bm7D+WGJzTtZOZMQnQ==} engines: {node: '>=20.0.0'} peerDependencies: react: ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^18.0 || ^19.0 || ^19.0.0-rc - '@react-email/row@0.0.13': - resolution: {integrity: sha512-bYnOac40vIKCId7IkwuLAAsa3fKfSfqCvv6epJKmPE0JBuu5qI4FHFCl9o9dVpIIS08s/ub+Y/txoMt0dYziGw==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/section@0.0.17': - resolution: {integrity: sha512-qNl65ye3W0Rd5udhdORzTV9ezjb+GFqQQSae03NDzXtmJq6sqVXNWNiVolAjvJNypim+zGXmv6J9TcV5aNtE/w==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - - '@react-email/tailwind@2.0.6': - resolution: {integrity: sha512-3PgL/GYWmgS+puLPQ2aLlsplHSOFztRl70fowBkbLIb8ZUIgvx5YId6zYCCHeM2+DQ/EG3iXXqLNTahVztuMqQ==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@react-email/body': '>=0' - '@react-email/button': '>=0' - '@react-email/code-block': '>=0' - '@react-email/code-inline': '>=0' - '@react-email/container': '>=0' - '@react-email/heading': '>=0' - '@react-email/hr': '>=0' - '@react-email/img': '>=0' - '@react-email/link': '>=0' - '@react-email/preview': '>=0' - '@react-email/text': '>=0' - react: ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@react-email/body': - optional: true - '@react-email/button': - optional: true - '@react-email/code-block': - optional: true - '@react-email/code-inline': - optional: true - '@react-email/container': - optional: true - '@react-email/heading': - optional: true - '@react-email/hr': - optional: true - '@react-email/img': - optional: true - '@react-email/link': - optional: true - '@react-email/preview': - optional: true - - '@react-email/text@0.1.6': - resolution: {integrity: sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw==} - engines: {node: '>=20.0.0'} - peerDependencies: - react: ^18.0 || ^19.0 || ^19.0.0-rc - '@redis/client@5.11.0': resolution: {integrity: sha512-GHoprlNQD51Xq2Ztd94HHV94MdFZQ3CVrpA04Fz8MVoHM0B7SlbmPEVIjwTbcv58z8QyjnrOuikS0rWF03k5dQ==} engines: {node: '>= 18'} @@ -4298,222 +4182,58 @@ packages: '@sinonjs/fake-timers@15.1.1': resolution: {integrity: sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==} - '@smithy/abort-controller@4.2.12': - resolution: {integrity: sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==} + '@slidoapp/emoji-mart-data@1.2.4': + resolution: {integrity: sha512-wyZjYb0Q/Cxgq+90G9Bs8Ynvaed3WMgXhkKs/7LOFd5XWfdQ5XQEPeiv52xp8zuNHjCQK964JiK/YP0g0bpQsQ==} + + '@slidoapp/emoji-mart-react@1.1.5': + resolution: {integrity: sha512-I0FPvnxazx8AjiWXvU3KcusZ0Aw+FpC57ikyXyeHi9aMUUSYZbXkiaIumKmYtAPWP/9S+q3cCWXnECFHokOV1A==} + peerDependencies: + '@slidoapp/emoji-mart': ^5.6 + react: ^16.8 || ^17 || ^18 || ^19 + + '@slidoapp/emoji-mart@5.8.7': + resolution: {integrity: sha512-zn8CBoZRFY0M0Tps0rFgcbu1nhQDmnSoD8CqkTVobI9k3MK2WHIYRp50DXhMxRoQnoct4nomrVaY9kU01ARDTQ==} + + '@smithy/core@3.24.3': + resolution: {integrity: sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==} engines: {node: '>=18.0.0'} - '@smithy/chunked-blob-reader-native@4.2.3': - resolution: {integrity: sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==} + '@smithy/credential-provider-imds@4.3.3': + resolution: {integrity: sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==} engines: {node: '>=18.0.0'} - '@smithy/chunked-blob-reader@5.2.2': - resolution: {integrity: sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==} - engines: {node: '>=18.0.0'} - - '@smithy/config-resolver@4.4.13': - resolution: {integrity: sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==} - engines: {node: '>=18.0.0'} - - '@smithy/core@3.23.13': - resolution: {integrity: sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q==} - engines: {node: '>=18.0.0'} - - '@smithy/credential-provider-imds@4.2.12': - resolution: {integrity: sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-codec@4.2.12': - resolution: {integrity: sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-browser@4.2.12': - resolution: {integrity: sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-config-resolver@4.3.12': - resolution: {integrity: sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-node@4.2.12': - resolution: {integrity: sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-universal@4.2.12': - resolution: {integrity: sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==} - engines: {node: '>=18.0.0'} - - '@smithy/fetch-http-handler@5.3.15': - resolution: {integrity: sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==} - engines: {node: '>=18.0.0'} - - '@smithy/hash-blob-browser@4.2.13': - resolution: {integrity: sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g==} - engines: {node: '>=18.0.0'} - - '@smithy/hash-node@4.2.12': - resolution: {integrity: sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==} - engines: {node: '>=18.0.0'} - - '@smithy/hash-stream-node@4.2.12': - resolution: {integrity: sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw==} - engines: {node: '>=18.0.0'} - - '@smithy/invalid-dependency@4.2.12': - resolution: {integrity: sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==} + '@smithy/fetch-http-handler@5.4.3': + resolution: {integrity: sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==} engines: {node: '>=18.0.0'} '@smithy/is-array-buffer@2.2.0': resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} engines: {node: '>=14.0.0'} - '@smithy/is-array-buffer@4.2.2': - resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} + '@smithy/node-http-handler@4.7.3': + resolution: {integrity: sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==} engines: {node: '>=18.0.0'} - '@smithy/md5-js@4.2.12': - resolution: {integrity: sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ==} + '@smithy/signature-v4@5.4.3': + resolution: {integrity: sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==} engines: {node: '>=18.0.0'} - '@smithy/middleware-content-length@4.2.12': - resolution: {integrity: sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==} + '@smithy/types@4.14.1': + resolution: {integrity: sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==} engines: {node: '>=18.0.0'} - '@smithy/middleware-endpoint@4.4.28': - resolution: {integrity: sha512-p1gfYpi91CHcs5cBq982UlGlDrxoYUX6XdHSo91cQ2KFuz6QloHosO7Jc60pJiVmkWrKOV8kFYlGFFbQ2WUKKQ==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-retry@4.4.46': - resolution: {integrity: sha512-SpvWNNOPOrKQGUqZbEPO+es+FRXMWvIyzUKUOYdDgdlA6BdZj/R58p4umoQ76c2oJC44PiM7mKizyyex1IJzow==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-serde@4.2.16': - resolution: {integrity: sha512-beqfV+RZ9RSv+sQqor3xroUUYgRFCGRw6niGstPG8zO9LgTl0B0MCucxjmrH/2WwksQN7UUgI7KNANoZv+KALA==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-stack@4.2.12': - resolution: {integrity: sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==} - engines: {node: '>=18.0.0'} - - '@smithy/node-config-provider@4.3.12': - resolution: {integrity: sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==} - engines: {node: '>=18.0.0'} - - '@smithy/node-http-handler@4.5.1': - resolution: {integrity: sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw==} - engines: {node: '>=18.0.0'} - - '@smithy/property-provider@4.2.12': - resolution: {integrity: sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==} - engines: {node: '>=18.0.0'} - - '@smithy/protocol-http@5.3.12': - resolution: {integrity: sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==} - engines: {node: '>=18.0.0'} - - '@smithy/querystring-builder@4.2.12': - resolution: {integrity: sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==} - engines: {node: '>=18.0.0'} - - '@smithy/querystring-parser@4.2.12': - resolution: {integrity: sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==} - engines: {node: '>=18.0.0'} - - '@smithy/service-error-classification@4.2.12': - resolution: {integrity: sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==} - engines: {node: '>=18.0.0'} - - '@smithy/shared-ini-file-loader@4.4.7': - resolution: {integrity: sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==} - engines: {node: '>=18.0.0'} - - '@smithy/signature-v4@5.3.12': - resolution: {integrity: sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==} - engines: {node: '>=18.0.0'} - - '@smithy/smithy-client@4.12.8': - resolution: {integrity: sha512-aJaAX7vHe5i66smoSSID7t4rKY08PbD8EBU7DOloixvhOozfYWdcSYE4l6/tjkZ0vBZhGjheWzB2mh31sLgCMA==} - engines: {node: '>=18.0.0'} - - '@smithy/types@4.13.1': - resolution: {integrity: sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==} - engines: {node: '>=18.0.0'} - - '@smithy/url-parser@4.2.12': - resolution: {integrity: sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-base64@4.3.2': - resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-body-length-browser@4.2.2': - resolution: {integrity: sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-body-length-node@4.2.3': - resolution: {integrity: sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==} + '@smithy/types@4.14.2': + resolution: {integrity: sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==} engines: {node: '>=18.0.0'} '@smithy/util-buffer-from@2.2.0': resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} engines: {node: '>=14.0.0'} - '@smithy/util-buffer-from@4.2.2': - resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==} - engines: {node: '>=18.0.0'} - - '@smithy/util-config-provider@4.2.2': - resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-defaults-mode-browser@4.3.44': - resolution: {integrity: sha512-eZg6XzaCbVr2S5cAErU5eGBDaOVTuTo1I65i4tQcHENRcZ8rMWhQy1DaIYUSLyZjsfXvmCqZrstSMYyGFocvHA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-defaults-mode-node@4.2.48': - resolution: {integrity: sha512-FqOKTlqSaoV3nzO55pMs5NBnZX8EhoI0DGmn9kbYeXWppgHD6dchyuj2HLqp4INJDJbSrj6OFYJkAh/WhSzZPg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-endpoints@3.3.3': - resolution: {integrity: sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==} - engines: {node: '>=18.0.0'} - - '@smithy/util-hex-encoding@4.2.2': - resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-middleware@4.2.12': - resolution: {integrity: sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-retry@4.2.13': - resolution: {integrity: sha512-qQQsIvL0MGIbUjeSrg0/VlQ3jGNKyM3/2iU3FPNgy01z+Sp4OvcaxbgIoFOTvB61ZoohtutuOvOcgmhbD0katQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-stream@4.5.21': - resolution: {integrity: sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q==} - engines: {node: '>=18.0.0'} - - '@smithy/util-uri-escape@4.2.2': - resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==} - engines: {node: '>=18.0.0'} - '@smithy/util-utf8@2.3.0': resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} engines: {node: '>=14.0.0'} - '@smithy/util-utf8@4.2.2': - resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} - engines: {node: '>=18.0.0'} - - '@smithy/util-waiter@4.2.14': - resolution: {integrity: sha512-2zqq5o/oizvMaFUlNiTyZ7dbgYv1a893aGut2uaxtbzTx/VYYnRxWzDHuD/ftgcw94ffenua+ZNLrbqwUYE+Bg==} - engines: {node: '>=18.0.0'} - - '@smithy/uuid@1.1.2': - resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} - engines: {node: '>=18.0.0'} - '@socket.io/component-emitter@3.1.0': resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} @@ -4526,6 +4246,12 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@statsig/client-core@3.33.0': + resolution: {integrity: sha512-i0eiAtFkm7Ny38etC5rJ/fkWiAECgyfK23/KIFk8z8LvPW77QmUXJfNeTqFikEOKAsAGU/pxXmchpo8hn9nWBA==} + + '@statsig/js-client@3.33.0': + resolution: {integrity: sha512-jwAjyCI0FQSvoviQqdO2u1pchuG88eUT2/EoCtEV4vda4XilzuX3eUBK+J2nhRZB2u8CLNV7PZoONdy/Tls7yQ==} + '@swc/core-darwin-arm64@1.5.25': resolution: {integrity: sha512-YbD0SBgVJS2DM0vwJTU5m7+wOyCjHPBDMf3nCBJQzFZzOLzK11eRW7SzU2jhJHr9HI9sKcNFfN4lIC2Sj+4inA==} engines: {node: '>=10'} @@ -4601,8 +4327,8 @@ packages: '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} - '@swc/types@0.1.25': - resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} + '@swc/types@0.1.26': + resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==} '@tabler/icons-react@3.40.0': resolution: {integrity: sha512-oO5+6QCnna4a//mYubx4euZfECtzQZFDGsDMIdzZUhbdyBCT+3bRVFBPueGIcemWld4Vb/0UQ39C/cmGfGylAg==} @@ -4621,11 +4347,11 @@ packages: typescript: optional: true - '@tanstack/query-core@5.99.1': - resolution: {integrity: sha512-5E8xwxyWvr22yt7zvzP3KOZ5TUElOdVA45NP3/Ao1m9mvc9i18NLTDe9m3M00BH2DR5J20cv7xckMPlhKNs+vQ==} + '@tanstack/query-core@5.90.17': + resolution: {integrity: sha512-hDww+RyyYhjhUfoYQ4es6pbgxY7LNiPWxt4l1nJqhByjndxJ7HIjDxTBtfvMr5HwjYavMrd+ids5g4Rfev3lVQ==} - '@tanstack/react-query@5.99.1': - resolution: {integrity: sha512-akg5GdwW70lvJvCqVHZ7tizGyc+TATjUzKX9RuF1xknhEe/1leofXk7YLYbrbRsuhNbHJBAayaQUMvvOFZ5L5g==} + '@tanstack/react-query@5.90.17': + resolution: {integrity: sha512-PGc2u9KLwohDUSchjW9MZqeDQJfJDON7y4W7REdNBgiFKxQy+Pf7eGjiFWEj5xPqKzAeHYdAb62IWI1a9UJyGQ==} peerDependencies: react: ^18 || ^19 @@ -4649,6 +4375,29 @@ packages: '@tanstack/virtual-core@3.14.0': resolution: {integrity: sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==} + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.6.0': + resolution: {integrity: sha512-Y76dmd7C85xekWqylJqRmO6lr83cdVprTs0muSvkXr6M73auYK5OvZMc3tKe1F7wMFdzfeBCwVbkoGrRKWb+fg==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.1.0': + resolution: {integrity: sha512-Q2ToPvg0KsVL0ohND9A3zLJWcOXXcO8IDu3fj11KhNt0UlCWyFyvnCIBkd12tidB2lkiVRG8VFqdhcqhqnAQtg==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@tiptap/core@3.20.4': resolution: {integrity: sha512-3i/DG89TFY/b34T5P+j35UcjYuB5d3+9K8u6qID+iUqNPiza015HPIZLuPfE5elNwVdV3EXIoPo0LLeBLgXXAg==} peerDependencies: @@ -4935,6 +4684,9 @@ packages: '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -4959,6 +4711,9 @@ packages: '@types/bytes@3.1.5': resolution: {integrity: sha512-VgZkrJckypj85YxEsEavcMmmSOIzkUHqWmM4CCyia5dc54YwsXzJ5uT4fYxBQNEXx+oF1krlhgCbvfubXqZYsQ==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -5070,6 +4825,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -5079,6 +4837,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/express-serve-static-core@4.17.43': resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==} @@ -5324,6 +5085,10 @@ packages: resolution: {integrity: sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typespec/ts-http-runtime@0.3.5': + resolution: {integrity: sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==} + engines: {node: '>=20.0.0'} + '@ucast/core@1.10.2': resolution: {integrity: sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==} @@ -5454,6 +5219,35 @@ packages: babel-plugin-react-compiler: optional: true + '@vitest/expect@4.1.6': + resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==} + + '@vitest/mocker@4.1.6': + resolution: {integrity: sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.6': + resolution: {integrity: sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==} + + '@vitest/runner@4.1.6': + resolution: {integrity: sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==} + + '@vitest/snapshot@4.1.6': + resolution: {integrity: sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==} + + '@vitest/spy@4.1.6': + resolution: {integrity: sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==} + + '@vitest/utils@4.1.6': + resolution: {integrity: sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==} + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -5503,8 +5297,8 @@ packages: resolution: {integrity: sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q==} engines: {node: '>= 16'} - '@xmldom/xmldom@0.8.12': - resolution: {integrity: sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==} + '@xmldom/xmldom@0.8.13': + resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} engines: {node: '>=10.0.0'} '@xtuc/ieee754@1.2.0': @@ -5564,10 +5358,6 @@ packages: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} - agent-base@7.1.1: - resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} - engines: {node: '>= 14'} - agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} @@ -5634,10 +5424,6 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -5667,8 +5453,11 @@ packages: resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} engines: {node: '>=10'} - array-buffer-byte-length@1.0.1: - resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} array-buffer-byte-length@1.0.2: @@ -5698,10 +5487,6 @@ packages: resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} engines: {node: '>= 0.4'} - arraybuffer.prototype.slice@1.0.3: - resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} - engines: {node: '>= 0.4'} - arraybuffer.prototype.slice@1.0.4: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} @@ -5709,6 +5494,10 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + async-lock@1.4.1: resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} @@ -5735,8 +5524,8 @@ packages: avvio@9.1.0: resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==} - axios@1.15.0: - resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} + axios@1.16.0: + resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==} babel-jest@30.3.0: resolution: {integrity: sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==} @@ -5826,6 +5615,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bind-event-listener@3.0.0: + resolution: {integrity: sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -5855,8 +5647,8 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - brace-expansion@5.0.5: - resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} braces@3.0.3: @@ -5893,8 +5685,9 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - bullmq@5.71.0: - resolution: {integrity: sha512-aeNWh4drsafSKnAJeiNH/nZP/5O8ZdtdMbnOPZmpjXj7NZUP5YC901U3bIH41iZValm7d1i3c34ojv7q31m30w==} + bullmq@5.76.10: + resolution: {integrity: sha512-LWve7SpQjYSpCP2GEsWmoyzTz2H37L8HRmSTu3YihYsTOr5kJxrfEX6aEV7m6eskEMWXSHZYTMZepX6qNaH6CQ==} + engines: {node: '>=12.22.0'} bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} @@ -5907,10 +5700,6 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} - call-bind@1.0.7: - resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} - engines: {node: '>= 0.4'} - call-bind@1.0.8: resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} engines: {node: '>= 0.4'} @@ -5941,14 +5730,18 @@ packages: canvas-roundrect-polyfill@0.0.1: resolution: {integrity: sha512-yWq+R3U3jE+coOeEb3a3GgE2j/0MMiDKM/QpLb6h9ihf5fGY9UXtvK9o4vNqjWXoZz7/3EaSVU3IX53TvFFUOw==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -5975,9 +5768,6 @@ packages: chevrotain@11.0.3: resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} - chevrotain@11.1.2: - resolution: {integrity: sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==} - chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -5994,8 +5784,8 @@ packages: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} - citty@0.1.6: - resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + citty@0.2.2: + resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} cjs-module-lexer@2.2.0: resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} @@ -6014,10 +5804,6 @@ packages: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} - cli-cursor@5.0.0: - resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} - engines: {node: '>=18'} - cli-spinners@2.6.1: resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} engines: {node: '>=6'} @@ -6127,9 +5913,6 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - confbox@0.2.2: - resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} - consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -6232,8 +6015,8 @@ packages: engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} hasBin: true - cross-fetch@4.0.0: - resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} @@ -6242,10 +6025,17 @@ packages: css-select@5.1.0: resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@6.1.0: resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} engines: {node: '>= 6'} + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -6255,8 +6045,8 @@ packages: resolution: {integrity: sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==} engines: {node: '>=18'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} csv-stringify@6.7.0: resolution: {integrity: sha512-UdtziYp5HuTz7e5j8Nvq+a/3HQo+2/aJZ9xntNTpmRRIg/3YYqDVgiS9fvAhtNbnyfbv2ZBe0bqCHqzhE7FqWQ==} @@ -6421,26 +6211,14 @@ packages: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} - data-view-buffer@1.0.1: - resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} - engines: {node: '>= 0.4'} - data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} - data-view-byte-length@1.0.1: - resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} - engines: {node: '>= 0.4'} - data-view-byte-length@1.0.2: resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} engines: {node: '>= 0.4'} - data-view-byte-offset@1.0.0: - resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} - engines: {node: '>= 0.4'} - data-view-byte-offset@1.0.1: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} @@ -6480,15 +6258,6 @@ packages: supports-color: optional: true - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -6598,13 +6367,16 @@ packages: dingbat-to-unicode@1.0.1: resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==} - dnd-core@14.0.1: - resolution: {integrity: sha512-+PVS2VPTgKFPYWo3vAFEA8WPbTf7/xo43TifH9G8S1KqnrQu0o77A3unrF5yOugy4mIz7K5wAVFHUcha7wsz6A==} - doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -6618,8 +6390,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.3.3: - resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==} + dompurify@3.4.1: + resolution: {integrity: sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -6640,14 +6412,14 @@ packages: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} - dotenv@17.2.3: - resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} - engines: {node: '>=12'} - dotenv@17.2.4: resolution: {integrity: sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==} engines: {node: '>=12'} + dotenv@17.4.1: + resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} + engines: {node: '>=12'} + duck@0.1.12: resolution: {integrity: sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==} @@ -6673,12 +6445,6 @@ packages: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} - emoji-mart@5.6.0: - resolution: {integrity: sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==} - - emoji-regex@10.6.0: - resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -6703,10 +6469,6 @@ packages: resolution: {integrity: sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==} engines: {node: '>=10.2.0'} - enhanced-resolve@5.19.0: - resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} - engines: {node: '>=10.13.0'} - enhanced-resolve@5.20.1: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} @@ -6742,10 +6504,6 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - es-abstract@1.23.5: - resolution: {integrity: sha512-vlmniQ0WNPwXqA0BnmwV3Ng7HxiGlh6r5U6JcTMNx8OilcAGqVJBHJcPjqOMaczU9fRuRK5Px2BdVyPRnKMMVQ==} - engines: {node: '>= 0.4'} - es-abstract@1.24.1: resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} engines: {node: '>= 0.4'} @@ -6762,8 +6520,8 @@ packages: resolution: {integrity: sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==} engines: {node: '>= 0.4'} - es-module-lexer@2.0.0: - resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} @@ -6780,17 +6538,20 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es-toolkit@1.46.1: + resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} + es6-promise-pool@2.5.0: resolution: {integrity: sha512-VHErXfzR/6r/+yyzPKeBvO0lgjfC5cbDCQWjWwMZWSb6YU39TGIl51OUmCfWCq4ylMdJSB8zkz2vIuIeIxXApA==} engines: {node: '>=0.10.0'} - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} engines: {node: '>=18'} hasBin: true - esbuild@0.27.4: - resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} engines: {node: '>=18'} hasBin: true @@ -6860,6 +6621,16 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@9.28.0: + resolution: {integrity: sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + eslint@9.39.4: resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -6895,6 +6666,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -6929,6 +6703,10 @@ packages: resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} engines: {node: '>= 0.8.0'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + expect@30.2.0: resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -6947,9 +6725,6 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} - exsolve@1.0.7: - resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} - fast-bm25@0.0.5: resolution: {integrity: sha512-6HTiLmPkgeqcPJHccN0pXdqnA7OzhaEQZTFzWnfjIyPoX5sGVKUUpfRc2K2o6zMwK+g009miRhADYn/f2Ax0Mg==} engines: {node: '>=14.0.0'} @@ -6987,11 +6762,11 @@ packages: fast-uri@3.0.6: resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} - fast-xml-builder@1.1.4: - resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} + fast-xml-builder@1.2.0: + resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} - fast-xml-parser@5.5.8: - resolution: {integrity: sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==} + fast-xml-parser@5.7.3: + resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} hasBin: true fastify-ip@2.0.0: @@ -7004,8 +6779,8 @@ packages: fastify-plugin@5.1.0: resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} - fastify@5.8.3: - resolution: {integrity: sha512-XJXpRQ41+rsJ/GLeP9vyDC+fBXilcTlEXokMSexkdEkla4uf7ZQNaI5xl3el+kW5TZQulqYxLr659ey/KX7XmQ==} + fastify@5.8.5: + resolution: {integrity: sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==} fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -7083,9 +6858,6 @@ packages: debug: optional: true - for-each@0.3.3: - resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} - for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -7145,10 +6917,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - function.prototype.name@1.1.6: - resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} - engines: {node: '>= 0.4'} - function.prototype.name@1.1.8: resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} engines: {node: '>= 0.4'} @@ -7168,10 +6936,6 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.4.0: - resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} - engines: {node: '>=18'} - get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -7192,10 +6956,6 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} - get-symbol-description@1.0.2: - resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} - engines: {node: '>= 0.4'} - get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} @@ -7270,10 +7030,6 @@ packages: has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - has-proto@1.0.3: - resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} - engines: {node: '>= 0.4'} - has-proto@1.2.0: resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} engines: {node: '>= 0.4'} @@ -7310,11 +7066,8 @@ packages: highlightjs-sap-abap@0.3.0: resolution: {integrity: sha512-nSiUvEOCycjtFA3pHaTowrbAAk5+lciBHyoVkDsd6FTRBtW9sT2dt42o2jAKbXjZVUidtacdk+j0Y2xnd233Mw==} - hoist-non-react-statics@3.3.2: - resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - - hono@4.12.12: - resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==} + hono@4.12.18: + resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==} engines: {node: '>=16.9.0'} hookified@1.15.1: @@ -7343,10 +7096,6 @@ packages: htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} - http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -7363,8 +7112,8 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} - i18next-http-backend@3.0.2: - resolution: {integrity: sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==} + i18next-http-backend@3.0.6: + resolution: {integrity: sha512-mBOqy8993jtqAoj6XaI1XeC/8/9v6EPS+681ziegrPvTB0DoaCY7PpTS0SpY56qLMoS4OI1TZEM2Zf59zNh05w==} i18next@25.10.1: resolution: {integrity: sha512-d7MZx1UDamSmjbaqFg00w+EXUTqIB8x8cmYRGsAzQqXFVyrNFprGqPItANtlF6V1tuBFyZyp+4/q2MFqODWerg==} @@ -7425,13 +7174,13 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - internal-slot@1.0.7: - resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} - engines: {node: '>= 0.4'} - internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -7450,12 +7199,8 @@ packages: resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} engines: {node: '>=12.22.0'} - ioredis@5.9.3: - resolution: {integrity: sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==} - engines: {node: '>=12.22.0'} - - ip-address@10.1.0: - resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + ip-address@10.1.1: + resolution: {integrity: sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw==} engines: {node: '>= 12'} ipaddr.js@1.9.1: @@ -7466,10 +7211,6 @@ packages: resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} engines: {node: '>= 10'} - is-array-buffer@3.0.4: - resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} - engines: {node: '>= 0.4'} - is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -7489,10 +7230,6 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} - is-boolean-object@1.2.0: - resolution: {integrity: sha512-kR5g0+dXf/+kXnqI+lu0URKYPKgICtHGGNCDSB10AaUFj3o/HkB3u7WfpRBJGFopxxY0oH3ux7ZsDjLtK7xqvw==} - engines: {node: '>= 0.4'} - is-boolean-object@1.2.2: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} @@ -7504,18 +7241,10 @@ packages: is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} - is-data-view@1.0.1: - resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} - engines: {node: '>= 0.4'} - is-data-view@1.0.2: resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} engines: {node: '>= 0.4'} - is-date-object@1.0.5: - resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} - engines: {node: '>= 0.4'} - is-date-object@1.1.0: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} @@ -7553,10 +7282,6 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} - is-interactive@2.0.0: - resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} - engines: {node: '>=12'} - is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -7565,10 +7290,6 @@ packages: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} - is-number-object@1.1.0: - resolution: {integrity: sha512-KVSZV0Dunv9DTPkhXwcZ3Q+tUc9TsaE1ZwX5J2WMvsSGS6Md8TFPun5uwh0yRdrNerI6vf/tbJxqSx4c1ZI1Lw==} - engines: {node: '>= 0.4'} - is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -7583,10 +7304,6 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-regex@1.2.0: - resolution: {integrity: sha512-B6ohK4ZmoftlUe+uvenXSbPJFo6U37BH7oO1B3nQH8f/7h27N56s85MhUtbFJAziz5dcmuR3i8ovUl35zp8pFA==} - engines: {node: '>= 0.4'} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -7595,10 +7312,6 @@ packages: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} - is-shared-array-buffer@1.0.3: - resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} - engines: {node: '>= 0.4'} - is-shared-array-buffer@1.0.4: resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} engines: {node: '>= 0.4'} @@ -7607,26 +7320,14 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - is-string@1.1.0: - resolution: {integrity: sha512-PlfzajuF9vSo5wErv3MJAKD/nqf9ngAs1NFQYm16nUYFO2IzxJ2hcm+IOCg+EEopdykNNUhVq5cz35cAUxU8+g==} - engines: {node: '>= 0.4'} - is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} - is-symbol@1.1.0: - resolution: {integrity: sha512-qS8KkNNXUZ/I+nX6QT8ZS1/Yx0A444yhzdTKxCzKkNjQ9sHErBxJnJAgh+f5YhusYECEcjo4XcyH87hn6+ks0A==} - engines: {node: '>= 0.4'} - is-symbol@1.1.1: resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} engines: {node: '>= 0.4'} - is-typed-array@1.1.13: - resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} - engines: {node: '>= 0.4'} - is-typed-array@1.1.15: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} @@ -7635,10 +7336,6 @@ packages: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} - is-unicode-supported@1.3.0: - resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} - engines: {node: '>=12'} - is-unicode-supported@2.1.0: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} @@ -7647,9 +7344,6 @@ packages: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} - is-weakref@1.0.2: - resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} - is-weakref@1.1.1: resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} engines: {node: '>= 0.4'} @@ -7933,6 +7627,15 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdom@25.0.0: + resolution: {integrity: sha512-OhoFVT59T7aEq75TVw9xxEfkXgacpqAhQaYgP9y/fDqWQCMB/b1H66RfmPm/MaeaAIU9nDwMOVTlPN51+ao6CQ==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + jsdom@26.1.0: resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} engines: {node: '>=18'} @@ -8088,26 +7791,22 @@ packages: postgres: optional: true - kysely@0.28.14: - resolution: {integrity: sha512-SU3lgh0rPvq7upc6vvdVrCsSMUG1h3ChvHVOY7wJ2fw4C9QEB7X3d5eyYEyULUX7UQtxZJtZXGuT6U2US72UYA==} + kysely@0.28.17: + resolution: {integrity: sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q==} engines: {node: '>=20.0.0'} langium@3.3.1: resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} engines: {node: '>=16.0.0'} - langium@4.2.1: - resolution: {integrity: sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==} - engines: {node: '>=20.10.0', npm: '>=10.2.3'} - - langsmith@0.5.18: - resolution: {integrity: sha512-3zuZUWffTHQ+73EAwnodADtf534VNEZUpXr9jC12qyG8/IQuJET7PRsCpTb9wX2lmBspakwLUpqpj3tNm/0bVA==} + langsmith@0.7.0: + resolution: {integrity: sha512-iiPAGHJZ3uIHGnnLSkgcYZ4+thzhsGp5U48pWuW3ETgCRtbYzoDxYJigiQ3iWkK8ovF7Vr37tYvbI1ZE0tB+6A==} peerDependencies: '@opentelemetry/api': '*' '@opentelemetry/exporter-trace-otlp-proto': '*' '@opentelemetry/sdk-trace-base': '*' openai: '*' - ws: 8.20.0 + ws: 8.20.1 peerDependenciesMeta: '@opentelemetry/api': optional: true @@ -8308,10 +8007,6 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} - log-symbols@6.0.0: - resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} - engines: {node: '>=18'} - log-symbols@7.0.1: resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} engines: {node: '>=18'} @@ -8347,9 +8042,16 @@ packages: resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} engines: {node: '>=12'} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} @@ -8399,6 +8101,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} @@ -8410,9 +8115,6 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} - memoize-one@5.2.1: - resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -8420,8 +8122,8 @@ packages: merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - mermaid@11.13.0: - resolution: {integrity: sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw==} + mermaid@11.15.0: + resolution: {integrity: sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==} methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} @@ -8470,6 +8172,10 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + minimatch@10.2.4: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} @@ -8508,12 +8214,12 @@ packages: resolution: {integrity: sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==} hasBin: true - msgpackr@1.11.5: - resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} - msgpackr@1.11.9: resolution: {integrity: sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw==} + msgpackr@2.0.1: + resolution: {integrity: sha512-9J+tqTEsbHqY8YohazYgty7LgerFIWxvMLpUjqETSmjHojtJm2WnX2kK/2a1fLI7CO7ERP1YSEUXMucz4j+yBA==} + multimath@2.0.0: resolution: {integrity: sha512-toRx66cAMJ+Ccz7pMIg38xSIrtnbozk0dchXezwQDMgQmbGpfxjtv68H+L00iFL8hxDaVjrmwAFSb3I6bg8Q2g==} @@ -8548,8 +8254,8 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - needle@3.3.1: - resolution: {integrity: sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==} + needle@3.5.0: + resolution: {integrity: sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==} engines: {node: '>= 4.4.x'} hasBin: true @@ -8620,9 +8326,6 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-readable-to-web-readable-stream@0.4.2: - resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==} - node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -8659,9 +8362,9 @@ packages: '@swc/core': optional: true - nypm@0.6.2: - resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==} - engines: {node: ^14.16.0 || >=16.10.0} + nypm@0.6.6: + resolution: {integrity: sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==} + engines: {node: '>=18'} hasBin: true oauth4webapi@3.8.5: @@ -8690,10 +8393,6 @@ packages: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} - object.assign@4.1.5: - resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} - engines: {node: '>= 0.4'} - object.assign@4.1.7: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} @@ -8710,6 +8409,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + ollama@0.6.3: resolution: {integrity: sha512-KEWEhIqE5wtfzEIZbDCLH51VFZ6Z3ZSa6sIOg/E/tBV8S51flyqBOXi+bRxlOYKDf8i327zG9eSTb8IJxvm3Zg==} @@ -8728,10 +8430,6 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} - onetime@7.0.0: - resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} - engines: {node: '>=18'} - open@8.4.2: resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} engines: {node: '>=12'} @@ -8740,7 +8438,7 @@ packages: resolution: {integrity: sha512-qqjzHls7F5xkXNGy9P1Ei1rorI5LWupUUFWP66zPU8FlZbiITX8SFcHMKNZg/NATJ0LpIZcMUFxSwQmdeQPwSw==} hasBin: true peerDependencies: - ws: 8.20.0 + ws: 8.20.1 zod: ^3.25 || ^4.0 peerDependenciesMeta: ws: @@ -8769,10 +8467,6 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} - ora@8.2.0: - resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} - engines: {node: '>=18'} - orderedmap@2.1.1: resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} @@ -8882,8 +8576,8 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-expression-matcher@1.2.0: - resolution: {integrity: sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==} + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} engines: {node: '>=14.0.0'} path-is-absolute@1.0.1: @@ -8914,10 +8608,6 @@ packages: pause@0.0.1: resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} - pdfjs-dist@5.5.207: - resolution: {integrity: sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==} - engines: {node: '>=20.19.0 || >=22.13.0 || >=24'} - peberminta@0.9.0: resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} @@ -8930,20 +8620,20 @@ packages: pg-cloudflare@1.3.0: resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} - pg-connection-string@2.11.0: - resolution: {integrity: sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==} + pg-connection-string@2.12.0: + resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==} pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} - pg-pool@3.11.0: - resolution: {integrity: sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==} + pg-pool@3.13.0: + resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==} peerDependencies: pg: '>=8.0' - pg-protocol@1.11.0: - resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==} + pg-protocol@1.13.0: + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} pg-tsquery@8.4.2: resolution: {integrity: sha512-waJSlBIKE+shDhuDpuQglTH6dG5zakDhnrnxu8XB8V5c7yoDSuy4pOxY6t2dyoxTjaKMcMmlByJN7n9jx9eqMA==} @@ -8983,6 +8673,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + picospinner@3.0.0: + resolution: {integrity: sha512-lGA1TNsmy2bxvRsTI2cV01kfTwKzZjnZSDmF9llYNyMHMrU4sP87lQ5taiIKm88L3cbswjl008nwyGc3WpNvzg==} + engines: {node: '>=18.0.0'} + pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} @@ -9022,9 +8716,6 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - pkg-types@2.3.0: - resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -9091,8 +8782,8 @@ packages: peerDependencies: postcss: ^8.2.1 - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -9115,8 +8806,8 @@ packages: resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==} engines: {node: '>=12'} - posthog-js@1.363.1: - resolution: {integrity: sha512-iaDtRxCs/FiB+RXe83uo7RZXgpLlyB6qFoNHl3bNMgRCgrPI2nkzx2m9Va1l30HHl/zA1kPOXSy2/tZC5Ql5kg==} + posthog-js@1.372.2: + resolution: {integrity: sha512-FS+vKDXB1vghrVch3EDi3IRcoH5OnLQYxchHWi+8U4D4PzWQZnZLo5vyMRL1+ZUHNEZ2v599uX3UKhRZv2z6Cg==} postmark@4.0.7: resolution: {integrity: sha512-DjNniUl1XNCGUKhCR98ePd5gv16rlUAVKKaU9TUqnE3hDSqfT9XDulu1idjagQmdyGscqnRtXk/puAEiYMeevg==} @@ -9133,6 +8824,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-format@30.2.0: resolution: {integrity: sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -9219,8 +8914,8 @@ packages: prosemirror-view@1.40.0: resolution: {integrity: sha512-2G3svX0Cr1sJjkD/DYWSe3cfV5VPVTBOxI9XQEGWJDFEpsZb/gh4MV29ctv+OJx2RFX4BLt09i+6zaGM/ldkCw==} - protobufjs@7.5.4: - resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + protobufjs@7.5.8: + resolution: {integrity: sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==} engines: {node: '>=12.0.0'} proxy-addr@2.0.7: @@ -9234,6 +8929,9 @@ packages: prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + pump@3.0.3: resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} @@ -9263,6 +8961,9 @@ packages: query-selector-shadow-dom@1.0.1: resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} @@ -9279,6 +8980,9 @@ packages: '@types/react-dom': optional: true + raf-schd@4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -9287,12 +8991,6 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} - react-arborist@3.4.0: - resolution: {integrity: sha512-QI46oRGXJr0oaQfqqVobIiIoqPp5Y5gM69D2A2P7uHVif+X75XWnScR5drC7YDKgJ4CXVaDeFwnYKOWRRfncMg==} - peerDependencies: - react: '>= 16.14' - react-dom: '>= 16.14' - react-clear-modal@2.0.18: resolution: {integrity: sha512-Aiv8Bw5NVm19tlUt3RLV2a1I/ya+UlyEZjREosn5G887nnusnefT+ls4AXkuP8XLn1KOah6DrM5MemV7cXgwWg==} peerDependencies: @@ -9303,24 +9001,6 @@ packages: '@types/react': optional: true - react-dnd-html5-backend@14.1.0: - resolution: {integrity: sha512-6ONeqEC3XKVf4eVmMTe0oPds+c5B9Foyj8p/ZKLb7kL2qh9COYxiBHv3szd6gztqi/efkmriywLUVlPotqoJyw==} - - react-dnd@14.0.5: - resolution: {integrity: sha512-9i1jSgbyVw0ELlEVt/NkCUkxy1hmhJOkePoCH713u75vzHGyXhPDm28oLfc2NMSBjZRM1Y+wRjHXJT3sPrTy+A==} - peerDependencies: - '@types/hoist-non-react-statics': '>= 3.3.1' - '@types/node': '>= 12' - '@types/react': '>= 16' - react: '>= 16.14' - peerDependenciesMeta: - '@types/hoist-non-react-statics': - optional: true - '@types/node': - optional: true - '@types/react': - optional: true - react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -9331,10 +9011,13 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-email@5.2.10: - resolution: {integrity: sha512-Ys8yR5/a0nXf5u2GlT2UV93PJHC3ZnuMnNebEn7I5UE9XfMFPtlpgDs02mPJOJn49fhJjDTWIUlZD1vmQPDgJg==} + react-email@6.0.8: + resolution: {integrity: sha512-/kArasUY1ahkrs8z8OG0wz+d7hedV/wUSZ8qaXI2TtB6omUMmMtm//FBHSSdnsEvkUnAZ5LzCyge1h6oQNhpTA==} engines: {node: '>=20.0.0'} hasBin: true + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^18.0 || ^19.0 || ^19.0.0-rc react-error-boundary@6.1.1: resolution: {integrity: sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==} @@ -9368,6 +9051,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -9436,13 +9122,6 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' - react-window@1.8.10: - resolution: {integrity: sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==} - engines: {node: '>8.0.0'} - peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -9466,6 +9145,10 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -9474,12 +9157,6 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} - redux@4.2.1: - resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} - - redux@5.0.1: - resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} - reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -9487,10 +9164,6 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} - reflect.getprototypeof@1.0.7: - resolution: {integrity: sha512-bMvFGIUKlc/eSfXNX+aZ+EL95/EgZzuwA0OBPTbZZDEJw/0AkentjMuM1oiRfwHrshqk4RzdgiTg5CcDalXN5g==} - engines: {node: '>= 0.4'} - regenerate-unicode-properties@10.1.1: resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} engines: {node: '>=4'} @@ -9498,16 +9171,9 @@ packages: regenerate@1.4.2: resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - regenerator-transform@0.15.2: resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} - regexp.prototype.flags@1.5.3: - resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} - engines: {node: '>= 0.4'} - regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -9531,6 +9197,9 @@ packages: require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -9562,10 +9231,6 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} - restore-cursor@5.1.0: - resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} - engines: {node: '>=18'} - ret@0.5.0: resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} engines: {node: '>=10'} @@ -9601,6 +9266,9 @@ packages: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} + rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} @@ -9613,10 +9281,6 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - safe-array-concat@1.1.2: - resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} - engines: {node: '>=0.4'} - safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -9631,10 +9295,6 @@ packages: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} - safe-regex-test@1.0.3: - resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} - engines: {node: '>= 0.4'} - safe-regex-test@1.1.0: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} @@ -9649,8 +9309,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sanitize-filename-ts@1.0.2: - resolution: {integrity: sha512-bON2VOJoappmaBHlnxvBNk5R7HkUAsirf5m1M5Kz15uZykDGbHfGPCQNcEQKR8HrQhgh9CmQ6Xe9y71yM9ywkw==} + sanitize-filename@1.6.3: + resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} sass@1.51.0: resolution: {integrity: sha512-haGdpTgywJTvHC2b91GSq+clTKGbtkkZmVAb82jZQN/wTy6qs8DdFm2lhEQbEwrY0QDRgSQ3xDurqM977C3noA==} @@ -9660,8 +9320,8 @@ packages: sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} - sax@1.4.4: - resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==} + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} saxes@6.0.0: @@ -9679,6 +9339,10 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} + scimmy@1.3.5: + resolution: {integrity: sha512-JTrUOoqH1gMH2zZhgk01hGgY7cH9v4qUli5b3OGVVOzjAwY8h4Z2mSNH8kXjW2pz8ypzpiRuMEtFGBaWQWJz7w==} + engines: {node: '>=16'} + secure-json-parse@4.0.0: resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==} @@ -9698,6 +9362,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} @@ -9761,6 +9430,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -9828,20 +9500,18 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - stdin-discarder@0.2.2: - resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} - engines: {node: '>=18'} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} @@ -9861,10 +9531,6 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} - string-width@7.2.0: - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} - engines: {node: '>=18'} - string.prototype.matchall@4.0.12: resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} engines: {node: '>= 0.4'} @@ -9876,13 +9542,6 @@ packages: resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} engines: {node: '>= 0.4'} - string.prototype.trim@1.2.9: - resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.8: - resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} - string.prototype.trimend@1.0.9: resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} engines: {node: '>= 0.4'} @@ -9901,10 +9560,6 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} - strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -9917,6 +9572,10 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -9929,8 +9588,8 @@ packages: resolution: {integrity: sha512-aT2BU9KkizY9SATf14WhhYVv2uOapBWX0OFWF4xvcj1mPaNotlSc2CsxpS4DS46ZueSppmCF5BX1sNYBtwBvfw==} engines: {node: '>=12.*'} - strnum@2.2.1: - resolution: {integrity: sha512-BwRvNd5/QoAtyW1na1y1LsJGQNvRlkde6Q/ipqqEaivoMdV+B1OMOTVdwR+N/cwVUcIt9PYyHmV8HyexCZSupg==} + strnum@2.2.3: + resolution: {integrity: sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==} strtok3@10.3.4: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} @@ -10028,18 +9687,26 @@ packages: thread-stream@3.0.2: resolution: {integrity: sha512-cBL4xF2A3lSINV4rD5tyqnKH4z/TgWPvT+NaVhJDSwK962oo/Ye7cHSMbDzwcu7tAE1SfU6Q4XtV6Hucmi6Hlw==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tinyexec@1.0.1: - resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + tinyexec@1.1.2: + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + engines: {node: '>=18'} tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tiptap-extension-global-drag-handle@0.1.18: - resolution: {integrity: sha512-jwFuy1K8DP3a4bFy76Hpc63w1Sil0B7uZ3mvhQomVvUFCU787Lg2FowNhn7NFzeyok761qY2VG+PZ/FDthWUdg==} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} tlds@1.261.0: resolution: {integrity: sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==} @@ -10078,6 +9745,10 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -10204,26 +9875,14 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} - typed-array-buffer@1.0.2: - resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} - engines: {node: '>= 0.4'} - typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} - typed-array-byte-length@1.0.1: - resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} - engines: {node: '>= 0.4'} - typed-array-byte-length@1.0.3: resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} engines: {node: '>= 0.4'} - typed-array-byte-offset@1.0.3: - resolution: {integrity: sha512-GsvTyUHTriq6o/bHcTd0vM7OQ9JEdlvluu9YISaA7+KzDzPaIzEeDFNkTfhdE3MYcNhNi0vq/LlegYgIs5yPAw==} - engines: {node: '>= 0.4'} - typed-array-byte-offset@1.0.4: resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} engines: {node: '>= 0.4'} @@ -10276,9 +9935,6 @@ packages: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} - unbox-primitive@1.0.2: - resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} - unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -10312,6 +9968,10 @@ packages: resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} engines: {node: '>=4'} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -10332,6 +9992,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -10375,11 +10038,6 @@ packages: '@types/react': optional: true - use-sync-external-store@1.2.2: - resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -10397,14 +10055,11 @@ packages: uuid@10.0.0: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true - uuid@11.1.0: - resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} - hasBin: true - - uuid@13.0.0: - resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} hasBin: true v8-compile-cache-lib@3.0.1: @@ -10465,6 +10120,47 @@ packages: yaml: optional: true + vitest@4.1.6: + resolution: {integrity: sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.6 + '@vitest/browser-preview': 4.1.6 + '@vitest/browser-webdriverio': 4.1.6 + '@vitest/coverage-istanbul': 4.1.6 + '@vitest/coverage-v8': 4.1.6 + '@vitest/ui': 4.1.6 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} @@ -10489,9 +10185,6 @@ packages: vscode-uri@3.0.8: resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} - vscode-uri@3.1.0: - resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} @@ -10527,8 +10220,8 @@ packages: resolution: {integrity: sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==} engines: {node: '>=10.13.0'} - webpack@5.105.4: - resolution: {integrity: sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==} + webpack@5.106.0: + resolution: {integrity: sha512-Pkx5joZ9RrdgO5LBkyX1L2ZAJeK/Taz3vqZ9CbcP0wS5LEMx5QkKsEwLl29QJfihZ+DKRBFldzy1O30pJ1MDpA==} engines: {node: '>=10.13.0'} hasBin: true peerDependencies: @@ -10566,18 +10259,10 @@ packages: when-exit@2.1.5: resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} - which-boxed-primitive@1.1.0: - resolution: {integrity: sha512-Ei7Miu/AXe2JJ4iNF5j/UphAgRoma4trE6PtisM09bPygb3egMH3YLW/befsWb1A1AxvNSFidOFTB18XtnIIng==} - engines: {node: '>= 0.4'} - which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} - which-builtin-type@1.2.0: - resolution: {integrity: sha512-I+qLGQ/vucCby4tf5HsLmGueEla4ZhwTBSqaooS+Y0BuxN4Cp+okmGuV+8mXZ84KDI9BA+oklo+RzKg0ONdSUA==} - engines: {node: '>= 0.4'} - which-builtin-type@1.2.1: resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} engines: {node: '>= 0.4'} @@ -10589,10 +10274,6 @@ packages: which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - which-typed-array@1.1.16: - resolution: {integrity: sha512-g+N+GAWiRj66DngFwHvISJd+ITsyphZvD1vChfVg6cEdnzy53GzB3oy0fUNlvhz7H7+MiqhYr26qxQShCpKTTQ==} - engines: {node: '>= 0.4'} - which-typed-array@1.1.20: resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} engines: {node: '>= 0.4'} @@ -10602,6 +10283,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + widest-line@3.1.0: resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} engines: {node: '>=8'} @@ -10624,8 +10310,8 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -10647,6 +10333,10 @@ packages: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} + xml-naming@0.1.0: + resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} + engines: {node: '>=16.0.0'} + xml2js@0.6.2: resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} engines: {node: '>=4.0.0'} @@ -10853,10 +10543,21 @@ snapshots: optionalDependencies: chokidar: 4.0.3 - '@angular-devkit/schematics-cli@19.2.23(@types/node@25.5.0)(chokidar@4.0.3)': + '@angular-devkit/core@19.2.24(chokidar@4.0.3)': dependencies: - '@angular-devkit/core': 19.2.23(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.23(chokidar@4.0.3) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + jsonc-parser: 3.3.1 + picomatch: 4.0.4 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/schematics-cli@19.2.24(@types/node@25.5.0)(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) '@inquirer/prompts': 7.3.2(@types/node@25.5.0) ansi-colors: 4.1.3 symbol-observable: 4.0.0 @@ -10875,10 +10576,20 @@ snapshots: transitivePeerDependencies: - chokidar + '@angular-devkit/schematics@19.2.24(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.3.0 - tinyexec: 1.0.1 + tinyexec: 1.1.2 '@asamuzakjp/css-color@2.8.3': dependencies: @@ -10888,23 +10599,121 @@ snapshots: '@csstools/css-tokenizer': 3.0.3 lru-cache: 10.4.3 + '@atlaskit/atlassian-context@0.8.0(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + react: 18.3.1 + + '@atlaskit/browser-apis@0.0.1(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + react: 18.3.1 + + '@atlaskit/css@0.19.4(react@18.3.1)': + dependencies: + '@atlaskit/tokens': 13.0.4(react@18.3.1) + '@babel/runtime': 7.29.2 + '@compiled/react': 0.20.0(react@18.3.1) + react: 18.3.1 + transitivePeerDependencies: + - supports-color + + '@atlaskit/ds-lib@7.0.0(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + bind-event-listener: 3.0.0 + react: 18.3.1 + tiny-invariant: 1.3.3 + + '@atlaskit/feature-gate-js-client@5.5.11(react@18.3.1)': + dependencies: + '@atlaskit/atlassian-context': 0.8.0(react@18.3.1) + '@babel/runtime': 7.29.2 + '@statsig/client-core': 3.33.0 + '@statsig/js-client': 3.33.0 + eventemitter3: 4.0.7 + transitivePeerDependencies: + - react + + '@atlaskit/motion@6.2.2(react@18.3.1)': + dependencies: + '@atlaskit/browser-apis': 0.0.1(react@18.3.1) + '@atlaskit/css': 0.19.4(react@18.3.1) + '@atlaskit/ds-lib': 7.0.0(react@18.3.1) + '@atlaskit/platform-feature-flags': 1.1.3(react@18.3.1) + '@atlaskit/tokens': 13.0.4(react@18.3.1) + '@babel/runtime': 7.29.2 + '@compiled/react': 0.20.0(react@18.3.1) + bind-event-listener: 3.0.0 + react: 18.3.1 + transitivePeerDependencies: + - supports-color + + '@atlaskit/platform-feature-flags@1.1.3(react@18.3.1)': + dependencies: + '@atlaskit/feature-gate-js-client': 5.5.11(react@18.3.1) + '@babel/runtime': 7.29.2 + transitivePeerDependencies: + - react + + '@atlaskit/pragmatic-drag-and-drop-auto-scroll@2.1.5': + dependencies: + '@atlaskit/pragmatic-drag-and-drop': 1.8.1 + '@babel/runtime': 7.29.2 + + '@atlaskit/pragmatic-drag-and-drop-flourish@2.0.15(react@18.3.1)': + dependencies: + '@atlaskit/motion': 6.2.2(react@18.3.1) + '@atlaskit/tokens': 13.0.4(react@18.3.1) + '@babel/runtime': 7.29.2 + transitivePeerDependencies: + - react + - supports-color + + '@atlaskit/pragmatic-drag-and-drop-hitbox@1.1.0': + dependencies: + '@atlaskit/pragmatic-drag-and-drop': 1.8.1 + '@babel/runtime': 7.29.2 + + '@atlaskit/pragmatic-drag-and-drop-live-region@1.3.4': + dependencies: + '@babel/runtime': 7.29.2 + + '@atlaskit/pragmatic-drag-and-drop@1.8.1': + dependencies: + '@babel/runtime': 7.29.2 + bind-event-listener: 3.0.0 + raf-schd: 4.0.3 + + '@atlaskit/tokens@13.0.4(react@18.3.1)': + dependencies: + '@atlaskit/ds-lib': 7.0.0(react@18.3.1) + '@atlaskit/platform-feature-flags': 1.1.3(react@18.3.1) + '@babel/runtime': 7.29.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + bind-event-listener: 3.0.0 + react: 18.3.1 + transitivePeerDependencies: + - supports-color + '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 tslib: 2.8.1 '@aws-crypto/crc32c@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 tslib: 2.8.1 '@aws-crypto/sha1-browser@5.2.0': dependencies: '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 '@aws-sdk/util-locate-window': 3.535.0 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -10914,7 +10723,7 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 '@aws-sdk/util-locate-window': 3.535.0 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -10922,7 +10731,7 @@ snapshots: '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -10931,442 +10740,372 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 - '@aws-sdk/client-s3@3.1014.0': + '@aws-sdk/client-s3@3.1050.0': dependencies: '@aws-crypto/sha1-browser': 5.2.0 '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.26 - '@aws-sdk/credential-provider-node': 3.972.29 - '@aws-sdk/middleware-bucket-endpoint': 3.972.8 - '@aws-sdk/middleware-expect-continue': 3.972.8 - '@aws-sdk/middleware-flexible-checksums': 3.974.6 - '@aws-sdk/middleware-host-header': 3.972.8 - '@aws-sdk/middleware-location-constraint': 3.972.8 - '@aws-sdk/middleware-logger': 3.972.8 - '@aws-sdk/middleware-recursion-detection': 3.972.9 - '@aws-sdk/middleware-sdk-s3': 3.972.27 - '@aws-sdk/middleware-ssec': 3.972.8 - '@aws-sdk/middleware-user-agent': 3.972.28 - '@aws-sdk/region-config-resolver': 3.972.10 - '@aws-sdk/signature-v4-multi-region': 3.996.15 - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-endpoints': 3.996.5 - '@aws-sdk/util-user-agent-browser': 3.972.8 - '@aws-sdk/util-user-agent-node': 3.973.14 - '@smithy/config-resolver': 4.4.13 - '@smithy/core': 3.23.13 - '@smithy/eventstream-serde-browser': 4.2.12 - '@smithy/eventstream-serde-config-resolver': 4.3.12 - '@smithy/eventstream-serde-node': 4.2.12 - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/hash-blob-browser': 4.2.13 - '@smithy/hash-node': 4.2.12 - '@smithy/hash-stream-node': 4.2.12 - '@smithy/invalid-dependency': 4.2.12 - '@smithy/md5-js': 4.2.12 - '@smithy/middleware-content-length': 4.2.12 - '@smithy/middleware-endpoint': 4.4.28 - '@smithy/middleware-retry': 4.4.46 - '@smithy/middleware-serde': 4.2.16 - '@smithy/middleware-stack': 4.2.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/node-http-handler': 4.5.1 - '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.44 - '@smithy/util-defaults-mode-node': 4.2.48 - '@smithy/util-endpoints': 3.3.3 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.13 - '@smithy/util-stream': 4.5.21 - '@smithy/util-utf8': 4.2.2 - '@smithy/util-waiter': 4.2.14 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/core@3.973.26': - dependencies: - '@aws-sdk/types': 3.973.6 - '@aws-sdk/xml-builder': 3.972.16 - '@smithy/core': 3.23.13 - '@smithy/node-config-provider': 4.3.12 - '@smithy/property-provider': 4.2.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/signature-v4': 5.3.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/util-base64': 4.3.2 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-utf8': 4.2.2 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/credential-provider-node': 3.972.43 + '@aws-sdk/middleware-bucket-endpoint': 3.972.14 + '@aws-sdk/middleware-expect-continue': 3.972.12 + '@aws-sdk/middleware-flexible-checksums': 3.974.20 + '@aws-sdk/middleware-location-constraint': 3.972.10 + '@aws-sdk/middleware-sdk-s3': 3.972.41 + '@aws-sdk/middleware-ssec': 3.972.10 + '@aws-sdk/signature-v4-multi-region': 3.996.27 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/fetch-http-handler': 5.4.3 + '@smithy/node-http-handler': 4.7.3 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/crc64-nvme@3.972.5': + '@aws-sdk/core@3.974.12': dependencies: - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/xml-builder': 3.972.24 + '@aws/lambda-invoke-store': 0.2.3 + '@smithy/core': 3.24.3 + '@smithy/signature-v4': 5.4.3 + '@smithy/types': 4.14.1 + bowser: 2.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.972.24': + '@aws-sdk/crc64-nvme@3.972.8': dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.972.26': + '@aws-sdk/credential-provider-env@3.972.38': dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/types': 3.973.6 - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/node-http-handler': 4.5.1 - '@smithy/property-provider': 4.2.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/util-stream': 4.5.21 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-ini@3.972.28': + '@aws-sdk/credential-provider-http@3.972.40': dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/credential-provider-env': 3.972.24 - '@aws-sdk/credential-provider-http': 3.972.26 - '@aws-sdk/credential-provider-login': 3.972.28 - '@aws-sdk/credential-provider-process': 3.972.24 - '@aws-sdk/credential-provider-sso': 3.972.28 - '@aws-sdk/credential-provider-web-identity': 3.972.28 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/types': 3.973.6 - '@smithy/credential-provider-imds': 4.2.12 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-login@3.972.28': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-node@3.972.29': - dependencies: - '@aws-sdk/credential-provider-env': 3.972.24 - '@aws-sdk/credential-provider-http': 3.972.26 - '@aws-sdk/credential-provider-ini': 3.972.28 - '@aws-sdk/credential-provider-process': 3.972.24 - '@aws-sdk/credential-provider-sso': 3.972.28 - '@aws-sdk/credential-provider-web-identity': 3.972.28 - '@aws-sdk/types': 3.973.6 - '@smithy/credential-provider-imds': 4.2.12 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-process@3.972.24': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/fetch-http-handler': 5.4.3 + '@smithy/node-http-handler': 4.7.3 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.972.28': + '@aws-sdk/credential-provider-ini@3.972.42': dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/token-providers': 3.1021.0 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/credential-provider-env': 3.972.38 + '@aws-sdk/credential-provider-http': 3.972.40 + '@aws-sdk/credential-provider-login': 3.972.42 + '@aws-sdk/credential-provider-process': 3.972.38 + '@aws-sdk/credential-provider-sso': 3.972.42 + '@aws-sdk/credential-provider-web-identity': 3.972.42 + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/credential-provider-imds': 4.3.3 + '@smithy/types': 4.14.1 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/credential-provider-web-identity@3.972.28': + '@aws-sdk/credential-provider-login@3.972.42': dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.1 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/lib-storage@3.1014.0(@aws-sdk/client-s3@3.1014.0)': + '@aws-sdk/credential-provider-node@3.972.43': dependencies: - '@aws-sdk/client-s3': 3.1014.0 - '@smithy/abort-controller': 4.2.12 - '@smithy/middleware-endpoint': 4.4.28 - '@smithy/smithy-client': 4.12.8 + '@aws-sdk/credential-provider-env': 3.972.38 + '@aws-sdk/credential-provider-http': 3.972.40 + '@aws-sdk/credential-provider-ini': 3.972.42 + '@aws-sdk/credential-provider-process': 3.972.38 + '@aws-sdk/credential-provider-sso': 3.972.42 + '@aws-sdk/credential-provider-web-identity': 3.972.42 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/credential-provider-imds': 4.3.3 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.38': + dependencies: + '@aws-sdk/core': 3.974.12 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.42': + dependencies: + '@aws-sdk/core': 3.974.12 + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/token-providers': 3.1049.0 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.42': + dependencies: + '@aws-sdk/core': 3.974.12 + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.1 + tslib: 2.8.1 + + '@aws-sdk/lib-storage@3.1050.0(@aws-sdk/client-s3@3.1050.0)': + dependencies: + '@aws-sdk/client-s3': 3.1050.0 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.1 buffer: 5.6.0 events: 3.3.0 stream-browserify: 3.0.0 tslib: 2.8.1 - '@aws-sdk/middleware-bucket-endpoint@3.972.8': + '@aws-sdk/middleware-bucket-endpoint@3.972.14': dependencies: - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-arn-parser': 3.972.3 - '@smithy/node-config-provider': 4.3.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-config-provider': 4.2.2 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-expect-continue@3.972.8': + '@aws-sdk/middleware-expect-continue@3.972.12': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-flexible-checksums@3.974.6': + '@aws-sdk/middleware-flexible-checksums@3.974.20': dependencies: '@aws-crypto/crc32': 5.2.0 '@aws-crypto/crc32c': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/core': 3.973.26 - '@aws-sdk/crc64-nvme': 3.972.5 - '@aws-sdk/types': 3.973.6 - '@smithy/is-array-buffer': 4.2.2 - '@smithy/node-config-provider': 4.3.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-stream': 4.5.21 - '@smithy/util-utf8': 4.2.2 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/crc64-nvme': 3.972.8 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-host-header@3.972.8': + '@aws-sdk/middleware-location-constraint@3.972.10': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-location-constraint@3.972.8': + '@aws-sdk/middleware-sdk-s3@3.972.41': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/signature-v4-multi-region': 3.996.27 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/signature-v4': 5.4.3 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-logger@3.972.8': + '@aws-sdk/middleware-ssec@3.972.10': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.8 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.972.9': - dependencies: - '@aws-sdk/types': 3.973.6 - '@aws/lambda-invoke-store': 0.2.3 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-sdk-s3@3.972.27': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-arn-parser': 3.972.3 - '@smithy/core': 3.23.13 - '@smithy/node-config-provider': 4.3.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/signature-v4': 5.3.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/util-config-provider': 4.2.2 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-stream': 4.5.21 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@aws-sdk/middleware-ssec@3.972.8': - dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-user-agent@3.972.28': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-endpoints': 3.996.5 - '@smithy/core': 3.23.13 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-retry': 4.2.13 - tslib: 2.8.1 - - '@aws-sdk/nested-clients@3.996.18': + '@aws-sdk/nested-clients@3.997.10': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.26 - '@aws-sdk/middleware-host-header': 3.972.8 - '@aws-sdk/middleware-logger': 3.972.8 - '@aws-sdk/middleware-recursion-detection': 3.972.9 - '@aws-sdk/middleware-user-agent': 3.972.28 - '@aws-sdk/region-config-resolver': 3.972.10 - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-endpoints': 3.996.5 - '@aws-sdk/util-user-agent-browser': 3.972.8 - '@aws-sdk/util-user-agent-node': 3.973.14 - '@smithy/config-resolver': 4.4.13 - '@smithy/core': 3.23.13 - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/hash-node': 4.2.12 - '@smithy/invalid-dependency': 4.2.12 - '@smithy/middleware-content-length': 4.2.12 - '@smithy/middleware-endpoint': 4.4.28 - '@smithy/middleware-retry': 4.4.46 - '@smithy/middleware-serde': 4.2.16 - '@smithy/middleware-stack': 4.2.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/node-http-handler': 4.5.1 - '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.44 - '@smithy/util-defaults-mode-node': 4.2.48 - '@smithy/util-endpoints': 3.3.3 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.13 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/region-config-resolver@3.972.10': - dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/config-resolver': 4.4.13 - '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/signature-v4-multi-region': 3.996.27 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/fetch-http-handler': 5.4.3 + '@smithy/node-http-handler': 4.7.3 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/s3-request-presigner@3.1014.0': + '@aws-sdk/s3-request-presigner@3.1050.0': dependencies: - '@aws-sdk/signature-v4-multi-region': 3.996.15 - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-format-url': 3.972.8 - '@smithy/middleware-endpoint': 4.4.28 - '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/signature-v4-multi-region': 3.996.27 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.996.15': + '@aws-sdk/signature-v4-multi-region@3.996.27': dependencies: - '@aws-sdk/middleware-sdk-s3': 3.972.27 - '@aws-sdk/types': 3.973.6 - '@smithy/protocol-http': 5.3.12 - '@smithy/signature-v4': 5.3.12 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/signature-v4': 5.4.3 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/token-providers@3.1021.0': + '@aws-sdk/token-providers@3.1049.0': dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/types@3.973.6': - dependencies: - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.1 tslib: 2.8.1 - '@aws-sdk/util-arn-parser@3.972.3': + '@aws-sdk/types@3.973.8': dependencies: - tslib: 2.8.1 - - '@aws-sdk/util-endpoints@3.996.5': - dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-endpoints': 3.3.3 - tslib: 2.8.1 - - '@aws-sdk/util-format-url@3.972.8': - dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/querystring-builder': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.1 tslib: 2.8.1 '@aws-sdk/util-locate-window@3.535.0': dependencies: tslib: 2.8.1 - '@aws-sdk/util-user-agent-browser@3.972.8': + '@aws-sdk/xml-builder@3.972.24': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 - bowser: 2.14.1 - tslib: 2.8.1 - - '@aws-sdk/util-user-agent-node@3.973.14': - dependencies: - '@aws-sdk/middleware-user-agent': 3.972.28 - '@aws-sdk/types': 3.973.6 - '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-config-provider': 4.2.2 - tslib: 2.8.1 - - '@aws-sdk/xml-builder@3.972.16': - dependencies: - '@smithy/types': 4.13.1 - fast-xml-parser: 5.5.8 + '@nodable/entities': 2.1.0 + '@smithy/types': 4.14.1 + fast-xml-parser: 5.7.3 tslib: 2.8.1 '@aws/lambda-invoke-store@0.2.3': {} + '@azure/abort-controller@2.1.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-auth@1.10.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-client@1.10.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-http-compat@2.4.0(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.23.0)': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-client': 1.10.1 + '@azure/core-rest-pipeline': 1.23.0 + + '@azure/core-lro@2.7.2': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-paging@1.6.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-rest-pipeline@1.23.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + '@typespec/ts-http-runtime': 0.3.5 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-tracing@1.3.1': + dependencies: + tslib: 2.8.1 + + '@azure/core-util@1.13.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@typespec/ts-http-runtime': 0.3.5 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-xml@1.5.1': + dependencies: + fast-xml-parser: 5.7.3 + tslib: 2.8.1 + + '@azure/logger@1.3.0': + dependencies: + '@typespec/ts-http-runtime': 0.3.5 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/storage-blob@12.31.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-client': 1.10.1 + '@azure/core-http-compat': 2.4.0(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.23.0) + '@azure/core-lro': 2.7.2 + '@azure/core-paging': 1.6.2 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/core-xml': 1.5.1 + '@azure/logger': 1.3.0 + '@azure/storage-common': 12.3.0(@azure/core-client@1.10.1) + events: 3.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/storage-common@12.3.0(@azure/core-client@1.10.1)': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-http-compat': 2.4.0(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.23.0) + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + events: 3.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - '@azure/core-client' + - supports-color + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.28.5': {} '@babel/core@7.28.5': @@ -12102,10 +11841,6 @@ snapshots: '@babel/regjsgen@0.8.0': {} - '@babel/runtime@7.26.10': - dependencies: - regenerator-runtime: 0.14.1 - '@babel/runtime@7.29.2': {} '@babel/template@7.27.2': @@ -12173,34 +11908,19 @@ snapshots: '@chevrotain/types': 11.0.3 lodash-es: 4.18.1 - '@chevrotain/cst-dts-gen@11.1.2': - dependencies: - '@chevrotain/gast': 11.1.2 - '@chevrotain/types': 11.1.2 - lodash-es: 4.18.1 - '@chevrotain/gast@11.0.3': dependencies: '@chevrotain/types': 11.0.3 lodash-es: 4.18.1 - '@chevrotain/gast@11.1.2': - dependencies: - '@chevrotain/types': 11.1.2 - lodash-es: 4.18.1 - '@chevrotain/regexp-to-ast@11.0.3': {} - '@chevrotain/regexp-to-ast@11.1.2': {} - '@chevrotain/types@11.0.3': {} '@chevrotain/types@11.1.2': {} '@chevrotain/utils@11.0.3': {} - '@chevrotain/utils@11.1.2': {} - '@clickhouse/client-common@1.18.2': {} '@clickhouse/client@1.18.2': @@ -12214,6 +11934,11 @@ snapshots: dependencies: commander: 11.1.0 + '@compiled/react@0.20.0(react@18.3.1)': + dependencies: + csstype: 3.2.3 + react: 18.3.1 + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -12270,6 +11995,8 @@ snapshots: react: 18.3.1 tslib: 2.8.1 + '@docmost/pdf-inspector@1.9.6': {} + '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -12288,171 +12015,169 @@ snapshots: dependencies: tslib: 2.8.1 - '@emoji-mart/data@1.2.1': {} - - '@emoji-mart/react@1.1.1(emoji-mart@5.6.0)(react@18.3.1)': - dependencies: - emoji-mart: 5.6.0 - react: 18.3.1 - '@epic-web/invariant@1.0.0': {} - '@esbuild/aix-ppc64@0.27.3': - optional: true - '@esbuild/aix-ppc64@0.27.4': optional: true - '@esbuild/android-arm64@0.27.3': + '@esbuild/aix-ppc64@0.28.0': optional: true '@esbuild/android-arm64@0.27.4': optional: true - '@esbuild/android-arm@0.27.3': + '@esbuild/android-arm64@0.28.0': optional: true '@esbuild/android-arm@0.27.4': optional: true - '@esbuild/android-x64@0.27.3': + '@esbuild/android-arm@0.28.0': optional: true '@esbuild/android-x64@0.27.4': optional: true - '@esbuild/darwin-arm64@0.27.3': + '@esbuild/android-x64@0.28.0': optional: true '@esbuild/darwin-arm64@0.27.4': optional: true - '@esbuild/darwin-x64@0.27.3': + '@esbuild/darwin-arm64@0.28.0': optional: true '@esbuild/darwin-x64@0.27.4': optional: true - '@esbuild/freebsd-arm64@0.27.3': + '@esbuild/darwin-x64@0.28.0': optional: true '@esbuild/freebsd-arm64@0.27.4': optional: true - '@esbuild/freebsd-x64@0.27.3': + '@esbuild/freebsd-arm64@0.28.0': optional: true '@esbuild/freebsd-x64@0.27.4': optional: true - '@esbuild/linux-arm64@0.27.3': + '@esbuild/freebsd-x64@0.28.0': optional: true '@esbuild/linux-arm64@0.27.4': optional: true - '@esbuild/linux-arm@0.27.3': + '@esbuild/linux-arm64@0.28.0': optional: true '@esbuild/linux-arm@0.27.4': optional: true - '@esbuild/linux-ia32@0.27.3': + '@esbuild/linux-arm@0.28.0': optional: true '@esbuild/linux-ia32@0.27.4': optional: true - '@esbuild/linux-loong64@0.27.3': + '@esbuild/linux-ia32@0.28.0': optional: true '@esbuild/linux-loong64@0.27.4': optional: true - '@esbuild/linux-mips64el@0.27.3': + '@esbuild/linux-loong64@0.28.0': optional: true '@esbuild/linux-mips64el@0.27.4': optional: true - '@esbuild/linux-ppc64@0.27.3': + '@esbuild/linux-mips64el@0.28.0': optional: true '@esbuild/linux-ppc64@0.27.4': optional: true - '@esbuild/linux-riscv64@0.27.3': + '@esbuild/linux-ppc64@0.28.0': optional: true '@esbuild/linux-riscv64@0.27.4': optional: true - '@esbuild/linux-s390x@0.27.3': + '@esbuild/linux-riscv64@0.28.0': optional: true '@esbuild/linux-s390x@0.27.4': optional: true - '@esbuild/linux-x64@0.27.3': + '@esbuild/linux-s390x@0.28.0': optional: true '@esbuild/linux-x64@0.27.4': optional: true - '@esbuild/netbsd-arm64@0.27.3': + '@esbuild/linux-x64@0.28.0': optional: true '@esbuild/netbsd-arm64@0.27.4': optional: true - '@esbuild/netbsd-x64@0.27.3': + '@esbuild/netbsd-arm64@0.28.0': optional: true '@esbuild/netbsd-x64@0.27.4': optional: true - '@esbuild/openbsd-arm64@0.27.3': + '@esbuild/netbsd-x64@0.28.0': optional: true '@esbuild/openbsd-arm64@0.27.4': optional: true - '@esbuild/openbsd-x64@0.27.3': + '@esbuild/openbsd-arm64@0.28.0': optional: true '@esbuild/openbsd-x64@0.27.4': optional: true - '@esbuild/openharmony-arm64@0.27.3': + '@esbuild/openbsd-x64@0.28.0': optional: true '@esbuild/openharmony-arm64@0.27.4': optional: true - '@esbuild/sunos-x64@0.27.3': + '@esbuild/openharmony-arm64@0.28.0': optional: true '@esbuild/sunos-x64@0.27.4': optional: true - '@esbuild/win32-arm64@0.27.3': + '@esbuild/sunos-x64@0.28.0': optional: true '@esbuild/win32-arm64@0.27.4': optional: true - '@esbuild/win32-ia32@0.27.3': + '@esbuild/win32-arm64@0.28.0': optional: true '@esbuild/win32-ia32@0.27.4': optional: true - '@esbuild/win32-x64@0.27.3': + '@esbuild/win32-ia32@0.28.0': optional: true '@esbuild/win32-x64@0.27.4': optional: true + '@esbuild/win32-x64@0.28.0': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.28.0(jiti@2.4.2))': + dependencies: + eslint: 9.28.0(jiti@2.4.2) + eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.4.2))': dependencies: eslint: 9.39.4(jiti@2.4.2) @@ -12460,6 +12185,14 @@ snapshots: '@eslint-community/regexpp@4.12.2': {} + '@eslint/config-array@0.20.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + '@eslint/config-array@0.21.2': dependencies: '@eslint/object-schema': 2.1.7 @@ -12468,10 +12201,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/config-helpers@0.2.3': {} + '@eslint/config-helpers@0.4.2': dependencies: '@eslint/core': 0.17.0 + '@eslint/core@0.14.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/core@0.17.0': dependencies: '@types/json-schema': 7.0.15 @@ -12490,10 +12233,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/js@9.28.0': {} + '@eslint/js@9.39.4': {} '@eslint/object-schema@2.1.7': {} + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + '@eslint/plugin-kit@0.4.1': dependencies: '@eslint/core': 0.17.0 @@ -12561,7 +12311,7 @@ snapshots: dependencies: '@excalidraw/markdown-to-text': 0.1.2 '@mermaid-js/parser': 0.6.3 - mermaid: 11.13.0 + mermaid: 11.15.0 nanoid: 4.0.2 '@excalidraw/random-username@1.1.0': {} @@ -12605,7 +12355,7 @@ snapshots: dependencies: dequal: 2.0.3 - '@fastify/multipart@9.4.0': + '@fastify/multipart@10.0.0': dependencies: '@fastify/busboy': 3.1.1 '@fastify/deepmerge': 3.1.0 @@ -12623,10 +12373,10 @@ snapshots: '@lukeed/ms': 2.0.2 escape-html: 1.0.3 fast-decode-uri-component: 1.0.1 - http-errors: 2.0.0 + http-errors: 2.0.1 mime: 3.0.0 - '@fastify/static@9.0.0': + '@fastify/static@9.1.3': dependencies: '@fastify/accept-negotiator': 2.0.1 '@fastify/send': 4.1.0 @@ -12674,7 +12424,7 @@ snapshots: '@hocuspocus/common': 3.4.4 '@lifeomic/attempt': 3.0.3 lib0: 0.2.117 - ws: 8.20.0 + ws: 8.20.1 y-protocols: 1.0.6(yjs@13.6.30) yjs: 13.6.30 transitivePeerDependencies: @@ -12688,7 +12438,7 @@ snapshots: async-mutex: 0.5.0 kleur: 4.1.5 lib0: 0.2.117 - ws: 8.20.0 + ws: 8.20.1 y-protocols: 1.0.6(yjs@13.6.30) yjs: 13.6.30 transitivePeerDependencies: @@ -12703,9 +12453,9 @@ snapshots: y-prosemirror: 1.3.7(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.30))(yjs@13.6.30) yjs: 13.6.30 - '@hono/node-server@1.19.13(hono@4.12.12)': + '@hono/node-server@1.19.13(hono@4.12.18)': dependencies: - hono: 4.12.12 + hono: 4.12.18 '@humanfs/core@0.19.1': {} @@ -12868,8 +12618,6 @@ snapshots: optionalDependencies: '@types/node': 25.5.0 - '@ioredis/commands@1.5.0': {} - '@ioredis/commands@1.5.1': {} '@istanbuljs/load-nyc-config@1.1.0': @@ -13090,7 +12838,7 @@ snapshots: '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/remapping@2.3.5': @@ -13107,6 +12855,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -13128,18 +12878,14 @@ snapshots: '@keyv/serialize@1.1.1': {} - '@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)': + '@langchain/core@1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.1)(zod@4.3.6))(ws@8.20.1)': dependencies: '@cfworker/json-schema': 4.1.1 '@standard-schema/spec': 1.1.0 - ansi-styles: 5.2.0 - camelcase: 6.3.0 - decamelize: 1.2.0 js-tiktoken: 1.0.21 - langsmith: 0.5.18(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) + langsmith: 0.7.0(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.1)(zod@4.3.6))(ws@8.20.1) mustache: 4.2.0 p-queue: 6.6.2 - uuid: 11.1.0 zod: 4.3.6 transitivePeerDependencies: - '@opentelemetry/api' @@ -13148,9 +12894,9 @@ snapshots: - openai - ws - '@langchain/textsplitters@1.0.1(@langchain/core@1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))': + '@langchain/textsplitters@1.0.1(@langchain/core@1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.1)(zod@4.3.6))(ws@8.20.1))': dependencies: - '@langchain/core': 1.1.39(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) + '@langchain/core': 1.1.46(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.1)(zod@4.3.6))(ws@8.20.1) js-tiktoken: 1.0.21 '@lifeomic/attempt@3.0.3': {} @@ -13226,13 +12972,13 @@ snapshots: dependencies: langium: 3.3.1 - '@mermaid-js/parser@1.0.1': + '@mermaid-js/parser@1.1.1': dependencies: - langium: 4.2.1 + '@chevrotain/types': 11.1.2 '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6)': dependencies: - '@hono/node-server': 1.19.13(hono@4.12.12) + '@hono/node-server': 1.19.13(hono@4.12.18) ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) content-type: 1.0.5 @@ -13242,7 +12988,7 @@ snapshots: eventsource-parser: 3.0.6 express: 5.2.1 express-rate-limit: 8.2.2(express@5.2.1) - hono: 4.12.12 + hono: 4.12.18 jose: 6.1.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -13272,54 +13018,6 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2': optional: true - '@napi-rs/canvas-android-arm64@0.1.97': - optional: true - - '@napi-rs/canvas-darwin-arm64@0.1.97': - optional: true - - '@napi-rs/canvas-darwin-x64@0.1.97': - optional: true - - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.97': - optional: true - - '@napi-rs/canvas-linux-arm64-gnu@0.1.97': - optional: true - - '@napi-rs/canvas-linux-arm64-musl@0.1.97': - optional: true - - '@napi-rs/canvas-linux-riscv64-gnu@0.1.97': - optional: true - - '@napi-rs/canvas-linux-x64-gnu@0.1.97': - optional: true - - '@napi-rs/canvas-linux-x64-musl@0.1.97': - optional: true - - '@napi-rs/canvas-win32-arm64-msvc@0.1.97': - optional: true - - '@napi-rs/canvas-win32-x64-msvc@0.1.97': - optional: true - - '@napi-rs/canvas@0.1.97': - optionalDependencies: - '@napi-rs/canvas-android-arm64': 0.1.97 - '@napi-rs/canvas-darwin-arm64': 0.1.97 - '@napi-rs/canvas-darwin-x64': 0.1.97 - '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.97 - '@napi-rs/canvas-linux-arm64-gnu': 0.1.97 - '@napi-rs/canvas-linux-arm64-musl': 0.1.97 - '@napi-rs/canvas-linux-riscv64-gnu': 0.1.97 - '@napi-rs/canvas-linux-x64-gnu': 0.1.97 - '@napi-rs/canvas-linux-x64-musl': 0.1.97 - '@napi-rs/canvas-win32-arm64-msvc': 0.1.97 - '@napi-rs/canvas-win32-x64-msvc': 0.1.97 - optional: true - '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.8.1 @@ -13340,63 +13038,63 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@nest-lab/throttler-storage-redis@1.2.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/throttler@6.5.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2))(ioredis@5.10.1)(reflect-metadata@0.2.2)': + '@nest-lab/throttler-storage-redis@1.2.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/throttler@6.5.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.2.2))(ioredis@5.10.1)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/throttler': 6.5.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.2.2) ioredis: 5.10.1 reflect-metadata: 0.2.2 tslib: 2.8.1 - '@nestjs-labs/nestjs-ioredis@11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(ioredis@5.10.1)': + '@nestjs-labs/nestjs-ioredis@11.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(ioredis@5.10.1)': dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) ioredis: 5.10.1 tslib: 2.8.1 - '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': + '@nestjs/bull-shared@11.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)': dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(bullmq@5.71.0)': + '@nestjs/bullmq@11.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(bullmq@5.76.10)': dependencies: - '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) - bullmq: 5.71.0 + '@nestjs/bull-shared': 11.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) + bullmq: 5.76.10 tslib: 2.8.1 - '@nestjs/cache-manager@3.1.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2)': + '@nestjs/cache-manager@3.1.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(cache-manager@7.2.8)(keyv@5.6.0)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) cache-manager: 7.2.8 keyv: 5.6.0 rxjs: 7.8.2 - '@nestjs/cli@11.0.18(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@25.5.0)': + '@nestjs/cli@11.0.21(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@25.5.0)': dependencies: - '@angular-devkit/core': 19.2.23(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.23(chokidar@4.0.3) - '@angular-devkit/schematics-cli': 19.2.23(@types/node@25.5.0)(chokidar@4.0.3) + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics-cli': 19.2.24(@types/node@25.5.0)(chokidar@4.0.3) '@inquirer/prompts': 7.10.1(@types/node@25.5.0) '@nestjs/schematics': 11.0.10(chokidar@4.0.3)(typescript@5.9.3) ansis: 4.2.0 chokidar: 4.0.3 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5))) + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.5.25(@swc/helpers@0.5.5))) glob: 13.0.6 node-emoji: 1.11.0 ora: 5.4.1 tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.9.3 - webpack: 5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5)) + webpack: 5.106.0(@swc/core@1.5.25(@swc/helpers@0.5.5)) webpack-node-externals: 3.0.0 optionalDependencies: '@swc/core': 1.5.25(@swc/helpers@0.5.5) @@ -13406,7 +13104,7 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: file-type: 21.3.4 iterare: 1.2.1 @@ -13421,17 +13119,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/config@4.0.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + '@nestjs/config@4.0.4(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - dotenv: 17.2.3 + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + dotenv: 17.4.1 dotenv-expand: 12.0.3 lodash: 4.18.1 rxjs: 7.8.2 - '@nestjs/core@11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -13441,41 +13139,41 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/websockets': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/platform-socket.io@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/websockets': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-socket.io@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': + '@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)': dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) eventemitter2: 6.4.9 - '@nestjs/jwt@11.0.2(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))': + '@nestjs/jwt@11.0.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@types/jsonwebtoken': 9.0.10 jsonwebtoken: 9.0.3 - '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.15.1 - '@nestjs/passport@11.0.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': + '@nestjs/passport@11.0.5(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) passport: 0.7.0 - '@nestjs/platform-fastify@11.1.18(@fastify/static@9.0.0)(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': + '@nestjs/platform-fastify@11.1.19(@fastify/static@9.1.3)(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)': dependencies: '@fastify/cors': 11.2.0 '@fastify/formbody': 8.0.2 - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) fast-querystring: 1.1.2 - fastify: 5.8.3 + fastify: 5.8.5 fastify-plugin: 5.1.0 find-my-way: 9.5.0 light-my-request: 6.6.0 @@ -13483,12 +13181,12 @@ snapshots: reusify: 1.1.0 tslib: 2.8.1 optionalDependencies: - '@fastify/static': 9.0.0 + '@fastify/static': 9.1.3 - '@nestjs/platform-socket.io@11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(rxjs@7.8.2)': + '@nestjs/platform-socket.io@11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/websockets': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/platform-socket.io@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/websockets': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-socket.io@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) rxjs: 7.8.2 socket.io: 4.8.3 tslib: 2.8.1 @@ -13497,10 +13195,10 @@ snapshots: - supports-color - utf-8-validate - '@nestjs/schedule@6.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': + '@nestjs/schedule@6.1.3(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)': dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) cron: 4.4.0 '@nestjs/schematics@11.0.10(chokidar@4.0.3)(typescript@5.9.3)': @@ -13514,43 +13212,45 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/terminus@11.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/terminus@11.1.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) boxen: 5.1.2 check-disk-space: 3.4.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 - '@nestjs/testing@11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': + '@nestjs/testing@11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)': dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 - '@nestjs/throttler@6.5.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2)': + '@nestjs/throttler@6.5.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 - '@nestjs/websockets@11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/platform-socket.io@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/websockets@11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(@nestjs/platform-socket.io@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) iterare: 1.2.1 object-hash: 3.0.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-socket.io': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(rxjs@7.8.2) + '@nestjs/platform-socket.io': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(rxjs@7.8.2) '@noble/hashes@1.8.0': {} '@noble/hashes@2.0.1': {} + '@nodable/entities@2.1.0': {} + '@node-saml/node-saml@5.1.0': dependencies: '@types/debug': 4.1.12 @@ -13558,8 +13258,8 @@ snapshots: '@types/xml-encryption': 1.2.4 '@types/xml2js': 0.4.14 '@xmldom/is-dom-node': 1.0.1 - '@xmldom/xmldom': 0.8.12 - debug: 4.4.0 + '@xmldom/xmldom': 0.8.13 + debug: 4.4.3 xml-crypto: 6.1.2 xml-encryption: 3.1.0 xml2js: 0.6.2 @@ -13716,7 +13416,7 @@ snapshots: '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) - protobufjs: 7.5.4 + protobufjs: 7.5.8 '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': dependencies: @@ -13762,34 +13462,34 @@ snapshots: '@pkgr/core@0.2.9': {} - '@posthog/core@1.24.1': + '@posthog/core@1.27.6': dependencies: - cross-spawn: 7.0.6 + '@posthog/types': 1.372.2 - '@posthog/types@1.363.1': {} + '@posthog/types@1.372.2': {} '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} - '@protobufjs/codegen@2.0.4': {} + '@protobufjs/codegen@2.0.5': {} '@protobufjs/eventemitter@1.1.0': {} '@protobufjs/fetch@1.1.0': dependencies: '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.0 + '@protobufjs/inquire': 1.1.1 '@protobufjs/float@1.0.2': {} - '@protobufjs/inquire@1.1.0': {} + '@protobufjs/inquire@1.1.1': {} '@protobufjs/path@1.1.2': {} '@protobufjs/pool@1.1.0': {} - '@protobufjs/utf8@1.1.0': {} + '@protobufjs/utf8@1.1.1': {} '@radix-ui/number@1.1.1': {} @@ -14538,136 +14238,13 @@ snapshots: '@radix-ui/rect@1.1.1': {} - '@react-dnd/asap@4.0.1': {} - - '@react-dnd/invariant@2.0.0': {} - - '@react-dnd/shallowequal@2.0.0': {} - - '@react-email/body@0.3.0(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-email/button@0.2.1(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-email/code-block@0.2.1(react@18.3.1)': - dependencies: - prismjs: 1.30.0 - react: 18.3.1 - - '@react-email/code-inline@0.0.6(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-email/column@0.0.14(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-email/components@1.0.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@react-email/body': 0.3.0(react@18.3.1) - '@react-email/button': 0.2.1(react@18.3.1) - '@react-email/code-block': 0.2.1(react@18.3.1) - '@react-email/code-inline': 0.0.6(react@18.3.1) - '@react-email/column': 0.0.14(react@18.3.1) - '@react-email/container': 0.0.16(react@18.3.1) - '@react-email/font': 0.0.10(react@18.3.1) - '@react-email/head': 0.0.13(react@18.3.1) - '@react-email/heading': 0.0.16(react@18.3.1) - '@react-email/hr': 0.0.12(react@18.3.1) - '@react-email/html': 0.0.12(react@18.3.1) - '@react-email/img': 0.0.12(react@18.3.1) - '@react-email/link': 0.0.13(react@18.3.1) - '@react-email/markdown': 0.0.18(react@18.3.1) - '@react-email/preview': 0.0.14(react@18.3.1) - '@react-email/render': 2.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-email/row': 0.0.13(react@18.3.1) - '@react-email/section': 0.0.17(react@18.3.1) - '@react-email/tailwind': 2.0.6(@react-email/body@0.3.0(react@18.3.1))(@react-email/button@0.2.1(react@18.3.1))(@react-email/code-block@0.2.1(react@18.3.1))(@react-email/code-inline@0.0.6(react@18.3.1))(@react-email/container@0.0.16(react@18.3.1))(@react-email/heading@0.0.16(react@18.3.1))(@react-email/hr@0.0.12(react@18.3.1))(@react-email/img@0.0.12(react@18.3.1))(@react-email/link@0.0.13(react@18.3.1))(@react-email/preview@0.0.14(react@18.3.1))(@react-email/text@0.1.6(react@18.3.1))(react@18.3.1) - '@react-email/text': 0.1.6(react@18.3.1) - react: 18.3.1 - transitivePeerDependencies: - - react-dom - - '@react-email/container@0.0.16(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-email/font@0.0.10(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-email/head@0.0.13(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-email/heading@0.0.16(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-email/hr@0.0.12(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-email/html@0.0.12(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-email/img@0.0.12(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-email/link@0.0.13(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-email/markdown@0.0.18(react@18.3.1)': - dependencies: - marked: 15.0.12 - react: 18.3.1 - - '@react-email/preview@0.0.14(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-email/render@2.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-email/render@2.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: html-to-text: 9.0.5 prettier: 3.8.1 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@react-email/row@0.0.13(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-email/section@0.0.17(react@18.3.1)': - dependencies: - react: 18.3.1 - - '@react-email/tailwind@2.0.6(@react-email/body@0.3.0(react@18.3.1))(@react-email/button@0.2.1(react@18.3.1))(@react-email/code-block@0.2.1(react@18.3.1))(@react-email/code-inline@0.0.6(react@18.3.1))(@react-email/container@0.0.16(react@18.3.1))(@react-email/heading@0.0.16(react@18.3.1))(@react-email/hr@0.0.12(react@18.3.1))(@react-email/img@0.0.12(react@18.3.1))(@react-email/link@0.0.13(react@18.3.1))(@react-email/preview@0.0.14(react@18.3.1))(@react-email/text@0.1.6(react@18.3.1))(react@18.3.1)': - dependencies: - '@react-email/text': 0.1.6(react@18.3.1) - react: 18.3.1 - tailwindcss: 4.1.18 - optionalDependencies: - '@react-email/body': 0.3.0(react@18.3.1) - '@react-email/button': 0.2.1(react@18.3.1) - '@react-email/code-block': 0.2.1(react@18.3.1) - '@react-email/code-inline': 0.0.6(react@18.3.1) - '@react-email/container': 0.0.16(react@18.3.1) - '@react-email/heading': 0.0.16(react@18.3.1) - '@react-email/hr': 0.0.12(react@18.3.1) - '@react-email/img': 0.0.12(react@18.3.1) - '@react-email/link': 0.0.13(react@18.3.1) - '@react-email/preview': 0.0.14(react@18.3.1) - - '@react-email/text@0.1.6(react@18.3.1)': - dependencies: - react: 18.3.1 - '@redis/client@5.11.0': dependencies: cluster-key-slot: 1.1.2 @@ -14747,254 +14324,54 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@smithy/abort-controller@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 + '@slidoapp/emoji-mart-data@1.2.4': {} - '@smithy/chunked-blob-reader-native@4.2.3': + '@slidoapp/emoji-mart-react@1.1.5(@slidoapp/emoji-mart@5.8.7)(react@18.3.1)': dependencies: - '@smithy/util-base64': 4.3.2 - tslib: 2.8.1 + '@slidoapp/emoji-mart': 5.8.7 + react: 18.3.1 - '@smithy/chunked-blob-reader@5.2.2': - dependencies: - tslib: 2.8.1 + '@slidoapp/emoji-mart@5.8.7': {} - '@smithy/config-resolver@4.4.13': - dependencies: - '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-config-provider': 4.2.2 - '@smithy/util-endpoints': 3.3.3 - '@smithy/util-middleware': 4.2.12 - tslib: 2.8.1 - - '@smithy/core@3.23.13': - dependencies: - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-stream': 4.5.21 - '@smithy/util-utf8': 4.2.2 - '@smithy/uuid': 1.1.2 - tslib: 2.8.1 - - '@smithy/credential-provider-imds@4.2.12': - dependencies: - '@smithy/node-config-provider': 4.3.12 - '@smithy/property-provider': 4.2.12 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - tslib: 2.8.1 - - '@smithy/eventstream-codec@4.2.12': + '@smithy/core@3.24.3': dependencies: '@aws-crypto/crc32': 5.2.0 - '@smithy/types': 4.13.1 - '@smithy/util-hex-encoding': 4.2.2 + '@smithy/types': 4.14.2 tslib: 2.8.1 - '@smithy/eventstream-serde-browser@4.2.12': + '@smithy/credential-provider-imds@4.3.3': dependencies: - '@smithy/eventstream-serde-universal': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - '@smithy/eventstream-serde-config-resolver@4.3.12': + '@smithy/fetch-http-handler@5.4.3': dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/eventstream-serde-node@4.2.12': - dependencies: - '@smithy/eventstream-serde-universal': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/eventstream-serde-universal@4.2.12': - dependencies: - '@smithy/eventstream-codec': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/fetch-http-handler@5.3.15': - dependencies: - '@smithy/protocol-http': 5.3.12 - '@smithy/querystring-builder': 4.2.12 - '@smithy/types': 4.13.1 - '@smithy/util-base64': 4.3.2 - tslib: 2.8.1 - - '@smithy/hash-blob-browser@4.2.13': - dependencies: - '@smithy/chunked-blob-reader': 5.2.2 - '@smithy/chunked-blob-reader-native': 4.2.3 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/hash-node@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - '@smithy/util-buffer-from': 4.2.2 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@smithy/hash-stream-node@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@smithy/invalid-dependency@4.2.12': - dependencies: - '@smithy/types': 4.13.1 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 '@smithy/is-array-buffer@2.2.0': dependencies: tslib: 2.8.1 - '@smithy/is-array-buffer@4.2.2': + '@smithy/node-http-handler@4.7.3': + dependencies: + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@smithy/signature-v4@5.4.3': + dependencies: + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + + '@smithy/types@4.14.1': dependencies: tslib: 2.8.1 - '@smithy/md5-js@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@smithy/middleware-content-length@4.2.12': - dependencies: - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/middleware-endpoint@4.4.28': - dependencies: - '@smithy/core': 3.23.13 - '@smithy/middleware-serde': 4.2.16 - '@smithy/node-config-provider': 4.3.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-middleware': 4.2.12 - tslib: 2.8.1 - - '@smithy/middleware-retry@4.4.46': - dependencies: - '@smithy/node-config-provider': 4.3.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/service-error-classification': 4.2.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.13 - '@smithy/uuid': 1.1.2 - tslib: 2.8.1 - - '@smithy/middleware-serde@4.2.16': - dependencies: - '@smithy/core': 3.23.13 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/middleware-stack@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/node-config-provider@4.3.12': - dependencies: - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/node-http-handler@4.5.1': - dependencies: - '@smithy/protocol-http': 5.3.12 - '@smithy/querystring-builder': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/property-provider@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/protocol-http@5.3.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/querystring-builder@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - '@smithy/util-uri-escape': 4.2.2 - tslib: 2.8.1 - - '@smithy/querystring-parser@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/service-error-classification@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - - '@smithy/shared-ini-file-loader@4.4.7': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/signature-v4@5.3.12': - dependencies: - '@smithy/is-array-buffer': 4.2.2 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-hex-encoding': 4.2.2 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-uri-escape': 4.2.2 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@smithy/smithy-client@4.12.8': - dependencies: - '@smithy/core': 3.23.13 - '@smithy/middleware-endpoint': 4.4.28 - '@smithy/middleware-stack': 4.2.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-stream': 4.5.21 - tslib: 2.8.1 - - '@smithy/types@4.13.1': - dependencies: - tslib: 2.8.1 - - '@smithy/url-parser@4.2.12': - dependencies: - '@smithy/querystring-parser': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-base64@4.3.2': - dependencies: - '@smithy/util-buffer-from': 4.2.2 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@smithy/util-body-length-browser@4.2.2': - dependencies: - tslib: 2.8.1 - - '@smithy/util-body-length-node@4.2.3': + '@smithy/types@4.14.2': dependencies: tslib: 2.8.1 @@ -15003,87 +14380,11 @@ snapshots: '@smithy/is-array-buffer': 2.2.0 tslib: 2.8.1 - '@smithy/util-buffer-from@4.2.2': - dependencies: - '@smithy/is-array-buffer': 4.2.2 - tslib: 2.8.1 - - '@smithy/util-config-provider@4.2.2': - dependencies: - tslib: 2.8.1 - - '@smithy/util-defaults-mode-browser@4.3.44': - dependencies: - '@smithy/property-provider': 4.2.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-defaults-mode-node@4.2.48': - dependencies: - '@smithy/config-resolver': 4.4.13 - '@smithy/credential-provider-imds': 4.2.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/property-provider': 4.2.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-endpoints@3.3.3': - dependencies: - '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-hex-encoding@4.2.2': - dependencies: - tslib: 2.8.1 - - '@smithy/util-middleware@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-retry@4.2.13': - dependencies: - '@smithy/service-error-classification': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-stream@4.5.21': - dependencies: - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/node-http-handler': 4.5.1 - '@smithy/types': 4.13.1 - '@smithy/util-base64': 4.3.2 - '@smithy/util-buffer-from': 4.2.2 - '@smithy/util-hex-encoding': 4.2.2 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@smithy/util-uri-escape@4.2.2': - dependencies: - tslib: 2.8.1 - '@smithy/util-utf8@2.3.0': dependencies: '@smithy/util-buffer-from': 2.2.0 tslib: 2.8.1 - '@smithy/util-utf8@4.2.2': - dependencies: - '@smithy/util-buffer-from': 4.2.2 - tslib: 2.8.1 - - '@smithy/util-waiter@4.2.14': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/uuid@1.1.2': - dependencies: - tslib: 2.8.1 - '@socket.io/component-emitter@3.1.0': {} '@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.4)': @@ -15097,6 +14398,12 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@statsig/client-core@3.33.0': {} + + '@statsig/js-client@3.33.0': + dependencies: + '@statsig/client-core': 3.33.0 + '@swc/core-darwin-arm64@1.5.25': optional: true @@ -15130,7 +14437,7 @@ snapshots: '@swc/core@1.5.25(@swc/helpers@0.5.5)': dependencies: '@swc/counter': 0.1.3 - '@swc/types': 0.1.25 + '@swc/types': 0.1.26 optionalDependencies: '@swc/core-darwin-arm64': 1.5.25 '@swc/core-darwin-x64': 1.5.25 @@ -15154,7 +14461,7 @@ snapshots: tslib: 2.8.1 optional: true - '@swc/types@0.1.25': + '@swc/types@0.1.26': dependencies: '@swc/counter': 0.1.3 optional: true @@ -15166,20 +14473,20 @@ snapshots: '@tabler/icons@3.40.0': {} - '@tanstack/eslint-plugin-query@5.94.4(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)': + '@tanstack/eslint-plugin-query@5.94.4(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3)': dependencies: - '@typescript-eslint/utils': 8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3) - eslint: 9.39.4(jiti@2.4.2) + '@typescript-eslint/utils': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + eslint: 9.28.0(jiti@2.4.2) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@tanstack/query-core@5.99.1': {} + '@tanstack/query-core@5.90.17': {} - '@tanstack/react-query@5.99.1(react@18.3.1)': + '@tanstack/react-query@5.90.17(react@18.3.1)': dependencies: - '@tanstack/query-core': 5.99.1 + '@tanstack/query-core': 5.90.17 react: 18.3.1 '@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -15198,6 +14505,37 @@ snapshots: '@tanstack/virtual-core@3.14.0': {} + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.6.0': + dependencies: + '@adobe/css-tools': 4.4.3 + aria-query: 5.3.2 + chalk: 3.0.0 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + lodash: 4.18.1 + redent: 3.0.0 + + '@testing-library/react@16.1.0(@testing-library/dom@10.4.1)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.29.2 + '@testing-library/dom': 10.4.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@tiptap/core@3.20.4(@tiptap/pm@3.20.4)': dependencies: '@tiptap/pm': 3.20.4 @@ -15495,6 +14833,8 @@ snapshots: dependencies: tslib: 2.8.1 + '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.5 @@ -15529,6 +14869,11 @@ snapshots: '@types/bytes@3.1.5': {} + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/connect@3.4.38': dependencies: '@types/node': 25.5.0 @@ -15664,18 +15009,22 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 8.56.10 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/eslint@8.56.10': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} + '@types/express-serve-static-core@4.17.43': dependencies: '@types/node': 25.5.0 @@ -15834,7 +15183,7 @@ snapshots: '@types/react@18.3.12': dependencies: '@types/prop-types': 15.7.11 - csstype: 3.1.3 + csstype: 3.2.3 '@types/send@0.17.4': dependencies: @@ -15875,7 +15224,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.5.0 + '@types/node': 22.19.1 '@types/xml-encryption@1.2.4': dependencies: @@ -15895,6 +15244,22 @@ snapshots: dependencies: '@types/node': 25.5.0 + '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/type-utils': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.1 + eslint: 9.28.0(jiti@2.4.2) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3))(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -15911,6 +15276,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.57.1 + debug: 4.4.3 + eslint: 9.28.0(jiti@2.4.2) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.57.1 @@ -15941,6 +15318,18 @@ snapshots: dependencies: typescript: 5.9.3 + '@typescript-eslint/type-utils@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.28.0(jiti@2.4.2) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/type-utils@8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.57.1 @@ -15970,6 +15359,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.28.0(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.57.1 + '@typescript-eslint/types': 8.57.1 + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + eslint: 9.28.0(jiti@2.4.2) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.4.2)) @@ -15986,6 +15386,14 @@ snapshots: '@typescript-eslint/types': 8.57.1 eslint-visitor-keys: 5.0.1 + '@typespec/ts-http-runtime@0.3.5': + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@ucast/core@1.10.2': {} '@ucast/js@3.0.4': @@ -16070,10 +15478,51 @@ snapshots: '@vercel/oidc@3.1.0': {} - '@vitejs/plugin-react@6.0.1(vite@8.0.5(@types/node@22.19.1)(esbuild@0.27.4)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.8))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': + '@vitejs/plugin-react@6.0.1(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.5(@types/node@22.19.1)(esbuild@0.27.4)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.8))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + + '@vitest/expect@4.1.6': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.6 + '@vitest/utils': 4.1.6 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.6(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.6 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + + '@vitest/pretty-format@4.1.6': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.6': + dependencies: + '@vitest/utils': 4.1.6 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.6': + dependencies: + '@vitest/pretty-format': 4.1.6 + '@vitest/utils': 4.1.6 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.6': {} + + '@vitest/utils@4.1.6': + dependencies: + '@vitest/pretty-format': 4.1.6 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 '@webassemblyjs/ast@1.14.1': dependencies: @@ -16153,7 +15602,7 @@ snapshots: '@xmldom/is-dom-node@1.0.1': {} - '@xmldom/xmldom@0.8.12': {} + '@xmldom/xmldom@0.8.13': {} '@xtuc/ieee754@1.2.0': {} @@ -16198,12 +15647,6 @@ snapshots: address@1.2.2: {} - agent-base@7.1.1: - dependencies: - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - agent-base@7.1.4: {} ai-sdk-ollama@3.8.1(ai@6.0.134(zod@4.3.6))(zod@4.3.6): @@ -16269,8 +15712,6 @@ snapshots: ansi-regex@5.0.1: {} - ansi-regex@6.0.1: {} - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -16296,10 +15737,11 @@ snapshots: dependencies: tslib: 2.8.1 - array-buffer-byte-length@1.0.1: + aria-query@5.3.0: dependencies: - call-bind: 1.0.7 - is-array-buffer: 3.0.4 + dequal: 2.0.3 + + aria-query@5.3.2: {} array-buffer-byte-length@1.0.2: dependencies: @@ -16308,57 +15750,46 @@ snapshots: array-includes@3.1.8: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 - is-string: 1.1.0 + is-string: 1.1.1 array-timsort@1.0.3: {} array.prototype.findlast@1.2.5: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-errors: 1.3.0 es-object-atoms: 1.1.1 es-shim-unscopables: 1.0.2 array.prototype.flat@1.3.2: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-shim-unscopables: 1.0.2 array.prototype.flatmap@1.3.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-shim-unscopables: 1.0.2 array.prototype.tosorted@1.1.4: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-errors: 1.3.0 es-shim-unscopables: 1.0.2 - arraybuffer.prototype.slice@1.0.3: - dependencies: - array-buffer-byte-length: 1.0.1 - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.5 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.4 - is-shared-array-buffer: 1.0.3 - arraybuffer.prototype.slice@1.0.4: dependencies: array-buffer-byte-length: 1.0.2 @@ -16371,6 +15802,8 @@ snapshots: asap@2.0.6: {} + assertion-error@2.0.1: {} + async-lock@1.4.1: {} async-mutex@0.5.0: @@ -16397,7 +15830,7 @@ snapshots: '@fastify/error': 4.0.0 fastq: 1.17.1 - axios@1.15.0: + axios@1.16.0: dependencies: follow-redirects: 1.16.0 form-data: 4.0.5 @@ -16522,6 +15955,8 @@ snapshots: binary-extensions@2.3.0: {} + bind-event-listener@3.0.0: {} + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -16570,7 +16005,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.5: + brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -16612,15 +16047,14 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - bullmq@5.71.0: + bullmq@5.76.10: dependencies: cron-parser: 4.9.0 - ioredis: 5.9.3 - msgpackr: 1.11.5 + ioredis: 5.10.1 + msgpackr: 2.0.1 node-abort-controller: 3.1.1 - semver: 7.7.4 + semver: 7.8.0 tslib: 2.8.1 - uuid: 11.1.0 transitivePeerDependencies: - supports-color @@ -16636,14 +16070,6 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 - call-bind@1.0.7: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - call-bind@1.0.8: dependencies: call-bind-apply-helpers: 1.0.2 @@ -16668,12 +16094,17 @@ snapshots: canvas-roundrect-polyfill@0.0.1: {} - chalk@4.1.2: + chai@6.2.2: {} + + chalk@3.0.0: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.6.2: {} + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 char-regex@1.0.2: {} @@ -16709,11 +16140,6 @@ snapshots: chevrotain: 11.0.3 lodash-es: 4.18.1 - chevrotain-allstar@0.3.1(chevrotain@11.1.2): - dependencies: - chevrotain: 11.1.2 - lodash-es: 4.18.1 - chevrotain@11.0.3: dependencies: '@chevrotain/cst-dts-gen': 11.0.3 @@ -16723,15 +16149,6 @@ snapshots: '@chevrotain/utils': 11.0.3 lodash-es: 4.18.1 - chevrotain@11.1.2: - dependencies: - '@chevrotain/cst-dts-gen': 11.1.2 - '@chevrotain/gast': 11.1.2 - '@chevrotain/regexp-to-ast': 11.1.2 - '@chevrotain/types': 11.1.2 - '@chevrotain/utils': 11.1.2 - lodash-es: 4.18.1 - chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -16752,9 +16169,7 @@ snapshots: ci-info@4.4.0: {} - citty@0.1.6: - dependencies: - consola: 3.4.2 + citty@0.2.2: {} cjs-module-lexer@2.2.0: {} @@ -16772,10 +16187,6 @@ snapshots: dependencies: restore-cursor: 3.1.0 - cli-cursor@5.0.0: - dependencies: - restore-cursor: 5.1.0 - cli-spinners@2.6.1: {} cli-spinners@2.9.2: {} @@ -16873,8 +16284,6 @@ snapshots: confbox@0.1.8: {} - confbox@0.2.2: {} - consola@3.4.2: {} content-disposition@1.0.1: {} @@ -16967,7 +16376,7 @@ snapshots: dependencies: cross-spawn: 7.0.6 - cross-fetch@4.0.0: + cross-fetch@4.1.0: dependencies: node-fetch: 2.7.0 transitivePeerDependencies: @@ -16987,8 +16396,15 @@ snapshots: domutils: 3.2.2 nth-check: 2.1.1 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + css-what@6.1.0: {} + css.escape@1.5.1: {} + cssesc@3.0.0: {} cssstyle@4.2.1: @@ -16996,7 +16412,7 @@ snapshots: '@asamuzakjp/css-color': 2.8.3 rrweb-cssom: 0.8.0 - csstype@3.1.3: {} + csstype@3.2.3: {} csv-stringify@6.7.0: {} @@ -17189,36 +16605,18 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - data-view-buffer@1.0.1: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-data-view: 1.0.1 - data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 - data-view-byte-length@1.0.1: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-data-view: 1.0.1 - data-view-byte-length@1.0.2: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 - data-view-byte-offset@1.0.0: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-data-view: 1.0.1 - data-view-byte-offset@1.0.1: dependencies: call-bound: 1.0.4 @@ -17245,10 +16643,6 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.0: - dependencies: - ms: 2.1.3 - debug@4.4.1: dependencies: ms: 2.1.3 @@ -17329,20 +16723,18 @@ snapshots: dingbat-to-unicode@1.0.1: {} - dnd-core@14.0.1: - dependencies: - '@react-dnd/asap': 4.0.1 - '@react-dnd/invariant': 2.0.0 - redux: 4.2.1 - doctrine@2.1.0: dependencies: esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + dom-helpers@5.2.1: dependencies: '@babel/runtime': 7.29.2 - csstype: 3.1.3 + csstype: 3.2.3 dom-serializer@2.0.0: dependencies: @@ -17356,7 +16748,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.3.3: + dompurify@3.4.1: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -17380,10 +16772,10 @@ snapshots: dotenv@16.4.7: {} - dotenv@17.2.3: {} - dotenv@17.2.4: {} + dotenv@17.4.1: {} + duck@0.1.12: dependencies: underscore: 1.13.8 @@ -17408,10 +16800,6 @@ snapshots: emittery@0.13.1: {} - emoji-mart@5.6.0: {} - - emoji-regex@10.6.0: {} - emoji-regex@8.0.0: {} encodeurl@2.0.0: {} @@ -17430,7 +16818,7 @@ snapshots: '@socket.io/component-emitter': 3.1.0 debug: 4.3.7 engine.io-parser: 5.2.2 - ws: 8.20.0 + ws: 8.20.1 xmlhttprequest-ssl: 2.1.2 transitivePeerDependencies: - bufferutil @@ -17450,17 +16838,12 @@ snapshots: cors: 2.8.5 debug: 4.3.7 engine.io-parser: 5.2.2 - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - enhanced-resolve@5.19.0: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.0 - enhanced-resolve@5.20.1: dependencies: graceful-fs: 4.2.11 @@ -17489,55 +16872,6 @@ snapshots: dependencies: is-arrayish: 0.2.1 - es-abstract@1.23.5: - dependencies: - array-buffer-byte-length: 1.0.1 - arraybuffer.prototype.slice: 1.0.3 - available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - data-view-buffer: 1.0.1 - data-view-byte-length: 1.0.1 - data-view-byte-offset: 1.0.0 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.6 - get-intrinsic: 1.3.0 - get-symbol-description: 1.0.2 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.0.3 - has-symbols: 1.1.0 - hasown: 2.0.2 - internal-slot: 1.0.7 - is-array-buffer: 3.0.4 - is-callable: 1.2.7 - is-data-view: 1.0.1 - is-negative-zero: 2.0.3 - is-regex: 1.2.0 - is-shared-array-buffer: 1.0.3 - is-string: 1.1.0 - is-typed-array: 1.1.13 - is-weakref: 1.0.2 - object-inspect: 1.13.3 - object-keys: 1.1.1 - object.assign: 4.1.5 - regexp.prototype.flags: 1.5.3 - safe-array-concat: 1.1.2 - safe-regex-test: 1.0.3 - string.prototype.trim: 1.2.9 - string.prototype.trimend: 1.0.8 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.2 - typed-array-byte-length: 1.0.1 - typed-array-byte-offset: 1.0.3 - typed-array-length: 1.0.7 - unbox-primitive: 1.0.2 - which-typed-array: 1.1.16 - es-abstract@1.24.1: dependencies: array-buffer-byte-length: 1.0.2 @@ -17619,7 +16953,7 @@ snapshots: math-intrinsics: 1.1.0 safe-array-concat: 1.1.3 - es-module-lexer@2.0.0: {} + es-module-lexer@2.1.0: {} es-object-atoms@1.1.1: dependencies: @@ -17639,40 +16973,13 @@ snapshots: es-to-primitive@1.3.0: dependencies: is-callable: 1.2.7 - is-date-object: 1.0.5 - is-symbol: 1.1.0 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + es-toolkit@1.46.1: {} es6-promise-pool@2.5.0: {} - esbuild@0.27.3: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 - esbuild@0.27.4: optionalDependencies: '@esbuild/aix-ppc64': 0.27.4 @@ -17702,6 +17009,35 @@ snapshots: '@esbuild/win32-ia32': 0.27.4 '@esbuild/win32-x64': 0.27.4 + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -17718,22 +17054,22 @@ snapshots: dependencies: eslint: 9.39.4(jiti@2.4.2) - eslint-plugin-react-hooks@7.0.1(eslint@9.39.4(jiti@2.4.2)): + eslint-plugin-react-hooks@7.0.1(eslint@9.28.0(jiti@2.4.2)): dependencies: '@babel/core': 7.28.5 '@babel/parser': 7.28.5 - eslint: 9.39.4(jiti@2.4.2) + eslint: 9.28.0(jiti@2.4.2) hermes-parser: 0.25.1 zod: 4.3.6 zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.5.2(eslint@9.39.4(jiti@2.4.2)): + eslint-plugin-react-refresh@0.5.2(eslint@9.28.0(jiti@2.4.2)): dependencies: - eslint: 9.39.4(jiti@2.4.2) + eslint: 9.28.0(jiti@2.4.2) - eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.4.2)): + eslint-plugin-react@7.37.5(eslint@9.28.0(jiti@2.4.2)): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -17741,7 +17077,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.3.1 - eslint: 9.39.4(jiti@2.4.2) + eslint: 9.28.0(jiti@2.4.2) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -17771,6 +17107,48 @@ snapshots: eslint-visitor-keys@5.0.1: {} + eslint@9.28.0(jiti@2.4.2): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.28.0(jiti@2.4.2)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.20.1 + '@eslint/config-helpers': 0.2.3 + '@eslint/core': 0.14.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.28.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + ajv: 6.14.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.3 + optionalDependencies: + jiti: 2.4.2 + transitivePeerDependencies: + - supports-color + eslint@9.39.4(jiti@2.4.2): dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.4.2)) @@ -17832,6 +17210,10 @@ snapshots: estraverse@5.3.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + esutils@2.0.3: {} etag@1.8.1: {} @@ -17862,6 +17244,8 @@ snapshots: exit-x@0.2.2: {} + expect-type@1.3.0: {} + expect@30.2.0: dependencies: '@jest/expect-utils': 30.2.0 @@ -17883,7 +17267,7 @@ snapshots: express-rate-limit@8.2.2(express@5.2.1): dependencies: express: 5.2.1 - ip-address: 10.1.0 + ip-address: 10.1.1 express@5.2.1: dependencies: @@ -17918,8 +17302,6 @@ snapshots: transitivePeerDependencies: - supports-color - exsolve@1.0.7: {} - fast-bm25@0.0.5(typescript@5.9.3): dependencies: porter2: 1.1.0 @@ -17954,15 +17336,17 @@ snapshots: fast-uri@3.0.6: {} - fast-xml-builder@1.1.4: + fast-xml-builder@1.2.0: dependencies: - path-expression-matcher: 1.2.0 + path-expression-matcher: 1.5.0 + xml-naming: 0.1.0 - fast-xml-parser@5.5.8: + fast-xml-parser@5.7.3: dependencies: - fast-xml-builder: 1.1.4 - path-expression-matcher: 1.2.0 - strnum: 2.2.1 + '@nodable/entities': 2.1.0 + fast-xml-builder: 1.2.0 + path-expression-matcher: 1.5.0 + strnum: 2.2.3 fastify-ip@2.0.0: dependencies: @@ -17972,7 +17356,7 @@ snapshots: fastify-plugin@5.1.0: {} - fastify@5.8.3: + fastify@5.8.5: dependencies: '@fastify/ajv-compiler': 4.0.5 '@fastify/error': 4.0.0 @@ -18069,15 +17453,11 @@ snapshots: follow-redirects@1.16.0: {} - for-each@0.3.3: - dependencies: - is-callable: 1.2.7 - for-each@0.3.5: dependencies: is-callable: 1.2.7 - fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5))): + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.5.25(@swc/helpers@0.5.5))): dependencies: '@babel/code-frame': 7.27.1 chalk: 4.1.2 @@ -18092,7 +17472,7 @@ snapshots: semver: 7.7.4 tapable: 2.3.0 typescript: 5.9.3 - webpack: 5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5)) + webpack: 5.106.0(@swc/core@1.5.25(@swc/helpers@0.5.5)) form-data@4.0.5: dependencies: @@ -18141,13 +17521,6 @@ snapshots: function-bind@1.1.2: {} - function.prototype.name@1.1.6: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.5 - functions-have-names: 1.2.3 - function.prototype.name@1.1.8: dependencies: call-bind: 1.0.8 @@ -18165,8 +17538,6 @@ snapshots: get-caller-file@2.0.5: {} - get-east-asian-width@1.4.0: {} - get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -18191,12 +17562,6 @@ snapshots: get-stream@6.0.1: {} - get-symbol-description@1.0.2: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 @@ -18255,12 +17620,12 @@ snapshots: happy-dom@20.8.9: dependencies: - '@types/node': 25.5.0 + '@types/node': 22.19.1 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 entities: 7.0.1 whatwg-mimetype: 3.0.0 - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -18273,8 +17638,6 @@ snapshots: dependencies: es-define-property: 1.0.1 - has-proto@1.0.3: {} - has-proto@1.2.0: dependencies: dunder-proto: 1.0.1 @@ -18305,11 +17668,7 @@ snapshots: highlightjs-sap-abap@0.3.0: {} - hoist-non-react-statics@3.3.2: - dependencies: - react-is: 16.13.1 - - hono@4.12.12: {} + hono@4.12.18: {} hookified@1.15.1: {} @@ -18347,14 +17706,6 @@ snapshots: domutils: 3.2.2 entities: 4.5.0 - http-errors@2.0.0: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -18365,7 +17716,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: - agent-base: 7.1.1 + agent-base: 7.1.4 debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -18379,9 +17730,9 @@ snapshots: human-signals@2.1.0: {} - i18next-http-backend@3.0.2: + i18next-http-backend@3.0.6: dependencies: - cross-fetch: 4.0.0 + cross-fetch: 4.1.0 transitivePeerDependencies: - encoding @@ -18430,13 +17781,9 @@ snapshots: imurmurhash@0.1.4: {} - inherits@2.0.4: {} + indent-string@4.0.0: {} - internal-slot@1.0.7: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.2 - side-channel: 1.1.0 + inherits@2.0.4: {} internal-slot@1.1.0: dependencies: @@ -18466,31 +17813,12 @@ snapshots: transitivePeerDependencies: - supports-color - ioredis@5.9.3: - dependencies: - '@ioredis/commands': 1.5.0 - cluster-key-slot: 1.1.2 - debug: 4.4.3 - denque: 2.1.0 - lodash.defaults: 4.2.0 - lodash.isarguments: 3.1.0 - redis-errors: 1.2.0 - redis-parser: 3.0.0 - standard-as-callback: 2.1.0 - transitivePeerDependencies: - - supports-color - - ip-address@10.1.0: {} + ip-address@10.1.1: {} ipaddr.js@1.9.1: {} ipaddr.js@2.2.0: {} - is-array-buffer@3.0.4: - dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.3.0 - is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -18511,11 +17839,6 @@ snapshots: dependencies: binary-extensions: 2.3.0 - is-boolean-object@1.2.0: - dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 - is-boolean-object@1.2.2: dependencies: call-bound: 1.0.4 @@ -18527,20 +17850,12 @@ snapshots: dependencies: hasown: 2.0.2 - is-data-view@1.0.1: - dependencies: - is-typed-array: 1.1.13 - is-data-view@1.0.2: dependencies: call-bound: 1.0.4 get-intrinsic: 1.3.0 is-typed-array: 1.1.15 - is-date-object@1.0.5: - dependencies: - has-tostringtag: 1.0.2 - is-date-object@1.1.0: dependencies: call-bound: 1.0.4 @@ -18568,17 +17883,10 @@ snapshots: is-interactive@1.0.0: {} - is-interactive@2.0.0: {} - is-map@2.0.3: {} is-negative-zero@2.0.3: {} - is-number-object@1.1.0: - dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 - is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -18590,13 +17898,6 @@ snapshots: is-promise@4.0.0: {} - is-regex@1.2.0: - dependencies: - call-bind: 1.0.7 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.2 - is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -18606,58 +17907,33 @@ snapshots: is-set@2.0.3: {} - is-shared-array-buffer@1.0.3: - dependencies: - call-bind: 1.0.7 - is-shared-array-buffer@1.0.4: dependencies: call-bound: 1.0.4 is-stream@2.0.1: {} - is-string@1.1.0: - dependencies: - call-bind: 1.0.7 - has-tostringtag: 1.0.2 - is-string@1.1.1: dependencies: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-symbol@1.1.0: - dependencies: - call-bind: 1.0.7 - has-symbols: 1.1.0 - safe-regex-test: 1.0.3 - is-symbol@1.1.1: dependencies: call-bound: 1.0.4 has-symbols: 1.1.0 safe-regex-test: 1.1.0 - is-typed-array@1.1.13: - dependencies: - which-typed-array: 1.1.16 - is-typed-array@1.1.15: dependencies: which-typed-array: 1.1.20 is-unicode-supported@0.1.0: {} - is-unicode-supported@1.3.0: {} - is-unicode-supported@2.1.0: {} is-weakmap@2.0.2: {} - is-weakref@1.0.2: - dependencies: - call-bind: 1.0.7 - is-weakref@1.1.1: dependencies: call-bound: 1.0.4 @@ -19134,6 +18410,34 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@25.0.0: + dependencies: + cssstyle: 4.2.1 + data-urls: 5.0.0 + decimal.js: 10.6.0 + form-data: 4.0.5 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.16 + parse5: 7.3.0 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.20.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + jsdom@26.1.0: dependencies: cssstyle: 4.2.1 @@ -19154,7 +18458,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.20.0 + ws: 8.20.1 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -19214,7 +18518,7 @@ snapshots: dependencies: array-includes: 3.1.8 array.prototype.flat: 1.3.2 - object.assign: 4.1.5 + object.assign: 4.1.7 object.values: 1.2.1 jszip@3.10.1: @@ -19257,14 +18561,14 @@ snapshots: klona@2.0.6: {} - kysely-codegen@0.20.0(kysely@0.28.14)(pg@8.16.3)(typescript@5.9.3): + kysely-codegen@0.20.0(kysely@0.28.17)(pg@8.16.3)(typescript@5.9.3): dependencies: chalk: 4.1.2 cosmiconfig: 9.0.0(typescript@5.9.3) diff: 8.0.3 dotenv: 17.2.4 dotenv-expand: 12.0.3 - kysely: 0.28.14 + kysely: 0.28.17 micromatch: 4.0.8 minimist: 1.2.8 pluralize: 8.0.0 @@ -19279,13 +18583,13 @@ snapshots: '@commander-js/extra-typings': 11.1.0(commander@11.1.0) commander: 11.1.0 - kysely-postgres-js@3.0.0(kysely@0.28.14)(postgres@3.4.8): + kysely-postgres-js@3.0.0(kysely@0.28.17)(postgres@3.4.8): dependencies: - kysely: 0.28.14 + kysely: 0.28.17 optionalDependencies: postgres: 3.4.8 - kysely@0.28.14: {} + kysely@0.28.17: {} langium@3.3.1: dependencies: @@ -19295,23 +18599,14 @@ snapshots: vscode-languageserver-textdocument: 1.0.12 vscode-uri: 3.0.8 - langium@4.2.1: - dependencies: - chevrotain: 11.1.2 - chevrotain-allstar: 0.3.1(chevrotain@11.1.2) - vscode-languageserver: 9.0.1 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - - langsmith@0.5.18(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0): + langsmith@0.7.0(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0))(openai@6.2.0(ws@8.20.1)(zod@4.3.6))(ws@8.20.1): dependencies: p-queue: 6.6.2 - uuid: 10.0.0 optionalDependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) - openai: 6.2.0(ws@8.20.0)(zod@4.3.6) - ws: 8.20.0 + openai: 6.2.0(ws@8.20.1)(zod@4.3.6) + ws: 8.20.1 layout-base@1.0.2: {} @@ -19334,7 +18629,7 @@ snapshots: image-size: 0.5.5 make-dir: 2.1.0 mime: 1.6.0 - needle: 3.3.1 + needle: 3.5.0 source-map: 0.6.1 optional: true @@ -19467,11 +18762,6 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 - log-symbols@6.0.0: - dependencies: - chalk: 5.6.2 - is-unicode-supported: 1.3.0 - log-symbols@7.0.1: dependencies: is-unicode-supported: 2.1.0 @@ -19507,10 +18797,16 @@ snapshots: luxon@3.7.2: {} + lz-string@1.5.0: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + make-dir@2.1.0: dependencies: pify: 4.0.1 @@ -19529,7 +18825,7 @@ snapshots: mammoth@1.12.0: dependencies: - '@xmldom/xmldom': 0.8.12 + '@xmldom/xmldom': 0.8.13 argparse: 1.0.10 base64-js: 1.5.1 bluebird: 3.4.7 @@ -19562,6 +18858,8 @@ snapshots: math-intrinsics@1.1.0: {} + mdn-data@2.27.1: {} + mdurl@2.0.0: {} media-typer@1.1.0: {} @@ -19570,17 +18868,15 @@ snapshots: dependencies: fs-monkey: 1.0.5 - memoize-one@5.2.1: {} - merge-descriptors@2.0.0: {} merge-stream@2.0.0: {} - mermaid@11.13.0: + mermaid@11.15.0: dependencies: '@braintree/sanitize-url': 7.1.2 '@iconify/utils': 3.1.0 - '@mermaid-js/parser': 1.0.1 + '@mermaid-js/parser': 1.1.1 '@types/d3': 7.4.3 '@upsetjs/venn.js': 2.0.0 cytoscape: 3.33.1 @@ -19590,15 +18886,15 @@ snapshots: d3-sankey: 0.12.3 dagre-d3-es: 7.0.14 dayjs: 1.11.19 - dompurify: 3.3.3 + dompurify: 3.4.1 + es-toolkit: 1.46.1 katex: 0.16.40 khroma: 2.1.0 - lodash-es: 4.18.1 marked: 16.4.2 roughjs: 4.6.6 stylis: 4.3.6 ts-dedent: 2.2.0 - uuid: 11.1.0 + uuid: 14.0.0 methods@1.1.2: {} @@ -19630,9 +18926,11 @@ snapshots: mimic-function@5.0.1: {} + min-indent@1.0.1: {} + minimatch@10.2.4: dependencies: - brace-expansion: 5.0.5 + brace-expansion: 5.0.6 minimatch@3.1.5: dependencies: @@ -19673,11 +18971,11 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.2 optional: true - msgpackr@1.11.5: + msgpackr@1.11.9: optionalDependencies: msgpackr-extract: 3.0.2 - msgpackr@1.11.9: + msgpackr@2.0.1: optionalDependencies: msgpackr-extract: 3.0.2 @@ -19700,10 +18998,10 @@ snapshots: natural-compare@1.4.0: {} - needle@3.3.1: + needle@3.5.0: dependencies: iconv-lite: 0.6.3 - sax: 1.4.4 + sax: 1.6.0 optional: true negotiator@0.6.3: {} @@ -19712,23 +19010,23 @@ snapshots: neo-async@2.6.2: {} - nestjs-cls@6.2.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2): + nestjs-cls@6.2.0(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 - nestjs-kysely@3.1.2(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(kysely@0.28.14)(reflect-metadata@0.2.2): + nestjs-kysely@3.1.2(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.19)(kysely@0.28.17)(reflect-metadata@0.2.2): dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) - kysely: 0.28.14 + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.19(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@11.1.19)(reflect-metadata@0.2.2)(rxjs@7.8.2) + kysely: 0.28.17 reflect-metadata: 0.2.2 - nestjs-pino@4.6.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2): + nestjs-pino@4.6.1(@nestjs/common@11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(pino-http@11.0.0)(pino@10.1.0)(rxjs@7.8.2): dependencies: - '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.19(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) pino: 10.1.0 pino-http: 11.0.0 rxjs: 7.8.2 @@ -19752,9 +19050,6 @@ snapshots: node-int64@0.4.0: {} - node-readable-to-web-readable-stream@0.4.2: - optional: true - node-releases@2.0.27: {} nodemailer@8.0.5: {} @@ -19780,7 +19075,7 @@ snapshots: '@yarnpkg/lockfile': 1.1.0 '@yarnpkg/parsers': 3.0.2 '@zkochan/js-yaml': 0.0.7 - axios: 1.15.0 + axios: 1.16.0 cli-cursor: 3.1.0 cli-spinners: 2.6.1 cliui: 8.0.1 @@ -19825,13 +19120,11 @@ snapshots: transitivePeerDependencies: - debug - nypm@0.6.2: + nypm@0.6.6: dependencies: - citty: 0.1.6 - consola: 3.4.2 + citty: 0.2.2 pathe: 2.0.3 - pkg-types: 2.3.0 - tinyexec: 1.0.1 + tinyexec: 1.1.2 oauth4webapi@3.8.5: {} @@ -19847,13 +19140,6 @@ snapshots: object-keys@1.1.1: {} - object.assign@4.1.5: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - has-symbols: 1.1.0 - object-keys: 1.1.1 - object.assign@4.1.7: dependencies: call-bind: 1.0.8 @@ -19872,9 +19158,9 @@ snapshots: object.fromentries@2.0.8: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 es-object-atoms: 1.1.1 object.values@1.2.1: @@ -19884,6 +19170,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obug@2.1.1: {} + ollama@0.6.3: dependencies: whatwg-fetch: 3.6.20 @@ -19902,19 +19190,15 @@ snapshots: dependencies: mimic-fn: 2.1.0 - onetime@7.0.0: - dependencies: - mimic-function: 5.0.1 - open@8.4.2: dependencies: define-lazy-prop: 2.0.0 is-docker: 2.2.1 is-wsl: 2.2.0 - openai@6.2.0(ws@8.20.0)(zod@4.3.6): + openai@6.2.0(ws@8.20.1)(zod@4.3.6): optionalDependencies: - ws: 8.20.0 + ws: 8.20.1 zod: 4.3.6 optional: true @@ -19959,18 +19243,6 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 - ora@8.2.0: - dependencies: - chalk: 5.6.2 - cli-cursor: 5.0.0 - cli-spinners: 2.9.2 - is-interactive: 2.0.0 - is-unicode-supported: 2.1.0 - log-symbols: 6.0.0 - stdin-discarder: 0.2.2 - string-width: 7.2.0 - strip-ansi: 7.1.0 - orderedmap@2.1.1: {} otpauth@9.5.0: @@ -20085,7 +19357,7 @@ snapshots: path-exists@4.0.0: {} - path-expression-matcher@1.2.0: {} + path-expression-matcher@1.5.0: {} path-is-absolute@1.0.1: {} @@ -20106,11 +19378,6 @@ snapshots: pause@0.0.1: {} - pdfjs-dist@5.5.207: - optionalDependencies: - '@napi-rs/canvas': 0.1.97 - node-readable-to-web-readable-stream: 0.4.2 - peberminta@0.9.0: {} pend@1.2.0: {} @@ -20120,18 +19387,18 @@ snapshots: pg-cloudflare@1.3.0: optional: true - pg-connection-string@2.11.0: + pg-connection-string@2.12.0: optional: true pg-int8@1.0.1: optional: true - pg-pool@3.11.0(pg@8.16.3): + pg-pool@3.13.0(pg@8.16.3): dependencies: pg: 8.16.3 optional: true - pg-protocol@1.11.0: + pg-protocol@1.13.0: optional: true pg-tsquery@8.4.2: {} @@ -20147,9 +19414,9 @@ snapshots: pg@8.16.3: dependencies: - pg-connection-string: 2.11.0 - pg-pool: 3.11.0(pg@8.16.3) - pg-protocol: 1.11.0 + pg-connection-string: 2.12.0 + pg-pool: 3.13.0(pg@8.16.3) + pg-protocol: 1.13.0 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: @@ -20177,6 +19444,8 @@ snapshots: picomatch@4.0.4: {} + picospinner@3.0.0: {} + pify@4.0.1: optional: true @@ -20241,12 +19510,6 @@ snapshots: mlly: 1.8.0 pathe: 2.0.3 - pkg-types@2.3.0: - dependencies: - confbox: 0.2.2 - exsolve: 1.0.7 - pathe: 2.0.3 - pluralize@8.0.0: {} png-chunk-text@1.0.0: {} @@ -20275,40 +19538,40 @@ snapshots: possible-typed-array-names@1.0.0: {} - postcss-js@4.0.1(postcss@8.5.8): + postcss-js@4.0.1(postcss@8.5.14): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.8 + postcss: 8.5.14 - postcss-mixins@12.1.2(postcss@8.5.8): + postcss-mixins@12.1.2(postcss@8.5.14): dependencies: - postcss: 8.5.8 - postcss-js: 4.0.1(postcss@8.5.8) - postcss-simple-vars: 7.0.1(postcss@8.5.8) - sugarss: 5.0.1(postcss@8.5.8) + postcss: 8.5.14 + postcss-js: 4.0.1(postcss@8.5.14) + postcss-simple-vars: 7.0.1(postcss@8.5.14) + sugarss: 5.0.1(postcss@8.5.14) tinyglobby: 0.2.15 - postcss-nested@7.0.2(postcss@8.5.8): + postcss-nested@7.0.2(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 postcss-selector-parser: 7.1.1 - postcss-preset-mantine@1.18.0(postcss@8.5.8): + postcss-preset-mantine@1.18.0(postcss@8.5.14): dependencies: - postcss: 8.5.8 - postcss-mixins: 12.1.2(postcss@8.5.8) - postcss-nested: 7.0.2(postcss@8.5.8) + postcss: 8.5.14 + postcss-mixins: 12.1.2(postcss@8.5.14) + postcss-nested: 7.0.2(postcss@8.5.14) postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-simple-vars@7.0.1(postcss@8.5.8): + postcss-simple-vars@7.0.1(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 - postcss@8.5.8: + postcss@8.5.14: dependencies: nanoid: 3.3.8 picocolors: 1.1.1 @@ -20330,17 +19593,17 @@ snapshots: postgres@3.4.8: {} - posthog-js@1.363.1: + posthog-js@1.372.2: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/api-logs': 0.208.0 '@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.5.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0) - '@posthog/core': 1.24.1 - '@posthog/types': 1.363.1 + '@posthog/core': 1.27.6 + '@posthog/types': 1.372.2 core-js: 3.43.0 - dompurify: 3.3.3 + dompurify: 3.4.1 fflate: 0.4.8 preact: 10.28.3 query-selector-shadow-dom: 1.0.1 @@ -20348,7 +19611,7 @@ snapshots: postmark@4.0.7: dependencies: - axios: 1.15.0 + axios: 1.16.0 transitivePeerDependencies: - debug @@ -20358,6 +19621,12 @@ snapshots: prettier@3.8.1: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + pretty-format@30.2.0: dependencies: '@jest/schemas': 30.0.5 @@ -20492,18 +19761,18 @@ snapshots: prosemirror-state: 1.4.3 prosemirror-transform: 1.10.4 - protobufjs@7.5.4: + protobufjs@7.5.8: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.4 + '@protobufjs/codegen': 2.0.5 '@protobufjs/eventemitter': 1.1.0 '@protobufjs/fetch': 1.1.0 '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.0 + '@protobufjs/inquire': 1.1.1 '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.0 + '@protobufjs/utf8': 1.1.1 '@types/node': 25.5.0 long: 5.3.2 @@ -20517,6 +19786,10 @@ snapshots: prr@1.0.1: optional: true + psl@1.15.0: + dependencies: + punycode: 2.3.1 + pump@3.0.3: dependencies: end-of-stream: 1.4.4 @@ -20542,6 +19815,8 @@ snapshots: query-selector-shadow-dom@1.0.1: {} + querystringify@2.2.0: {} + quick-format-unescaped@4.0.4: {} radix-ui@1.4.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -20607,6 +19882,8 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + raf-schd@4.0.3: {} + range-parser@1.2.1: {} raw-body@3.0.2: @@ -20616,20 +19893,6 @@ snapshots: iconv-lite: 0.7.2 unpipe: 1.0.0 - react-arborist@3.4.0(patch_hash=419b3b02e24afe928cc006a006f6e906666aff19aa6fd7daaa788ccc2202678a)(@types/node@22.19.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - react: 18.3.1 - react-dnd: 14.0.5(@types/node@22.19.1)(@types/react@18.3.12)(react@18.3.1) - react-dnd-html5-backend: 14.1.0 - react-dom: 18.3.1(react@18.3.1) - react-window: 1.8.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - redux: 5.0.1 - use-sync-external-store: 1.2.2(react@18.3.1) - transitivePeerDependencies: - - '@types/hoist-non-react-statics' - - '@types/node' - - '@types/react' - react-clear-modal@2.0.18(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 @@ -20637,22 +19900,6 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 - react-dnd-html5-backend@14.1.0: - dependencies: - dnd-core: 14.0.1 - - react-dnd@14.0.5(@types/node@22.19.1)(@types/react@18.3.12)(react@18.3.1): - dependencies: - '@react-dnd/invariant': 2.0.0 - '@react-dnd/shallowequal': 2.0.0 - dnd-core: 14.0.1 - fast-deep-equal: 3.1.3 - hoist-non-react-statics: 3.3.2 - react: 18.3.1 - optionalDependencies: - '@types/node': 22.19.1 - '@types/react': 18.3.12 - react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -20663,24 +19910,31 @@ snapshots: dependencies: react: 18.3.1 - react-email@5.2.10: + react-email@6.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/parser': 7.27.0 '@babel/traverse': 7.27.0 + '@react-email/render': 2.0.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) chokidar: 4.0.3 commander: 13.1.0 conf: 15.1.0 + css-tree: 3.2.1 debounce: 2.0.0 - esbuild: 0.27.3 + esbuild: 0.28.0 glob: 13.0.6 jiti: 2.4.2 log-symbols: 7.0.1 + marked: 15.0.12 mime-types: 3.0.2 normalize-path: 3.0.0 - nypm: 0.6.2 - ora: 8.2.0 + nypm: 0.6.6 + picospinner: 3.0.0 + prismjs: 1.30.0 prompts: 2.4.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) socket.io: 4.8.3 + tailwindcss: 4.1.18 tsconfig-paths: 4.2.0 transitivePeerDependencies: - bufferutil @@ -20713,6 +19967,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-is@18.3.1: {} react-number-format@5.4.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): @@ -20779,13 +20035,6 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-window@1.8.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - '@babel/runtime': 7.26.10 - memoize-one: 5.2.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -20814,18 +20063,17 @@ snapshots: real-require@0.2.0: {} + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + redis-errors@1.2.0: {} redis-parser@3.0.0: dependencies: redis-errors: 1.2.0 - redux@4.2.1: - dependencies: - '@babel/runtime': 7.29.2 - - redux@5.0.1: {} - reflect-metadata@0.2.2: {} reflect.getprototypeof@1.0.10: @@ -20839,35 +20087,16 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 - reflect.getprototypeof@1.0.7: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.5 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - which-builtin-type: 1.2.0 - regenerate-unicode-properties@10.1.1: dependencies: regenerate: 1.4.2 regenerate@1.4.2: {} - regenerator-runtime@0.14.1: {} - regenerator-transform@0.15.2: dependencies: '@babel/runtime': 7.29.2 - regexp.prototype.flags@1.5.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-errors: 1.3.0 - set-function-name: 2.0.2 - regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -20896,6 +20125,8 @@ snapshots: require-main-filename@2.0.0: {} + requires-port@1.0.0: {} + resolve-cwd@3.0.0: dependencies: resolve-from: 5.0.0 @@ -20925,11 +20156,6 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 - restore-cursor@5.1.0: - dependencies: - onetime: 7.0.0 - signal-exit: 4.1.0 - ret@0.5.0: {} reusify@1.1.0: {} @@ -20987,6 +20213,8 @@ snapshots: transitivePeerDependencies: - supports-color + rrweb-cssom@0.7.1: {} + rrweb-cssom@0.8.0: {} rw@1.3.3: {} @@ -20999,13 +20227,6 @@ snapshots: dependencies: tslib: 2.8.1 - safe-array-concat@1.1.2: - dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -21023,12 +20244,6 @@ snapshots: es-errors: 1.3.0 isarray: 2.0.5 - safe-regex-test@1.0.3: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-regex: 1.2.0 - safe-regex-test@1.1.0: dependencies: call-bound: 1.0.4 @@ -21043,7 +20258,7 @@ snapshots: safer-buffer@2.1.2: {} - sanitize-filename-ts@1.0.2: + sanitize-filename@1.6.3: dependencies: truncate-utf8-bytes: 1.0.2 @@ -21055,7 +20270,7 @@ snapshots: sax@1.4.1: {} - sax@1.4.4: + sax@1.6.0: optional: true saxes@6.0.0: @@ -21079,6 +20294,8 @@ snapshots: ajv-formats: 2.1.1(ajv@8.18.0) ajv-keywords: 5.1.0(ajv@8.18.0) + scimmy@1.3.5(patch_hash=775d80f86830b2c5dd1a250c9802c10f8fc3da3c7898373de5aa0c23993d1673): {} + secure-json-parse@4.0.0: {} selderee@0.11.0: @@ -21092,6 +20309,8 @@ snapshots: semver@7.7.4: {} + semver@7.8.0: {} + send@1.2.1: dependencies: debug: 4.4.3 @@ -21185,6 +20404,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -21198,7 +20419,7 @@ snapshots: socket.io-adapter@2.5.4: dependencies: debug: 4.3.7 - ws: 8.20.0 + ws: 8.20.1 transitivePeerDependencies: - bufferutil - supports-color @@ -21269,13 +20490,13 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 - standard-as-callback@2.1.0: {} + stackback@0.0.2: {} - statuses@2.0.1: {} + standard-as-callback@2.1.0: {} statuses@2.0.2: {} - stdin-discarder@0.2.2: {} + std-env@4.1.0: {} stop-iteration-iterator@1.1.0: dependencies: @@ -21300,12 +20521,6 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - string-width@7.2.0: - dependencies: - emoji-regex: 10.6.0 - get-east-asian-width: 1.4.0 - strip-ansi: 7.1.0 - string.prototype.matchall@4.0.12: dependencies: call-bind: 1.0.8 @@ -21318,14 +20533,14 @@ snapshots: gopd: 1.2.0 has-symbols: 1.1.0 internal-slot: 1.1.0 - regexp.prototype.flags: 1.5.3 + regexp.prototype.flags: 1.5.4 set-function-name: 2.0.2 side-channel: 1.1.0 string.prototype.repeat@1.0.0: dependencies: define-properties: 1.2.1 - es-abstract: 1.23.5 + es-abstract: 1.24.1 string.prototype.trim@1.2.10: dependencies: @@ -21337,19 +20552,6 @@ snapshots: es-object-atoms: 1.1.1 has-property-descriptors: 1.0.2 - string.prototype.trim@1.2.9: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-abstract: 1.23.5 - es-object-atoms: 1.1.1 - - string.prototype.trimend@1.0.8: - dependencies: - call-bind: 1.0.7 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - string.prototype.trimend@1.0.9: dependencies: call-bind: 1.0.8 @@ -21359,7 +20561,7 @@ snapshots: string.prototype.trimstart@1.0.8: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 es-object-atoms: 1.1.1 @@ -21375,16 +20577,16 @@ snapshots: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.0: - dependencies: - ansi-regex: 6.0.1 - strip-bom@3.0.0: {} strip-bom@4.0.0: {} strip-final-newline@2.0.0: {} + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + strip-json-comments@3.1.1: {} strip-json-comments@5.0.3: {} @@ -21394,7 +20596,7 @@ snapshots: '@types/node': 25.5.0 qs: 6.14.2 - strnum@2.2.1: {} + strnum@2.2.3: {} strtok3@10.3.4: dependencies: @@ -21408,9 +20610,9 @@ snapshots: stylis@4.3.6: {} - sugarss@5.0.1(postcss@8.5.8): + sugarss@5.0.1(postcss@8.5.14): dependencies: - postcss: 8.5.8 + postcss: 8.5.14 superagent@10.3.0: dependencies: @@ -21468,13 +20670,13 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - terser-webpack-plugin@5.4.0(@swc/core@1.5.25(@swc/helpers@0.5.5))(webpack@5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5))): + terser-webpack-plugin@5.4.0(@swc/core@1.5.25(@swc/helpers@0.5.5))(webpack@5.106.0(@swc/core@1.5.25(@swc/helpers@0.5.5))): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.39.0 - webpack: 5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5)) + webpack: 5.106.0(@swc/core@1.5.25(@swc/helpers@0.5.5)) optionalDependencies: '@swc/core': 1.5.25(@swc/helpers@0.5.5) @@ -21495,16 +20697,20 @@ snapshots: dependencies: real-require: 0.2.0 + tiny-invariant@1.3.3: {} + + tinybench@2.9.0: {} + tinycolor2@1.6.0: {} - tinyexec@1.0.1: {} + tinyexec@1.1.2: {} tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - tiptap-extension-global-drag-handle@0.1.18: {} + tinyrainbow@3.1.0: {} tlds@1.261.0: {} @@ -21536,6 +20742,13 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + tough-cookie@5.1.2: dependencies: tldts: 6.1.72 @@ -21578,7 +20791,7 @@ snapshots: babel-jest: 30.3.0(@babel/core@7.28.5) jest-util: 30.3.0 - ts-loader@9.5.7(typescript@5.9.3)(webpack@5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5))): + ts-loader@9.5.7(typescript@5.9.3)(webpack@5.106.0(@swc/core@1.5.25(@swc/helpers@0.5.5))): dependencies: chalk: 4.1.2 enhanced-resolve: 5.20.1 @@ -21586,7 +20799,7 @@ snapshots: semver: 7.7.4 source-map: 0.7.4 typescript: 5.9.3 - webpack: 5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5)) + webpack: 5.106.0(@swc/core@1.5.25(@swc/helpers@0.5.5)) ts-node@10.9.2(@swc/core@1.5.25(@swc/helpers@0.5.5))(@types/node@25.5.0)(typescript@5.9.3): dependencies: @@ -21611,7 +20824,7 @@ snapshots: tsconfig-paths-webpack-plugin@4.2.0: dependencies: chalk: 4.1.2 - enhanced-resolve: 5.19.0 + enhanced-resolve: 5.20.1 tapable: 2.3.0 tsconfig-paths: 4.2.0 @@ -21662,49 +20875,25 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 - typed-array-buffer@1.0.2: - dependencies: - call-bind: 1.0.7 - es-errors: 1.3.0 - is-typed-array: 1.1.13 - typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 is-typed-array: 1.1.15 - typed-array-byte-length@1.0.1: - dependencies: - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.2.0 - has-proto: 1.0.3 - is-typed-array: 1.1.13 - typed-array-byte-length@1.0.3: dependencies: call-bind: 1.0.8 - for-each: 0.3.3 + for-each: 0.3.5 gopd: 1.2.0 has-proto: 1.2.0 is-typed-array: 1.1.15 - typed-array-byte-offset@1.0.3: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.2.0 - has-proto: 1.0.3 - is-typed-array: 1.1.13 - reflect.getprototypeof: 1.0.7 - typed-array-byte-offset@1.0.4: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.8 - for-each: 0.3.3 + for-each: 0.3.5 gopd: 1.2.0 has-proto: 1.2.0 is-typed-array: 1.1.15 @@ -21712,12 +20901,23 @@ snapshots: typed-array-length@1.0.7: dependencies: - call-bind: 1.0.7 - for-each: 0.3.3 + call-bind: 1.0.8 + for-each: 0.3.5 gopd: 1.2.0 - is-typed-array: 1.1.13 + is-typed-array: 1.1.15 possible-typed-array-names: 1.0.0 - reflect.getprototypeof: 1.0.7 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + '@typescript-eslint/parser': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.57.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.9.3) + eslint: 9.28.0(jiti@2.4.2) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color typescript-eslint@8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3): dependencies: @@ -21735,7 +20935,7 @@ snapshots: typesense@3.0.5(@babel/runtime@7.29.2): dependencies: '@babel/runtime': 7.29.2 - axios: 1.15.0 + axios: 1.16.0 loglevel: 1.9.2 tslib: 2.8.1 transitivePeerDependencies: @@ -21758,13 +20958,6 @@ snapshots: uint8array-extras@1.5.0: {} - unbox-primitive@1.0.2: - dependencies: - call-bind: 1.0.7 - has-bigints: 1.0.2 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.0 - unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -21791,6 +20984,8 @@ snapshots: unicode-property-aliases-ecmascript@2.1.0: {} + universalify@0.2.0: {} + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -21829,6 +21024,11 @@ snapshots: dependencies: punycode: 2.3.1 + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + use-callback-ref@1.3.3(@types/react@18.3.12)(react@18.3.1): dependencies: react: 18.3.1 @@ -21861,10 +21061,6 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 - use-sync-external-store@1.2.2(react@18.3.1): - dependencies: - react: 18.3.1 - use-sync-external-store@1.6.0(react@18.3.1): dependencies: react: 18.3.1 @@ -21877,9 +21073,7 @@ snapshots: uuid@10.0.0: {} - uuid@11.1.0: {} - - uuid@13.0.0: {} + uuid@14.0.0: {} v8-compile-cache-lib@3.0.1: {} @@ -21893,24 +21087,54 @@ snapshots: vary@1.1.2: {} - vite@8.0.5(@types/node@22.19.1)(esbuild@0.27.4)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.8))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3): + vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.8 + postcss: 8.5.14 rolldown: 1.0.0-rc.12 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 22.19.1 - esbuild: 0.27.4 + esbuild: 0.28.0 fsevents: 2.3.3 jiti: 2.4.2 less: 4.2.0 - sugarss: 5.0.1(postcss@8.5.8) + sugarss: 5.0.1(postcss@8.5.14) terser: 5.39.0 tsx: 4.21.0 yaml: 2.8.3 + vitest@4.1.6(@opentelemetry/api@1.9.0)(@types/node@22.19.1)(happy-dom@20.8.9)(jsdom@25.0.0)(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.6 + '@vitest/mocker': 4.1.6(vite@8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.6 + '@vitest/runner': 4.1.6 + '@vitest/snapshot': 4.1.6 + '@vitest/spy': 4.1.6 + '@vitest/utils': 4.1.6 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.1.0 + vite: 8.0.5(@types/node@22.19.1)(esbuild@0.28.0)(jiti@2.4.2)(less@4.2.0)(sugarss@5.0.1(postcss@8.5.14))(terser@5.39.0)(tsx@4.21.0)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/node': 22.19.1 + happy-dom: 20.8.9 + jsdom: 25.0.0 + transitivePeerDependencies: + - msw + void-elements@3.1.0: {} vscode-jsonrpc@8.2.0: {} @@ -21930,8 +21154,6 @@ snapshots: vscode-uri@3.0.8: {} - vscode-uri@3.1.0: {} - w3c-keyname@2.2.8: {} w3c-xmlserializer@5.0.0: @@ -21961,10 +21183,10 @@ snapshots: webpack-sources@3.3.4: {} - webpack@5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5)): + webpack@5.106.0(@swc/core@1.5.25(@swc/helpers@0.5.5)): dependencies: '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 @@ -21974,7 +21196,7 @@ snapshots: browserslist: 4.28.1 chrome-trace-event: 1.0.3 enhanced-resolve: 5.20.1 - es-module-lexer: 2.0.0 + es-module-lexer: 2.1.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -21985,7 +21207,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.0 - terser-webpack-plugin: 5.4.0(@swc/core@1.5.25(@swc/helpers@0.5.5))(webpack@5.105.4(@swc/core@1.5.25(@swc/helpers@0.5.5))) + terser-webpack-plugin: 5.4.0(@swc/core@1.5.25(@swc/helpers@0.5.5))(webpack@5.106.0(@swc/core@1.5.25(@swc/helpers@0.5.5))) watchpack: 2.5.1 webpack-sources: 3.3.4 transitivePeerDependencies: @@ -22017,14 +21239,6 @@ snapshots: when-exit@2.1.5: {} - which-boxed-primitive@1.1.0: - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.0 - is-number-object: 1.1.0 - is-string: 1.1.0 - is-symbol: 1.1.0 - which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -22033,22 +21247,6 @@ snapshots: is-string: 1.1.1 is-symbol: 1.1.1 - which-builtin-type@1.2.0: - dependencies: - call-bind: 1.0.7 - function.prototype.name: 1.1.6 - has-tostringtag: 1.0.2 - is-async-function: 2.0.0 - is-date-object: 1.0.5 - is-finalizationregistry: 1.1.0 - is-generator-function: 1.0.10 - is-regex: 1.2.0 - is-weakref: 1.0.2 - isarray: 2.0.5 - which-boxed-primitive: 1.1.0 - which-collection: 1.0.2 - which-typed-array: 1.1.16 - which-builtin-type@1.2.1: dependencies: call-bound: 1.0.4 @@ -22061,7 +21259,7 @@ snapshots: is-regex: 1.2.1 is-weakref: 1.1.1 isarray: 2.0.5 - which-boxed-primitive: 1.1.0 + which-boxed-primitive: 1.1.1 which-collection: 1.0.2 which-typed-array: 1.1.20 @@ -22074,14 +21272,6 @@ snapshots: which-module@2.0.1: {} - which-typed-array@1.1.16: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.7 - for-each: 0.3.3 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - which-typed-array@1.1.20: dependencies: available-typed-arrays: 1.0.7 @@ -22096,6 +21286,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + widest-line@3.1.0: dependencies: string-width: 4.2.3 @@ -22121,22 +21316,24 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 - ws@8.20.0: {} + ws@8.20.1: {} xml-crypto@6.1.2: dependencies: '@xmldom/is-dom-node': 1.0.1 - '@xmldom/xmldom': 0.8.12 + '@xmldom/xmldom': 0.8.13 xpath: 0.0.33 xml-encryption@3.1.0: dependencies: - '@xmldom/xmldom': 0.8.12 + '@xmldom/xmldom': 0.8.13 escape-html: 1.0.3 xpath: 0.0.32 xml-name-validator@5.0.0: {} + xml-naming@0.1.0: {} + xml2js@0.6.2: dependencies: sax: 1.4.1
    /. Returns null when absent, + * non-numeric, or a non-px unit (em, %). + */ +function parsePixelWidth(el: Cheerio): number | null { + const attr = el.attr('width'); + if (attr) { + const n = parseInt(attr, 10); + if (Number.isFinite(n) && n > 0) return n; + } + const style = el.attr('style') || ''; + const m = style.match(/(?:^|;)\s*width\s*:\s*([\d.]+)\s*px/i); + if (m) { + const n = parseInt(m[1], 10); + if (Number.isFinite(n) && n > 0) return n; + } + return null; +} + +/** + * Derives per-column widths for a table, in visual column order. + * Priority:
    / to build the runtime