From 0b159c4963932215adf5fcacf874841db8fe9d40 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Wed, 2 Jul 2025 15:25:52 +0530 Subject: [PATCH] [WIKI-400] feat: page navigation pane (#7206) * init: page navigation pane * chore: outline and info tabs * chore: asset download endpoint * chore: realtime document info updates * chore: add support for code splitting * fix: formatting * refactor: image block id generation * chore: implement translation * refactor: assets list storage logic * fix: build errors * fix: image extension name * refactor: add support for additional asset items * refactor: asset extraction logic * chore: add translations * fix: merge conflicts resolved from preview * chore: remove version history option from the dropdown * chore: query params handling * chore: remove unnecessary logic * refactor: empty state components * fix: empty state asset path --- admin/styles/globals.css | 1 + packages/editor/src/ce/constants/assets.ts | 6 + .../editor/src/ce/constants/extensions.ts | 1 + packages/editor/src/ce/types/asset.ts | 1 + packages/editor/src/ce/types/storage.ts | 2 + .../editors/document/collaborative-editor.tsx | 6 +- .../custom-image/components/block.tsx | 3 +- .../custom-image/components/uploader.tsx | 2 +- .../src/core/extensions/custom-image/utils.ts | 2 + .../editor/src/core/extensions/utility.ts | 29 ++- packages/editor/src/core/helpers/assets.ts | 37 +++ .../editor/src/core/helpers/editor-ref.ts | 55 +++++ .../core/hooks/use-collaborative-editor.ts | 2 + packages/editor/src/core/hooks/use-editor.ts | 215 +++++++++--------- .../src/core/hooks/use-read-only-editor.ts | 41 +--- .../editor/src/core/plugins/drag-handle.ts | 2 +- .../editor/src/core/plugins/file/delete.ts | 4 + .../editor/src/core/plugins/file/restore.ts | 9 + packages/editor/src/core/types/asset.ts | 14 ++ packages/editor/src/core/types/editor.ts | 47 ++-- packages/editor/src/core/types/hook.ts | 2 + packages/editor/src/core/types/index.ts | 1 + packages/editor/src/index.ts | 3 + .../i18n/src/locales/cs/translations.json | 42 +++- .../i18n/src/locales/de/translations.json | 40 ++++ .../i18n/src/locales/en/translations.json | 42 +++- .../i18n/src/locales/es/translations.json | 42 +++- .../i18n/src/locales/fr/translations.json | 42 +++- .../i18n/src/locales/id/translations.json | 44 +++- .../i18n/src/locales/it/translations.json | 42 +++- .../i18n/src/locales/ja/translations.json | 42 +++- .../i18n/src/locales/ko/translations.json | 42 +++- .../i18n/src/locales/pl/translations.json | 42 +++- .../i18n/src/locales/pt-BR/translations.json | 42 +++- .../i18n/src/locales/ro/translations.json | 42 +++- .../i18n/src/locales/ru/translations.json | 44 +++- .../i18n/src/locales/sk/translations.json | 42 +++- .../i18n/src/locales/tr-TR/translations.json | 42 +++- .../i18n/src/locales/ua/translations.json | 42 +++- .../i18n/src/locales/vi-VN/translations.json | 42 +++- .../i18n/src/locales/zh-CN/translations.json | 42 +++- .../i18n/src/locales/zh-TW/translations.json | 42 +++- packages/utils/src/editor.ts | 15 ++ space/styles/globals.css | 29 ++- .../components/pages/navigation-pane/index.ts | 31 +++ .../navigation-pane/tab-panels/assets.tsx | 11 + .../tab-panels/empty-states/assets.tsx | 26 +++ .../tab-panels/empty-states/outline.tsx | 26 +++ .../pages/navigation-pane/tab-panels/root.tsx | 13 ++ .../components/pages/editor/editor-body.tsx | 43 +++- .../components/pages/editor/page-root.tsx | 123 ++++++---- .../pages/editor/summary/content-browser.tsx | 25 +- .../editor/summary/heading-components.tsx | 24 +- .../components/pages/editor/toolbar/index.ts | 1 - .../pages/editor/toolbar/info-popover.tsx | 139 ----------- .../pages/editor/toolbar/options-dropdown.tsx | 43 +--- .../components/pages/editor/toolbar/root.tsx | 78 +++++-- web/core/components/pages/header/actions.tsx | 3 +- .../components/pages/navigation-pane/index.ts | 11 + .../components/pages/navigation-pane/root.tsx | 88 +++++++ .../navigation-pane/tab-panels/assets.tsx | 109 +++++++++ .../tab-panels/info/actors-info.tsx | 68 ++++++ .../tab-panels/info/document-info.tsx | 82 +++++++ .../navigation-pane/tab-panels/info/root.tsx | 27 +++ .../tab-panels/info/version-history.tsx | 142 ++++++++++++ .../navigation-pane/tab-panels/outline.tsx | 28 +++ .../pages/navigation-pane/tab-panels/root.tsx | 39 ++++ .../pages/navigation-pane/tabs-list.tsx | 37 +++ web/core/components/pages/version/editor.tsx | 8 +- web/core/components/pages/version/index.ts | 3 - .../components/pages/version/main-content.tsx | 44 ++-- web/core/components/pages/version/root.tsx | 61 +++-- .../pages/version/sidebar-list-item.tsx | 49 ---- .../components/pages/version/sidebar-list.tsx | 99 -------- .../components/pages/version/sidebar-root.tsx | 38 ---- web/core/hooks/use-query-params.ts | 39 ++-- web/core/store/pages/base-page.ts | 23 +- web/core/store/pages/page-editor-info.ts | 41 ++++ .../pages/navigation-pane/assets-dark.webp | Bin 0 -> 21086 bytes .../pages/navigation-pane/assets-light.webp | Bin 0 -> 21472 bytes .../pages/navigation-pane/outline-dark.webp | Bin 0 -> 18452 bytes .../pages/navigation-pane/outline-light.webp | Bin 0 -> 18782 bytes web/styles/globals.css | 1 + 83 files changed, 2185 insertions(+), 767 deletions(-) create mode 100644 packages/editor/src/ce/constants/assets.ts create mode 100644 packages/editor/src/ce/constants/extensions.ts create mode 100644 packages/editor/src/ce/types/asset.ts create mode 100644 packages/editor/src/core/helpers/assets.ts create mode 100644 packages/editor/src/core/helpers/editor-ref.ts create mode 100644 packages/editor/src/core/types/asset.ts create mode 100644 web/ce/components/pages/navigation-pane/index.ts create mode 100644 web/ce/components/pages/navigation-pane/tab-panels/assets.tsx create mode 100644 web/ce/components/pages/navigation-pane/tab-panels/empty-states/assets.tsx create mode 100644 web/ce/components/pages/navigation-pane/tab-panels/empty-states/outline.tsx create mode 100644 web/ce/components/pages/navigation-pane/tab-panels/root.tsx delete mode 100644 web/core/components/pages/editor/toolbar/info-popover.tsx create mode 100644 web/core/components/pages/navigation-pane/index.ts create mode 100644 web/core/components/pages/navigation-pane/root.tsx create mode 100644 web/core/components/pages/navigation-pane/tab-panels/assets.tsx create mode 100644 web/core/components/pages/navigation-pane/tab-panels/info/actors-info.tsx create mode 100644 web/core/components/pages/navigation-pane/tab-panels/info/document-info.tsx create mode 100644 web/core/components/pages/navigation-pane/tab-panels/info/root.tsx create mode 100644 web/core/components/pages/navigation-pane/tab-panels/info/version-history.tsx create mode 100644 web/core/components/pages/navigation-pane/tab-panels/outline.tsx create mode 100644 web/core/components/pages/navigation-pane/tab-panels/root.tsx create mode 100644 web/core/components/pages/navigation-pane/tabs-list.tsx delete mode 100644 web/core/components/pages/version/sidebar-list-item.tsx delete mode 100644 web/core/components/pages/version/sidebar-list.tsx delete mode 100644 web/core/components/pages/version/sidebar-root.tsx create mode 100644 web/core/store/pages/page-editor-info.ts create mode 100644 web/public/empty-state/pages/navigation-pane/assets-dark.webp create mode 100644 web/public/empty-state/pages/navigation-pane/assets-light.webp create mode 100644 web/public/empty-state/pages/navigation-pane/outline-dark.webp create mode 100644 web/public/empty-state/pages/navigation-pane/outline-light.webp diff --git a/admin/styles/globals.css b/admin/styles/globals.css index d5554ce2f..bdd91161b 100644 --- a/admin/styles/globals.css +++ b/admin/styles/globals.css @@ -332,6 +332,7 @@ text-rendering: optimizeLegibility; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; + scroll-behavior: smooth; } body { diff --git a/packages/editor/src/ce/constants/assets.ts b/packages/editor/src/ce/constants/assets.ts new file mode 100644 index 000000000..12b89b3ab --- /dev/null +++ b/packages/editor/src/ce/constants/assets.ts @@ -0,0 +1,6 @@ +// helpers +import { TAssetMetaDataRecord } from "@/helpers/assets"; +// local imports +import { ADDITIONAL_EXTENSIONS } from "./extensions"; + +export const ADDITIONAL_ASSETS_META_DATA_RECORD: Partial> = {}; diff --git a/packages/editor/src/ce/constants/extensions.ts b/packages/editor/src/ce/constants/extensions.ts new file mode 100644 index 000000000..8787ec0c1 --- /dev/null +++ b/packages/editor/src/ce/constants/extensions.ts @@ -0,0 +1 @@ +export enum ADDITIONAL_EXTENSIONS {} diff --git a/packages/editor/src/ce/types/asset.ts b/packages/editor/src/ce/types/asset.ts new file mode 100644 index 000000000..4410c0f2c --- /dev/null +++ b/packages/editor/src/ce/types/asset.ts @@ -0,0 +1 @@ +export type TAdditionalEditorAsset = never; diff --git a/packages/editor/src/ce/types/storage.ts b/packages/editor/src/ce/types/storage.ts index 84eee65f9..da90d529e 100644 --- a/packages/editor/src/ce/types/storage.ts +++ b/packages/editor/src/ce/types/storage.ts @@ -1,3 +1,4 @@ +import { CharacterCountStorage } from "@tiptap/extension-character-count"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; // extensions @@ -15,6 +16,7 @@ export type ExtensionStorageMap = { [CORE_EXTENSIONS.HEADINGS_LIST]: HeadingExtensionStorage; [CORE_EXTENSIONS.MENTION]: MentionExtensionStorage; [CORE_EXTENSIONS.UTILITY]: UtilityExtensionStorage; + [CORE_EXTENSIONS.CHARACTER_COUNT]: CharacterCountStorage; }; export type ExtensionFileSetStorageKey = Extract; diff --git a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx index 8bbf2e7ce..e20ac3f5f 100644 --- a/packages/editor/src/core/components/editors/document/collaborative-editor.tsx +++ b/packages/editor/src/core/components/editors/document/collaborative-editor.tsx @@ -17,8 +17,6 @@ import { EditorRefApi, ICollaborativeDocumentEditorProps } from "@/types"; const CollaborativeDocumentEditor: React.FC = (props) => { const { - onChange, - onTransaction, aiHandler, bubbleMenuEnabled = true, containerClassName, @@ -33,6 +31,9 @@ const CollaborativeDocumentEditor: React.FC = handleEditorReady, id, mentionHandler, + onAssetChange, + onChange, + onTransaction, placeholder, realtimeConfig, serverHandler, @@ -63,6 +64,7 @@ const CollaborativeDocumentEditor: React.FC = handleEditorReady, id, mentionHandler, + onAssetChange, onChange, onTransaction, placeholder, diff --git a/packages/editor/src/core/extensions/custom-image/components/block.tsx b/packages/editor/src/core/extensions/custom-image/components/block.tsx index 1ff36abca..c895d19cc 100644 --- a/packages/editor/src/core/extensions/custom-image/components/block.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/block.tsx @@ -4,7 +4,7 @@ import React, { useRef, useState, useCallback, useLayoutEffect, useEffect } from import { cn } from "@plane/utils"; // local imports import { Pixel, TCustomImageAttributes, TCustomImageSize } from "../types"; -import { ensurePixelString } from "../utils"; +import { ensurePixelString, getImageBlockId } from "../utils"; import type { CustomImageNodeViewProps } from "./node-view"; import { ImageToolbarRoot } from "./toolbar"; import { ImageUploadStatus } from "./upload-status"; @@ -196,6 +196,7 @@ export const CustomImageBlock: React.FC = (props) => { return (
{ } return "Add an image"; - }, [draggedInside, failedToLoadImage, isImageBeingUploaded, editor.isEditable]); + }, [draggedInside, editor.isEditable, failedToLoadImage, isImageBeingUploaded]); return (
( return value; }; + +export const getImageBlockId = (id: string) => `editor-image-block-${id}`; diff --git a/packages/editor/src/core/extensions/utility.ts b/packages/editor/src/core/extensions/utility.ts index 758c74241..571a66da0 100644 --- a/packages/editor/src/core/extensions/utility.ts +++ b/packages/editor/src/core/extensions/utility.ts @@ -1,5 +1,4 @@ import { Extension } from "@tiptap/core"; -// prosemirror plugins import codemark from "prosemirror-codemark"; // helpers import { restorePublicImages } from "@/helpers/image-helpers"; @@ -8,17 +7,27 @@ import { DropHandlerPlugin } from "@/plugins/drop"; import { FilePlugins } from "@/plugins/file/root"; import { MarkdownClipboardPlugin } from "@/plugins/markdown-clipboard"; // types -import type { IEditorProps, TFileHandler, TReadOnlyFileHandler } from "@/types"; +import type { IEditorProps, TEditorAsset, TFileHandler, TReadOnlyFileHandler } from "@/types"; declare module "@tiptap/core" { interface Commands { utility: { updateAssetsUploadStatus: (updatedStatus: TFileHandler["assetsUploadStatus"]) => () => void; + updateAssetsList: ( + args: + | { + asset: TEditorAsset; + } + | { + idToRemove: string; + } + ) => () => void; }; } } export interface UtilityExtensionStorage { + assetsList: TEditorAsset[]; assetsUploadStatus: TFileHandler["assetsUploadStatus"]; uploadInProgress: boolean; } @@ -58,6 +67,7 @@ export const UtilityExtension = (props: Props) => { addStorage() { return { + assetsList: [], assetsUploadStatus: isEditable && "assetsUploadStatus" in fileHandler ? fileHandler.assetsUploadStatus : {}, uploadInProgress: false, }; @@ -68,6 +78,21 @@ export const UtilityExtension = (props: Props) => { updateAssetsUploadStatus: (updatedStatus) => () => { this.storage.assetsUploadStatus = updatedStatus; }, + updateAssetsList: (args) => () => { + const uniqueAssets = new Set(this.storage.assetsList); + if ("asset" in args) { + const alreadyExists = this.storage.assetsList.find((asset) => asset.id === args.asset.id); + if (!alreadyExists) { + uniqueAssets.add(args.asset); + } + } else if ("idToRemove" in args) { + const asset = this.storage.assetsList.find((asset) => asset.id === args.idToRemove); + if (asset) { + uniqueAssets.delete(asset); + } + } + this.storage.assetsList = Array.from(uniqueAssets); + }, }; }, }); diff --git a/packages/editor/src/core/helpers/assets.ts b/packages/editor/src/core/helpers/assets.ts new file mode 100644 index 000000000..74179f6c4 --- /dev/null +++ b/packages/editor/src/core/helpers/assets.ts @@ -0,0 +1,37 @@ +import { Node as ProseMirrorNode } from "@tiptap/pm/model"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// extensions +import { getImageBlockId } from "@/extensions/custom-image/utils"; +// plane editor imports +import { ADDITIONAL_ASSETS_META_DATA_RECORD } from "@/plane-editor/constants/assets"; +// types +import { TEditorAsset } from "@/types"; + +export type TAssetMetaDataRecord = (attrs: ProseMirrorNode["attrs"]) => TEditorAsset | undefined; + +export const CORE_ASSETS_META_DATA_RECORD: Partial> = { + [CORE_EXTENSIONS.IMAGE]: (attrs) => { + if (!attrs?.src) return; + return { + href: `#${getImageBlockId(attrs?.id ?? "")}`, + id: attrs?.id, + name: `image-${attrs?.id}`, + size: 0, + src: attrs?.src, + type: CORE_EXTENSIONS.IMAGE, + }; + }, + [CORE_EXTENSIONS.CUSTOM_IMAGE]: (attrs) => { + if (!attrs?.src) return; + return { + href: `#${getImageBlockId(attrs?.id ?? "")}`, + id: attrs?.id, + name: `image-${attrs?.id}`, + size: 0, + src: attrs?.src, + type: CORE_EXTENSIONS.CUSTOM_IMAGE, + }; + }, + ...ADDITIONAL_ASSETS_META_DATA_RECORD, +}; diff --git a/packages/editor/src/core/helpers/editor-ref.ts b/packages/editor/src/core/helpers/editor-ref.ts new file mode 100644 index 000000000..1b9843df9 --- /dev/null +++ b/packages/editor/src/core/helpers/editor-ref.ts @@ -0,0 +1,55 @@ +import { HocuspocusProvider } from "@hocuspocus/provider"; +import { Editor } from "@tiptap/core"; +import * as Y from "yjs"; +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +import { CORE_EDITOR_META } from "@/constants/meta"; +// types +import { EditorReadOnlyRefApi } from "@/types"; +// local imports +import { getParagraphCount } from "./common"; +import { getExtensionStorage } from "./get-extension-storage"; +import { scrollSummary } from "./scroll-to-node"; + +type TArgs = { + editor: Editor | null; + provider: HocuspocusProvider | undefined; +}; + +export const getEditorRefHelpers = (args: TArgs): EditorReadOnlyRefApi => { + const { editor, provider } = args; + + return { + clearEditor: (emitUpdate = false) => { + editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run(); + }, + getDocument: () => { + const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null; + const documentHTML = editor?.getHTML() ?? "

"; + const documentJSON = editor?.getJSON() ?? null; + + return { + binary: documentBinary, + html: documentHTML, + json: documentJSON, + }; + }, + getDocumentInfo: () => ({ + characters: editor ? getExtensionStorage(editor, CORE_EXTENSIONS.CHARACTER_COUNT)?.characters?.() : 0, + paragraphs: getParagraphCount(editor?.state), + words: editor ? getExtensionStorage(editor, CORE_EXTENSIONS.CHARACTER_COUNT)?.words?.() : 0, + }), + getHeadings: () => (editor ? getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings : []), + getMarkDown: () => { + const markdownOutput = editor?.storage?.markdown?.getMarkdown?.(); + return markdownOutput; + }, + scrollSummary: (marking) => { + if (!editor) return; + scrollSummary(editor, marking); + }, + setEditorValue: (content, emitUpdate = false) => { + editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: true }); + }, + }; +}; diff --git a/packages/editor/src/core/hooks/use-collaborative-editor.ts b/packages/editor/src/core/hooks/use-collaborative-editor.ts index 9c436dff2..3b4b333e6 100644 --- a/packages/editor/src/core/hooks/use-collaborative-editor.ts +++ b/packages/editor/src/core/hooks/use-collaborative-editor.ts @@ -13,6 +13,7 @@ import { TCollaborativeEditorHookProps } from "@/types"; export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) => { const { + onAssetChange, onChange, onTransaction, disabledExtensions, @@ -106,6 +107,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) => forwardedRef, handleEditorReady, mentionHandler, + onAssetChange, onChange, onTransaction, placeholder, diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 4c1b93d84..1979d46b1 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -1,23 +1,23 @@ import { DOMSerializer } from "@tiptap/pm/model"; -import { useEditor as useTiptapEditor } from "@tiptap/react"; +import { useEditorState, useEditor as useTiptapEditor } from "@tiptap/react"; import { useImperativeHandle, useEffect } from "react"; import * as Y from "yjs"; // components import { getEditorMenuItems } from "@/components/menus"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; -import { CORE_EDITOR_META } from "@/constants/meta"; // extensions import { CoreEditorExtensions } from "@/extensions"; // helpers import { getParagraphCount } from "@/helpers/common"; +import { getEditorRefHelpers } from "@/helpers/editor-ref"; import { getExtensionStorage } from "@/helpers/get-extension-storage"; import { insertContentAtSavedSelection } from "@/helpers/insert-content-at-cursor-position"; -import { IMarking, scrollSummary, scrollToNodeViaDOMCoordinates } from "@/helpers/scroll-to-node"; +import { scrollToNodeViaDOMCoordinates } from "@/helpers/scroll-to-node"; // props import { CoreEditorProps } from "@/props"; // types -import type { TDocumentEventsServer, TEditorCommands, TEditorHookProps } from "@/types"; +import type { TEditorCommands, TEditorHookProps } from "@/types"; export const useEditor = (props: TEditorHookProps) => { const { @@ -35,6 +35,7 @@ export const useEditor = (props: TEditorHookProps) => { id = "", initialValue, mentionHandler, + onAssetChange, onChange, onTransaction, placeholder, @@ -109,27 +110,26 @@ export const useEditor = (props: TEditorHookProps) => { editor.commands.updateAssetsUploadStatus?.(assetsUploadStatus); }, [editor, fileHandler.assetsUploadStatus]); + // subscribe to assets list changes + const assetsList = useEditorState({ + editor, + selector: ({ editor }) => ({ + assets: editor ? getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.assetsList : [], + }), + }); + // trigger callback when assets list changes + useEffect(() => { + const assets = assetsList?.assets; + if (!assets || !onAssetChange) return; + onAssetChange(assets); + }, [assetsList?.assets, onAssetChange]); + useImperativeHandle( forwardedRef, () => ({ + ...getEditorRefHelpers({ editor, provider }), blur: () => editor?.commands.blur(), - scrollToNodeViaDOMCoordinates(behavior?: ScrollBehavior, pos?: number) { - const resolvedPos = pos ?? editor?.state.selection.from; - if (!editor || !resolvedPos) return; - scrollToNodeViaDOMCoordinates(editor, resolvedPos, behavior); - }, - getCurrentCursorPosition: () => editor?.state.selection.from, - clearEditor: (emitUpdate = false) => { - editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run(); - }, - setEditorValue: (content: string, emitUpdate = false) => { - editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: true }); - }, - setEditorValueAtCursorPosition: (content: string) => { - if (editor?.state.selection) { - insertContentAtSavedSelection(editor, content); - } - }, + emitRealTimeUpdate: (message) => provider?.sendStateless(message), executeMenuItemCommand: (props) => { const { itemKey } = props; const editorItems = getEditorMenuItems(editor); @@ -143,83 +143,7 @@ export const useEditor = (props: TEditorHookProps) => { console.warn(`No command found for item: ${itemKey}`); } }, - isMenuItemActive: (props) => { - const { itemKey } = props; - const editorItems = getEditorMenuItems(editor); - - const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey); - const item = getEditorMenuItem(itemKey); - if (!item) return false; - - return item.isActive(props); - }, - onHeadingChange: (callback: (headings: IMarking[]) => void) => { - // Subscribe to update event emitted from headers extension - editor?.on("update", () => { - const headings = getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings; - if (headings) { - callback(headings); - } - }); - // Return a function to unsubscribe to the continuous transactions of - // the editor on unmounting the component that has subscribed to this - // method - return () => { - editor?.off("update"); - }; - }, - getHeadings: () => (editor ? getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings : []), - onStateChange: (callback: () => void) => { - // Subscribe to editor state changes - editor?.on("transaction", () => { - callback(); - }); - - // Return a function to unsubscribe to the continuous transactions of - // the editor on unmounting the component that has subscribed to this - // method - return () => { - editor?.off("transaction"); - }; - }, - getMarkDown: (): string => { - const markdownOutput = editor?.storage.markdown.getMarkdown(); - return markdownOutput; - }, - getDocument: () => { - const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null; - const documentHTML = editor?.getHTML() ?? "

"; - const documentJSON = editor?.getJSON() ?? null; - - return { - binary: documentBinary, - html: documentHTML, - json: documentJSON, - }; - }, - scrollSummary: (marking: IMarking): void => { - if (!editor) return; - scrollSummary(editor, marking); - }, - isEditorReadyToDiscard: () => - !!editor && getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress === false, - setFocusAtPosition: (position: number) => { - if (!editor || editor.isDestroyed) { - console.error("Editor reference is not available or has been destroyed."); - return; - } - try { - const docSize = editor.state.doc.content.size; - const safePosition = Math.max(0, Math.min(position, docSize)); - editor - .chain() - .insertContentAt(safePosition, [{ type: CORE_EXTENSIONS.PARAGRAPH }]) - .focus() - .run(); - } catch (error) { - console.error("An error occurred while setting focus at position:", error); - } - }, + getCurrentCursorPosition: () => editor?.state.selection.from, getSelectedText: () => { if (!editor) return null; @@ -253,18 +177,99 @@ export const useEditor = (props: TEditorHookProps) => { editor.chain().focus().deleteRange({ from, to }).insertContent(contentHTML).run(); } }, - getDocumentInfo: () => ({ - characters: editor?.storage?.characterCount?.characters?.() ?? 0, - paragraphs: getParagraphCount(editor?.state), - words: editor?.storage?.characterCount?.words?.() ?? 0, - }), + isEditorReadyToDiscard: () => + !!editor && getExtensionStorage(editor, CORE_EXTENSIONS.UTILITY)?.uploadInProgress === false, + isMenuItemActive: (props) => { + const { itemKey } = props; + const editorItems = getEditorMenuItems(editor); + + const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey); + const item = getEditorMenuItem(itemKey); + if (!item) return false; + + return item.isActive(props); + }, + listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) }, + onDocumentInfoChange: (callback) => { + const handleDocumentInfoChange = () => { + if (!editor) return; + callback({ + characters: editor ? getExtensionStorage(editor, CORE_EXTENSIONS.CHARACTER_COUNT)?.characters?.() : 0, + paragraphs: getParagraphCount(editor?.state), + words: editor ? getExtensionStorage(editor, CORE_EXTENSIONS.CHARACTER_COUNT)?.words?.() : 0, + }); + }; + + // Subscribe to update event emitted from character count extension + editor?.on("update", handleDocumentInfoChange); + // Return a function to unsubscribe to the continuous transactions of + // the editor on unmounting the component that has subscribed to this + // method + return () => { + editor?.off("update", handleDocumentInfoChange); + }; + }, + onHeadingChange: (callback) => { + const handleHeadingChange = () => { + if (!editor) return; + const headings = getExtensionStorage(editor, CORE_EXTENSIONS.HEADINGS_LIST)?.headings; + if (headings) { + callback(headings); + } + }; + + // Subscribe to update event emitted from headers extension + editor?.on("update", handleHeadingChange); + // Return a function to unsubscribe to the continuous transactions of + // the editor on unmounting the component that has subscribed to this + // method + return () => { + editor?.off("update", handleHeadingChange); + }; + }, + onStateChange: (callback) => { + // Subscribe to editor state changes + editor?.on("transaction", callback); + + // Return a function to unsubscribe to the continuous transactions of + // the editor on unmounting the component that has subscribed to this + // method + return () => { + editor?.off("transaction", callback); + }; + }, + scrollToNodeViaDOMCoordinates(behavior, pos) { + const resolvedPos = pos ?? editor?.state.selection.from; + if (!editor || !resolvedPos) return; + scrollToNodeViaDOMCoordinates(editor, resolvedPos, behavior); + }, + setEditorValueAtCursorPosition: (content) => { + if (editor?.state.selection) { + insertContentAtSavedSelection(editor, content); + } + }, + setFocusAtPosition: (position) => { + if (!editor || editor.isDestroyed) { + console.error("Editor reference is not available or has been destroyed."); + return; + } + try { + const docSize = editor.state.doc.content.size; + const safePosition = Math.max(0, Math.min(position, docSize)); + editor + .chain() + .insertContentAt(safePosition, [{ type: CORE_EXTENSIONS.PARAGRAPH }]) + .focus() + .run(); + } catch (error) { + console.error("An error occurred while setting focus at position:", error); + } + }, setProviderDocument: (value) => { const document = provider?.document; if (!document) return; Y.applyUpdate(document, value); }, - emitRealTimeUpdate: (message: TDocumentEventsServer) => provider?.sendStateless(message), - listenToRealTimeUpdate: () => provider && { on: provider.on.bind(provider), off: provider.off.bind(provider) }, }), [editor] ); diff --git a/packages/editor/src/core/hooks/use-read-only-editor.ts b/packages/editor/src/core/hooks/use-read-only-editor.ts index d259470ac..43e9c9581 100644 --- a/packages/editor/src/core/hooks/use-read-only-editor.ts +++ b/packages/editor/src/core/hooks/use-read-only-editor.ts @@ -1,13 +1,9 @@ import { useEditor as useTiptapEditor } from "@tiptap/react"; import { useImperativeHandle, useEffect } from "react"; -import * as Y from "yjs"; -// constants -import { CORE_EDITOR_META } from "@/constants/meta"; // extensions import { CoreReadOnlyEditorExtensions } from "@/extensions"; // helpers -import { getParagraphCount } from "@/helpers/common"; -import { IMarking, scrollSummary } from "@/helpers/scroll-to-node"; +import { getEditorRefHelpers } from "@/helpers/editor-ref"; // props import { CoreReadOnlyEditorProps } from "@/props"; // types @@ -30,7 +26,7 @@ export const useReadOnlyEditor = (props: TReadOnlyEditorHookProps) => { const editor = useTiptapEditor({ editable: false, - immediatelyRender: true, + immediatelyRender: false, shouldRerenderOnTransaction: false, content: typeof initialValue === "string" && initialValue.trim() !== "" ? initialValue : "

", parseOptions: { preserveWhitespace: true }, @@ -63,38 +59,7 @@ export const useReadOnlyEditor = (props: TReadOnlyEditorHookProps) => { if (editor && !editor.isDestroyed) editor?.commands.setContent(initialValue, false, { preserveWhitespace: true }); }, [editor, initialValue]); - useImperativeHandle(forwardedRef, () => ({ - clearEditor: (emitUpdate = false) => { - editor?.chain().setMeta(CORE_EDITOR_META.SKIP_FILE_DELETION, true).clearContent(emitUpdate).run(); - }, - setEditorValue: (content: string, emitUpdate = false) => { - editor?.commands.setContent(content, emitUpdate, { preserveWhitespace: true }); - }, - getMarkDown: (): string => { - const markdownOutput = editor?.storage.markdown.getMarkdown(); - return markdownOutput; - }, - getDocument: () => { - const documentBinary = provider?.document ? Y.encodeStateAsUpdate(provider?.document) : null; - const documentHTML = editor?.getHTML() ?? "

"; - const documentJSON = editor?.getJSON() ?? null; - - return { - binary: documentBinary, - html: documentHTML, - json: documentJSON, - }; - }, - scrollSummary: (marking: IMarking): void => { - if (!editor) return; - scrollSummary(editor, marking); - }, - getDocumentInfo: () => ({ - characters: editor.storage?.characterCount?.characters?.() ?? 0, - paragraphs: getParagraphCount(editor.state), - words: editor.storage?.characterCount?.words?.() ?? 0, - }), - })); + useImperativeHandle(forwardedRef, () => getEditorRefHelpers({ editor, provider })); if (!editor) { return null; diff --git a/packages/editor/src/core/plugins/drag-handle.ts b/packages/editor/src/core/plugins/drag-handle.ts index 4a534bc4c..e04bbaba4 100644 --- a/packages/editor/src/core/plugins/drag-handle.ts +++ b/packages/editor/src/core/plugins/drag-handle.ts @@ -170,7 +170,7 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp return; } - const scrollableParent = getScrollParent(dragHandleElement); + const scrollableParent = getScrollParent(dragHandleElement!); if (!scrollableParent) return; const scrollRegionUp = options.scrollThreshold.up; diff --git a/packages/editor/src/core/plugins/file/delete.ts b/packages/editor/src/core/plugins/file/delete.ts index ac69b1819..427b100b7 100644 --- a/packages/editor/src/core/plugins/file/delete.ts +++ b/packages/editor/src/core/plugins/file/delete.ts @@ -57,6 +57,10 @@ export const TrackFileDeletionPlugin = (editor: Editor, deleteHandler: TFileHand if (!nodeFileSetDetails || !src) return; try { editor.storage[nodeType][nodeFileSetDetails.fileSetName]?.set(src, true); + // update assets list storage value + editor.commands.updateAssetsList?.({ + idToRemove: node.attrs.id, + }); await deleteHandler(src); } catch (error) { console.error("Error deleting file via delete utility plugin:", error); diff --git a/packages/editor/src/core/plugins/file/restore.ts b/packages/editor/src/core/plugins/file/restore.ts index 04a4c295c..bb4eb2afb 100644 --- a/packages/editor/src/core/plugins/file/restore.ts +++ b/packages/editor/src/core/plugins/file/restore.ts @@ -2,6 +2,8 @@ import { Editor } from "@tiptap/core"; import { EditorState, Plugin, PluginKey, Transaction } from "@tiptap/pm/state"; // constants import { CORE_EXTENSIONS } from "@/constants/extension"; +// helpers +import { CORE_ASSETS_META_DATA_RECORD } from "@/helpers/assets"; // plane editor imports import { NODE_FILE_MAP } from "@/plane-editor/constants/utility"; // types @@ -42,6 +44,13 @@ export const TrackFileRestorationPlugin = (editor: Editor, restoreHandler: TFile if (!isAValidNode) return; if (pos < 0 || pos > newState.doc.content.size) return; if (oldFileSources[nodeType]?.has(node.attrs.src)) return; + // update assets list storage value + const assetMetaData = CORE_ASSETS_META_DATA_RECORD[nodeType]?.(node.attrs); + if (assetMetaData) { + editor.commands.updateAssetsList?.({ + asset: assetMetaData, + }); + } // if the src is just a id (private bucket), then we don't need to handle restore from here but // only while it fails to load if (nodeType === CORE_EXTENSIONS.CUSTOM_IMAGE && !node.attrs.src?.startsWith("http")) return; diff --git a/packages/editor/src/core/types/asset.ts b/packages/editor/src/core/types/asset.ts new file mode 100644 index 000000000..5760da157 --- /dev/null +++ b/packages/editor/src/core/types/asset.ts @@ -0,0 +1,14 @@ +// constants +import { CORE_EXTENSIONS } from "@/constants/extension"; +// plane editor imports +import { TAdditionalEditorAsset } from "@/plane-editor/types/asset"; + +export type TEditorImageAsset = { + href: string; + id: string; + name: string; + src: string; + type: CORE_EXTENSIONS.IMAGE | CORE_EXTENSIONS.CUSTOM_IMAGE; +}; + +export type TEditorAsset = TEditorImageAsset | TAdditionalEditorAsset; diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index cf3d7d2c7..68d8424ab 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -1,5 +1,5 @@ -import type { Extensions, JSONContent } from "@tiptap/core"; -import type { Selection } from "@tiptap/pm/state"; +import { Extensions, JSONContent } from "@tiptap/core"; +import { Selection } from "@tiptap/pm/state"; // extension types import type { TTextAlign } from "@/extensions"; // helpers @@ -10,6 +10,7 @@ import type { TDisplayConfig, TDocumentEventEmitter, TDocumentEventsServer, + TEditorAsset, TEmbedConfig, TExtensions, TFileHandler, @@ -75,41 +76,44 @@ type TCommandWithPropsWithItemKey = T extends keyof T ? { itemKey: T } & TCommandExtraProps[T] : { itemKey: T }; +export type TDocumentInfo = { + characters: number; + paragraphs: number; + words: number; +}; + // editor refs export type EditorReadOnlyRefApi = { - getMarkDown: () => string; + clearEditor: (emitUpdate?: boolean) => void; getDocument: () => { binary: Uint8Array | null; html: string; json: JSONContent | null; }; - clearEditor: (emitUpdate?: boolean) => void; - setEditorValue: (content: string, emitUpdate?: boolean) => void; + getDocumentInfo: () => TDocumentInfo; + getHeadings: () => IMarking[]; + getMarkDown: () => string; scrollSummary: (marking: IMarking) => void; - getDocumentInfo: () => { - characters: number; - paragraphs: number; - words: number; - }; + setEditorValue: (content: string, emitUpdate?: boolean) => void; }; export interface EditorRefApi extends EditorReadOnlyRefApi { blur: () => void; - scrollToNodeViaDOMCoordinates: (behavior?: ScrollBehavior, position?: number) => void; - getCurrentCursorPosition: () => number | undefined; - setEditorValueAtCursorPosition: (content: string) => void; + emitRealTimeUpdate: (action: TDocumentEventsServer) => void; executeMenuItemCommand: (props: TCommandWithPropsWithItemKey) => void; - isMenuItemActive: (props: TCommandWithPropsWithItemKey) => boolean; - onStateChange: (callback: () => void) => () => void; - setFocusAtPosition: (position: number) => void; - isEditorReadyToDiscard: () => boolean; + getCurrentCursorPosition: () => number | undefined; getSelectedText: () => string | null; insertText: (contentHTML: string, insertOnNextLine?: boolean) => void; - setProviderDocument: (value: Uint8Array) => void; - onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void; - getHeadings: () => IMarking[]; - emitRealTimeUpdate: (action: TDocumentEventsServer) => void; + isEditorReadyToDiscard: () => boolean; + isMenuItemActive: (props: TCommandWithPropsWithItemKey) => boolean; listenToRealTimeUpdate: () => TDocumentEventEmitter | undefined; + onDocumentInfoChange: (callback: (documentInfo: TDocumentInfo) => void) => () => void; + onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void; + onStateChange: (callback: () => void) => () => void; + scrollToNodeViaDOMCoordinates: (behavior?: ScrollBehavior, position?: number) => void; + setEditorValueAtCursorPosition: (content: string) => void; + setFocusAtPosition: (position: number) => void; + setProviderDocument: (value: Uint8Array) => void; } // editor props @@ -128,6 +132,7 @@ export interface IEditorProps { id: string; initialValue: string; mentionHandler: TMentionHandler; + onAssetChange?: (assets: TEditorAsset[]) => void; onChange?: (json: object, html: string) => void; onEnterKeyPress?: (e?: any) => void; onTransaction?: () => void; diff --git a/packages/editor/src/core/types/hook.ts b/packages/editor/src/core/types/hook.ts index 2224935ca..40974981b 100644 --- a/packages/editor/src/core/types/hook.ts +++ b/packages/editor/src/core/types/hook.ts @@ -18,6 +18,7 @@ export type TEditorHookProps = TCoreHookProps & | "forwardedRef" | "id" | "mentionHandler" + | "onAssetChange" | "onChange" | "onTransaction" | "placeholder" @@ -38,6 +39,7 @@ export type TCollaborativeEditorHookProps = TCoreHookProps & | "forwardedRef" | "id" | "mentionHandler" + | "onAssetChange" | "onChange" | "onTransaction" | "placeholder" diff --git a/packages/editor/src/core/types/index.ts b/packages/editor/src/core/types/index.ts index 619fa0c78..cfa67ba97 100644 --- a/packages/editor/src/core/types/index.ts +++ b/packages/editor/src/core/types/index.ts @@ -1,4 +1,5 @@ export * from "./ai"; +export * from "./asset"; export * from "./collaboration"; export * from "./config"; export * from "./editor"; diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index fec933f91..43b295647 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -35,5 +35,8 @@ export { useEditor } from "@/hooks/use-editor"; export { type IMarking, useEditorMarkings } from "@/hooks/use-editor-markings"; export { useReadOnlyEditor } from "@/hooks/use-read-only-editor"; +export { CORE_EXTENSIONS } from "@/constants/extension"; +export { ADDITIONAL_EXTENSIONS } from "@/plane-editor/constants/extensions"; + // types export * from "@/types"; diff --git a/packages/i18n/src/locales/cs/translations.json b/packages/i18n/src/locales/cs/translations.json index 396ca03e5..b392788a0 100644 --- a/packages/i18n/src/locales/cs/translations.json +++ b/packages/i18n/src/locales/cs/translations.json @@ -2470,5 +2470,45 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane se nespustil. To může být způsobeno tím, že se jeden nebo více služeb Plane nepodařilo spustit.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Vyberte View Logs z setup.sh a Docker logů, abyste si byli jisti." + }, + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "Osnova", + "empty_state": { + "title": "Chybí nadpisy", + "description": "Přidejte na tuto stránku nějaké nadpisy, aby se zde zobrazily." + } + }, + "info": { + "label": "Info", + "document_info": { + "words": "Slova", + "characters": "Znaky", + "paragraphs": "Odstavce", + "read_time": "Doba čtení" + }, + "actors_info": { + "edited_by": "Upravil", + "created_by": "Vytvořil" + }, + "version_history": { + "label": "Historie verzí", + "current_version": "Aktuální verze" + } + }, + "assets": { + "label": "Přílohy", + "download_button": "Stáhnout", + "empty_state": { + "title": "Chybí obrázky", + "description": "Přidejte obrázky, aby se zde zobrazily." + } + } + }, + "open_button": "Otevřít navigační panel", + "close_button": "Zavřít navigační panel", + "outline_floating_button": "Otevřít osnovu" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/de/translations.json b/packages/i18n/src/locales/de/translations.json index 1b6e4778e..a6c73e845 100644 --- a/packages/i18n/src/locales/de/translations.json +++ b/packages/i18n/src/locales/de/translations.json @@ -2469,5 +2469,45 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane ist nicht gestartet. Dies könnte daran liegen, dass einer oder mehrere Plane-Services nicht starten konnten.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Wählen Sie View Logs aus setup.sh und Docker-Logs, um sicherzugehen." + }, + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "Gliederung", + "empty_state": { + "title": "Fehlende Überschriften", + "description": "Fügen Sie einige Überschriften zu dieser Seite hinzu, um sie hier zu sehen." + } + }, + "info": { + "label": "Info", + "document_info": { + "words": "Wörter", + "characters": "Zeichen", + "paragraphs": "Absätze", + "read_time": "Lesezeit" + }, + "actors_info": { + "edited_by": "Bearbeitet von", + "created_by": "Erstellt von" + }, + "version_history": { + "label": "Versionsverlauf", + "current_version": "Aktuelle Version" + } + }, + "assets": { + "label": "Assets", + "download_button": "Herunterladen", + "empty_state": { + "title": "Fehlende Bilder", + "description": "Fügen Sie Bilder hinzu, um sie hier zu sehen." + } + } + }, + "open_button": "Navigationsbereich öffnen", + "close_button": "Navigationsbereich schließen", + "outline_floating_button": "Gliederung öffnen" } } diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index fafed9c77..da650cbbe 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -2346,5 +2346,45 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane didn't start up. This could be because one or more Plane services failed to start.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Choose View Logs from setup.sh and Docker logs to be sure." + }, + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "Outline", + "empty_state": { + "title": "Missing headings", + "description": "Let's put some headings in this page to see them here." + } + }, + "info": { + "label": "Info", + "document_info": { + "words": "Words", + "characters": "Characters", + "paragraphs": "Paragraphs", + "read_time": "Read time" + }, + "actors_info": { + "edited_by": "Edited by", + "created_by": "Created by" + }, + "version_history": { + "label": "Version history", + "current_version": "Current version" + } + }, + "assets": { + "label": "Assets", + "download_button": "Download", + "empty_state": { + "title": "Missing images", + "description": "Add images to see them here." + } + } + }, + "open_button": "Open navigation pane", + "close_button": "Close navigation pane", + "outline_floating_button": "Open outline" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index 49ca53ea1..d8a350ce3 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -2472,5 +2472,45 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane no se inició. Esto podría deberse a que uno o más servicios de Plane fallaron al iniciar.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Selecciona View Logs desde setup.sh y los logs de Docker para estar seguro." + }, + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "Esquema", + "empty_state": { + "title": "Faltan encabezados", + "description": "Añade algunos encabezados a esta página para verlos aquí." + } + }, + "info": { + "label": "Info", + "document_info": { + "words": "Palabras", + "characters": "Caracteres", + "paragraphs": "Párrafos", + "read_time": "Tiempo de lectura" + }, + "actors_info": { + "edited_by": "Editado por", + "created_by": "Creado por" + }, + "version_history": { + "label": "Historial de versiones", + "current_version": "Versión actual" + } + }, + "assets": { + "label": "Recursos", + "download_button": "Descargar", + "empty_state": { + "title": "Faltan imágenes", + "description": "Añade imágenes para verlas aquí." + } + } + }, + "open_button": "Abrir panel de navegación", + "close_button": "Cerrar panel de navegación", + "outline_floating_button": "Abrir esquema" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index e42db2c52..a1f4c3cec 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -2470,5 +2470,45 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane n'a pas démarré. Cela pourrait être dû au fait qu'un ou plusieurs services Plane ont échoué à démarrer.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Choisissez View Logs depuis setup.sh et les logs Docker pour en être sûr." + }, + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "Plan", + "empty_state": { + "title": "Titres manquants", + "description": "Ajoutons quelques titres à cette page pour les voir ici." + } + }, + "info": { + "label": "Info", + "document_info": { + "words": "Mots", + "characters": "Caractères", + "paragraphs": "Paragraphes", + "read_time": "Temps de lecture" + }, + "actors_info": { + "edited_by": "Modifié par", + "created_by": "Créé par" + }, + "version_history": { + "label": "Historique des versions", + "current_version": "Version actuelle" + } + }, + "assets": { + "label": "Ressources", + "download_button": "Télécharger", + "empty_state": { + "title": "Images manquantes", + "description": "Ajoutez des images pour les voir ici." + } + } + }, + "open_button": "Ouvrir le panneau de navigation", + "close_button": "Fermer le panneau de navigation", + "outline_floating_button": "Ouvrir le plan" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/id/translations.json b/packages/i18n/src/locales/id/translations.json index 372aefde9..0f388db39 100644 --- a/packages/i18n/src/locales/id/translations.json +++ b/packages/i18n/src/locales/id/translations.json @@ -2465,5 +2465,45 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane tidak berhasil dimulai. Ini bisa karena satu atau lebih layanan Plane gagal untuk dimulai.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Pilih View Logs dari setup.sh dan log Docker untuk memastikan." }, - "no_of": "Jumlah {entity}" -} \ No newline at end of file + "no_of": "Jumlah {entity}", + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "Garis Besar", + "empty_state": { + "title": "Judul hilang", + "description": "Mari tambahkan beberapa judul di halaman ini untuk melihatnya di sini." + } + }, + "info": { + "label": "Info", + "document_info": { + "words": "Kata", + "characters": "Karakter", + "paragraphs": "Paragraf", + "read_time": "Waktu baca" + }, + "actors_info": { + "edited_by": "Disunting oleh", + "created_by": "Dibuat oleh" + }, + "version_history": { + "label": "Riwayat versi", + "current_version": "Versi saat ini" + } + }, + "assets": { + "label": "Aset", + "download_button": "Unduh", + "empty_state": { + "title": "Gambar hilang", + "description": "Tambahkan gambar untuk melihatnya di sini." + } + } + }, + "open_button": "Buka panel navigasi", + "close_button": "Tutup panel navigasi", + "outline_floating_button": "Buka garis besar" + } +} diff --git a/packages/i18n/src/locales/it/translations.json b/packages/i18n/src/locales/it/translations.json index b859ce217..a6275f53d 100644 --- a/packages/i18n/src/locales/it/translations.json +++ b/packages/i18n/src/locales/it/translations.json @@ -2469,5 +2469,45 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane non si è avviato. Questo potrebbe essere dovuto al fatto che uno o più servizi Plane non sono riusciti ad avviarsi.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Scegli View Logs da setup.sh e dai log Docker per essere sicuro." + }, + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "Schema", + "empty_state": { + "title": "Intestazioni mancanti", + "description": "Aggiungiamo alcune intestazioni a questa pagina per vederle qui." + } + }, + "info": { + "label": "Info", + "document_info": { + "words": "Parole", + "characters": "Caratteri", + "paragraphs": "Paragrafi", + "read_time": "Tempo di lettura" + }, + "actors_info": { + "edited_by": "Modificato da", + "created_by": "Creato da" + }, + "version_history": { + "label": "Cronologia versioni", + "current_version": "Versione corrente" + } + }, + "assets": { + "label": "Risorse", + "download_button": "Scarica", + "empty_state": { + "title": "Immagini mancanti", + "description": "Aggiungi immagini per vederle qui." + } + } + }, + "open_button": "Apri pannello di navigazione", + "close_button": "Chiudi pannello di navigazione", + "outline_floating_button": "Apri schema" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index 4c6f27a6e..8147451c7 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -2470,5 +2470,45 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Planeが起動しませんでした。これは1つまたは複数のPlaneサービスの起動に失敗したことが原因である可能性があります。", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "setup.shとDockerログからView Logsを選択して確認してください。" + }, + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "アウトライン", + "empty_state": { + "title": "見出しがありません", + "description": "このページに見出しを追加してここで確認しましょう。" + } + }, + "info": { + "label": "情報", + "document_info": { + "words": "単語数", + "characters": "文字数", + "paragraphs": "段落数", + "read_time": "読了時間" + }, + "actors_info": { + "edited_by": "編集者", + "created_by": "作成者" + }, + "version_history": { + "label": "バージョン履歴", + "current_version": "現在のバージョン" + } + }, + "assets": { + "label": "アセット", + "download_button": "ダウンロード", + "empty_state": { + "title": "画像がありません", + "description": "画像を追加してここで確認してください。" + } + } + }, + "open_button": "ナビゲーションパネルを開く", + "close_button": "ナビゲーションパネルを閉じる", + "outline_floating_button": "アウトラインを開く" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ko/translations.json b/packages/i18n/src/locales/ko/translations.json index ee1f61adc..4a984b4c3 100644 --- a/packages/i18n/src/locales/ko/translations.json +++ b/packages/i18n/src/locales/ko/translations.json @@ -2472,5 +2472,45 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane이 시작되지 않았습니다. 이는 하나 이상의 Plane 서비스가 시작에 실패했기 때문일 수 있습니다.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "확실히 하려면 setup.sh와 Docker 로그에서 View Logs를 선택하세요." + }, + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "개요", + "empty_state": { + "title": "제목이 없습니다", + "description": "이 페이지에 제목을 추가하여 여기에서 확인해보세요." + } + }, + "info": { + "label": "정보", + "document_info": { + "words": "단어", + "characters": "문자", + "paragraphs": "단락", + "read_time": "읽기 시간" + }, + "actors_info": { + "edited_by": "편집자", + "created_by": "작성자" + }, + "version_history": { + "label": "버전 기록", + "current_version": "현재 버전" + } + }, + "assets": { + "label": "자산", + "download_button": "다운로드", + "empty_state": { + "title": "이미지가 없습니다", + "description": "이미지를 추가하여 여기에서 확인하세요." + } + } + }, + "open_button": "네비게이션 패널 열기", + "close_button": "네비게이션 패널 닫기", + "outline_floating_button": "개요 열기" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/pl/translations.json b/packages/i18n/src/locales/pl/translations.json index b26e6e2f4..38a564e3e 100644 --- a/packages/i18n/src/locales/pl/translations.json +++ b/packages/i18n/src/locales/pl/translations.json @@ -2471,5 +2471,45 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane nie uruchomił się. Może to być spowodowane tym, że jedna lub więcej usług Plane nie mogła się uruchomić.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Wybierz View Logs z setup.sh i logów Docker, aby mieć pewność." + }, + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "Konspekt", + "empty_state": { + "title": "Brakuje nagłówków", + "description": "Dodajmy kilka nagłówków na tej stronie, aby je tutaj zobaczyć." + } + }, + "info": { + "label": "Info", + "document_info": { + "words": "Słowa", + "characters": "Znaki", + "paragraphs": "Akapity", + "read_time": "Czas czytania" + }, + "actors_info": { + "edited_by": "Edytowane przez", + "created_by": "Utworzone przez" + }, + "version_history": { + "label": "Historia wersji", + "current_version": "Bieżąca wersja" + } + }, + "assets": { + "label": "Zasoby", + "download_button": "Pobierz", + "empty_state": { + "title": "Brakuje obrazów", + "description": "Dodaj obrazy, aby je tutaj zobaczyć." + } + } + }, + "open_button": "Otwórz panel nawigacji", + "close_button": "Zamknij panel nawigacji", + "outline_floating_button": "Otwórz konspekt" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/pt-BR/translations.json b/packages/i18n/src/locales/pt-BR/translations.json index 6e7f216ab..e8fd5ee28 100644 --- a/packages/i18n/src/locales/pt-BR/translations.json +++ b/packages/i18n/src/locales/pt-BR/translations.json @@ -2466,5 +2466,45 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "O Plane não inicializou. Isso pode ser porque um ou mais serviços do Plane falharam ao iniciar.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Escolha View Logs do setup.sh e logs do Docker para ter certeza." + }, + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "Estrutura", + "empty_state": { + "title": "Cabeçalhos ausentes", + "description": "Vamos adicionar alguns cabeçalhos nesta página para vê-los aqui." + } + }, + "info": { + "label": "Info", + "document_info": { + "words": "Palavras", + "characters": "Caracteres", + "paragraphs": "Parágrafos", + "read_time": "Tempo de leitura" + }, + "actors_info": { + "edited_by": "Editado por", + "created_by": "Criado por" + }, + "version_history": { + "label": "Histórico de versões", + "current_version": "Versão atual" + } + }, + "assets": { + "label": "Recursos", + "download_button": "Baixar", + "empty_state": { + "title": "Imagens ausentes", + "description": "Adicione imagens para vê-las aqui." + } + } + }, + "open_button": "Abrir painel de navegação", + "close_button": "Fechar painel de navegação", + "outline_floating_button": "Abrir estrutura" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ro/translations.json b/packages/i18n/src/locales/ro/translations.json index 8f40c0a22..55b4ae2c5 100644 --- a/packages/i18n/src/locales/ro/translations.json +++ b/packages/i18n/src/locales/ro/translations.json @@ -2464,5 +2464,45 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane nu a pornit. Aceasta ar putea fi din cauza că unul sau mai multe servicii Plane au eșuat să pornească.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Alegeți View Logs din setup.sh și logurile Docker pentru a fi siguri." + }, + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "Contur", + "empty_state": { + "title": "Titluri lipsă", + "description": "Să punem câteva titluri în această pagină pentru a le vedea aici." + } + }, + "info": { + "label": "Info", + "document_info": { + "words": "Cuvinte", + "characters": "Caractere", + "paragraphs": "Paragrafe", + "read_time": "Timp de citire" + }, + "actors_info": { + "edited_by": "Editat de", + "created_by": "Creat de" + }, + "version_history": { + "label": "Istoricul versiunilor", + "current_version": "Versiunea curentă" + } + }, + "assets": { + "label": "Resurse", + "download_button": "Descarcă", + "empty_state": { + "title": "Imagini lipsă", + "description": "Adăugați imagini pentru a le vedea aici." + } + } + }, + "open_button": "Deschide panoul de navigare", + "close_button": "Închide panoul de navigare", + "outline_floating_button": "Deschide conturul" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ru/translations.json b/packages/i18n/src/locales/ru/translations.json index 1981999b7..bbe979e89 100644 --- a/packages/i18n/src/locales/ru/translations.json +++ b/packages/i18n/src/locales/ru/translations.json @@ -2473,5 +2473,45 @@ "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane не запустился. Это может быть из-за того, что один или несколько сервисов Plane не смогли запуститься.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Выберите View Logs из setup.sh и логов Docker, чтобы убедиться." }, - "no_of": "Количество {entity}" -} \ No newline at end of file + "no_of": "Количество {entity}", + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "Структура", + "empty_state": { + "title": "Отсутствуют заголовки", + "description": "Давайте добавим несколько заголовков на эту страницу, чтобы увидеть их здесь." + } + }, + "info": { + "label": "Информация", + "document_info": { + "words": "Слова", + "characters": "Символы", + "paragraphs": "Абзацы", + "read_time": "Время чтения" + }, + "actors_info": { + "edited_by": "Отредактировано", + "created_by": "Создано" + }, + "version_history": { + "label": "История версий", + "current_version": "Текущая версия" + } + }, + "assets": { + "label": "Ресурсы", + "download_button": "Скачать", + "empty_state": { + "title": "Отсутствуют изображения", + "description": "Добавьте изображения, чтобы увидеть их здесь." + } + } + }, + "open_button": "Открыть панель навигации", + "close_button": "Закрыть панель навигации", + "outline_floating_button": "Открыть структуру" + } +} diff --git a/packages/i18n/src/locales/sk/translations.json b/packages/i18n/src/locales/sk/translations.json index af6971aae..5cc3e360f 100644 --- a/packages/i18n/src/locales/sk/translations.json +++ b/packages/i18n/src/locales/sk/translations.json @@ -2471,5 +2471,45 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane sa nespustil. Toto môže byť spôsobené tým, že sa jedna alebo viac služieb Plane nepodarilo spustiť.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Vyberte View Logs z setup.sh a Docker logov, aby ste si boli istí." + }, + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "Osnova", + "empty_state": { + "title": "Chýbajú nadpisy", + "description": "Pridajme na túto stránku nejaké nadpisy, aby sa tu zobrazili." + } + }, + "info": { + "label": "Info", + "document_info": { + "words": "Slová", + "characters": "Znaky", + "paragraphs": "Odseky", + "read_time": "Čas čítania" + }, + "actors_info": { + "edited_by": "Upravil", + "created_by": "Vytvoril" + }, + "version_history": { + "label": "História verzií", + "current_version": "Aktuálna verzia" + } + }, + "assets": { + "label": "Prílohy", + "download_button": "Stiahnuť", + "empty_state": { + "title": "Chýbajú obrázky", + "description": "Pridajte obrázky, aby sa tu zobrazili." + } + } + }, + "open_button": "Otvoriť navigačný panel", + "close_button": "Zavrieť navigačný panel", + "outline_floating_button": "Otvoriť osnovu" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/tr-TR/translations.json b/packages/i18n/src/locales/tr-TR/translations.json index a4ae00670..d088aa835 100644 --- a/packages/i18n/src/locales/tr-TR/translations.json +++ b/packages/i18n/src/locales/tr-TR/translations.json @@ -2450,5 +2450,45 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane başlatılamadı. Bu, bir veya daha fazla Plane servisinin başlatılamaması nedeniyle olabilir.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Emin olmak için setup.sh ve Docker loglarından View Logs'u seçin." + }, + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "Ana Hat", + "empty_state": { + "title": "Eksik başlıklar", + "description": "Bu sayfaya bazı başlıklar ekleyelim ki burada görebilelim." + } + }, + "info": { + "label": "Bilgi", + "document_info": { + "words": "Kelimeler", + "characters": "Karakterler", + "paragraphs": "Paragraflar", + "read_time": "Okuma süresi" + }, + "actors_info": { + "edited_by": "Düzenleyen", + "created_by": "Oluşturan" + }, + "version_history": { + "label": "Sürüm geçmişi", + "current_version": "Mevcut sürüm" + } + }, + "assets": { + "label": "Varlıklar", + "download_button": "İndir", + "empty_state": { + "title": "Eksik görseller", + "description": "Burada görmek için görseller ekleyin." + } + } + }, + "open_button": "Navigasyon panelini aç", + "close_button": "Navigasyon panelini kapat", + "outline_floating_button": "Ana hatları aç" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/ua/translations.json b/packages/i18n/src/locales/ua/translations.json index bfa6c3281..04695f9b1 100644 --- a/packages/i18n/src/locales/ua/translations.json +++ b/packages/i18n/src/locales/ua/translations.json @@ -2471,5 +2471,45 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane не запустився. Це може бути через те, що один або декілька сервісів Plane не змогли запуститися.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Виберіть View Logs з setup.sh та логів Docker, щоб переконатися." + }, + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "Структура", + "empty_state": { + "title": "Відсутні заголовки", + "description": "Давайте додамо кілька заголовків на цю сторінку, щоб побачити їх тут." + } + }, + "info": { + "label": "Інформація", + "document_info": { + "words": "Слова", + "characters": "Символи", + "paragraphs": "Абзаци", + "read_time": "Час читання" + }, + "actors_info": { + "edited_by": "Відредаговано", + "created_by": "Створено" + }, + "version_history": { + "label": "Історія версій", + "current_version": "Поточна версія" + } + }, + "assets": { + "label": "Ресурси", + "download_button": "Завантажити", + "empty_state": { + "title": "Відсутні зображення", + "description": "Додайте зображення, щоб побачити їх тут." + } + } + }, + "open_button": "Відкрити панель навігації", + "close_button": "Закрити панель навігації", + "outline_floating_button": "Відкрити структуру" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/vi-VN/translations.json b/packages/i18n/src/locales/vi-VN/translations.json index 3b31f81fe..37ceaeda8 100644 --- a/packages/i18n/src/locales/vi-VN/translations.json +++ b/packages/i18n/src/locales/vi-VN/translations.json @@ -2469,5 +2469,45 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane không khởi động được. Điều này có thể do một hoặc nhiều dịch vụ Plane không khởi động được.", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "Chọn View Logs từ setup.sh và log Docker để chắc chắn." + }, + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "Phác thảo", + "empty_state": { + "title": "Thiếu tiêu đề", + "description": "Hãy thêm một số tiêu đề vào trang này để xem chúng ở đây." + } + }, + "info": { + "label": "Thông tin", + "document_info": { + "words": "Từ", + "characters": "Ký tự", + "paragraphs": "Đoạn văn", + "read_time": "Thời gian đọc" + }, + "actors_info": { + "edited_by": "Được chỉnh sửa bởi", + "created_by": "Được tạo bởi" + }, + "version_history": { + "label": "Lịch sử phiên bản", + "current_version": "Phiên bản hiện tại" + } + }, + "assets": { + "label": "Tài sản", + "download_button": "Tải xuống", + "empty_state": { + "title": "Thiếu hình ảnh", + "description": "Thêm hình ảnh để xem chúng ở đây." + } + } + }, + "open_button": "Mở bảng điều hướng", + "close_button": "Đóng bảng điều hướng", + "outline_floating_button": "Mở phác thảo" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/zh-CN/translations.json b/packages/i18n/src/locales/zh-CN/translations.json index 304b435a8..3f08eec17 100644 --- a/packages/i18n/src/locales/zh-CN/translations.json +++ b/packages/i18n/src/locales/zh-CN/translations.json @@ -2451,5 +2451,45 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane 未能启动。这可能是因为一个或多个 Plane 服务启动失败。", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "请选择“查看日志”来查看 setup.sh 和 Docker 日志,以确认问题。" + }, + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "大纲", + "empty_state": { + "title": "缺少标题", + "description": "让我们在这个页面添加一些标题来在这里查看它们。" + } + }, + "info": { + "label": "信息", + "document_info": { + "words": "字数", + "characters": "字符数", + "paragraphs": "段落数", + "read_time": "阅读时间" + }, + "actors_info": { + "edited_by": "编辑者", + "created_by": "创建者" + }, + "version_history": { + "label": "版本历史", + "current_version": "当前版本" + } + }, + "assets": { + "label": "资源", + "download_button": "下载", + "empty_state": { + "title": "缺少图片", + "description": "添加图片以在这里查看它们。" + } + } + }, + "open_button": "打开导航面板", + "close_button": "关闭导航面板", + "outline_floating_button": "打开大纲" } -} \ No newline at end of file +} diff --git a/packages/i18n/src/locales/zh-TW/translations.json b/packages/i18n/src/locales/zh-TW/translations.json index 5f3165ecb..df2a4ba4a 100644 --- a/packages/i18n/src/locales/zh-TW/translations.json +++ b/packages/i18n/src/locales/zh-TW/translations.json @@ -2472,5 +2472,45 @@ "self_hosted_maintenance_message": { "plane_didnt_start_up_this_could_be_because_one_or_more_plane_services_failed_to_start": "Plane 未能啟動。這可能是因為一個或多個 Plane 服務啟動失敗。", "choose_view_logs_from_setup_sh_and_docker_logs_to_be_sure": "從 setup.sh 和 Docker 日誌中選擇 View Logs 來確認。" + }, + + "page_navigation_pane": { + "tabs": { + "outline": { + "label": "大綱", + "empty_state": { + "title": "缺少標題", + "description": "讓我們在這個頁面添加一些標題來在這裡查看它們。" + } + }, + "info": { + "label": "資訊", + "document_info": { + "words": "字數", + "characters": "字元數", + "paragraphs": "段落數", + "read_time": "閱讀時間" + }, + "actors_info": { + "edited_by": "編輯者", + "created_by": "建立者" + }, + "version_history": { + "label": "版本歷史", + "current_version": "目前版本" + } + }, + "assets": { + "label": "資源", + "download_button": "下載", + "empty_state": { + "title": "缺少圖片", + "description": "添加圖片以在這裡查看它們。" + } + } + }, + "open_button": "打開導航面板", + "close_button": "關閉導航面板", + "outline_floating_button": "打開大綱" } -} \ No newline at end of file +} diff --git a/packages/utils/src/editor.ts b/packages/utils/src/editor.ts index 1bdf3a504..fb15edd07 100644 --- a/packages/utils/src/editor.ts +++ b/packages/utils/src/editor.ts @@ -22,6 +22,21 @@ export const getEditorAssetSrc = (args: TEditorSrcArgs): string | undefined => { return url; }; +/** + * @description generate the file source using assetId + * @param {TEditorSrcArgs} args + */ +export const getEditorAssetDownloadSrc = (args: TEditorSrcArgs): string | undefined => { + const { assetId, projectId, workspaceSlug } = args; + let url: string | undefined = ""; + if (projectId) { + url = getFileURL(`/api/assets/v2/workspaces/${workspaceSlug}/projects/${projectId}/download/${assetId}/`); + } else { + url = getFileURL(`/api/assets/v2/workspaces/${workspaceSlug}/download/${assetId}/`); + } + return url; +}; + export const getTextContent = (jsx: JSX.Element | React.ReactNode | null | undefined): string => { if (!jsx) return ""; diff --git a/space/styles/globals.css b/space/styles/globals.css index 5d27de674..783a15792 100644 --- a/space/styles/globals.css +++ b/space/styles/globals.css @@ -47,23 +47,31 @@ --color-border-300: 212, 212, 212; /* strong border- 1 */ --color-border-400: 185, 185, 185; /* strong border- 2 */ - --color-shadow-2xs: 0px 0px 1px 0px rgba(23, 23, 23, 0.06), 0px 1px 2px 0px rgba(23, 23, 23, 0.06), + --color-shadow-2xs: + 0px 0px 1px 0px rgba(23, 23, 23, 0.06), 0px 1px 2px 0px rgba(23, 23, 23, 0.06), 0px 1px 2px 0px rgba(23, 23, 23, 0.14); - --color-shadow-xs: 0px 1px 2px 0px rgba(0, 0, 0, 0.16), 0px 2px 4px 0px rgba(16, 24, 40, 0.12), + --color-shadow-xs: + 0px 1px 2px 0px rgba(0, 0, 0, 0.16), 0px 2px 4px 0px rgba(16, 24, 40, 0.12), 0px 1px 8px -1px rgba(16, 24, 40, 0.1); - --color-shadow-sm: 0px 1px 4px 0px rgba(0, 0, 0, 0.01), 0px 4px 8px 0px rgba(0, 0, 0, 0.02), - 0px 1px 12px 0px rgba(0, 0, 0, 0.12); - --color-shadow-rg: 0px 3px 6px 0px rgba(0, 0, 0, 0.1), 0px 4px 4px 0px rgba(16, 24, 40, 0.08), + --color-shadow-sm: + 0px 1px 4px 0px rgba(0, 0, 0, 0.01), 0px 4px 8px 0px rgba(0, 0, 0, 0.02), 0px 1px 12px 0px rgba(0, 0, 0, 0.12); + --color-shadow-rg: + 0px 3px 6px 0px rgba(0, 0, 0, 0.1), 0px 4px 4px 0px rgba(16, 24, 40, 0.08), 0px 1px 12px 0px rgba(16, 24, 40, 0.04); - --color-shadow-md: 0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12), + --color-shadow-md: + 0px 4px 8px 0px rgba(0, 0, 0, 0.12), 0px 6px 12px 0px rgba(16, 24, 40, 0.12), 0px 1px 16px 0px rgba(16, 24, 40, 0.12); - --color-shadow-lg: 0px 6px 12px 0px rgba(0, 0, 0, 0.12), 0px 8px 16px 0px rgba(0, 0, 0, 0.12), + --color-shadow-lg: + 0px 6px 12px 0px rgba(0, 0, 0, 0.12), 0px 8px 16px 0px rgba(0, 0, 0, 0.12), 0px 1px 24px 0px rgba(16, 24, 40, 0.12); - --color-shadow-xl: 0px 0px 18px 0px rgba(0, 0, 0, 0.16), 0px 0px 24px 0px rgba(16, 24, 40, 0.16), + --color-shadow-xl: + 0px 0px 18px 0px rgba(0, 0, 0, 0.16), 0px 0px 24px 0px rgba(16, 24, 40, 0.16), 0px 0px 52px 0px rgba(16, 24, 40, 0.16); - --color-shadow-2xl: 0px 8px 16px 0px rgba(0, 0, 0, 0.12), 0px 12px 24px 0px rgba(16, 24, 40, 0.12), + --color-shadow-2xl: + 0px 8px 16px 0px rgba(0, 0, 0, 0.12), 0px 12px 24px 0px rgba(16, 24, 40, 0.12), 0px 1px 32px 0px rgba(16, 24, 40, 0.12); - --color-shadow-3xl: 0px 12px 24px 0px rgba(0, 0, 0, 0.12), 0px 16px 32px 0px rgba(0, 0, 0, 0.12), + --color-shadow-3xl: + 0px 12px 24px 0px rgba(0, 0, 0, 0.12), 0px 16px 32px 0px rgba(0, 0, 0, 0.12), 0px 1px 48px 0px rgba(16, 24, 40, 0.12); --color-shadow-4xl: 0px 8px 40px 0px rgba(0, 0, 61, 0.05), 0px 12px 32px -16px rgba(0, 0, 0, 0.05); @@ -359,6 +367,7 @@ text-rendering: optimizeLegibility; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; + scroll-behavior: smooth; } body { diff --git a/web/ce/components/pages/navigation-pane/index.ts b/web/ce/components/pages/navigation-pane/index.ts new file mode 100644 index 000000000..79ee20c26 --- /dev/null +++ b/web/ce/components/pages/navigation-pane/index.ts @@ -0,0 +1,31 @@ +export type TPageNavigationPaneTab = "outline" | "info" | "assets"; + +export const PAGE_NAVIGATION_PANE_TABS_LIST: Record< + TPageNavigationPaneTab, + { + key: TPageNavigationPaneTab; + i18n_label: string; + } +> = { + outline: { + key: "outline", + i18n_label: "page_navigation_pane.tabs.outline.label", + }, + info: { + key: "info", + i18n_label: "page_navigation_pane.tabs.info.label", + }, + assets: { + key: "assets", + i18n_label: "page_navigation_pane.tabs.assets.label", + }, +}; + +export const ORDERED_PAGE_NAVIGATION_TABS_LIST: { + key: TPageNavigationPaneTab; + i18n_label: string; +}[] = [ + PAGE_NAVIGATION_PANE_TABS_LIST.outline, + PAGE_NAVIGATION_PANE_TABS_LIST.info, + PAGE_NAVIGATION_PANE_TABS_LIST.assets, +]; diff --git a/web/ce/components/pages/navigation-pane/tab-panels/assets.tsx b/web/ce/components/pages/navigation-pane/tab-panels/assets.tsx new file mode 100644 index 000000000..960f0653c --- /dev/null +++ b/web/ce/components/pages/navigation-pane/tab-panels/assets.tsx @@ -0,0 +1,11 @@ +// plane imports +import { TEditorAsset } from "@plane/editor"; +// store +import { TPageInstance } from "@/store/pages/base-page"; + +export type TAdditionalPageNavigationPaneAssetItemProps = { + asset: TEditorAsset; + page: TPageInstance; +}; + +export const AdditionalPageNavigationPaneAssetItem: React.FC = () => null; diff --git a/web/ce/components/pages/navigation-pane/tab-panels/empty-states/assets.tsx b/web/ce/components/pages/navigation-pane/tab-panels/empty-states/assets.tsx new file mode 100644 index 000000000..e0bf49ad1 --- /dev/null +++ b/web/ce/components/pages/navigation-pane/tab-panels/empty-states/assets.tsx @@ -0,0 +1,26 @@ +import Image from "next/image"; +// plane imports +import { useTranslation } from "@plane/i18n"; +// hooks +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; + +export const PageNavigationPaneAssetsTabEmptyState = () => { + // asset resolved path + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/pages/navigation-pane/assets" }); + // translation + const { t } = useTranslation(); + + return ( +
+
+ An image depicting the assets of a page +
+

