Composing layouts
Every template is a composition of Clover’s parts, and so is a layout of your
own. A Root<clover-root> loads the content and holds where the reader is. Every part
inside it reads from the nearest Root<clover-root>, wherever it sits in your markup, so a
layout is your own HTML with parts placed in it.
A layout of your own
The parts are named exports. import * as Clover gives them dot notation, and they
tree-shake: a page loads only the parts it renders. Here the title, position and buttons
sit beside the Body. The code is live: move the aside’s parts, or change what a Label
reads, and the layout follows.
import * as Clover from "@nulib/clover";import "@nulib/clover/styles.css"; <Clover.RootiiifContent="https://iiif.io/api/cookbook/recipe/0009-book-1/manifest.json"className="my-layout"><Clover.Body /><aside><Clover.Label as= /><Clover.Label of= /><Clover.Position /><Clover.Previous /><Clover.Next /></aside></Clover.Root>;<script type="module">import "@nulib/clover/web-components";</script> <clover-rootiiif-content="https://iiif.io/api/cookbook/recipe/0009-book-1/manifest.json"class="my-layout"><clover-body></clover-body><aside><clover-label as=></clover-label><clover-label of=></clover-label><clover-position></clover-position><clover-previous></clover-previous><clover-next></clover-next></aside></clover-root>Start from a template
The templates are built only from these parts, so a template’s source
is a good place to start. Copy Default.tsx and Default.css from
packages/clover/src/templates/default/ (or another template’s folder) into your app,
and read ../../parts in its imports as @nulib/clover. Rename its
clover-template-default-* classes to your own, and change what you like: nothing in a
template is private.
To keep the Viewer’s shell, pass your layout as its template; it takes the Root<clover-root> props
like any template:
import Viewer, * as Clover from "@nulib/clover";
function MyLayout(props) {
return (
<Clover.Root {...props}>
<Clover.Body />
<Clover.Items layout="pager" />
</Clover.Root>
);
}
<Viewer iiifContent={url} template={MyLayout} />;<!-- A layout of your own is the markup itself: a root and its parts. -->
<clover-root
iiif-content="https://iiif.io/api/cookbook/recipe/0009-book-1/manifest.json"
>
<clover-body></clover-body>
<clover-items layout="pager"></clover-items>
</clover-root>Root
iiifContentiiif-content can be a Manifest, a Collection, a Canvas or a content state, as a URL or
an object; see what the Viewer opens. Every
template, and the Viewer, takes these props and hands them to its Root<clover-root>.
Parts
Parts that have nothing to show render nothing: Items<clover-items> and its pager for a single view,
Label<clover-label> with no label.
Properties
Primitives has a page for each. Each IIIF descriptive property is a part of its own, reading the Manifest, or the
current Canvas with of="canvas". A value passed as value wins, so they also work
outside a Root<clover-root>. Text is in the reader’s language and marked with lang; HTML is
limited to what IIIF allows and sanitized, by a sanitizer that loads only when a value
carries markup.
Value<clover-value> shows any other IIIF language map you hold, an annotation’s text say, marked
and sanitized the same way. It reads a value strictly by its format: text/html as
IIIF’s HTML, sanitized to the tags the Presentation API allows; text/plain as written,
markup and all, its line breaks kept; and with no format, by IIIF’s rule for property
values, as HTML only when it runs from a first < to a last >. With
format="text/markdown" it reads Markdown, as
Annotations<clover-annotations> does for a text/markdown body: its lists are lists, and its headings are
headings to assistive technology (role="heading", drawn bold, so your page’s heading
styles stay out of them). The first heading takes headingLevelheading-level (default 3), and each
later one sits a level under the nearest shallower heading before it, so none is skipped
even where the source skips one. In Annotations<clover-annotations> they sit a level under the set’s
heading. Past h6, none is a heading (Headings).
Every summary Clover shows is Summary<clover-summary>, cut to five lines with a “Read more” button
under it when it runs longer, and “Read less” to cut it again. The number of lines is a
CSS custom property, --clover-summary-lines, set on .clover-summary or anything
around it; none shows it whole:
.my-layout .clover-summary {
--clover-summary-lines: 3;
}ReadMore<clover-read-more> gives text of your own the same button: wrap one element (or a component that
forwards its ref) and clamp it in your CSS; it carries data-read-more="open" while
open.
<Clover.ReadMore>
<p className="my-note">{note}</p>
</Clover.ReadMore><clover-read-more>
<p class="my-note">…</p>
</clover-read-more>The information panel
Panel<clover-panel> shows About, and each other section the content has something for:
Collection, Contents, Search, Map, Transcript and Annotations. Give it Tab<clover-tab>s to choose
the sections yourself, in your order. Each part a default section uses works on its own
too, in a Tab<clover-tab> or anywhere in the layout, and so does a part of your own.
<Clover.Panel>
<Clover.Tab value="about" label="About">
<Clover.About />
</Clover.Tab>
<Clover.Tab value="page" label="This page">
<Clover.Label of="canvas" as="h3" />
<Clover.Summary of="canvas" />
<Clover.Metadata of="canvas" />
</Clover.Tab>
</Clover.Panel><clover-panel>
<clover-tab value="about" label="About">
<clover-about></clover-about>
</clover-tab>
<clover-tab value="page" label="This page">
<clover-label of="canvas" as="h3"></clover-label>
<clover-summary of="canvas"></clover-summary>
<clover-metadata of="canvas"></clover-metadata>
</clover-tab>
</clover-panel>PanelToggle<clover-panel-toggle> shows and hides it; as a child of Body<clover-body>, it sits with the Canvas’s
controls. Panel has its layouts, the sections and
when each shows, headings, closing a panel that floats over the page, and its props.
Panes side by side
Split<clover-split>, SplitPane<clover-split-pane> and SplitHandle<clover-split-handle> put panes side by side, with a bar between them
that shares the width. One pane beside each bar has a size (with min and max, in px,
rem or %); the other takes what is left. The reader drags the bar by its grip, or clicks
or taps it: each click steps the pane on to its next width, from its narrowest to its own
size to its widest and then around again, so one pointer can resize it without dragging. The
bar is a focusable separator that names itself on hover and focus: the arrow keys move
it (Shift for bigger steps), and Home and End go to the limits. With storageKeystorage-key the
reader’s sizes are remembered. The Default and Document templates use it, and nothing
ties it to any part.
<Clover.Root iiifContent={url}>
<Clover.Split storageKey="my-layout">
<Clover.SplitPane min="16rem">
<Clover.Body />
</Clover.SplitPane>
<Clover.SplitHandle />
<Clover.SplitPane size="24rem" min="14rem">
<Clover.Panel />
</Clover.SplitPane>
</Clover.Split>
</Clover.Root><clover-root
iiif-content="https://iiif.io/api/cookbook/recipe/0009-book-1/manifest.json">
<clover-split storage-key="my-layout">
<clover-split-pane min="16rem">
<clover-body></clover-body>
</clover-split-pane>
<clover-split-handle></clover-split-handle>
<clover-split-pane size="24rem" min="14rem">
<clover-panel></clover-panel>
</clover-split-pane>
</clover-split>
</clover-root>Parts of your own
Hooks read from the nearest Root<clover-root>. Each re-renders only when what it reads changes.
function PageCount() {
const { position, total } = Clover.useNavigation();
return (
<span>
{position + 1} / {total}
</span>
);
}<span id="page-count"></span>
<script type="module">
const root = document.querySelector("clover-root");
const count = document.getElementById("page-count");
const show = () => {
const { position, total } = root.clover.navigation();
count.textContent = `${position + 1} / ${total}`;
};
const start = () => {
show();
root.clover.subscribe(show);
};
if (root.clover) start();
else root.addEventListener("clover-ready", start, { once: true });
</script>useClover()root.clover gives { clover }, the Root’s state and actions (clover.goTo(canvasId),
clover.next(), clover.search(query) and the rest), for parts that act rather than
read.
Headings
Every part that draws a title takes headingLevelheading-level, one below the heading it sits under,
so a screen reader’s list of headings follows your page. HTML’s headings end at h6, and
screen readers misread a deeper aria-level (JAWS, and TalkBack with Chrome, announce a
7 as a 2), so past 6 Clover draws no heading: a title that would be an h7 names the
group it heads instead. Clover.titled does the same in a layout of your own.
function MyLayout({ url, headingLevel }) {
const title = Clover.titled(headingLevel, "my-title");
return (
<Clover.Root iiifContent={url} {...title.group}>
<Clover.Label as={title.as} {...title.title} />
<Clover.Body />
</Clover.Root>
);
}<clover-root
iiif-content="https://iiif.io/api/cookbook/recipe/0009-book-1/manifest.json"
>
<clover-label as="h2"></clover-label>
<clover-body></clover-body>
</clover-root>Controls of your own
Every control Clover draws as an icon alone names itself in a popover on hover and on
keyboard focus, with its keyboard shortcut when it has one. Give a control of your own
the same: its name as aria-label, its key (if it has one) as aria-keyshortcuts, and
Tip<clover-tip> inside it, last, with the same words. Tip is hidden from assistive technology,
which reads the control’s own name, and hides on Escape. Each time it shows, it is kept
inside whatever clips it (the Body, a scrolling panel, the window): it wraps to the room
there is and moves in from the edge.
<button type="button" aria-label="Bookmark" onClick={save}>
<BookmarkIcon aria-hidden="true" />
<Clover.Tip label="Bookmark" />
</button><button type="button" id="bookmark" aria-label="Bookmark">
<svg aria-hidden="true">…</svg>
<clover-tip label="Bookmark"></clover-tip>
</button>
<script type="module">
document.getElementById("bookmark").addEventListener("click", save);
</script>Clover’s popups open one at a time: the Canvas’s information and its menus
(annotations, Choice, an animation’s frames), the playback controls’ menus and
Download’s list. Opening one closes whichever other is open, however it was opened. A
popup of your own joins them with useOneOpenroot.clover.oneOpen(), given its open state and the way to
close it:
const [open, setOpen] = useState(false);
Clover.useOneOpen(open, () => setOpen(false));<script type="module">
const root = document.querySelector("clover-root");
let release = null;
const close = () => {
menu.hidden = true;
release?.();
};
const open = () => {
menu.hidden = false;
release = root.clover.oneOpen(close);
};
</script>Languages
Clover’s own words (its controls’ names, its messages, what it announces) are in its
interface language: the one set with initCloverI18n, else the page’s <html lang>, else
the first of the browser’s languages it has, else English, so it speaks the language of
the page around it. It ships English, Finnish,
French, Norwegian (Bokmål and Nynorsk), Portuguese (European) and Spanish, each complete.
IIIF text is read in the Root’s language, else the page’s, else the browser’s. Where
what the resource says of itself comes in several languages, the Panel’s About offers
them, and the one the reader chooses is the whole Root’s until the page sets another
language.
import { initCloverI18n } from "@nulib/clover";
// The page's language, and strings of your own for one Clover does not ship.
initCloverI18n({
lng: "de",
resources: { de: { clover: { informationPanelTabsAbout: "Über" } } },
});<script type="module">
import { initCloverI18n } from "@nulib/clover/web-components";
initCloverI18n({
lng: "de",
resources: { de: { clover: { informationPanelTabsAbout: "Über" } } },
});
</script>English comes with Clover; each other language loads the first time Clover is to speak
it, so a page in English never fetches one. Until it arrives, Clover’s words show in
English, marked lang="en", and they change when it does. A page rendered on the server,
or a test, that needs them in place first waits for them:
await initCloverI18n({ lng: "fr" }).ready();Where the interface language is not the page’s (a French reader on an English page), each
of Clover’s strings carries it as lang, on the element that shows it or is named by it.
The page’s language is the nearest lang around the Root<clover-root>, or one you give the Root<clover-root>.
The Root<clover-root> carries none of its own, so what you put in it, and IIIF text in no language
(none), keep the page’s. A language you bootstrap with some strings falls back to
English for the rest, each such string marked lang="en" too. In a part of your own,
mark Clover’s strings the same way:
const t = Clover.useT();
const langOf = Clover.useLangOf();
<button
type="button"
aria-label={t("commonClose")}
lang={langOf("commonClose")}
>
<CloseIcon aria-hidden="true" />
<Clover.Tip label={t("commonClose")} />
</button>;<button type="button" id="close">
<svg aria-hidden="true">…</svg>
<clover-tip></clover-tip>
</button>
<script type="module">
const root = document.querySelector("clover-root");
const close = document.getElementById("close");
const start = () => {
const label = root.clover.t("commonClose");
const lang = root.clover.langOf("commonClose");
close.setAttribute("aria-label", label);
close.querySelector("clover-tip").setAttribute("label", label);
if (lang) close.lang = lang;
else close.removeAttribute("lang");
};
if (root.clover) start();
else root.addEventListener("clover-ready", start, { once: true });
</script>Renderers of your own
Give Root<clover-root> (or one Body<clover-body>) renderers.renderers, and a view they claim is drawn by your
component instead of Clover’s own (Canvas, Timeline or Scene): a PDF, a 3D model drawn
your way, anything Clover does not draw, or draws otherwise than you want. Claim a view by
its Canvas id, by a painted body’s format, or with a match test. The first renderer
that claims a view draws it. This is 3.x’s customDisplays.
function ModelViewer({ bodies }) {
// Inside the Root: Clover's hooks work here (useNavigation, useActiveCanvas, useClover…).
return <model-viewer src={bodies[0].id} camera-controls />;
}
<Clover.Root
iiifContent={url}
renderers={[{ format: ["model/gltf-binary"], component: ModelViewer }]}
>
<Clover.Body />
</Clover.Root>;<clover-root
iiif-content="https://iiif.io/api/cookbook/recipe/0608-mvm-3d/manifest.json"
>
<clover-body></clover-body>
</clover-root>
<script type="module">
document.querySelector("clover-root").renderers = [
{
format: ["model/gltf-binary"],
render(container, { bodies }) {
const model = document.createElement("model-viewer");
model.setAttribute("src", bodies[0].id);
model.setAttribute("camera-controls", "");
container.append(model);
},
},
];
</script>A renderer gets the view’s canvases.canvases, their painting bodies (a Choice as its first
item, a SpecificResource as its source) and Body<clover-body>’s children. The Viewer and every
template pass renderers.renderers through to their Root<clover-root>.
Styles
Clover’s stylesheets, the custom properties a layout sets (the Body’s height, the insets, the colors), the classes and states to select, and the color pairs to keep accessible are on Styling.