jekyll-theme-zer0 1.26.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.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +558 -5
  3. data/README.md +10 -27
  4. data/_data/authors.yml +4 -3
  5. data/_data/backlog.yml +28 -0
  6. data/_data/consumers.yml +147 -0
  7. data/_data/features.yml +142 -23
  8. data/_data/i18n/fr.yml +98 -0
  9. data/_data/i18n/languages.yml +36 -0
  10. data/_data/i18n/manifest.yml +1502 -0
  11. data/_data/navigation/docs.yml +2 -0
  12. data/_data/navigation/main.yml +6 -0
  13. data/_data/series.yml +19 -0
  14. data/_data/theme-manifest.yml +348 -268
  15. data/_data/ui-text.yml +36 -246
  16. data/_includes/README.md +25 -2
  17. data/_includes/analytics/google-tag-manager-body.html +11 -2
  18. data/_includes/analytics/google-tag-manager-head.html +16 -6
  19. data/_includes/analytics/posthog.html +33 -4
  20. data/_includes/components/abc-letter.html +43 -0
  21. data/_includes/components/author-avatar-url.html +4 -2
  22. data/_includes/components/book-card.html +42 -0
  23. data/_includes/components/book-nav.html +80 -0
  24. data/_includes/components/book-plate.html +31 -0
  25. data/_includes/components/book-toc.html +47 -0
  26. data/_includes/components/bookshelf.html +68 -0
  27. data/_includes/components/card-grid.html +60 -0
  28. data/_includes/components/data-card.html +95 -0
  29. data/_includes/components/env-switcher.html +3 -1
  30. data/_includes/components/halfmoon.html +5 -1
  31. data/_includes/components/language-toggle.html +81 -0
  32. data/_includes/components/page-feedback.html +65 -2
  33. data/_includes/components/search-modal.html +2 -2
  34. data/_includes/components/shortcuts-modal.html +1 -1
  35. data/_includes/components/theme-controls-bar.html +10 -2
  36. data/_includes/components/theme-customizer.html +8 -2
  37. data/_includes/components/translation-notice.html +27 -0
  38. data/_includes/content/intro.html +16 -15
  39. data/_includes/content/seo.html +9 -3
  40. data/_includes/core/color-mode-init.html +13 -4
  41. data/_includes/core/favicon.html +46 -0
  42. data/_includes/core/footer.html +9 -4
  43. data/_includes/core/head.html +18 -0
  44. data/_includes/core/header.html +9 -4
  45. data/_includes/core/hreflang.html +33 -0
  46. data/_includes/core/i18n.html +36 -0
  47. data/_includes/custom/body-end.html +18 -0
  48. data/_includes/custom/body-start.html +17 -0
  49. data/_includes/custom/footer.html +18 -0
  50. data/_includes/custom/head.html +18 -0
  51. data/_includes/navigation/breadcrumbs.html +1 -1
  52. data/_includes/navigation/local-graph.html +28 -2
  53. data/_includes/navigation/nav-tree.html +3 -3
  54. data/_includes/navigation/navbar.html +5 -4
  55. data/_includes/navigation/sidebar-config.html +21 -0
  56. data/_includes/navigation/sidebar-nav.html +4 -0
  57. data/_includes/navigation/sidebar-pagetree.html +150 -0
  58. data/_includes/navigation/sidebar-right.html +3 -2
  59. data/_includes/navigation/unified-drawer.html +9 -3
  60. data/_includes/obsidian/full-graph.html +165 -136
  61. data/_layouts/404.html +260 -0
  62. data/_layouts/README.md +2 -0
  63. data/_layouts/article.html +7 -1
  64. data/_layouts/book-abc.html +106 -0
  65. data/_layouts/book-story.html +91 -0
  66. data/_layouts/book.html +113 -0
  67. data/_layouts/collection.html +17 -5
  68. data/_layouts/default.html +5 -3
  69. data/_layouts/home.html +5 -2
  70. data/_layouts/landing.html +9 -0
  71. data/_layouts/news.html +159 -33
  72. data/_layouts/root.html +29 -9
  73. data/_layouts/section.html +61 -17
  74. data/_sass/components/_book.scss +423 -0
  75. data/_sass/core/_navbar.scss +13 -0
  76. data/_sass/core/_obsidian.scss +295 -7
  77. data/_sass/layouts/_navbar-extras.scss +6 -1
  78. data/_sass/theme/_backgrounds.scss +21 -8
  79. data/assets/css/main.scss +1 -0
  80. data/assets/js/auto-hide-nav.js +5 -1
  81. data/assets/js/halfmoon.js +26 -0
  82. data/assets/js/obsidian-graph.js +707 -265
  83. data/assets/js/obsidian-local-graph.js +161 -54
  84. data/assets/js/search-modal.js +4 -1
  85. data/scripts/README.md +20 -26
  86. data/scripts/bin/audit-consumer +1 -1
  87. data/scripts/bin/manifest +31 -5
  88. data/scripts/bin/sync-plugins +0 -1
  89. data/scripts/bin/validate +5 -1
  90. data/scripts/dev/rasterize-svg.js +65 -0
  91. data/scripts/features/generate-preview-images +49 -1390
  92. data/scripts/features/install-preview-generator +55 -33
  93. data/scripts/install/README.md +55 -25
  94. data/scripts/install/ai/client.sh +302 -93
  95. data/scripts/install/ai/prompts/spec.schema.json +1 -1
  96. data/scripts/install/ai/prompts/wizard.system.md +8 -17
  97. data/scripts/install/ai/wizard.sh +10 -5
  98. data/scripts/install/apply.sh +7 -3
  99. data/scripts/install/cli.sh +54 -4
  100. data/scripts/install/config.sh +167 -0
  101. data/scripts/install/doctor.sh +38 -0
  102. data/scripts/install/plan.sh +10 -0
  103. data/scripts/install/spec.sh +15 -7
  104. data/scripts/install/template.sh +4 -0
  105. data/scripts/lib/README.md +1 -5
  106. data/scripts/lib/install/deploy/README.md +3 -9
  107. data/scripts/lib/preview_generator.py +2261 -1341
  108. data/scripts/propagate.rb +277 -0
  109. data/scripts/translate.rb +1204 -0
  110. metadata +34 -3
  111. data/_plugins/preview_image_generator.rb +0 -351
