diff --git a/.github/actions/apply-version/action.yml b/.github/actions/apply-version/action.yml index eb4bfe40..a9068633 100644 --- a/.github/actions/apply-version/action.yml +++ b/.github/actions/apply-version/action.yml @@ -115,6 +115,13 @@ runs: src-tauri/icons-staging/icon.png src-tauri/icons-staging/icon.icns src-tauri/icons-staging/icon.ico \ src-tauri/icons-staging/Square*.png src-tauri/icons-staging/StoreLogo.png \ src-tauri/icons/ + # icon.icon is the Icon Composer (Liquid Glass) source shared by + # macOS and iOS: the macOS bundler compiles it to Assets.car via + # actool (icon.icns stays as the pre-macOS-26 fallback), and the + # iOS step below copies it into the asset catalog. It's a directory, + # so replace it wholesale rather than cp-ing into the existing one. + rm -rf src-tauri/icons/icon.icon + cp -r src-tauri/icons-staging/icon.icon src-tauri/icons/icon.icon echo "Using staging icon set (src-tauri/icons-staging)" else echo "Using release icon set (src-tauri/icons)" @@ -177,16 +184,16 @@ runs: /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName ${DISPLAY_NAME}" "$plist" || true done - # src-tauri/icons/ios (or icons-staging/ios) is never read by the - # Xcode build directly — Assets.xcassets in gen/apple is a separate, - # committed copy that only xcodegen/xcodebuild actually see. Keep - # them in sync here so the right icon set (and any future icon - # regeneration) reaches the shipped binary. - IOS_ICON_SRC="src-tauri/icons/ios" - if [[ "$INPUT_STAGING" == "true" && -d "src-tauri/icons-staging/ios" ]]; then - IOS_ICON_SRC="src-tauri/icons-staging/ios" - fi - cp "$IOS_ICON_SRC"/*.png src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/ - echo "Synced iOS app icons from $IOS_ICON_SRC" - - echo "Applied iOS settings (display: '$DISPLAY_NAME', icons: $IOS_ICON_SRC)" + # gen/apple/Assets.xcassets is a separate, committed copy that only + # xcodebuild sees — it holds the committed (release) icon.icon. The + # "Select app icon set" step above already swapped src-tauri/icons/icon.icon + # to the staging Icon Composer source when staging, so sync whatever is + # there into the catalog. xcodebuild's actool compiles it into the app's + # Assets.car (Liquid Glass on iOS 26) and auto-generates the PNG raster + # fallbacks for older iOS. ASSETCATALOG_COMPILER_APPICON_NAME=icon + # (project.pbxproj) selects this entry. + rm -rf src-tauri/gen/apple/Assets.xcassets/icon.icon + cp -r src-tauri/icons/icon.icon src-tauri/gen/apple/Assets.xcassets/icon.icon + echo "Synced iOS app icon (icon.icon) into Assets.xcassets" + + echo "Applied iOS settings (display: '$DISPLAY_NAME')" diff --git a/components/editor/sidebar/DocumentTreeItem.module.css b/components/editor/sidebar/DocumentTreeItem.module.css index 85223270..8ec1df28 100644 --- a/components/editor/sidebar/DocumentTreeItem.module.css +++ b/components/editor/sidebar/DocumentTreeItem.module.css @@ -132,6 +132,32 @@ flex-shrink: 0; } +/* Touch-only "⋮" affordance that opens the node context menu (the desktop + * right-click has no touch equivalent). Hidden on fine pointers. */ +.menu_btn { + display: none; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 2px; + border: none; + border-radius: 4px; + background: none; + color: var(--secondary-text); + cursor: pointer; +} + +.menu_btn:hover { + background-color: var(--editor-style-bg-hover); + color: var(--primary-text); +} + +@media (pointer: coarse) { + .menu_btn { + display: flex; + } +} + .action_btn:hover { background-color: var(--editor-style-bg-hover); color: var(--primary-text); diff --git a/components/editor/sidebar/DocumentTreeItem.tsx b/components/editor/sidebar/DocumentTreeItem.tsx index 0c5ceaea..e33101eb 100644 --- a/components/editor/sidebar/DocumentTreeItem.tsx +++ b/components/editor/sidebar/DocumentTreeItem.tsx @@ -4,7 +4,7 @@ import { useCallback, useRef } from "react"; import { useTranslations } from "next-intl"; import { DocumentNode, DocumentNodeType } from "@src/lib/project/project-state"; import { join } from "@src/lib/utils/misc"; -import { ChevronRight, FileText, Folder, LayoutDashboard } from "lucide-react"; +import { ChevronRight, FileText, Folder, LayoutDashboard, MoreVertical } from "lucide-react"; import styles from "./DocumentTreeItem.module.css"; @@ -94,6 +94,23 @@ const DocumentTreeItem = ({ onRenameCommit(node.id, (renameInputRef.current?.value ?? "").trim()); }, [node.id, onRenameCommit]); + // Touch equivalent of right-click: the ⋮ button (shown only on coarse + // pointers) opens the same node menu, anchored under the button. + const handleMenuButton = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + onContextMenu(node, { + preventDefault: () => {}, + stopPropagation: () => {}, + clientX: rect.left, + clientY: rect.bottom, + } as React.MouseEvent); + }, + [node, onContextMenu], + ); + const handleDragOver = useCallback( (e: React.DragEvent) => { if (!draggingId || draggingId === node.id) return; @@ -200,6 +217,17 @@ const DocumentTreeItem = ({ ) : ( {node.title} )} + + {!busy && ( + + )} {isFolder && diff --git a/components/editor/sidebar/EditorSidebar.module.css b/components/editor/sidebar/EditorSidebar.module.css index e1967d2f..d0d1953b 100644 --- a/components/editor/sidebar/EditorSidebar.module.css +++ b/components/editor/sidebar/EditorSidebar.module.css @@ -50,8 +50,8 @@ width: var(--mobile-drawer-width); height: 100%; padding: 12px calc(12px + var(--safe-right)) calc(20px + var(--safe-bottom)) 12px; - background-color: var(--main-bg); - box-shadow: var(--panel-shadow); + /* No drawer background — only the rounded .element panel shows, floating + * over the (backdrop-dimmed) editor, matching the desktop look. */ transform: translateX(0); transition: transform 0.25s ease; pointer-events: auto; diff --git a/components/editor/sidebar/EditorSidebarNavigation.module.css b/components/editor/sidebar/EditorSidebarNavigation.module.css index cf6bc203..c798d510 100644 --- a/components/editor/sidebar/EditorSidebarNavigation.module.css +++ b/components/editor/sidebar/EditorSidebarNavigation.module.css @@ -64,8 +64,8 @@ width: var(--mobile-drawer-width); height: 100%; padding: 12px 12px calc(20px + var(--safe-bottom)) calc(12px + var(--safe-left)); - background-color: var(--main-bg); - box-shadow: var(--panel-shadow); + /* No drawer background — only the rounded .element panel shows, floating + * over the (backdrop-dimmed) editor, matching the desktop look. */ transform: translateX(0); transition: transform 0.25s ease; pointer-events: auto; diff --git a/components/editor/sidebar/SidebarItem.module.css b/components/editor/sidebar/SidebarItem.module.css index 55e1d4aa..2f115ece 100644 --- a/components/editor/sidebar/SidebarItem.module.css +++ b/components/editor/sidebar/SidebarItem.module.css @@ -117,3 +117,29 @@ opacity: 0.4; cursor: grabbing; } + +/* Touch-only "⋮" affordance that opens the scene context menu (the desktop + * right-click has no touch equivalent). Hidden on fine pointers. */ +.menu_btn { + display: none; + align-items: center; + justify-content: center; + flex-shrink: 0; + padding: 2px; + border: none; + border-radius: 4px; + background: none; + color: var(--secondary-text); + cursor: pointer; +} + +.menu_btn:hover { + background-color: var(--editor-style-bg-hover); + color: var(--primary-text); +} + +@media (pointer: coarse) { + .menu_btn { + display: flex; + } +} diff --git a/components/editor/sidebar/SidebarSceneItem.tsx b/components/editor/sidebar/SidebarSceneItem.tsx index feba9a69..da4a892a 100644 --- a/components/editor/sidebar/SidebarSceneItem.tsx +++ b/components/editor/sidebar/SidebarSceneItem.tsx @@ -1,6 +1,7 @@ "use client"; import { useContext, memo, useCallback, Ref } from "react"; +import { MoreVertical } from "lucide-react"; import { ContextMenuType, SceneContextProps } from "./ContextMenu"; import { UserContext } from "@src/context/UserContext"; import { join } from "@src/lib/utils/misc"; @@ -26,15 +27,35 @@ type SidebarSceneItemProps = SceneContextProps & { const SidebarSceneItem = memo(({ scene, index, showDropIndicator, isDragging, isCurrent, label, isOmitted, scrollRef, onPointerDown, onDoubleClick }: SidebarSceneItemProps) => { const { updateContextMenu } = useContext(UserContext); + // 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.SceneItem, + position: { + x: Math.min(x, window.innerWidth - 230), + y: Math.min(y, window.innerHeight - 220), + }, + typeSpecificProps: { scene }, + }); + }, + [updateContextMenu, scene], + ); + const handleDropdown = (e: React.MouseEvent) => { e.preventDefault(); - updateContextMenu({ - type: ContextMenuType.SceneItem, - position: { x: e.clientX, y: e.clientY }, - typeSpecificProps: { - scene, - }, - }); + openMenu(e.clientX, e.clientY); + }; + + // Touch equivalent of right-click: the ⋮ button (shown only on coarse + // pointers). stopPropagation keeps it from starting a drag or, on click, + // bubbling to the context-menu host's close-on-click handler. + const handleMenuButton = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + openMenu(rect.left, rect.bottom); }; const handleDoubleClick = useCallback(() => { @@ -72,6 +93,14 @@ const SidebarSceneItem = memo(({ scene, index, showDropIndicator, isDragging, is