{t("page_navigation_pane.tabs.assets.empty_state.title")}

+

+ {t("page_navigation_pane.tabs.assets.empty_state.description")} +

+
+
+
+ ); +}; diff --git a/web/ce/components/pages/navigation-pane/tab-panels/empty-states/outline.tsx b/web/ce/components/pages/navigation-pane/tab-panels/empty-states/outline.tsx new file mode 100644 index 000000000..dd71bf3c1 --- /dev/null +++ b/web/ce/components/pages/navigation-pane/tab-panels/empty-states/outline.tsx @@ -0,0 +1,26 @@ +import Image from "next/image"; +// plane imports +import { useTranslation } from "@plane/i18n"; +// hooks +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; + +export const PageNavigationPaneOutlineTabEmptyState = () => { + // asset resolved path + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/pages/navigation-pane/outline" }); + // translation + const { t } = useTranslation(); + + return ( +
+
+ An image depicting the outline of a page +
+

{t("page_navigation_pane.tabs.outline.empty_state.title")}

+

+ {t("page_navigation_pane.tabs.outline.empty_state.description")} +

+
+
+
+ ); +}; diff --git a/web/ce/components/pages/navigation-pane/tab-panels/root.tsx b/web/ce/components/pages/navigation-pane/tab-panels/root.tsx new file mode 100644 index 000000000..93419437a --- /dev/null +++ b/web/ce/components/pages/navigation-pane/tab-panels/root.tsx @@ -0,0 +1,13 @@ +// store +import type { TPageInstance } from "@/store/pages/base-page"; +// local imports +import { TPageNavigationPaneTab } from ".."; + +export type TPageNavigationPaneAdditionalTabPanelsRootProps = { + activeTab: TPageNavigationPaneTab; + page: TPageInstance; +}; + +export const PageNavigationPaneAdditionalTabPanelsRoot: React.FC< + TPageNavigationPaneAdditionalTabPanelsRootProps +> = () => null; diff --git a/web/core/components/pages/editor/editor-body.tsx b/web/core/components/pages/editor/editor-body.tsx index a06f11312..bbcc596f8 100644 --- a/web/core/components/pages/editor/editor-body.tsx +++ b/web/core/components/pages/editor/editor-body.tsx @@ -10,7 +10,7 @@ import { TRealtimeConfig, TServerHandler, } from "@plane/editor"; -// plane imports +import { useTranslation } from "@plane/i18n"; import { TSearchEntityRequestPayload, TSearchResponse, TWebhookConnectionQueryParams } from "@plane/types"; import { ERowVariant, Row } from "@plane/ui"; import { cn, generateRandomColor, hslToHex } from "@plane/utils"; @@ -46,7 +46,9 @@ type Props = { editorForwardRef: React.RefObject; handleConnectionStatus: Dispatch>; handleEditorReady: (status: boolean) => void; + handleOpenNavigationPane: () => void; handlers: TEditorBodyHandlers; + isNavigationPaneOpen: boolean; page: TPageInstance; webhookConnectionParams: TWebhookConnectionQueryParams; workspaceSlug: string; @@ -58,7 +60,9 @@ export const PageEditorBody: React.FC = observer((props) => { editorForwardRef, handleConnectionStatus, handleEditorReady, + handleOpenNavigationPane, handlers, + isNavigationPaneOpen, page, webhookConnectionParams, workspaceSlug, @@ -67,9 +71,14 @@ export const PageEditorBody: React.FC = observer((props) => { const { data: currentUser } = useUser(); const { getWorkspaceBySlug } = useWorkspace(); const { getUserDetails } = useMember(); - // derived values - const { id: pageId, name: pageTitle, isContentEditable, updateTitle, editorRef } = page; + const { + id: pageId, + name: pageTitle, + isContentEditable, + updateTitle, + editor: { editorRef, updateAssetsList }, + } = page; const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id ?? ""; // issue-embed const { issueEmbedProps } = useIssueEmbed({ @@ -84,6 +93,8 @@ export const PageEditorBody: React.FC = observer((props) => { const { document: documentEditorExtensions } = useEditorFlagging(workspaceSlug); // page filters const { fontSize, fontStyle, isFullWidth } = usePageFilters(); + // translation + const { t } = useTranslation(); // derived values const displayConfig: TDisplayConfig = useMemo( () => ({ @@ -167,18 +178,25 @@ export const PageEditorBody: React.FC = observer((props) => { >
{/* table of content */} -
-
-
-
- -
-
- + {!isNavigationPaneOpen && ( +
+
+
+
+ +
+
+ +
-
+ )}
@@ -218,6 +236,7 @@ export const PageEditorBody: React.FC = observer((props) => { aiHandler={{ menu: getAIMenu, }} + onAssetChange={updateAssetsList} />
diff --git a/web/core/components/pages/editor/page-root.tsx b/web/core/components/pages/editor/page-root.tsx index 2f1595e33..e0e2fa9f4 100644 --- a/web/core/components/pages/editor/page-root.tsx +++ b/web/core/components/pages/editor/page-root.tsx @@ -1,9 +1,8 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; import { useSearchParams } from "next/navigation"; -// editor +// plane imports import { EditorRefApi } from "@plane/editor"; -// types import { TDocumentPayload, TPage, TPageVersion, TWebhookConnectionQueryParams } from "@plane/types"; // components import { @@ -18,8 +17,17 @@ import { import { useAppRouter } from "@/hooks/use-app-router"; import { usePageFallback } from "@/hooks/use-page-fallback"; import { useQueryParams } from "@/hooks/use-query-params"; +// plane web import +import { TPageNavigationPaneTab } from "@/plane-web/components/pages/navigation-pane"; // store import { TPageInstance } from "@/store/pages/base-page"; +// local imports +import { + PAGE_NAVIGATION_PANE_TAB_KEYS, + PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, + PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM, + PageNavigationPaneRoot, +} from "../navigation-pane"; export type TPageRootHandlers = { create: (payload: Partial) => Promise | undefined>; @@ -45,7 +53,6 @@ export const PageRoot = observer((props: TPageRootProps) => { // states const [editorReady, setEditorReady] = useState(false); const [hasConnectionFailed, setHasConnectionFailed] = useState(false); - const [isVersionsOverlayOpen, setIsVersionsOverlayOpen] = useState(false); // refs const editorRef = useRef(null); // router @@ -53,7 +60,10 @@ export const PageRoot = observer((props: TPageRootProps) => { // search params const searchParams = useSearchParams(); // derived values - const { isContentEditable, setEditorRef } = page; + const { + isContentEditable, + editor: { setEditorRef }, + } = page; // page fallback usePageFallback({ editorRef, @@ -67,11 +77,11 @@ export const PageRoot = observer((props: TPageRootProps) => { const handleEditorReady = useCallback( (status: boolean) => { setEditorReady(status); - if (editorRef.current && !page.editorRef) { + if (editorRef.current && !page.editor.editorRef) { setEditorRef(editorRef.current); } }, - [page.editorRef, setEditorRef] + [page.editor.editorRef, setEditorRef] ); useEffect(() => { @@ -80,27 +90,10 @@ export const PageRoot = observer((props: TPageRootProps) => { }, 0); }, [isContentEditable, setEditorRef]); - const version = searchParams.get("version"); - useEffect(() => { - if (!version) { - setIsVersionsOverlayOpen(false); - return; - } - setIsVersionsOverlayOpen(true); - }, [version]); - - const handleCloseVersionsOverlay = () => { - const updatedRoute = updateQueryParams({ - paramsToRemove: ["version"], - }); - router.push(updatedRoute); - }; - - const handleRestoreVersion = async (descriptionHTML: string) => { + const handleRestoreVersion = useCallback(async (descriptionHTML: string) => { editorRef.current?.clearEditor(); editorRef.current?.setEditorValue(descriptionHTML); - }; - const currentVersionDescription = editorRef.current?.getDocument().html; + }, []); // reset editor ref on unmount useEffect( @@ -110,32 +103,64 @@ export const PageRoot = observer((props: TPageRootProps) => { [setEditorRef] ); + const navigationPaneQueryParam = searchParams.get( + PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM + ) as TPageNavigationPaneTab | null; + const isValidNavigationPaneTab = + !!navigationPaneQueryParam && PAGE_NAVIGATION_PANE_TAB_KEYS.includes(navigationPaneQueryParam); + + const handleOpenNavigationPane = useCallback(() => { + const updatedRoute = updateQueryParams({ + paramsToAdd: { [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM]: "outline" }, + }); + router.push(updatedRoute); + }, [router, updateQueryParams]); + + const handleCloseNavigationPane = useCallback(() => { + const updatedRoute = updateQueryParams({ + paramsToRemove: [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM], + }); + router.push(updatedRoute); + }, [router, updateQueryParams]); + return ( - <> - - - +
+ + + +
+ - +
); }); diff --git a/web/core/components/pages/editor/summary/content-browser.tsx b/web/core/components/pages/editor/summary/content-browser.tsx index e0ef27116..d22a1ec2c 100644 --- a/web/core/components/pages/editor/summary/content-browser.tsx +++ b/web/core/components/pages/editor/summary/content-browser.tsx @@ -1,17 +1,20 @@ import { useState, useEffect, useCallback } from "react"; -// plane editor +// plane imports import { EditorRefApi, IMarking } from "@plane/editor"; +import { cn } from "@plane/utils"; // components -import { OutlineHeading1, OutlineHeading2, OutlineHeading3 } from "./heading-components"; +import { OutlineHeading1, OutlineHeading2, OutlineHeading3, THeadingComponentProps } from "./heading-components"; type Props = { + className?: string; + emptyState?: React.ReactNode; editorRef: EditorRefApi | null; setSidePeekVisible?: (sidePeekState: boolean) => void; showOutline?: boolean; }; export const PageContentBrowser: React.FC = (props) => { - const { editorRef, setSidePeekVisible, showOutline = false } = props; + const { className, editorRef, emptyState, setSidePeekVisible, showOutline = false } = props; // states const [headings, setHeadings] = useState([]); @@ -20,7 +23,7 @@ export const PageContentBrowser: React.FC = (props) => { // for initial render of this component to get the editor headings setHeadings(editorRef?.getHeadings() ?? []); return () => { - if (unsubscribe) unsubscribe(); + unsubscribe?.(); }; }, [editorRef]); @@ -33,15 +36,25 @@ export const PageContentBrowser: React.FC = (props) => { ); const HeadingComponent: { - [key: number]: React.FC<{ marking: IMarking; onClick: () => void }>; + [key: number]: React.FC; } = { 1: OutlineHeading1, 2: OutlineHeading2, 3: OutlineHeading3, }; + if (headings.length === 0) return emptyState ?? null; + return ( -
+
{headings.map((marking) => { const Component = HeadingComponent[marking.level]; if (!Component) return null; diff --git a/web/core/components/pages/editor/summary/heading-components.tsx b/web/core/components/pages/editor/summary/heading-components.tsx index c2e78dd67..d06eaded4 100644 --- a/web/core/components/pages/editor/summary/heading-components.tsx +++ b/web/core/components/pages/editor/summary/heading-components.tsx @@ -1,37 +1,29 @@ -// plane editor +// plane imports import type { IMarking } from "@plane/editor"; +import { cn } from "@plane/utils"; export type THeadingComponentProps = { marking: IMarking; onClick: (event: React.MouseEvent) => void; }; +const COMMON_CLASSNAME = + "w-full py-1 text-left font-medium text-custom-text-300 hover:text-custom-primary-100 truncate transition-colors"; + export const OutlineHeading1 = ({ marking, onClick }: THeadingComponentProps) => ( - ); export const OutlineHeading2 = ({ marking, onClick }: THeadingComponentProps) => ( - ); export const OutlineHeading3 = ({ marking, onClick }: THeadingComponentProps) => ( - ); diff --git a/web/core/components/pages/editor/toolbar/index.ts b/web/core/components/pages/editor/toolbar/index.ts index 66652b2db..2c36785bd 100644 --- a/web/core/components/pages/editor/toolbar/index.ts +++ b/web/core/components/pages/editor/toolbar/index.ts @@ -1,5 +1,4 @@ export * from "./color-dropdown"; -export * from "./info-popover"; export * from "./options-dropdown"; export * from "./root"; export * from "./toolbar"; diff --git a/web/core/components/pages/editor/toolbar/info-popover.tsx b/web/core/components/pages/editor/toolbar/info-popover.tsx deleted file mode 100644 index 49ad06b9b..000000000 --- a/web/core/components/pages/editor/toolbar/info-popover.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { useState } from "react"; -import { observer } from "mobx-react"; -import Link from "next/link"; -import { useParams } from "next/navigation"; -import { usePopper } from "react-popper"; -import { Info } from "lucide-react"; -// plane imports -import { Avatar } from "@plane/ui"; -import { calculateTimeAgoShort, getFileURL, getReadTimeFromWordsCount, renderFormattedDate } from "@plane/utils"; -// hooks -import { useMember } from "@/hooks/store"; -// store -import { TPageInstance } from "@/store/pages/base-page"; - -type Props = { - page: TPageInstance; -}; - -export const PageInfoPopover: React.FC = observer((props) => { - const { page } = props; - // states - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - // refs - const [referenceElement, setReferenceElement] = useState(null); - const [popperElement, setPopperElement] = useState(null); - // router - const { workspaceSlug } = useParams(); - // popper-js - const { styles: infoPopoverStyles, attributes: infoPopoverAttributes } = usePopper(referenceElement, popperElement, { - placement: "bottom-start", - }); - // store hooks - const { getUserDetails } = useMember(); - // derived values - const editorInformation = page.updated_by ? getUserDetails(page.updated_by) : undefined; - const creatorInformation = page.created_by ? getUserDetails(page.created_by) : undefined; - - const documentsInfo = page.editorRef?.getDocumentInfo() || { words: 0, characters: 0, paragraphs: 0 }; - - const secondsToReadableTime = () => { - const wordsCount = documentsInfo.words; - const readTimeInSeconds = Number(getReadTimeFromWordsCount(wordsCount).toFixed(0)); - return readTimeInSeconds < 60 ? `${readTimeInSeconds}s` : `${Math.ceil(readTimeInSeconds / 60)}m`; - }; - - const documentInfoCards = [ - { - key: "words-count", - title: "Words", - info: documentsInfo.words, - }, - { - key: "characters-count", - title: "Characters", - info: documentsInfo.characters, - }, - { - key: "paragraphs-count", - title: "Paragraphs", - info: documentsInfo.paragraphs, - }, - { - key: "read-time", - title: "Read time", - info: secondsToReadableTime(), - }, - ]; - - return ( -
setIsPopoverOpen(true)} - onMouseLeave={() => setIsPopoverOpen(false)} - > - - {isPopoverOpen && ( -
-
- {documentInfoCards.map((card) => ( -
-
{card.info}
-

{card.title}

-
- ))} -
-
-
-

Edited by

- - - - {editorInformation?.display_name}{" "} - {calculateTimeAgoShort(page.updated_at ?? "")} ago - - -
-
-

Created by

- - - - {creatorInformation?.display_name}{" "} - {renderFormattedDate(page.created_at)} - - -
-
-
- )} -
- ); -}); diff --git a/web/core/components/pages/editor/toolbar/options-dropdown.tsx b/web/core/components/pages/editor/toolbar/options-dropdown.tsx index 407ee03c4..8ff976571 100644 --- a/web/core/components/pages/editor/toolbar/options-dropdown.tsx +++ b/web/core/components/pages/editor/toolbar/options-dropdown.tsx @@ -2,18 +2,15 @@ import { useMemo, useState } from "react"; import { observer } from "mobx-react"; -import { useRouter } from "next/navigation"; -import { ArrowUpToLine, Clipboard, History } from "lucide-react"; +import { ArrowUpToLine, Clipboard } from "lucide-react"; // plane imports import { TContextMenuItem, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; -// components import { copyTextToClipboard } from "@plane/utils"; +// components import { ExportPageModal, PageActions, TPageActions } from "@/components/pages"; -// helpers // hooks import { usePageFilters } from "@/hooks/use-page-filters"; -import { useQueryParams } from "@/hooks/use-query-params"; -// plane web hooks +// plane web imports import { EPageStoreType } from "@/plane-web/hooks/store"; // store import { TPageInstance } from "@/store/pages/base-page"; @@ -27,14 +24,14 @@ export const PageOptionsDropdown: React.FC = observer((props) => { const { page, storeType } = props; // states const [isExportModalOpen, setIsExportModalOpen] = useState(false); - // router - const router = useRouter(); // store values - const { name, isContentEditable, editorRef } = page; + const { + name, + isContentEditable, + editor: { editorRef }, + } = page; // page filters const { isFullWidth, handleFullWidth, isStickyToolbarEnabled, handleStickyToolbar } = usePageFilters(); - // update query params - const { updateQueryParams } = useQueryParams(); // menu items list const EXTRA_MENU_OPTIONS: (TContextMenuItem & { key: TPageActions })[] = useMemo( () => [ @@ -77,19 +74,6 @@ export const PageOptionsDropdown: React.FC = observer((props) => { icon: Clipboard, shouldRender: true, }, - { - key: "version-history", - action: () => { - // add query param, version=current to the route - const updatedRoute = updateQueryParams({ - paramsToAdd: { version: "current" }, - }); - router.push(updatedRoute); - }, - title: "Version history", - icon: History, - shouldRender: true, - }, { key: "export", action: () => setIsExportModalOpen(true), @@ -98,16 +82,7 @@ export const PageOptionsDropdown: React.FC = observer((props) => { shouldRender: true, }, ], - [ - editorRef, - handleFullWidth, - handleStickyToolbar, - isContentEditable, - isFullWidth, - isStickyToolbarEnabled, - router, - updateQueryParams, - ] + [editorRef, handleFullWidth, handleStickyToolbar, isContentEditable, isFullWidth, isStickyToolbarEnabled] ); return ( diff --git a/web/core/components/pages/editor/toolbar/root.tsx b/web/core/components/pages/editor/toolbar/root.tsx index 72c9da3d4..e779c618e 100644 --- a/web/core/components/pages/editor/toolbar/root.tsx +++ b/web/core/components/pages/editor/toolbar/root.tsx @@ -1,6 +1,10 @@ import { observer } from "mobx-react"; -// components +import { PanelRight } from "lucide-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +import { Tooltip } from "@plane/ui"; import { cn } from "@plane/utils"; +// components import { PageToolbar } from "@/components/pages"; // helpers // hooks @@ -11,38 +15,74 @@ import { PageCollaboratorsList } from "@/plane-web/components/pages/header/colla import { TPageInstance } from "@/store/pages/base-page"; type Props = { + handleOpenNavigationPane: () => void; + isNavigationPaneOpen: boolean; page: TPageInstance; }; export const PageEditorToolbarRoot: React.FC = observer((props) => { - const { page } = props; + const { handleOpenNavigationPane, isNavigationPaneOpen, page } = props; + // translation + const { t } = useTranslation(); // derived values - const { isContentEditable, editorRef } = page; + const { + isContentEditable, + editor: { editorRef }, + } = page; // page filters const { isFullWidth, isStickyToolbarEnabled } = usePageFilters(); // derived values const shouldHideToolbar = !isStickyToolbarEnabled || !isContentEditable; return ( -
+ <>
-
- {editorRef && } - +
+
+ {editorRef && } +
+ + {!isNavigationPaneOpen && ( + + )} +
+
-
+ {shouldHideToolbar && ( +
+ {!isNavigationPaneOpen && ( + + + + )} +
+ )} + ); }); diff --git a/web/core/components/pages/header/actions.tsx b/web/core/components/pages/header/actions.tsx index 6c6cb2f6c..ccf07191a 100644 --- a/web/core/components/pages/header/actions.tsx +++ b/web/core/components/pages/header/actions.tsx @@ -2,7 +2,7 @@ import { observer } from "mobx-react"; // components -import { PageInfoPopover, PageOptionsDropdown } from "@/components/pages"; +import { PageOptionsDropdown } from "@/components/pages"; // plane web components import { PageLockControl } from "@/plane-web/components/pages/header/lock-control"; import { PageMoveControl } from "@/plane-web/components/pages/header/move-control"; @@ -31,7 +31,6 @@ export const PageHeaderActions: React.FC = observer((props) => { - diff --git a/web/core/components/pages/navigation-pane/index.ts b/web/core/components/pages/navigation-pane/index.ts new file mode 100644 index 000000000..520265106 --- /dev/null +++ b/web/core/components/pages/navigation-pane/index.ts @@ -0,0 +1,11 @@ +// plane web imports +import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane"; + +export * from "./root"; + +export const PAGE_NAVIGATION_PANE_WIDTH = 294; + +export const PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM = "sidebarTab"; +export const PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM = "version"; + +export const PAGE_NAVIGATION_PANE_TAB_KEYS = ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => tab.key); diff --git a/web/core/components/pages/navigation-pane/root.tsx b/web/core/components/pages/navigation-pane/root.tsx new file mode 100644 index 000000000..a2497d385 --- /dev/null +++ b/web/core/components/pages/navigation-pane/root.tsx @@ -0,0 +1,88 @@ +import React, { useCallback } from "react"; +import { observer } from "mobx-react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { ArrowRightCircle } from "lucide-react"; +import { Tab } from "@headlessui/react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +import { Tooltip } from "@plane/ui"; +// hooks +import { useQueryParams } from "@/hooks/use-query-params"; +// plane web components +import { TPageNavigationPaneTab } from "@/plane-web/components/pages/navigation-pane"; +// store +import { TPageInstance } from "@/store/pages/base-page"; +// local imports +import { TPageRootHandlers } from "../editor"; +import { PageNavigationPaneTabPanelsRoot } from "./tab-panels/root"; +import { PageNavigationPaneTabsList } from "./tabs-list"; +import { + PAGE_NAVIGATION_PANE_TAB_KEYS, + PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, + PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM, + PAGE_NAVIGATION_PANE_WIDTH, +} from "./index"; + +type Props = { + handleClose: () => void; + isNavigationPaneOpen: boolean; + page: TPageInstance; + versionHistory: Pick; +}; + +export const PageNavigationPaneRoot: React.FC = observer((props) => { + const { handleClose, isNavigationPaneOpen, page, versionHistory } = props; + // navigation + const router = useRouter(); + const searchParams = useSearchParams(); + // query params + const { updateQueryParams } = useQueryParams(); + // derived values + const navigationPaneQueryParam = searchParams.get( + PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM + ) as TPageNavigationPaneTab | null; + const activeTab: TPageNavigationPaneTab = navigationPaneQueryParam || "outline"; + const selectedIndex = PAGE_NAVIGATION_PANE_TAB_KEYS.indexOf(activeTab); + // translation + const { t } = useTranslation(); + + const handleTabChange = useCallback( + (index: number) => { + const updatedTab = PAGE_NAVIGATION_PANE_TAB_KEYS[index]; + const isUpdatedTabInfo = updatedTab === "info"; + const updatedRoute = updateQueryParams({ + paramsToAdd: { [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM]: updatedTab }, + paramsToRemove: !isUpdatedTabInfo ? [PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM] : undefined, + }); + router.push(updatedRoute); + }, + [router, updateQueryParams] + ); + + return ( + + ); +}); diff --git a/web/core/components/pages/navigation-pane/tab-panels/assets.tsx b/web/core/components/pages/navigation-pane/tab-panels/assets.tsx new file mode 100644 index 000000000..f770ae7b4 --- /dev/null +++ b/web/core/components/pages/navigation-pane/tab-panels/assets.tsx @@ -0,0 +1,109 @@ +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { Download } from "lucide-react"; +// plane imports +import { CORE_EXTENSIONS, type TEditorAsset } from "@plane/editor"; +import { useTranslation } from "@plane/i18n"; +import { getEditorAssetDownloadSrc, getEditorAssetSrc } from "@plane/utils"; +// plane web imports +import { AdditionalPageNavigationPaneAssetItem } from "@/plane-web/components/pages/navigation-pane/tab-panels/assets"; +import { PageNavigationPaneAssetsTabEmptyState } from "@/plane-web/components/pages/navigation-pane/tab-panels/empty-states/assets"; +// store +import { TPageInstance } from "@/store/pages/base-page"; + +type Props = { + page: TPageInstance; +}; + +type AssetItemProps = { + asset: TEditorAsset; + page: TPageInstance; +}; + +const AssetItem = observer((props: AssetItemProps) => { + const { asset, page } = props; + // navigation + const { workspaceSlug } = useParams(); + // derived values + const { project_ids } = page; + // translation + const { t } = useTranslation(); + + const getAssetSrc = (path: string) => { + if (!path || !workspaceSlug) return ""; + if (path.startsWith("http")) { + return path; + } else { + return ( + getEditorAssetSrc({ + assetId: path, + projectId: project_ids?.[0], + workspaceSlug: workspaceSlug.toString(), + }) ?? "" + ); + } + }; + + const getAssetDownloadSrc = (path: string) => { + if (!path || !workspaceSlug) return ""; + if (path.startsWith("http")) { + return path; + } else { + return ( + getEditorAssetDownloadSrc({ + assetId: path, + projectId: project_ids?.[0], + workspaceSlug: workspaceSlug.toString(), + }) ?? "" + ); + } + }; + + if ([CORE_EXTENSIONS.IMAGE, CORE_EXTENSIONS.CUSTOM_IMAGE].includes(asset.type)) + return ( + +
+ + + ); + + return ; +}); + +export const PageNavigationPaneAssetsTabPanel: React.FC = observer((props) => { + const { page } = props; + // derived values + const { + editor: { assetsList }, + } = page; + + if (assetsList.length === 0) return ; + + return ( +
+ {assetsList?.map((asset) => )} +
+ ); +}); diff --git a/web/core/components/pages/navigation-pane/tab-panels/info/actors-info.tsx b/web/core/components/pages/navigation-pane/tab-panels/info/actors-info.tsx new file mode 100644 index 000000000..d4c166ddf --- /dev/null +++ b/web/core/components/pages/navigation-pane/tab-panels/info/actors-info.tsx @@ -0,0 +1,68 @@ +import { observer } from "mobx-react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +// plane imports +import { useTranslation } from "@plane/i18n"; +import { Avatar } from "@plane/ui"; +import { calculateTimeAgoShort, getFileURL, renderFormattedDate } from "@plane/utils"; +// hooks +import { useMember } from "@/hooks/store"; +// store +import { TPageInstance } from "@/store/pages/base-page"; + +type Props = { + page: TPageInstance; +}; + +export const PageNavigationPaneInfoTabActorsInfo: React.FC = observer((props) => { + const { page } = props; + // navigation + const { workspaceSlug } = useParams(); + // store hooks + const { getUserDetails } = useMember(); + // derived values + const { created_by, updated_by } = page; + const editorInformation = updated_by ? getUserDetails(updated_by) : undefined; + const creatorInformation = created_by ? getUserDetails(created_by) : undefined; + // translation + const { t } = useTranslation(); + + return ( +
+
+