@@ -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>
@@ -132,7 +132,8 @@
132
132
  </div>
133
133
  </li>
134
134
 
135
- <!-- Development URL -->
135
+ {% unless is_production %}
136
+ <!-- Development URL (dev builds only — never leak localhost into production HTML) -->
136
137
  <li class="list-group-item d-flex justify-content-between align-items-center gap-2">
137
138
  <div class="d-flex align-items-center min-w-0">
138
139
  <span class="badge bg-warning text-dark me-2">Dev</span>
@@ -162,6 +163,7 @@
162
163
  </button>
163
164
  </div>
164
165
  </li>
166
+ {% endunless %}
165
167
 
166
168
  {% if site.repository %}
167
169
  <!-- GitHub Source -->
@@ -9,8 +9,11 @@
9
9
  page (settings offcanvas + theme-controls-bar).
10
10
  params: class (string, optional, default "") — extra btn-group classes,
11
11
  e.g. "w-100" to fill the parent.
12
+ config: site.color_mode_lock: true removes the control entirely — the
13
+ site pins one mode and visitor switching is disabled
14
+ (halfmoon.js + core/color-mode-init.html enforce the same).
12
15
  -->
13
-
16
+ {% unless site.color_mode_lock %}
14
17
  <div class="btn-group bd-theme-switch {{ include.class | default: '' }}" role="group" aria-label="Color mode">
15
18
  <button type="button" class="btn btn-outline-secondary btn-sm d-flex align-items-center justify-content-center gap-1" data-bs-theme-value="light" aria-pressed="false">
16
19
  <svg class="bi flex-shrink-0" width="1em" height="1em" fill="currentColor" aria-hidden="true"><use href="#sun-fill"></use></svg>
@@ -25,3 +28,4 @@
25
28
  <span>Auto</span>
26
29
  </button>
27
30
  </div>
