@heroiclands/package-build 22.22.0 → 22.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +1 -0
- package/bin/content-commands.mjs +51 -14
- package/ci/content-format-reference.mjs +1 -1
- package/content-config.mjs +80 -2
- package/docs/api.md +85 -72
- package/docs/authoring/assets.md +29 -0
- package/docs/authoring/dates-and-calendars.md +37 -1
- package/docs/authoring/links-and-markup.md +112 -31
- package/docs/commands.md +30 -29
- package/docs/configuration.md +92 -19
- package/docs/content-format.md +1 -1
- package/docs/guides/book.md +5 -4
- package/docs/guides/module-project.md +2 -2
- package/docs/guides/site.md +41 -0
- package/docs/project-setup.md +4 -4
- package/docs/reference/format-details.md +323 -60
- package/docs/reference/note-types.md +9 -9
- package/docs/reference/theme-pages.md +496 -0
- package/engine/anchors.mjs +16 -10
- package/engine/art-fields.mjs +118 -0
- package/engine/art-slots.mjs +9 -2
- package/engine/asset-index.mjs +160 -10
- package/engine/asset-replacement.mjs +93 -0
- package/engine/asset-types.mjs +240 -0
- package/engine/base-compiler.mjs +54 -2
- package/engine/bundles.mjs +13 -1
- package/engine/content-blocks.mjs +4 -4
- package/engine/content-embeds.mjs +19 -0
- package/engine/content-figures.mjs +468 -0
- package/engine/content-footnotes.mjs +10 -2
- package/engine/content-format.yaml +1 -1
- package/engine/content-images.mjs +126 -25
- package/engine/content-index.mjs +36 -10
- package/engine/field-reference.mjs +31 -10
- package/engine/field-spec.mjs +10 -8
- package/engine/foreign-catalog.mjs +145 -19
- package/engine/frontmatter-lint.mjs +73 -1
- package/engine/heading-attributes.mjs +31 -18
- package/engine/helpers.mjs +102 -33
- package/engine/infobox-registry.mjs +4 -1
- package/engine/infobox.mjs +47 -8
- package/engine/itinerary-scenes.mjs +4 -10
- package/engine/journals.mjs +264 -39
- package/engine/lore-events.mjs +197 -0
- package/engine/markdown-expressions.mjs +257 -7
- package/engine/note-dates.mjs +305 -12
- package/engine/note-vocabulary.mjs +11 -5
- package/engine/pathnames.mjs +36 -0
- package/engine/pdf-build.mjs +357 -34
- package/engine/pdf-images.mjs +64 -0
- package/engine/pdf-render.mjs +160 -36
- package/engine/scenes.mjs +16 -4
- package/engine/site-build.mjs +127 -13
- package/engine/site-config.mjs +19 -24
- package/engine/site-index.mjs +72 -1
- package/engine/site-preview.mjs +2 -0
- package/engine/system-block.mjs +44 -16
- package/hm3/actors.mjs +2 -2
- package/hugo-theme/data/banners.yaml +69 -0
- package/hugo-theme/layouts/404.html +85 -0
- package/hugo-theme/layouts/_default/baseof.html +64 -0
- package/hugo-theme/layouts/_default/list.html +139 -0
- package/hugo-theme/layouts/_default/single.html +4 -0
- package/hugo-theme/layouts/blog/list.html +54 -0
- package/hugo-theme/layouts/blog-post/single.html +36 -0
- package/hugo-theme/layouts/index.html +75 -0
- package/hugo-theme/layouts/partials/banner-url.html +117 -0
- package/hugo-theme/layouts/partials/cdn-url.html +36 -0
- package/hugo-theme/layouts/partials/draft-notice.html +41 -0
- package/hugo-theme/layouts/partials/draft-state.html +26 -0
- package/hugo-theme/layouts/partials/footer.html +29 -0
- package/hugo-theme/layouts/partials/header.html +35 -0
- package/hugo-theme/layouts/partials/hero-banner.html +86 -0
- package/hugo-theme/layouts/partials/holdings/entry.html +21 -0
- package/hugo-theme/layouts/partials/holdings/kind-groups.html +71 -0
- package/hugo-theme/layouts/partials/holdings.html +61 -0
- package/hugo-theme/layouts/partials/infobox/entry.html +15 -0
- package/hugo-theme/layouts/partials/infobox/items.html +26 -0
- package/hugo-theme/layouts/partials/infobox/section.html +56 -0
- package/hugo-theme/layouts/partials/infobox/value.html +43 -0
- package/hugo-theme/layouts/partials/infobox.html +57 -0
- package/hugo-theme/layouts/partials/page-body.html +154 -0
- package/hugo-theme/layouts/partials/place-map.html +9 -0
- package/hugo-theme/layouts/partials/related.html +121 -0
- package/hugo-theme/layouts/partials/search.html +84 -0
- package/hugo-theme/layouts/partials/site-url.html +32 -0
- package/hugo-theme/layouts/partials/tags.html +30 -0
- package/hugo-theme/layouts/partials/toc.html +28 -0
- package/hugo-theme/layouts/partials/warn-missing-banner.html +3 -0
- package/hugo-theme/layouts/shortcodes/cartouche.html +64 -0
- package/hugo-theme/layouts/shortcodes/exturl.html +12 -0
- package/hugo-theme/layouts/shortcodes/glyph.html +43 -0
- package/hugo-theme/layouts/shortcodes/pagelist.html +63 -0
- package/hugo-theme/static/css/sohl-icons.css +57 -0
- package/hugo-theme/static/css/style.css +2953 -0
- package/hugo-theme/static/fonts/game-icons-kb.woff2 +0 -0
- package/hugo-theme/static/img/banners/takheperu-hieroglyphs.svg +1 -0
- package/hugo-theme/static/img/cartouches/takheperu.svg +1 -0
- package/hugo-theme/theme.toml +11 -0
- package/manifest.mjs +30 -24
- package/package.json +2 -1
- package/packagebuild-metadata.jsonl +13 -13
- package/sohl/item-fields.mjs +4 -3
- package/types/content-config.d.mts +48 -1
- package/types/engine/anchors.d.mts +9 -4
- package/types/engine/art-fields.d.mts +64 -0
- package/types/engine/art-slots.d.mts +2 -1
- package/types/engine/asset-index.d.mts +43 -0
- package/types/engine/asset-replacement.d.mts +22 -0
- package/types/engine/asset-types.d.mts +18 -0
- package/types/engine/base-compiler.d.mts +0 -17
- package/types/engine/content-embeds.d.mts +16 -0
- package/types/engine/content-figures.d.mts +101 -0
- package/types/engine/content-footnotes.d.mts +7 -0
- package/types/engine/content-images.d.mts +78 -12
- package/types/engine/content-index.d.mts +8 -2
- package/types/engine/field-reference.d.mts +21 -0
- package/types/engine/field-spec.d.mts +20 -16
- package/types/engine/foreign-catalog.d.mts +39 -0
- package/types/engine/heading-attributes.d.mts +13 -8
- package/types/engine/helpers.d.mts +28 -10
- package/types/engine/infobox-registry.d.mts +4 -1
- package/types/engine/infobox.d.mts +6 -0
- package/types/engine/journals.d.mts +31 -9
- package/types/engine/lore-events.d.mts +14 -0
- package/types/engine/markdown-expressions.d.mts +23 -2
- package/types/engine/note-dates.d.mts +64 -1
- package/types/engine/pathnames.d.mts +26 -0
- package/types/engine/pdf-build.d.mts +28 -6
- package/types/engine/pdf-images.d.mts +52 -0
- package/types/engine/pdf-render.d.mts +12 -0
- package/types/engine/site-build.d.mts +14 -3
- package/types/engine/site-config.d.mts +9 -9
- package/types/engine/site-index.d.mts +54 -0
- package/types/engine/system-block.d.mts +26 -2
- package/types/hm3/actors.d.mts +18 -0
- package/types/manifest.d.mts +19 -23
- package/engine/content-captions.mjs +0 -230
- package/types/engine/content-captions.d.mts +0 -58
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{{ define "main" }}
|
|
2
|
+
{{ partial "hero-banner.html" . }}
|
|
3
|
+
<article class="single">
|
|
4
|
+
<div class="single-content">
|
|
5
|
+
{{ if .Content }}
|
|
6
|
+
<div class="single-body">
|
|
7
|
+
{{ .Content }}
|
|
8
|
+
</div>
|
|
9
|
+
{{ end }}
|
|
10
|
+
|
|
11
|
+
{{- /* Blog list: walk every descendant page in any /blog/YYYY/MM/
|
|
12
|
+
date bucket, filter to actual blog posts, sort newest-first,
|
|
13
|
+
and emit a list of entries with date + series + title +
|
|
14
|
+
description. The default list.html skips auto-listing for
|
|
15
|
+
hand-crafted section indexes (any _index.md), which is
|
|
16
|
+
correct for most sections but wrong for a blog landing. */ -}}
|
|
17
|
+
{{- $posts := where .Site.RegularPages "Section" "blog" -}}
|
|
18
|
+
{{- $posts = where $posts "Type" "blog-post" -}}
|
|
19
|
+
{{- $posts = $posts.ByDate.Reverse -}}
|
|
20
|
+
|
|
21
|
+
{{- with $posts }}
|
|
22
|
+
<ul class="blog-list">
|
|
23
|
+
{{- range . }}
|
|
24
|
+
<li class="blog-list-item">
|
|
25
|
+
{{ if .Date }}
|
|
26
|
+
<time class="blog-list-date" datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "January 2, 2006" }}</time>
|
|
27
|
+
{{ end }}
|
|
28
|
+
<a class="blog-list-title" href="{{ .RelPermalink }}">{{ .Title }}</a>
|
|
29
|
+
{{- /* Auto-generate a ~100-word preview from the rendered body.
|
|
30
|
+
`.Plain` strips HTML tags. `htmlUnescape` then decodes
|
|
31
|
+
entities so smart quotes Goldmark emitted as `’`
|
|
32
|
+
etc. become real characters (otherwise the browser
|
|
33
|
+
displays them literally after Hugo re-escapes the `&`).
|
|
34
|
+
Collapse paragraph-break whitespace into single spaces,
|
|
35
|
+
and append an ellipsis only if we actually truncated. */ -}}
|
|
36
|
+
{{- $plain := .Plain | htmlUnescape -}}
|
|
37
|
+
{{- $collapsed := trim (replaceRE `\s+` " " $plain) " " -}}
|
|
38
|
+
{{- $words := split $collapsed " " -}}
|
|
39
|
+
{{- $preview := $collapsed -}}
|
|
40
|
+
{{- if gt (len $words) 100 -}}
|
|
41
|
+
{{- $preview = printf "%s…" (delimit (first 100 $words) " ") -}}
|
|
42
|
+
{{- end -}}
|
|
43
|
+
{{- with $preview }}
|
|
44
|
+
<p class="blog-list-desc">{{ . }}</p>
|
|
45
|
+
{{- end }}
|
|
46
|
+
</li>
|
|
47
|
+
{{- end }}
|
|
48
|
+
</ul>
|
|
49
|
+
{{- else }}
|
|
50
|
+
<p class="page-list-empty">No posts yet.</p>
|
|
51
|
+
{{- end }}
|
|
52
|
+
</div>
|
|
53
|
+
</article>
|
|
54
|
+
{{ end }}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{{ define "main" }}
|
|
2
|
+
{{ partial "hero-banner.html" . }}
|
|
3
|
+
|
|
4
|
+
{{- /* Blog posts use the same content chrome as other pages but with
|
|
5
|
+
a blog-specific meta line (series + date) and no right-rail
|
|
6
|
+
sidebar or auto-generated TOC. If a post grows long enough to
|
|
7
|
+
warrant a TOC, we can opt it in via a frontmatter flag later. */ -}}
|
|
8
|
+
|
|
9
|
+
<article class="single">
|
|
10
|
+
<div class="single-content">
|
|
11
|
+
<div class="single-meta blog-meta">
|
|
12
|
+
{{ with .Params.series }}
|
|
13
|
+
<div class="blog-series">
|
|
14
|
+
<span class="blog-series-label">Series</span>
|
|
15
|
+
<span class="blog-series-name">{{ . }}</span>
|
|
16
|
+
</div>
|
|
17
|
+
{{ end }}
|
|
18
|
+
{{ if .Params.date }}
|
|
19
|
+
<time class="single-date" datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "January 2, 2006" }}</time>
|
|
20
|
+
{{ end }}
|
|
21
|
+
{{ partial "tags.html" . }}
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="single-body" data-pagefind-body>
|
|
25
|
+
{{ .Content }}
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
{{ partial "related.html" . }}
|
|
29
|
+
|
|
30
|
+
<nav class="single-nav">
|
|
31
|
+
{{ with .PrevInSection }}<a href="{{ .Permalink }}" class="nav-prev">← {{ .Title }}</a>{{ end }}
|
|
32
|
+
{{ with .NextInSection }}<a href="{{ .Permalink }}" class="nav-next">{{ .Title }} →</a>{{ end }}
|
|
33
|
+
</nav>
|
|
34
|
+
</div>
|
|
35
|
+
</article>
|
|
36
|
+
{{ end }}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{{- /*
|
|
2
|
+
Home layout. Two shapes share it, chosen by what the home page carries.
|
|
3
|
+
|
|
4
|
+
**A package homepage** — the entry point to one published package, at its
|
|
5
|
+
own prefix (/sohl/, /thalorna/, /kethira/, …), mounted as the package's
|
|
6
|
+
`_index.md` and selected when it declares `type: homepage`. It renders
|
|
7
|
+
through the same structure `_default/single.html` gives a note — the hero
|
|
8
|
+
from its `banner`, then its body — through the shared `page-body.html`
|
|
9
|
+
partial, so a homepage and a page look like one family. A package supplies
|
|
10
|
+
only its own title, description, banner and body; the theme owns the
|
|
11
|
+
arrangement.
|
|
12
|
+
|
|
13
|
+
**The featured grid** — a site whose home page is a set of entry points into
|
|
14
|
+
everything it publishes. This is the older shape and stays the default, so a
|
|
15
|
+
consumer that has not adopted the homepage note keeps rendering exactly as
|
|
16
|
+
before.
|
|
17
|
+
|
|
18
|
+
The theme owns the arrangement; the consuming site owns every address and
|
|
19
|
+
every word. The grid reads them from `params.home`:
|
|
20
|
+
|
|
21
|
+
heroimage Banner behind the hero band. Resolved through cdn-url.html,
|
|
22
|
+
so it may be absolute or relative to `params.cdnBaseURL`.
|
|
23
|
+
herotitle Hero heading.
|
|
24
|
+
herotagline Hero standfirst.
|
|
25
|
+
headings Column headings across the top row of the featured grid.
|
|
26
|
+
Each: `title`, and an optional `class`, appended as
|
|
27
|
+
`home-section-title--<class>` for grid placement.
|
|
28
|
+
cards The entry-point cards. Each: `title`, `url`, `text`, and
|
|
29
|
+
optional `image`, `alt` (image alt text, defaulting to
|
|
30
|
+
`title`) and `key` (appended as `home-card--<key>` for
|
|
31
|
+
grid placement).
|
|
32
|
+
|
|
33
|
+
Headings and cards render flat, as direct children of `.home-featured`,
|
|
34
|
+
because they are the grid's items — the CSS places each one by its
|
|
35
|
+
modifier class.
|
|
36
|
+
*/ -}}
|
|
37
|
+
{{ define "main" }}
|
|
38
|
+
{{- if eq (.Params.type | default "") "homepage" -}}
|
|
39
|
+
{{- partial "hero-banner.html" . -}}
|
|
40
|
+
{{- partial "page-body.html" . -}}
|
|
41
|
+
{{- else -}}
|
|
42
|
+
{{- with .Site.Params.home }}
|
|
43
|
+
<section class="hero hero-with-image"{{ with .heroimage }} style="background-image: url('{{ partial "cdn-url.html" (dict "site" $.Site "path" .) }}');"{{ end }}>
|
|
44
|
+
<div class="hero-inner" data-pagefind-ignore>
|
|
45
|
+
<h1>{{ .herotitle }}</h1>
|
|
46
|
+
{{- with .herotagline }}
|
|
47
|
+
<p class="hero-tagline">{{ . }}</p>
|
|
48
|
+
{{- end }}
|
|
49
|
+
</div>
|
|
50
|
+
</section>
|
|
51
|
+
|
|
52
|
+
<section class="home-sections">
|
|
53
|
+
<div class="home-featured">
|
|
54
|
+
{{- range .headings }}
|
|
55
|
+
<h2 class="home-section-title{{ with .class }} home-section-title--{{ . }}{{ end }}">{{ .title }}</h2>
|
|
56
|
+
{{- end }}
|
|
57
|
+
{{- range .cards }}
|
|
58
|
+
|
|
59
|
+
<div class="home-card{{ with .key }} home-card--{{ . }}{{ end }}">
|
|
60
|
+
{{- $url := .url }}{{ $title := .title }}{{ $alt := .alt | default .title }}
|
|
61
|
+
{{- with .image }}
|
|
62
|
+
<a href="{{ $url }}">
|
|
63
|
+
<img src="{{ partial "cdn-url.html" (dict "site" $.Site "path" .) }}" alt="{{ $alt }}" class="card-image">
|
|
64
|
+
</a>
|
|
65
|
+
{{- end }}
|
|
66
|
+
<h2><a href="{{ $url }}">{{ $title }}</a></h2>
|
|
67
|
+
<p>{{ .text }}</p>
|
|
68
|
+
<a href="{{ $url }}" class="card-button">More Info →</a>
|
|
69
|
+
</div>
|
|
70
|
+
{{- end }}
|
|
71
|
+
</div>
|
|
72
|
+
</section>
|
|
73
|
+
{{- end }}
|
|
74
|
+
{{- end }}
|
|
75
|
+
{{ end }}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{{- /*
|
|
2
|
+
Resolve a `banner:` value to a URL, or to "" when there is none.
|
|
3
|
+
|
|
4
|
+
Split out of `hero-banner.html` (issue #41): a single implementation is what
|
|
5
|
+
keeps a second caller from drifting the way issue #36 already failed once —
|
|
6
|
+
silently, behind a dead URL.
|
|
7
|
+
|
|
8
|
+
Resolution, in order:
|
|
9
|
+
1. `data.banner` — a note's own hero image, which the content build has
|
|
10
|
+
already resolved from an asset Address into the address its asset host
|
|
11
|
+
serves. It is used exactly as given. A blank one is a note naming no
|
|
12
|
+
hero image on purpose and declines an image.
|
|
13
|
+
2. `banner` — a hand-authored page's value. `none` (or `false`) declines an
|
|
14
|
+
image and returns "". A full URL is used as-is; anything else is a
|
|
15
|
+
fragment under `images/`.
|
|
16
|
+
3. `images/banners/{subtype}.webp`, when neither is set and a `subtype` is
|
|
17
|
+
supplied. A caller that passes no subtype gets "" instead, which is
|
|
18
|
+
what a card wants: a section with no banner of its own renders as the
|
|
19
|
+
text card it rendered before.
|
|
20
|
+
4. `images/banners/{fallback}` — whenever the name picked by 2 or 3 is not
|
|
21
|
+
in `data/banners.yaml`'s declared inventory. Hugo cannot ask a remote
|
|
22
|
+
host whether a URL exists, so the declaration stands in for the test;
|
|
23
|
+
see `hero-banner.html` for why, and `data/banners.yaml` for how the
|
|
24
|
+
list is kept honest.
|
|
25
|
+
|
|
26
|
+
The check applies only to a relative path resolving directly inside the
|
|
27
|
+
declared banner directory. An absolute URL, and a fragment pointing anywhere
|
|
28
|
+
else under `images/`, pass through untouched: the theme has no inventory for
|
|
29
|
+
either and must not second-guess an address it cannot know about. A step 1
|
|
30
|
+
address is never checked either — the build resolved it against a real asset
|
|
31
|
+
record and dropped one nothing answered, so there is no question left to
|
|
32
|
+
ask.
|
|
33
|
+
|
|
34
|
+
Whatever survives resolves through `cdn-url.html`, so the theme names a path
|
|
35
|
+
and never a host.
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
{{ partial "banner-url.html" (dict "site" .Site "banner" .Params.banner
|
|
39
|
+
"subtype" $subtype "context" .RelPermalink) }}
|
|
40
|
+
|
|
41
|
+
Params:
|
|
42
|
+
site — the site context (.Site), for `params.cdnBaseURL`.
|
|
43
|
+
data — the page's `data` map (`.Params.data`), read for the `banner`
|
|
44
|
+
the content build resolved. Omit for a page that carries none.
|
|
45
|
+
banner — the raw `banner:` value; may be unset, `none`/`false`, a
|
|
46
|
+
fragment or an absolute URL.
|
|
47
|
+
subtype — optional. The name to default to when `banner` is unset. Omit
|
|
48
|
+
for "no banner means no image".
|
|
49
|
+
context — optional. A page address, named in the missing-banner warning so
|
|
50
|
+
the report says where the want came from.
|
|
51
|
+
*/ -}}
|
|
52
|
+
{{- $site := .site -}}
|
|
53
|
+
{{- $subtype := .subtype | default "" -}}
|
|
54
|
+
{{- $context := .context | default "" -}}
|
|
55
|
+
{{- $data := .data | default dict -}}
|
|
56
|
+
|
|
57
|
+
{{- $banners := hugo.Data.banners | default dict -}}
|
|
58
|
+
{{- $dir := $banners.dir | default "images/banners" -}}
|
|
59
|
+
{{- $fallback := $banners.fallback | default "default.webp" -}}
|
|
60
|
+
{{- $available := $banners.available | default slice -}}
|
|
61
|
+
|
|
62
|
+
{{- /* `none`/`false` is a decision, not an absence, so it is read before the
|
|
63
|
+
resolution order rather than as a step in it. Formatted rather than
|
|
64
|
+
tested for truth, and deliberately not passed through `default`: both
|
|
65
|
+
`false` and an unset banner are falsy, and only formatting tells them
|
|
66
|
+
apart. */ -}}
|
|
67
|
+
{{- $raw := printf "%v" .banner -}}
|
|
68
|
+
{{- $declined := or (eq (lower $raw) "none") (eq $raw "false") -}}
|
|
69
|
+
|
|
70
|
+
{{- /* A note's resolved banner, formatted for the same reason the decline
|
|
71
|
+
above is: an unset key and a deliberate blank are both falsy, and only
|
|
72
|
+
formatting tells them apart. `<nil>` covers both an absent key and an
|
|
73
|
+
explicit `null`, which the note format reads as *unset* — so the
|
|
74
|
+
resolution carries on to the subtype default. `""` is the format's
|
|
75
|
+
deliberate blank: a note naming no hero image, which declines one. */ -}}
|
|
76
|
+
{{- $resolved := printf "%v" (index $data "banner") -}}
|
|
77
|
+
{{- $fromBuild := ne $resolved "<nil>" -}}
|
|
78
|
+
|
|
79
|
+
{{- $banner := "" -}}
|
|
80
|
+
{{- $absolute := false -}}
|
|
81
|
+
{{- if $fromBuild -}}
|
|
82
|
+
{{- $banner = $resolved -}}
|
|
83
|
+
{{- $absolute = true -}}
|
|
84
|
+
{{- else if not $declined -}}
|
|
85
|
+
{{- with .banner -}}
|
|
86
|
+
{{- $banner = printf "images/%s" (strings.TrimPrefix "/" .) -}}
|
|
87
|
+
{{- if or (hasPrefix . "http://") (hasPrefix . "https://") (hasPrefix . "//") -}}
|
|
88
|
+
{{- $banner = . -}}
|
|
89
|
+
{{- $absolute = true -}}
|
|
90
|
+
{{- end -}}
|
|
91
|
+
{{- else -}}
|
|
92
|
+
{{- with $subtype -}}
|
|
93
|
+
{{- $banner = printf "%s/%s.webp" $dir . -}}
|
|
94
|
+
{{- end -}}
|
|
95
|
+
{{- end -}}
|
|
96
|
+
|
|
97
|
+
{{- /* The declared-existence check. Deduplicated by name, so a subtype with
|
|
98
|
+
no artwork is reported once per build rather than once per page. */ -}}
|
|
99
|
+
{{- if and $banner (not $absolute) (eq (path.Dir $banner) $dir) -}}
|
|
100
|
+
{{- $name := strings.TrimSuffix (path.Ext $banner) (path.Base $banner) -}}
|
|
101
|
+
{{- if not (in $available $name) -}}
|
|
102
|
+
{{- partialCached "warn-missing-banner.html" (dict
|
|
103
|
+
"name" $name
|
|
104
|
+
"dir" $dir
|
|
105
|
+
"fallback" $fallback
|
|
106
|
+
"context" $context
|
|
107
|
+
) $name -}}
|
|
108
|
+
{{- $banner = printf "%s/%s" $dir $fallback -}}
|
|
109
|
+
{{- end -}}
|
|
110
|
+
{{- end -}}
|
|
111
|
+
{{- end -}}
|
|
112
|
+
|
|
113
|
+
{{- $url := "" -}}
|
|
114
|
+
{{- if $banner -}}
|
|
115
|
+
{{- $url = partial "cdn-url.html" (dict "site" $site "path" $banner) -}}
|
|
116
|
+
{{- end -}}
|
|
117
|
+
{{- return $url -}}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{{- /*
|
|
2
|
+
Resolve an asset path against the consuming site's asset host.
|
|
3
|
+
|
|
4
|
+
The theme carries layout, not addresses: it never names the host its
|
|
5
|
+
imagery is served from. A consumer sets `params.cdnBaseURL` to the root of
|
|
6
|
+
wherever it publishes shared artwork, and every image reference in the
|
|
7
|
+
theme resolves through here.
|
|
8
|
+
|
|
9
|
+
Resolution:
|
|
10
|
+
1. An already-absolute path (http://, https://, //) is returned as-is,
|
|
11
|
+
so page frontmatter can point anywhere.
|
|
12
|
+
2. Otherwise the path is joined onto `params.cdnBaseURL`.
|
|
13
|
+
3. With no `params.cdnBaseURL` set, the path resolves against the site
|
|
14
|
+
itself via relURL — correct under a bare root *and* under a baseURL
|
|
15
|
+
carrying a path prefix.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
{{ partial "cdn-url.html" (dict "site" .Site "path" "images/x.webp") }}
|
|
19
|
+
|
|
20
|
+
Params:
|
|
21
|
+
site — the site context (.Site), for reading params.
|
|
22
|
+
path — the asset path, absolute or relative to the asset root.
|
|
23
|
+
*/ -}}
|
|
24
|
+
{{- $path := .path -}}
|
|
25
|
+
{{- $url := "" -}}
|
|
26
|
+
{{- if or (hasPrefix $path "http://") (hasPrefix $path "https://") (hasPrefix $path "//") -}}
|
|
27
|
+
{{- $url = $path -}}
|
|
28
|
+
{{- else -}}
|
|
29
|
+
{{- $rel := strings.TrimPrefix "/" $path -}}
|
|
30
|
+
{{- with .site.Params.cdnBaseURL -}}
|
|
31
|
+
{{- $url = printf "%s/%s" (strings.TrimSuffix "/" .) $rel -}}
|
|
32
|
+
{{- else -}}
|
|
33
|
+
{{- $url = $rel | relURL -}}
|
|
34
|
+
{{- end -}}
|
|
35
|
+
{{- end -}}
|
|
36
|
+
{{- return $url -}}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{{- /*
|
|
2
|
+
A page's statement that its own content is not settled.
|
|
3
|
+
|
|
4
|
+
A draft page reads exactly like a finished one, and a reader who is not told
|
|
5
|
+
assumes it is finished — so the page says so itself, at the head of the
|
|
6
|
+
article, above the infobox and above the prose. A reader deciding whether to
|
|
7
|
+
rely on an entry has to be told before they read it.
|
|
8
|
+
|
|
9
|
+
The label alone will not do. "Draft" names the writing, so it reads as rough
|
|
10
|
+
prose; what has to land is that the facts may change. The label and the
|
|
11
|
+
sentence are one unit, and the sentence carries the meaning by itself — which
|
|
12
|
+
is the floor this is built to, because the mark is a third-party font and the
|
|
13
|
+
rule is a stylesheet, and either can be absent.
|
|
14
|
+
|
|
15
|
+
The sentence is the theme's default and a consumer overrides it with
|
|
16
|
+
`params.draftNotice`, the way `params.notfound` supplies its own wording. The
|
|
17
|
+
theme states no consumer's name and no address.
|
|
18
|
+
|
|
19
|
+
The mark is `circle-exclamation`: a notice, not a hazard. Draft-ness is a
|
|
20
|
+
status, and a page in a corpus where one page in several carries one cannot
|
|
21
|
+
wear a hazard mark without teaching a reader to discount it. `aria-hidden`
|
|
22
|
+
keeps it out of a screen reader, where the label reads the same thing.
|
|
23
|
+
|
|
24
|
+
The sentence sets in italic and the label in bold roman. Every other word on
|
|
25
|
+
the page is the world describing itself; this sentence is the page speaking
|
|
26
|
+
about its own state, and italic is how a reader is told the voice has
|
|
27
|
+
changed. It is an `em`, so the italic arrives with the markup rather than
|
|
28
|
+
with the stylesheet, and a bold italic beside the mark would be one emphasis
|
|
29
|
+
too many.
|
|
30
|
+
|
|
31
|
+
Silent-disappear: a settled page emits nothing here.
|
|
32
|
+
|
|
33
|
+
Context: the page.
|
|
34
|
+
*/ -}}
|
|
35
|
+
{{- if partial "draft-state.html" . -}}
|
|
36
|
+
{{- $notice := .Site.Params.draftNotice | default "This entry is unfinished. What it states may change, and nothing in it is settled." -}}
|
|
37
|
+
<aside class="draft-notice">
|
|
38
|
+
<i class="fa-solid fa-circle-exclamation draft-notice-icon" aria-hidden="true"></i>
|
|
39
|
+
<p class="draft-notice-text"><strong class="draft-notice-label">Draft.</strong> <em>{{ $notice }}</em></p>
|
|
40
|
+
</aside>
|
|
41
|
+
{{ end -}}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{{- /*
|
|
2
|
+
Whether a page's note is an unfinished draft.
|
|
3
|
+
|
|
4
|
+
The `draft` tag is the authored signal, and it is the only one either side
|
|
5
|
+
can read: the content build asks it through its own single reader
|
|
6
|
+
(package-build/engine/note-vocabulary.mjs, `DRAFT_TAG`) and a theme cannot
|
|
7
|
+
import that, so the shared fact is the tag itself. It reaches the page
|
|
8
|
+
because the build copies a note's own front matter onto it.
|
|
9
|
+
|
|
10
|
+
It is spelled once, here. Two partials act on the state — the tag row leaves
|
|
11
|
+
it out of a page's subjects, and the notice states it — and a second literal
|
|
12
|
+
is how the two come apart.
|
|
13
|
+
|
|
14
|
+
Hugo's own reserved `draft: true` is a different thing entirely and is never
|
|
15
|
+
written by the build: a page carrying it is not rendered at all.
|
|
16
|
+
|
|
17
|
+
Context: the page. Returns "true" for a draft and "" for anything else, which
|
|
18
|
+
is what `if` reads.
|
|
19
|
+
*/ -}}
|
|
20
|
+
{{- $draft := false -}}
|
|
21
|
+
{{- range .Params.tags -}}
|
|
22
|
+
{{- if eq (lower .) "draft" -}}
|
|
23
|
+
{{- $draft = true -}}
|
|
24
|
+
{{- end -}}
|
|
25
|
+
{{- end -}}
|
|
26
|
+
{{- return $draft -}}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<footer class="site-footer" data-pagefind-ignore>
|
|
2
|
+
<div class="footer-inner">
|
|
3
|
+
<p class="footer-text">
|
|
4
|
+
© 2020–{{ now.Year }} Tom Rodriguez. Content under The World of Thalorna and Song of Heroic Lands is licensed under
|
|
5
|
+
<a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>; all other content is © Tom Rodriguez, all rights reserved.{{ with .Site.Params.brand }}{{ with .licenseURL }} See
|
|
6
|
+
<a href="{{ . }}">License</a>.{{ end }}{{ end }}
|
|
7
|
+
</p>
|
|
8
|
+
<div class="footer-links">
|
|
9
|
+
<a href="https://creativecommons.org/licenses/by-sa/4.0/">
|
|
10
|
+
<img src="https://licensebuttons.net/l/by-sa/4.0/88x31.png" alt="CC BY-SA 4.0" class="cc-badge">
|
|
11
|
+
</a>
|
|
12
|
+
{{- with .Site.Params.brand }}{{ with .discordURL }}
|
|
13
|
+
<a href="{{ . }}">
|
|
14
|
+
<svg width="24" height="19" viewBox="0 0 71 55" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
|
15
|
+
<path d="M60.1 4.9A58.5 58.5 0 0045.4.2a.2.2 0 00-.2.1 40.8 40.8 0 00-1.8 3.7 54 54 0 00-16.2 0A39 39 0 0025.4.3a.2.2 0 00-.2-.1A58.4 58.4 0 0010.5 5a.2.2 0 00-.1 0A60 60 0 00.3 45.3a.2.2 0 000 .2 58.7 58.7 0 0017.7 9 .2.2 0 00.3-.1 42 42 0 003.6-5.9.2.2 0 00-.1-.3 38.6 38.6 0 01-5.5-2.6.2.2 0 01 0-.4c.4-.3.7-.6 1.1-.8a.2.2 0 01.2 0 41.9 41.9 0 0035.6 0 .2.2 0 01.2 0l1 .9a.2.2 0 010 .3 36.2 36.2 0 01-5.5 2.6.2.2 0 00-.1.4 47.1 47.1 0 003.6 5.8.2.2 0 00.2.1A58.5 58.5 0 0070.3 45.5a.2.2 0 000-.2A59.7 59.7 0 0060.2 5a.2.2 0 00-.1 0zM23.7 37.3c-3.5 0-6.4-3.2-6.4-7.1s2.8-7.1 6.4-7.1 6.5 3.2 6.4 7.1c0 3.9-2.8 7.1-6.4 7.1zm23.7 0c-3.5 0-6.4-3.2-6.4-7.1s2.8-7.1 6.4-7.1 6.5 3.2 6.4 7.1c0 3.9-2.8 7.1-6.4 7.1z"/>
|
|
16
|
+
</svg>
|
|
17
|
+
Discord
|
|
18
|
+
</a>
|
|
19
|
+
{{- end }}{{ end }}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</footer>
|
|
23
|
+
|
|
24
|
+
<script>
|
|
25
|
+
document.querySelector('.nav-toggle')?.addEventListener('click', function() {
|
|
26
|
+
document.querySelector('.site-nav').classList.toggle('open');
|
|
27
|
+
this.classList.toggle('open');
|
|
28
|
+
});
|
|
29
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<header class="site-header" data-pagefind-ignore>
|
|
2
|
+
<div class="header-inner">
|
|
3
|
+
<a href="{{ .Site.Home.RelPermalink }}" class="site-title">
|
|
4
|
+
{{- with .Site.Params.brand }}{{ with .logo }}
|
|
5
|
+
<img src="{{ partial "cdn-url.html" (dict "site" $.Site "path" .) }}" alt="Heroic Lands" class="site-logo">
|
|
6
|
+
{{- end }}{{ end }}
|
|
7
|
+
<span class="title-main">Heroic Lands</span>
|
|
8
|
+
</a>
|
|
9
|
+
<div class="header-actions">
|
|
10
|
+
<nav class="site-nav">
|
|
11
|
+
{{ range .Site.Menus.main }}
|
|
12
|
+
{{ if .HasChildren }}
|
|
13
|
+
<div class="nav-dropdown">
|
|
14
|
+
<a href="{{ .URL }}" class="nav-link{{ if $.IsMenuCurrent "main" . }} active{{ end }}">
|
|
15
|
+
{{ .Name }}
|
|
16
|
+
<svg class="dropdown-arrow" width="10" height="6" viewBox="0 0 10 6" fill="currentColor"><path d="M1 1l4 4 4-4"/></svg>
|
|
17
|
+
</a>
|
|
18
|
+
<div class="dropdown-menu">
|
|
19
|
+
{{ range .Children }}
|
|
20
|
+
<a href="{{ .URL }}" class="dropdown-item">{{ .Name }}</a>
|
|
21
|
+
{{ end }}
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
{{ else }}
|
|
25
|
+
<a href="{{ .URL }}" class="nav-link{{ if $.IsMenuCurrent "main" . }} active{{ end }}">{{ .Name }}</a>
|
|
26
|
+
{{ end }}
|
|
27
|
+
{{ end }}
|
|
28
|
+
</nav>
|
|
29
|
+
{{ partial "search.html" . }}
|
|
30
|
+
<button class="nav-toggle" aria-label="Toggle navigation">
|
|
31
|
+
<span></span><span></span><span></span>
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</header>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{{- /*
|
|
2
|
+
Hero banner partial.
|
|
3
|
+
|
|
4
|
+
Displays a full-width banner band (using the same .hero.hero-with-image
|
|
5
|
+
classes as the homepage) with a centered page title and description
|
|
6
|
+
above the content.
|
|
7
|
+
|
|
8
|
+
Every path below resolves through cdn-url.html against the consuming
|
|
9
|
+
site's `params.cdnBaseURL`; the theme names no host of its own.
|
|
10
|
+
|
|
11
|
+
The resolution itself lives in `banner-url.html`, which a derived landing card
|
|
12
|
+
calls too. The order is documented in both places because this is where a
|
|
13
|
+
reader looking at a banner starts; that partial is where it happens.
|
|
14
|
+
|
|
15
|
+
Banner image resolution order:
|
|
16
|
+
1. .Params.data.banner — a note's own hero image, which the content
|
|
17
|
+
build resolved from an asset Address into
|
|
18
|
+
the address its asset host serves. Used
|
|
19
|
+
exactly as given. A blank declines a hero
|
|
20
|
+
image; unset or `null` carries on.
|
|
21
|
+
2. .Params.banner — a hand-authored page's value. `none` (or
|
|
22
|
+
`false`) declines a hero image entirely —
|
|
23
|
+
see "Declining" below. A full URL is used
|
|
24
|
+
as-is; anything else is treated as a
|
|
25
|
+
fragment under images/.
|
|
26
|
+
3. images/banners/{subtype}.webp — subtype-specific default, where
|
|
27
|
+
subtype = category for type=doc pages,
|
|
28
|
+
otherwise type
|
|
29
|
+
4. images/banners/default.webp — used when the page has no type, *and*
|
|
30
|
+
whenever the name picked by 2 or 3 is not
|
|
31
|
+
in the declared inventory (see below).
|
|
32
|
+
|
|
33
|
+
Existence, and why step 4 is a declaration rather than a test.
|
|
34
|
+
The image is served from the consumer's asset host, and Hugo cannot ask a
|
|
35
|
+
remote host whether a URL exists — so a name that was never drawn would be
|
|
36
|
+
emitted unchecked and 404 in silence, the band rendering with its title,
|
|
37
|
+
palette and gradient intact behind a dead URL. `data/banners.yaml`
|
|
38
|
+
is the declaration that stands in for the test it cannot make: a resolved name
|
|
39
|
+
absent from `available` falls back to `fallback` and warns once. That file
|
|
40
|
+
documents how the list is kept honest (`npm run lint:banners` fetches every
|
|
41
|
+
name and fails on a non-200), and a consumer publishing its own artwork set
|
|
42
|
+
overrides the whole list by shipping its own `data/banners.yaml`.
|
|
43
|
+
|
|
44
|
+
The check applies only to a *relative* path resolving directly inside the
|
|
45
|
+
declared banner directory. An absolute URL, and a fragment pointing anywhere
|
|
46
|
+
else under images/, pass through untouched: the theme has no inventory for
|
|
47
|
+
either and must not second-guess an address it cannot know about. A step 1
|
|
48
|
+
address is not checked either — the build resolved it against a real asset
|
|
49
|
+
record, so the question has already been answered.
|
|
50
|
+
|
|
51
|
+
Declining a hero image.
|
|
52
|
+
`banner: none` on a hand-authored page, and a blank `data.banner` on a note,
|
|
53
|
+
render the band with no image at all — the title, the palette
|
|
54
|
+
and the .hero-with-image gradient, which is what gives the band its presence.
|
|
55
|
+
This is not the same as having no banner yet: a package may have a standing
|
|
56
|
+
editorial reason to publish no imagery, and the fan-material carve-outs do.
|
|
57
|
+
Falling back to a default for them would substitute artwork where the
|
|
58
|
+
considered answer was "none", so declining has to be sayable.
|
|
59
|
+
*/ -}}
|
|
60
|
+
|
|
61
|
+
{{- /* For narrative content (type=doc) the meaningful kind of page lives in
|
|
62
|
+
`category` — continent, region, polity, faith, lore, etc. — so prefer
|
|
63
|
+
that when picking a default banner. */ -}}
|
|
64
|
+
{{- $type := .Params.type | default "" -}}
|
|
65
|
+
{{- $subtype := cond (eq $type "doc") (.Params.category | default "") $type -}}
|
|
66
|
+
{{- if not $subtype }}{{ $subtype = "default" }}{{ end -}}
|
|
67
|
+
|
|
68
|
+
{{- /* The order above, the `none` decision and the declared-existence check
|
|
69
|
+
all live in `banner-url.html`, which a derived landing card calls too
|
|
70
|
+
(issue #41). A hero band always wants the subtype default; a card never
|
|
71
|
+
does. */ -}}
|
|
72
|
+
{{- $bannerURL := partial "banner-url.html" (dict
|
|
73
|
+
"site" .Site
|
|
74
|
+
"data" .Params.data
|
|
75
|
+
"banner" .Params.banner
|
|
76
|
+
"subtype" $subtype
|
|
77
|
+
"context" .RelPermalink) -}}
|
|
78
|
+
|
|
79
|
+
<section class="hero hero-with-image"{{ with $bannerURL }} style="background-image: url('{{ . }}');"{{ end }}>
|
|
80
|
+
<div class="hero-inner" data-pagefind-ignore>
|
|
81
|
+
<h1>{{ .Title }}</h1>
|
|
82
|
+
{{- with .Params.description }}
|
|
83
|
+
<p class="hero-tagline">{{ . }}</p>
|
|
84
|
+
{{- end }}
|
|
85
|
+
</div>
|
|
86
|
+
</section>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{{- /*
|
|
2
|
+
One entry of a holdings list — `{title, url, type, subType}`, as
|
|
3
|
+
`@heroiclands/package-build` derives from `contains`, `held_by` and
|
|
4
|
+
`holdings` (see the "holdings card" section of the README).
|
|
5
|
+
|
|
6
|
+
`url` is absent for a stub: a note with an empty body, which carries its
|
|
7
|
+
facts into every list that names it but publishes no page of its own. The
|
|
8
|
+
build omits the key outright rather than writing `url: null` — confirmed by
|
|
9
|
+
stringifying a stub entry through the same `gray-matter`/`js-yaml` path the
|
|
10
|
+
build writes frontmatter with — but `with` guards both the same way, since
|
|
11
|
+
a missing map key and an empty string both read false. This is the same
|
|
12
|
+
guard `infobox/entry.html` already applies to its own `{text, url}` shape.
|
|
13
|
+
|
|
14
|
+
Takes `entry` and the `class` its medium gives it.
|
|
15
|
+
*/ -}}
|
|
16
|
+
{{- $entry := .entry -}}
|
|
17
|
+
{{- with $entry.url -}}
|
|
18
|
+
<a class="{{ $.class }}" href="{{ . }}">{{ $entry.title }}</a>
|
|
19
|
+
{{- else -}}
|
|
20
|
+
<span class="{{ $.class }}">{{ $entry.title }}</span>
|
|
21
|
+
{{- end -}}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{{- /*
|
|
2
|
+
The kind-grouped body of a "Within" or "Holdings" table: its entries
|
|
3
|
+
broken into named groups by `subType`, in the closed order region,
|
|
4
|
+
settlement, site, structure, feature — the order a reader wants a place
|
|
5
|
+
hierarchy read in, not alphabetical. A `subType` outside that order, or
|
|
6
|
+
missing entirely, is grouped last, one group per remaining kind,
|
|
7
|
+
alphabetically by its `humanize`d label.
|
|
8
|
+
|
|
9
|
+
Each group is a CSS grid (`repeat(auto-fill, minmax(14em, 1fr))`, the same
|
|
10
|
+
the Related card uses) so a region with forty settlements stays scannable.
|
|
11
|
+
|
|
12
|
+
Takes the entries (a `HoldingsEntry` list). Renders nothing for an empty
|
|
13
|
+
list, so a caller can include it unconditionally once its own presence
|
|
14
|
+
check on the list has passed.
|
|
15
|
+
*/ -}}
|
|
16
|
+
{{- $entries := . -}}
|
|
17
|
+
{{- $kindOrder := slice "region" "settlement" "site" "structure" "feature" -}}
|
|
18
|
+
{{- $kindLabels := dict
|
|
19
|
+
"region" "Regions"
|
|
20
|
+
"settlement" "Settlements"
|
|
21
|
+
"site" "Sites"
|
|
22
|
+
"structure" "Structures"
|
|
23
|
+
"feature" "Features"
|
|
24
|
+
-}}
|
|
25
|
+
|
|
26
|
+
{{- /* Bucket entries by subType, defaulting an absent one to "". */ -}}
|
|
27
|
+
{{- $byKind := dict -}}
|
|
28
|
+
{{- range $entries -}}
|
|
29
|
+
{{- $kind := .subType | default "" -}}
|
|
30
|
+
{{- $existing := index $byKind $kind | default slice -}}
|
|
31
|
+
{{- $byKind = merge $byKind (dict $kind ($existing | append .)) -}}
|
|
32
|
+
{{- end -}}
|
|
33
|
+
|
|
34
|
+
{{- /* The closed order first. */ -}}
|
|
35
|
+
{{- $groups := slice -}}
|
|
36
|
+
{{- $seen := slice -}}
|
|
37
|
+
{{- range $kindOrder -}}
|
|
38
|
+
{{- $kind := . -}}
|
|
39
|
+
{{- with index $byKind $kind -}}
|
|
40
|
+
{{- $groups = $groups | append (dict "label" (index $kindLabels $kind) "entries" .) -}}
|
|
41
|
+
{{- $seen = $seen | append $kind -}}
|
|
42
|
+
{{- end -}}
|
|
43
|
+
{{- end -}}
|
|
44
|
+
|
|
45
|
+
{{- /* Anything else — a kind outside the closed set, or none at all —
|
|
46
|
+
grouped last, alphabetically by its rendered label. */ -}}
|
|
47
|
+
{{- $leftover := slice -}}
|
|
48
|
+
{{- range $kind, $group := $byKind -}}
|
|
49
|
+
{{- if not (in $seen $kind) -}}
|
|
50
|
+
{{- $label := humanize $kind -}}
|
|
51
|
+
{{- if eq $kind "" -}}{{- $label = "Other" -}}{{- end -}}
|
|
52
|
+
{{- $leftover = $leftover | append (dict "label" $label "entries" $group) -}}
|
|
53
|
+
{{- end -}}
|
|
54
|
+
{{- end -}}
|
|
55
|
+
{{- if $leftover -}}
|
|
56
|
+
{{- $leftover = sort $leftover "label" "asc" -}}
|
|
57
|
+
{{- range $leftover -}}
|
|
58
|
+
{{- $groups = $groups | append . -}}
|
|
59
|
+
{{- end -}}
|
|
60
|
+
{{- end -}}
|
|
61
|
+
|
|
62
|
+
{{- range $groups }}
|
|
63
|
+
<div class="holdings-kind-group">
|
|
64
|
+
<h4 class="holdings-kind-label">{{ .label }}</h4>
|
|
65
|
+
<ul class="holdings-table-grid">
|
|
66
|
+
{{- range .entries }}
|
|
67
|
+
<li>{{ partial "holdings/entry.html" (dict "entry" . "class" "holdings-entry-name") }}</li>
|
|
68
|
+
{{- end }}
|
|
69
|
+
</ul>
|
|
70
|
+
</div>
|
|
71
|
+
{{- end -}}
|