refactor add explicit interface for the Ui module

Ui had no interface, so its whole implementation was public — including the attribute plumbing (opt_id, opt_class, opt_aria_label, flag_current, flag_open) and the Diff node builders. For a module presented as a reusable library that is the wrong surface: callers should compose blocks, not assemble attributes. ui.mli exports 50 values and four types, and hides the five attribute helpers plus Diff.{line,section,file}_node. crumb and nav_link become abstract, so trails and navigation entries can only be built through their constructors. Documentation moves to the interface, which is now the single place the API is described; ui.ml keeps only comments explaining implementation choices — attribute ordering, why a badge links its text rather than its padding, why an unavailable pagination neighbour still occupies its slot. Verified by rendering all 17 page types from a fixture repository at this commit and at HEAD in a detached worktree: output is identical. dune build, fmt and the 62 tests pass.

Commit
b94287bda1a1a7b375f41fc01cccd927c51d0ddf
Author
Claude Sonnet 4 <claude@anthropic.invalid>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
README.org
index e83ca301..15d33e62 100644..100644
@@ -48,7 +48,10 @@
48 48 plain strings and already-built nodes. It supplies links, lists,
49 49 disclosures, breadcrumbs, toolbars, pagination, line-numbered code
50 50 listings, a diff viewer, and document scaffolding. The class names it
51 Removed: emits are its contract with =styles.css=.
51 Added: emits are its contract with =styles.css=. Its public surface is
52 Added: fixed by =ui.mli=, which also carries the module's documentation;
53 Added: attribute plumbing stays private, so callers compose blocks rather
54 Added: than assembling attributes.
52 55 2. =Components= names ogit's page parts — navigation bars, repository
53 56 rows, tree rows, commit-type badges — and wires them to =Routes=, so
54 57 URLs are never written by hand. =Layout= composes the page shell.
lib/views/ui.ml
index 1f9376e5..18dfcd72 100644..100644
@@ -1,66 +1,35 @@
1 1 (* -*- mode: tuareg; -*- *)
2 2
3 Removed: (** Generic site building blocks.
3 Added: (* Implementation of the site building blocks. The API and its rationale are
4 Added: documented in ui.mli; comments here cover implementation choices only.
4 5
5 Removed: This module is the only place in the view layer that names HTML elements. It
6 Removed: knows nothing about the application domain — no Git, repositories, or ogit
7 Removed: routes — so the same vocabulary would serve any static-first web
8 Removed: application: every function takes plain strings and already-built nodes.
6 Added: Attribute order is deliberate and load-bearing for readability of the
7 Added: rendered HTML: id, then href, then class, then ARIA. Keeping it uniform means
8 Added: a page's markup diffs cleanly when a block changes. *)
9 9
10 Removed: {2 Conventions}
11 Removed:
12 Removed: - {b Semantic first.} Each block picks the most meaningful element available
13 Removed: ([nav], [header], [time], [dl], [details]) rather than a [div] with a
14 Removed: class. Callers choose blocks by meaning, not by appearance.
15 Removed: - {b Classes are a contract.} Blocks emit a fixed vocabulary of class names
16 Removed: — [tree-dir], [tree-toggle], [line-anchor], [pagination-btn] and so on —
17 Removed: which a stylesheet is expected to implement. Those names are structural,
18 Removed: never domain-specific: a "tree" here is any hierarchical list, not a file
19 Removed: tree in particular. Optional [?class_] arguments add a modifier
20 Removed: {i alongside} the base class rather than replacing it.
21 Removed: - {b No scripting.} Interactive blocks ({!disclosure}, {!css_toggle}) rely
22 Removed: on native HTML and CSS, so pages stay usable with JavaScript disabled.
23 Removed:
24 Removed: Nothing here performs I/O. *)
25 Removed:
26 10 open Dream_html
27 11
28 12 type node = Dream_html.node
29 Removed: (** A rendered fragment. Exposed so callers can annotate lists of children
30 Removed: without opening [Dream_html] themselves. *)
31 13
32 Removed: (** Join class names, dropping empty ones. Lets callers pass a modifier without
33 Removed: having to manage separators or risk a stray leading space. *)
34 14 let classes parts =
35 15 parts |> List.filter (fun part -> part <> "") |> String.concat " "
36 16
37 Removed: (** {1 Attribute plumbing} *)
38 Removed:
17 Added: (* Optional attributes collapse to the empty list so they can be concatenated
18 Added: unconditionally at each call site. *)
39 19 let opt_id = function None -> [] | Some value -> [ HTML.id "%s" value ]
40 20 let opt_class = function None -> [] | Some value -> [ HTML.class_ "%s" value ]
41 21 let opt_aria_label = function None -> [] | Some v -> [ Aria.label "%s" v ]
42 22 let flag_current = function false -> [] | true -> [ Aria.current `page ]
43 23 let flag_open = function false -> [] | true -> HTML.[ open_ ]
44 24
45 Removed: (** {1 Text and grouping} *)
25 Added: (* Text and grouping *)
46 26
47 Removed: (** Renders no markup. Use for absent optional content. *)
48 27 let nothing = HTML.null []
49 Removed:
50 Removed: (** Escaped text. *)
51 28 let text value = txt "%s" value
52 Removed:
53 Removed: (** Several nodes where one is expected, without introducing a wrapper element.
54 Removed: *)
55 29 let group nodes = HTML.null nodes
56 30
57 Removed: (** {1 Inline} *)
31 Added: (* Inline *)
58 32
59 Removed: (** An inline run of text or nodes.
60 Removed:
61 Removed: @param decorative
62 Removed: hides the span from assistive technology, for glyphs that repeat
63 Removed: information already available as text. *)
64 33 let inline ?class_ ?(decorative = false) children =
65 34 let hidden = if decorative then [ Aria.hidden true ] else [] in
66 35 HTML.span (opt_class class_ @ hidden) children
@@ -68,13 +37,8 @@
68 37 let inline_text ?class_ ?decorative value =
69 38 inline ?class_ ?decorative [ text value ]
70 39
71 Removed: (** {1 Links} *)
40 Added: (* Links *)
72 41
73 Removed: (** A hyperlink.
74 Removed:
75 Removed: @param label
76 Removed: an accessible name, for links whose visible text is not descriptive on its
77 Removed: own. *)
78 42 let link ?id ?class_ ?label ~href children =
79 43 HTML.a
80 44 (opt_id id
@@ -85,20 +49,19 @@
85 49 let text_link ?id ?class_ ?label ~href value =
86 50 link ?id ?class_ ?label ~href [ text value ]
87 51
88 Removed: (** {1 Images} *)
52 Added: (* Images *)
89 53
90 Removed: (** @param alt
91 Removed: omit for decorative images; the block then marks itself presentational so
92 Removed: screen readers skip it. *)
93 54 let image ?class_ ?alt ~src () =
94 55 let describe =
95 56 match alt with
96 57 | Some value -> [ HTML.alt "%s" value ]
58 Added: (* An empty alt alone is enough for most readers, but the explicit
59 Added: presentation role removes any doubt. *)
97 60 | None -> [ HTML.alt ""; HTML.role `presentation ]
98 61 in
99 62 HTML.img ((HTML.src "%s" src :: describe) @ opt_class class_)
100 63
101 Removed: (** {1 Blocks} *)
64 Added: (* Blocks *)
102 65
103 66 let block ?id ?class_ children =
104 67 HTML.div (opt_id id @ opt_class class_) children
@@ -109,10 +72,6 @@
109 72 let paragraph ?class_ children = HTML.p (opt_class class_) children
110 73 let paragraph_text ?class_ value = paragraph ?class_ [ text value ]
111 74
112 Removed: (** A heading. [level] follows the document outline: 1 for the page's subject, 2
113 Removed: and 3 for nested sections. Skipping levels breaks screen-reader navigation,
114 Removed: so pass the level that matches the structure rather than the one that looks
115 Removed: right. *)
116 75 let heading ?(level = 1) ?class_ children =
117 76 let element =
118 77 match level with
@@ -125,28 +84,26 @@
125 84 in
126 85 element (opt_class class_) children
127 86
128 Removed: (** {1 Lists} *)
87 Added: (* Lists *)
129 88
130 89 let items ?id ?class_ children = HTML.ul (opt_id id @ opt_class class_) children
131 90
132 91 let item ?class_ ?(current = false) children =
133 92 HTML.li (opt_class class_ @ flag_current current) children
134 93
135 Removed: (** A list built from values, saving callers a [List.map]. *)
136 94 let items_of ?id ?class_ render values =
137 95 items ?id ?class_ (List.map render values)
138 96
139 Removed: (** {1 Badges} *)
97 Added: (* Badges *)
140 98
141 Removed: (** A small rounded label. [variant] is appended to the base class as
142 Removed: [<base> <base>-<variant>] so a stylesheet can colour each kind. [href] turns
143 Removed: the label's text into a link while leaving the badge itself inert. *)
144 99 let badge ?(base_class = "badge") ?variant ?href value =
145 100 let classes =
146 101 match variant with
147 102 | None -> base_class
148 103 | Some variant -> Printf.sprintf "%s %s-%s" base_class base_class variant
149 104 in
105 Added: (* Only the text is linked: a link wrapping the whole badge would make its
106 Added: padding clickable, which reads as a button rather than a label. *)
150 107 let body =
151 108 match href with
152 109 | None -> [ text value ]
@@ -154,44 +111,30 @@
154 111 in
155 112 inline ~class_:classes body
156 113
157 Removed: (** {1 Time} *)
114 Added: (* Time *)
158 115
159 Removed: (** A machine-readable timestamp: [machine] fills the [datetime] attribute,
160 Removed: [display] is the visible text. *)
161 116 let timestamp ~machine display =
162 117 HTML.time [ HTML.datetime "%s" machine ] [ text display ]
163 118
164 Removed: (** {1 Definition lists} *)
119 Added: (* Definition lists *)
165 120
166 Removed: (** Term/description pairs, for metadata panels. *)
167 121 let definitions ?class_ pairs =
122 Added: (* dt and dd are siblings, not nested, so each pair becomes a flat group. *)
168 123 let entry (term, description) =
169 124 group [ HTML.dt [] [ text term ]; HTML.dd [] description ]
170 125 in
171 126 HTML.dl (opt_class class_) (List.map entry pairs)
172 127
173 Removed: (** {1 Disclosure} *)
128 Added: (* Disclosure *)
174 129
175 Removed: (** Decorative open/close indicator, rotated by CSS from the enclosing
176 Removed: [details]. It carries no textual meaning, so it is hidden from assistive
177 Removed: technology. *)
178 130 let chevron ?(class_ = "tree-chevron") () =
179 131 inline ~class_ ~decorative:true [ text "\xe2\x80\xba" ]
180 132
181 Removed: (** A native disclosure widget: [details] wrapping a clickable [summary] and its
182 Removed: panel. No JavaScript involved.
183 Removed:
184 Removed: @param expanded renders the panel open on load.
185 Removed: @param summary the always-visible header contents.
186 Removed: @param children the panel contents, revealed when open. *)
187 133 let disclosure ?class_ ?(expanded = false) ?summary_class ~summary children =
188 134 HTML.details
189 135 (opt_class class_ @ flag_open expanded)
190 136 (HTML.summary (opt_class summary_class) summary :: children)
191 137
192 Removed: (** A CSS-only toggle: a visually hidden checkbox paired with a [label] acting
193 Removed: as its control. Lets stylesheets reveal and collapse content without
194 Removed: scripting. *)
195 138 let css_toggle ~id:toggle_id ~toggle_class ~control_class ~label:control_label
196 139 ~glyph () =
197 140 group
@@ -211,23 +154,11 @@
211 154 [ text glyph ];
212 155 ]
213 156
214 Removed: (** {1 Trees} *)
157 Added: (* Trees *)
215 158
216 Removed: (** A leaf row in a hierarchical list.
217 Removed:
218 Removed: @param modifier a class added alongside the base [tree-file] class. *)
219 159 let tree_leaf ?(modifier = "") ~href label =
220 160 item ~class_:(classes [ "tree-file"; modifier ]) [ text_link ~href label ]
221 161
222 Removed: (** A branch row in a hierarchical list.
223 Removed:
224 Removed: Renders a disclosure inside the list item: the summary holds a chevron and a
225 Removed: link, the panel holds the nested list. Clicking the summary padding or the
226 Removed: chevron toggles; clicking the link navigates. Every nested collection on a
227 Removed: site therefore gets the same keyboard and pointer behaviour.
228 Removed:
229 Removed: @param modifier a class added alongside the base [tree-dir] class.
230 Removed: @param expanded renders the nested list open on load. *)
231 162 let tree_branch ?(modifier = "") ?(expanded = false) ~href label children =
232 163 item
233 164 ~class_:(classes [ "tree-dir"; modifier ])
@@ -237,25 +168,19 @@
237 168 [ items ~class_:"tree-nested" children ];
238 169 ]
239 170
240 Removed: (** A row standing in for entries omitted from a truncated list. *)
241 171 let tree_more ?(class_ = "tree-overflow") ~href label =
242 172 item ~class_ [ text_link ~href label ]
243 173
244 Removed: (** {1 Breadcrumbs} *)
174 Added: (* Breadcrumbs *)
245 175
246 176 type crumb = { crumb_text : string; crumb_href : string option }
247 Removed: (** One step in a trail. A crumb without an href renders as plain text. *)
248 177
249 178 let crumb ?href text = { crumb_text = text; crumb_href = href }
250 179
251 Removed: (** A trail of links joined by a separator.
252 Removed:
253 Removed: @param separator_decorative
254 Removed: hides the separators from assistive technology. Appropriate when the trail
255 Removed: already reads as a list of links; leave it off when the separator carries
256 Removed: meaning, such as a path delimiter worth reading aloud. *)
257 180 let breadcrumb ?id ?class_ ?link_class ?separator_class
258 181 ?(separator_decorative = false) ~separator crumbs =
182 Added: (* The separator precedes every crumb but the first, so the trail has no
183 Added: leading or trailing delimiter. *)
259 184 let render index { crumb_text; crumb_href } =
260 185 let body =
261 186 match crumb_href with
@@ -273,31 +198,27 @@
273 198 in
274 199 HTML.span (opt_id id @ opt_class class_) (List.mapi render crumbs)
275 200
276 Removed: (** {1 Navigation} *)
201 Added: (* Navigation *)
277 202
278 203 type nav_link = { nav_href : string; nav_text : string; nav_current : bool }
279 204
280 205 let nav_link ?(current = false) ~href text =
281 206 { nav_href = href; nav_text = text; nav_current = current }
282 207
283 Removed: (** A navigation landmark. [label] names it for assistive technology, which
284 Removed: matters as soon as a page has more than one. *)
285 208 let navigation ?id ?class_ ~label children =
286 209 HTML.nav (opt_id id @ opt_class class_ @ [ Aria.label "%s" label ]) children
287 210
288 Removed: (** A list of navigation links; the current page's item carries
289 Removed: [aria-current="page"]. *)
290 211 let nav_links ?id ?class_ ?item_class links =
212 Added: (* aria-current goes on the list item rather than the link so the marker
213 Added: survives styling the item as the highlighted row. *)
291 214 let render { nav_href; nav_text; nav_current } =
292 215 item ?class_:item_class ~current:nav_current
293 216 [ text_link ~href:nav_href nav_text ]
294 217 in
295 218 items ?id ?class_ (List.map render links)
296 219
297 Removed: (** {1 Toolbars} *)
220 Added: (* Toolbars *)
298 221
299 Removed: (** A bar of controls acting on the current page. An empty toolbar renders
300 Removed: nothing, so layout offsets that depend on its presence stay consistent. *)
301 222 let toolbar ?id ?class_ ?label children =
302 223 match children with
303 224 | [] -> nothing
@@ -308,16 +229,9 @@
308 229 @ opt_aria_label label)
309 230 children
310 231
311 Removed: (** A link styled as a toolbar button. Still a link, not a [button], because it
312 Removed: navigates rather than acting on the current page — which keeps middle-click
313 Removed: and "open in new tab" working. *)
314 232 let button_link ?(class_ = "toolbar-button") ?label ~href text =
315 233 text_link ~class_ ?label ~href text
316 234
317 Removed: (** An active filter together with a control that removes it.
318 Removed:
319 Removed: @param value_class styles the displayed value.
320 Removed: @param dismiss_label accessible name of the remove control. *)
321 235 let dismissible ?(class_ = "toolbar-filter")
322 236 ?(dismiss_class = "toolbar-dismiss") ~value_class ~dismiss_href
323 237 ~dismiss_label value =
@@ -328,18 +242,13 @@
328 242 "\xc3\x97";
329 243 ]
330 244
331 Removed: (** {1 Pagination} *)
245 Added: (* Pagination *)
332 246
333 Removed: (** Previous/next controls around a page number.
334 Removed:
335 Removed: A missing neighbour renders as an inert, aria-hidden placeholder rather than
336 Removed: disappearing, so the controls keep their position between pages.
337 Removed:
338 Removed: The glyphs are decorative; [previous_label] and [next_label] carry the
339 Removed: accessible names. *)
340 247 let pagination ?(label = "Pagination") ?(previous_text = "<") ?(next_text = ">")
341 248 ?(previous_label = "Previous page") ?(next_label = "Next page")
342 249 ?previous_href ?next_href page =
250 Added: (* An unavailable neighbour still occupies its slot, so the page number does
251 Added: not shift horizontally as the reader moves through the list. *)
343 252 let control href_opt glyph control_label =
344 253 match href_opt with
345 254 | Some href ->
@@ -357,17 +266,13 @@
357 266 control next_href next_text next_label;
358 267 ]
359 268
360 Removed: (** {1 Code} *)
269 Added: (* Code *)
361 270
362 Removed: (** A line-numbered listing of source text.
363 Removed:
364 Removed: Every line gets a stable anchor so single lines can be linked and
365 Removed: highlighted. [anchor_prefix] namespaces those anchors, which is required
366 Removed: when one page shows more than one listing.
367 Removed:
368 Removed: Content is emitted verbatim as text; syntax colouring, if any, is a
369 Removed: progressive enhancement layered on top. *)
370 271 let code_listing ?id ?class_ ?(anchor_prefix = "") content =
272 Added: (* Anchor and text alternate as siblings of one grid container, so the
273 Added: stylesheet can align numbers against wrapping lines without a table. The
274 Added: leading tab and trailing newline preserve the source's shape when the
275 Added: listing is copied. *)
371 276 let numbered_line index line =
372 277 let number = index + 1 in
373 278 let name = Printf.sprintf "%s%d" anchor_prefix number in
@@ -386,16 +291,14 @@
386 291 block ?id ?class_
387 292 (String.split_on_char '\n' content |> List.mapi numbered_line |> List.concat)
388 293
389 Removed: (** {1 Diffs} *)
294 Added: (* Diffs *)
390 295
391 Removed: (** A viewer for line-oriented change sets. The data types are deliberately
392 Removed: plain so any producer of diffs can feed them. *)
393 296 module Diff = struct
394 297 type change = Unchanged | Added | Removed
395 298
396 299 type line = {
397 Removed: before : string; (** line number in the old revision, or [""] *)
398 Removed: after : string; (** line number in the new revision, or [""] *)
300 Added: before : string;
301 Added: after : string;
399 302 change : change;
400 303 content : string;
401 304 }
@@ -404,9 +307,9 @@
404 307
405 308 type file = {
406 309 path : string;
407 Removed: detail : string; (** provenance line, e.g. revision identifiers *)
310 Added: detail : string;
408 311 sections : section list;
409 Removed: note : string option; (** shown instead of sections, e.g. binary files *)
312 Added: note : string option;
410 313 }
411 314
412 315 let line_node { before; after; change; content } =
@@ -431,6 +334,7 @@
431 334 disclosure ~class_:"diff-hunk" ~expanded:true ~summary_class:"hunk-header"
432 335 ~summary:[ text section_heading ]
433 336 [
337 Added: (* The inner scroll container keeps long lines from widening the page. *)
434 338 block ~class_:"diff-lines-scroll"
435 339 [ block ~class_:"diff-lines" (List.map line_node lines) ];
436 340 ]
@@ -446,13 +350,12 @@
446 350 ~summary:[ text path ]
447 351 (block ~class_:"diff-meta" [ text detail ] :: body)
448 352
449 Removed: (** Render a change set, or [empty_message] when there is nothing to show. *)
450 353 let view ~empty_message = function
451 354 | [] -> [ paragraph_text empty_message ]
452 355 | files -> List.map file_node files
453 356 end
454 357
455 Removed: (** {1 Document scaffolding} *)
358 Added: (* Document scaffolding *)
456 359
457 360 let meta_viewport =
458 361 HTML.meta
@@ -464,16 +367,11 @@
464 367 HTML.link [ HTML.rel "icon"; HTML.type_ "%s" media_type; HTML.href "%s" href ]
465 368
466 369 let deferred_script src = HTML.script [ HTML.src "%s" src; HTML.defer ] ""
467 Removed:
468 Removed: (** Inline behaviour. Reserved for progressive enhancement: pages must stay
469 Removed: usable when it does not run. *)
470 370 let inline_script source = HTML.script [] "%s" source
471 371
472 372 let document_head ~title:document_title extra =
473 373 HTML.head [] (HTML.title [] "%s" document_title :: extra)
474 374
475 Removed: (** A link that jumps past repeated navigation, revealed on focus. Expected on
476 Removed: every page for keyboard users. *)
477 375 let skip_link ~href label = text_link ~class_:"skip-link" ~href label
478 376
479 377 let page_header ?id ?class_ children =
@@ -488,7 +386,7 @@
488 386 let document ?(lang = "en") ~head ~body () =
489 387 HTML.html [ HTML.lang "%s" lang ] [ head; body ]
490 388
491 Removed: (** {1 Responses} *)
389 Added: (* Responses *)
492 390
493 391 let respond ?status page =
494 392 match status with
lib/views/ui.mli
index 00000000..35e8d1ef 000000..100644
@@ -0,0 +1,371 @@
1 Added: (* -*- mode: tuareg; -*- *)
2 Added:
3 Added: (** Generic site building blocks.
4 Added:
5 Added: This module is the only place in the view layer that names HTML elements. It
6 Added: knows nothing about the application domain — no Git, repositories, or ogit
7 Added: routes — so the same vocabulary would serve any static-first web
8 Added: application: every function takes plain strings and already-built nodes.
9 Added:
10 Added: {2 Conventions}
11 Added:
12 Added: - {b Semantic first.} Each block picks the most meaningful element available
13 Added: ([nav], [header], [time], [dl], [details]) rather than a [div] with a
14 Added: class. Callers choose blocks by meaning, not by appearance.
15 Added: - {b Classes are a contract.} Blocks emit a fixed vocabulary of class names
16 Added: — [tree-dir], [tree-toggle], [line-anchor], [pagination-btn] and so on —
17 Added: which a stylesheet is expected to implement. Those names are structural,
18 Added: never domain-specific: a "tree" here is any hierarchical list, not a file
19 Added: tree in particular. Optional [?class_] arguments add a modifier
20 Added: {i alongside} the base class rather than replacing it.
21 Added: - {b No scripting.} Interactive blocks ({!disclosure}, {!css_toggle}) rely
22 Added: on native HTML and CSS, so pages stay usable with JavaScript disabled.
23 Added: - {b Accessibility is not optional.} Where a block can only be used
24 Added: correctly with an accessible name, that name is a required argument rather
25 Added: than an optional one — see {!navigation} and {!dismissible}.
26 Added:
27 Added: Nothing here performs I/O. The attribute plumbing that assembles these
28 Added: blocks is deliberately not exported: callers compose blocks, they do not
29 Added: assemble attributes. *)
30 Added:
31 Added: type node = Dream_html.node
32 Added: (** A rendered fragment. Exposed so callers can annotate lists of children
33 Added: without opening [Dream_html] themselves. *)
34 Added:
35 Added: val classes : string list -> string
36 Added: (** Join class names, dropping empty ones. Lets callers pass a modifier without
37 Added: having to manage separators or risk a stray leading space. *)
38 Added:
39 Added: (** {1 Text and grouping} *)
40 Added:
41 Added: val nothing : node
42 Added: (** Renders no markup. Use for absent optional content. *)
43 Added:
44 Added: val text : string -> node
45 Added: (** Escaped text. *)
46 Added:
47 Added: val group : node list -> node
48 Added: (** Several nodes where one is expected, without introducing a wrapper element.
49 Added: *)
50 Added:
51 Added: (** {1 Inline} *)
52 Added:
53 Added: val inline : ?class_:string -> ?decorative:bool -> node list -> node
54 Added: (** An inline run of text or nodes.
55 Added:
56 Added: @param decorative
57 Added: hides the span from assistive technology, for glyphs that repeat
58 Added: information already available as text. *)
59 Added:
60 Added: val inline_text : ?class_:string -> ?decorative:bool -> string -> node
61 Added: (** {!inline} around a single string. *)
62 Added:
63 Added: (** {1 Links} *)
64 Added:
65 Added: val link :
66 Added: ?id:string ->
67 Added: ?class_:string ->
68 Added: ?label:string ->
69 Added: href:string ->
70 Added: node list ->
71 Added: node
72 Added: (** A hyperlink.
73 Added:
74 Added: @param label
75 Added: an accessible name, for links whose visible text is not descriptive on its
76 Added: own. *)
77 Added:
78 Added: val text_link :
79 Added: ?id:string -> ?class_:string -> ?label:string -> href:string -> string -> node
80 Added: (** {!link} around a single string. *)
81 Added:
82 Added: (** {1 Images} *)
83 Added:
84 Added: val image : ?class_:string -> ?alt:string -> src:string -> unit -> node
85 Added: (** @param alt
86 Added: omit for decorative images; the block then marks itself presentational so
87 Added: screen readers skip it. *)
88 Added:
89 Added: (** {1 Blocks} *)
90 Added:
91 Added: val block : ?id:string -> ?class_:string -> node list -> node
92 Added: (** A generic grouping box. Reach for {!region} or one of the page landmarks
93 Added: first; this is for layout wrappers that carry no meaning of their own. *)
94 Added:
95 Added: val region : ?id:string -> ?class_:string -> node list -> node
96 Added: (** A self-contained part of a page. *)
97 Added:
98 Added: val paragraph : ?class_:string -> node list -> node
99 Added: val paragraph_text : ?class_:string -> string -> node
100 Added:
101 Added: val heading : ?level:int -> ?class_:string -> node list -> node
102 Added: (** A heading. [level] follows the document outline: 1 for the page's subject, 2
103 Added: and 3 for nested sections. Skipping levels breaks screen-reader navigation,
104 Added: so pass the level that matches the structure rather than the one that looks
105 Added: right. Levels outside 1–6 clamp to 6. *)
106 Added:
107 Added: (** {1 Lists} *)
108 Added:
109 Added: val items : ?id:string -> ?class_:string -> node list -> node
110 Added: (** An unordered list wrapping already-built {!item} nodes. *)
111 Added:
112 Added: val item : ?class_:string -> ?current:bool -> node list -> node
113 Added: (** A list entry.
114 Added:
115 Added: @param current
116 Added: marks the entry as the one matching the current page, for navigation
117 Added: lists. *)
118 Added:
119 Added: val items_of : ?id:string -> ?class_:string -> ('a -> node) -> 'a list -> node
120 Added: (** A list built from values, saving callers a [List.map]. *)
121 Added:
122 Added: (** {1 Badges} *)
123 Added:
124 Added: val badge :
125 Added: ?base_class:string -> ?variant:string -> ?href:string -> string -> node
126 Added: (** A small rounded label. [variant] is appended to the base class as
127 Added: [<base> <base>-<variant>] so a stylesheet can colour each kind. [href] turns
128 Added: the label's text into a link while leaving the badge itself inert. *)
129 Added:
130 Added: (** {1 Time} *)
131 Added:
132 Added: val timestamp : machine:string -> string -> node
133 Added: (** A machine-readable timestamp: [machine] fills the [datetime] attribute, the
134 Added: positional argument is the visible text. *)
135 Added:
136 Added: (** {1 Definition lists} *)
137 Added:
138 Added: val definitions : ?class_:string -> (string * node list) list -> node
139 Added: (** Term/description pairs, for metadata panels. *)
140 Added:
141 Added: (** {1 Disclosure} *)
142 Added:
143 Added: val chevron : ?class_:string -> unit -> node
144 Added: (** Decorative open/close indicator, rotated by CSS from the enclosing
145 Added: [details]. It carries no textual meaning, so it is hidden from assistive
146 Added: technology. *)
147 Added:
148 Added: val disclosure :
149 Added: ?class_:string ->
150 Added: ?expanded:bool ->
151 Added: ?summary_class:string ->
152 Added: summary:node list ->
153 Added: node list ->
154 Added: node
155 Added: (** A native disclosure widget: [details] wrapping a clickable [summary] and its
156 Added: panel. No JavaScript involved.
157 Added:
158 Added: @param expanded renders the panel open on load.
159 Added: @param summary the always-visible header contents. *)
160 Added:
161 Added: val css_toggle :
162 Added: id:string ->
163 Added: toggle_class:string ->
164 Added: control_class:string ->
165 Added: label:string ->
166 Added: glyph:string ->
167 Added: unit ->
168 Added: node
169 Added: (** A CSS-only toggle: a visually hidden checkbox paired with a [label] acting
170 Added: as its control. Lets stylesheets reveal and collapse content without
171 Added: scripting.
172 Added:
173 Added: @param label the accessible name of the control, whose [glyph] has none. *)
174 Added:
175 Added: (** {1 Trees}
176 Added:
177 Added: A "tree" is any hierarchical list: rows that either stand alone or expand to
178 Added: reveal nested rows. Compose these into {!items}. *)
179 Added:
180 Added: val tree_leaf : ?modifier:string -> href:string -> string -> node
181 Added: (** A leaf row.
182 Added:
183 Added: @param modifier a class added alongside the base [tree-file] class. *)
184 Added:
185 Added: val tree_branch :
186 Added: ?modifier:string ->
187 Added: ?expanded:bool ->
188 Added: href:string ->
189 Added: string ->
190 Added: node list ->
191 Added: node
192 Added: (** A branch row.
193 Added:
194 Added: Renders a disclosure inside the list item: the summary holds a chevron and a
195 Added: link, the panel holds the nested list. Clicking the summary padding or the
196 Added: chevron toggles; clicking the link navigates. Every nested collection on a
197 Added: site therefore gets the same keyboard and pointer behaviour.
198 Added:
199 Added: @param modifier a class added alongside the base [tree-dir] class.
200 Added: @param expanded renders the nested list open on load. *)
201 Added:
202 Added: val tree_more : ?class_:string -> href:string -> string -> node
203 Added: (** A row standing in for entries omitted from a truncated list. *)
204 Added:
205 Added: (** {1 Breadcrumbs} *)
206 Added:
207 Added: type crumb
208 Added: (** One step in a trail. Build with {!crumb}. *)
209 Added:
210 Added: val crumb : ?href:string -> string -> crumb
211 Added: (** A trail step. Without an href it renders as plain text, which is how the
212 Added: current location should be shown. *)
213 Added:
214 Added: val breadcrumb :
215 Added: ?id:string ->
216 Added: ?class_:string ->
217 Added: ?link_class:string ->
218 Added: ?separator_class:string ->
219 Added: ?separator_decorative:bool ->
220 Added: separator:string ->
221 Added: crumb list ->
222 Added: node
223 Added: (** A trail of links joined by a separator.
224 Added:
225 Added: @param separator_decorative
226 Added: hides the separators from assistive technology. Appropriate when the trail
227 Added: already reads as a list of links; leave it off when the separator carries
228 Added: meaning, such as a path delimiter worth reading aloud. *)
229 Added:
230 Added: (** {1 Navigation} *)
231 Added:
232 Added: type nav_link
233 Added: (** A destination in a navigation list. Build with {!nav_link}. *)
234 Added:
235 Added: val nav_link : ?current:bool -> href:string -> string -> nav_link
236 Added:
237 Added: val navigation :
238 Added: ?id:string -> ?class_:string -> label:string -> node list -> node
239 Added: (** A navigation landmark. [label] is required because it names the landmark for
240 Added: assistive technology, which matters as soon as a page has more than one. *)
241 Added:
242 Added: val nav_links :
243 Added: ?id:string -> ?class_:string -> ?item_class:string -> nav_link list -> node
244 Added: (** A list of navigation links; the current page's item carries
245 Added: [aria-current="page"]. *)
246 Added:
247 Added: (** {1 Toolbars} *)
248 Added:
249 Added: val toolbar : ?id:string -> ?class_:string -> ?label:string -> node list -> node
250 Added: (** A bar of controls acting on the current page. An empty toolbar renders
251 Added: nothing, so layout offsets that depend on its presence stay consistent. *)
252 Added:
253 Added: val button_link :
254 Added: ?class_:string -> ?label:string -> href:string -> string -> node
255 Added: (** A link styled as a toolbar button. Still a link, not a [button], because it
256 Added: navigates rather than acting on the current page — which keeps middle-click
257 Added: and "open in new tab" working. *)
258 Added:
259 Added: val dismissible :
260 Added: ?class_:string ->
261 Added: ?dismiss_class:string ->
262 Added: value_class:string ->
263 Added: dismiss_href:string ->
264 Added: dismiss_label:string ->
265 Added: string ->
266 Added: node
267 Added: (** An active filter together with a control that removes it.
268 Added:
269 Added: @param value_class styles the displayed value.
270 Added: @param dismiss_label
271 Added: accessible name of the remove control, required because its glyph conveys
272 Added: nothing on its own. *)
273 Added:
274 Added: (** {1 Pagination} *)
275 Added:
276 Added: val pagination :
277 Added: ?label:string ->
278 Added: ?previous_text:string ->
279 Added: ?next_text:string ->
280 Added: ?previous_label:string ->
281 Added: ?next_label:string ->
282 Added: ?previous_href:string ->
283 Added: ?next_href:string ->
284 Added: int ->
285 Added: node
286 Added: (** Previous/next controls around a page number.
287 Added:
288 Added: A missing neighbour — an absent [previous_href] or [next_href] — renders as
289 Added: an inert, aria-hidden placeholder rather than disappearing, so the controls
290 Added: keep their position between pages.
291 Added:
292 Added: The glyphs are decorative; [previous_label] and [next_label] carry the
293 Added: accessible names. *)
294 Added:
295 Added: (** {1 Code} *)
296 Added:
297 Added: val code_listing :
298 Added: ?id:string -> ?class_:string -> ?anchor_prefix:string -> string -> node
299 Added: (** A line-numbered listing of source text.
300 Added:
301 Added: Every line gets a stable anchor so single lines can be linked and
302 Added: highlighted. [anchor_prefix] namespaces those anchors, which is required
303 Added: when one page shows more than one listing.
304 Added:
305 Added: Content is emitted verbatim as text; syntax colouring, if any, is a
306 Added: progressive enhancement layered on top. *)
307 Added:
308 Added: (** {1 Diffs} *)
309 Added:
310 Added: (** A viewer for line-oriented change sets. The data types are deliberately
311 Added: plain so any producer of diffs can feed them. *)
312 Added: module Diff : sig
313 Added: type change = Unchanged | Added | Removed
314 Added:
315 Added: type line = {
316 Added: before : string; (** line number in the old revision, or [""] *)
317 Added: after : string; (** line number in the new revision, or [""] *)
318 Added: change : change;
319 Added: content : string;
320 Added: }
321 Added:
322 Added: type section = { section_heading : string; lines : line list }
323 Added:
324 Added: type file = {
325 Added: path : string;
326 Added: detail : string; (** provenance line, e.g. revision identifiers *)
327 Added: sections : section list;
328 Added: note : string option; (** shown instead of sections, e.g. binary files *)
329 Added: }
330 Added:
331 Added: val view : empty_message:string -> file list -> node list
332 Added: (** Render a change set, or a single paragraph carrying [empty_message] when
333 Added: there is nothing to show. *)
334 Added: end
335 Added:
336 Added: (** {1 Document scaffolding} *)
337 Added:
338 Added: val meta_viewport : node
339 Added: (** Opts the page into responsive layout. Without it mobile browsers assume a
340 Added: desktop-width viewport and scale the page down. *)
341 Added:
342 Added: val stylesheet : string -> node
343 Added: val icon : ?media_type:string -> string -> node
344 Added:
345 Added: val deferred_script : string -> node
346 Added: (** An external script that does not block rendering. *)
347 Added:
348 Added: val inline_script : string -> node
349 Added: (** Inline behaviour. Reserved for progressive enhancement: pages must stay
350 Added: usable when it does not run. *)
351 Added:
352 Added: val document_head : title:string -> node list -> node
353 Added: (** The document head. [title] comes first so it cannot be forgotten. *)
354 Added:
355 Added: val skip_link : href:string -> string -> node
356 Added: (** A link that jumps past repeated navigation, revealed on focus. Expected on
357 Added: every page for keyboard users. *)
358 Added:
359 Added: val page_header : ?id:string -> ?class_:string -> node list -> node
360 Added: val page_main : ?id:string -> ?class_:string -> node list -> node
361 Added: val page_footer : ?class_:string -> node list -> node
362 Added: val document_body : ?class_:string -> node list -> node
363 Added:
364 Added: val document : ?lang:string -> head:node -> body:node -> unit -> node
365 Added: (** A complete document. [lang] defaults to ["en"]; set it so screen readers
366 Added: pick the right pronunciation. *)
367 Added:
368 Added: (** {1 Responses} *)
369 Added:
370 Added: val respond : ?status:[< Dream.status ] -> node -> Dream.response Dream.promise
371 Added: (** Send a rendered document as an HTTP response. *)