31
+ {% endunless %}
@@ -0,0 +1,81 @@
1
+ {% comment %} Feature: ZER0-078 {% endcomment %}
2
+ {%- comment -%}
3
+ Component: language-toggle
4
+ Path: _includes/components/language-toggle.html
5
+ Purpose: Navbar dropdown that switches between the English original and
6
+ its machine-generated translations (scripts/translate.rb).
7
+ Shows the source language plus every configured target
8
+ language; targets without a generated translation for the
9
+ current page render as disabled entries.
10
+ Params: none — reads page.lang / page.url / page.translation_source_url
11
+ and the generated _data/i18n/manifest.yml.
12
+ Depends on: site.translation.* config, site.data.i18n.manifest,
13
+ _data/i18n/languages.yml (display names), core/i18n.html,
14
+ Bootstrap 5 dropdown, Bootstrap Icons (bi-translate)
15
+ Notes: Renders nothing when translation is disabled or no target
16
+ languages are configured. The chosen language is remembered in
17
+ localStorage("zer0-lang") for future UX (no auto-redirect).
18
+ Must NOT be include_cached — the menu is page-dependent.
19
+ {%- endcomment -%}
20
+ {%- assign _lt_langs = site.translation.languages -%}
21
+ {%- if site.translation.enabled and _lt_langs and _lt_langs.size > 0 -%}
22
+ {%- include core/i18n.html -%}
23
+ {%- assign _lt_source = site.translation.source_lang | default: 'en' -%}
24
+ {%- assign _lt_current = page.lang | default: _lt_source -%}
25
+ {%- if _lt_current == _lt_source -%}
26
+ {%- assign _lt_src_url = page.url -%}
27
+ {%- else -%}
28
+ {%- assign _lt_src_url = page.translation_source_url -%}
29
+ {%- endif -%}
30
+ {%- assign _lt_entry = site.data.i18n.manifest.pages[_lt_src_url] -%}
31
+ {%- assign _lt_names = site.data.i18n.languages -%}
32
+ {%- assign _lt_source_name = _lt_names[_lt_source].native | default: _lt_source -%}
33
+ <div class="dropdown nav-lang-toggle" id="zer0-lang-toggle">
34
+ <button
35
+ class="btn nav-lang-button dropdown-toggle"
36
+ type="button"
37
+ id="langToggleButton"
38
+ data-bs-toggle="dropdown"
39
+ aria-expanded="false"
40
+ aria-label="{{ ui.lang_toggle_aria | default: 'Change language' }}"
41
+ title="{{ ui.lang_toggle_label | default: 'Language' }}"
42
+ >
43
+ <i class="{{ site.default_icon }} bi-translate" aria-hidden="true"></i>
44
+ <span class="nav-link-text d-none d-xl-inline ms-1 text-uppercase">{{ _lt_current | slice: 0, 2 }}</span>
45
+ </button>
46
+ <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="langToggleButton">
47
+ <li>
48
+ {%- if _lt_current == _lt_source %}
49
+ <span class="dropdown-item active" aria-current="true">{{ _lt_source_name }}</span>
50
+ {%- else %}
51
+ <a class="dropdown-item" data-lang="{{ _lt_source }}" hreflang="{{ _lt_source }}" href="{{ _lt_src_url | default: '/' | relative_url }}">{{ _lt_source_name }}</a>
52
+ {%- endif %}
53
+ </li>
54
+ {%- for _lt_lang in _lt_langs -%}
55
+ {%- assign _lt_name = _lt_names[_lt_lang].native | default: _lt_lang -%}
56
+ {%- assign _lt_t = _lt_entry[_lt_lang] %}
57
+ <li>
58
+ {%- if _lt_current == _lt_lang %}
59
+ <span class="dropdown-item active" aria-current="true">{{ _lt_name }}</span>
60
+ {%- elsif _lt_t %}
61
+ <a class="dropdown-item" data-lang="{{ _lt_lang }}" hreflang="{{ _lt_lang }}" href="{{ _lt_t.url | relative_url }}">{{ _lt_name }}</a>
62
+ {%- else %}
63
+ <span class="dropdown-item disabled" aria-disabled="true" title="{{ ui.lang_not_available | default: 'Not yet translated' }}">{{ _lt_name }} <small class="text-body-secondary">({{ ui.lang_not_available | default: 'Not yet translated' }})</small></span>
64
+ {%- endif %}
65
+ </li>
66
+ {%- endfor %}
67
+ </ul>
68
+ </div>
69
+ <script>
70
+ // Remember the visitor's language pick (no auto-redirect — just preference).
71
+ (function () {
72
+ var toggle = document.getElementById('zer0-lang-toggle');
73
+ if (!toggle) return;
74
+ toggle.addEventListener('click', function (event) {
75
+ var item = event.target.closest('a.dropdown-item[data-lang]');
76
+ if (!item) return;
77
+ try { localStorage.setItem('zer0-lang', item.getAttribute('data-lang')); } catch (e) { /* private mode */ }
78
+ });
79
+ })();
80
+ </script>
81
+ {%- endif -%}
@@ -103,6 +103,14 @@
103
103
  <!-- Step 1 — request type -->
