@zeropress/build-pages 0.6.2 → 0.6.3

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,201 @@
1
+ /* ---------- Sticky header scrolled state ---------- */
2
+
3
+ const siteHeader = document.querySelector('.site-header');
4
+
5
+ if (siteHeader) {
6
+ let lastScrolled = false;
7
+ const updateHeader = () => {
8
+ const scrolled = window.scrollY > 4;
9
+ if (scrolled !== lastScrolled) {
10
+ siteHeader.classList.toggle('is-scrolled', scrolled);
11
+ lastScrolled = scrolled;
12
+ }
13
+ };
14
+ updateHeader();
15
+ window.addEventListener('scroll', updateHeader, { passive: true });
16
+ }
17
+
18
+ /* ---------- Mobile nav toggle ---------- */
19
+
20
+ const navToggle = document.querySelector('[data-nav-toggle]');
21
+ const navPanel = document.querySelector('[data-nav-panel]');
22
+
23
+ if (navToggle && navPanel) {
24
+ const closeNav = () => {
25
+ navPanel.classList.remove('is-open');
26
+ navToggle.setAttribute('aria-expanded', 'false');
27
+ };
28
+
29
+ navToggle.addEventListener('click', () => {
30
+ const open = !navPanel.classList.contains('is-open');
31
+ navPanel.classList.toggle('is-open', open);
32
+ navToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
33
+ });
34
+
35
+ navPanel.addEventListener('click', (event) => {
36
+ if (event.target instanceof HTMLAnchorElement) {
37
+ closeNav();
38
+ }
39
+ });
40
+
41
+ window.addEventListener('resize', () => {
42
+ if (window.innerWidth > 820) {
43
+ closeNav();
44
+ }
45
+ });
46
+ }
47
+
48
+ /* ---------- Theme toggle (light/dark) ---------- */
49
+
50
+ const themeToggle = document.querySelector('[data-theme-toggle]');
51
+
52
+ if (themeToggle) {
53
+ const root = document.documentElement;
54
+
55
+ const getResolvedTheme = () => {
56
+ const stored = root.dataset.theme;
57
+ if (stored === 'light' || stored === 'dark') return stored;
58
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
59
+ };
60
+
61
+ themeToggle.addEventListener('click', () => {
62
+ const next = getResolvedTheme() === 'dark' ? 'light' : 'dark';
63
+ root.dataset.theme = next;
64
+ try {
65
+ localStorage.setItem('zp-theme', next);
66
+ } catch (e) {}
67
+ });
68
+ }
69
+
70
+ /* ---------- Highlight active primary nav link ---------- */
71
+
72
+ const primaryLinks = Array.from(document.querySelectorAll('.site-nav a'));
73
+
74
+ if (primaryLinks.length) {
75
+ const path = window.location.pathname.replace(/\/+$/, '') || '/';
76
+ let bestMatch = null;
77
+ let bestScore = -1;
78
+
79
+ for (const link of primaryLinks) {
80
+ let href;
81
+ try {
82
+ href = new URL(link.href, window.location.origin).pathname.replace(/\/+$/, '') || '/';
83
+ } catch {
84
+ continue;
85
+ }
86
+
87
+ if (href === '/' && path !== '/') continue;
88
+ if (path === href || path.startsWith(href + '/')) {
89
+ if (href.length > bestScore) {
90
+ bestScore = href.length;
91
+ bestMatch = link;
92
+ }
93
+ }
94
+ }
95
+
96
+ if (bestMatch) {
97
+ bestMatch.setAttribute('aria-current', 'page');
98
+ }
99
+ }
100
+
101
+ /* ---------- Table of contents scroll spy ---------- */
102
+
103
+ const tocLinks = Array.from(document.querySelectorAll('[data-doc-toc-link]'));
104
+
105
+ if (tocLinks.length) {
106
+ const decodeHashId = (value) => {
107
+ try {
108
+ return decodeURIComponent(value);
109
+ } catch {
110
+ return value;
111
+ }
112
+ };
113
+
114
+ const getHashId = (value) => {
115
+ const hash = value || '';
116
+ return hash.startsWith('#') ? decodeHashId(hash.slice(1)) : '';
117
+ };
118
+
119
+ const tocItems = tocLinks
120
+ .map((link) => {
121
+ const heading = document.getElementById(getHashId(link.getAttribute('href')));
122
+ return heading ? { heading, link } : null;
123
+ })
124
+ .filter(Boolean);
125
+
126
+ if (tocItems.length) {
127
+ let activeId = '';
128
+ let frameRequested = false;
129
+
130
+ const anchorLine = () => (window.matchMedia('(max-width: 860px)').matches ? 160 : 120);
131
+
132
+ const setActive = (id) => {
133
+ if (!id || id === activeId) return;
134
+ activeId = id;
135
+
136
+ for (const item of tocItems) {
137
+ const match = item.heading.id === id;
138
+ item.link.classList.toggle('is-active', match);
139
+
140
+ if (match) {
141
+ item.link.setAttribute('aria-current', 'true');
142
+ const toc = item.link.closest('.doc-toc');
143
+ if (toc && toc.scrollHeight > toc.clientHeight) {
144
+ item.link.scrollIntoView({ block: 'nearest' });
145
+ }
146
+ } else {
147
+ item.link.removeAttribute('aria-current');
148
+ }
149
+ }
150
+ };
151
+
152
+ const updateActive = () => {
153
+ frameRequested = false;
154
+ let current = tocItems[0];
155
+ const line = anchorLine();
156
+ for (const item of tocItems) {
157
+ if (item.heading.getBoundingClientRect().top <= line) {
158
+ current = item;
159
+ } else {
160
+ break;
161
+ }
162
+ }
163
+ setActive(current.heading.id);
164
+ };
165
+
166
+ const requestUpdate = () => {
167
+ if (frameRequested) return;
168
+ frameRequested = true;
169
+ window.requestAnimationFrame(updateActive);
170
+ };
171
+
172
+ const setHashActive = () => {
173
+ const hashId = getHashId(window.location.hash);
174
+ const item = tocItems.find((entry) => entry.heading.id === hashId);
175
+ if (item) setActive(item.heading.id);
176
+ };
177
+
178
+ setHashActive();
179
+ requestUpdate();
180
+ window.setTimeout(requestUpdate, 80);
181
+
182
+ window.addEventListener('scroll', requestUpdate, { passive: true });
183
+ window.addEventListener('resize', requestUpdate);
184
+ window.addEventListener('hashchange', () => {
185
+ setHashActive();
186
+ requestUpdate();
187
+ });
188
+ }
189
+ }
190
+
191
+ /* ---------- Search ---------- */
192
+
1
193
  const searchRoot = document.querySelector('[data-site-search]');
