Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 20 additions & 13 deletions .github/actions/apply-version/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down Expand Up @@ -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')"
26 changes: 26 additions & 0 deletions components/editor/sidebar/DocumentTreeItem.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
30 changes: 29 additions & 1 deletion components/editor/sidebar/DocumentTreeItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -200,6 +217,17 @@ const DocumentTreeItem = ({
) : (
<span className={join(styles.title, "unselectable")}>{node.title}</span>
)}

{!busy && (
<button
className={styles.menu_btn}
onPointerDown={(e) => e.stopPropagation()}
onClick={handleMenuButton}
aria-label="Document options"
>
<MoreVertical size={14} />
</button>
)}
</div>

{isFolder &&
Expand Down
4 changes: 2 additions & 2 deletions components/editor/sidebar/EditorSidebar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions components/editor/sidebar/EditorSidebarNavigation.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
26 changes: 26 additions & 0 deletions components/editor/sidebar/SidebarItem.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
43 changes: 36 additions & 7 deletions components/editor/sidebar/SidebarSceneItem.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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(() => {
Expand Down Expand Up @@ -72,6 +93,14 @@ const SidebarSceneItem = memo(({ scene, index, showDropIndicator, isDragging, is
</p>
</div>
<SceneLengthItem scene={scene} />
<button
className={nav_item.menu_btn}
onPointerDown={(e) => e.stopPropagation()}
onClick={handleMenuButton}
aria-label="Scene options"
>
<MoreVertical size={16} />
</button>
</div>
<p className={join(nav_item.preview, "unselectable")}>{displayText}</p>
</div>
Expand Down
13 changes: 12 additions & 1 deletion components/navbar/ProjectNavbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,18 @@ const ProjectNavbar = () => {
{isInProject && <ScreenplaySearch />}
<div
className={`${navBtn.button} ${isMobileMenuOpen ? navBtn.active : ""}`}
onClick={() => 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;
})
}
>
<Menu size={18} />
</div>
Expand Down
13 changes: 9 additions & 4 deletions components/navbar/ProjectNavbarMobileMenu.module.css
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
29 changes: 29 additions & 0 deletions components/navbar/ScreenplaySearch.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
21 changes: 18 additions & 3 deletions components/navbar/ScreenplaySearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -50,6 +51,7 @@ const ScreenplaySearch = () => {
searchMatches,
} = useContext(ProjectContext);

const isPhone = useIsPhone();
const [isOpen, setIsOpen] = useState(false);
const [replaceValue, setReplaceValue] = useState("");
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
Expand Down Expand Up @@ -226,8 +228,10 @@ const ScreenplaySearch = () => {

return (
<div className={styles.container} ref={containerRef}>
<div className={`${styles.search_wrapper} ${isOpen ? styles.search_wrapper_open : ""}`}>
{isOpen && (
<div className={`${styles.search_wrapper} ${isOpen && !isPhone ? styles.search_wrapper_open : ""}`}>
{/* 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 && (
<input
ref={inputRef}
type="text"
Expand All @@ -239,14 +243,25 @@ const ScreenplaySearch = () => {
)}
<div
className={`${styles.search_btn} ${isOpen ? styles.search_btn_active : ""}`}
onClick={isOpen ? undefined : handleOpen}
onClick={isOpen ? (isPhone ? handleClose : undefined) : handleOpen}
>
<Search size={18} />
</div>
</div>

{isOpen && (
<div className={styles.dropdown}>
{isPhone && (
<input
ref={inputRef}
type="text"
className={styles.panel_search_input}
placeholder={t("placeholder")}
defaultValue={searchTerm}
onChange={handleSearchChange}
/>
)}

{/* Navigation section */}
<div className={styles.navigation}>
<button
Expand Down
Loading
Loading