104
104
  <fieldset class="pf-field pf-types">
105
105
  <legend class="pf-legend">What kind of feedback?</legend>
106
+ {%- comment -%}
107
+ _data/feedback_types.yml is optional for consumers (remote_theme
108
+ cannot ship _data). Without it the widget falls back to a minimal
109
+ generic taxonomy (see pf_fallback_types below) instead of rendering
110
+ an empty selector. The fallback entries carry no GitHub labels and
111
+ no agent directive — labels must exist in the target repo or GitHub
112
+ silently drops them from prefilled issue URLs.
113
+ {%- endcomment -%}
106
114
  {%- assign pf_group = "" -%}
107
115
  {%- for t in site.data.feedback_types -%}
108
116
  {%- if t.group != pf_group -%}
@@ -124,6 +132,54 @@
124
132
  </span>
125
133
  </button>
126
134
  {%- if forloop.last -%}</div>{%- endif -%}
135
+ {%- else -%}
136
+ {%- comment -%}
137
+ _data/feedback_types.yml is optional for consumers (remote_theme
138
+ cannot ship _data). Without it, render this minimal generic
139
+ taxonomy instead of an empty selector. Ids/markup must stay in
140
+ sync with the fallback JSON in the #pageFeedbackTypes script
141
+ below. Fallback entries carry no GitHub labels (labels must exist
142
+ in the target repo or GitHub silently drops them) and no agent
143
+ directive.
144
+ {%- endcomment -%}
145
+ <p class="pf-types-group">This page</p>
146
+ <div class="pf-types-grid" role="radiogroup" aria-label="This page">
147
+ <button type="button" class="pf-type" role="radio" aria-checked="false"
148
+ data-pf-type-id="fix-page" tabindex="0">
149
+ <i class="bi bi-bug pf-type-icon" aria-hidden="true"></i>
150
+ <span class="pf-type-text">
151
+ <span class="pf-type-label">Report a problem</span>
152
+ <span class="pf-type-desc">A typo, broken link, wrong information, or something rendering badly</span>
153
+ </span>
154
+ </button>
155
+ <button type="button" class="pf-type" role="radio" aria-checked="false"
156
+ data-pf-type-id="improve-page" tabindex="-1">
157
+ <i class="bi bi-stars pf-type-icon" aria-hidden="true"></i>
158
+ <span class="pf-type-text">
159
+ <span class="pf-type-label">Improve this page</span>
160
+ <span class="pf-type-desc">Polish the copy, structure, or presentation</span>
161
+ </span>
162
+ </button>
163
+ </div>
164
+ <p class="pf-types-group">The site</p>
165
+ <div class="pf-types-grid" role="radiogroup" aria-label="The site">
166
+ <button type="button" class="pf-type" role="radio" aria-checked="false"
167
+ data-pf-type-id="feature" tabindex="-1">
168
+ <i class="bi bi-lightbulb pf-type-icon" aria-hidden="true"></i>
169
+ <span class="pf-type-text">
170
+ <span class="pf-type-label">Suggest a feature</span>
171
+ <span class="pf-type-desc">An idea that would make the site more useful</span>
172
+ </span>
173
+ </button>
174
+ <button type="button" class="pf-type" role="radio" aria-checked="false"
175
+ data-pf-type-id="question" tabindex="-1">
176
+ <i class="bi bi-question-circle pf-type-icon" aria-hidden="true"></i>
177
+ <span class="pf-type-text">
178
+ <span class="pf-type-label">Ask a question</span>
179
+ <span class="pf-type-desc">Something unclear that is not exactly a bug</span>
180
+ </span>
181
+ </button>
182
+ </div>
127
183
  {%- endfor -%}
128
184
  </fieldset>
129
185
 
@@ -219,9 +275,16 @@
219
275
  }
220
276
  </script>
221
277
 
222
- <!-- Request-type taxonomy (from _data/feedback_types.yml) -->
278
+ <!-- Request-type taxonomy (from _data/feedback_types.yml; generic fallback
279
+ when the site ships no data file — keep in sync with the fallback markup
280
+ in the pf-types fieldset above) -->
223
281
  <script id="pageFeedbackTypes" type="application/json">
