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.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 {