jekyll-theme-zer0 1.27.0 → 1.28.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +386 -4
- data/_data/consumers.yml +147 -0
- data/_data/features.yml +77 -5
- data/_data/i18n/fr.yml +98 -0
- data/_data/i18n/manifest.yml +1502 -0
- data/_data/navigation/docs.yml +2 -0
- data/_data/navigation/main.yml +6 -0
- data/_data/series.yml +19 -0
- data/_data/theme-manifest.yml +348 -266
- data/_includes/README.md +21 -2
- data/_includes/analytics/google-tag-manager-body.html +11 -2
- data/_includes/analytics/google-tag-manager-head.html +16 -6
- data/_includes/analytics/posthog.html +33 -4
- data/_includes/components/abc-letter.html +43 -0
- data/_includes/components/book-card.html +42 -0
- data/_includes/components/book-nav.html +80 -0
- data/_includes/components/book-plate.html +31 -0
- data/_includes/components/book-toc.html +47 -0
- data/_includes/components/bookshelf.html +68 -0
- data/_includes/components/card-grid.html +60 -0
- data/_includes/components/data-card.html +95 -0
- data/_includes/components/halfmoon.html +5 -1
- data/_includes/components/page-feedback.html +65 -2
- data/_includes/components/theme-controls-bar.html +10 -2
- data/_includes/components/theme-customizer.html +8 -2
- data/_includes/content/seo.html +9 -3
- data/_includes/core/color-mode-init.html +13 -4
- data/_includes/core/favicon.html +46 -0
- data/_includes/core/head.html +9 -0
- data/_includes/custom/body-end.html +18 -0
- data/_includes/custom/body-start.html +17 -0
- data/_includes/custom/footer.html +18 -0
- data/_includes/custom/head.html +18 -0
- data/_includes/navigation/local-graph.html +28 -2
- data/_includes/navigation/nav-tree.html +3 -3
- data/_includes/navigation/sidebar-config.html +21 -0
- data/_includes/navigation/sidebar-nav.html +4 -0
- data/_includes/navigation/sidebar-pagetree.html +150 -0
- data/_includes/navigation/unified-drawer.html +8 -2
- data/_includes/obsidian/full-graph.html +165 -136
- data/_layouts/404.html +260 -0
- data/_layouts/README.md +2 -0
- data/_layouts/book-abc.html +106 -0
- data/_layouts/book-story.html +91 -0
- data/_layouts/book.html +113 -0
- data/_layouts/collection.html +17 -5
- data/_layouts/home.html +5 -2
- data/_layouts/landing.html +9 -0
- data/_layouts/news.html +155 -31
- data/_layouts/root.html +16 -2
- data/_layouts/section.html +57 -15
- data/_sass/components/_book.scss +423 -0
- data/_sass/core/_navbar.scss +13 -0
- data/_sass/core/_obsidian.scss +286 -6
- data/_sass/theme/_backgrounds.scss +21 -8
- data/assets/css/main.scss +1 -0
- data/assets/js/auto-hide-nav.js +5 -1
- data/assets/js/halfmoon.js +26 -0
- data/assets/js/obsidian-graph.js +702 -264
- data/assets/js/obsidian-local-graph.js +161 -54
- data/assets/js/search-modal.js +4 -1
- data/scripts/bin/manifest +31 -4
- data/scripts/bin/validate +5 -1
- data/scripts/install/README.md +47 -6
- data/scripts/install/ai/client.sh +302 -93
- data/scripts/install/ai/prompts/spec.schema.json +1 -1
- data/scripts/install/ai/wizard.sh +10 -5
- data/scripts/install/apply.sh +7 -3
- data/scripts/install/cli.sh +54 -4
- data/scripts/install/config.sh +167 -0
- data/scripts/install/doctor.sh +38 -0
- data/scripts/install/plan.sh +10 -0
- data/scripts/install/spec.sh +15 -7
- data/scripts/install/template.sh +4 -0
- data/scripts/propagate.rb +277 -0
- data/scripts/translate.rb +90 -0
- metadata +27 -2
data/_includes/README.md
CHANGED
|
@@ -9,6 +9,7 @@ This directory has been reorganized for better maintainability and clarity. File
|
|
|
9
9
|
Essential layout components that form the foundation of the site:
|
|
10
10
|
|
|
11
11
|
- `head.html` - HTML document head with meta tags, scripts, and styles
|
|
12
|
+
- `favicon.html` - Favicon / browser-identity tags (icon links, apple-touch, manifest, theme-color) driven by the optional `favicon:` config block
|
|
12
13
|
- `header.html` - Main site header with navigation
|
|
13
14
|
- `footer.html` - Site footer (if exists)
|
|
14
15
|
- `branding.html` - Site branding and title display
|
|
@@ -34,8 +35,8 @@ All navigation-related components:
|
|
|
34
35
|
Analytics and tracking integrations:
|
|
35
36
|
|
|
36
37
|
- `google-analytics.html` - Google Analytics tracking
|
|
37
|
-
- `google-tag-manager-head.html` - GTM head section
|
|
38
|
-
- `google-tag-manager-body.html` - GTM body section
|
|
38
|
+
- `google-tag-manager-head.html` - GTM head section (emits nothing unless `google_tag_manager:` is set in `_config.yml`)
|
|
39
|
+
- `google-tag-manager-body.html` - GTM body section (same config gate)
|
|
39
40
|
|
|
40
41
|
### `components/`
|
|
41
42
|
|
|
@@ -55,6 +56,13 @@ Reusable UI components and widgets:
|
|
|
55
56
|
- `js-cdn.html` - CDN JavaScript libraries
|
|
56
57
|
- `preview-image.html` - Consistent preview image rendering with lazy loading
|
|
57
58
|
- `post-card.html` - Reusable post card component for listings
|
|
59
|
+
- `data-card.html` - One generic card rendered from a plain data hash (title/url/icon/badge/meta/buttons schema in its header)
|
|
60
|
+
- `card-grid.html` - Responsive grid of `data-card`s from any data array — hub dashboards, fleet registries, service grids
|
|
61
|
+
- `bookshelf.html` - Grid of every book in the `books` collection (home-page library)
|
|
62
|
+
- `book-card.html` - One book's portrait cover card (bookshelf tile)
|
|
63
|
+
- `book-toc.html` - Ordered story/chapter list for one book
|
|
64
|
+
- `book-nav.html` - Prev/next-story + contents navigation inside a book
|
|
65
|
+
- `book-plate.html` - Full-width illustration figure with optional caption for story pages
|
|
58
66
|
|
|
59
67
|
### `content/`
|
|
60
68
|
|
|
@@ -79,6 +87,17 @@ Documentation and reference materials:
|
|
|
79
87
|
|
|
80
88
|
- `bootstrap-docs.html` - Bootstrap documentation (moved from style.html)
|
|
81
89
|
|
|
90
|
+
### `custom/` — consumer extension hooks
|
|
91
|
+
|
|
92
|
+
Empty stubs the theme includes at fixed points in the page chrome. A consumer site creates a file at the same path to inject markup there — **no fork of `root.html`, `head.html`, or `footer.html` needed**, so theme updates keep flowing without merge drift:
|
|
93
|
+
|
|
94
|
+
- `custom/head.html` - end of `<head>` (JSON-LD, font preloads, verification tags, extra meta)
|
|
95
|
+
- `custom/body-start.html` - immediately after `<body>` (e.g. GTM `<noscript>`)
|
|
96
|
+
- `custom/footer.html` - after the theme footer, before the cookie-consent banner
|
|
97
|
+
- `custom/body-end.html` - last thing before `</body>` (site scripts, third-party widgets)
|
|
98
|
+
|
|
99
|
+
The stubs ship empty (comment-only) and render nothing until shadowed.
|
|
100
|
+
|
|
82
101
|
## Usage
|
|
83
102
|
|
|
84
103
|
When including files in layouts or other templates, use the full path:
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
{% comment %} Feature: ZER0-039 {% endcomment %}
|
|
2
|
+
{%- comment -%}
|
|
3
|
+
Component: google-tag-manager-body
|
|
4
|
+
Path: _includes/analytics/google-tag-manager-body.html
|
|
5
|
+
Purpose: Google Tag Manager noscript fallback (body section). Config-driven:
|
|
6
|
+
emits nothing unless the site sets a container ID.
|
|
7
|
+
Params: none — reads `google_tag_manager: "<container-id>"` from _config.yml
|
|
8
|
+
Depends on: google-tag-manager-head.html (loader)
|
|
9
|
+
{%- endcomment -%}
|
|
10
|
+
{%- if site.google_tag_manager -%}
|
|
2
11
|
<!-- Google Tag Manager (noscript) -->
|
|
3
12
|
<noscript>
|
|
4
|
-
<iframe src="https://www.googletagmanager.com/ns.html?id=
|
|
13
|
+
<iframe src="https://www.googletagmanager.com/ns.html?id={{ site.google_tag_manager }}"
|
|
5
14
|
height="0" width="0" style="display:none;visibility:hidden">
|
|
6
15
|
</iframe>
|
|
7
16
|
</noscript>
|
|
8
17
|
<!-- End Google Tag Manager (noscript) -->
|
|
9
|
-
|
|
18
|
+
{%- endif -%}
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{% comment %} Feature: ZER0-039 {% endcomment %}
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
{%- comment -%}
|
|
3
|
+
Component: google-tag-manager-head
|
|
4
|
+
Path: _includes/analytics/google-tag-manager-head.html
|
|
5
|
+
Purpose: Google Tag Manager loader (head section). Config-driven: emits
|
|
6
|
+
nothing unless the site sets a container ID, so consumer sites
|
|
7
|
+
never inherit this theme's own GTM container.
|
|
8
|
+
Params: none — reads `google_tag_manager: "<container-id>"` from _config.yml
|
|
9
|
+
Depends on: google-tag-manager-body.html (optional noscript counterpart)
|
|
10
|
+
Notes: Previously the container ID was hardcoded, silently enrolling
|
|
11
|
+
every consumer site's production build into the theme owner's
|
|
12
|
+
GTM container. Set your own ID (or leave unset for no GTM).
|
|
13
|
+
{%- endcomment -%}
|
|
14
|
+
{%- if site.google_tag_manager -%}
|
|
6
15
|
<script>
|
|
7
16
|
// Skip GTM on local/dev hostnames even if a prod build is served locally (Docker/CI preview).
|
|
8
17
|
(function () {
|
|
@@ -13,7 +22,8 @@
|
|
|
13
22
|
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
|
14
23
|
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
|
15
24
|
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
|
16
|
-
})(window,document,'script','dataLayer','
|
|
25
|
+
})(window,document,'script','dataLayer','{{ site.google_tag_manager }}');
|
|
17
26
|
})();
|
|
18
27
|
</script>
|
|
19
|
-
<!-- End Google Tag Manager -->
|
|
28
|
+
<!-- End Google Tag Manager -->
|
|
29
|
+
{%- endif -%}
|
|
@@ -64,6 +64,9 @@ Privacy: Respects Do Not Track and provides opt-out mechanisms
|
|
|
64
64
|
capturePageleave: {{ site.posthog.capture_pageleave | default: true }},
|
|
65
65
|
disableCookie: {{ site.posthog.disable_cookie | default: false }},
|
|
66
66
|
respectDnt: {{ site.posthog.respect_dnt | default: true }},
|
|
67
|
+
respectGpc: {{ site.posthog.privacy.respect_gpc | default: true }},
|
|
68
|
+
requireConsent: {{ site.posthog.privacy.require_consent | default: false }},
|
|
69
|
+
geoipDisable: {{ site.posthog.privacy.geoip_disable | default: false }},
|
|
67
70
|
crossSubdomainCookie: {{ site.posthog.cross_subdomain_cookie | default: false }},
|
|
68
71
|
secureCookie: {{ site.posthog.secure_cookie | default: true }},
|
|
69
72
|
persistence: '{{ site.posthog.persistence | default: "localStorage+cookie" }}',
|
|
@@ -74,9 +77,28 @@ Privacy: Respects Do Not Track and provides opt-out mechanisms
|
|
|
74
77
|
}
|
|
75
78
|
};
|
|
76
79
|
|
|
77
|
-
{% comment %}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
{% comment %} Consent gate (privacy.require_consent): until the visitor
|
|
81
|
+
accepts the "analytics" category in the theme's cookie-consent banner
|
|
82
|
+
(localStorage "zer0-cookie-consent"), PostHog starts opted OUT with
|
|
83
|
+
memory-only persistence — no cookies, nothing stored. The banner's
|
|
84
|
+
applyConsent() flips opt-in/opt-out live once a choice is made. {% endcomment %}
|
|
85
|
+
window.__phHasAnalyticsConsent = true;
|
|
86
|
+
if (window.posthogConfig.requireConsent) {
|
|
87
|
+
window.__phHasAnalyticsConsent = false;
|
|
88
|
+
try {
|
|
89
|
+
var __phStored = window.localStorage.getItem('zer0-cookie-consent');
|
|
90
|
+
if (__phStored) {
|
|
91
|
+
var __phConsent = JSON.parse(__phStored);
|
|
92
|
+
window.__phHasAnalyticsConsent = __phConsent && __phConsent.analytics === true;
|
|
93
|
+
}
|
|
94
|
+
} catch (e) { /* localStorage unavailable — stay opted out */ }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
{% comment %} Honor browser privacy signals before loading: Do Not Track
|
|
98
|
+
and Global Privacy Control (GPC is a legal signal under CCPA/CPRA). {% endcomment %}
|
|
99
|
+
if ((window.posthogConfig.respectDnt && navigator.doNotTrack === '1') ||
|
|
100
|
+
(window.posthogConfig.respectGpc && window.navigator.globalPrivacyControl === true)) {
|
|
101
|
+
console.log('PostHog: respecting browser privacy signal (DNT/GPC)');
|
|
80
102
|
} else {
|
|
81
103
|
{% comment %} PostHog Loading Script {% endcomment %}
|
|
82
104
|
!function(t,e){var o,n,p,r;e.__SV||(window.posthog && window.posthog.__loaded)||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init Rr Mr fi Or Ar ci Tr Cr capture Mi calculateEventProperties Lr register register_once register_for_session unregister unregister_for_session Hr getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey displaySurvey canRenderSurvey canRenderSurveyAsync identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty Ur jr createPersonProfile zr kr Br opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing get_explicit_consent_status is_capturing clear_opt_in_out_capturing Dr debug M Nr getPageViewId captureTraceFeedback captureTraceMetric $r".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
|
@@ -91,7 +113,8 @@ Privacy: Respects Do Not Track and provides opt-out mechanisms
|
|
|
91
113
|
disable_cookie: window.posthogConfig.disableCookie,
|
|
92
114
|
cross_subdomain_cookie: window.posthogConfig.crossSubdomainCookie,
|
|
93
115
|
secure_cookie: window.posthogConfig.secureCookie,
|
|
94
|
-
persistence: window.posthogConfig.persistence,
|
|
116
|
+
persistence: window.__phHasAnalyticsConsent ? window.posthogConfig.persistence : 'memory',
|
|
117
|
+
opt_out_capturing_by_default: !window.__phHasAnalyticsConsent,
|
|
95
118
|
session_recording: window.posthogConfig.sessionRecording,
|
|
96
119
|
{% if site.posthog.privacy.ip_anonymization %}
|
|
97
120
|
ip: false,
|
|
@@ -100,6 +123,12 @@ Privacy: Respects Do Not Track and provides opt-out mechanisms
|
|
|
100
123
|
{% comment %} PostHog loaded successfully {% endcomment %}
|
|
101
124
|
console.log('PostHog analytics loaded successfully');
|
|
102
125
|
|
|
126
|
+
{% comment %} privacy.geoip_disable: stop PostHog from resolving
|
|
127
|
+
IP to location on every event {% endcomment %}
|
|
128
|
+
if (window.posthogConfig.geoipDisable) {
|
|
129
|
+
posthog.register({ '$geoip_disable': true });
|
|
130
|
+
}
|
|
131
|
+
|
|
103
132
|
{% comment %} Track Jekyll-specific page properties {% endcomment %}
|
|
104
133
|
posthog.register({
|
|
105
134
|
'site_title': '{{ site.title }}',
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{% comment %} Feature: ZER0-081 {% endcomment %}
|
|
2
|
+
{%- comment -%}
|
|
3
|
+
Component: abc-letter
|
|
4
|
+
Path: _includes/components/abc-letter.html
|
|
5
|
+
Purpose: One card of an ABC board book — the illustration (or a friendly
|
|
6
|
+
"coming soon" placeholder), a big letter badge, the word, and the
|
|
7
|
+
read-aloud tagline. Rendered once per `alphabet:` entry by
|
|
8
|
+
_layouts/book-abc.html.
|
|
9
|
+
Params: entry (hash, required) — an alphabet entry with keys:
|
|
10
|
+
letter, word, tagline, image, alt, status ("planned"|"rendered")
|
|
11
|
+
Depends on: components/preview-image.html,
|
|
12
|
+
_sass/components/_book.scss (.abc-letter, .abc-letter-*)
|
|
13
|
+
Notes: The big letter is HTML typography, never baked into the art (so
|
|
14
|
+
raster models never garble it). A `status: planned` entry (no
|
|
15
|
+
rendered plate yet) shows a coloured placeholder tile instead of
|
|
16
|
+
a broken image, so the book reads end-to-end before any art runs.
|
|
17
|
+
{%- endcomment -%}
|
|
18
|
+
{%- if include.entry == nil -%}
|
|
19
|
+
{%- comment -%} ERROR: abc-letter requires `entry` {%- endcomment -%}
|
|
20
|
+
{%- else -%}
|
|
21
|
+
{%- assign _e = include.entry -%}
|
|
22
|
+
{%- assign _letter = _e.letter -%}
|
|
23
|
+
{%- comment -%} A plate counts as rendered only when it has an image AND is not
|
|
24
|
+
still "planned". Branch inline: Liquid's assign does not reliably apply the
|
|
25
|
+
"not-equals" comparison, so the condition lives in the if-tag below. {%- endcomment -%}
|
|
26
|
+
<article class="abc-letter" id="letter-{{ _letter | downcase }}">
|
|
27
|
+
<div class="abc-letter-plate">
|
|
28
|
+
{%- if _e.image and _e.status != "planned" -%}
|
|
29
|
+
{%- include components/preview-image.html src=_e.image alt=_e.alt class="abc-letter-img" loading="lazy" -%}
|
|
30
|
+
{%- else -%}
|
|
31
|
+
<div class="abc-letter-placeholder" role="img" aria-label="{{ _e.alt | default: _e.word | escape }} — illustration coming soon">
|
|
32
|
+
<span class="abc-letter-glyph" aria-hidden="true">{{ _letter }}</span>
|
|
33
|
+
<span class="abc-letter-planned">illustration coming soon</span>
|
|
34
|
+
</div>
|
|
35
|
+
{%- endif -%}
|
|
36
|
+
<span class="abc-letter-badge" aria-hidden="true">{{ _letter }}</span>
|
|
37
|
+
</div>
|
|
38
|
+
<p class="abc-letter-word">{{ _e.word }}</p>
|
|
39
|
+
{%- if _e.tagline -%}
|
|
40
|
+
<p class="abc-letter-tagline">{{ _e.tagline }}</p>
|
|
41
|
+
{%- endif -%}
|
|
42
|
+
</article>
|
|
43
|
+
{%- endif -%}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{% comment %} Feature: ZER0-080 {% endcomment %}
|
|
2
|
+
{%- comment -%}
|
|
3
|
+
Component: book-card
|
|
4
|
+
Path: _includes/components/book-card.html
|
|
5
|
+
Purpose: Bookshelf card for one book — portrait cover, title, subtitle,
|
|
6
|
+
chapter count, and audience badge, linking to the book page.
|
|
7
|
+
Params: book (document, required) — a `books` collection doc with
|
|
8
|
+
`layout: book` (the book's landing page)
|
|
9
|
+
Depends on: components/preview-image.html,
|
|
10
|
+
_sass/components/_book.scss (.book-card, .book-cover-frame)
|
|
11
|
+
Notes: Chapter count is derived live from the collection so the card
|
|
12
|
+
never goes stale. Whole card is one link for a big tap target.
|
|
13
|
+
{%- endcomment -%}
|
|
14
|
+
{%- if include.book == nil -%}
|
|
15
|
+
{%- comment -%} ERROR: book-card requires `book` {%- endcomment -%}
|
|
16
|
+
{%- else -%}
|
|
17
|
+
{%- assign _book = include.book -%}
|
|
18
|
+
{%- assign _slug = _book.book | default: _book.slug -%}
|
|
19
|
+
{%- assign _is_abc = _book.layout == "book-abc" -%}
|
|
20
|
+
{%- assign _chapters = site.books | where: "book", _slug | where_exp: "doc", "doc.chapter" -%}
|
|
21
|
+
{%- assign _cover = _book.cover_image | default: _book.preview | default: site.teaser -%}
|
|
22
|
+
<article class="book-card h-100">
|
|
23
|
+
<a href="{{ _book.url | relative_url }}" class="d-block text-decoration-none text-body h-100">
|
|
24
|
+
<div class="book-cover-frame mb-3">
|
|
25
|
+
{%- include components/preview-image.html src=_cover alt=_book.title class="" -%}
|
|
26
|
+
</div>
|
|
27
|
+
<h3 class="h5 mb-1">{{ _book.title }}</h3>
|
|
28
|
+
{%- if _book.subtitle -%}
|
|
29
|
+
<p class="text-body-secondary small mb-2">{{ _book.subtitle }}</p>
|
|
30
|
+
{%- endif -%}
|
|
31
|
+
<p class="small text-body-secondary mb-0">
|
|
32
|
+
{%- if _is_abc -%}
|
|
33
|
+
{%- assign _letters = _book.alphabet | size -%}
|
|
34
|
+
<i class="bi bi-type me-1" aria-hidden="true"></i>{{ _letters }} {% if _letters == 1 %}letter{% else %}letters{% endif %}
|
|
35
|
+
{%- else -%}
|
|
36
|
+
<i class="bi bi-book me-1" aria-hidden="true"></i>{{ _chapters.size }} {% if _chapters.size == 1 %}story{% else %}stories{% endif %}
|
|
37
|
+
{%- endif -%}
|
|
38
|
+
{%- if _book.audience %} <span class="mx-1" aria-hidden="true">•</span> {{ _book.audience }}{% endif -%}
|
|
39
|
+
</p>
|
|
40
|
+
</a>
|
|
41
|
+
</article>
|
|
42
|
+
{%- endif -%}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{% comment %} Feature: ZER0-080 {% endcomment %}
|
|
2
|
+
{%- comment -%}
|
|
3
|
+
Component: book-nav
|
|
4
|
+
Path: _includes/components/book-nav.html
|
|
5
|
+
Purpose: Previous/next story navigation within one book, with a
|
|
6
|
+
"Contents" link back to the book's landing page in the middle.
|
|
7
|
+
Params: book (string, required) — book slug matching chapters' `book:`
|
|
8
|
+
chapter (number, required) — the current chapter number
|
|
9
|
+
Depends on: the `books` collection,
|
|
10
|
+
_sass/components/_post-navigation.scss (.post-nav-card reuse)
|
|
11
|
+
Notes: Ordering comes from the numeric `chapter:` front matter, not
|
|
12
|
+
dates, so books read cover-to-cover. Reuses the article
|
|
13
|
+
post-nav-card styles for a consistent feel.
|
|
14
|
+
{%- endcomment -%}
|
|
15
|
+
{%- if include.book == nil or include.chapter == nil -%}
|
|
16
|
+
{%- comment -%} ERROR: book-nav requires `book` and `chapter` {%- endcomment -%}
|
|
17
|
+
{%- else -%}
|
|
18
|
+
{%- assign _chapters = site.books | where: "book", include.book | where_exp: "doc", "doc.chapter" | sort: "chapter" -%}
|
|
19
|
+
{%- assign _book_page = site.books | where: "book", include.book | where: "layout", "book" | first -%}
|
|
20
|
+
{%- assign _prev = nil -%}
|
|
21
|
+
{%- assign _next = nil -%}
|
|
22
|
+
{%- for _ch in _chapters -%}
|
|
23
|
+
{%- if _ch.chapter == include.chapter -%}
|
|
24
|
+
{%- unless forloop.first -%}
|
|
25
|
+
{%- assign _i = forloop.index0 | minus: 1 -%}
|
|
26
|
+
{%- assign _prev = _chapters[_i] -%}
|
|
27
|
+
{%- endunless -%}
|
|
28
|
+
{%- unless forloop.last -%}
|
|
29
|
+
{%- assign _i = forloop.index0 | plus: 1 -%}
|
|
30
|
+
{%- assign _next = _chapters[_i] -%}
|
|
31
|
+
{%- endunless -%}
|
|
32
|
+
{%- break -%}
|
|
33
|
+
{%- endif -%}
|
|
34
|
+
{%- endfor -%}
|
|
35
|
+
<nav class="book-story-nav my-5 pt-4 border-top" aria-label="Story navigation">
|
|
36
|
+
<div class="row g-3 align-items-stretch">
|
|
37
|
+
<div class="col-md-5">
|
|
38
|
+
{%- if _prev -%}
|
|
39
|
+
<a href="{{ _prev.url | relative_url }}" class="post-nav-card post-nav-card--prev">
|
|
40
|
+
<div class="post-nav-card__body">
|
|
41
|
+
<small class="post-nav-card__label"><i class="bi bi-chevron-left me-1" aria-hidden="true"></i>Previous story</small>
|
|
42
|
+
<span class="post-nav-card__title">{{ _prev.title | truncate: 50 }}</span>
|
|
43
|
+
</div>
|
|
44
|
+
</a>
|
|
45
|
+
{%- else -%}
|
|
46
|
+
<div class="post-nav-card post-nav-card--prev post-nav-card--disabled" aria-disabled="true">
|
|
47
|
+
<div class="post-nav-card__body">
|
|
48
|
+
<small class="post-nav-card__label"><i class="bi bi-chevron-left me-1" aria-hidden="true"></i>Previous story</small>
|
|
49
|
+
<span class="post-nav-card__title text-muted">This is the first story</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
{%- endif -%}
|
|
53
|
+
</div>
|
|
54
|
+
<div class="col-md-2 d-flex">
|
|
55
|
+
{%- if _book_page -%}
|
|
56
|
+
<a href="{{ _book_page.url | relative_url }}" class="btn btn-outline-secondary w-100 align-self-center" aria-label="Back to {{ _book_page.title | escape }} contents">
|
|
57
|
+
<i class="bi bi-list-ul me-1" aria-hidden="true"></i>Contents
|
|
58
|
+
</a>
|
|
59
|
+
{%- endif -%}
|
|
60
|
+
</div>
|
|
61
|
+
<div class="col-md-5">
|
|
62
|
+
{%- if _next -%}
|
|
63
|
+
<a href="{{ _next.url | relative_url }}" class="post-nav-card post-nav-card--next">
|
|
64
|
+
<div class="post-nav-card__body">
|
|
65
|
+
<small class="post-nav-card__label">Next story<i class="bi bi-chevron-right ms-1" aria-hidden="true"></i></small>
|
|
66
|
+
<span class="post-nav-card__title">{{ _next.title | truncate: 50 }}</span>
|
|
67
|
+
</div>
|
|
68
|
+
</a>
|
|
69
|
+
{%- else -%}
|
|
70
|
+
<div class="post-nav-card post-nav-card--next post-nav-card--disabled" aria-disabled="true">
|
|
71
|
+
<div class="post-nav-card__body">
|
|
72
|
+
<small class="post-nav-card__label">Next story<i class="bi bi-chevron-right ms-1" aria-hidden="true"></i></small>
|
|
73
|
+
<span class="post-nav-card__title text-muted">The end — for now</span>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
{%- endif -%}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</nav>
|
|
80
|
+
{%- endif -%}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{% comment %} Feature: ZER0-080 {% endcomment %}
|
|
2
|
+
{%- comment -%}
|
|
3
|
+
Component: book-plate
|
|
4
|
+
Path: _includes/components/book-plate.html
|
|
5
|
+
Purpose: Full-width illustration "plate" inside a book story — a figure
|
|
6
|
+
with rounded art, soft shadow, and an optional caption.
|
|
7
|
+
Params: src (string, required) — image path (site-relative or /assets/…)
|
|
8
|
+
alt (string, required) — accessible description of the scene
|
|
9
|
+
caption (string, optional, default: none) — visible caption text
|
|
10
|
+
class (string, optional, default: "") — extra figure classes
|
|
11
|
+
loading (string, optional, default: "lazy")
|
|
12
|
+
Depends on: components/preview-image.html (path prefixing + relative_url),
|
|
13
|
+
_sass/components/_book.scss (.book-plate)
|
|
14
|
+
Notes: Alt text stays on the <img>; the caption renders only when
|
|
15
|
+
explicitly passed so read-aloud story text isn't duplicated.
|
|
16
|
+
{%- endcomment -%}
|
|
17
|
+
{%- if include.src == nil -%}
|
|
18
|
+
{%- comment -%} ERROR: book-plate requires `src` {%- endcomment -%}
|
|
19
|
+
{%- else -%}
|
|
20
|
+
<figure class="book-plate {{ include.class | default: '' }}">
|
|
21
|
+
{%- include components/preview-image.html
|
|
22
|
+
src=include.src
|
|
23
|
+
alt=include.alt
|
|
24
|
+
class="book-plate-img"
|
|
25
|
+
loading=include.loading
|
|
26
|
+
-%}
|
|
27
|
+
{%- if include.caption -%}
|
|
28
|
+
<figcaption>{{ include.caption | escape }}</figcaption>
|
|
29
|
+
{%- endif -%}
|
|
30
|
+
</figure>
|
|
31
|
+
{%- endif -%}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{% comment %} Feature: ZER0-080 {% endcomment %}
|
|
2
|
+
{%- comment -%}
|
|
3
|
+
Component: book-toc
|
|
4
|
+
Path: _includes/components/book-toc.html
|
|
5
|
+
Purpose: Ordered table of contents for one book — chapter number bubble,
|
|
6
|
+
label (e.g. the month), title, and an optional thumbnail.
|
|
7
|
+
Params: book (string, required) — book slug matching chapters' `book:`
|
|
8
|
+
heading (string, optional, default: "Table of Contents")
|
|
9
|
+
heading_level (string, optional, default: "h2")
|
|
10
|
+
Depends on: the `books` collection,
|
|
11
|
+
_sass/components/_book.scss (.book-toc)
|
|
12
|
+
Notes: Chapters are docs with a numeric `chapter:`; sorted ascending.
|
|
13
|
+
Thumbnails come from each chapter's `preview` and are hidden on
|
|
14
|
+
the smallest screens to keep rows tappable.
|
|
15
|
+
{%- endcomment -%}
|
|
16
|
+
{%- if include.book == nil -%}
|
|
17
|
+
{%- comment -%} ERROR: book-toc requires `book` {%- endcomment -%}
|
|
18
|
+
{%- else -%}
|
|
19
|
+
{%- assign _chapters = site.books | where: "book", include.book | where_exp: "doc", "doc.chapter" | sort: "chapter" -%}
|
|
20
|
+
{%- assign _hl = include.heading_level | default: "h2" -%}
|
|
21
|
+
{%- if _chapters.size > 0 -%}
|
|
22
|
+
<nav class="book-toc" aria-label="Table of contents">
|
|
23
|
+
<{{ _hl }} class="mb-3">{{ include.heading | default: "Table of Contents" }}</{{ _hl }}>
|
|
24
|
+
<ol class="list-unstyled d-flex flex-column gap-2 mb-0">
|
|
25
|
+
{%- for _ch in _chapters -%}
|
|
26
|
+
<li>
|
|
27
|
+
<a class="book-toc-item" href="{{ _ch.url | relative_url }}">
|
|
28
|
+
<span class="book-toc-number" aria-hidden="true">{{ _ch.chapter }}</span>
|
|
29
|
+
{%- if _ch.preview -%}
|
|
30
|
+
<img class="book-toc-thumb d-none d-sm-block" src="{{ _ch.preview | relative_url }}" alt="" loading="lazy" decoding="async">
|
|
31
|
+
{%- endif -%}
|
|
32
|
+
<span class="flex-grow-1">
|
|
33
|
+
{%- if _ch.chapter_label -%}
|
|
34
|
+
<span class="d-block small text-body-secondary">{{ _ch.chapter_label }}</span>
|
|
35
|
+
{%- endif -%}
|
|
36
|
+
<span class="fw-semibold">{{ _ch.title }}</span>
|
|
37
|
+
</span>
|
|
38
|
+
<i class="bi bi-chevron-right text-body-secondary" aria-hidden="true"></i>
|
|
39
|
+
</a>
|
|
40
|
+
</li>
|
|
41
|
+
{%- endfor -%}
|
|
42
|
+
</ol>
|
|
43
|
+
</nav>
|
|
44
|
+
{%- else -%}
|
|
45
|
+
{%- comment -%} book-toc: no chapters found for book "{{ include.book }}" — skipping render {%- endcomment -%}
|
|
46
|
+
{%- endif -%}
|
|
47
|
+
{%- endif -%}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{% comment %} Feature: ZER0-080 · series grouping ZER0-081 {% endcomment %}
|
|
2
|
+
{%- comment -%}
|
|
3
|
+
Component: bookshelf
|
|
4
|
+
Path: _includes/components/bookshelf.html
|
|
5
|
+
Purpose: Responsive grid of every book in the `books` collection, grouped
|
|
6
|
+
into series/category shelves — drop it on a home or index page.
|
|
7
|
+
Params: heading (string, optional) — overall section heading (h2)
|
|
8
|
+
class (string, optional, default: "") — extra wrapper classes
|
|
9
|
+
Depends on: components/book-card.html, the `books` collection,
|
|
10
|
+
_data/series.yml (per-series display metadata)
|
|
11
|
+
Notes: A "book" is any books-collection doc whose layout is `book`
|
|
12
|
+
(story books) or `book-abc` (alphabet books); chapters are never
|
|
13
|
+
listed. Books group by `series:`; a book with no series falls
|
|
14
|
+
under the default `stories` shelf. Series render in the order
|
|
15
|
+
they appear in _data/series.yml, then any undeclared series.
|
|
16
|
+
{%- endcomment -%}
|
|
17
|
+
{%- assign _story_books = site.books | where: "layout", "book" -%}
|
|
18
|
+
{%- assign _abc_books = site.books | where: "layout", "book-abc" -%}
|
|
19
|
+
{%- assign _all = _story_books | concat: _abc_books -%}
|
|
20
|
+
{%- if _all and _all.size > 0 -%}
|
|
21
|
+
{%- assign _default_series = "stories" -%}
|
|
22
|
+
{%- assign _groups = _all | group_by_exp: "doc", "doc.series | default: _default_series" -%}
|
|
23
|
+
|
|
24
|
+
{%- comment -%} Build the shelf order: declared series (file order) first, then any others. {%- endcomment -%}
|
|
25
|
+
{%- assign _order = "," -%}
|
|
26
|
+
{%- for _pair in site.data.series -%}
|
|
27
|
+
{%- assign _needle = _pair[0] | prepend: "," | append: "," -%}
|
|
28
|
+
{%- unless _order contains _needle -%}
|
|
29
|
+
{%- for _g in _groups -%}{%- if _g.name == _pair[0] -%}{%- assign _order = _order | append: _pair[0] | append: "," -%}{%- endif -%}{%- endfor -%}
|
|
30
|
+
{%- endunless -%}
|
|
31
|
+
{%- endfor -%}
|
|
32
|
+
{%- for _g in _groups -%}
|
|
33
|
+
{%- assign _needle = _g.name | prepend: "," | append: "," -%}
|
|
34
|
+
{%- unless _order contains _needle -%}{%- assign _order = _order | append: _g.name | append: "," -%}{%- endunless -%}
|
|
35
|
+
{%- endfor -%}
|
|
36
|
+
{%- assign _order_list = _order | split: "," -%}
|
|
37
|
+
|
|
38
|
+
<section class="bookshelf {{ include.class | default: '' }}">
|
|
39
|
+
{%- if include.heading -%}
|
|
40
|
+
<h2 class="mb-4">{{ include.heading }}</h2>
|
|
41
|
+
{%- endif -%}
|
|
42
|
+
|
|
43
|
+
{%- for _name in _order_list -%}
|
|
44
|
+
{%- if _name == "" -%}{%- continue -%}{%- endif -%}
|
|
45
|
+
{%- assign _group = _groups | where: "name", _name | first -%}
|
|
46
|
+
{%- if _group == nil -%}{%- continue -%}{%- endif -%}
|
|
47
|
+
{%- assign _meta = site.data.series[_name] -%}
|
|
48
|
+
{%- assign _books = _group.items | sort: "title" -%}
|
|
49
|
+
<div class="book-series mb-5" id="series-{{ _name }}">
|
|
50
|
+
<h3 class="h4 mb-1">
|
|
51
|
+
<i class="bi {{ _meta.icon | default: 'bi-book-half' }} me-2" aria-hidden="true"></i>{{ _meta.title | default: _name | escape }}
|
|
52
|
+
</h3>
|
|
53
|
+
{%- if _meta.tagline -%}
|
|
54
|
+
<p class="text-body-secondary mb-3">{{ _meta.tagline | escape }}</p>
|
|
55
|
+
{%- endif -%}
|
|
56
|
+
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-4 g-4">
|
|
57
|
+
{%- for _book in _books -%}
|
|
58
|
+
<div class="col">
|
|
59
|
+
{%- include components/book-card.html book=_book -%}
|
|
60
|
+
</div>
|
|
61
|
+
{%- endfor -%}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
{%- endfor -%}
|
|
65
|
+
</section>
|
|
66
|
+
{%- else -%}
|
|
67
|
+
{%- comment -%} bookshelf: no books collection or no book landing pages — skipping render {%- endcomment -%}
|
|
68
|
+
{%- endif -%}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{%- comment -%}
|
|
2
|
+
===================================================================
|
|
3
|
+
CARD-GRID — responsive grid of data-driven cards
|
|
4
|
+
===================================================================
|
|
5
|
+
|
|
6
|
+
Path: _includes/components/card-grid.html
|
|
7
|
+
Purpose: Render an array of card hashes (see components/data-card.html
|
|
8
|
+
for the item schema) as a responsive Bootstrap grid. Built for
|
|
9
|
+
org hub dashboards, fleet registries, service grids, and any
|
|
10
|
+
"cards from a _data file" page — the markup consumers have
|
|
11
|
+
been hand-rolling per site.
|
|
12
|
+
|
|
13
|
+
Parameters:
|
|
14
|
+
items (array, required) — card hashes (data-card schema)
|
|
15
|
+
columns (string) — row-cols classes,
|
|
16
|
+
default "row-cols-1 row-cols-md-2 row-cols-lg-3"
|
|
17
|
+
gap (string) — gutter class, default "g-4"
|
|
18
|
+
heading_level (string) — heading tag for card titles, default "h3"
|
|
19
|
+
(pick to fit the page outline, e.g. "h2" under an h1)
|
|
20
|
+
card_class (string) — extra classes for every card
|
|
21
|
+
empty_message (string) — message when items is empty/missing;
|
|
22
|
+
omit to render nothing at all
|
|
23
|
+
id (string) — id for the grid wrapper
|
|
24
|
+
aria_label (string) — aria-label for the grid wrapper
|
|
25
|
+
|
|
26
|
+
Example — an org hub page over a generated registry:
|
|
27
|
+
{% include components/card-grid.html
|
|
28
|
+
items=site.data.hub_index.repos
|
|
29
|
+
columns="row-cols-1 row-cols-md-2"
|
|
30
|
+
heading_level="h2"
|
|
31
|
+
empty_message="No member repositories registered yet." %}
|
|
32
|
+
|
|
33
|
+
Data files map 1:1 to the schema, e.g.:
|
|
34
|
+
- title: "1776"
|
|
35
|
+
url: "https://year-of-ai.github.io/1776/"
|
|
36
|
+
external: true
|
|
37
|
+
icon: bi-journal-richtext
|
|
38
|
+
description: "The revolutionary year."
|
|
39
|
+
badge: live
|
|
40
|
+
badge_variant: success
|
|
41
|
+
meta: [ { icon: bi-files, text: "42 pages" } ]
|
|
42
|
+
===================================================================
|
|
43
|
+
{%- endcomment -%}
|
|
44
|
+
{%- assign _cg_items = include.items -%}
|
|
45
|
+
{%- if _cg_items and _cg_items.size > 0 -%}
|
|
46
|
+
<div class="row {{ include.columns | default: 'row-cols-1 row-cols-md-2 row-cols-lg-3' }} {{ include.gap | default: 'g-4' }} zer0-card-grid"
|
|
47
|
+
{%- if include.id %} id="{{ include.id }}"{% endif %}
|
|
48
|
+
{%- if include.aria_label %} role="list" aria-label="{{ include.aria_label | escape }}"{% endif %}>
|
|
49
|
+
{%- for _cg_item in _cg_items %}
|
|
50
|
+
<div class="col"{% if include.aria_label %} role="listitem"{% endif %}>
|
|
51
|
+
{%- include components/data-card.html item=_cg_item heading_level=include.heading_level card_class=include.card_class -%}
|
|
52
|
+
</div>
|
|
53
|
+
{%- endfor %}
|
|
54
|
+
</div>
|
|
55
|
+
{%- elsif include.empty_message -%}
|
|
56
|
+
<div class="text-center py-5 text-body-secondary zer0-card-grid-empty">
|
|
57
|
+
<i class="bi bi-inbox fs-1 d-block mb-2" aria-hidden="true"></i>
|
|
58
|
+
<p class="mb-0">{{ include.empty_message }}</p>
|
|
59
|
+
</div>
|
|
60
|
+
{%- endif -%}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{%- comment -%}
|
|
2
|
+
===================================================================
|
|
3
|
+
DATA-CARD — one generic, data-driven Bootstrap card
|
|
4
|
+
===================================================================
|
|
5
|
+
|
|
6
|
+
Path: _includes/components/data-card.html
|
|
7
|
+
Purpose: Render ONE card from a plain data hash so hub dashboards,
|
|
8
|
+
fleet registries, service grids, and link boards can be
|
|
9
|
+
data-driven instead of hand-written Bootstrap markup.
|
|
10
|
+
Usually invoked via components/card-grid.html.
|
|
11
|
+
|
|
12
|
+
Parameters:
|
|
13
|
+
item (hash, required) — the card data (see schema below)
|
|
14
|
+
heading_level (string) — heading tag for the title, default "h3"
|
|
15
|
+
heading_class (string) — classes for the heading, default "h5"
|
|
16
|
+
card_class (string) — extra classes on the .card element
|
|
17
|
+
|
|
18
|
+
Item schema (every key optional except title):
|
|
19
|
+
title card heading text
|
|
20
|
+
url makes the title a link
|
|
21
|
+
external true → the title link opens in a new tab
|
|
22
|
+
description body copy (plain text or inline HTML)
|
|
23
|
+
icon Bootstrap Icons class shown before the title (e.g. "bi-robot")
|
|
24
|
+
image image path/URL rendered via components/preview-image.html
|
|
25
|
+
image_alt alt text for the image (defaults to title)
|
|
26
|
+
badge short status label rendered next to the title
|
|
27
|
+
badge_variant Bootstrap color suffix for the badge (default "secondary")
|
|
28
|
+
badge_title tooltip for the badge
|
|
29
|
+
meta array of { icon, text } pills rendered under the body
|
|
30
|
+
buttons array of { label, url, icon, variant, external } footer actions
|
|
31
|
+
footer small muted footer text (rendered when no buttons)
|
|
32
|
+
|
|
33
|
+
Example (_data/projects.yml + a page):
|
|
34
|
+
{% for p in site.data.projects %}
|
|
35
|
+
{% include components/data-card.html item=p %}
|
|
36
|
+
{% endfor %}
|
|
37
|
+
===================================================================
|
|
38
|
+
{%- endcomment -%}
|
|
39
|
+
{%- assign _dc = include.item -%}
|
|
40
|
+
{%- assign _dc_heading = include.heading_level | default: "h3" -%}
|
|
41
|
+
{%- assign _dc_heading_class = include.heading_class | default: "h5" -%}
|
|
42
|
+
<div class="card h-100 zer0-data-card {{ include.card_class }}">
|
|
43
|
+
{%- if _dc.image -%}
|
|
44
|
+
{%- assign _dc_img_alt = _dc.image_alt | default: _dc.title -%}
|
|
45
|
+
{%- if _dc.url -%}<a href="{{ _dc.url | relative_url }}" tabindex="-1" aria-hidden="true">{%- endif -%}
|
|
46
|
+
{%- include components/preview-image.html src=_dc.image alt=_dc_img_alt class="card-img-top" style="height: 160px; object-fit: cover;" -%}
|
|
47
|
+
{%- if _dc.url -%}</a>{%- endif -%}
|
|
48
|
+
{%- endif -%}
|
|
49
|
+
<div class="card-body d-flex flex-column">
|
|
50
|
+
<{{ _dc_heading }} class="card-title {{ _dc_heading_class }} mb-2 d-flex align-items-center">
|
|
51
|
+
{%- if _dc.icon %}<i class="bi {{ _dc.icon }} me-2" aria-hidden="true"></i>{% endif -%}
|
|
52
|
+
<span class="flex-grow-1">
|
|
53
|
+
{%- if _dc.url -%}
|
|
54
|
+
<a href="{{ _dc.url | relative_url }}" class="text-decoration-none text-body-emphasis"
|
|
55
|
+
{%- if _dc.external %} target="_blank" rel="noopener"{% endif %}>{{ _dc.title }}</a>
|
|
56
|
+
{%- else -%}
|
|
57
|
+
{{ _dc.title }}
|
|
58
|
+
{%- endif -%}
|
|
59
|
+
</span>
|
|
60
|
+
{%- if _dc.badge %}
|
|
61
|
+
<span class="badge text-bg-{{ _dc.badge_variant | default: 'secondary' }} ms-2"
|
|
62
|
+
{%- if _dc.badge_title %} title="{{ _dc.badge_title | escape }}"{% endif %}>{{ _dc.badge }}</span>
|
|
63
|
+
{%- endif %}
|
|
64
|
+
</{{ _dc_heading }}>
|
|
65
|
+
{%- if _dc.description %}
|
|
66
|
+
<p class="card-text text-body-secondary mb-2">{{ _dc.description }}</p>
|
|
67
|
+
{%- endif %}
|
|
68
|
+
{%- if _dc.meta and _dc.meta.size > 0 %}
|
|
69
|
+
<div class="d-flex flex-wrap gap-2 mt-auto pt-2">
|
|
70
|
+
{%- for m in _dc.meta %}
|
|
71
|
+
<span class="badge rounded-pill text-bg-light border text-body-secondary fw-normal">
|
|
72
|
+
{%- if m.icon %}<i class="bi {{ m.icon }} me-1" aria-hidden="true"></i>{% endif -%}
|
|
73
|
+
{{ m.text }}
|
|
74
|
+
</span>
|
|
75
|
+
{%- endfor %}
|
|
76
|
+
</div>
|
|
77
|
+
{%- endif %}
|
|
78
|
+
</div>
|
|
79
|
+
{%- if _dc.buttons and _dc.buttons.size > 0 %}
|
|
80
|
+
<div class="card-footer bg-transparent d-flex flex-wrap gap-2">
|
|
81
|
+
{%- for b in _dc.buttons %}
|
|
82
|
+
<a class="btn btn-sm btn-{{ b.variant | default: 'outline-primary' }}"
|
|
83
|
+
href="{{ b.url | relative_url }}"
|
|
84
|
+
{%- if b.external %} target="_blank" rel="noopener"{% endif %}>
|
|
85
|
+
{%- if b.icon %}<i class="bi {{ b.icon }} me-1" aria-hidden="true"></i>{% endif -%}
|
|
86
|
+
{{ b.label }}
|
|
87
|
+
</a>
|
|
88
|
+
{%- endfor %}
|
|
89
|
+
</div>
|
|
90
|
+
{%- elsif _dc.footer %}
|
|
91
|
+
<div class="card-footer bg-transparent">
|
|
92
|
+
<small class="text-body-secondary">{{ _dc.footer }}</small>
|
|
93
|
+
</div>
|
|
94
|
+
{%- endif %}
|
|
95
|
+
</div>
|