Skip to content
File

Blob: src/client/components/ui/page-chrome.tsx

typescript59 lines
1import type { ReactNode } from "react";
2import { EmojiIcon } from "@/client/components/ui/emoji-icon";
3import { PAGE_CONTENT_COLUMN_CLASS, PAGE_ICON_ROW_CLASS } from "@/client/components/ui/page-layout";
4 
5/**
6 * Page chrome shared by the workspace surface, share surface, and loading
7 * skeleton. Owns the layout (cover wrapper -> breadcrumb row -> icon row) and
8 * exposes named slots for surface-specific content. Consumers cannot reorder
9 * or restyle the rows -- that's the point of the primitive.
10 */
11export interface PageChromeProps {
12 /** PageCover element. Primitive supplies the gutter wrapper, group/cover, and relative positioning. */
13 cover?: ReactNode;
14 /** Optional overlay positioned absolute right-2 top-2 inside the cover wrapper (workspace CoverPicker). */
15 coverOverlay?: ReactNode;
16 /** PageBreadcrumbs (any mode) or breadcrumb skeleton. The row is omitted entirely when this is null/undefined (e.g., share root page); otherwise reserves min-h-8 even when content is shorter than the right-side actions. */
17 breadcrumb?: ReactNode;
18 /** Right-aligned actions on the breadcrumb row. Workspace passes Summarize/Share/AvatarStack/SyncStatusDot. */
19 breadcrumbActions?: ReactNode;
20 /** Icon row content: IconPicker (+ optional CoverPicker), <PageEmojiIcon>, or icon-shaped skeleton. */
21 icon?: ReactNode;
22}
23 
24export function PageChrome({ cover, coverOverlay, breadcrumb, breadcrumbActions, icon }: PageChromeProps) {
25 return (
26 <div className={PAGE_CONTENT_COLUMN_CLASS}>
27 {cover ? (
28 <div className="group/cover relative -mx-4 -mt-10 mb-6 sm:-mx-8 lg:mx-0">
29 {cover}
30 {coverOverlay ? <div className="absolute right-2 top-2">{coverOverlay}</div> : null}
31 </div>
32 ) : null}
33
34 {breadcrumb ? (
35 <div className="mb-6 flex min-h-8 items-center gap-3">
36 <div className="min-w-0 flex-1">{breadcrumb}</div>
37 {breadcrumbActions ? <div className="flex shrink-0 items-center gap-3">{breadcrumbActions}</div> : null}
38 </div>
39 ) : null}
40
41 {icon ? <div className={PAGE_ICON_ROW_CLASS}>{icon}</div> : null}
42 </div>
43 );
44}
45 
46/**
47 * Read-only counterpart to IconPicker's interactive button. The `px-2 py-1`
48 * inset matches IconPicker's button (icon-picker.tsx:34, 57) so the painted
49 * emoji lands at the same horizontal position whether the icon is editable
50 * or not. If IconPicker's button padding changes, update here in lockstep.
51 */
52export function PageEmojiIcon({ emoji }: { emoji: string }) {
53 return (
54 <div className="flex items-center px-2 py-1">
55 <EmojiIcon emoji={emoji} size={28} />
56 </div>
57 );
58}