From 6600076cef09432c1876cbfac7148c0833c98f5b Mon Sep 17 00:00:00 2001 From: fccview Date: Thu, 29 Jan 2026 14:22:55 +0000 Subject: [PATCH 01/28] Fix various sidebar state issues --- .../Checklists/Parts/ChecklistClient.tsx | 6 +-- .../FeatureComponents/Notes/NoteClient.tsx | 4 +- .../FeatureComponents/Sidebar/Sidebar.tsx | 52 ++++++++++++------- app/_providers/AppModeProvider.tsx | 5 +- app/_utils/sidebar-store.ts | 15 +++++- 5 files changed, 56 insertions(+), 26 deletions(-) diff --git a/app/_components/FeatureComponents/Checklists/Parts/ChecklistClient.tsx b/app/_components/FeatureComponents/Checklists/Parts/ChecklistClient.tsx index 36691c7d..df2f4630 100644 --- a/app/_components/FeatureComponents/Checklists/Parts/ChecklistClient.tsx +++ b/app/_components/FeatureComponents/Checklists/Parts/ChecklistClient.tsx @@ -60,9 +60,9 @@ export const ChecklistClient = ({ setLocalChecklist(updatedChecklist); }, []); - const handleBack = () => { + const handleBack = () => { checkNavigation(() => { - router.push("/"); + router.push("/?mode=checklists"); }); }; @@ -106,7 +106,7 @@ export const ChecklistClient = ({ const handleDelete = (deletedId: string) => { checkNavigation(() => { - router.push("/"); + router.push("/?mode=checklists"); }); }; diff --git a/app/_components/FeatureComponents/Notes/NoteClient.tsx b/app/_components/FeatureComponents/Notes/NoteClient.tsx index 16970ac7..e9d23192 100644 --- a/app/_components/FeatureComponents/Notes/NoteClient.tsx +++ b/app/_components/FeatureComponents/Notes/NoteClient.tsx @@ -41,7 +41,7 @@ export const NoteClient = ({ note, categories }: NoteClientProps) => { const handleBack = () => { checkNavigation(() => { - router.push("/"); + router.push("/?mode=notes"); }); }; @@ -75,7 +75,7 @@ export const NoteClient = ({ note, categories }: NoteClientProps) => { const handleDelete = () => { checkNavigation(() => { - router.push("/"); + router.push("/?mode=notes"); }); }; diff --git a/app/_components/FeatureComponents/Sidebar/Sidebar.tsx b/app/_components/FeatureComponents/Sidebar/Sidebar.tsx index 2768a387..311e2dff 100644 --- a/app/_components/FeatureComponents/Sidebar/Sidebar.tsx +++ b/app/_components/FeatureComponents/Sidebar/Sidebar.tsx @@ -17,6 +17,7 @@ import { Modes } from "@/app/_types/enums"; import { SidebarProps, useSidebar } from "@/app/_hooks/useSidebar"; import { usePathname, useSearchParams } from "next/navigation"; import { useAppMode } from "@/app/_providers/AppModeProvider"; +import { useSidebarStore } from "@/app/_utils/sidebar-store"; import { useEffect } from "react"; import { useTranslations } from "next-intl"; @@ -42,28 +43,41 @@ export const Sidebar = (props: SidebarProps) => { const sidebar = useSidebar(props); - useEffect(() => { - const searchMode = searchParams?.get("mode") as typeof mode; - const localStorageMode = - mode || (localStorage.getItem("app-mode") as typeof mode); + const { mode: storedMode, setMode: setStoredMode } = useSidebarStore(); + const searchMode = searchParams?.get("mode") as typeof mode; + const isLastVisited = user?.landingPage === "last-visited"; - let updatedMode = - user?.landingPage !== "last-visited" - ? user?.landingPage - : localStorageMode || Modes.CHECKLISTS; + const persistedMode = isLastVisited && typeof window !== "undefined" + ? localStorage.getItem("app-mode") as typeof mode + : null; - if (isSomePage) { - updatedMode = isNotesPage - ? Modes.NOTES - : isChecklistsPage - ? Modes.CHECKLISTS - : sidebar.mode || Modes.CHECKLISTS; - } + const defaultMode = !isLastVisited + ? user?.landingPage || Modes.CHECKLISTS + : Modes.CHECKLISTS; + + let sidebarMode = searchMode || storedMode || persistedMode || defaultMode || Modes.CHECKLISTS; + + if (isSomePage) { + sidebarMode = isNotesPage + ? Modes.NOTES + : isChecklistsPage + ? Modes.CHECKLISTS + : sidebarMode; + } - setMode(searchMode || updatedMode || Modes.CHECKLISTS); - }, []); + useEffect(() => { + if (mode !== sidebarMode) { + setMode(sidebarMode); + } + if (storedMode !== sidebarMode) { + setStoredMode(sidebarMode); + } + if (isLastVisited && sidebarMode) { + localStorage.setItem("app-mode", sidebarMode); + } + }, [sidebarMode]); - const currentItems = mode === Modes.CHECKLISTS ? checklists : notes || []; + const currentItems = sidebarMode === Modes.CHECKLISTS ? checklists : notes || []; if (!sidebar.isInitialized) return null; @@ -87,7 +101,7 @@ export const Sidebar = (props: SidebarProps) => { } navigation={ } diff --git a/app/_providers/AppModeProvider.tsx b/app/_providers/AppModeProvider.tsx index b74f44b8..d531046c 100644 --- a/app/_providers/AppModeProvider.tsx +++ b/app/_providers/AppModeProvider.tsx @@ -22,6 +22,7 @@ import { import { Modes } from "@/app/_types/enums"; import { LinkIndex } from "../_server/actions/link"; import { buildTagsIndex } from "../_utils/tag-utils"; +import { useSidebarStore } from "../_utils/sidebar-store"; const AppModeContext = createContext(undefined); @@ -93,10 +94,12 @@ export const AppModeProvider = ({ } }, [initialUser]); + const { setMode: setStoredMode } = useSidebarStore(); + const handleSetMode = (newMode: AppMode) => { setMode(newMode); setSelectedFilter(null); - localStorage.setItem("app-mode", newMode); + setStoredMode(newMode); }; const tagsEnabled = appSettings?.editor?.enableTags !== false; diff --git a/app/_utils/sidebar-store.ts b/app/_utils/sidebar-store.ts index ae2088fa..7a380d12 100644 --- a/app/_utils/sidebar-store.ts +++ b/app/_utils/sidebar-store.ts @@ -1,7 +1,11 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; +import { AppMode } from "@/app/_types"; interface SidebarState { + mode: AppMode | null; + setMode: (mode: AppMode) => void; + sidebarWidth: number; setSidebarWidth: (width: number) => void; @@ -77,6 +81,12 @@ const migrateOldLocalStorage = (): Partial => { migrated.collapsedTags = JSON.parse(oldCollapsedTags); localStorage.removeItem("sidebar-collapsed-tags"); } + + const oldMode = localStorage.getItem("app-mode"); + if (oldMode && (oldMode === "checklists" || oldMode === "notes")) { + migrated.mode = oldMode as AppMode; + localStorage.removeItem("app-mode"); + } } catch (error) { console.error("Failed to migrate old sidebar localStorage:", error); } @@ -87,6 +97,9 @@ const migrateOldLocalStorage = (): Partial => { export const useSidebarStore = create()( persist( (set, get) => ({ + mode: null, + setMode: (mode) => set({ mode }), + sidebarWidth: 320, setSidebarWidth: (width) => set({ sidebarWidth: Math.max(320, Math.min(800, width)) }), @@ -165,7 +178,7 @@ export const useSidebarStore = create()( { name: "sidebar-state", partialize: (state) => { - const { scrollTop, ...rest } = state; + const { scrollTop, mode, ...rest } = state; return rest; }, onRehydrateStorage: () => (state) => { From 40f45f80feaeb81dfdcf44400fdbb1de9ec6ef0d Mon Sep 17 00:00:00 2001 From: fccview Date: Thu, 29 Jan 2026 14:57:08 +0000 Subject: [PATCH 02/28] actually calculate char width and line height to accurately show line numbers --- .../Parts/TipTap/SyntaxHighlightedEditor.tsx | 55 +++++++++++-------- app/_styles/globals.css | 27 +++++++++ 2 files changed, 58 insertions(+), 24 deletions(-) diff --git a/app/_components/FeatureComponents/Notes/Parts/TipTap/SyntaxHighlightedEditor.tsx b/app/_components/FeatureComponents/Notes/Parts/TipTap/SyntaxHighlightedEditor.tsx index a2a3956c..8e0b79ee 100644 --- a/app/_components/FeatureComponents/Notes/Parts/TipTap/SyntaxHighlightedEditor.tsx +++ b/app/_components/FeatureComponents/Notes/Parts/TipTap/SyntaxHighlightedEditor.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import Editor from "react-simple-code-editor"; import Prism from "prismjs"; import "prismjs/components/prism-markup"; @@ -19,11 +19,7 @@ interface SyntaxHighlightedEditorProps { onCodeBlockRequest?: (language?: string) => void; } -const editorFontStyle = { - fontFamily: "Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace", - fontSize: "14px", - lineHeight: "21px", -}; +const LINE_HEIGHT = 21; export const SyntaxHighlightedEditor = ({ content, @@ -34,8 +30,9 @@ export const SyntaxHighlightedEditor = ({ onCodeBlockRequest, }: SyntaxHighlightedEditorProps) => { const { user } = useAppMode(); - const [lineCount, setLineCount] = useState(1); const editorRef = useRef(null); + const charWidthRef = useRef(0); + const [editorWidth, setEditorWidth] = useState(0); const pendingSelectionRef = useRef<{ start: number; end: number } | null>( null ); @@ -43,8 +40,27 @@ export const SyntaxHighlightedEditor = ({ usePrismTheme(user?.markdownTheme || "prism"); useEffect(() => { - setLineCount(content.split("\n").length); - }, [content]); + const el = document.createElement("span"); + el.className = "markdown-line-measure"; + el.textContent = "x".repeat(100); + document.body.append(el); + charWidthRef.current = el.offsetWidth / 100; + el.remove(); + + const updateWidth = () => { + const pre = editorRef.current?.querySelector("pre"); + if (pre) setEditorWidth(pre.clientWidth - 32); + }; + updateWidth(); + const obs = new ResizeObserver(updateWidth); + if (editorRef.current) obs.observe(editorRef.current); + return () => obs.disconnect(); + }, []); + + const calcHeight = (line: string) => { + if (!editorWidth || !line || !charWidthRef.current) return LINE_HEIGHT; + return Math.max(1, Math.ceil((line.length * charWidthRef.current) / editorWidth)) * LINE_HEIGHT; + }; useEffect(() => { if (pendingSelectionRef.current) { @@ -174,23 +190,14 @@ export const SyntaxHighlightedEditor = ({ >
{showLineNumbers && ( -
- {Array.from({ length: lineCount }, (_, i) => i + 1).map((num) => ( -
{num}
+
+ {content.split("\n").map((line, i) => ( +
+ {i + 1} +
))}
)} - Date: Thu, 29 Jan 2026 19:31:34 +0000 Subject: [PATCH 03/28] add allowed origins to next config --- howto/ENV-VARIABLES.md | 27 +++++++++++++++++++++++++++ next.config.js | 7 ++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/howto/ENV-VARIABLES.md b/howto/ENV-VARIABLES.md index 89c2691a..14c55282 100644 --- a/howto/ENV-VARIABLES.md +++ b/howto/ENV-VARIABLES.md @@ -58,3 +58,30 @@ OIDC_ADMIN_GROUPS=admins - `OIDC_USER_ROLES=user,member` Optional. Comma-separated list of OIDC roles allowed to access the application. If set, only users with these roles (or admins) can log in. - `OIDC_GROUPS_SCOPE=groups` Optional. Scope to request for groups. Defaults to "groups". Set to empty string or "no" to disable for providers like Entra ID that don't support the groups scope. - `OIDC_LOGOUT_URL=https://authprovider.local/realms/master/logout` Optional. Custom logout URL for global logout. Full URL to redirect to when logging out. + +### Swag configuration + +Sometimes using a swag may cause problems with Next. +If you see an error similar to this + +``` + jotty | `x-forwarded-host` header with value `jotty.mydomain.com` does not match `origin` header with value `jotty.mydomain.com:` from a forwarded Server Actions + request. Aborting the action. + jotty | Error: Invalid Server Actions request. + jotty | at rE (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:15:7338) + jotty | at no (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:19:1150) + jotty | at /app/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:20:726 + jotty | at AsyncLocalStorage.run (node:async_hooks:346:14) + jotty | at Object.wrap (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:13:17831) + jotty | at /app/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:20:616 + jotty | at AsyncLocalStorage.run (node:async_hooks:346:14) + jotty | at Object.wrap (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:13:16935) + jotty | at ni (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:20:543) + jotty | at nT.render (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:20:4578) { + jotty | digest: '340658036' + jotty | } +``` + +You can this env variable to match the exact origin (you can add comma separated urls): + +`ALLOWED_ORIGINS=jotty.mydomain.com:,jotty.mydomain.com:` \ No newline at end of file diff --git a/next.config.js b/next.config.js index c353f48c..04fa970f 100644 --- a/next.config.js +++ b/next.config.js @@ -13,7 +13,12 @@ const nextConfig = { output: 'standalone', experimental: { serverComponentsExternalPackages: [], - webpackBuildWorker: true + webpackBuildWorker: true, + serverActions: { + allowedOrigins: process.env.ALLOWED_ORIGINS + ? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim()) + : undefined, + }, }, swcMinify: true, images: { From 662ec9bc3ed44c7ab47ef1dff22ea230b3b78333 Mon Sep 17 00:00:00 2001 From: fccview Date: Sat, 31 Jan 2026 08:25:19 +0000 Subject: [PATCH 04/28] persist table of content --- .../Notes/Parts/NoteEditor/NoteEditor.tsx | 5 ++-- app/_utils/notes-store.ts | 19 ++++++++++++ howto/ENV-VARIABLES.md | 29 +------------------ next.config.js | 7 +---- package.json | 2 +- 5 files changed, 25 insertions(+), 37 deletions(-) create mode 100644 app/_utils/notes-store.ts diff --git a/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditor.tsx b/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditor.tsx index ad7145d9..3a43aa6b 100644 --- a/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditor.tsx +++ b/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditor.tsx @@ -5,10 +5,11 @@ import { UnsavedChangesModal } from "@/app/_components/GlobalComponents/Modals/C import { useNoteEditor } from "@/app/_hooks/useNoteEditor"; import { NoteEditorHeader } from "@/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditorHeader"; import { NoteEditorContent } from "@/app/_components/FeatureComponents/Notes/Parts/NoteEditor/NoteEditorContent"; -import { useState, useRef } from "react"; +import { useRef } from "react"; import { TableOfContents } from "../TableOfContents"; import { useSearchParams } from "next/navigation"; import { usePermissions } from "@/app/_providers/PermissionsProvider"; +import { useNotesStore } from "@/app/_utils/notes-store"; export interface NoteEditorProps { note: Note; @@ -27,7 +28,7 @@ export const NoteEditor = ({ }: NoteEditorProps) => { const { permissions } = usePermissions(); const isOwner = permissions?.isOwner || false; - const [showTOC, setShowTOC] = useState(false); + const { showTOC, setShowTOC } = useNotesStore(); const decryptModalRef = useRef<(() => void) | null>(null); const viewModalRef = useRef<(() => void) | null>(null); diff --git a/app/_utils/notes-store.ts b/app/_utils/notes-store.ts new file mode 100644 index 00000000..f367f452 --- /dev/null +++ b/app/_utils/notes-store.ts @@ -0,0 +1,19 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +interface NotesState { + showTOC: boolean; + setShowTOC: (show: boolean) => void; +} + +export const useNotesStore = create()( + persist( + (set) => ({ + showTOC: false, + setShowTOC: (show) => set({ showTOC: show }), + }), + { + name: "notes-state", + } + ) +); diff --git a/howto/ENV-VARIABLES.md b/howto/ENV-VARIABLES.md index 14c55282..4d959502 100644 --- a/howto/ENV-VARIABLES.md +++ b/howto/ENV-VARIABLES.md @@ -57,31 +57,4 @@ OIDC_ADMIN_GROUPS=admins - `OIDC_USER_GROUPS=jotty_users,app_users` Optional. Comma-separated list of OIDC groups allowed to access the application. If set, only users in these groups (or admins) can log in. - `OIDC_USER_ROLES=user,member` Optional. Comma-separated list of OIDC roles allowed to access the application. If set, only users with these roles (or admins) can log in. - `OIDC_GROUPS_SCOPE=groups` Optional. Scope to request for groups. Defaults to "groups". Set to empty string or "no" to disable for providers like Entra ID that don't support the groups scope. -- `OIDC_LOGOUT_URL=https://authprovider.local/realms/master/logout` Optional. Custom logout URL for global logout. Full URL to redirect to when logging out. - -### Swag configuration - -Sometimes using a swag may cause problems with Next. -If you see an error similar to this - -``` - jotty | `x-forwarded-host` header with value `jotty.mydomain.com` does not match `origin` header with value `jotty.mydomain.com:` from a forwarded Server Actions - request. Aborting the action. - jotty | Error: Invalid Server Actions request. - jotty | at rE (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:15:7338) - jotty | at no (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:19:1150) - jotty | at /app/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:20:726 - jotty | at AsyncLocalStorage.run (node:async_hooks:346:14) - jotty | at Object.wrap (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:13:17831) - jotty | at /app/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:20:616 - jotty | at AsyncLocalStorage.run (node:async_hooks:346:14) - jotty | at Object.wrap (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:13:16935) - jotty | at ni (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:20:543) - jotty | at nT.render (/app/node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:20:4578) { - jotty | digest: '340658036' - jotty | } -``` - -You can this env variable to match the exact origin (you can add comma separated urls): - -`ALLOWED_ORIGINS=jotty.mydomain.com:,jotty.mydomain.com:` \ No newline at end of file +- `OIDC_LOGOUT_URL=https://authprovider.local/realms/master/logout` Optional. Custom logout URL for global logout. Full URL to redirect to when logging out. \ No newline at end of file diff --git a/next.config.js b/next.config.js index 04fa970f..c353f48c 100644 --- a/next.config.js +++ b/next.config.js @@ -13,12 +13,7 @@ const nextConfig = { output: 'standalone', experimental: { serverComponentsExternalPackages: [], - webpackBuildWorker: true, - serverActions: { - allowedOrigins: process.env.ALLOWED_ORIGINS - ? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim()) - : undefined, - }, + webpackBuildWorker: true }, swcMinify: true, images: { diff --git a/package.json b/package.json index 799ced81..fe109492 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jotty.page", - "version": "1.19.1", + "version": "1.19.2", "private": true, "scripts": { "dev": "next dev", From 7eb65f1a4925fecd738ea6715e0314fa57bf1174 Mon Sep 17 00:00:00 2001 From: fccview Date: Sat, 31 Jan 2026 08:49:15 +0000 Subject: [PATCH 05/28] add ruler to markdown editor --- .../Notes/Parts/TipTap/MarkdownEditor.tsx | 45 +++-- .../Notes/Parts/TipTap/MinimalModeEditor.tsx | 60 +++---- .../Parts/TipTap/SyntaxHighlightedEditor.tsx | 46 +++-- .../Notes/Parts/TipTap/TipTapEditor.tsx | 5 - .../TipTap/Toolbar/EditorSettingsDropdown.tsx | 97 +++++++++++ .../Parts/TipTap/Toolbar/TipTapToolbar.tsx | 57 +----- .../Notes/Parts/TipTap/VisualGuideRuler.tsx | 162 ++++++++++++++++++ app/_translations/de.json | 4 +- app/_translations/en.json | 4 +- app/_translations/es.json | 4 +- app/_translations/fr.json | 4 +- app/_translations/it.json | 4 +- app/_translations/klingon.json | 4 +- app/_translations/ko.json | 4 +- app/_translations/nl.json | 4 +- app/_translations/pirate.json | 4 +- app/_translations/pl.json | 4 +- app/_translations/zh.json | 4 +- app/_utils/notes-store.ts | 33 ++++ 19 files changed, 427 insertions(+), 122 deletions(-) create mode 100644 app/_components/FeatureComponents/Notes/Parts/TipTap/Toolbar/EditorSettingsDropdown.tsx create mode 100644 app/_components/FeatureComponents/Notes/Parts/TipTap/VisualGuideRuler.tsx diff --git a/app/_components/FeatureComponents/Notes/Parts/TipTap/MarkdownEditor.tsx b/app/_components/FeatureComponents/Notes/Parts/TipTap/MarkdownEditor.tsx index a5a06892..a341c27a 100644 --- a/app/_components/FeatureComponents/Notes/Parts/TipTap/MarkdownEditor.tsx +++ b/app/_components/FeatureComponents/Notes/Parts/TipTap/MarkdownEditor.tsx @@ -1,10 +1,12 @@ import { SyntaxHighlightedEditor } from "./SyntaxHighlightedEditor"; +import { useNotesStore } from "@/app/_utils/notes-store"; +import { VisualGuideRuler } from "./VisualGuideRuler"; +import { useEffect, useState } from "react"; interface MarkdownEditorProps { content: string; onChange: (e: React.ChangeEvent) => void; onFileDrop: (files: File[]) => void; - showLineNumbers?: boolean; onLinkRequest?: (hasSelection: boolean) => void; onCodeBlockRequest?: (language?: string) => void; } @@ -13,10 +15,21 @@ export const MarkdownEditor = ({ content, onChange, onFileDrop, - showLineNumbers = true, onLinkRequest, onCodeBlockRequest, }: MarkdownEditorProps) => { + const { showLineNumbers, showRuler, showVisualGuides, visualGuideColumns } = useNotesStore(); + const [charWidth, setCharWidth] = useState(0); + + useEffect(() => { + const el = document.createElement("span"); + el.className = "markdown-line-measure"; + el.textContent = "x".repeat(100); + document.body.append(el); + setCharWidth(el.offsetWidth / 100); + el.remove(); + }, []); + const handleValueChange = (newValue: string) => { const syntheticEvent = { target: { value: newValue }, @@ -25,15 +38,25 @@ export const MarkdownEditor = ({ }; return ( -
- +
+ {showRuler && ( + + )} +
+ +
); }; diff --git a/app/_components/FeatureComponents/Notes/Parts/TipTap/MinimalModeEditor.tsx b/app/_components/FeatureComponents/Notes/Parts/TipTap/MinimalModeEditor.tsx index a1678390..126afd08 100644 --- a/app/_components/FeatureComponents/Notes/Parts/TipTap/MinimalModeEditor.tsx +++ b/app/_components/FeatureComponents/Notes/Parts/TipTap/MinimalModeEditor.tsx @@ -3,9 +3,6 @@ import { useState, useCallback, useEffect } from "react"; import { ViewIcon, - ViewOffSlashIcon, - LeftToRightListBulletIcon, - RightToLeftListTriangleIcon, File02Icon, } from "hugeicons-react"; import { SyntaxHighlightedEditor } from "./SyntaxHighlightedEditor"; @@ -15,6 +12,9 @@ import { extractYamlMetadata } from "@/app/_utils/yaml-metadata-utils"; import { Button } from "@/app/_components/GlobalComponents/Buttons/Button"; import { useTranslations } from "next-intl"; import { useAppMode } from "@/app/_providers/AppModeProvider"; +import { useNotesStore } from "@/app/_utils/notes-store"; +import { VisualGuideRuler } from "./VisualGuideRuler"; +import { EditorSettingsDropdown } from "./Toolbar/EditorSettingsDropdown"; interface MinimalModeEditorProps { isEditing: boolean; @@ -31,12 +31,22 @@ export const MinimalModeEditor = ({ }: MinimalModeEditorProps) => { const t = useTranslations(); const { user } = useAppMode(); + const { showLineNumbers, showRuler, showVisualGuides, visualGuideColumns } = useNotesStore(); const { contentWithoutMetadata } = extractYamlMetadata(noteContent); const [markdownContent, setMarkdownContent] = useState( contentWithoutMetadata ); const [showPreview, setShowPreview] = useState(false); - const [showLineNumbers, setShowLineNumbers] = useState(true); + const [charWidth, setCharWidth] = useState(0); + + useEffect(() => { + const el = document.createElement("span"); + el.className = "markdown-line-measure"; + el.textContent = "x".repeat(100); + document.body.append(el); + setCharWidth(el.offsetWidth / 100); + el.remove(); + }, []); useEffect(() => { const { contentWithoutMetadata: newContent } = @@ -82,35 +92,11 @@ export const MinimalModeEditor = ({
- - + setShowPreview(!showPreview)} + />
@@ -137,6 +123,12 @@ export const MinimalModeEditor = ({
+ {!showPreview && showRuler && ( + + )}
{showPreview ? (
)} diff --git a/app/_components/FeatureComponents/Notes/Parts/TipTap/SyntaxHighlightedEditor.tsx b/app/_components/FeatureComponents/Notes/Parts/TipTap/SyntaxHighlightedEditor.tsx index 8e0b79ee..1767001a 100644 --- a/app/_components/FeatureComponents/Notes/Parts/TipTap/SyntaxHighlightedEditor.tsx +++ b/app/_components/FeatureComponents/Notes/Parts/TipTap/SyntaxHighlightedEditor.tsx @@ -17,6 +17,8 @@ interface SyntaxHighlightedEditorProps { showLineNumbers?: boolean; onLinkRequest?: (hasSelection: boolean) => void; onCodeBlockRequest?: (language?: string) => void; + showVisualGuides?: boolean; + visualGuideColumns?: number[]; } const LINE_HEIGHT = 21; @@ -28,6 +30,8 @@ export const SyntaxHighlightedEditor = ({ showLineNumbers = true, onLinkRequest, onCodeBlockRequest, + showVisualGuides = false, + visualGuideColumns = [], }: SyntaxHighlightedEditorProps) => { const { user } = useAppMode(); const editorRef = useRef(null); @@ -198,20 +202,34 @@ export const SyntaxHighlightedEditor = ({ ))}
)} - +
+ {showVisualGuides && charWidthRef.current > 0 && ( +
); diff --git a/app/_components/FeatureComponents/Notes/Parts/TipTap/TipTapEditor.tsx b/app/_components/FeatureComponents/Notes/Parts/TipTap/TipTapEditor.tsx index 315ae468..24d7aa80 100644 --- a/app/_components/FeatureComponents/Notes/Parts/TipTap/TipTapEditor.tsx +++ b/app/_components/FeatureComponents/Notes/Parts/TipTap/TipTapEditor.tsx @@ -78,7 +78,6 @@ export const TiptapEditor = forwardRef( isMarkdownMode ? initialOutput : "" ); const [showBubbleMenu, setShowBubbleMenu] = useState(false); - const [showLineNumbers, setShowLineNumbers] = useState(true); const [showPreview, setShowPreview] = useState(false); const [linkRequestPending, setLinkRequestPending] = useState(false); const [linkRequestHasSelection, setLinkRequestHasSelection] = useState(false); @@ -343,11 +342,8 @@ export const TiptapEditor = forwardRef( editor={editor} isMarkdownMode={isMarkdownMode} toggleMode={toggleMode} - showLineNumbers={showLineNumbers} - onToggleLineNumbers={() => setShowLineNumbers(!showLineNumbers)} showPreview={showPreview} onTogglePreview={() => setShowPreview(!showPreview)} - markdownContent={markdownContent} onMarkdownChange={setMarkdownContent} linkRequestPending={linkRequestPending} linkRequestHasSelection={linkRequestHasSelection} @@ -381,7 +377,6 @@ export const TiptapEditor = forwardRef( content={markdownContent} onChange={handleMarkdownChange} onFileDrop={handleMarkdownFileDrop} - showLineNumbers={showLineNumbers} onLinkRequest={(hasSelection) => { setLinkRequestHasSelection(hasSelection); setLinkRequestPending(true); diff --git a/app/_components/FeatureComponents/Notes/Parts/TipTap/Toolbar/EditorSettingsDropdown.tsx b/app/_components/FeatureComponents/Notes/Parts/TipTap/Toolbar/EditorSettingsDropdown.tsx new file mode 100644 index 00000000..b7e7b715 --- /dev/null +++ b/app/_components/FeatureComponents/Notes/Parts/TipTap/Toolbar/EditorSettingsDropdown.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { + Settings02Icon, + ArrowDown01Icon, + Tick02Icon, + ViewIcon, + LeftToRightListNumberIcon, + RulerIcon, +} from "hugeicons-react"; +import { Button } from "@/app/_components/GlobalComponents/Buttons/Button"; +import { ToolbarDropdown } from "./ToolbarDropdown"; +import { useNotesStore } from "@/app/_utils/notes-store"; +import { useTranslations } from "next-intl"; + +interface EditorSettingsDropdownProps { + isMarkdownMode: boolean; + showPreview?: boolean; + onTogglePreview?: () => void; +} + +export const EditorSettingsDropdown = ({ + isMarkdownMode, + showPreview, + onTogglePreview, +}: EditorSettingsDropdownProps) => { + const t = useTranslations(); + const { + showLineNumbers, + setShowLineNumbers, + showRuler, + setShowRuler, + } = useNotesStore(); + + if (!isMarkdownMode) return null; + + const trigger = ( + + ); + + return ( + +
+ {onTogglePreview && ( + + )} + + +
+
+ ); +}; diff --git a/app/_components/FeatureComponents/Notes/Parts/TipTap/Toolbar/TipTapToolbar.tsx b/app/_components/FeatureComponents/Notes/Parts/TipTap/Toolbar/TipTapToolbar.tsx index ea17d872..de647511 100644 --- a/app/_components/FeatureComponents/Notes/Parts/TipTap/Toolbar/TipTapToolbar.tsx +++ b/app/_components/FeatureComponents/Notes/Parts/TipTap/Toolbar/TipTapToolbar.tsx @@ -5,8 +5,6 @@ import { TextStrikethroughIcon, SourceCodeIcon, Heading02Icon, - RightToLeftListTriangleIcon, - LeftToRightListNumberIcon, QuoteUpIcon, Attachment01Icon, File02Icon, @@ -28,6 +26,7 @@ import { useState, useEffect } from "react"; import { cn } from "@/app/_utils/global-utils"; import { ExtraItemsDropdown } from "@/app/_components/FeatureComponents/Notes/Parts/TipTap/Toolbar/ExtraItemsDropdown"; import { PrismThemeDropdown } from "@/app/_components/FeatureComponents/Notes/Parts/TipTap/Toolbar/PrismThemeDropdown"; +import { EditorSettingsDropdown } from "@/app/_components/FeatureComponents/Notes/Parts/TipTap/Toolbar/EditorSettingsDropdown"; import { useTranslations } from "next-intl"; import { PromptModal } from "@/app/_components/GlobalComponents/Modals/ConfirmationModals/PromptModal"; import { useAppMode } from "@/app/_providers/AppModeProvider"; @@ -42,11 +41,8 @@ type ToolbarProps = { editor: Editor | null; isMarkdownMode: boolean; toggleMode: () => void; - showLineNumbers?: boolean; - onToggleLineNumbers?: () => void; showPreview?: boolean; onTogglePreview?: () => void; - markdownContent?: string; onMarkdownChange?: (content: string) => void; linkRequestPending?: boolean; linkRequestHasSelection?: boolean; @@ -57,11 +53,8 @@ export const TiptapToolbar = ({ editor, isMarkdownMode, toggleMode, - showLineNumbers = true, - onToggleLineNumbers, showPreview = false, onTogglePreview, - markdownContent = "", onMarkdownChange, linkRequestPending = false, linkRequestHasSelection = false, @@ -75,7 +68,6 @@ export const TiptapToolbar = ({ const [showLinkModal, setShowLinkModal] = useState(false); const [showLinkTextModal, setShowLinkTextModal] = useState(false); const [previousUrl, setPreviousUrl] = useState(""); - const [pendingLinkUrl, setPendingLinkUrl] = useState(""); const [selectedImageUrl, setSelectedImageUrl] = useState(""); const [selectedImageWidth, setSelectedImageWidth] = useState< number | undefined @@ -292,51 +284,20 @@ export const TiptapToolbar = ({ )} >
- {isMarkdownMode && onToggleLineNumbers && ( - + {isMarkdownMode && ( +
+ +
)} {isMarkdownMode && (
)} - {isMarkdownMode && onTogglePreview && ( - - )}