@abreen/tada 1.5.2 → 1.5.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.
package/build/globals.ts CHANGED
@@ -85,7 +85,7 @@ function readableDate(date: string | Date | null | undefined): string {
85
85
  return `${months[month]} ${day}, ${year}`;
86
86
  }
87
87
 
88
- function classNames(obj: Record<string, unknown>): string {
88
+ export function classNames(obj: Record<string, unknown>): string {
89
89
  const names: string[] = [];
90
90
  for (const key in obj) {
91
91
  if (obj[key]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
package/src/code/index.ts CHANGED
@@ -28,15 +28,16 @@ export default async function mount(window: Window): Promise<void> {
28
28
  const container = fragment.querySelector('.code-body');
29
29
  const topNodes = container ? container.childNodes : fragment.childNodes;
30
30
  topNodes.forEach(node => {
31
- if (node.nodeType === Node.TEXT_NODE) {
31
+ if (node.nodeType === 3 /* Node.TEXT_NODE */) {
32
32
  const text = node.textContent ?? '';
33
33
  if (text.trim()) {
34
34
  lines.push(text);
35
35
  }
36
- } else if (node instanceof Element) {
37
- const codeRows = node.querySelectorAll('.code-row');
36
+ } else if ('querySelectorAll' in node) {
37
+ const el = node as Element;
38
+ const codeRows = el.querySelectorAll('.code-row');
38
39
  if (codeRows.length > 0) {
39
- codeRows.forEach(row => {
40
+ codeRows.forEach((row: Element) => {
40
41
  const codeEl = row.querySelector('code');
41
42
  if (codeEl) {
42
43
  const text = codeEl.textContent ?? '';
package/src/index.ts CHANGED
@@ -42,7 +42,7 @@ document.addEventListener('DOMContentLoaded', async () => {
42
42
  const persistentEntries = Object.entries(PERSISTENT_COMPONENTS);
43
43
  const persistentPromises = persistentEntries.map(([name, mount]) => {
44
44
  return new Promise<void>((resolve, reject) => {
45
- scheduleTask(async () => {
45
+ scheduleTask(window, async () => {
46
46
  try {
47
47
  mount(window);
48
48
  resolve();
@@ -78,7 +78,8 @@ function swapContent(newDoc: Document): void {
78
78
  const newContainer = newDoc.querySelector('.container');
79
79
  const oldContainer = document.querySelector('.container');
80
80
  if (newContainer && oldContainer) {
81
- oldContainer.replaceChildren(...newContainer.childNodes);
81
+ const imported = document.importNode(newContainer, true);
82
+ oldContainer.replaceChildren(...imported.childNodes);
82
83
  }
83
84
 
84
85
  // Update body class (carries template name and toc-is-active)
@@ -146,7 +147,7 @@ async function performNavigation(
146
147
  updateHead(newDoc);
147
148
  if (isPush) {
148
149
  historyIndex++;
149
- history.pushState({ navIndex: historyIndex }, '', url);
150
+ window.history.pushState({ navIndex: historyIndex }, '', url);
150
151
  }
151
152
  const parsed = new URL(url);
152
153
  currentPath = parsed.pathname + parsed.search;
@@ -177,7 +178,7 @@ async function performNavigation(
177
178
  const headerHeight =
178
179
  document.querySelector('header')?.getBoundingClientRect().height ?? 0;
179
180
  const titleVisible =
180
- titleEl != null && titleEl.getBoundingClientRect().bottom >= headerHeight;
181
+ titleEl != null && titleEl.getBoundingClientRect().top >= headerHeight;
181
182
 
182
183
  if (titleVisible) {
183
184
  const h1 = titleEl.querySelector('h1') as HTMLElement | null;
@@ -202,24 +203,29 @@ async function performNavigation(
202
203
 
203
204
  const transition = doc.startViewTransition(() => {
204
205
  doSwap();
205
- // Apply transition names to the new title if the old one was visible
206
- if (titleVisible) {
207
- const newTitleEl = document.querySelector('.title-and-info');
208
- if (newTitleEl) {
209
- const h1 = newTitleEl.querySelector('h1') as HTMLElement | null;
210
- const info = newTitleEl.querySelector('.info') as HTMLElement | null;
211
- const breadcrumb = newTitleEl.querySelector(
212
- 'a.breadcrumb',
213
- ) as HTMLElement | null;
214
- if (h1) {
215
- h1.style.viewTransitionName = 'page-title';
216
- }
217
- if (info) {
218
- info.style.viewTransitionName = 'page-info';
219
- }
220
- if (breadcrumb) {
221
- breadcrumb.style.viewTransitionName = 'page-breadcrumb';
222
- }
206
+ // Check title visibility on the new page independently (scroll
207
+ // has been restored by doSwap, so the position reflects the
208
+ // destination page)
209
+ const newTitleEl = document.querySelector('.title-and-info');
210
+ const newHeaderHeight =
211
+ document.querySelector('header')?.getBoundingClientRect().height ?? 0;
212
+ const newTitleVisible =
213
+ newTitleEl != null &&
214
+ newTitleEl.getBoundingClientRect().top >= newHeaderHeight;
215
+ if (newTitleVisible && newTitleEl) {
216
+ const h1 = newTitleEl.querySelector('h1') as HTMLElement | null;
217
+ const info = newTitleEl.querySelector('.info') as HTMLElement | null;
218
+ const breadcrumb = newTitleEl.querySelector(
219
+ 'a.breadcrumb',
220
+ ) as HTMLElement | null;
221
+ if (h1) {
222
+ h1.style.viewTransitionName = 'page-title';
223
+ }
224
+ if (info) {
225
+ info.style.viewTransitionName = 'page-info';
226
+ }
227
+ if (breadcrumb) {
228
+ breadcrumb.style.viewTransitionName = 'page-breadcrumb';
223
229
  }
224
230
  }
225
231
  });
@@ -31,7 +31,7 @@ export async function mountPerPageComponents(
31
31
 
32
32
  const mountPromises = entries.map(([name, mount]) => {
33
33
  return new Promise<void>((resolve, reject) => {
34
- scheduleTask(async () => {
34
+ scheduleTask(window, async () => {
35
35
  try {
36
36
  const cleanup = await mount(window);
37
37
  if (typeof cleanup === 'function') {
@@ -1,5 +1,5 @@
1
- $shift: 40px;
2
- $duration: 200ms;
1
+ $shift: 12px;
2
+ $duration: 150ms;
3
3
 
4
4
  @keyframes slide-out-left {
5
5
  to {
@@ -106,6 +106,8 @@ function render(
106
106
  state: State,
107
107
  loading = false,
108
108
  ) {
109
+ const doc = resultsContainer.ownerDocument;
110
+
109
111
  if (state.showResults) {
110
112
  resultsContainer.removeAttribute('inert');
111
113
  }
@@ -114,12 +116,12 @@ function render(
114
116
  '.results',
115
117
  ) as HTMLElement | null;
116
118
  if (!resultsDiv) {
117
- resultsDiv = document.createElement('div');
119
+ resultsDiv = doc.createElement('div');
118
120
  resultsDiv.className = 'results';
119
121
  resultsContainer.appendChild(resultsDiv);
120
122
  }
121
123
 
122
- const ol = document.createElement('ol');
124
+ const ol = doc.createElement('ol');
123
125
  ol.id = `${input.name}-results`;
124
126
  ol.role = 'listbox';
125
127
  ol.tabIndex = -1;
@@ -128,29 +130,29 @@ function render(
128
130
  const totalVisible = Math.min(state.results.length, MAX_RESULTS);
129
131
 
130
132
  state.results.slice(0, MAX_RESULTS).forEach((result, i) => {
131
- const a = document.createElement('a');
133
+ const a = doc.createElement('a');
132
134
  a.id = `result-${i}`;
133
135
  a.className = 'result';
134
136
  a.href = result.url;
135
137
  a.tabIndex = 0;
136
138
 
137
- const titleEl = document.createElement('div');
139
+ const titleEl = doc.createElement('div');
138
140
  titleEl.id = `title-${i}`;
139
141
  titleEl.className = 'title';
140
142
  titleEl.textContent = result.title;
141
143
  a.appendChild(titleEl);
142
144
 
143
- const subtitle = document.createElement('div');
145
+ const subtitle = doc.createElement('div');
144
146
  subtitle.className = 'subtitle';
145
147
  subtitle.innerText = result.url;
146
148
  a.appendChild(subtitle);
147
149
 
148
- const excerpt = document.createElement('div');
150
+ const excerpt = doc.createElement('div');
149
151
  excerpt.className = 'excerpt';
150
152
  excerpt.innerHTML = result.excerpt;
151
153
  a.appendChild(excerpt);
152
154
 
153
- const li = document.createElement('li');
155
+ const li = doc.createElement('li');
154
156
  li.id = `option-${i}`;
155
157
  li.setAttribute('role', 'option');
156
158
  li.setAttribute('aria-selected', 'false');
@@ -161,26 +163,26 @@ function render(
161
163
 
162
164
  const subsToShow = result.subResults.slice(0, 5);
163
165
  if (subsToShow.length > 0) {
164
- const subList = document.createElement('ul');
166
+ const subList = doc.createElement('ul');
165
167
  subList.className = 'sub-results';
166
168
  for (const sub of subsToShow) {
167
- const subA = document.createElement('a');
169
+ const subA = doc.createElement('a');
168
170
  subA.href = sub.url;
169
171
  subA.className = 'sub-result';
170
172
 
171
- const subTitle = document.createElement('div');
173
+ const subTitle = doc.createElement('div');
172
174
  subTitle.className = 'title';
173
175
  subTitle.textContent = sub.title;
174
176
  subA.appendChild(subTitle);
175
177
 
176
178
  if (sub.excerpt) {
177
- const subExcerpt = document.createElement('div');
179
+ const subExcerpt = doc.createElement('div');
178
180
  subExcerpt.className = 'excerpt';
179
181
  subExcerpt.innerHTML = sub.excerpt;
180
182
  subA.appendChild(subExcerpt);
181
183
  }
182
184
 
183
- const subLi = document.createElement('li');
185
+ const subLi = doc.createElement('li');
184
186
  subLi.appendChild(subA);
185
187
  subList.appendChild(subLi);
186
188
  }
@@ -196,12 +198,12 @@ function render(
196
198
  '.results-info',
197
199
  ) as HTMLSpanElement | null;
198
200
  if (!infoSpan) {
199
- infoSpan = document.createElement('span');
201
+ infoSpan = doc.createElement('span');
200
202
  infoSpan.className = 'results-info';
201
203
 
202
- const hint = document.createElement('span');
204
+ const hint = doc.createElement('span');
203
205
  hint.className = 'search-hint';
204
- const kbd1 = document.createElement('kbd');
206
+ const kbd1 = doc.createElement('kbd');
205
207
  kbd1.textContent = '/';
206
208
  hint.append('Press\u00a0', kbd1, '\u00a0to search');
207
209
  infoSpan.appendChild(hint);
@@ -213,7 +215,7 @@ function render(
213
215
  '.results-count',
214
216
  ) as HTMLSpanElement | null;
215
217
  if (!countSpan) {
216
- countSpan = document.createElement('span');
218
+ countSpan = doc.createElement('span');
217
219
  countSpan.className = 'results-count';
218
220
  infoSpan.insertBefore(countSpan, infoSpan.firstChild);
219
221
  }
@@ -248,6 +250,7 @@ function render(
248
250
  }
249
251
 
250
252
  export default (window: Window) => {
253
+ const { document } = window;
251
254
  const input = document.querySelector(
252
255
  'input.quick-search',
253
256
  ) as HTMLInputElement | null;
package/src/toc/index.ts CHANGED
@@ -270,7 +270,7 @@ export default (window: Window) => {
270
270
  scrollIfNeeded(nextItem, window.document);
271
271
  }
272
272
  }
273
- const debounced = debounce(handleScroll, LATENCY_MS);
273
+ const debounced = debounce(window, handleScroll, LATENCY_MS);
274
274
  window.addEventListener('scroll', debounced, { passive: true });
275
275
 
276
276
  // Call after load to set current item
package/src/top/index.ts CHANGED
@@ -30,9 +30,9 @@ export default (window: Window) => {
30
30
  e.preventDefault();
31
31
  const cleanUrl = window.location.pathname + window.location.search;
32
32
  if (window.location.hash) {
33
- history.pushState(null, '', cleanUrl);
33
+ window.history.pushState(null, '', cleanUrl);
34
34
  } else {
35
- history.replaceState(null, '', cleanUrl);
35
+ window.history.replaceState(null, '', cleanUrl);
36
36
  }
37
37
  window.scrollTo({ top: 0, behavior: 'smooth' });
38
38
  const toc = window.document.querySelector('nav.toc') as HTMLElement | null;
@@ -65,7 +65,7 @@ export default (window: Window) => {
65
65
  }
66
66
  }
67
67
 
68
- const debounced = debounce(updateVisibility, LATENCY_MS);
68
+ const debounced = debounce(window, updateVisibility, LATENCY_MS);
69
69
  window.addEventListener('scroll', debounced, { passive: true });
70
70
  updateVisibility();
71
71
 
package/src/util.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  export function debounce<T extends (...args: unknown[]) => void>(
2
+ win: Window,
2
3
  fn: T,
3
4
  time: number,
4
5
  ) {
5
6
  let timer: number;
6
7
  return (...args: Parameters<T>) => {
7
- window.clearTimeout(timer);
8
- timer = window.setTimeout(() => {
8
+ win.clearTimeout(timer);
9
+ timer = win.setTimeout(() => {
9
10
  fn(...args);
10
11
  }, time);
11
12
  };
@@ -41,11 +42,11 @@ export function applyBasePath(subPath: string): string {
41
42
  return path + subPath;
42
43
  }
43
44
 
44
- export function scheduleTask(fn: () => void) {
45
- if (typeof window.requestIdleCallback === 'function') {
46
- window.requestIdleCallback(fn);
45
+ export function scheduleTask(win: Window, fn: () => void) {
46
+ if (typeof win.requestIdleCallback === 'function') {
47
+ win.requestIdleCallback(fn);
47
48
  } else {
48
- return setTimeout(fn, 0);
49
+ return win.setTimeout(fn, 0);
49
50
  }
50
51
  }
51
52
 
@@ -8,6 +8,7 @@
8
8
  <a
9
9
  class="<%= cx({disabled: link.disabled, external: link.external}) %>"
10
10
  <% if (!link.disabled) { %>href="<%= link.internal ? applyBasePath(link.internal) : link.external %>"<% } %>
11
+ <% if (link.external) { %>target="_blank"<% } %>
11
12
  >
12
13
  <%= link.text %></a>
13
14
  </li>