@zeropress/build-pages 0.6.2 → 0.6.4

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.
@@ -1,8 +1,221 @@
1
+ /* ---------- Mobile nav toggle ---------- */
2
+
3
+ const navToggle = document.querySelector('[data-nav-toggle]');
4
+ const navPanel = document.querySelector('[data-nav-panel]');
5
+
6
+ if (navToggle && navPanel) {
7
+ const closeNav = () => {
8
+ navPanel.classList.remove('is-open');
9
+ navToggle.setAttribute('aria-expanded', 'false');
10
+ };
11
+
12
+ navToggle.addEventListener('click', () => {
13
+ const open = !navPanel.classList.contains('is-open');
14
+ navPanel.classList.toggle('is-open', open);
15
+ navToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
16
+ });
17
+
18
+ navPanel.addEventListener('click', (event) => {
19
+ if (event.target instanceof HTMLAnchorElement) {
20
+ closeNav();
21
+ }
22
+ });
23
+
24
+ window.addEventListener('resize', () => {
25
+ if (window.innerWidth > 820) {
26
+ closeNav();
27
+ }
28
+ });
29
+ }
30
+
31
+ /* ---------- Theme toggle (light/dark) ---------- */
32
+
33
+ const themeToggle = document.querySelector('[data-theme-toggle]');
34
+
35
+ if (themeToggle) {
36
+ const root = document.documentElement;
37
+ themeToggle.disabled = false;
38
+
39
+ const getResolvedTheme = () => {
40
+ const stored = root.dataset.theme;
41
+ if (stored === 'light' || stored === 'dark') return stored;
42
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
43
+ };
44
+
45
+ themeToggle.addEventListener('click', () => {
46
+ const next = getResolvedTheme() === 'dark' ? 'light' : 'dark';
47
+ root.dataset.theme = next;
48
+ try {
49
+ localStorage.setItem('zp-theme', next);
50
+ } catch (e) {}
51
+ });
52
+ }
53
+
54
+ /* ---------- Highlight active primary nav link ---------- */
55
+
56
+ const primaryLinks = Array.from(document.querySelectorAll('.site-nav a'));
57
+
58
+ if (primaryLinks.length) {
59
+ const path = window.location.pathname.replace(/\/+$/, '') || '/';
60
+ let bestMatch = null;
61
+ let bestScore = -1;
62
+
63
+ for (const link of primaryLinks) {
64
+ let href;
65
+ try {
66
+ href = new URL(link.href, window.location.origin).pathname.replace(/\/+$/, '') || '/';
67
+ } catch {
68
+ continue;
69
+ }
70
+
71
+ if (href === '/' && path !== '/') continue;
72
+ if (path === href || path.startsWith(href + '/')) {
73
+ if (href.length > bestScore) {
74
+ bestScore = href.length;
75
+ bestMatch = link;
76
+ }
77
+ }
78
+ }
79
+
80
+ if (bestMatch) {
81
+ bestMatch.setAttribute('aria-current', 'page');
82
+ }
83
+ }
84
+
85
+ /* ---------- Table of contents scroll spy ---------- */
86
+
87
+ document.querySelectorAll('[data-enhance-toc]').forEach((toc) => {
88
+ const layout = toc.closest('.doc-layout');
89
+ const prose = layout?.querySelector('.prose');
90
+ if (!layout || !prose) return;
91
+
92
+ const headings = Array.from(prose.querySelectorAll('h2[id], h3[id], h4[id]'))
93
+ .filter((heading) => heading.id && heading.textContent.trim());
94
+ if (!headings.length) return;
95
+
96
+ const title = document.createElement('p');
97
+ title.className = 'doc-toc__title';
98
+ title.textContent = 'On this page';
99
+
100
+ const nav = document.createElement('nav');
101
+ const list = document.createElement('ol');
102
+
103
+ for (const heading of headings) {
104
+ const level = Number(heading.tagName.slice(1)) || 2;
105
+ const item = document.createElement('li');
106
+ const link = document.createElement('a');
107
+
108
+ item.className = `doc-toc__item doc-toc__item--level-${level}`;
109
+ link.href = `#${encodeURIComponent(heading.id)}`;
110
+ link.dataset.docTocLink = '';
111
+ link.textContent = heading.textContent.trim();
112
+
113
+ item.append(link);
114
+ list.append(item);
115
+ }
116
+
117
+ nav.append(list);
118
+ toc.replaceChildren(title, nav);
119
+ toc.hidden = false;
120
+ layout.classList.add('doc-layout--with-toc');
121
+ });
122
+
123
+ const tocLinks = Array.from(document.querySelectorAll('[data-doc-toc-link]'));
124
+
125
+ if (tocLinks.length) {
126
+ const decodeHashId = (value) => {
127
+ try {
128
+ return decodeURIComponent(value);
129
+ } catch {
130
+ return value;
131
+ }
132
+ };
133
+
134
+ const getHashId = (value) => {
135
+ const hash = value || '';
136
+ return hash.startsWith('#') ? decodeHashId(hash.slice(1)) : '';
137
+ };
138
+
139
+ const tocItems = tocLinks
140
+ .map((link) => {
141
+ const heading = document.getElementById(getHashId(link.getAttribute('href')));
142
+ return heading ? { heading, link } : null;
143
+ })
144
+ .filter(Boolean);
145
+
146
+ if (tocItems.length) {
147
+ let activeId = '';
148
+ let frameRequested = false;
149
+
150
+ const anchorLine = () => (window.matchMedia('(max-width: 860px)').matches ? 160 : 120);
151
+
152
+ const setActive = (id) => {
153
+ if (!id || id === activeId) return;
154
+ activeId = id;
155
+
156
+ for (const item of tocItems) {
157
+ const match = item.heading.id === id;
158
+ item.link.classList.toggle('is-active', match);
159
+
160
+ if (match) {
161
+ item.link.setAttribute('aria-current', 'true');
162
+ const toc = item.link.closest('.doc-toc');
163
+ if (toc && toc.scrollHeight > toc.clientHeight) {
164
+ item.link.scrollIntoView({ block: 'nearest' });
165
+ }
166
+ } else {
167
+ item.link.removeAttribute('aria-current');
168
+ }
169
+ }
170
+ };
171
+
172
+ const updateActive = () => {
173
+ frameRequested = false;
174
+ let current = tocItems[0];
175
+ const line = anchorLine();
176
+ for (const item of tocItems) {
177
+ if (item.heading.getBoundingClientRect().top <= line) {
178
+ current = item;
179
+ } else {
180
+ break;
181
+ }
182
+ }
183
+ setActive(current.heading.id);
184
+ };
185
+
186
+ const requestUpdate = () => {
187
+ if (frameRequested) return;
188
+ frameRequested = true;
189
+ window.requestAnimationFrame(updateActive);
190
+ };
191
+
192
+ const setHashActive = () => {
193
+ const hashId = getHashId(window.location.hash);
194
+ const item = tocItems.find((entry) => entry.heading.id === hashId);
195
+ if (item) setActive(item.heading.id);
196
+ };
197
+
198
+ setHashActive();
199
+ requestUpdate();
200
+ window.setTimeout(requestUpdate, 80);
201
+
202
+ window.addEventListener('scroll', requestUpdate, { passive: true });
203
+ window.addEventListener('resize', requestUpdate);
204
+ window.addEventListener('hashchange', () => {
205
+ setHashActive();
206
+ requestUpdate();
207
+ });
208
+ }
209
+ }
210
+
211
+ /* ---------- Search ---------- */
212
+
1
213
  const searchRoot = document.querySelector('[data-site-search]');