2
194
 
3
195
  if (searchRoot) {
4
196
  const form = searchRoot.querySelector('[data-site-search-form]');
5
197
  const input = searchRoot.querySelector('[data-site-search-input]');
198
+ const submit = searchRoot.querySelector('[data-site-search-submit]');
6
199
  const panel = searchRoot.querySelector('[data-site-search-panel]');
7
200
  const status = searchRoot.querySelector('[data-site-search-status]');
8
201
  const resultsList = searchRoot.querySelector('[data-site-search-results]');
@@ -31,6 +224,10 @@ if (searchRoot) {
31
224
  status.textContent = message;
32
225
  };
33
226
 
227
+ input.disabled = false;
228
+ if (submit) submit.disabled = false;
229
+ setStatus('Type at least two characters.');
230
+
34
231
  const renderResults = async (items) => {
35
232
  clearResults();
36
233
  if (!items.length) {
@@ -101,15 +298,30 @@ if (searchRoot) {
101
298
  }
102
299
  });
103
300
 
104
- form.addEventListener('submit', (event) => {
105
- event.preventDefault();
106
- runSearch();
107
- });
301
+ if (form) {
302
+ form.addEventListener('submit', (event) => {
303
+ event.preventDefault();
304
+ runSearch();
305
+ });
306
+ }
108
307
 
109
308
  document.addEventListener('keydown', (event) => {
110
309
  if (event.key === 'Escape') {
111
310
  hidePanel();
112
311
  input.blur();
312
+ return;
313
+ }
314
+
315
+ // Slash to focus search (when not in another input)
316
+ if (event.key === '/' && !event.ctrlKey && !event.metaKey && !event.altKey) {
317
+ const target = event.target;
318
+ const isFormField = target instanceof HTMLElement &&
319
+ (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable);
320
+ if (!isFormField) {
321
+ event.preventDefault();
322
+ input.focus();
323
+ input.select();
324
+ }
113
325
  }
114
326
  });
115
327
 
@@ -3,33 +3,71 @@
3
3
  <meta charset="utf-8">
4
4
  <meta name="viewport" content="width=device-width, initial-scale=1">
5
5
  <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)">
6
+ <meta name="theme-color" content="#fbfbfd" media="(prefers-color-scheme: light)">
7
+ <meta name="theme-color" content="#0b0d12" media="(prefers-color-scheme: dark)">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <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
11
  <link rel="stylesheet" href="/assets/style.css">
12
+ {{partial:theme-bootstrap}}
9
13
  {{meta.head_tags}}
10
14
  </head>
11
15
  <body>
12
16
  <a class="skip-link" href="#content">Skip to content</a>
13
17
  <header class="site-header">
14
18
  <div class="shell site-header__inner">
15
- <a class="brand" href="/">{{site.title}}</a>
16
- <div class="site-header__actions">
19
+ <a class="brand" href="/">
20
+ {{#if site.logo.src}}
21
+ <img
22
+ class="brand__mark"
23
+ src="{{site.logo.src}}"
24
+ alt="{{#if site.logo.alt}}{{site.logo.alt}}{{#else}}{{site.title}}{{/if}}"
25
+ aria-hidden="true"
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">
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,7 +20,7 @@
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>
@@ -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,6 +1,9 @@
1
1
  <article class="shell doc-content">
2
2
  <header class="doc-header">
3
- <p class="eyebrow">Post</p>
3
+ <p class="eyebrow">
4
+ <span class="eyebrow__dot" aria-hidden="true"></span>
5
+ Post
6
+ </p>
4
7
  <h1>{{post.title}}</h1>
5
8
  </header>
6
9
  <div class="prose" {{#if site.search}}{{#if_neq post.discoverability "delist"}}data-pagefind-body{{/if_neq}}{{/if}}>
@@ -1,11 +1,11 @@
1
1
  {
2
- "name": "ZeroPress Build Pages Docs",
2
+ "name": "ZeroPress Template Docs1",
3
3
  "namespace": "zeropress",
4
- "slug": "zeropress-docs",
5
- "version": "0.6.0",
4
+ "slug": "docs1",
5
+ "version": "0.6.1",
6
6
  "license": "MIT",
7
7
  "runtime": "0.6",
8
- "description": "Bundled documentation theme for @zeropress/build-pages",
8
+ "description": "Clean documentation theme",
9
9
  "features": {
10
10
  "post_index": false,
11
11
  "search": true
@@ -14,6 +14,10 @@
14
14
  "primary": {
15
15
  "title": "Primary navigation",
16
16
  "description": "Top-level documentation navigation"
17
+ },
18
+ "footer": {
19
+ "title": "Footer menu",
20
+ "description": "Compact footer links"
17
21
  }
18
22
  }
19
23
  }