224
- {{ site.data.feedback_types | jsonify }}
282
+ {% if site.data.feedback_types %}{{ site.data.feedback_types | jsonify }}{% else %}[
283
+ {"id":"fix-page","label":"Report a problem","icon":"bi-bug","group":"This page","scope":"page","description":"A typo, broken link, wrong information, or something rendering badly","labels":[],"agent":false,"placeholder":"What is wrong, where on the page, and what did you expect instead?"},
284
+ {"id":"improve-page","label":"Improve this page","icon":"bi-stars","group":"This page","scope":"page","description":"Polish the copy, structure, or presentation","labels":[],"agent":false,"placeholder":"What would make this page clearer, tighter, or more useful?"},
285
+ {"id":"feature","label":"Suggest a feature","icon":"bi-lightbulb","group":"The site","scope":"site","description":"An idea that would make the site more useful","labels":[],"agent":false,"placeholder":"Describe the feature and the problem it would solve."},
286
+ {"id":"question","label":"Ask a question","icon":"bi-question-circle","group":"The site","scope":"site","description":"Something unclear that is not exactly a bug","labels":[],"agent":false,"placeholder":"What would you like to know?"}
287
+ ]{% endif %}
225
288
  </script>
226
289
 
227
290
  <!-- Live page context (build-time front matter; runtime detail added by JS) -->
@@ -18,8 +18,8 @@
18
18
  ===================================================================
19
19
  -->
20
20
 
21
- {%- assign ui_text = site.data.ui-text[site.locale] | default: site.data.ui-text.en -%}
22
- {%- assign search_placeholder = ui_text.search_placeholder_text | default: "Enter your search term..." -%}
21
+ {%- include core/i18n.html lang=include.lang -%}
22
+ {%- assign search_placeholder = ui.search_placeholder_text | default: "Enter your search term..." -%}
23
23
  {%- comment -%}
24
24
  Existence-gate the form action to /sitemap/ only when that page is present
25
25
  in the build (mirroring the footer Quick Links guard, and the section-sidebar
@@ -14,7 +14,7 @@
14
14
  is a no-op when the element is absent.
15
15
  ===================================================================
16
16
  {%- endcomment -%}
17
- {%- assign ui = site.data.ui-text[site.locale] | default: site.data.ui-text.en -%}
17
+ {%- include core/i18n.html lang=include.lang -%}
18
18
  <div class="modal fade" id="zer0-shortcuts-modal" tabindex="-1"
19
19
  aria-labelledby="zer0-shortcuts-modal-title" aria-hidden="true">
20
20
  <div class="modal-dialog modal-dialog-centered">
@@ -13,16 +13,24 @@
13
13
  Used by theme settings and theme preview pages.
14
14
  ===================================================================
15
15
  {%- endcomment -%}
16
- {% assign active_skin = site.theme_skin | default: site.data.theme_skins.default | default: "dark" %}
17
- {% assign skin_order = site.data.theme_skins.order %}
16
+ {%- comment -%}
17
+ _data/theme_skins.yml is optional for consumers (remote_theme cannot ship
18
+ _data), so fall back to the compiled skin registry. Keep this list in sync
19
+ with the [data-theme-skin] blocks in _sass/theme/_skins.scss.
20
+ {%- endcomment -%}
21
+ {% assign _skin_fallback = "air|aqua|dirt|neon|mint|plum|sunrise" | split: "|" %}
22
+ {% assign active_skin = site.theme_skin | default: site.data.theme_skins.default | default: "air" %}
23
+ {% assign skin_order = site.data.theme_skins.order | default: _skin_fallback %}
18
24
 
19
25
  <div class="theme-controls-bar d-flex flex-wrap align-items-center gap-3 mb-4 p-3 bg-body-tertiary rounded-3 border">
26
+ {% unless site.color_mode_lock %}
20
27
  <div class="d-flex align-items-center gap-2">
21
28
  <i class="bi bi-moon-stars" aria-hidden="true"></i>
22
29
  <span class="fw-semibold small">Mode:</span>
23
30
  {% include components/halfmoon.html %}
24
31
  </div>
25
32
  <div class="vr d-none d-sm-block" aria-hidden="true"></div>
33
+ {% endunless %}
26
34
  <div class="d-flex align-items-center gap-2 flex-grow-1">
27
35
  <i class="bi bi-palette2" aria-hidden="true"></i>
28
36
  <span class="fw-semibold small">Skin:</span>