+

{displayText}

diff --git a/components/navbar/ProjectNavbar.tsx b/components/navbar/ProjectNavbar.tsx index 14124789..da9e6ead 100644 --- a/components/navbar/ProjectNavbar.tsx +++ b/components/navbar/ProjectNavbar.tsx @@ -287,7 +287,18 @@ const ProjectNavbar = () => { {isInProject && }
setIsMobileMenuOpen((prev) => !prev)} + onClick={() => + setIsMobileMenuOpen((prev) => { + const next = !prev; + // The menu drawer opens on the right, same as the format + // sidebar; close both side drawers so it opens cleanly on top. + if (next) { + setLeftSidebarOpen(false); + setRightSidebarOpen(false); + } + return next; + }) + } >
diff --git a/components/navbar/ProjectNavbarMobileMenu.module.css b/components/navbar/ProjectNavbarMobileMenu.module.css index a63ec0e7..6821d2d1 100644 --- a/components/navbar/ProjectNavbarMobileMenu.module.css +++ b/components/navbar/ProjectNavbarMobileMenu.module.css @@ -1,23 +1,28 @@ +/* Starts below the navbar so it lines up with the left/right sidebar drawers + * rather than covering the bar. Sits above them (z 60) so it overlays an + * already-open sidebar. */ .backdrop { position: fixed; - inset: 0; + top: var(--navbar-height); + left: 0; + right: 0; + bottom: 0; z-index: 70; background-color: rgba(0, 0, 0, 0.4); } .drawer { position: fixed; - top: 0; + top: var(--navbar-height); right: 0; bottom: 0; z-index: 71; width: var(--mobile-drawer-width); - max-width: 360px; display: flex; flex-direction: column; gap: 4px; - padding: calc(12px + var(--safe-top)) calc(16px + var(--safe-right)) calc(16px + var(--safe-bottom)) 16px; + padding: 12px calc(12px + var(--safe-right)) calc(20px + var(--safe-bottom)) 12px; overflow-y: auto; background-color: var(--main-bg); diff --git a/components/navbar/ScreenplaySearch.module.css b/components/navbar/ScreenplaySearch.module.css index 48ef6aa0..203a60fc 100644 --- a/components/navbar/ScreenplaySearch.module.css +++ b/components/navbar/ScreenplaySearch.module.css @@ -77,6 +77,35 @@ z-index: 1; } +/* Phone: the search opens as a panel below the navbar with the input at the + * top, instead of an inline field that would expand into (and crush) the + * navbar's format dropdown. */ +@media (max-width: 767px) { + .dropdown { + position: fixed; + top: calc(var(--navbar-height) + 4px); + left: calc(12px + var(--safe-left)); + right: calc(12px + var(--safe-right)); + width: auto; + } +} + +.panel_search_input { + width: 100%; + border: none; + border-radius: 8px; + padding: 10px 12px; + margin-bottom: 8px; + background-color: var(--secondary); + color: var(--primary-text); + font-size: 0.9rem; + outline: none; +} + +.panel_search_input::placeholder { + color: var(--secondary-text); +} + .navigation { display: flex; flex-direction: row; diff --git a/components/navbar/ScreenplaySearch.tsx b/components/navbar/ScreenplaySearch.tsx index 0d2fa15a..060ad16e 100644 --- a/components/navbar/ScreenplaySearch.tsx +++ b/components/navbar/ScreenplaySearch.tsx @@ -3,6 +3,7 @@ import { useContext, useRef, useEffect, useCallback, useState } from "react"; import { useTranslations } from "next-intl"; import { ProjectContext } from "@src/context/ProjectContext"; +import { useIsPhone } from "@src/lib/utils/hooks"; import { ScreenplayElement } from "@src/lib/utils/enums"; import { scrollToMatch, SearchMatch } from "@src/lib/screenplay/extensions/search-highlight-extension"; import { Search, ChevronUp, ChevronDown, X, Replace, ReplaceAll } from "lucide-react"; @@ -50,6 +51,7 @@ const ScreenplaySearch = () => { searchMatches, } = useContext(ProjectContext); + const isPhone = useIsPhone(); const [isOpen, setIsOpen] = useState(false); const [replaceValue, setReplaceValue] = useState(""); const debounceRef = useRef | null>(null); @@ -226,8 +228,10 @@ const ScreenplaySearch = () => { return (
-
- {isOpen && ( +
+ {/* On phone the input lives in the panel below (see dropdown) so the + expanding field doesn't crush the navbar's format dropdown. */} + {isOpen && !isPhone && ( { )}
@@ -247,6 +251,17 @@ const ScreenplaySearch = () => { {isOpen && (
+ {isPhone && ( + + )} + {/* Navigation section */}