| 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 |