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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +386 -4
  3. data/_data/consumers.yml +147 -0
  4. data/_data/features.yml +77 -5
  5. data/_data/i18n/fr.yml +98 -0
  6. data/_data/i18n/manifest.yml +1502 -0
  7. data/_data/navigation/docs.yml +2 -0
  8. data/_data/navigation/main.yml +6 -0
  9. data/_data/series.yml +19 -0
  10. data/_data/theme-manifest.yml +348 -266
  11. data/_includes/README.md +21 -2
  12. data/_includes/analytics/google-tag-manager-body.html +11 -2
  13. data/_includes/analytics/google-tag-manager-head.html +16 -6
  14. data/_includes/analytics/posthog.html +33 -4
  15. data/_includes/components/abc-letter.html +43 -0
  16. data/_includes/components/book-card.html +42 -0
  17. data/_includes/components/book-nav.html +80 -0
  18. data/_includes/components/book-plate.html +31 -0
  19. data/_includes/components/book-toc.html +47 -0
  20. data/_includes/components/bookshelf.html +68 -0
  21. data/_includes/components/card-grid.html +60 -0
  22. data/_includes/components/data-card.html +95 -0
  23. data/_includes/components/halfmoon.html +5 -1
  24. data/_includes/components/page-feedback.html +65 -2
  25. data/_includes/components/theme-controls-bar.html +10 -2
  26. data/_includes/components/theme-customizer.html +8 -2
  27. data/_includes/content/seo.html +9 -3
  28. data/_includes/core/color-mode-init.html +13 -4
  29. data/_includes/core/favicon.html +46 -0
  30. data/_includes/core/head.html +9 -0
  31. data/_includes/custom/body-end.html +18 -0
  32. data/_includes/custom/body-start.html +17 -0
  33. data/_includes/custom/footer.html +18 -0
  34. data/_includes/custom/head.html +18 -0
  35. data/_includes/navigation/local-graph.html +28 -2
  36. data/_includes/navigation/nav-tree.html +3 -3
  37. data/_includes/navigation/sidebar-config.html +21 -0
  38. data/_includes/navigation/sidebar-nav.html +4 -0
  39. data/_includes/navigation/sidebar-pagetree.html +150 -0
  40. data/_includes/navigation/unified-drawer.html +8 -2
  41. data/_includes/obsidian/full-graph.html +165 -136
  42. data/_layouts/404.html +260 -0
  43. data/_layouts/README.md +2 -0
  44. data/_layouts/book-abc.html +106 -0
  45. data/_layouts/book-story.html +91 -0
  46. data/_layouts/book.html +113 -0
  47. data/_layouts/collection.html +17 -5
  48. data/_layouts/home.html +5 -2
  49. data/_layouts/landing.html +9 -0
  50. data/_layouts/news.html +155 -31
  51. data/_layouts/root.html +16 -2
  52. data/_layouts/section.html +57 -15
  53. data/_sass/components/_book.scss +423 -0
  54. data/_sass/core/_navbar.scss +13 -0
  55. data/_sass/core/_obsidian.scss +286 -6
  56. data/_sass/theme/_backgrounds.scss +21 -8
  57. data/assets/css/main.scss +1 -0
  58. data/assets/js/auto-hide-nav.js +5 -1
  59. data/assets/js/halfmoon.js +26 -0
  60. data/assets/js/obsidian-graph.js +702 -264
  61. data/assets/js/obsidian-local-graph.js +161 -54
  62. data/assets/js/search-modal.js +4 -1
  63. data/scripts/bin/manifest +31 -4
  64. data/scripts/bin/validate +5 -1
  65. data/scripts/install/README.md +47 -6
  66. data/scripts/install/ai/client.sh +302 -93
  67. data/scripts/install/ai/prompts/spec.schema.json +1 -1
  68. data/scripts/install/ai/wizard.sh +10 -5
  69. data/scripts/install/apply.sh +7 -3
  70. data/scripts/install/cli.sh +54 -4
  71. data/scripts/install/config.sh +167 -0
  72. data/scripts/install/doctor.sh +38 -0
  73. data/scripts/install/plan.sh +10 -0
  74. data/scripts/install/spec.sh +15 -7
  75. data/scripts/install/template.sh +4 -0
  76. data/scripts/propagate.rb +277 -0
  77. data/scripts/translate.rb +90 -0
  78. metadata +27 -2
