返回 CodeWhale
page-header.tsx
根目录 / web / components / page-header.tsx
1 import type { ReactNode } from "react";
2 import { Icon, type IconName } from "./icon";
3 import { Seal } from "./seal";
4 import { WhalePose, type WhalePoseName } from "./whale-pose";
5
6 /**
7 * The one page header: what the reader knows (the title), why they are here
8 * (one lede), what to do next (the actions). An optional kicker names the
9 * place, optionally stamped with a seal; an optional aside repeats the title
10 * in the other script (the bilingual Han title); an optional whale pose shows
11 * the page's job.
12 */
13 export function PageHeader({
14 title,
15 titleAside,
16 titleAsideLang,
17 lede,
18 kicker,
19 kickerIcon,
20 seal,
21 pose,
22 actions,
23 meta,
24 children,
25 titleId,
26 }: {
27 title: ReactNode;
28 /** The title in the other script, set beside it in the ombre ink. */
29 titleAside?: ReactNode;
30 titleAsideLang?: string;
31 lede?: ReactNode;
32 kicker?: ReactNode;
33 kickerIcon?: IconName;
34 /** One Han glyph stamped beside the kicker (components/seal.tsx). */
35 seal?: string;
36 pose?: WhalePoseName;
37 actions?: ReactNode;
38 meta?: ReactNode;
39 children?: ReactNode;
40 titleId?: string;
41 }) {
42 return (
43 <header className="page-head">
44 <div className="page-head-inner">
45 <div className="page-head-text">
46 {kicker || seal ? (
47 <p className="page-kicker">
48 {seal ? <Seal char={seal} /> : null}
49 {kickerIcon ? <Icon name={kickerIcon} /> : null}
50 {kicker}
51 </p>
52 ) : null}
53 <h1 className="page-title" id={titleId}>
54 {title}
55 {titleAside ? (
56 <>
57 {" "}
58 <span className="page-title-aside" lang={titleAsideLang}>{titleAside}</span>
59 </>
60 ) : null}
61 </h1>
62 {lede ? <p className="page-lede">{lede}</p> : null}
63 {meta ? <div className="page-meta">{meta}</div> : null}
64 {actions ? <div className="actions">{actions}</div> : null}
65 {children}
66 </div>
67 {pose ? <WhalePose pose={pose} className="page-head-pose" priority /> : null}
68 </div>
69 </header>
70 );
71 }
72
73 /**
74 * A page section with one title, an optional scope line and one link.
75 *
76 * `layout="split"` is the editorial form: the title, scope and link hold the
77 * left column and the content (usually a ruled list) the right. `label` is a
78 * small running head above the title ("01 / Your terminal"); `seal` stamps a
79 * Han glyph beside it.
80 */
81 export function Section({
82 id,
83 title,
84 scope,
85 link,
86 label,
87 seal,
88 layout = "stack",
89 children,
90 className = "",
91 }: {
92 id: string;
93 title: ReactNode;
94 scope?: ReactNode;
95 link?: ReactNode;
96 label?: ReactNode;
97 seal?: string;
98 layout?: "stack" | "split";
99 children?: ReactNode;
100 className?: string;
101 }) {
102 const head = (
103 <div className="section-head-text">
104 {label || seal ? (
105 <p className="section-label">
106 {seal ? <Seal char={seal} size="sm" /> : null}
107 {label}
108 </p>
109 ) : null}
110 <h2 className="section-title" id={id}>
111 {title}
112 </h2>
113 {scope ? <p className="section-scope">{scope}</p> : null}
114 {layout === "split" ? link : null}
115 </div>
116 );
117 if (layout === "split") {
118 return (
119 <section className={`page-section section-split ${className}`.trim()} aria-labelledby={id}>
120 <div className="section-split-head">{head}</div>
121 <div className="section-split-body">{children}</div>
122 </section>
123 );
124 }
125 return (
126 <section className={`page-section ${className}`.trim()} aria-labelledby={id}>
127 <div className="section-head">
128 {head}
129 {link}
130 </div>
131 {children}
132 </section>
133 );
134 }
135
135 lines Plain Text