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
113 changes: 42 additions & 71 deletions apps/web/src/components/a4-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ export function A4Preview() {
return (
<section className="flex flex-col gap-3">
{/* Pinned to the screen, so the selected frame's tools never shift the page. */}
<div className="pointer-events-none fixed inset-x-0 bottom-4 z-30 flex justify-center px-4">
<div className="pointer-events-none fixed inset-x-0 bottom-24 z-30 flex justify-center px-4 lg:bottom-4">
<SheetInspector />
</div>
<div
ref={setContainer}
className="mx-auto flex w-full max-w-xl flex-col gap-5"
className="mx-auto flex w-full max-w-xl flex-col gap-8"
>
{sheetFormat === "strip"
? stripPages.map((stripPhotos, page) => (
Expand All @@ -105,8 +105,10 @@ export function A4Preview() {
editable
/>
{pageLabel(page) && (
<div className="absolute top-2 left-2">
<PageBadge label={pageLabel(page)} />
<div className="absolute inset-x-1 top-0 z-10 -mt-4 flex -translate-y-1/2 items-center">
<span className="text-muted-foreground text-xs font-medium">
{pageLabel(page)}
</span>
</div>
)}
</div>
Expand All @@ -131,16 +133,17 @@ export function A4Preview() {
showCameraLine={showCameraLine}
editable
/>
{/* Screen-only controls float over the sheet's top margin so the
sheet's top edge stays level with both sidebars. */}
<div className="pointer-events-none absolute inset-x-0 top-0 flex items-start justify-between gap-2 p-1">
{pageLabel(page) ? <PageBadge label={pageLabel(page)} /> : <span />}
<div className="pointer-events-auto">
<PageShapeToggle
value={slice.shape}
onChange={(shape) => setPageShape(page, shape)}
/>
</div>
{/* Page number + frame-shape control sit just above the sheet,
out of the page, so the sheet's top edge stays aligned with
the side columns while these float above that line. */}
<div className="absolute inset-x-1 top-0 z-10 -mt-4 flex -translate-y-1/2 items-center justify-between gap-2">
<span className="text-muted-foreground text-xs font-medium">
{pageLabel(page)}
</span>
<PageShapeToggle
value={slice.shape}
onChange={(shape) => setPageShape(page, shape)}
/>
</div>
</div>
))}
Expand All @@ -149,71 +152,39 @@ export function A4Preview() {
);
}

function PageBadge({ label }: { label: string }) {
return (
<span className="pointer-events-auto rounded-md bg-white/40 px-1.5 py-0.5 text-xs text-neutral-500 ring-1 ring-black/5 backdrop-blur-md">
{label}
</span>
);
}

/**
* Collapses to a single glassy icon of the current shape so it stays out of the
* way of the sheet's margin guide; reveals the full selector on hover or focus
* (focus covers touch, where there's no hover).
*/
/** Always-visible glassy segmented control for this page's frame shape. */
function PageShapeToggle({
value,
onChange,
}: {
value: Orientation;
onChange: (shape: Orientation) => void;
}) {
const CurrentIcon = SHAPE_ICONS[value];
const glass =
"rounded-md bg-white/40 ring-1 ring-black/5 backdrop-blur-md";
return (
<div className="group/shape relative flex justify-end">
<button
type="button"
aria-label="Change page frame shape"
className={cn(
"flex size-5 items-center justify-center text-neutral-500 transition-opacity group-hover/shape:pointer-events-none group-hover/shape:opacity-0 group-focus-within/shape:pointer-events-none group-focus-within/shape:opacity-0",
glass,
)}
>
<CurrentIcon className="size-3" />
</button>
<div
className={cn(
"pointer-events-none absolute top-0 right-0 flex gap-0.5 p-0.5 opacity-0 transition-opacity group-hover/shape:pointer-events-auto group-hover/shape:opacity-100 group-focus-within/shape:pointer-events-auto group-focus-within/shape:opacity-100",
glass,
)}
>
{FRAME_SHAPES.map(({ id, label }) => {
const Icon = SHAPE_ICONS[id];
const active = value === id;
return (
<Tooltip key={id}>
<TooltipTrigger asChild>
<button
type="button"
aria-label={`${label} frames on this page`}
aria-pressed={active}
onClick={() => onChange(id)}
className={cn(
"flex size-5 items-center justify-center rounded text-neutral-500 transition-colors hover:bg-black/5 hover:text-neutral-900",
active && "bg-black/10 text-neutral-900",
)}
>
<Icon className="size-3" />
</button>
</TooltipTrigger>
<TooltipContent>{label} frames on this page</TooltipContent>
</Tooltip>
);
})}
</div>
<div className="flex gap-0.5 rounded-full bg-white/20 p-0.5 ring-1 ring-black/5 backdrop-blur-md dark:bg-neutral-900/30 dark:ring-white/10">
{FRAME_SHAPES.map(({ id, label }) => {
const Icon = SHAPE_ICONS[id];
const active = value === id;
return (
<Tooltip key={id}>
<TooltipTrigger asChild>
<button
type="button"
aria-label={`${label} frames on this page`}
aria-pressed={active}
onClick={() => onChange(id)}
className={cn(
"flex size-5 items-center justify-center rounded-full text-neutral-900 transition-colors hover:bg-black/5 dark:text-neutral-100 dark:hover:bg-white/10",
active && "bg-black/[0.08] dark:bg-white/10",
)}
>
<Icon className="size-3" />
</button>
</TooltipTrigger>
<TooltipContent>{label} frames on this page</TooltipContent>
</Tooltip>
);
})}
</div>
);
}
34 changes: 0 additions & 34 deletions apps/web/src/components/add-photos-fab.tsx

This file was deleted.

121 changes: 121 additions & 0 deletions apps/web/src/components/mobile-bottom-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { type ComponentProps, type ComponentType, useState } from "react";
import { FileDown, ImagePlus, SlidersHorizontal, X } from "lucide-react";

import { OptionsPanel } from "@/components/options-panel";
import { Button } from "@/components/ui/button";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerTitle,
} from "@/components/ui/drawer";
import { LiquidGlass } from "@/components/ui/liquid-glass";
import { useAddPhotos } from "@/hooks/use-add-photos";
import { useExportPdf } from "@/hooks/use-export-pdf";
import { PHOTO_ACCEPT } from "@/lib/upload";
import { cn } from "@/lib/utils";

const SNAP_POINTS = [0.55, 1];

/**
* Mobile-only floating actions: two liquid-glass buttons — Customize on its own,
* and an Add + Export pair — always a tap away. Icons follow the theme foreground.
*/
export function MobileBottomBar() {
const [optionsOpen, setOptionsOpen] = useState(false);
const [snap, setSnap] = useState<number | string | null>(SNAP_POINTS[0]);
const { inputRef, open: openPicker, onChange } = useAddPhotos();
const { exportPdf, isExporting, canExport } = useExportPdf();

return (
<Drawer
open={optionsOpen}
onOpenChange={setOptionsOpen}
snapPoints={SNAP_POINTS}
activeSnapPoint={snap}
setActiveSnapPoint={setSnap}
shouldScaleBackground={false}
>
<input
ref={inputRef}
type="file"
accept={PHOTO_ACCEPT}
multiple
hidden
onChange={onChange}
/>
<div className="fixed inset-x-0 bottom-0 z-40 flex items-center justify-between gap-3 px-4 pb-[max(0.75rem,env(safe-area-inset-bottom))] lg:hidden">
<LiquidGlass>
<GlassIcon
icon={SlidersHorizontal}
label="Customize sheet"
onClick={() => setOptionsOpen(true)}
/>
</LiquidGlass>
<LiquidGlass>
<div className="flex items-center">
<GlassIcon
icon={ImagePlus}
label="Add photos"
onClick={openPicker}
/>
<span aria-hidden className="bg-foreground/15 my-2 w-px self-stretch" />
<GlassIcon
icon={FileDown}
label="Export PDF"
text="PDF"
accent
disabled={!canExport || isExporting}
onClick={() => void exportPdf()}
/>
</div>
</LiquidGlass>
</div>
<DrawerContent className="lg:hidden">
<DrawerHeader>
<DrawerTitle>Sheet options</DrawerTitle>
<DrawerClose asChild>
<Button variant="ghost" size="icon" aria-label="Close">
<X className="size-4" />
</Button>
</DrawerClose>
</DrawerHeader>
<div className="overflow-y-auto px-4 pb-8">
<OptionsPanel bare />
</div>
</DrawerContent>
</Drawer>
);
}

function GlassIcon({
icon: Icon,
label,
text,
accent = false,
className,
...props
}: {
icon: ComponentType<{ className?: string }>;
label: string;
text?: string;
accent?: boolean;
} & ComponentProps<"button">) {
return (
<button
type="button"
aria-label={label}
className={cn(
"active:bg-foreground/10 flex h-12 items-center justify-center gap-1.5 rounded-full transition-colors disabled:opacity-40",
text ? "px-4" : "w-12",
accent ? "text-primary" : "text-foreground",
className,
)}
{...props}
>
<Icon className="size-5" />
{text && <span className="text-sm font-medium">{text}</span>}
</button>
);
}
55 changes: 0 additions & 55 deletions apps/web/src/components/mobile-options.tsx

This file was deleted.

Loading
Loading