+ {t("page_navigation_pane.tabs.info.actors_info.edited_by")} +

+
+ + + {editorInformation?.display_name ?? t("common.deactivated_user")} + + {calculateTimeAgoShort(page.updated_at ?? "")} ago +
+
+
+

+ {t("page_navigation_pane.tabs.info.actors_info.created_by")} +

+
+ + + {creatorInformation?.display_name ?? t("common.deactivated_user")} + + {renderFormattedDate(page.created_at)} +
+
+
+ ); +}); diff --git a/web/core/components/pages/navigation-pane/tab-panels/info/document-info.tsx b/web/core/components/pages/navigation-pane/tab-panels/info/document-info.tsx new file mode 100644 index 000000000..b301e9cbe --- /dev/null +++ b/web/core/components/pages/navigation-pane/tab-panels/info/document-info.tsx @@ -0,0 +1,82 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { observer } from "mobx-react"; +// plane imports +import type { TDocumentInfo } from "@plane/editor"; +import { useTranslation } from "@plane/i18n"; +import { getReadTimeFromWordsCount } from "@plane/utils"; +// store +import { TPageInstance } from "@/store/pages/base-page"; + +type Props = { + page: TPageInstance; +}; + +const DEFAULT_DOCUMENT_INFO: TDocumentInfo = { + words: 0, + characters: 0, + paragraphs: 0, +}; + +export const PageNavigationPaneInfoTabDocumentInfo: React.FC = observer((props) => { + const { page } = props; + // states + const [documentInfo, setDocumentInfo] = useState(DEFAULT_DOCUMENT_INFO); + // derived values + const { + editor: { editorRef }, + } = page; + // translation + const { t } = useTranslation(); + // subscribe to asset changes + useEffect(() => { + const unsubscribe = editorRef?.onDocumentInfoChange(setDocumentInfo); + // for initial render of this component to get the editor assets + setDocumentInfo(editorRef?.getDocumentInfo() ?? DEFAULT_DOCUMENT_INFO); + return () => { + unsubscribe?.(); + }; + }, [editorRef]); + + const secondsToReadableTime = useCallback(() => { + const wordsCount = documentInfo.words; + const readTimeInSeconds = Number(getReadTimeFromWordsCount(wordsCount).toFixed(0)); + return readTimeInSeconds < 60 ? `${readTimeInSeconds}s` : `${Math.ceil(readTimeInSeconds / 60)}m`; + }, [documentInfo.words]); + + const documentInfoCards = useMemo( + () => [ + { + key: "words-count", + title: t("page_navigation_pane.tabs.info.document_info.words"), + info: documentInfo.words, + }, + { + key: "characters-count", + title: t("page_navigation_pane.tabs.info.document_info.characters"), + info: documentInfo.characters, + }, + { + key: "paragraphs-count", + title: t("page_navigation_pane.tabs.info.document_info.paragraphs"), + info: documentInfo.paragraphs, + }, + { + key: "read-time", + title: t("page_navigation_pane.tabs.info.document_info.read_time"), + info: secondsToReadableTime(), + }, + ], + [documentInfo, secondsToReadableTime, t] + ); + + return ( +
+ {documentInfoCards.map((card) => ( +
+
{card.info}
+

{card.title}

+
+ ))} +
+ ); +}); diff --git a/web/core/components/pages/navigation-pane/tab-panels/info/root.tsx b/web/core/components/pages/navigation-pane/tab-panels/info/root.tsx new file mode 100644 index 000000000..77edc24e0 --- /dev/null +++ b/web/core/components/pages/navigation-pane/tab-panels/info/root.tsx @@ -0,0 +1,27 @@ +import { observer } from "mobx-react"; +// components +import { TPageRootHandlers } from "@/components/pages/editor"; +// store +import { TPageInstance } from "@/store/pages/base-page"; +// local imports +import { PageNavigationPaneInfoTabActorsInfo } from "./actors-info"; +import { PageNavigationPaneInfoTabDocumentInfo } from "./document-info"; +import { PageNavigationPaneInfoTabVersionHistory } from "./version-history"; + +type Props = { + page: TPageInstance; + versionHistory: Pick; +}; + +export const PageNavigationPaneInfoTabPanel: React.FC = observer((props) => { + const { page, versionHistory } = props; + + return ( +
+ + +
+ +
+ ); +}); diff --git a/web/core/components/pages/navigation-pane/tab-panels/info/version-history.tsx b/web/core/components/pages/navigation-pane/tab-panels/info/version-history.tsx new file mode 100644 index 000000000..31069299f --- /dev/null +++ b/web/core/components/pages/navigation-pane/tab-panels/info/version-history.tsx @@ -0,0 +1,142 @@ +import { useCallback } from "react"; +import { observer } from "mobx-react"; +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; +import useSWR from "swr"; +// plane imports +import { useTranslation } from "@plane/i18n"; +import { TPageVersion } from "@plane/types"; +import { Avatar } from "@plane/ui"; +import { cn, getFileURL, renderFormattedDate, renderFormattedTime } from "@plane/utils"; +// components +import { TPageRootHandlers } from "@/components/pages/editor"; +// hooks +import { useMember } from "@/hooks/store"; +import { useQueryParams } from "@/hooks/use-query-params"; +// store +import { TPageInstance } from "@/store/pages/base-page"; +// local imports +import { PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM } from "../.."; + +type Props = { + page: TPageInstance; + versionHistory: Pick; +}; + +type VersionHistoryItemProps = { + getVersionLink: (versionID: string) => string; + isVersionActive: boolean; + version: TPageVersion; +}; + +const VersionHistoryItem = observer((props: VersionHistoryItemProps) => { + const { getVersionLink, isVersionActive, version } = props; + // store hooks + const { getUserDetails } = useMember(); + // derived values + const versionCreator = getUserDetails(version.created_by); + // translation + const { t } = useTranslation(); + + return ( +
  • + {/* timeline icon */} +
    +
    +
    + {/* end timeline icon */} + +

    + {renderFormattedDate(version.last_saved_at)}, {renderFormattedTime(version.last_saved_at)} +

    +

    + + {versionCreator?.display_name ?? t("common.deactivated_user")} +

    + +
  • + ); +}); + +export const PageNavigationPaneInfoTabVersionHistory: React.FC = observer((props) => { + const { page, versionHistory } = props; + // navigation + const searchParams = useSearchParams(); + const activeVersion = searchParams.get(PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM); + // derived values + const { id } = page; + // translation + const { t } = useTranslation(); + // query params + const { updateQueryParams } = useQueryParams(); + // fetch all versions + const { data: versionsList } = useSWR( + id ? `PAGE_VERSIONS_LIST_${id}` : null, + id ? () => versionHistory.fetchAllVersions(id) : null + ); + + const getVersionLink = useCallback( + (versionID?: string) => { + if (versionID) { + return updateQueryParams({ + paramsToAdd: { [PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM]: versionID }, + }); + } else { + return updateQueryParams({ + paramsToRemove: [PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM], + }); + } + }, + [updateQueryParams] + ); + + return ( +
    +

    + {t("page_navigation_pane.tabs.info.version_history.label")} +

    +
    +
      + {/* timeline line */} +
      +
      +
      + {/* end timeline line */} +
    • + {/* timeline icon */} +
      +
      +
      + {/* end timeline icon */} + + {t("page_navigation_pane.tabs.info.version_history.current_version")} + +
    • + {versionsList?.map((version) => ( + + ))} +
    +
    +
    + ); +}); diff --git a/web/core/components/pages/navigation-pane/tab-panels/outline.tsx b/web/core/components/pages/navigation-pane/tab-panels/outline.tsx new file mode 100644 index 000000000..d563e52eb --- /dev/null +++ b/web/core/components/pages/navigation-pane/tab-panels/outline.tsx @@ -0,0 +1,28 @@ +// plane web imports +import { PageNavigationPaneOutlineTabEmptyState } from "@/plane-web/components/pages/navigation-pane/tab-panels/empty-states/outline"; +// store +import { TPageInstance } from "@/store/pages/base-page"; +// local imports +import { PageContentBrowser } from "../../editor"; + +type Props = { + page: TPageInstance; +}; + +export const PageNavigationPaneOutlineTabPanel: React.FC = (props) => { + const { page } = props; + // derived values + const { + editor: { editorRef }, + } = page; + + return ( +
    + } + /> +
    + ); +}; diff --git a/web/core/components/pages/navigation-pane/tab-panels/root.tsx b/web/core/components/pages/navigation-pane/tab-panels/root.tsx new file mode 100644 index 000000000..c9880f0d5 --- /dev/null +++ b/web/core/components/pages/navigation-pane/tab-panels/root.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { Tab } from "@headlessui/react"; +// components +import { TPageRootHandlers } from "@/components/pages/editor"; +// plane web imports +import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane"; +import { PageNavigationPaneAdditionalTabPanelsRoot } from "@/plane-web/components/pages/navigation-pane/tab-panels/root"; +// store +import { TPageInstance } from "@/store/pages/base-page"; +// local imports +import { PageNavigationPaneAssetsTabPanel } from "./assets"; +import { PageNavigationPaneInfoTabPanel } from "./info/root"; +import { PageNavigationPaneOutlineTabPanel } from "./outline"; + +type Props = { + page: TPageInstance; + versionHistory: Pick; +}; + +export const PageNavigationPaneTabPanelsRoot: React.FC = (props) => { + const { page, versionHistory } = props; + + return ( + + {ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => ( + + {tab.key === "outline" && } + {tab.key === "info" && } + {tab.key === "assets" && } + + + ))} + + ); +}; diff --git a/web/core/components/pages/navigation-pane/tabs-list.tsx b/web/core/components/pages/navigation-pane/tabs-list.tsx new file mode 100644 index 000000000..bf4383216 --- /dev/null +++ b/web/core/components/pages/navigation-pane/tabs-list.tsx @@ -0,0 +1,37 @@ +import { Tab } from "@headlessui/react"; +// plane imports +import { useTranslation } from "@plane/i18n"; +// plane web components +import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane"; + +export const PageNavigationPaneTabsList = () => { + // translation + const { t } = useTranslation(); + + return ( + + {({ selectedIndex }) => ( + <> + {ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => ( + + {t(tab.i18n_label)} + + ))} + {/* active tab indicator */} +
    + + )} + + ); +}; diff --git a/web/core/components/pages/version/editor.tsx b/web/core/components/pages/version/editor.tsx index 8491408ac..1a2c23e28 100644 --- a/web/core/components/pages/version/editor.tsx +++ b/web/core/components/pages/version/editor.tsx @@ -16,13 +16,11 @@ import { useIssueEmbed } from "@/plane-web/hooks/use-issue-embed"; export type TVersionEditorProps = { activeVersion: string | null; - currentVersionDescription: string | null; - isCurrentVersionActive: boolean; versionDetails: TPageVersion | undefined; }; export const PagesVersionEditor: React.FC = observer((props) => { - const { activeVersion, currentVersionDescription, isCurrentVersionActive, versionDetails } = props; + const { activeVersion, versionDetails } = props; // store hooks const { getUserDetails } = useMember(); // params @@ -49,7 +47,7 @@ export const PagesVersionEditor: React.FC = observer((props wideLayout: true, }; - if (!isCurrentVersionActive && !versionDetails) + if (!versionDetails) return (
    @@ -91,7 +89,7 @@ export const PagesVersionEditor: React.FC = observer((props
    ); - const description = isCurrentVersionActive ? currentVersionDescription : versionDetails?.description_html; + const description = versionDetails?.description_html; if (description === undefined || description?.trim() === "") return null; return ( diff --git a/web/core/components/pages/version/index.ts b/web/core/components/pages/version/index.ts index 8e04e4de9..5da43e959 100644 --- a/web/core/components/pages/version/index.ts +++ b/web/core/components/pages/version/index.ts @@ -1,6 +1,3 @@ export * from "./editor"; export * from "./main-content"; export * from "./root"; -export * from "./sidebar-list-item"; -export * from "./sidebar-list"; -export * from "./sidebar-root"; diff --git a/web/core/components/pages/version/main-content.tsx b/web/core/components/pages/version/main-content.tsx index e94bfefa7..ab05ba256 100644 --- a/web/core/components/pages/version/main-content.tsx +++ b/web/core/components/pages/version/main-content.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { observer } from "mobx-react"; import useSWR from "swr"; -import { TriangleAlert } from "lucide-react"; +import { EyeIcon, TriangleAlert } from "lucide-react"; // plane types import { TPageVersion } from "@plane/types"; // plane ui @@ -13,7 +13,6 @@ import { TVersionEditorProps } from "@/components/pages"; type Props = { activeVersion: string | null; - currentVersionDescription: string | null; editorComponent: React.FC; fetchVersionDetails: (pageId: string, versionId: string) => Promise; handleClose: () => void; @@ -23,16 +22,8 @@ type Props = { }; export const PageVersionsMainContent: React.FC = observer((props) => { - const { - activeVersion, - currentVersionDescription, - editorComponent, - fetchVersionDetails, - handleClose, - handleRestore, - pageId, - restoreEnabled, - } = props; + const { activeVersion, editorComponent, fetchVersionDetails, handleClose, handleRestore, pageId, restoreEnabled } = + props; // states const [isRestoring, setIsRestoring] = useState(false); const [isRetrying, setIsRetrying] = useState(false); @@ -42,12 +33,10 @@ export const PageVersionsMainContent: React.FC = observer((props) => { error: versionDetailsError, mutate: mutateVersionDetails, } = useSWR( - pageId && activeVersion && activeVersion !== "current" ? `PAGE_VERSION_${activeVersion}` : null, - pageId && activeVersion && activeVersion !== "current" ? () => fetchVersionDetails(pageId, activeVersion) : null + pageId && activeVersion ? `PAGE_VERSION_${activeVersion}` : null, + pageId && activeVersion ? () => fetchVersionDetails(pageId, activeVersion) : null ); - const isCurrentVersionActive = activeVersion === "current"; - const handleRestoreVersion = async () => { if (!restoreEnabled) return; setIsRestoring(true); @@ -96,14 +85,18 @@ export const PageVersionsMainContent: React.FC = observer((props) => { ) : ( <>
    -
    - {isCurrentVersionActive - ? "Current version" - : versionDetails +
    +
    + {versionDetails ? `${renderFormattedDate(versionDetails.last_saved_at)} ${renderFormattedTime(versionDetails.last_saved_at)}` : "Loading version details"} -
    - {!isCurrentVersionActive && restoreEnabled && ( +
    + + + View only + +
    + {restoreEnabled && (
    - +
    )} diff --git a/web/core/components/pages/version/root.tsx b/web/core/components/pages/version/root.tsx index f1dd0248b..64b4f43da 100644 --- a/web/core/components/pages/version/root.tsx +++ b/web/core/components/pages/version/root.tsx @@ -1,54 +1,56 @@ +import { useCallback } from "react"; import { observer } from "mobx-react"; -// plane types +import { useRouter, useSearchParams } from "next/navigation"; +// plane imports import { TPageVersion } from "@plane/types"; -// components import { cn } from "@plane/utils"; -import { PageVersionsMainContent, PageVersionsSidebarRoot, TVersionEditorProps } from "@/components/pages"; -// helpers +// components +import { PageVersionsMainContent, TVersionEditorProps } from "@/components/pages"; +// hooks +import { useQueryParams } from "@/hooks/use-query-params"; +// local imports +import { PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM, PAGE_NAVIGATION_PANE_WIDTH } from "../navigation-pane"; type Props = { - activeVersion: string | null; - currentVersionDescription: string | null; editorComponent: React.FC; - fetchAllVersions: (pageId: string) => Promise; fetchVersionDetails: (pageId: string, versionId: string) => Promise; handleRestore: (descriptionHTML: string) => Promise; - isOpen: boolean; - onClose: () => void; pageId: string; restoreEnabled: boolean; }; export const PageVersionsOverlay: React.FC = observer((props) => { - const { - activeVersion, - currentVersionDescription, - editorComponent, - fetchAllVersions, - fetchVersionDetails, - handleRestore, - isOpen, - onClose, - pageId, - restoreEnabled, - } = props; + const { editorComponent, fetchVersionDetails, handleRestore, pageId, restoreEnabled } = props; + // navigation + const router = useRouter(); + const searchParams = useSearchParams(); + // query params + const { updateQueryParams } = useQueryParams(); + // derived values + const activeVersion = searchParams.get(PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM); + const isOpen = !!activeVersion; - const handleClose = () => { - onClose(); - }; + const handleClose = useCallback(() => { + const updatedRoute = updateQueryParams({ + paramsToRemove: [PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM], + }); + router.push(updatedRoute); + }, [router, updateQueryParams]); return (
    = observer((props) => { pageId={pageId} restoreEnabled={restoreEnabled} /> -
    ); }); diff --git a/web/core/components/pages/version/sidebar-list-item.tsx b/web/core/components/pages/version/sidebar-list-item.tsx deleted file mode 100644 index c5df2c0d2..000000000 --- a/web/core/components/pages/version/sidebar-list-item.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { observer } from "mobx-react"; -import Link from "next/link"; -// plane imports -import { useTranslation } from "@plane/i18n"; -import { TPageVersion } from "@plane/types"; -import { Avatar } from "@plane/ui"; -import { cn, renderFormattedDate, renderFormattedTime, getFileURL } from "@plane/utils"; -// helpers -// hooks -import { useMember } from "@/hooks/store"; - -type Props = { - href: string; - isActive: boolean; - version: TPageVersion; -}; - -export const PlaneVersionsSidebarListItem: React.FC = observer((props) => { - const { href, isActive, version } = props; - // store hooks - const { getUserDetails } = useMember(); - // derived values - const ownerDetails = getUserDetails(version.owned_by); - // translation - const { t } = useTranslation(); - - return ( - -

    - {renderFormattedDate(version.last_saved_at)} {renderFormattedTime(version.last_saved_at)} -

    -

    - - {ownerDetails?.display_name ?? t("common.deactivated_user")} -

    - - ); -}); diff --git a/web/core/components/pages/version/sidebar-list.tsx b/web/core/components/pages/version/sidebar-list.tsx deleted file mode 100644 index bff9c3698..000000000 --- a/web/core/components/pages/version/sidebar-list.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useState } from "react"; -import Link from "next/link"; -import useSWR from "swr"; -import { TriangleAlert } from "lucide-react"; -// plane types -import { TPageVersion } from "@plane/types"; -// plane ui -import { Button, Loader } from "@plane/ui"; -// components -import { cn } from "@plane/utils"; -import { PlaneVersionsSidebarListItem } from "@/components/pages"; -// helpers -// hooks -import { useQueryParams } from "@/hooks/use-query-params"; - -type Props = { - activeVersion: string | null; - fetchAllVersions: (pageId: string) => Promise; - isOpen: boolean; - pageId: string; -}; - -export const PageVersionsSidebarList: React.FC = (props) => { - const { activeVersion, fetchAllVersions, isOpen, pageId } = props; - // states - const [isRetrying, setIsRetrying] = useState(false); - // update query params - const { updateQueryParams } = useQueryParams(); - - const { - data: versionsList, - error: versionsListError, - mutate: mutateVersionsList, - } = useSWR( - pageId && isOpen ? `PAGE_VERSIONS_LIST_${pageId}` : null, - pageId && isOpen ? () => fetchAllVersions(pageId) : null - ); - - const handleRetry = async () => { - setIsRetrying(true); - await mutateVersionsList(); - setIsRetrying(false); - }; - - const getVersionLink = (versionID: string) => - updateQueryParams({ - paramsToAdd: { version: versionID }, - }); - - return ( -
    - -

    Current version

    - - {versionsListError ? ( -
    -
    - - - -
    -
    Something went wrong!
    -

    - There was a problem while loading previous -
    - versions, please try again. -

    -
    - -
    -
    - ) : versionsList ? ( - versionsList.map((version) => ( - - )) - ) : ( - - - - - - - - )} -
    - ); -}; diff --git a/web/core/components/pages/version/sidebar-root.tsx b/web/core/components/pages/version/sidebar-root.tsx deleted file mode 100644 index 793d7fed9..000000000 --- a/web/core/components/pages/version/sidebar-root.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { X } from "lucide-react"; -// plane types -import { TPageVersion } from "@plane/types"; -// components -import { PageVersionsSidebarList } from "@/components/pages"; - -type Props = { - activeVersion: string | null; - fetchAllVersions: (pageId: string) => Promise; - handleClose: () => void; - isOpen: boolean; - pageId: string; -}; - -export const PageVersionsSidebarRoot: React.FC = (props) => { - const { activeVersion, fetchAllVersions, handleClose, isOpen, pageId } = props; - - return ( -
    -
    -
    Version history
    - -
    - -
    - ); -}; diff --git a/web/core/hooks/use-query-params.ts b/web/core/hooks/use-query-params.ts index 8b689f0cb..84d65bebb 100644 --- a/web/core/hooks/use-query-params.ts +++ b/web/core/hooks/use-query-params.ts @@ -1,3 +1,4 @@ +import { useCallback } from "react"; import { useSearchParams, usePathname } from "next/navigation"; type TParamsToAdd = { @@ -9,29 +10,27 @@ export const useQueryParams = () => { const searchParams = useSearchParams(); const pathname = usePathname(); - const updateQueryParams = ({ - paramsToAdd = {}, - paramsToRemove = [], - }: { - paramsToAdd?: TParamsToAdd; - paramsToRemove?: string[]; - }) => { - const currentParams = new URLSearchParams(searchParams.toString()); + const updateQueryParams = useCallback( + ({ paramsToAdd = {}, paramsToRemove = [] }: { paramsToAdd?: TParamsToAdd; paramsToRemove?: string[] }) => { + const currentParams = new URLSearchParams(searchParams.toString()); - // add or update query parameters - Object.keys(paramsToAdd).forEach((key) => { - currentParams.set(key, paramsToAdd[key]); - }); + // add or update query parameters + Object.keys(paramsToAdd).forEach((key) => { + currentParams.set(key, paramsToAdd[key]); + }); - // remove specified query parameters - paramsToRemove.forEach((key) => { - currentParams.delete(key); - }); + // remove specified query parameters + paramsToRemove.forEach((key) => { + currentParams.delete(key); + }); - // construct the new route with the updated query parameters - const newRoute = `${pathname}?${currentParams.toString()}`; - return newRoute; - }; + // construct the new route with the updated query parameters + const query = currentParams.toString(); + const newRoute = query ? `${pathname}?${query}` : pathname; + return newRoute; + }, + [pathname, searchParams] + ); return { updateQueryParams, diff --git a/web/core/store/pages/base-page.ts b/web/core/store/pages/base-page.ts index 294c370e7..e5ca4e4d5 100644 --- a/web/core/store/pages/base-page.ts +++ b/web/core/store/pages/base-page.ts @@ -2,18 +2,18 @@ import set from "lodash/set"; import { action, computed, makeObservable, observable, reaction, runInAction } from "mobx"; // plane imports import { EPageAccess } from "@plane/constants"; -import { EditorRefApi } from "@plane/editor"; import { TDocumentPayload, TLogoProps, TNameDescriptionLoader, TPage } from "@plane/types"; import { TChangeHandlerProps } from "@plane/ui"; import { convertHexEmojiToDecimal } from "@plane/utils"; // plane web store import { ExtendedBasePage } from "@/plane-web/store/pages/extended-base-page"; import { RootStore } from "@/plane-web/store/root.store"; +// local imports +import { PageEditorInstance } from "./page-editor-info"; export type TBasePage = TPage & { // observables isSubmitting: TNameDescriptionLoader; - editorRef: EditorRefApi | null; // computed asJSON: TPage | undefined; isCurrentUserOwner: boolean; @@ -36,7 +36,8 @@ export type TBasePage = TPage & { removePageFromFavorites: () => Promise; duplicate: () => Promise; mutateProperties: (data: Partial, shouldUpdateName?: boolean) => void; - setEditorRef: (editorRef: EditorRefApi | null) => void; + // sub-store + editor: PageEditorInstance; }; export type TBasePagePermissions = { @@ -73,7 +74,6 @@ export type TPageInstance = TBasePage & export class BasePage extends ExtendedBasePage implements TBasePage { // loaders isSubmitting: TNameDescriptionLoader = "saved"; - editorRef: EditorRefApi | null = null; // page properties id: string | undefined; name: string | undefined; @@ -100,6 +100,9 @@ export class BasePage extends ExtendedBasePage implements TBasePage { disposers: Array<() => void> = []; // root store rootStore: RootStore; + // sub-store + editor: PageEditorInstance; + constructor( private store: RootStore, page: TPage, @@ -129,7 +132,6 @@ export class BasePage extends ExtendedBasePage implements TBasePage { makeObservable(this, { // loaders isSubmitting: observable.ref, - editorRef: observable.ref, // page properties id: observable.ref, name: observable.ref, @@ -170,11 +172,12 @@ export class BasePage extends ExtendedBasePage implements TBasePage { removePageFromFavorites: action, duplicate: action, mutateProperties: action, - setEditorRef: action, }); - this.rootStore = store; + // init this.services = services; + this.rootStore = store; + this.editor = new PageEditorInstance(); const titleDisposer = reaction( () => this.name, @@ -524,10 +527,4 @@ export class BasePage extends ExtendedBasePage implements TBasePage { set(this, key, value); }); }; - - setEditorRef = (editorRef: EditorRefApi | null) => { - runInAction(() => { - this.editorRef = editorRef; - }); - }; } diff --git a/web/core/store/pages/page-editor-info.ts b/web/core/store/pages/page-editor-info.ts new file mode 100644 index 000000000..442b534f3 --- /dev/null +++ b/web/core/store/pages/page-editor-info.ts @@ -0,0 +1,41 @@ +import { action, makeObservable, observable, runInAction } from "mobx"; +// plane imports +import { EditorRefApi, TEditorAsset } from "@plane/editor"; + +export type TPageEditorInstance = { + // observables + assetsList: TEditorAsset[]; + editorRef: EditorRefApi | null; + // actions + setEditorRef: (editorRef: EditorRefApi | null) => void; + updateAssetsList: (assets: TEditorAsset[]) => void; +}; + +export class PageEditorInstance implements TPageEditorInstance { + // observables + editorRef: EditorRefApi | null = null; + assetsList: TEditorAsset[] = []; + + constructor() { + makeObservable(this, { + // observables + editorRef: observable.ref, + assetsList: observable, + // actions + setEditorRef: action, + updateAssetsList: action, + }); + } + + setEditorRef: TPageEditorInstance["setEditorRef"] = (editorRef) => { + runInAction(() => { + this.editorRef = editorRef; + }); + }; + + updateAssetsList: TPageEditorInstance["updateAssetsList"] = (assets) => { + runInAction(() => { + this.assetsList = assets; + }); + }; +} diff --git a/web/public/empty-state/pages/navigation-pane/assets-dark.webp b/web/public/empty-state/pages/navigation-pane/assets-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..e454d6dc17d77d199edb1cb18bf0e097fa5e1fab GIT binary patch literal 21086 zcmV)>K!d+hNk&FiQUCx~MM6+kP&il$0000G0000%0RUwI06|PpNT@vk009{XZ6rC8 zs(13-@h2SqAw=|l0{H*HL;yLtYZD-!fSnLZ&vSP2Lg5@f0-!P$8jVwGtJqx9Af~fR%9~IA|L^}_QZ|r7M~8!q15l9B z1Om9|atSE|#sJa4CINe4VY}^7Lq~;xf3-HK zknr&FsaC-1q0CNl6^{d=4pe+GfPp*VSis3*V5=3g-D+45)01<_b)5?Xt6?PIWE>C; zjHS_VDq=MXsB$%9VH6+(1GzsPl8)VInBZ1;bg{CQT!luVt*j%BwhCYDN5RTE z(vhpsXhIc^T!l|Du0o@Q?^Yqp)ppNfT!mIsoS-XD6>{aRLe?oxxZ16P)#Y2=Q;aTF zK2@-?7As#EU946k9k~jPF7~5fWsNktRmi&JDl~ExvQBZf3XLvSA#1Df6yqvnO?9!d ztMDntRcJ+{iFweS7a{HlJ|Px;!{ zXfaKr(KLwP+qb{{%3ul?$N@TdIX_crH-=Gu>v>(V^^_BX%zyZi3H_B#o_ z_t*4;zlQH^E%(pvm+PWwuHXFj@9ew3`N7+Kv!nmcufg(;>)CNx78c9;^6S5cuYdca zcJ#-6^wDA&v6h7imK`!JYcd22)B1gU{kQVXkJr&R`!qHq5!Qs!vi%uGEY`9XYZ`sN zSVoN2zWn-c?du<@qd%_C;)5YzfYzYVXo$IOw1o!Ewb9)7$M~M#$TvSmn?LoNJrct~ zxL8=H=gqZwzMQ}7KmMLCKRl28%#L2i_=V}od#=l3xvuqSEiCxyzm4zt$ZY=9K8Z&{ zgkXdiS(Y`L4vT47i{*NQ5!PZZmc^I9fuH_S+59{Hj+>25Bp49Ob-`j9IWKEX%V>>Q z7Rwr$*0TJ4zl-14mmiQj{Ek2WIxv7?(0~kDhDM`ljt$N6ay^U&&G9n)!$0lI55^rn zm!F?u14cr>$qv)R+_uAggQdA|XqML4YxLP~>!*GBfw;r3_UIgh%R8=huukjp&dVE{ z^Zw!A(3c;Cb)V_)_(8L%u^BB!3v0o$Mn+gqMr$qCwU)&)GA+NIzvjsA6|nB-_)Ioq za72XIixaHTa$P33ixCs+&{__YWeA65T7TcWdD0c%$N43E*hoNPjD%h=;AjwoOoMi5 z4Xw527;74u+eXtMMmh2ed^an)88I4U?)Mrp&24LFjxDAo z@Kye5C#~4leSe?vI))9SgO0jCVX@a}Sl+zXJY37+{lB0;Y=w5G*YaV3iII_!$Z&*Z zuN}}Xmh)PeEYsnA*ZV|*-}>+Pb*#YFeU9&uNDv4TBLN{22|_ZhWi4wRXoR(3TGl$O z2h(INqqVHbF_O3c1%JmYtvmg5$030x2oPxl(gx6IG}qQzjGX7bIcC}+YqW;u+FJ9_ z;Lgwd>s(Q-`#U}(oCG8o)AVi93^z|3jn*v9ZF6i|yAgv+qe0WOh%p9uu@_rG9rtB^ zQ~;5MMj|ZioL#Q1&%ATl(r5U0>}OF8Bc0I1Xjxb+)?&{V7R$0| z8q0AV)`NQk#17lOkS}>fwEyq;4Cu#EkqEIv1UeSebXm@mwH7R_<-8UPCfBtrYhhg= zMhFtT$xry5tb7jpia$((F<^xB1R~vl5g^X7wP3BwWMMK*(`cGlu$IYMBZjQe46VdS zIO&t!xzhRAzl?Q8h(PEYz!;1K(+Ng)a*WAZli|{w+XX`g&BKh=!ZZY841nRJ&-1HU z*?jh&9S6Mu3$+Ji*X8L5miu*K!Ng*D z7#oNTe-oen&XvhwU;gC~kl{{1fEbAoiNqkGwXhb;2-7mzP0Mj)oisEVtp%gCL?Xdp zOh>+rPhsauwees4o4>3b-X5KF z6S{=GeD)5SCWm%em-A|;dD!9p^f2or;urUO`U8A*@n8JhV7*~2?>OeXV3+lV+YZmy z<#~sNiFJ6-SS;t?)en6&@$37!E{ktA&5|e;_tT)WKE1}$sem=JPIBB))w`LF(=mBFv) zFV8IJc40a0)j>i#(cOa9E?cfm)*IRh3+*N$A#d$d-rgbq=Qq6)_!aydU*z)kwGLnC z@_bpB3+3|#bQmC zWntYdEv>cIx~yfb8(I(6zU!~>^yTjIfB1*FEKHW=yjT+&A=O$~Fb9NXU4~c_j3yRL ztgJOOLnH@@E^9){gsJ&BU(W93?ebs$mq&(%*3b@_+eWzzsW~>8mN_xmTa)QVZgUwzbDL>s9yiT>=C-va8hhbz z&3!n%Irn~>U)FN=xqX`4uESfGuhi9GS#BH6xgFj;A>U!Y_OD_2`t#nq3Cmg*)`RO> zrqg<`tYuADZr9|tVZnN^EY|6ES{93i$+|2Srs)~C{Q8!wkN=;ezhJpe&m;;$L8sAZS(Y_Hwbszk5;ZifL@n~TDS&zLKk&tuqy4Y-T-pYiYs-)|nOX?K z;hGvWt+~cr<1^QWc0<#+jHXWyqb4o6rg_{Yy4*j`Pquy;`kekchMVQa7e4Q}ym!NL zzZ~15HODOEVU9tLA@^w>e!S6Rg=I0pay!9tUZyoI%etVtNV$%f zWG%ER|LpwWU%J2`h*%QTsbM%G#wEu&>x zLuH5{c`%~2c8Q1q^DV!p^~=il_x&NkXs%78Wze*yWST{;IX1*-uB|lK2&A(FS}VZKlq z`9*(Q%g3HC_p}Buu5Gz44;RgK^+1PpSk8+^JFG@?ZjNg@EXR-0SLYh9e~m;2>*TH11*EY_p*nk=OCWO=$SvM`-M8lLQHSuQ@t zn*;*_36^PE3l?iMS+GVf(=Ll?nJ}6x%d%XDu$Cb#EG!AJ7MhmP2t%#4!&(vy(nS8b zW0r}p;zJ7p#6ZhrGL6>IvO=Rp4Uz_7!5Ud>u32lDOoLpPWihQe2DuiIYLba2*Ib(r zK{VpG{UvQ#7Jh?27cgJ|4VujR=gmz>*agjP)0!qmL!pu3z@owHzG# z#{h=NLE9eW+JnVhTcf)-HfY|s#j zICMGwXuq6a$}j1k{>D3eW&R2N+CR<c|7~Jqfs4@y z%d$qS$u(<@=9=YO|I~lzJM=&DSG`!M$h7XnT6b&7v~WO(05*KSPd@Wr?1L0zgmhY3 zB#B8xiP3-nVh}|HS@RM6hrWY)2{^-N_i1pyj^E}dx z0&>EK#4J&4%Rcrm%XkZQb&DaDZ1Oy{+*s;t`6+y%_ z;3y9k(=vG)BQb(`@fTY=!*2T#1~H(DL=$T?jc6U#kYGTJVgMr_@&(LPEvhKBM601$ zu4^K6K`e*_0j0rPepR#U*nd*NpahMEGcq}7hCz7)+ zW&EGyXIn<=9Sjjngsqzwiw0FxOO=MjvX;fVWIc-k3{<%5TbWJA{%RT(h!P`OnykfI zlQo@)y(mGDaNL?vf4o23U+cI$ST{ks5%^F);>AO&5JW+wI$9!Hme>ptAtHhZh%f$6 zZkb7+&ixDqz!+N7C?a!Bqt+4(5(5Un0Qa88{I$QoKV6flMTVLTl>nxK;h+^ts~kx? ztqGZHLIjMof&q|g{0cMav)vw>y z)~Iji-~|;H2t`vUtwFUA(F#mMUyOoT`&e6M&?o!}ReFn=bBAM;ELdsR96aW){e?7Y zEV9?6Ss);QAmh(|f|C~lN@vJ2JGE3Atw7L?8~t&NF@yv**!%Mu2GOMcQdW z(-Z3qF(Q4D;GR*X#S1T%>kHF_C=@nvlpgbP7lbOdB}LsxI!r2BBe9{0ksd$c*)!*_ zJ0Tz_NQ?vtYC*`5(Hh1W31Bb+EI@K?RBOmeh(fRcigEblE(k=WwKR4Sqe07B6~=S| zf&q*?-8E*-!JmUDt$?8w9h+;KplU$`34Q5~Q%locqms;P1QfA|0dW-SVt}ZYrD&C! z6bYKM0R#{Rz^whzw#=9lKQ7m0L=bmWl$u?YwfV=x%}Gyboc@*8YNK!O1wV8B61rIH*IN(`XV8ez|+V_vkT)x1_FC5Z;9 zfRb_)PzC5^3jwSWyIP_(X-G-HAc7IVfS_*r|7XcZ{f{$JMjMBDm~+koD6I_cA0_g# zh_vW6wFChKgN#>zDy_v38pkGYthpDELFpQJb`Sk{9Xn@FTD>>s5?uq=yZ zfe~VZM8_dmsOW-F8JPP)G)ZU;R%r zE6)0FfXHY^oEy1a$i0XI4jns2H0PGvU2ct`b=#=J?kS>D*e(dRH1p2uG}3{{ptt>Q zXT-_ItXZom)I#k7PX?Da3|n z`(|gvb&n9D1W6-+KwPKeG)z^h|=y~z_Km`i^&Lx49B;yW6cOYx?4Q?fU-E- zo?u~GaG6Fd2?8`0dG4Ywo+GQ`e52D94REokcbnFtmmq_dRNrGLopdP0k@L=Pt*H-^@(dc+EYG zi~!*v%@;2m_ux^F(+e(OfrjCu9=U3HI&Y8VVOWQpf5uGM`9ED@3`hWp4n;bw3CouC zrr1Ov67MkTFFqZc!PvwFM2J1EJZ-6ng~f7Rcgyi~UHtf&aQ4%Sil9hWAS&%b#fr2n z%OVm%h=36ocbn4%h$CVQU<@{GjGaS2fivYT_&bwA{;*h>Mh&>kfs$7 zLu;I9G7TCTv;v3$M5TMdsGsX}gTY_`3{3tq~!T z$6bb;2VHQ=40xOmZx0Y@Ls)o)dCwp$T;TXqM{IhZj5I8|lZ-y|`p2&_3nTBHKJ(T* zz26Mj`M^U#0b&bCj6jUJ&WknOEC>$89uZDFe(2V3tBDO@AP&R`0qI8rhU0H^!K&gS z=(NPd(x@j3>uz_L0ms~!Agw?kfC!+HQA0XRvRoq&L1K&u$KPN>Z}nFT5ePzr2t;CJ zd=Q8b<3n$B{3^0wn#EdBT4C*(F6-o| zzv;Q(*>lqxR!Q@*9x-a(+`K*VG>_{JOYgMpUAk_NfOHj=euN+-7BN{XM1aH|5+nT) zaRJ+}yp)A+dX;O51Cg!)$03mgc;ZcKI>>dgLaTN0xTW{>5n>ZCNH8Kuv!*2&A&sVp zfRTuRfH6iQ0Xi{Axb({HEb2G#atR0#0zznFFxbRmAw$c=nkGz3+1;8JglKZjl3VjX z8CoMsUqC{)q>M%$79ha}!~jSug27;n1mhdLK8yX*pOF}Z&>D=O6$&5<#!*zMsYTET zji}2~moAGMe4s}ywd0PXD*{Ml*xtkBY2GNq(#oJ={0S$r`1ij*7>1((sGu=M#K`EP zIX1`0Id5z<$EHEnEVVV~8fhbdM!;s_&2=4E>q&;j+JG7J22bRmy!Pv6F@u0qq!mYX zI~>>Lafjq_t2tUnExFT>R_Q7S{hd7N#-SkH2oQS*|0oP^`4&#vw}lN?UHS&M4{dp} zwXqXN6ob-@jP~+8*L9k=uFJ^6f;CI+s-uKR3_{GPAQPs=gwbRggCmegj8Oe$5_7_hxA!>v<^%+SK_Ubp0z^_QFgtX^gqF1?Cd<-DBn!Q2sjb-@L=+jBoceUU=U(&&%?IUU;jSOi;)!qhTd#5%0C zOjvoi&~)R)OU=uo(MTi)jEWjN6G}qqvd|?qQG!4aqks^b{rB2KfBkdpGZ^Hw3pO0{ z%|#;I{q7y!_4)_|42B{UZ6pP8F{E9VEYqTDS(e3`>t%?z)VK_!FRgTqfN^Y^+gwu= z06Ss4Vc6_>*d7L7>*)~MyWbr+<2D$>vDfXr*7J}6NF*SDpb?OT2u<=l=UkI>m)n}M z)GtO58$cE)2q{XJhXXQJw5NcLfj!sV!{FEa4h`MAKbZFCdV&CK{j_`cc{qkPVi+qL zgD&KL(t}(>7ipK=R)M;BsmUV75eI!@(F3Fb_S|d_gV#DE(tYrQ=|22T(S47-Yn?$G zSzM7Yz6U~g$2sTeI^Bx1AdU$L5r_~U?R8nKk!28x#DUlW!3f##FYjUST2Byx$aNk} z|Cs+U5%~Uk59mQ85^O@n(5-RCGR@nUSj@6cFP4>s&IBV6BcKrhB!bqud)}qpoN8P)kLW!P8Dwg&&*l$9tM6s+3e&)@!4~>KP=7#afuq5|Y*;L`;56sXz=y z3|#{vDvi=eNIAtI!AJ}W(i%|4ZWTp!3&ZCi>Ua5yd!N)*9YheR_FAvGHhCUF2%;tl zilvEIYt6O&7@?Yg0J2bt4eD_AxBx+nV2}U;24Eb5r5s>*>-Lh8ORnAflr}W|Y9#Sm zYx35+V?*AtHRN$2W$6uf78pUWeDQ)|F(d3Ap_ZaDUQzGAyZ4W)mRe%#+DMYU?HGBw zbQ3|2>o68L0|Z%cBuclen=Z@3bQ4)b1%an!Sj7GahwE3Md(?eZRP572ti6WzhJ`Vk z)@8{x=V`7h#}_V}0jIAqM2KSqfk;R#CW}IpOaKdsF$n~o_T&-e;34bBFmTE4-UH8V zP^Br?g}uBMym~~I=;|`LU5C#LzU2hIEZYLTigW z?VTA`K_HA&TG_px{$nGGBHfCt)vCd>7fquV*Ja(jeSx^t_MImE*ozo} z&^%rqE{jD3S#adaD;ZU`jNPCp>b4R0Q$!UN=)Bx7Uf$TnbXnFjtlTcF2f4a_sd+Ad z0gRCd2%+CD8M2lsITmA(AjB9DxbjK{_bVt10b69`R#6KrwQSKE4XWs|u=&R0WZ-TUDf#wgj~0 z#nP&k-U1n=MJuW~x0Wo`(o!wO150i7H30-L2JIk#4Y>_Dz0qJ$1OcvpeFhx_8Q!{1 zExTh7QLx1>HZzu2MfB9S?MPCdANS5op&KJG7_6ObX~BlgG`qD2+|d9^$9Y# z?NQS~9fL?YZrxT<=+KljdGmUv!AEiKu;9Q_J8-8a5+j1dD3M@X&=M^s z3R*zKNQ}XFt2;8d?PuPmq8?b!$Wlv{v5J(s^~9I@c+Nt#)DjaGioFPJ_HMR;Dx*czl5GrcrM02`_-?xu6xG*UQ_%7(&nsG&`yxs~9yW3eOa7s!AQ+@o zA|PROgOZjn(shq{{=mj}vkefuXk1iL#%?IPbrIe3Z+jzKkcF(B(w=qd@VE!_xMzZm zOYgAbWNZNG3PdbVhup7I9Kqkaj{e4XvynzUdyM7aC{z&+^my34J8HcglVkMEx?rzo zSeW}R=Y@s2dg<*snHZZ0Muu-Ul}c;J~zXbils7q zmvtQmn|4~>*pOveYh+zqWG&0Yv^2FS82lVh}&2sM)C_~c7(?|n5AA#@Fb zbQKyYYL1b6X$}AF`Mm?b{^=k{tsm10l#JCXgaaE#hyY4Av}*Lyvs@clBea6%nnGHG z)*OrHo&h&LpYa+Qf4Sz|p#&ja0Ur23`mf}0FaQS_-fCHtDkCjLv<@(=Xq9cvxsv7` zf@q;BZ*LcLK&NeF2HxSQ0Xsk(XC2PVyGOX&=HA!)i!gXJ!&^l}IdEWv2x7@tRj|dF zASU$at!wG1@p#btq#j#T`ZOs)?zIzmT5NJ$bbS7gc!hJfFjjIyR=IUB1j}Z#KFBc zbxBNf^r?(NES2$!!bV1xs3oAdTG#cm@ajt<}&XL?$i7!)L+T6Qwn>U>RQo zLEh1up5NK{A4>PsQyDy13I!RfHYg)EL!n?PQt7oM%JtcsizxTGwjA?3o_iMTec<_> z1q)$#F}EGw_2B!`z30Q}o_t+~w~A6a=6pvo(k?2t7+P4OdEQ%|uglxlgT;TzOg#9~ z#fjiRQKuZ&WjGM?2JhT^2L~E+`|}4Z6{Czj7#bE((!mxbRZHRMtmwr3~G#k1OsAZh?E#LQNbXQe7XLE!)+1raLG{#-*({+}O!7$gQTU@%fZRFnlqgGLiHK}!%Yv<4$Y2*O`H8~F1k zqX8N~Lo1MT%WVh+46Wgp+}wG<1CX%!m!vN$SY9;Zd__upuIFO#$^Y{M01_~U)(FxS zL^7_DbFP&rBCQ~(AV?VAcC8ulu+!+u!lj#Yy1f4vK=)B^LHp4EwzGLNmMSuGt1aq* zJ2Lp>_c%b;(3%E`j5RoJIPVzqIM4w{obe84#5s3rWDz6jvJU3jX}#%(@BM-Aj0m^8 zOXue=hsQX)gW=WpSPftpAE6j83-m^h*5z7bY{m-l4IeTi&cA=r^y1gBnyb73&X zRN|BNezFGvWZ<`v&P5wp2q+_0|ADI`L`KFY0zrsC1jjAY7%dC2EJT705F;@HL37LX zGlEB1hd_)GMTn6I3SyIG4Vf4t!T5*u-qSZoz-!*V^P~?2l#3X)#ZpGDzR5-;28lf& zqC_PY!6LE%Au46ETtf!I7*GTw5DkaCAhYtsTZsV?lmvqyNYW)qIVL6}-`|;?=lHjR z@#6Q_`3>It2Ai>h3LbM{X!T828!!d~Mgj%|6*P)L7MeWHu@x%xr7sbp0bcGqn;D1Q zkwGIE1nK5FkBcaZk>2*S&U4;1wAVYcIqma#f44j0c4v-Iiz-EiSHGgwEZ7(@253M8 z5l5M)xh>D7FMUJ*Rc@LY`~;WTLq^8NLO088-tkc$+*$pkMBr(kw&Bc2`EwuT2u2~O zjNJE%Rs(U!VCYAH&LYdY8_P0}r7H_}_idP+&wiM(NhcDC5el)8SQo5EF?YIIhu`KR z+BMg^qFLaX4%Utl`<{O_qoX0l5fNe&AueRHutY2B3B*Vo5sbj$htH719y@>pgxEna zNNQ3RG0K|61~~Vx?7iZDs|okL*&>Ld;6+0hJz_Nygh&L41cwxgwumkOX$d(7v#gU~ zBzA&EAlKSGL%97e1wk+%28pCH5mK3=N^1wf!2V&3F^7gkV>GI-J3KL(7! z7$d>ZHDDBs5Q(UxN@z4qqeas)z+j{+5kSJWpNm;~~#=z8E0RYqQMv5bsF7A|9B=iD*-I4*0k?w-cDIH$iLv-QTe zicJIw0+D7V>#z=3SOCF*@j8#*<0rg&fR}UIzw)tu)yMj=f3AyAaUtg`kqD8&2t;r~ zhyi1;B5?G@qg)Jt_r< z$|>{(kX9QdaxE>TNyRYId%yP{`~JCo^u?knXf7J14oD2GL1Hi%1S$xiUh&J_^omih z-rVLIWGEC6kgf#4;LClv%-VCWKhO%Yn0b2u(=+hjp3`IBv+e7ss1|DJ6%1c&S%P8E zU@&mSIhS8y7%2g8%^SRe(fhu6=tORt!K#s4j4?>RRj(net1 zT8p)0Zj0C%&KrDPL+pDcF9@R8e}lKWf?+LXXo0!-dRMTDz7nG`t(8t|orVz+S>Ux# zpFvx1x{DqX3C7SW6{(aMO$ZPnHssX148eKt^KhjUx#HA2-kY&%m4Yz{0f{mGZWptP zzR&Y0X(7zeVPOG6z({N&y!cyT7T@l$7}Esf0JH`{bks0lkN^nW>s}*($NCNKc4*2mrj9l)^Qg`AuB zHs-nRGU}Xr{mZw%%N;Mgit#o|A%J20v;WJh$c{&V+~z(S)LiWt`bOYYZkb73cp8L} zHtCdnWk%qC`s=P5v8X#vw=L0595Ud;9=3}7WT!K@&<*qckyg$)gW0_2hk!T`8NCI| zLWk8Qc4B1ks)t>9P+am3ZL!-xB+^E%{BKr?9q;KGalnOjk`~rY1R~PUeYVUfu74B` z1c?N#rDY=LRhB>}1_{O>S3c~h#oFB(?X<2z#yHN3$J_BV(fS;_q3|05|qLq#4v20>oE_q z9dG3UI}8|2=C&qPSXKZ8V?Z?WD!0rmu75EGFpLaxjn2r~&>C6^Ai+G^l{a3%j(7Mc zy_HRZ0=5*b0b?*2$#t)L(Cm0CZ)H=kVhp59jt$K<4vZAu;X7n@@40ZIH4e==4;#7W zY*@qyx%Uljv2zEo|rU0?if5=0axRlXtX{L8t$P*?;2p&e*WzoG)tBKH}z z2OW;KpbIRu9Ne^I!x;#9ZZ)g*aTw}M=0@Tv2O0yVXTL3j)8xRspCBb`O#u)Zh6P9a^U0k|3cOXXE z2t2`#Wn;sQH$>u?Wle{5I4HuBi?E#D;3^h+6Rpd-OzJXdWy!S)K_*lH0~kmWglMi!mc?2cnpOxUwXC2L>>$$` z5laMvgjfF-mX!_HJYN-PhlhFCIt(Fdq-mZTL4ry!GPssLU0Yex!g5=qLC_kF44o8$ zrZr9=PP*x(v%H>v=Upsfd73xp?I8w`;RFrA5JR7}w)%X1_UG?2drfN^x;)J}Z>)pm zjZHSd-UqPUKJ?fT>t^1!8!YsIjW!4pgMgr5ArlrRt05Mx*3G(LxvodYCD+x>ZRFLn z{2q3c#AsdCB$jnpnCr5PV6Al#DJ)t8q>>c3qg@senzWi|G;z6RnhGMxG-zOg&=qweG2qlRwY@&o1f#G-(juefTtS(} zVJbqc=mc!KHOuk&_ud6mZZq!EkRUlGt!2z57!Wa4Q-~0V(bV*fSAFBfG9jxqt&Abp zNFqaSt3i_;r0b>^WO?5FP~>qFk^7Kq+@ypwPaqOnfk1$ zA=go^H9;ZQjj}Gwx=c(OnG{x&YE1uh-{m+FsgT4Pqo?LQ5mk8nV}P!v%X2U+3TrEb`C%1^>~PEowCx z%rSJi-RtfhBj_#MljZ;4`Kh0-&(;ZpU4$veH4_lfI1!J(>EHX;oqQ+)CPfi?9;>m;R|P)^yUkEP_TJHWPadH3TAiUh0>8H?427GW^yLoQQVPq_tR--JP=|CP?o1quuyWR?#2-U;QgB+UxVx zag!2+))KAA(nzey`mUe9Lh!A>=t58;($qX{ge`1PATZtNhdOZstMHHiH-G#q5}%)o zrpcNTk)^rFxk1Z!{d_CNZ~aSML7ww<>1^>UVGIQ`Q z5G3|uV~a{OSQRWpNoChuIQV#$>d$-)qCpB21d(VEj84WT#zZG0<}KWimFINuE+q&C z3}6sYKu`!4kU*hYp28A-vNup<9weefj7W?`x&|>p5CJZFMpmMSo^~jM+G|xPRS*=l zN+t;tB}@7NkKt6I6=M)EQWJn65d%2UiWl9VmFc0UY#11;ilA7kfmA?ES;$g9&_*L!nwCAd?|$m)sG@-%t(UBrysgAY+4IdQbiY ztYFt|rZqw&paMNbf`KZkN^ii@Kbcog5EM`lf(+UO0}v#y`?*=sUbgNs0gWJlRsm5F zN=4cvC){cVaH~U}K>-2LC>TWT1ThALw{ZWxtZ?ht%OrpSBO)q-I$;o{z4i$ zAUc837%;~i$;x`&Wk@SsF<7OPDPZmPS#s%fzcb2d9*6-FX$1+uftS0U7503?9$M8> zQA0wAljSpLRfC?+Wjj41tMj%4LswQg4jl~gRWtY86~w$GOBd!G>mqA8Gw zlk^Xqv7QzA`pfo%01XJ3C{hnOff;+mU5g1x1T&!o5tv_n2`lykTi2j~MpK{#6j{4{ z)@*oZM`o^KDhd^pNVdI0S;5bzi?m7v(LhxtJj1cf+%x|#WpWZhX@#smV>c`OH@tH_ zf>p}gQYF5vGiT4G_dGADX`XT_gaX)m%+;*;cc1a2K@)&%M5wI}bj=*4eb>T2+%^&Cfo3UlD+;`~oB<)at{Z@!_->dch5|X~Qdd@yz$`-g(R>3IefN@rNLp0+DQa zoF{l&M;*S9obi5<(>GU3=1l-pb3JRX2RHV>U$qo55s7 zm(Z$+leD$|Rh~YpUiTjZMM88U1CW&*9U&w{OfawYkXg0+8YPMX(J6LT_7OsopavlD zGiO$D6Dxu+6$D7*ijEM9QbMcZ+hfVoXD<8h#@8bey;%%Ck-;;uFC8}!4j}!vI4{voq>X2Ai-T>Vs>wv zAdmpbByCvPe{PUS216XVV+FW?%6f%iold)f`E{) zqW?qyCKR1yBIUMOwT*I@Bt=yEab-7PCXIo~M45P7v+5)#3otlM!48lW+<;KQ2&E9M zZL{hUiZWuTB0xGT_%&@n$OHnBThFRnn`w$905O7%D>t)%kPyr?p~%~sRYy*!K{S{G z2`he)u1HCuJZSf9+Wnw7i9lj7Ktfh>8xSarAW==c^=x`N%Xt#P+Eu}T2C{N;GJ$KKd zotJ$5e2#DEcydUl>k*L zNnx|HAE?eN6+@`9WXcOZ-Rya~offe)go$WCAX&*-gfImRRZcL$yyj0md+y3!tU!bk z2q6>Tiq0ZRB$Y4`gvfn%&zeh~x^@X>LNZAL1qfNujX)@3G^$YZs*N|9HE*IQF_wv% z5&|T+!s}K78U!>T_t-sKF1ahpUX0NgCe(%%{z-tCK!mC)-S&CUmM`3tOlm|;72j)d z4X9Fu1x6}(jeE|L2mQ=dYCsWU80iX+gn+3`kwAb{=|40}{$7-+lu!YYII!YRM42kg zk~G*J05XT$$V~w0uZ1=fUNuwNDN>qkd#<+{fFMxthnSKQVW6wk-o_H8XR3s z04I=&bH&>BQ}3P)JMU8Ed|o96M8HG$O-jZTs9XX8RD#?Y{B(jeX9{r)J8x za_%*k)O-3rB^z5a&Dgl)BRDZLikGOodSTc6K4;!|3H>zx(C`T_jcsObKldSEbKDx+ z49z!IPSb62;gWg8BkHABueOaXeWtd9?L#nQ8{6hu?GyD1aL*^daO;xT{Y$ zvUl5`#D%NePxB9D?%a^^S&i2|we7Th4Cdj8BwDXmN{D&E*Lt`8tJDkr6)&B6mGx?^ zyqd7u=H|n=oB3ql*wvPZDHS3w{wE!C=l!e9C69FL!nC!xQC{rEQWJ7vIG*f|)3fXp>cl|1#VZ;8di?I7KUhtMjV&*DUbLPy$YhKH; zF*dg!jBSG;q>04<)+DK^LWM(a@mgQe|8<+!eaY_K>)Frl-Mrl0Z|j)1wKGn8QL40X zj?C4nUL&-OT|Ss|-qviJPf1J$5@SS5z|0@#o&5^0^0mI3AN%>8fRvGxmARFYDzp%p z!9K@wy?lN!Zo@`+=}IOI>f}^}SRxdbnUh3oqEH33v{DsHp|rddVXpg-Y$IF3=RQSA zAW#&ABBhjA;I5()tzc263Jal3y?R-zZMMyIl#l0NY9(4AF^B*YY5*xo6@n2ey%h5> zCncGKe7-)%w9yIk@tk8DY<7KRGBHU76r*TSP({Hsp9BF8T0x&6JEHkH+dF{eVC?;!~ z2Q4BLDTK;Wy&~-LQ62ka!#1lxm%QPS-WAEkM8Vnt-+v~iU0_~NCgvu21$j8 zXfQ<-J1;ZFu#eB&PEjOIv|@;nuBM{$T|S-B8dSaA)paJG7pmoDTqj*f+>PG80-V}Ii?i=gCPir8Vo{e z5}SZdrcz8r1VIQhMn1^pb`aVakpPkaQ6d3@v_@(|n969HF~ep)_8G0LSZr<9tQSE~ z(mrIzUG8US!F=*L*R95Dl|deFKIGGdKF1m(%&?gDT2s83bGEyX4|>R$3k!2?4O-Ch zFtgll^0D9CZkYQmR+hI8^DuvI4^~h(AczzI05Ev~odGIB0cHU{kwlzJrK7&0qPChl z&=Lt}ZsBO&hW=*5R?GlvllTrgK6#d-`8l8F1L*hi59vO^yud$I{Lw$ge=GL^<^%S- z)nm~&pa=CY_MK%OfFHBIn*X>T318X&=Xzjjp?x#_5Bbis-YEHz^w+k0r5?NQ_wRj* zc1OSq@?Yrx>O12*X*Qqso`D|I{kQ&Kt9;HpE$jvSr}@A9|6l*$_+EM^um|$5=^y4l z#C}%&NB^1sJMI(P7t_D>zs`O<{eu4@{*C^#`}g^O{r~1a|NsB{(Dl*#ckDy@Y4`Fy zgK7rX@=sj`**fXZ@)W2U1OT-0;e`g&4X@;xzSe=p9LZ|GGJG8-8g-&YNBz5&{1I1fbdH)<90?5On`4+fm0UUbFv=YoU(IvsIoe6u zNZS?&7#rwloM%-ey#7LE2*oqS7+zO++XbQV_1B*BjVHTkk}KCyum?PXi~%RA+)8Lt zjx@0paeg%a+wfvdpnDnrzWbSRT0cB*{YvPDnMs3@m4~ig7O964SO}ZoLR-*?gy`3} zPy{39*`UQI3(5K(t)P?KpnA-0sRlfRmp(3ne~zR$jxc;2+*pL|HJ`~m&Dj>P>$CmH zC^xw{kA52n2JHjC{8gp{dj`}EuirHD7ocNGwJHb!;t?J0B^p&8Pw-SH%;}bhWBWGm z&u4w+R5KXcraB2DhvD^u<6-5c#Di>|bkv0{=&S$!jYT_CXS2ru1eg6VgA8_DJcHw* z^jsppTyN~F#G8zE8kBfUsoPLLDyofK)k94CkQM(zKSUokL0bCWy}^nETQ5I&KBZ&p zr{0QNR@v3?Q?Lm$66`#}?cRfIopbrxwU)gCqrdN7<|9fZ3+?iSU_=NI)`tMsId3ra zRNpMI*50E;DACJ8b9_G|Tvbd#kRD5B*=9Xi5J`FHeOu zIpDL!J|4nBB;`*!&NK2n05i00R4Bf}`SHuAKgdjH>K|=dGp>)DksU@BSnCidON@Mb zL|=@aC5VribFA^AWt!Grxk#Ti*B-L~0RFrL04&~RR6`0;KmWzVzKhb30Qm&G13&>1 z?#h4w7hd3(<~0EaX#f<$+H?EgW$=PKx8H}z%>Bdz4)4d1p;biElNg~Pa>-HECjQ+qEY?jsfCzRZw6zAjN}&bQW;?{I1u zem!AbC)I&Hva$RB^dRnn;q&yQk`-r#T9v^_Ashg>xIXNq?coHWxbUE6&Gcp)#sM~9 za5VO+9g9w7&)MBDp%d(325tWDSTc;NUSG55OTN$m zfU4r53x4RP!9BSzS{i}g`NVTI-?n9D()jh}^d84$Wj&d34m|ffZdmxOv2Y7K$A>#L zLgXT~flEkzBcEfNMaf(O1Q^BEA~9MDO+?+wJC5S!4lKgccM(a8OF0z;TeR#&M9`N~ z78(UaZtOp~Fv2Hu92jIl5Bda-nAWlf>3Oho2iAf*q>I@O9wVTDt(coa2$TTFynq{N zSOSLs3(}dtG|^hGVp>0Y7CjsFTl-)_&5a<%=o3kC;XEo65z+y2a_;cfMn@_oLGvm| z7+y+>OweB6dg1T;7nhi7AN+VB2KdO<{CfSR7!nK@ae&XlIALiLd($&#rCjpFeL zXl_Mu;f1F%&4KZ+Tn^*AjurgWy+c!xR;2ZwJnmB$ZK@VYvtuJaME_jYfYiNodwo`H z?zxRii$-?YA0eoSGef@pkLr0ZLudI^obole$9$JFnZ|zK_k!%Mw8OzbG&%qNOkC0i zdSUZzyo^tS1OLf9-rCu4pA7pi{#ozw-xp%`X}gZoZ2a&vl(P){7giF?EJw+kpMUcB zEod_*$5dB5;;dg?9%%T;*yV4z@^BA${CwTsbtennV?;#xC|Ymk>rYpohshkRtcaBN znIV}+?YR>3PTlc$BSOUL$xKgdp$MZI>(v??7u4Gtjfe@vOf~*#; zGU?l?(v&6q*=fRMZ%C;6dpR_XP$@pA(;tng;J77!XUz=q>_-?*Mxwwq$in}F{wyUQ z^GWm+fFwyBaVsuIg>{Q<3<7m8clb|6@Y=(E<9(3I$je?YN3N3z_?yl2Oc_t9T#SF4 z$*aWKzXof4QNq5?tJfeO9?rD9mgvcRXK$Y*pCS|F3%6aq|j-4H> zD^^$u#Wedb&MZh0Zt@^5S(apXr6*duih}1=ms$Tvqk!nRis^maaq@Ux%efH&clqu_ z!A3$mw6o+Rzo=RO-wNbQ>?O3o!yNn6J8ShB>UN>lA!OTf@H}Yk&Mg)&I}lVp%M2u~ zV2+!_ABZE8M#rxP+Mw3^DTf0}tCn&;eKM2>=;Sa2gcqfd!|?<(wYY z!;bl(VfQc;iWP$TCfJn=S8D~USwPL2Y~N;~UZG_sqFfEXC@rd=`35YGUB;$#yP4WI z+}%HI`p;Y0LWt(tHZM;4H!kqeC(@Rq;mmAZOKE@1xmltG|G#JsjT&Wb@}esKG*q)8 zPrdgv1iSDK_KCon1tvNv(Y|7HcZ(L`>Tg6-I%f99s6(a(eyi{Y0f= zH?FbD^}^&jcMD~JRXe-*Ok;y#@Dt+a!Bqkg!b{T;d0O@$e)9O)SfWgs+8k@Zc-2Dtr0!Cb^1Uqj zbIeRv^Xhn<{0N7qsBPY}eUpD?q@kjdXvoIysY2X~)hJbRrADe$&XkxPybZ{gEqb#d zu?)887$}H6eK#Ip+1qP*nllntAlhO5v@>y^O`N<;&2nUG|28A+`pV8#<>5wJz#n|_ z@g^a@NPPaLeLx8R5wg~|H1>=C2zeqn0SM|MprNPlfK!eEoR+(DFFD*iv{QTWSx~g# z+a$x%c#8V_-dnJ40H{h!&07JP-Si*xG4TpPl&SVy z6xlhE+kAOGJXhVzk<2K#_C+1+w4v2=CDTlS`sp{4T5z3tWH7Unz-IiFD>WM4TRLt> zbv>^*rNj@Fx)J^uJ5v~5-u0l6)m^H{y*#gG_cR%}a=$WZaPr;$MfdGWeHB796@0A> z!TCX`nXG?clnmq8FV{SH#Kj@CA022g&1$LJl_jxa@gcIj7t?MT(0_uoK1e16q{Qpr zvOX(6gjuyo5{4$}r1=tLh)c}q7&A1Itz+UW7T1|xBc5j*(_iaK9mC#FaQA+9Z=Db7< zf)!!eZ+$Afel^g~a>d!pEsVu$Jgp6s*q)E=S)BwJF5uq#+GzIwqB)$q@J)LxKIx>U z@O9@{EWKwbf?QDK7-cWBXEs{HF$c5To=8eXg!+D1;YYv92&y!ihdUv?;n|K>#Dlr@ zN)$-zgCQg9h>3%P7zZd3S%HYbbwq-f*xVuW5v+v$GLtS&j#A2#Z7Ag816J2I;uihy zwVAr={N!IPPe9FWA2CJ|18t?w8&7;Q0T}uG{U=97F`Hn1ej}Sv4G{y*Eeaj4R zkPq+iDlf3jgsjBY(|wJyqBvas_#HzGk^Rv%_rW|ly5?jq$LUk zbyEC;7X1LmN&bsvS@B}nR$xnrIqFI`yWEbaAeqt@PQVY47 z_irvp0$x|KGGa(tp;J) zcT%EnLx6Znw&d0U=5VeOzsoklepu-bmg1;kFx+Ol$j!cOn6!(Xr9g0MNnP=30hX+} zZe3_lr1YZ+$ouo>6jrwQE}D%>qh0oSFo6%?e0-87Nx=qp@hvoVs~{l8 zE>g0{f0&XdN9wO}LCFN`v~F&WBq6c6J^Hq8peLc#C*L|oIpsX4_0(u~1<+%BaPTIb zp2mBu^a6G28^kl0ZcA^fA8F8{ReyfR#g9m;9Qpr>Jd2Gjj#V7PJ=*5w?(qU}v#J)@ zVelAF02$~GGBd?m=q>J@I#dhwcZ4bZA5W7cb)|AQeIwrms?@b7Afw%r$fMLGkOD_; zWrGc`K$RGSHPq8fBXO8II}PHord@MP3KxQ7dY_F2J`I34gg7W{(SAk=h7K;(m(x+e zeIrd-dfaI`JPH{P{vW=eyHCg_#>w`Nuck0+o_`8U9blS%i0Z%ESj{ zecod3(IW;VDlY?Kx-JvK?O7zSgTx*E(IXdxRup_K949{Pe%zJO)#GL33A7}_@LqyH zg|qoU_y^~bM93n;sd{Sc#qV*c1Mw0B5mg3XX0I0-2bL=CWFq%hiM|x{*P8&fe`|dh zLa_|z_%zf{-RpLgi!6asoCaQu?kJHA?wCbe2eqe1yF+w0OV?nnY0? zbI|^eGKvJovp&+gn?3o@B0pLtz3EUz!f-$Ke{hZ7w{mLMBgacp?DV?LYb;O{lS_&{ zbl%}W8QraRG;h1d@!Jab;ZmJPa#M=(+vyFKU-||BY958qT>cMzXUNXdnDM|WA;tU9 zw$Cw&vu_`~CDgtMP^~Z2WNrOsXdX^q4(RjA3-8kV!H6+8ydBhxXYATM~H`&B=yGO3xZMH8# z>T6kGNvZjp8f@2zf?c*v-ePc?Uf-(Ky2hde{sAO{ zT`g2BuEC?P&!`BS?`(ZgM`$gH{@EnESoU;&Z+dGONw%1bL2`9}!4Ur$l6kRUgmFn? z_u40ZruY!EfibzE;IU|L|2!w$gIDO1a7V3|GT+;;IWPV#clYkz4bVImUw1%PkI%io z5nUWXc2Ub4NN8bj<-RSIeCgH%bwvGKED59%)Wc*Wel)lFkwx8^(Wh7A666q79l1v7 z2L!h{wCK%FC%Z#OcJ0Q0`reZO0~^SNXnA=#h_CY9?0&PAfh_?m2${vte(a^Y78Ul2 zl5se1=JXj9@`X(VO;^{;*5JRS5m!|LgP{i}9UQ64n0|8TRo5tIL3>h?JhhR`-Qp(V zVBH4$`u|6N^X$$zIHHiIq=^M#AXfF*Gh*B1g)JmR=6T|!bimLVdDfGINKTZy_&r9@ z`?e`qRyV7#*g{PL6TYO5mk4mA_gjGMSrJg}&eirkqni7qQSw-Hwjw5S0iske#_QbO zXT1rbv9lmnwPECPtB8EveD2VLwRE3g0;Y~B1o5JiwAg;76`|aP%4A~Obo!noyCHr! zx&+qiX@`%8n%kUY>&-0$3T3Ys<1c96mo7p+q89IxB zKp^z?ZUzQ=ijdk z*(`0%79XJ9mI7;yYMk3Rna301$Y0XmjR2OxCd@vw6(mo)ok=w%%$<|KdPD&&R48P%tdLh)5}8Q zzU?z7oKuMGJ^F6vuxH&4V|xy^YC3nyJbu*wQM0^oLr(H_)+w&pi%PQ^u_Ut>W(8-j z7x4;-ufLpr|Ne#9C-$E5GuMlDrxo&Kub#cWeeY$)Q?pNd(pY!SiWCNJN0limE2Oms zj-TR&%$~Y_=CJg$C7EOQ&$^^-2KiqVX-@eHdEjct^V7*w9aV+`rv|91_*o9)cNT`>{-Yb)DPjXoweG)hsO!0~T zw%jMTv;~V*e>!Ws4*R#>y;@lRHNN?7HE*ot&dr6pg=fdVZ>FXmyL+ZEuqSf(X4Qg? zZ9vaq1`TUPOCf`DzeI11jLbcpS&EDrm6WfO4t?D(J&1%MaMl1)0m>sTG(`sz*M7e5uVGM)H}-@iU7(y%ATc+x53$&=99p{E0@ zvqK{rkD$o!G^kY}2Ib>#g)zW^vkAQ|5;%pCoNNXV;<{s``xd49*j+v#>}jXp2uA z$)MTlrMl-O(Xt&sf7(8Lv309{AM^Qk&{(eEs1o0>O3|#(ugCBSEWN=vJy&Pw$3|@$ zRO#2i?oJ|#NF=)AJiM{kIEoDl#qCLR)SwAXmmhB-Y?EGWOpPc#s)RT5H97=G$B0Q4 z-oj2kh+wIU%I+g7(ySsZRR2tdh+Wx8p5NEX&-g-P267-^LH<64-5%JJsYFM`0tHN> zhDA8U1HR-^AD*83r`B9`K3f3ZB@pr*W6)L_;7VdBi&132?Ew)aFJNgPZ~lg9N%EG= zywjuh7ut zqxjDH0tJP_PQx>v4`T>yLJUb!R8L5pGu`Cdo_(?LW7xZyh(w~OZER;f9FIrNyP-!E zQ-EveSphKbIG9FErUV9Hcw0-D+*KOdk{Q4MIZRV<+EWh%Mfrh{C=?ND4&kAYy1Lw_ zEoB5AyQPd|V3~@7vAU)X)o$%>hnlR%V-QrD8JyaJjf=5Jp<>PA7;r3;YTb$B6WbFI zZ`+74GfW$ggxJ$oXfjb~Ls6NlI{OrVCfXXE~>W=nMkG((*P%j>phYCwqH)XJmQ+EkG>W152|Pu`{!fRqsT0 z0Ag+I6}H>Y-L1M?Iyf}k3K5gZBZH8#nP`Lp1cu4rZ70m*ZPpO!whX~RKvxZ!+6oX@ z2$M#qYhv@4Op|y7B*TnJKubxB#j=T11d#}Xc*`(I&g_doF~MH3(lZg=_FT3EqGg;kfvw7d=aL-J$SzBpVBiDY+ic? z0evEs8OH*BO9R*ll)4I=5C&Hueb+f-Sk-S2IPmJStUMfLB7iG;P=z?`{I2s0x zrliK1?}PZHe1EeE&TXk*X~@2;zw5b?e7tokbl0R$(GIaJJ!2n8M78QL_1E2hg)lQ6 z>o%P7Bk+Oy*O{7P`*y_e2-0R&2$y%}2%p2Lq!_06;g^hm4N*sTJvCmZhkng#rzG78 z{Q7RVl5eFRuZEa*V0x!>UlidnYSE`q1zC9|yOhzoMZ zPnS{6MZZnV_8iz%Fgv;IK_Wd0D;bybYc2B#V^-)xxgxRFOazqfW$k&}H06{B|9le$3WQDUGh7FG_!_A!Qew1cz3HvfgL z2wv=TR1%au=iCWw!!QHCqW^%1E-w(@zLw^6BAk6P6vqyX{O+{XxBYvY=8NKcx96AY z5K-uSk?nMQKKW@y)wi4jc^<(`Ft)>+$74pqqn8itN>|z_R)!HJb>$=g6p(PZS@P%L z`NA@f(uY6Ujc~l=)kG)5r?o?-*`zO7_Wv<;)p*Rtq*vhf4~8Y|=cPCLd3=4nSWx!P zK~OCB>8%Wpr3U55Gv1vFzN_#n@R9?A(|sDc(* zD6!(VndobyV!NSI@?L{MqcKbWlk4O*>!nF}>OIgyAyL9lQrkYJ~!n? zCJbw(n_YAOsf*U(^njp5$D_P>nwAn=Ul&0zZ{iQ~(@@PQq_YTo^Mmc5upH&#!sYiF z=)FnJ$q@V#1fPjbI&6+0KGFwsUv(3AqMGvXdk_dNpU*5q;4N|4=%p*}9n{g?xf14d zJ!mSCM?BvMV&kDi0$fS%qG8@~6pDvx264}O^Kfhi+!708d%@zUT<;8p!LN0jPi^7a z8|$=;OfZHC7F4@|45CGF)Y2Ve8vwt)WW=JoKXVQFOm9$+b$T?QCJ zsv9_&3ma}#-95&Ho0%~^GSC?oW*H&`GWK5d<3z*z#apX>Si8nJhES~pDZ+p;?6efE zq=%Plc1C^<69;dD1Q7cKL15(r9VnoTlh2*`^U19RX)A_II?)TlfHNdcWa19eLgEV9i+!8%#Dga9)SB_yb60@wH_gB%jj3eRphWYblw+VtAwM65Wizt$~}d%k{r9l@(tF1lyyHj`mF?wLvh7X3Xkw zrO*!q)&u+(pZhEfgazS{p0Sx6;K`0}O$Aeh(dVc@3|oUF_Yqef{lTm#r|< z>A?k!y8d{l1*|lNq|xiVA93_ism_lZGgmDR&Hcoh0k{$>$sa=zkNkv(dI{og(- zbo)6;KoVkb;0Ye@<=&B{=40LW1t&mY3vo(O5(j3GP}=qDsmWHsPs`ajVJcMMY^@f< z$kLhsS@;jsPk3n0i;%auGKl(~Z1?hV2~}@s!iSTk$Fc=^mMfJn_S1w1ot?952RFs+}H6 z&JUe<>U9sQlkBWYRx7Q%kFEK=_<3;a5`m|+i^m0nVdJj{>e_8f_MX^d1_2)j$nUOG z*41+-?En5(OuL9+F!yG0DQ~AxL8F~@Gg-D17?IJ)H6x#PenPpWb+wuK6CMt850B5fy)TJ__dbNO~#;^gIhc zLlZ}0^kEvkaa_j(hFPUApZ2s6nQ|x}{PVGoX9LRD|8`{+*}CGkbGiVB#g4r;cH`%l z4l_02j>-Y;&uj2)T1;n@#$HikUhMkG+`gVaFV8At<0RTN2rg)t5}&5*?QA{!dTKz4 z!2i^uLQPRBAzk5V%nh8iKWXVoMEQ9aX_+Giz2&3SkG%Da1e71WatW-CHwVGbTp54% zE6@f`5su~IE(X9{@N%)t8@Kj>Jw)`z_P+Y6x%$mtf4e1t&kK@}`${4%rd`jtZPzs` zcv<@(Jnz(D))FFR|8rFV!dVn1`158>>qz_T)p<6GNFZ`D!7lJlgf5~Z3mx$JRBoGy zN2~`H3+Kg@lD>cu`(QO?^kZ;Xpv{X{bFl}_X8!6acN@0w0S!2X%%p$>e_!l4yI_1a z`J+a=MuDFI{#zmY`9M8W9i*N*RC77j*;fL6D{mSs$rC9R`RtnvN`RlT-q##5^no{g$33bC*{RqHa84L1F>R%WwxWAqMTu zU=udrT_qFh?g+kh3QR``LP3!+RkIgQLruT#xC#ZDg(l5T3ra;bv$K~#6fBlZAjIDC z0D;YTjjcyoFAjSltaS2-O$1#4!9kEYDI5-y!1*U}FS_&1vKiokm2Bt#VJZX+6$cIN z*r4Y$Yk#C+U<8TF7zm!MlUTwO9r>G5Fq-BsPqQG>U?)UKR+w1w?RIBciUoYLd)YPJ zgGp%5V8nqGtk?Gx*G%PXIpFfdP`mN%guO%@AZ^ve{XDmCr^M_;aLfY&K^LWy>XX^D z2l7l4Ql_c-)xdf2bkqe)>NclCA=^Pp!e3Fdlh11GX&F>Bg9jrh9LAC6j_9Fa&@ltNn^R)a}dSfxl z0mieN`_}b(LcaPOGlr4ErZO2^4IIBr}?+G(A|r;^(SwS-(KY80%Cr&`zLYFkkEae@tEOcr3&s3Fz)3jv=k( zAdnn4X`GDD2cA&5QoXD^JS#t+lLy16d~1E-cI`$wh{3hjX}7ilcqHh;RMn(lG9^84 zn=3-Z@(7%SJ$urinl*nkuVy;Itbmw|G_n@F8z3YiyMbPIUZMN<_x*?5u`vvJqK>9vm8^_fd`Tz3;iR< zKQ^cT!5Da4%xKp`f-wkd0LR_yzdm=Y>(z)RevcTi%`EbOClHp8{;Yj^?p5tSOr|0x z#Oxw&ramMV@U@KTQz*DgNSuA9g@GMtJAj8+O>)SWkEQ&V;{ z{M;kiw_Rn&0|iOeKK92X^NnSf@G6JU}z%G=~L$JwY;vMt?`6cw8rwb!xG9 zRHtWs;v2FC!HAFvQb+-19^7A8x&5@}X(t&GFjdPEts>I3Mb=3hBSJY$Lm`t9EZH)u zab|VNz5QRI$@N)$rbatB7BItq+A1m94@e%6#P%|0!eO?i<8GFm6eh@o<*^Pp9zB<)SvVtr#=a; zjx+Qz`(;HG#1BuMmRwW`9pibT5}G4)-i@YwRM1B!F8??0HWBDl(JhuIazEJOmtxjF z0|F9>Z0Jl3>`T5{Mt#x7W)ss-FLDqpmJb54-I2WeAeM7v@tVoBMNOBqEuKkC%YM#X z4E*u@zYn6d*!V5Z5uTP3naq%i|A(Nf#AD;J3yyO-IX^JpCFfT*SajNX zK!;=A*H>8Z*gguw2} zn3$vr{nGn!J`yxCDCApRtU*%6EeZY0FTtKFFn#4HatlcRS4>yn?geAhfxD%$Xa2e8 znAWVzY3=gXRl`Yc9*O~<*2f-z2^<`iTK>pe?HWF71XX%;b`n{j_C=u}j7m#33 zBgDooqw`DgM&!G5n{B_&B)@{eYwMDY*Y3GhcrbadkbX#HX&~R}I2WTrhS2o@2);Sa zP|-hF8{*oi#f2&oNeD!li)aeSF<`Udn#sZux5;bAxvv_vuNAld-qhz-D0!gK@NO`w zh}G}oP+d~`9-))gWjcCq)D5H%N7WMKgByumL=jq!!txrmA6^#K(EHQ=#) z!{^`iG80|V4anj-t4A6NeS$x5!N2H8GiK=}El_h>5D}&H-{`#vGqgqN53xAtxkeC7 z(8nTGeqY@#b|-Gl$6}M#hIXvzM|tt9J421vPP-w9a?hR5`*nk{j|wv~L@3r0VhEO@ z;_w{}ICei$LeN)~iKAi{_!-AM;MlLLBfnR*>SxlMh&3Yx3en@*t=#a)#g}iQFQM4v z(;g*(bK!sjjS0rk&?li#ggL#x`Ug9l@(2u%kvI?=cNiR-r|GOH3Pm94<%v;07hKlw zG~CWG{nuliyOvA;x78Ijo(|zO)LHO?z#APIO*3tiMfcK4O}+DzPQ6RKM8odu=z0fD$L`UoN8*&T!Za z;XWo00@W2}0CpJIy?`#U{ubtTQfMQ_GC>d>o$U!2UO+(Yfo_-#Jbp;=T3Daut=K+F z&T%StIF0Jl%TiuZ*#<7SfCh5`iDXj9vS`AUraC}|hGsJ|&!ETe|YuVSpVTmW_WZTaexR~07A8x8Qyj&;8s080D?lB)xLS(YRN#8;j!w{ zaU2A&tyb)zqf$B8;HMPK$hoyS_qbWN+_TeqJ$h922Z#$NvMX+mTLUpnm`=ME)aubK z8QiY*mpVyDj{>?(`aK+4M{uwJFC@@O@%5%I_9v3GFvw`r%%S-Ec0&y4`BcAV=Ve2( zy_Xu=PMD~JKmdq5ber^yw*!cZZpGgEuJyo7lLa8GQqD6dG9q)4l{ZKX+lng2wO&;u zUVtW3%c)X@4Tlgch6)*JL4gp!x5*aPW9;Z#;iP0O91H7=B2XW-2Z2tZUCK6PU!H#c z=linwQT4D{#!|zA`DxG7U>0Z(%khvT#Uc@ON0^$g$ux7^6_vYdu-8q@p4xH{k#;lz zpXU0XPnk1pq{+AMgu<=ExZR-bP^(^H6f6PNQnLx=n`?tyh&c1r}%P134atx%DmL1G5UH0 znT7Usl}!{W-vh0lVu#;ahy@_0v*6-;mEPif5mtD~mFnbhU!hsenI*>E+o?LGE>CI^+IVHqwMAO+duKDfuQFu3l^hn}4K67N& ziP2xqSiGI#Jx|jreb@MVoAAT7tFUqcCe!(5MV0BO$~wMzw34Xp111vFE<%nE&*{Eg zDQPB@;PvI--F@Hk?!`Exz&z{Ct5?^va{~N@m-k&MxdSij(*hhX@k&=(x*ESVyM35Q zbZ2B|oy)xaUv1ckcvmpS7l!P9mJU08`5nCS!fJ~2j7IS&S3=Yv50~dsRHe>$j`%hb z-x^={kW}fLx#REok~qn} z7CFU?esrQ!Jt%QUc<}OrM=pB%hTknI;Cb^-(T3JyN=hdUm`umIN(FOLRV=K<=UB(t zrW?`YXJhhQ`hcA@MP@0sH$$l<(*3KKoXV1y2@25m4r3LmJehB`?z+?via_~20rO(9 zd)1k;`b9}z*?}LW0)~FQ6QCdkAgHVE_7z$xc8j~F6WuqmK-5^=68StJd`4=Pf4RQ- z6gcI#i)m5Ujs)1eVq zbKa?J{~~OnR@9#f^D8-MAw^RseN5}MXzWnhd?yr zB@lcfp3ipPv~>4gx)#+W>LRE8!lI3L_)EK|e{sexkUk1H@4O{}Wx}>$MGuSV*cKOI z;K2pWtAb;?LQn@EB+ddpDWVH3?(AEC6}^AK^&lJcR=(e)@Yn7#yxAOG?xGyD54bpn z-rB2Eh=ls)L9#^@JimXvYa~{DFc*LzQm_z+=W{8k1%y^}$>+@{{YonhAT7ok8R52$ z&lWtobIS8ci?5<6g0! zgGacY3AU7JLt~}TF6c$GBTZhmcGe$K8&@C(c?mR1ZcwHWISIp7E+?a z#O@#UiTF_PjLBq~v6uuF-VyFW=6d{LekQI}?C;3a-l~;-h;*rwxIgsq0RpMyjLfFS zhys2rk2XMrh5<9uES$Zsj7kMAn(LZF%%1?9ZJ?$$fppYxf+>Z2SAyDoo4C88;f@!M zrk{RDsf@PKgj@va=hH4r(_NRd04g5(42@S1e}FgBljvVPytqztqIv zCm^UzJohm{>bZY9x29F=H>XpI7cj_CFO%-<{gL~+QmIQ3#bH1Hq}QREi99wzjeo)L zt2Bj+q>{-X`|TMuLCTrZ9huA-F#XB}E8G%c5L%qKHOIqd6u37{JG{4Dy6zEZ|E6@N z+n->!ri{rA#VU{a|GFTkgCh&2S8IwF%1}a7ObG%o;PNHyEYO~Z9i(6qV5!46B=}6T zeb{D!j;5zIL?>S}CLy28si2zKzWHmqc@OTtzWL5=sm2O%Y3dTW`OqHwX`T(193*Y}jqyMQmX%M>C;~ zlc#Z5L0#sK0{J%I{3cgv5Cbr+Sk5<@pgn5b2Oic|e?Dhzu)Ad1bO(KwE>N4aw_Z7~ zht6DttfCC)^C^#C?IL3#42TTUzKpu_>xf6B6$}kmAcGk&3SiODb1QJ2UxP+xdXbXv z+~J|YG96UGmd;wM+W}tJ;s@!dOsAuL$qVaR!)wC;p(|QVPCG2?SQ7zB1cCK;7qNm~ zo?3Q$h5$Gj=`C~~m>_a+x<45;6V$4OC$j(?fah|o0FfYF9#yk z)71@Lt0pej8?*;x@Az7K{aAg9INMqv^l)nu@GL|*8Vd;|G0n_32lKRIHrKLMcdmDn zGN@n%h*D$?JkbJzaBlx>Qp4Bj*6yQ^r1!6i?63Y7)z$V*J75+SGq9dCNox51P}_rq z6Y{L{bksP!j}4$>QUnS~aBxBjnTeibBp>{5e}q6+#!JUi$HfJ-4Gu7?Hn*=msp6YN zorulqjsZv=_CPHd{!WJt@sE@ z5R=_nd%aDeJwNhJ!(X=Qq?u0F8nVln9bUWVQCRO|qvTbQ=pX7rU4pKzLMj`9UjQkW zZ#S%QuJudDZ1inq(+CL60(PaO)lkScKGZp_6u!^8;jQTeZFxmLh4*4*OWQlDKluu{ z5NeHRFG69g@`FbL$=wOe47t(_bcxKIwY-YD{P5Ks9EJ%%E>)&*cl!KO%{2&X>pG5D zCylI8W>)f3kn+*LUXZuVn44&YoG>8Wh-T$WmJ1M7VGGv}R& zQe-=upg(Wc6Qu7=g{J@XjsE>>X5}T4I{Y{IjBvf|@xNK4tNYd4>0j9&lTC~KnHr+c z!HGXTRW6rb&DH455P(zjtmIOmCSQYvn`;Jd_3kaI)?WQtCLgsg6C~eoGcnVB#bN8v zLJooa&gwM23ii%*(RQcbJg8I7bKcTHa1HMFlK zOZmck@y)-w^|?y>!A?1${nzEo{v<@N(B<5Rs@@!Q@`C)-NCYeOn*RQF;PYBThGpuw z^MF{%>1#`~3r}86P($-h`9kR`lXG1mt<$W}fprH&>sFrb_n!ZFNK)S=o7;ZhbiLGN zZPJwflc)6Q%@^(QnIoMcBQ*hif-e$zm%o+XyC7JMDifFVhC8h*>^L{y5uuzGblzHW zt5SQVZ}Ry+k7#~B*fns@IlNgZ=)1`u{dmFak0F{71-8ax*G&5tKdJZby*DK$m;1Zl zSx@dVbHLon2A{8-|(P`-T*fLdo5D zcce#RMy)p^s}GHamHN<%^ew9<*N&xsy;gN~Nt9GEU@}u2cFfUjQcmkj%vbpu*5#kU zDlK|CDE=oAsrc1v_ihwiU9gV@!zr&k;B9m)*h8Ph&)76+=o0&SEqc?pZmwxycqyi; zmi6)TD@lFqN!QVR?pc?AF1~=$zJmH=uzZl~*!XATl$c*t`rYX<*b}ADHjbS>-y0{3Qeca1djSfSV*fu{Z7Py;b>=C%b?r0 zY5U+MuNO(LV1S2X+;yn}sRCgWPsnsVGz#7Et7&BY213aXf{0NFOMhJR`_)%U^p72F zgG_c4IR)NC1Q1B%HKC_rS@G+x6C_K9w;VYRT>1mX?a45mls1_o?%&3RNL--=)bj#% zeNj8$=Gm3mbz=h_T!E94^2k~uHI5|qb@w}!v*+9=E)aE73BYb0Efy0iK_oz-LjKor zx9M@ofYBXSXZ^?E))GP@ISo-VyHs=N-2U3-I&HQDKMQ==lG#tTpjfdPgX<*a(a(b( z@tXOXNHtwGA$NS+AxR)U!Ytvn|CqZkC)DsI_OFB8e@GQil=0*4#IZeSf|c*ZE9N6h zJe9wBs&ipceDO|VA4G>o45GCQ5pVMlGa*Zw#x)~%0BXuxfThD}1HDAyq%qs{mWU$A zse{_d5L|2{v7Lx&CXf*^+$4H4IiHYnK5idn+wG8%{L1LazXLlCNxR4}&}f2(8O+im zgCxe*%U~Z*v2JtLM+u3U@nAvFVP_BZi+#X#Ss1JnhedU00l=TdgWson1d%gJa@%O$ zf~LsADIO3UnE(1Wnh*|~j@q_DWr1Z0ls3Wy z1yejJ`n|1T=^t|2GzPhCfuR!AEx^}Bfvq* zfNh&WEa>Q$Rj?7#WbC0GPaWOM(250EZCobs)EOY;c~cx5EEwB1T$rA&E|a5~0kWrb z%K-TtW|lo@TMWM3*lYt|ex2-1u^WGu2RjP4qMx8xVmcZ@GUOByFva2oh9q|aGltm~ z3l0Mm3$(*+DJcqp=kWgWIq~s_=j#gECXgCl-?!^nitB-zzL)j;FBe+w66&( z<=KiA5oyN4^d5fM^&8YEVPs^o(%NO0%H*<{Itse?v5-AXJb=Ul2p@$MF`0m4FZ=bZ z#8{dW6bTC%^j#y51&9J^v2pM`7Z{@bEo0y)lL^ts5A-u1 zXgZ3*(1(?)xBJFv&8w>!;idjKJHsOvacP_|<&wCtqKhCH!OG_-Jbaq8hs+gG@35B! z=!)cDDNG;4X&`@#lR9;B_&OAB6a(Z@yG#Dl%)?6TMPbZt?Sz6+ESfk!Ni5Ly^i5Ea zkC`tEY1e{={qF9ZjS32zyR#$iQxW&hzHN;jEYm`lVw5++9h4}DX{B3FArzQLw;7Ej z*=i_5)=g*y=w5-TNE)2&~CTL>?C_LrkFBKLO^tOr2&V71gQl?bB``bdPP zWT{KjZ`Ee%>L({d*YmO6xP5RL%@e|;AJdZF7%Xqx+me9gxw52?X3TvDl%vG;VlHi6 zFr?;&_DY(e3tfA%o+4x$<+3WMf}2VfGCD&YgGFOs``&*sU~^n7rN#cU6Q`wm`L|Km z>O+mXx269)irjMrFt~E|?S%JJJ68z~Kl90<4)Sd_sFYior^%0NuB3u1zdf#BYfoIO zN6|#-hCXmXzEv!t$1)BL7~+F9l5v9{SS1o2Ac8^CAq3|#pYBx}(RkM=(|x&--y-z( zZ3d~)D0D3;GO9%=1^+Qu;>;j!7dpy-*ztq;rG9u`AO#GuqFt6E~O zI{&^Fq;E6A2CN$4Btd_EjKJ)7c;0CJ06XC4WVrQlOTl&nmL6o|7CDwXuCXQMM}=8P z$=I2}oQtY-vdoy^Xp_h6&{t{BN_Jd0m?6klKOI>Ga4y$BaU^TP(ac zP!j2xD3+7}gFxsd_&l~xGk5;po5x~juF3OaAVe+}n#yB?hIl&-#I!``;yt;WX)of;_+X=yK=={0=>G3R z^0&dr#ZwJEDpaY~I3+TW`UJEGueguH3U_OJ`n_((OcpK@BV&b5A(}5Gv27c-a9ci`*={H;eOO)iBtsr;0rN1ctT|HdN%?zfQz3Mm zy*}&ylLorfKJm%>E0@Dc&_s&S)}CpP-CnqWWQllc5h3p@Ls-XeAFuk+=;YRkytWuj zDq*oum=xeBif`?k{G{?tfY{Vo)NV|fH3V_vP+xgS-*vxp1BPq2bNG{6zpAB}*U_ie zN=tJrj>2vQEIppMeWl@=C?3a;XNzI2f4K5`m9SS&keazReCwG1-`Vi#Hm~cLG&)(fY0y6{;ylQMVyi@o0Jq(e# z%MK}Ww9NlJ%_;C(Lt~WMjW)Wu@CInNC+XtsJNcjsHSQNEa>w`Z-@bG9&TD&@8X#lx z+jiq$;=o8IR<(L*!~jSG!bLukUQN+NG?DcJFd|$_Z}ZWOl%_+9BU$7^_V)z&Q1G zN~!7Jb7O50EQ5e{%Yk|YB{*zc1C0t?K4y4*cG7A}$p(pl zbpNRJba?i^{Ld=t2ggDq^z{pGOW^fyga|rbjLOuiba_lw`eOrYJ0@l}zch&4{NU^V z2Vr1U87cmy@Q~-Vh)GYI&g972MrMdrpL*WA+8-51#CKdhiz`c2Q1<0mhFH~Fis9k`$stkF!bon8!3yc!YS#co$IuK7Bn z6v^*t78yx>a~?spNUpWBfj+DmY0;e`86&5ICU(^uAo#%mH!M$75CSEe4QTJ zur@qn!xU^fyB|CCFs{L8qEP4bzl;hulT7|vy>6Gjn0B|~^X2~j5YW$ZvQ*2SEXPVSYuo2(&9x zo&42RwW*L1rmJEJwBebfj8+sO! zHMg!`9N~Y_?P-5n?`+Ka;r+Tf({#*j`-S2Qrjk=aZ;;5XX+O2A24PzS1F0h7WfD%PBjTZu4}zq8x?-b`knQmgzkw zjYO%ypx9D^vFfSikuN$;&qMg_yCb297_-|wvr!HkGxvHVM2G`EZ6^)wE$;-#qpvO% zOa4vQBm5^z7d^dlA{pZ44?Elo54#;^7c-z(Z+N_$R2W8MOpMj~c zzr?C6phD(`KK;UPWrQD$c)>v>_+M&Zr44prluT`H>nUI6VV`RbGtS16B_$?g%0P^#=|`!I8F1`cI<6{GJSUMeG__JKQHjYz zfxne^=M8FELcdfdsQjzQxjOQ9Z?)d&!3SmiROl*6WE0Zvt~R{bVoFb`Xe){U*;KLL5UXp~(1>XZ?7{`;(({p@ zUp@ywB71G$$W_ZC@g*JXvo+BajoU##>_zFML(rJlIsad%vT`j_0w-JrXQdmU7`hiEMCkI zJrn-o)mGQWmuD~iz!kgqh-I}o*r**wlkvCqvGuPVs9rh~xRpDXOCK;) zIyWSy#aXhu&ir2P(5;8@d2_UYyfTzKv-#QNuX1?H$ihLT0YOMw=gT+fYU!+$xV*1F z+4&GF2qocs%^NE}YW@4!72~CEzFjeErki7?kMt|YlKykWl&Igrwa{}MEkzJg4={IN zYjV4@Uk$CEG97xk&P;a;|A{-Z$go{XNgu0}9jFXUOVYO*G%mk6VD1S1^uo#6D(W&_uWYA_>M4PrRqsP*D=y5Sm{Me zkiKES>r%~cGGxtx_Cv9Qm7s$Ytk3_NqmGTt`)^+P%~T>>?5VW(Q>u97#X0{Z!!Whi zCLYAT_?{C^rG;l+^t14DRVi>h;b3>sFEFPyPENzGMN{Y67jR5|%e#zehz~+3+uqNw z74F)dnN(7FNI!WX*&w;VCpGD~fw%|(6k{lHiY%Vrme(Ga%^!Azd`NpEMUe87RC*JH zQYsM{t?Z~4Z_CfA*I=n#s5xKe59_J-y6D#}fUnKllr>iR+g~Yi^@GurFy+su^jbZP zQr=%l)t4eyE?wyI)a-9*N(d^5^nJ-C>xImre8=@ZBK$M}oDqHv9^-m-+ELm6oX>!V zr~iNfM}l=+pVdcy*~bFe`jSHf(q*(@g9a0DJ2GS7QuY0FM$&J>Z{Je7?T5*G2(uCHSN$lLR^G)Q<^jJk&vCLDb! zPP+dAGe_&%wU6B>?o*Cp%{LoZ2?=1nF(Wgrgb)9#%bJ4xK;!Z$7&)vD>xLhCTNHo1 zXaj&{+Q@%V(>fQLE1gKm(2&!P&~kBz7#L2gNX@1;!gYc58%yN!BENI9t2svsz zhe4jZH0rkj;)Ax=^GIYjAUo9RQX`^1Cv4WeOBO=^c635FoK|*RiJ~2x-r`@&+S)k< z-spV-?S?Kvd(D^DMs=6nm(C5ZZKyn3ez4WCSiF91!*@wu_~voslkv9qv%}$1)A7tB zC0if-J(=3V=;E!qMgJUqvg+H0{OE?o<_&}OTor%aCD5kFhUj|eN|!MCN%F$hujq&C zCzcxiPHvbkM1!K+q6?zSwnR5Qx6)VLbGqCcUb!`GJvNR0t|zJe*~|oDt|%)1>%J;T zt)M$7D7xD5&_)=`96VX$boZu0&HD$6J;~8-G|u>|&U4vxIp z1QWhFvVlLSR`cSdo_q&xGQve3`keyvdoK8&z#E&om_=R>j4O^heL+CoA7;SnY{_1> zNO*0gc4kKmTp3#~DjoMCfJ0L|r>9@&W4O%KyaADo%;2e(&7(ymF#jS*JX!_%HT(F9Wuj}p*WUlo7?RN zz(oa-KaDwBZNFy!f9xlpiHfZ4?V9`NF>ARW$4>-`F=yV7Zk_Q=BK>jeZKrKNkN}3H zT5kjTTSE^3+PMUp5uY zC?ClEO|w{5?5hrT^($9QR?T^xi;S(iO;i8?{`G(W00f`^!9I0$t^i&8ba(&$x~F7@rd z))5cX@AB4FFKjre53QQ+qGJltV`okoiZG$kC0&D9aNLqZ718h#ey|q)L51L1Jq2ax zb96}m5xb9;_l|xmG+J@`%8h;y`xQzfVZDbyQ;xTuc+KT1Nxf}Q5xNk!b!Z^mn3^lAA?}p$?SQGq zYJ^Z!N*+M&*^N|e@y|IKU;rW*$cZ98$90)f7}wiiAa2UTGm4J3{LWEk=+#hYJ*3`& zD=P3L`dYAXQ?%O(gt?LzY`qU+S1IJ-;~wh|$fK;wlqAcf=(fBEOlNJ5Ei{s+d=zxq zH9uI{W&u<`hm2$#v_JVhgWCS{HqsZE+@%f`DAeiRsz+oN9ya(=6Nss^PyXn@r>bPz zd_HiBC8drD_KsOgU;q0NX^Rjl|-&Gf8Np4T3W% zyFQ~v^=Jf&7lpc_8-+5)D>VFtBWR($R3r!pdB*M7W#|Qe{2luz04<8@nqC4R>`yCo z4F%@~4B)X6zX+y#mg72{*&NqrVrMpD3C`zgBf3R zJQE?&{Um3JM4SLa7Cg|8P<=^5ZnB~E^;Y<= z$`uqgrN0NT)B>1|jAsD+KoXzN4a^DOR+vJZ#^CmUD1clqB=bskNT8UK;{~-$(qbWG zuF=sSp{PSuHfT@QARFl04W`*{j{^GiS?V6_eTN~kYKL8Q55<6a)Zk|=h3@nV9&G_Y zwh7gtHw#Gj;(V>mVj)JfHj?1>~myu*!dZ+B7#}r~yv6JTP#uftd7{{C@~R z&j@2?=vRMWc^iEXzoPXxuC3@SUaq%Shw#Ej5N2w4?ngk8m+Cr}P=~O=;`uxh9ssOL z><)dcq1ZpnK|WXg4kyUq#%VbCQ3WmQ-v*xsW5{UFAb}KO5G4V_8DwyR=lfQ_cZB zEAKDo5ecUl;yY0kLp)+xz)Zzn(h-+l*(SfrQ&~ivqC|L}Cyx~U-jik-Ota!Cq}eYk z{bu>g7L8tqY7_oO7N13c(jf$fKh8!+%x{&D#vlY9Fr)?q!^6K15hCt;0Pa0o|4Hy)b0X>t&&q@=rKBu~E4|tumxJqNV z&Dg)3^3*a88SqGJb8aET&jyri&80EW7R>7Fc4*kn!C}a$`6hV^KVNcPI!Xt+j5P4s4scec!JfrU@h{YWVuV~! zcoc?T4_Ie94HQZtS%L*tred}8N||`0#{GqSMO?ZDS-J*@yR&SJhK(W{5D4t<2@d}o?!`+XLDRqFd&A@sIYUoSrv#JC77os4_bVYEv>!9}YkHtLrD(Tyj8 z5El&$i11$kmjR(YDa~+3;=qu<<{lRD2meP7a7)&LI+)LqFPjpUa!^oU-R|yhTwp4S6S-4s&A35TQ#G=9Y`*^ z4KKo%DFfI|KNUuV_#{j^*>G$rtGSWw@=E5}B8W!T?y;!`!kW9B)MRy;RxyJrxH;;e*1M>@@BUmgrSLbA1sSWcL?a-A)?e`q( z0MCVs#f>Ufah(`t4MyyaFF6(Hj z+W}OXsn-i!Hk+yO|CoJUy99j~hq*>`+CmWQ{U$xyRsKMXUv44Y9Nz1P;JXPt;UQA$Pn?os&%Tbhg zFcCi|Rl8gXbHS?>V$+MIiEk->X!2-h=`oa1$CTb{oBzkE2u-Sr9Nx*Me`J-r75I2V zx2eUdy0pP-F|bM^hFUe5Ba@Fre?{_gw0^F>K`MQnAYLC2(QA1Vx`GrkJB`oJfscn? zx5DMin;jnJld(zBPvVz+Eb<_K9mPf&$Rh1&STf#Ys8_phE9PSB@y_j_zIjeL?vGUO z{;g{AdSOXU318oEE~=Z;N)*?#^T_^zIhZu)>J_D04O!z1D0pw_x_hB(R1>U{Uq|FL z93_>#2{|~@oHds#Hxs`Y^&iSp{DLb5XLIZ6YR%I6@A6LSC+`*X8n*K^2SadpFcE<( z06wea)=)r)$}HELLgFN*-jmvr(7&--p|~#-QE#Q0Ayia8m<<5b*;ol^(LE0bk|f`u zdk0Q)%!jCn8{03J%S2J3!W4PPpd`mykwb_n?-QiBoow(|UF*GdwAG)c?L2v0xdQ)b zo0fb&BmIsCW#8+~YWF5XD7{HKOa|Y28FE_bAD`(N-8Y2WB#QDi@2j{&zl*cVpSN5P z8h#mVdyr+9f}*NA{DbacU%(Zg4D(&cQu3YM^^5LH_h>65maA`)Hg`3b$B|_G8xI4d zJfTw{G!;9ZxOH@524C6%W}Bs`4aAwRZ5gUm7Nf_G`}tyDO4=zrrdNJc5kl1~qQ28K(s$C2v|V>OGJJ|Oky^*Q*mI}y;>ij^<0AZdttcZ$6W;r0p_dc=g`5JL z!_!)%b=WMN%{PPr?LNnpIe4U=VUWzVkXtraJ6@y(Wy+a5B~h}caOfHoww9Zii!K2# zbY72}1Rw?w)cW7=Ed)ysZD-t^R!i#FO)J=+QKKt-?TgC`b!@-p(~XJ#2j>GpBupAs ztHwp~1<`u`{TqJ$=T40L0d(ocM~WzO>s1q3grC`)gU|%+DZM$otb*`?5rCH|K$vsgq_+`Nw7VhlD0U#pa1?drcY{F@dd3*Nl_u5hD zrna~j$qnp3vu_^(u#h5~$S{)fxg#EDI30ERm21FG?dDetTGj%~uM*TwxHBt9B>%e* z!+1p10+a7Z000yG+*~SdeX@5ibEye zX69LnZg9jHcq|Y(I?=knPGF>J%xDsE<-BScl0uw|-|6xNgBTDM)Z@#1CqNvL*zo{YbjhiQBV8I=4#N;rsR-Z_yYwrKh6wf;@>@ zO6Fm_{{v)3B^Nz#g_84talI*g__KNOn>As_d^;@wK_eexgM2mCW$0Orj&<>b(_a>j zmZKHY+u5(Mw*yj5^)b>a0SB3ep1@<1xexFL^{W6^Rc>$q+ShF$!!;GLprfpf8&c>= z-4htLQJDMJ)@qZC=-ORR`NanOXbMf#B(Pa`g==M*-g|LRMi6~?DxTx@+1Z)=E>7cnV`OKpKK@q@{|;Tdg}~d|&r!=z z!y(j2&xh(}EL2P9r)nc|ZmR`4ngMeMOY#}Ufr=&mS;Qf%@+m+7FRvt7dN6ixREUiM zf7)w{n6&OwD$RZ;0fw1(luZO5M~?){FZ(UZ5?wM)YA7{w6*oo=a{I zzkI|VOgOJ|_8Cc>(48S6lkx>?C7rO~0@%_iIih&X5OR zAzyO(FWVIQQ28@4-Qv7U?jEy@hcHGOkovgz8_R#Rb({79PdY>nQ%Y4kr)olSPD-ud z=PF8~X`C&Ic`$&pam}cH-P%W`fj)PCl;Hc1^Dbr!R4}+W0BYeW@Hmt49JfhWpThFd zm@Nn2Xgvhx=~}W@lQ~ujus;f6>#%sO5PZP&`u7`{lC%FSS^8+3?jANnCg6wddoa?$ KzUb%x0000IRB(p? literal 0 HcmV?d00001 diff --git a/web/public/empty-state/pages/navigation-pane/outline-dark.webp b/web/public/empty-state/pages/navigation-pane/outline-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..dcd26c3950b75609dbf14a2f9abbe1247739da8e GIT binary patch literal 18452 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 Z*A=aXJM{{G%bCUr@