2
214
 
3
215
  if (searchRoot) {
4
216
  const form = searchRoot.querySelector('[data-site-search-form]');
5
217
  const input = searchRoot.querySelector('[data-site-search-input]');
218
+ const submit = searchRoot.querySelector('[data-site-search-submit]');
6
219
  const panel = searchRoot.querySelector('[data-site-search-panel]');
7
220
  const status = searchRoot.querySelector('[data-site-search-status]');
8
221
  const resultsList = searchRoot.querySelector('[data-site-search-results]');
@@ -31,6 +244,10 @@ if (searchRoot) {
31
244
  status.textContent = message;
32
245
  };
33
246
 
247
+ input.disabled = false;
248
+ if (submit) submit.disabled = false;
249
+ setStatus('Type at least two characters.');
250
+
34
251
  const renderResults = async (items) => {
35
252
  clearResults();
36
253
  if (!items.length) {
@@ -101,15 +318,30 @@ if (searchRoot) {
101
318
  }
102
319
  });
103
320
 
104
- form.addEventListener('submit', (event) => {
105
- event.preventDefault();
106
- runSearch();
107
- });
321
+ if (form) {
322
+ form.addEventListener('submit', (event) => {
323
+ event.preventDefault();
324
+ runSearch();
325
+ });
326
+ }
108
327
 
109
328
  document.addEventListener('keydown', (event) => {
110
329
  if (event.key === 'Escape') {
111
330
  hidePanel();
112
331
  input.blur();
332
+ return;
333
+ }
334
+
335
+ // Slash to focus search (when not in another input)
336
+ if (event.key === '/' && !event.ctrlKey && !event.metaKey && !event.altKey) {
337
+ const target = event.target;
338
+ const isFormField = target instanceof HTMLElement &&
339
+ (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable);
340
+ if (!isFormField) {
341
+ event.preventDefault();
342
+ input.focus();
343
+ input.select();
344
+ }
113
345
  }
114
346
  });
115
347
 
@@ -1,35 +1,73 @@
1
+ <!doctype html>
1
2
  <html lang="{{site.locale}}">
2
3
  <head>
3
4
  <meta charset="utf-8">
4
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
5
6
  <title>{{meta.title}}</title>
6
- <meta name="theme-color" content="#f8fafc" media="(prefers-color-scheme: light)">
7
- <meta name="theme-color" content="#111316" media="(prefers-color-scheme: dark)">
7
+ <meta name="theme-color" content="#fbfbfd" media="(prefers-color-scheme: light)">
8
+ <meta name="theme-color" content="#0b0d12" media="(prefers-color-scheme: dark)">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap">
8
12
  <link rel="stylesheet" href="/assets/style.css">
13
+ {{partial:theme-bootstrap}}
9
14
  {{meta.head_tags}}
10
15
  </head>
11
16
  <body>
12
17
  <a class="skip-link" href="#content">Skip to content</a>
13
18
  <header class="site-header">
14
19
  <div class="shell site-header__inner">
15
- <a class="brand" href="/">{{site.title}}</a>
16
- <div class="site-header__actions">
20
+ <a class="brand" href="/">
21
+ {{#if site.logo.src}}
22
+ <img
23
+ class="brand__mark"
24
+ src="{{site.logo.src}}"
25
+ alt="{{#if site.logo.alt}}{{site.logo.alt}}{{#else}}{{site.title}}{{/if}}"
26
+ >
27
+ {{/if}}
28
+ <span class="brand__name">{{site.title}}</span>
29
+ </a>
30
+ <button class="nav-toggle" type="button" aria-expanded="false" aria-controls="site-nav-panel" data-nav-toggle>
31
+ <span class="visually-hidden">Toggle navigation</span>
32
+ <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
33
+ <line x1="4" y1="7" x2="20" y2="7"></line>
34
+ <line x1="4" y1="12" x2="20" y2="12"></line>
35
+ <line x1="4" y1="17" x2="20" y2="17"></line>
36
+ </svg>
37
+ </button>
38
+ <div class="site-header__actions" id="site-nav-panel" data-nav-panel>
17
39
  <nav class="site-nav" aria-label="Primary navigation">
18
40
  {{menu:primary}}
19
41
  </nav>
20
42
  {{#if site.search}}
21
43
  <div class="site-search" data-site-search>
22
44
  <form class="site-search__form" role="search" data-site-search-form>
45
+ <span class="site-search__icon" aria-hidden="true">
46
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
47
+ <circle cx="11" cy="11" r="7"></circle>
48
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
49
+ </svg>
50
+ </span>
23
51
  <label class="visually-hidden" for="site-search-input">Search docs</label>
24
- <input id="site-search-input" class="site-search__input" type="search" placeholder="Search docs" autocomplete="off" spellcheck="false" data-site-search-input>
25
- <button class="site-search__button" type="submit">Search</button>
52
+ <input id="site-search-input" class="site-search__input" type="search" placeholder="Search docs" autocomplete="off" spellcheck="false" data-site-search-input disabled>
53
+ <kbd class="site-search__kbd" aria-hidden="true">/</kbd>
54
+ <button class="visually-hidden" type="submit" data-site-search-submit disabled>Search</button>
26
55
  </form>
27
56
  <div class="site-search__panel" data-site-search-panel hidden>
28
- <p class="site-search__status" data-site-search-status>Type to search documentation.</p>
57
+ <p class="site-search__status" data-site-search-status>Search requires JavaScript.</p>
29
58
  <ol class="site-search__results" data-site-search-results></ol>
30
59
  </div>
31
60
  </div>
32
61
  {{/if}}
62
+ <button class="theme-toggle" type="button" aria-label="Toggle color theme" data-theme-toggle title="Toggle theme" disabled>
63
+ <svg class="theme-toggle__icon theme-toggle__icon--sun" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
64
+ <circle cx="12" cy="12" r="4"></circle>
65
+ <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"></path>
66
+ </svg>
67
+ <svg class="theme-toggle__icon theme-toggle__icon--moon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
68
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
69
+ </svg>
70
+ </button>
33
71
  </div>
34
72
  </div>
35
73
  </header>
@@ -38,14 +76,19 @@
38
76
  </main>
39
77
  <footer class="site-footer">
40
78
  <div class="shell site-footer__inner">
41
- {{#if site.footer.copyright_text}}
42
- <p>{{site.footer.copyright_text}}</p>
43
- {{#else_if site.title}}
44
- <p>{{site.title}}</p>
45
- {{/if}}
46
- {{#if site.footer.attribution}}
47
- <p>Published with <a href="https://zeropress.app" target="_blank" rel="noreferrer noopener">ZeroPress</a>.</p>
48
- {{/if}}
79
+ <div class="site-footer__meta">
80
+ {{#if site.footer.copyright_text}}
81
+ <p>{{site.footer.copyright_text}}</p>
82
+ {{#else_if site.title}}
83
+ <p>{{site.title}}</p>
84
+ {{/if}}
85
+ {{#if site.footer.attribution}}
86
+ <p>Published with <a href="https://zeropress.app" target="_blank" rel="noreferrer noopener">ZeroPress</a></p>
87
+ {{/if}}
88
+ </div>
89
+ <nav class="site-footer__nav" aria-label="Footer navigation">
90
+ {{menu:footer}}
91
+ </nav>
49
92
  </div>
50
93
  </footer>
51
94
  {{partial:theme-scripts}}
@@ -1,22 +1,17 @@
1
1
  <article class="shell doc-layout{{#if page.toc}} doc-layout--with-toc{{/if}}">
2
2
  <div class="doc-content">
3
- {{#if route.is_front_page}}
4
3
  <div class="prose" {{#if site.search}}{{#if_neq page.discoverability "delist"}}data-pagefind-body{{/if_neq}}{{/if}}>
5
4
  {{page.html}}
6
5
  </div>
7
- {{#else}}
8
- <header class="doc-header">
9
- <p class="eyebrow">Documentation</p>
10
- <h1>{{page.title}}</h1>
11
- {{#if page.excerpt}}<p class="lede">{{page.excerpt}}</p>{{/if}}
12
- </header>
13
- <div class="prose" {{#if site.search}}{{#if_neq page.discoverability "delist"}}data-pagefind-body{{/if_neq}}{{/if}}>
14
- {{page.html}}
15
- </div>
16
- {{/if}}
17
6
  {{#if page.meta.source_markdown_url}}
18
7
  <footer class="doc-source">
19
- <a href="{{page.meta.source_markdown_url}}">View this page as Markdown</a>
8
+ <a href="{{page.meta.source_markdown_url}}">
9
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
10
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
11
+ <polyline points="14 2 14 8 20 8"></polyline>
12
+ </svg>
13
+ View this page as Markdown
14
+ </a>
20
15
  </footer>
21
16
  {{/if}}
22
17
  </div>
@@ -25,9 +20,13 @@
25
20
  <p class="doc-toc__title">On this page</p>
26
21
  <nav>
27
22
  <ol>
28
- {{#for item in page.toc}}<li class="doc-toc__item doc-toc__item--level-{{item.level}}"><a href="{{item.href}}">{{item.title}}</a></li>{{/for}}
23
+ {{#for item in page.toc}}<li class="doc-toc__item doc-toc__item--level-{{item.level}}"><a href="{{item.href}}" data-doc-toc-link>{{item.title}}</a></li>{{/for}}
29
24
  </ol>
30
25
  </nav>
31
26
  </aside>
27
+ {{#else}}
28
+ {{#if_eq page.document_type "html"}}
29
+ <aside class="doc-toc" aria-label="Table of contents" data-enhance-toc hidden></aside>
30
+ {{/if_eq}}
32
31
  {{/if}}
33
32
  </article>
@@ -0,0 +1,10 @@
1
+ <script>
2
+ (function () {
3
+ try {
4
+ var stored = localStorage.getItem('zp-theme');
5
+ if (stored === 'light' || stored === 'dark') {
6
+ document.documentElement.dataset.theme = stored;
7
+ }
8
+ } catch (e) {}
9
+ })();
10
+ </script>
@@ -1,9 +1,39 @@
1
- <article class="shell doc-content">
2
- <header class="doc-header">
3
- <p class="eyebrow">Post</p>
4
- <h1>{{post.title}}</h1>
5
- </header>
6
- <div class="prose" {{#if site.search}}{{#if_neq post.discoverability "delist"}}data-pagefind-body{{/if_neq}}{{/if}}>
7
- {{post.html}}
1
+ <article class="shell doc-layout{{#if post.toc}} doc-layout--with-toc{{/if}}">
2
+ <div class="doc-content">
3
+ <header class="doc-header">
4
+ <p class="eyebrow">
5
+ <span class="eyebrow__dot" aria-hidden="true"></span>
6
+ Post
7
+ </p>
8
+ <h1>{{post.title}}</h1>
9
+ </header>
10
+ <div class="prose" {{#if site.search}}{{#if_neq post.discoverability "delist"}}data-pagefind-body{{/if_neq}}{{/if}}>
11
+ {{post.html}}
12
+ </div>
13
+ {{#if post.meta.source_markdown_url}}
14
+ <footer class="doc-source">
15
+ <a href="{{post.meta.source_markdown_url}}">
16
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
17
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
18
+ <polyline points="14 2 14 8 20 8"></polyline>
19
+ </svg>
20
+ View this post as Markdown
21
+ </a>
22
+ </footer>
23
+ {{/if}}
8
24
  </div>
25
+ {{#if post.toc}}
26
+ <aside class="doc-toc" aria-label="Table of contents">
27
+ <p class="doc-toc__title">On this page</p>
28
+ <nav>
29
+ <ol>
30
+ {{#for item in post.toc}}<li class="doc-toc__item doc-toc__item--level-{{item.level}}"><a href="{{item.href}}" data-doc-toc-link>{{item.title}}</a></li>{{/for}}
31
+ </ol>
32
+ </nav>
33
+ </aside>
34
+ {{#else}}
35
+ {{#if_eq post.document_type "html"}}
36
+ <aside class="doc-toc" aria-label="Table of contents" data-enhance-toc hidden></aside>
37
+ {{/if_eq}}
38
+ {{/if}}
9
39
  </article>
@@ -1,11 +1,12 @@
1
1
  {
2
- "name": "ZeroPress Build Pages Docs",
2
+ "$schema": "https://schemas.zeropress.dev/theme-runtime/v0.6/schema.json",
3
+ "name": "ZeroPress Template Docs1",
3
4
  "namespace": "zeropress",
4
- "slug": "zeropress-docs",
5
- "version": "0.6.0",
5
+ "slug": "docs1",
6
+ "version": "0.6.1",
6
7
  "license": "MIT",
7
8
  "runtime": "0.6",
8
- "description": "Bundled documentation theme for @zeropress/build-pages",
9
+ "description": "Clean documentation theme",
9
10
  "features": {
10
11
  "post_index": false,
11
12
  "search": true
@@ -14,6 +15,10 @@
14
15
  "primary": {
15
16
  "title": "Primary navigation",
16
17
  "description": "Top-level documentation navigation"
18
+ },
19
+ "footer": {
20
+ "title": "Footer menu",
21
+ "description": "Compact footer links"
17
22
  }
18
23
  }
19
24
  }