@@ -0,0 +1,150 @@
1
+ {%- comment -%}
2
+ Component: sidebar-pagetree
3
+ Path: _includes/navigation/sidebar-pagetree.html
4
+ Purpose: "pages" sidebar mode — a live URL-hierarchy tree of the site's
5
+ pages under a base path, grouped by their first path segment
6
+ into collapsible sections. Zero curated _data/navigation/*.yml
7
+ file needed: the tree is derived from page permalinks, so it
8
+ stays in sync with the content automatically.
9
+
10
+ Use it for flat page trees that aren't a Jekyll collection
11
+ (the "collection" mode covers collections). Example: a docs
12
+ area whose pages live at /docs/<section>/<page>/.
13
+
14
+ Params (include.* override the page.sidebar hash):
15
+ base (required) URL prefix to root the tree at, e.g.
16
+ "/docs/wargames/". Only pages whose url is under this
17
+ prefix are listed.
18
+ order_by (optional) front-matter key to sort each section's
19
+ pages by. Jekyll `sort` is numeric-aware, so a numeric
20
+ field like `nav_order` yields natural (0,1,2,…,10)
21
+ order. Default: url (path order).
22
+ expand (optional) true to expand every section (default:
23
+ only the section containing the current page).
24
+ Front matter (per page, all optional):
25
+ sidebar_exclude: true — hide this page from the tree.
26
+ sidebar_label: "…" — override the link text (else title).
27
+ Heading: title/icon resolved by sidebar-config.html (page → site).
28
+ Depends on: site.html_pages, Bootstrap 5 collapse, Bootstrap Icons, the
29
+ nav-tree styles (_sass/core/_nav-tree.scss). Emits the same
30
+ nav-tree markup as nav-tree.html / sidebar-folders.html so
31
+ active-state, collapse and styling behave identically.
32
+ {%- endcomment -%}
33
+
34
+ {%- assign _base = include.base | default: page.sidebar.base -%}
35
+ {%- if _base -%}
36
+ {%- assign _order_by = include.order_by | default: page.sidebar.order_by -%}
37
+ {%- assign _expand_all = page.sidebar.expand | default: false -%}
38
+
39
+ {%- comment -%} Pool = plain pages + every collection document, so the tree
40
+ works for flat pages (site.html_pages) AND collection content
41
+ (site.documents) alike. Then keep those under the base prefix. {%- endcomment -%}
42
+ {%- assign _pool = site.html_pages | concat: site.documents -%}
43
+ {%- assign _pages = _pool | where_exp: "p", "p.url contains _base" | where_exp: "p", "p.sidebar_exclude != true" -%}
44
+ {%- comment -%} The base index page (url == base) becomes a top-level link. {%- endcomment -%}
45
+ {%- assign _base_index = _pages | where: "url", _base | first -%}
46
+ {%- comment -%} Everything else groups by its first path segment after base. {%- endcomment -%}
47
+ {%- assign _rest = _pages | where_exp: "p", "p.url != _base" -%}
48
+ {%- assign _groups = _rest | group_by_exp: "p", "p.url | remove_first: _base | split: '/' | first" | sort: "name" -%}
49
+
50
+ <div class="sidebar-pagetree w-100">
51
+ <ul class="list-unstyled nav-tree-root mb-0">
52
+ {%- if _base_index -%}
53
+ <li class="nav-tree-item" data-depth="0">
54
+ <a href="{{ _base_index.url | relative_url }}"
55
+ class="nav-tree-link d-flex align-items-start rounded py-1 px-2 text-decoration-none{% if page.url == _base_index.url %} active{% endif %}"{% if page.url == _base_index.url %} aria-current="page"{% endif %}>
56
+ <i class="bi bi-collection me-2 flex-shrink-0" aria-hidden="true"></i>
57
+ <span>{{ _base_index.sidebar_label | default: _base_index.title | default: "Overview" }}</span>
58
+ </a>
59
+ </li>
60
+ {%- endif -%}
61
+
62
+ {%- for group in _groups -%}
63
+ {%- if group.name == "" or group.name == nil -%}{%- continue -%}{%- endif -%}
64
+ {%- assign _section = group.name -%}
65
+ {%- capture _section_url -%}{{ _base }}{{ _section }}/{%- endcapture -%}
66
+ {%- assign _group_id = _section | slugify | prepend: "sidebar-pt-" -%}
67
+ {%- assign _index = group.items | where: "url", _section_url | first -%}
68
+ {%- assign _children = group.items | where_exp: "p", "p.url != _section_url" -%}
69
+ {%- if _order_by -%}
70
+ {%- assign _children = _children | sort: _order_by -%}
71
+ {%- else -%}
72
+ {%- assign _children = _children | sort: "url" -%}
73
+ {%- endif -%}
74
+ {%- assign _child_count = _children | size -%}
75
+
76
+ {%- comment -%} Label the section from its URL segment (humanized), not the
77
+ index page's title — index titles are often generic ("Index") or
78
+ verbose. Mirrors sidebar-folders.html. A page can still override its
79
+ own child label via `sidebar_label`. {%- endcomment -%}
80
+ {%- assign _title = _section | replace: "-", " " | replace: "_", " " | capitalize -%}
81
+ {%- assign _active_here = group.items | where: "url", page.url | size -%}
82
+ {%- if _expand_all or _active_here > 0 -%}{%- assign _expanded = true -%}{%- else -%}{%- assign _expanded = false -%}{%- endif -%}
83
+
84
+ <li class="nav-tree-item sidebar-pagetree-group" data-depth="0">
85
+ {%- if _child_count == 0 and _index -%}
86
+ {%- comment -%} Section with only its index page — plain leaf, no toggle. {%- endcomment -%}
87
+ <a href="{{ _index.url | relative_url }}"
88
+ class="nav-tree-link d-flex align-items-start rounded py-1 px-2 text-decoration-none{% if page.url == _index.url %} active{% endif %}"{% if page.url == _index.url %} aria-current="page"{% endif %}>
89
+ <i class="bi bi-folder2 me-2 flex-shrink-0" aria-hidden="true"></i>
90
+ <span>{{ _title }}</span>
91
+ </a>
92
+ {%- else -%}
93
+ <div class="d-flex align-items-center">
94
+ {%- if _index -%}
95
+ <a href="{{ _index.url | relative_url }}"
96
+ class="nav-tree-link flex-grow-1 d-inline-flex align-items-center rounded py-1 px-2 text-decoration-none{% if page.url == _index.url %} active{% endif %}"{% if page.url == _index.url %} aria-current="page"{% endif %}>
97
+ <i class="bi bi-folder2 me-2" aria-hidden="true"></i>
98
+ <span>{{ _title }}</span>
99
+ </a>
100
+ <button class="btn btn-sm btn-link nav-tree-toggle p-1{% unless _expanded %} collapsed{% endunless %}"
101
+ type="button"
102
+ data-bs-toggle="collapse"
103
+ data-bs-target="#{{ _group_id }}"
104
+ aria-expanded="{{ _expanded }}"
105
+ aria-controls="{{ _group_id }}"
106
+ aria-label="Toggle {{ _title }} section">
107
+ <i class="bi bi-chevron-down nav-tree-chevron" aria-hidden="true"></i>
108
+ </button>
109
+ {%- else -%}
110
+ <button class="btn btn-link nav-tree-toggle w-100 d-flex align-items-center text-start py-1 px-2{% unless _expanded %} collapsed{% endunless %}"
111
+ type="button"
112
+ data-bs-toggle="collapse"
113
+ data-bs-target="#{{ _group_id }}"
114
+ aria-expanded="{{ _expanded }}"
115
+ aria-controls="{{ _group_id }}">
116
+ <i class="bi bi-folder2 me-2" aria-hidden="true"></i>
117
+ <span class="flex-grow-1">{{ _title }}</span>
118
+ <i class="bi bi-chevron-down nav-tree-chevron" aria-hidden="true"></i>
119
+ </button>
120
+ {%- endif -%}
121
+ </div>
122
+
123
+ <div class="collapse{% if _expanded %} show{% endif %}" id="{{ _group_id }}">
124
+ <ul class="list-unstyled nav-tree-children mb-0">
125
+ {%- for pg in _children -%}
126
+ {%- assign _pg_label = pg.sidebar_label | default: pg.title -%}
127
+ {%- unless _pg_label -%}
128
+ {%- assign _pg_label = pg.url | split: "/" | last | replace: "-", " " | replace: "_", " " | capitalize -%}
129
+ {%- endunless -%}
130
+ <li class="nav-tree-item" data-depth="1">
131
+ <a href="{{ pg.url | relative_url }}"
132
+ class="nav-tree-link d-flex align-items-start rounded py-1 px-2 text-decoration-none{% if page.url == pg.url %} active{% endif %}"{% if page.url == pg.url %} aria-current="page"{% endif %}>
133
+ <i class="bi bi-file-earmark-text me-2 flex-shrink-0" aria-hidden="true"></i>
134
+ <span>{{ _pg_label }}</span>
135
+ </a>
136
+ </li>
137
+ {%- endfor -%}
138
+ </ul>
139
+ </div>
140
+ {%- endif -%}
141
+ </li>
142
+ {%- endfor -%}
143
+ </ul>
144
+ </div>
145
+ {%- else -%}
146
+ <p class="text-body-secondary small mb-0">
147
+ <i class="bi bi-info-circle me-1" aria-hidden="true"></i>
148
+ Set <code>sidebar.base</code> (a URL prefix) to enable the page tree.
149
+ </p>
150
+ {%- endif -%}
@@ -87,10 +87,16 @@
87
87
  data-bs-target="#info-section">
88
88
  <i class="bi bi-gear me-1" aria-hidden="true"></i>Theme &amp; appearance
89
89
  </button>
90
+ {%- comment -%}
91
+ Opened via the shared [data-search-toggle] binding in search-modal.js
92
+ (which also closes this drawer first). A raw data-bs-target here once
93
+ pointed at "#search-modal", but the modal's real id is
94
+ #siteSearchModal — the button was dead.
95
+ {%- endcomment -%}
90
96
  <button type="button"
91
97
  class="btn btn-outline-secondary w-100"
92
- data-bs-toggle="modal"
93
- data-bs-target="#search-modal">
98
+ data-search-toggle
99
+ aria-haspopup="dialog">
94
100
  <i class="bi bi-search me-1" aria-hidden="true"></i>Search
95
101
  </button>
96
102
  </div>
@@ -1,158 +1,187 @@
1
1
  {% comment %} Features: ZER0-044, ZER0-045 {% endcomment %}
2
- <style>
3
- /* Scoped to the graph page so we don't leak into other docs. */
4
- #obsidian-graph {
5
- width: 100%;
6
- height: 82vh;
7
- min-height: 620px;
8
- border: 1px solid rgba(var(--bs-border-color-rgb, 222, 226, 230), 0.55);
9
- border-radius: var(--bs-border-radius-lg, .5rem);
10
- background: rgba(var(--bs-body-bg-rgb, 255, 255, 255), 0.45);
11
- backdrop-filter: blur(14px) saturate(1.15);
12
- -webkit-backdrop-filter: blur(14px) saturate(1.15);
13
- box-shadow:
14
- 0 1px 2px rgba(0, 0, 0, .03),
15
- 0 8px 24px rgba(0, 0, 0, .06),
16
- inset 0 1px 0 rgba(255, 255, 255, .04);
17
- position: relative;
18
- overflow: hidden;
19
- }
20
- [data-bs-theme="dark"] #obsidian-graph {
21
- background: rgba(var(--bs-body-bg-rgb, 33, 37, 41), 0.38);
22
- box-shadow:
23
- 0 1px 2px rgba(0, 0, 0, .12),
24
- 0 8px 28px rgba(0, 0, 0, .22),
25
- inset 0 1px 0 rgba(255, 255, 255, .03);
26
- }
27
- #obsidian-graph-stats {
28
- display: flex;
29
- flex-wrap: wrap;
30
- gap: .375rem;
31
- }
32
- #obsidian-graph-stats .badge {
33
- font-size: .8125rem;
34
- font-weight: 500;
35
- padding: .35rem .6rem;
36
- }
37
- .obsidian-graph-toolbar {
38
- display: flex;
39
- flex-wrap: wrap;
40
- gap: .5rem .75rem;
41
- align-items: center;
42
- margin: .75rem 0;
43
- padding: .625rem .75rem;
44
- background: var(--bs-tertiary-bg, #f8f9fa);
45
- border: 1px solid var(--bs-border-color, #dee2e6);
46
- border-radius: var(--bs-border-radius, .375rem);
47
- }
48
- .obsidian-graph-toolbar .form-control {
49
- max-width: 280px;
50
- flex: 1 1 200px;
51
- }
52
- .obsidian-graph-toolbar .form-check {
53
- margin-bottom: 0;
54
- }
55
- #obsidian-graph-status {
56
- flex-basis: 100%;
57
- font-size: .8125rem;
58
- color: var(--bs-secondary-color, #6c757d);
59
- min-height: 1.25rem;
60
- }
61
- .obsidian-graph-legend {
62
- display: flex;
63
- flex-wrap: wrap;
64
- gap: .5rem;
65
- margin-top: .75rem;
66
- font-size: .8125rem;
67
- }
68
- .obsidian-graph-legend > span {
69
- display: inline-flex;
70
- align-items: center;
71
- gap: .375rem;
72
- padding: .25rem .55rem;
73
- background: var(--bs-body-bg, #fff);
74
- border: 1px solid var(--bs-border-color, #dee2e6);
75
- border-radius: 999px;
76
- color: var(--bs-body-color, #212529);
77
- }
78
- .obsidian-graph-legend .swatch {
79
- display: inline-block;
80
- width: .7rem;
81
- height: .7rem;
82
- border-radius: 50%;
83
- box-shadow: 0 0 0 1px rgba(0, 0, 0, .12);
84
- opacity: 0.82;
85
- }
86
- .obsidian-graph-legend .swatch-broken {
87
- border: 1.5px dashed #dc3545;
88
- background: transparent !important;
89
- box-shadow: none;
90
- }
91
- .obsidian-graph-tips {
92
- margin-top: .75rem;
93
- padding: .625rem .75rem;
94
- font-size: .8125rem;
95
- color: var(--bs-secondary-color, #6c757d);
96
- background: var(--bs-tertiary-bg, #f8f9fa);
97
- border-left: 3px solid var(--bs-primary, #0d6efd);
98
- border-radius: .25rem;
99
- }
100
- .obsidian-graph-tips strong {
101
- color: var(--bs-body-color, #212529);
102
- }
103
- </style>
2
+ {%- comment -%}
3
+ ===================================================================
4
+ FULL GRAPH PAGE — Obsidian-style site knowledge graph
5
+ ===================================================================
6
+
7
+ File: full-graph.html
8
+ Path: _includes/obsidian/full-graph.html
9
+ Included from: pages/_docs/obsidian/graph.md
10
+ Renderer: assets/js/obsidian-graph.js (vendored cytoscape.js)
11
+ Styles: _sass/core/_obsidian.scss § "Full graph page"
12
+
13
+ Mirrors the Obsidian desktop graph controls:
14
+ - Filters: search (tag:/path: operators), per-collection toggles with
15
+ counts + color swatches (doubles as the legend), orphans, unresolved
16
+ - Display: arrows, node size, link thickness, label fade threshold
17
+ - Forces: repulsion, link distance, animated re-layout
18
+ Settings persist to localStorage (zer0.obsidianGraph.v1).
19
+
20
+ Element IDs are load-bearing: obsidian-graph, obsidian-graph-stats,
21
+ obsidian-graph-search, obsidian-graph-fit, obsidian-graph-orphans are
22
+ asserted by test/visual/features/obsidian-graph.spec.js.
23
+ ===================================================================
24
+ {%- endcomment -%}
104
25
 
105
26
  # Obsidian Graph View
106
27
 
107
28
  A live, force-directed map of every page on this site and every
108
29
  `[[wiki-link]]` between them. This is the rendered-site equivalent of
109
- Obsidian's local graph view — built from the same
30
+ Obsidian's graph view — built from the same
110
31
  [`assets/data/wiki-index.json`]({{ "/assets/data/wiki-index.json" | relative_url }})
111
32
  that powers the [client-side resolver]({{ "/docs/obsidian/syntax-reference/" | relative_url }})
112
33
  and [backlinks panel]({{ "/docs/obsidian/syntax-reference/#backlinks-panel" | relative_url }}).
113
34
 
114
35
  <div id="obsidian-graph-stats" class="mb-2" aria-live="polite"></div>
115
36
 
116
- <div class="obsidian-graph-toolbar">
117
- <input type="search"
118
- class="form-control form-control-sm"
119
- id="obsidian-graph-search"
120
- placeholder="Filter nodes by title…"
121
- aria-label="Filter graph nodes by title" />
122
- <button type="button"
123
- class="btn btn-outline-secondary btn-sm"
124
- id="obsidian-graph-fit">
125
- <i class="bi bi-arrows-fullscreen" aria-hidden="true"></i>
126
- Reset view
127
- </button>
128
- <div class="form-check form-switch">
129
- <input class="form-check-input"
130
- type="checkbox"
131
- role="switch"
132
- id="obsidian-graph-orphans" />
133
- <label class="form-check-label" for="obsidian-graph-orphans">
134
- Show orphans
135
- </label>
37
+ <div class="obsidian-graph-shell" id="obsidian-graph-shell">
38
+ <div id="obsidian-graph" role="group" aria-label="Site knowledge graph"></div>
39
+ <div class="obsidian-graph-controls d-print-none" id="obsidian-graph-controls">
40
+ <button type="button"
41
+ class="btn btn-sm obsidian-graph-controls-toggle"
42
+ data-bs-toggle="collapse"
43
+ data-bs-target="#obsidianGraphControlsBody"
44
+ aria-expanded="false"
45
+ aria-controls="obsidianGraphControlsBody">
46
+ <i class="bi bi-sliders me-1" aria-hidden="true"></i>Graph settings
47
+ <i class="bi bi-chevron-down ms-auto obsidian-graph-controls-chevron" aria-hidden="true"></i>
48
+ </button>
49
+ <div class="collapse obsidian-graph-controls-body" id="obsidianGraphControlsBody">
50
+ <div class="accordion accordion-flush" id="obsidianGraphAccordion">
51
+ <div class="accordion-item">
52
+ <h2 class="accordion-header">
53
+ <button class="accordion-button py-2" type="button"
54
+ data-bs-toggle="collapse" data-bs-target="#obsidianGraphFilters"
55
+ aria-expanded="true" aria-controls="obsidianGraphFilters">
56
+ <i class="bi bi-funnel me-2" aria-hidden="true"></i>Filters
57
+ </button>
58
+ </h2>
59
+ <div id="obsidianGraphFilters" class="accordion-collapse collapse show"
60
+ data-bs-parent="#obsidianGraphAccordion">
61
+ <div class="accordion-body py-2">
62
+ <input type="search"
63
+ class="form-control form-control-sm mb-2"
64
+ id="obsidian-graph-search"
65
+ placeholder="Search… (tag:foo, path:docs)"
66
+ aria-label="Filter graph nodes by title, tag: or path: operator" />
67
+ <div id="obsidian-graph-collections"
68
+ class="obsidian-graph-collections mb-2"
69
+ role="group"
70
+ aria-label="Toggle collections"></div>
71
+ <div class="form-check form-switch">
72
+ <input class="form-check-input" type="checkbox" role="switch"
73
+ id="obsidian-graph-orphans" />
74
+ <label class="form-check-label" for="obsidian-graph-orphans">Show orphans</label>
75
+ </div>
76
+ <div class="form-check form-switch">
77
+ <input class="form-check-input" type="checkbox" role="switch"
78
+ id="obsidian-graph-broken" checked />
79
+ <label class="form-check-label" for="obsidian-graph-broken">Show unresolved links</label>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ <div class="accordion-item">
85
+ <h2 class="accordion-header">
86
+ <button class="accordion-button collapsed py-2" type="button"
87
+ data-bs-toggle="collapse" data-bs-target="#obsidianGraphDisplay"
88
+ aria-expanded="false" aria-controls="obsidianGraphDisplay">
89
+ <i class="bi bi-eye me-2" aria-hidden="true"></i>Display
90
+ </button>
91
+ </h2>
92
+ <div id="obsidianGraphDisplay" class="accordion-collapse collapse"
93
+ data-bs-parent="#obsidianGraphAccordion">
94
+ <div class="accordion-body py-2">
95
+ <div class="form-check form-switch mb-2">
96
+ <input class="form-check-input" type="checkbox" role="switch"
97
+ id="obsidian-graph-arrows" checked />
98
+ <label class="form-check-label" for="obsidian-graph-arrows">Link arrows</label>
99
+ </div>
100
+ <label class="form-label small mb-1" for="obsidian-graph-node-size">Node size</label>
101
+ <input type="range" class="form-range" id="obsidian-graph-node-size"
102
+ min="0.5" max="2" step="0.05" value="1" />
103
+ <label class="form-label small mb-1" for="obsidian-graph-link-width">Link thickness</label>
104
+ <input type="range" class="form-range" id="obsidian-graph-link-width"
105
+ min="0.5" max="3" step="0.1" value="1" />
106
+ <label class="form-label small mb-1" for="obsidian-graph-label-zoom">Label fade threshold</label>
107
+ <input type="range" class="form-range" id="obsidian-graph-label-zoom"
108
+ min="0.4" max="2.5" step="0.05" value="1.25"
109
+ aria-describedby="obsidian-graph-label-zoom-help" />
110
+ <p class="small text-secondary mb-0" id="obsidian-graph-label-zoom-help">
111
+ Lower shows labels sooner while zooming in.
112
+ </p>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ <div class="accordion-item">
117
+ <h2 class="accordion-header">
118
+ <button class="accordion-button collapsed py-2" type="button"
119
+ data-bs-toggle="collapse" data-bs-target="#obsidianGraphForces"
120
+ aria-expanded="false" aria-controls="obsidianGraphForces">
121
+ <i class="bi bi-magnet me-2" aria-hidden="true"></i>Forces
122
+ </button>
123
+ </h2>
124
+ <div id="obsidianGraphForces" class="accordion-collapse collapse"
125
+ data-bs-parent="#obsidianGraphAccordion">
126
+ <div class="accordion-body py-2">
127
+ <label class="form-label small mb-1" for="obsidian-graph-repulsion">Repel force</label>
128
+ <input type="range" class="form-range" id="obsidian-graph-repulsion"
129
+ min="0.3" max="3" step="0.1" value="1" />
130
+ <label class="form-label small mb-1" for="obsidian-graph-link-distance">Link distance</label>
131
+ <input type="range" class="form-range" id="obsidian-graph-link-distance"
132
+ min="0.4" max="2.5" step="0.1" value="1" />
133
+ <div class="form-check form-switch mb-0">
134
+ <input class="form-check-input" type="checkbox" role="switch"
135
+ id="obsidian-graph-animate" checked />
136
+ <label class="form-check-label" for="obsidian-graph-animate">Animate layout</label>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ <div class="d-flex gap-2 p-2 border-top">
143
+ <button type="button" class="btn btn-outline-primary btn-sm flex-grow-1"
144
+ id="obsidian-graph-relayout">
145
+ <i class="bi bi-arrow-clockwise me-1" aria-hidden="true"></i>Re-run layout
146
+ </button>
147
+ <button type="button" class="btn btn-outline-secondary btn-sm"
148
+ id="obsidian-graph-reset"
149
+ title="Reset all graph settings to defaults">
150
+ Reset
151
+ </button>
152
+ </div>
153
+ </div>
136
154
  </div>
137
- <span id="obsidian-graph-status" role="status"></span>
155
+ <div class="obsidian-graph-zoombar btn-group-vertical d-print-none" role="group" aria-label="Graph zoom controls">
156
+ <button type="button" class="btn btn-sm" id="obsidian-graph-zoom-in"
157
+ aria-label="Zoom in" title="Zoom in">
158
+ <i class="bi bi-plus-lg" aria-hidden="true"></i>
159
+ </button>
160
+ <button type="button" class="btn btn-sm" id="obsidian-graph-zoom-out"
161
+ aria-label="Zoom out" title="Zoom out">
162
+ <i class="bi bi-dash-lg" aria-hidden="true"></i>
163
+ </button>
164
+ <button type="button" class="btn btn-sm" id="obsidian-graph-fit"
165
+ aria-label="Reset view" title="Reset view (clears search)">
166
+ <i class="bi bi-aspect-ratio" aria-hidden="true"></i>
167
+ </button>
168
+ <button type="button" class="btn btn-sm" id="obsidian-graph-fullscreen"
169
+ aria-label="Toggle fullscreen" title="Toggle fullscreen" aria-pressed="false">
170
+ <i class="bi bi-arrows-fullscreen" aria-hidden="true"></i>
171
+ </button>
172
+ </div>
173
+ <div id="obsidian-graph-preview" class="card shadow-sm obsidian-graph-preview" hidden aria-hidden="true"></div>
138
174
  </div>
139
175
 
140
- <div id="obsidian-graph" role="group" aria-label="Site knowledge graph"></div>
141
-
142
- <div class="obsidian-graph-legend" aria-label="Graph legend">
143
- <span><span class="swatch" style="background:#0d6efd"></span>Posts</span>
144
- <span><span class="swatch" style="background:#198754"></span>Docs</span>
145
- <span><span class="swatch" style="background:#6f42c1"></span>Notes</span>
146
- <span><span class="swatch" style="background:#d63384"></span>Notebooks</span>
147
- <span><span class="swatch" style="background:#fd7e14"></span>Quickstart</span>
148
- <span><span class="swatch" style="background:#6c757d"></span>Pages</span>
149
- <span><span class="swatch swatch-broken"></span>Broken links</span>
150
- </div>
176
+ <p id="obsidian-graph-status" class="obsidian-graph-status" role="status"></p>
151
177
 
152
178
  <div class="obsidian-graph-tips">
153
179
  <strong>Tips:</strong> click a node to open the page · ⌘/Ctrl-click to
154
180
  open in a new tab · drag to reposition · scroll to zoom · hover to
155
- highlight a node's neighborhood · type in the search box to filter.
181
+ preview a page and highlight its neighborhood · search supports
182
+ <code>tag:foo</code> and <code>path:docs</code> operators · the
183
+ <em>Graph settings</em> panel filters collections and tunes display
184
+ &amp; forces (persisted in your browser).
156
185
  </div>
157
186
 
158
187
  ## How it's built