diff --git a/components/dashboard/DashboardModal.module.css b/components/dashboard/DashboardModal.module.css index 80700618..2bab6a0c 100644 --- a/components/dashboard/DashboardModal.module.css +++ b/components/dashboard/DashboardModal.module.css @@ -49,6 +49,7 @@ display: none; align-items: center; justify-content: center; + flex-shrink: 0; width: 36px; height: 36px; border: none; @@ -146,6 +147,14 @@ min-width: 0; } +/* Truncate rather than grow: a long tab title must not push the close button off + its gutter, so the button sits at the same spot on every tab. */ +.headerLeft h3 { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + /* Back to the sections list; only rendered on phone (see DashboardModal). */ .back_btn { display: flex; @@ -210,6 +219,7 @@ display: flex; align-items: center; justify-content: center; + flex-shrink: 0; width: 36px; height: 36px; border: none; @@ -252,6 +262,11 @@ } .modal { + /* Single horizontal gutter for both drawer screens (sections list and + * section content). Both use it for their outer edges so the close button + * and the panel content sit on the same lines whichever screen is up — + * switching screens must not shift the close button. */ + --drawer-gutter: 16px; width: var(--mobile-drawer-width); max-width: var(--mobile-drawer-width); height: 100%; @@ -268,7 +283,8 @@ .sidebar { display: none; flex: 1; - padding: 8px calc(8px + var(--safe-right)) calc(16px + var(--safe-bottom)) 8px; + padding: 12px calc(var(--drawer-gutter) + var(--safe-right)) calc(16px + var(--safe-bottom)) + var(--drawer-gutter); border-right: none; overflow-y: auto; /* On mobile the whole modal is one drawer, so the sections screen wears the @@ -277,18 +293,23 @@ background: var(--main-bg); } + /* No horizontal padding of its own: the close button must land on the drawer + * gutter, exactly where the content screen's close button sits. */ .sidebarHeader { - padding: 4px 12px 8px; + padding: 4px 0 8px; } - /* Align the title with the section labels + item icons below. Their left edge is - * navMenu(12) + own padding(12); the header only carries 12, so the title needs - * its own 12 to reach the same line. */ + /* Align the title with the section labels + item icons below, whose left edge + * is the gutter + navItem's own padding(12). */ .sidebarTitle { padding: 0 12px; margin-bottom: 0; } + .navMenu { + padding: 0; + } + .navMenuFooter { margin-top: 24px; padding-top: 12px; @@ -309,10 +330,11 @@ .content { flex: 1; - /* Symmetric horizontal padding + the right safe-area inset so the close + /* Symmetric horizontal gutter + the right safe-area inset so the close * button and settings panels (Keybinds, Layout, …) never crowd the screen * edge. */ - padding: 12px calc(16px + var(--safe-right)) calc(16px + var(--safe-bottom)) 16px; + padding: 12px calc(var(--drawer-gutter) + var(--safe-right)) calc(16px + var(--safe-bottom)) + var(--drawer-gutter); /* Nudge inherited/em-based text down a touch for a denser, phone-friendly * scale; width-driven layouts reflow into the narrow drawer on their own. */ font-size: 0.92rem; @@ -326,7 +348,11 @@ font-size: 1.1rem; } + /* The drawer gutter is the only horizontal padding on phone: no extra right + * padding and no reserved scrollbar gutter, so panel content lines up with the + * header on both edges instead of drifting left. */ .scrollArea { - padding-right: 6px; + padding-right: 0; + scrollbar-gutter: auto; } } diff --git a/components/editor/sidebar/SidebarCharacterItem.tsx b/components/editor/sidebar/SidebarCharacterItem.tsx index d0165087..2b72b863 100644 --- a/components/editor/sidebar/SidebarCharacterItem.tsx +++ b/components/editor/sidebar/SidebarCharacterItem.tsx @@ -7,8 +7,9 @@ import { pasteText } from "@src/lib/screenplay/editor"; import { ProjectContext } from "@src/context/ProjectContext"; import { join } from "@src/lib/utils/misc"; +import { useTranslations } from "next-intl"; -import { Highlighter, Link } from "lucide-react"; +import { Highlighter, Link, MoreVertical } from "lucide-react"; import item from "./SidebarItem.module.css"; const DEFAULT_HIGHLIGHT_COLOR = "#6366f1"; // Indigo - matches extension default @@ -18,21 +19,45 @@ type SidebarCharacterItemProps = CharacterContextProps & { }; const SidebarCharacterItem = memo(({ character, isHighlighted }: SidebarCharacterItemProps) => { + const t = useTranslations("contextMenu"); const { updateContextMenu } = useContext(UserContext); - const { editor } = useContext(ProjectContext); + const { editor, isReadOnly } = useContext(ProjectContext); const highlightColor = character.color || DEFAULT_HIGHLIGHT_COLOR; + // Clamp so the menu never opens off the right/bottom edge (matters on touch, + // where it's triggered from the ⋮ button near the panel edge). Read-only + // collapses the menu to the single Highlight item, so it needs far less room. + const openMenu = useCallback( + (x: number, y: number) => { + updateContextMenu({ + type: ContextMenuType.CharacterItem, + position: { + x: Math.min(x, window.innerWidth - 230), + y: Math.min(y, window.innerHeight - (isReadOnly ? 60 : 180)), + }, + typeSpecificProps: { + character, + }, + }); + }, + [updateContextMenu, character, isReadOnly], + ); + const handleDropdown = useCallback((e: React.MouseEvent) => { e.preventDefault(); - updateContextMenu({ - type: ContextMenuType.CharacterItem, - position: { x: e.clientX, y: e.clientY }, - typeSpecificProps: { - character, - }, - }); - }, [updateContextMenu, character]); + openMenu(e.clientX, e.clientY); + }, [openMenu]); + + // Touch equivalent of right-click: the ⋮ button (shown only on coarse + // pointers). stopPropagation keeps the click from bubbling to the + // context-menu host's close-on-click handler. + const handleMenuButton = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + openMenu(rect.left, rect.bottom); + }, [openMenu]); const handleDoubleClick = useCallback(() => { // paste character name on double click @@ -53,6 +78,17 @@ const SidebarCharacterItem = memo(({ character, isHighlighted }: SidebarCharacte )} {character.persistent && } + {/* Always shown: the character menu keeps a working Highlight + * item in read-only, so gating this on write access would put + * highlighting out of reach on touch. */} + diff --git a/components/editor/sidebar/SidebarLocationItem.tsx b/components/editor/sidebar/SidebarLocationItem.tsx index e0886aab..eb831d30 100644 --- a/components/editor/sidebar/SidebarLocationItem.tsx +++ b/components/editor/sidebar/SidebarLocationItem.tsx @@ -6,24 +6,48 @@ import { UserContext } from "@src/context/UserContext"; import { pasteText } from "@src/lib/screenplay/editor"; import { ProjectContext } from "@src/context/ProjectContext"; import { join } from "@src/lib/utils/misc"; +import { useTranslations } from "next-intl"; -import { Link } from "lucide-react"; +import { Link, MoreVertical } from "lucide-react"; import item from "./SidebarItem.module.css"; const SidebarLocationItem = memo(({ location }: LocationContextProps) => { + const t = useTranslations("contextMenu"); const { updateContextMenu } = useContext(UserContext); - const { editor } = useContext(ProjectContext); + const { editor, isReadOnly } = useContext(ProjectContext); + + // Clamp so the menu never opens off the right/bottom edge (matters on touch, + // where it's triggered from the ⋮ button near the panel edge). + const openMenu = useCallback( + (x: number, y: number) => { + updateContextMenu({ + type: ContextMenuType.LocationItem, + position: { + x: Math.min(x, window.innerWidth - 230), + y: Math.min(y, window.innerHeight - 110), + }, + typeSpecificProps: { + location, + }, + }); + }, + [updateContextMenu, location], + ); const handleDropdown = useCallback((e: React.MouseEvent) => { e.preventDefault(); - updateContextMenu({ - type: ContextMenuType.LocationItem, - position: { x: e.clientX, y: e.clientY }, - typeSpecificProps: { - location, - }, - }); - }, [updateContextMenu, location]); + openMenu(e.clientX, e.clientY); + }, [openMenu]); + + // Touch equivalent of right-click: the ⋮ button (shown only on coarse + // pointers). stopPropagation keeps the click from bubbling to the + // context-menu host's close-on-click handler. + const handleMenuButton = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + openMenu(rect.left, rect.bottom); + }, [openMenu]); const handleDoubleClick = useCallback(() => { // paste location name on double click @@ -33,8 +57,22 @@ const SidebarLocationItem = memo(({ location }: LocationContextProps) => { return (
-

{location.name}

- {location.persistent && } +
+

{location.name}

+
+
+ {location.persistent && } + {!isReadOnly && ( + + )} +
); diff --git a/components/editor/sidebar/SidebarSceneItem.tsx b/components/editor/sidebar/SidebarSceneItem.tsx index da4a892a..14d96e96 100644 --- a/components/editor/sidebar/SidebarSceneItem.tsx +++ b/components/editor/sidebar/SidebarSceneItem.tsx @@ -5,6 +5,7 @@ import { MoreVertical } from "lucide-react"; import { ContextMenuType, SceneContextProps } from "./ContextMenu"; import { UserContext } from "@src/context/UserContext"; import { join } from "@src/lib/utils/misc"; +import { useTranslations } from "next-intl"; import { Scene } from "@src/lib/screenplay/scenes"; import SceneLengthItem from "../sidebar/SceneLengthItem"; @@ -25,6 +26,7 @@ type SidebarSceneItemProps = SceneContextProps & { }; const SidebarSceneItem = memo(({ scene, index, showDropIndicator, isDragging, isCurrent, label, isOmitted, scrollRef, onPointerDown, onDoubleClick }: SidebarSceneItemProps) => { + const t = useTranslations("contextMenu"); const { updateContextMenu } = useContext(UserContext); // Clamp so the menu never opens off the right/bottom edge (matters on touch, @@ -97,7 +99,7 @@ const SidebarSceneItem = memo(({ scene, index, showDropIndicator, isDragging, is className={nav_item.menu_btn} onPointerDown={(e) => e.stopPropagation()} onClick={handleMenuButton} - aria-label="Scene options" + aria-label={t("sceneOptions")} > diff --git a/messages/de.json b/messages/de.json index 43f98f73..f8ecacad 100644 --- a/messages/de.json +++ b/messages/de.json @@ -562,7 +562,10 @@ "unomitScene": "Szene wiederherstellen", "insertPageBreak": "Seitenumbruch einfügen", "removePageBreak": "Seitenumbruch entfernen", - "pageBreakHint": "Manueller Seitenumbruch" + "pageBreakHint": "Manueller Seitenumbruch", + "sceneOptions": "Szenenoptionen", + "characterOptions": "Charakteroptionen", + "locationOptions": "Ortsoptionen" }, "popup": { "character": { diff --git a/messages/en.json b/messages/en.json index 3dd15f3f..7cd2554f 100644 --- a/messages/en.json +++ b/messages/en.json @@ -561,7 +561,10 @@ "unomitScene": "Unomit scene", "insertPageBreak": "Insert page break", "removePageBreak": "Remove page break", - "pageBreakHint": "Manual page break" + "pageBreakHint": "Manual page break", + "sceneOptions": "Scene options", + "characterOptions": "Character options", + "locationOptions": "Location options" }, "popup": { "character": { diff --git a/messages/es.json b/messages/es.json index 81e16de2..fa617b16 100644 --- a/messages/es.json +++ b/messages/es.json @@ -561,7 +561,10 @@ "unomitScene": "Restaurar escena", "insertPageBreak": "Insertar salto de página", "removePageBreak": "Quitar salto de página", - "pageBreakHint": "Salto de página manual" + "pageBreakHint": "Salto de página manual", + "sceneOptions": "Opciones de escena", + "characterOptions": "Opciones de personaje", + "locationOptions": "Opciones de localización" }, "popup": { "character": { diff --git a/messages/fr.json b/messages/fr.json index 70efa565..e8bab596 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -562,7 +562,10 @@ "unomitScene": "Restaurer la scène", "insertPageBreak": "Insérer un saut de page", "removePageBreak": "Supprimer le saut de page", - "pageBreakHint": "Saut de page manuel" + "pageBreakHint": "Saut de page manuel", + "sceneOptions": "Options de la scène", + "characterOptions": "Options du personnage", + "locationOptions": "Options du lieu" }, "popup": { "character": { diff --git a/messages/ja.json b/messages/ja.json index 9f16eaea..f84e9553 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -561,7 +561,10 @@ "unomitScene": "シーンを復元", "insertPageBreak": "改ページを挿入", "removePageBreak": "改ページを削除", - "pageBreakHint": "手動改ページ" + "pageBreakHint": "手動改ページ", + "sceneOptions": "シーンのオプション", + "characterOptions": "登場人物のオプション", + "locationOptions": "場所のオプション" }, "popup": { "character": { diff --git a/messages/ko.json b/messages/ko.json index e9f30d83..eaedb148 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -561,7 +561,10 @@ "unomitScene": "씬 복원", "insertPageBreak": "페이지 나누기 삽입", "removePageBreak": "페이지 나누기 제거", - "pageBreakHint": "수동 페이지 나누기" + "pageBreakHint": "수동 페이지 나누기", + "sceneOptions": "장면 옵션", + "characterOptions": "인물 옵션", + "locationOptions": "장소 옵션" }, "popup": { "character": { diff --git a/messages/pl.json b/messages/pl.json index 1bd5eccf..27327b86 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -561,7 +561,10 @@ "unomitScene": "Przywróć scenę", "insertPageBreak": "Wstaw podział strony", "removePageBreak": "Usuń podział strony", - "pageBreakHint": "Ręczny podział strony" + "pageBreakHint": "Ręczny podział strony", + "sceneOptions": "Opcje sceny", + "characterOptions": "Opcje postaci", + "locationOptions": "Opcje lokalizacji" }, "popup": { "character": { diff --git a/messages/zh.json b/messages/zh.json index 15857c63..0226cf01 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -561,7 +561,10 @@ "unomitScene": "恢复场景", "insertPageBreak": "插入分页符", "removePageBreak": "移除分页符", - "pageBreakHint": "手动分页符" + "pageBreakHint": "手动分页符", + "sceneOptions": "场景选项", + "characterOptions": "角色选项", + "locationOptions": "位置选项" }, "popup": { "character": { diff --git a/styles/globals.css b/styles/globals.css index 32e0a526..d8838420 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -6,6 +6,14 @@ body { height: 100%; width: 100%; overflow: clip; + /* Disable mobile text autosizing ("font boosting"). Without this, phone + * browsers inflate the editor's 16px screenplay font (~18.7px) while the + * offscreen pagination measurement div (#pagination-test-div, hidden) stays + * at 16px — so text wraps differently than measured and page breaks stutter + * while typing. It also crams the boosted glyphs into the fixed 16px + * --line-height. All font sizes here are deliberate; never auto-inflate. */ + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; } .app-layout {