marksites 0.1.0 → 0.2.2

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.
@@ -61,6 +61,80 @@ function countComments(nodes) {
61
61
  ? node.commentCount
62
62
  : 0), 0);
63
63
  }
64
+ function collectRecentFiles(nodes) {
65
+ const files = [];
66
+ const collect = (items, parentPath = "") => {
67
+ for (const item of items) {
68
+ if (item.type === "directory") {
69
+ collect(item.children, parentPath ? `${parentPath}/${item.name}` : item.name);
70
+ continue;
71
+ }
72
+ if (!item.modifiedAt)
73
+ continue;
74
+ const date = new Date(item.modifiedAt);
75
+ if (Number.isNaN(date.getTime()))
76
+ throw new Error(`Invalid file tree modifiedAt timestamp: ${item.modifiedAt}`);
77
+ files.push({
78
+ name: item.name,
79
+ path: item.path ?? (parentPath ? `${parentPath}/${item.name}` : item.name),
80
+ href: item.href,
81
+ current: item.current === true,
82
+ modifiedAt: date.toISOString(),
83
+ timestamp: date.getTime(),
84
+ commentCount: item.commentCount,
85
+ });
86
+ }
87
+ };
88
+ collect(nodes);
89
+ return files.sort((left, right) => right.timestamp - left.timestamp ||
90
+ left.path.localeCompare(right.path, "en"));
91
+ }
92
+ function renderRecentFiles(nodes) {
93
+ const files = collectRecentFiles(nodes);
94
+ if (files.length === 0)
95
+ return ` <li class="file-tree-recent-empty">更新日時のあるファイルはありません</li>`;
96
+ let currentDate = "";
97
+ let groupNumber = 0;
98
+ const output = [];
99
+ for (let index = 0; index < files.length;) {
100
+ const first = files[index];
101
+ const date = first.modifiedAt.slice(0, 10);
102
+ if (date !== currentDate) {
103
+ const dateCount = files.filter((file) => file.modifiedAt.startsWith(date)).length;
104
+ output.push(` <li class="file-tree-date" data-date="${date}"><button type="button" data-recent-date-toggle aria-expanded="true"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg><time datetime="${date}">${Number(date.slice(0, 4))}年${Number(date.slice(5, 7))}月${Number(date.slice(8, 10))}日</time><span aria-label="ファイル${dateCount}件">${dateCount}</span></button></li>`);
105
+ currentDate = date;
106
+ }
107
+ const separator = first.path.lastIndexOf("/");
108
+ const directory = separator === -1 ? "" : first.path.slice(0, separator + 1);
109
+ let end = index + 1;
110
+ while (end < files.length) {
111
+ const candidate = files[end];
112
+ const candidateSeparator = candidate.path.lastIndexOf("/");
113
+ const candidateDirectory = candidateSeparator === -1
114
+ ? ""
115
+ : candidate.path.slice(0, candidateSeparator + 1);
116
+ if (candidate.modifiedAt.slice(0, 10) !== date ||
117
+ candidateDirectory !== directory)
118
+ break;
119
+ end++;
120
+ }
121
+ const grouped = end - index >= 2;
122
+ const groupId = grouped ? `recent-${groupNumber++}` : "";
123
+ const directoryLabel = directory ? `/${directory}` : "/";
124
+ if (grouped)
125
+ output.push(` <li class="file-tree-directory-group" data-recent-group="${groupId}" title="${escapeHtml(directoryLabel)}">${renderFolderIcon()}<span>${escapeHtml(directoryLabel)}</span></li>`);
126
+ for (; index < end; index++) {
127
+ const file = files[index];
128
+ const current = file.current ? ' aria-current="page"' : "";
129
+ const count = Number.isSafeInteger(file.commentCount) && file.commentCount > 0
130
+ ? `<span class="file-tree-comment-count" aria-label="コメント${file.commentCount}件">${file.commentCount}</span>`
131
+ : "";
132
+ const time = file.modifiedAt.slice(11, 16);
133
+ output.push(` <li class="file-tree-recent-file${grouped ? " is-grouped" : ""}" data-file-path="${escapeHtml(file.path)}" data-directory="${escapeHtml(directory)}" data-modified-at="${file.modifiedAt}"${grouped ? ` data-recent-group="${groupId}"` : ""}><a href="${escapeHtml(file.href)}" title="${escapeHtml(file.path)}"${current}><time datetime="${file.modifiedAt}">${time}</time><span class="file-tree-recent-label"><span class="file-tree-name">${escapeHtml(file.name)}</span><span class="file-tree-directory-path">${renderFolderIcon()}<span>${escapeHtml(directoryLabel)}</span></span></span>${count}</a></li>`);
134
+ }
135
+ }
136
+ return output.join("\n");
137
+ }
64
138
  export function renderFileTree(options) {
65
139
  if (!options || options.items.length === 0)
66
140
  return "";
@@ -71,13 +145,20 @@ ${contents}</nav>
71
145
  `;
72
146
  }
73
147
  function renderTreeContents(items, folderIds) {
74
- return ` <div class="file-tree-filter">
148
+ return ` <div class="file-tree-view-tabs" role="tablist" aria-label="ファイル表示">
149
+ <button type="button" role="tab" data-file-tree-view="tree" aria-selected="true">ツリー</button>
150
+ <button type="button" role="tab" data-file-tree-view="recent" aria-selected="false" tabindex="-1">更新順</button>
151
+ </div>
152
+ <div class="file-tree-filter">
75
153
  <input type="search" class="file-tree-filter-input" placeholder="ファイルを検索" aria-label="ファイル名で検索" autocomplete="off">
76
154
  <p class="file-tree-filter-empty" hidden>一致するファイルはありません</p>
77
155
  </div>
78
- <ul class="file-tree-root">
156
+ <ul class="file-tree-root" data-file-tree-panel="tree" role="tabpanel">
79
157
  ${renderNodes(items, folderIds)}
80
158
  </ul>
159
+ <ul class="file-tree-recent" data-file-tree-panel="recent" role="tabpanel" hidden>
160
+ ${renderRecentFiles(items)}
161
+ </ul>
81
162
  `;
82
163
  }
83
164
  export function renderFileSidebar(options) {
@@ -85,17 +166,12 @@ export function renderFileSidebar(options) {
85
166
  return "";
86
167
  const folderIds = createFolderIds(options.items);
87
168
  const title = escapeHtml(options.title ?? "ファイル");
169
+ const contents = renderTreeContents(options.items, folderIds);
88
170
  return `<button type="button" class="file-sidebar-close" data-file-sidebar-close aria-label="ファイルサイドバーを閉じる" title="ファイルサイドバーを閉じる"><svg class="file-sidebar-toggle-icon" viewBox="0 0 16 16" aria-hidden="true"><rect x="1.75" y="2.25" width="12.5" height="11.5" rx="1.5" /><path d="M6 2.5v11M10.5 5.5L8 8l2.5 2.5" /></svg></button>
89
171
  <aside class="file-sidebar" id="file-sidebar" aria-label="${title}">
90
172
  <div class="file-sidebar-header"><span>${title}</span></div>
91
173
  <nav class="file-tree file-tree-sidebar" aria-label="${title}">
92
- <div class="file-tree-filter">
93
- <input type="search" class="file-tree-filter-input" placeholder="ファイルを検索" aria-label="ファイル名で検索" autocomplete="off">
94
- <p class="file-tree-filter-empty" hidden>一致するファイルはありません</p>
95
- </div>
96
- <ul class="file-tree-root">
97
- ${renderNodes(options.items, folderIds)}
98
- </ul>
174
+ ${contents}
99
175
  </nav>
100
176
  </aside>
101
177
  `;
@@ -117,10 +193,12 @@ export function renderFileTreeScript(enabled) {
117
193
  const stateParameter = 'open';
118
194
  const popoverParameter = 'marksites-files';
119
195
  const sidebarParameter = 'file-sidebar';
196
+ const viewParameter = 'file-view';
120
197
  const pageUrl = new URL(location.href);
121
198
  const openPaths = new Set(pageUrl.searchParams.getAll(stateParameter));
122
199
  const compact = matchMedia('(max-width: 900px)');
123
200
  let sidebarPreferenceOpen = pageUrl.searchParams.get(sidebarParameter) !== 'closed';
201
+ let activeView = pageUrl.searchParams.get(viewParameter) === 'recent' ? 'recent' : 'tree';
124
202
  const ignoredToggles = new WeakSet();
125
203
  const initialOpenState = new Map();
126
204
 
@@ -141,6 +219,8 @@ export function renderFileTreeScript(enabled) {
141
219
  if (!popover.hidden) url.searchParams.set(popoverParameter, 'open');
142
220
  url.searchParams.delete(sidebarParameter);
143
221
  if (!sidebarPreferenceOpen) url.searchParams.set(sidebarParameter, 'closed');
222
+ url.searchParams.delete(viewParameter);
223
+ if (activeView === 'recent') url.searchParams.set(viewParameter, 'recent');
144
224
  return url;
145
225
  };
146
226
 
@@ -182,11 +262,14 @@ export function renderFileTreeScript(enabled) {
182
262
  };
183
263
 
184
264
  const applySidebarState = (open) => {
265
+ if (open && !popover.hidden) setPopoverOpen(false, false, false);
185
266
  sidebar.hidden = !open;
186
267
  sidebarCloseButton.hidden = !open;
187
268
  sidebarOpenButton.hidden = open;
188
269
  sidebarOpenButton.setAttribute('aria-expanded', String(open));
189
270
  document.body.classList.toggle('file-sidebar-collapsed', !open);
271
+ popoverToggle.disabled = open;
272
+ popoverToggle.setAttribute('aria-disabled', String(open));
190
273
  };
191
274
 
192
275
  const setSidebarOpen = (open, sync = true) => {
@@ -197,8 +280,162 @@ export function renderFileTreeScript(enabled) {
197
280
  else sidebarOpenButton.focus();
198
281
  };
199
282
 
200
- setPopoverOpen(pageUrl.searchParams.get(popoverParameter) === 'open', false, false);
283
+ const openRecentDates = new Set();
284
+
285
+ const rebuildRecentGroups = (tree) => {
286
+ const list = tree.querySelector('.file-tree-recent');
287
+ if (!list) return;
288
+ const files = [...list.querySelectorAll('.file-tree-recent-file')];
289
+ for (const heading of list.querySelectorAll('.file-tree-date,.file-tree-directory-group')) heading.remove();
290
+ for (const file of files) {
291
+ file.classList.remove('is-grouped');
292
+ delete file.dataset.recentGroup;
293
+ }
294
+ const pad = value => String(value).padStart(2, '0');
295
+ files.sort((left, right) => {
296
+ const dateDifference = new Date(right.dataset.modifiedAt) - new Date(left.dataset.modifiedAt);
297
+ return dateDifference || left.dataset.filePath.localeCompare(right.dataset.filePath, 'en');
298
+ });
299
+ let previousDate = '',groupNumber = 0;
300
+ for (let index = 0; index < files.length;) {
301
+ const first = files[index];
302
+ const firstDate = new Date(first.dataset.modifiedAt);
303
+ const dateKey = firstDate.getFullYear()+'-'+pad(firstDate.getMonth()+1)+'-'+pad(firstDate.getDate());
304
+ let end = index+1;
305
+ while (end < files.length) {
306
+ const candidate = files[end],candidateDate = new Date(candidate.dataset.modifiedAt);
307
+ const candidateDateKey = candidateDate.getFullYear()+'-'+pad(candidateDate.getMonth()+1)+'-'+pad(candidateDate.getDate());
308
+ if (candidateDateKey !== dateKey || candidate.dataset.directory !== first.dataset.directory) break;
309
+ end++;
310
+ }
311
+ const grouped = end-index >= 2;
312
+ const groupId = grouped ? 'recent-'+groupNumber++ : '';
313
+ if (dateKey !== previousDate) {
314
+ const heading = document.createElement('li');
315
+ heading.className = 'file-tree-date';
316
+ heading.dataset.date = dateKey;
317
+ const button = document.createElement('button');
318
+ button.type = 'button';
319
+ button.dataset.recentDateToggle = '';
320
+ button.setAttribute('aria-expanded', 'true');
321
+ button.innerHTML = '<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg>';
322
+ const label = document.createElement('time');
323
+ label.dateTime = dateKey;
324
+ label.textContent = firstDate.getFullYear()+'年'+(firstDate.getMonth()+1)+'月'+firstDate.getDate()+'日';
325
+ const count = document.createElement('span');
326
+ const dateCount = files.filter(file => {
327
+ const value = new Date(file.dataset.modifiedAt);
328
+ return value.getFullYear()+'-'+pad(value.getMonth()+1)+'-'+pad(value.getDate()) === dateKey;
329
+ }).length;
330
+ count.textContent = String(dateCount);
331
+ count.setAttribute('aria-label', 'ファイル'+dateCount+'件');
332
+ button.append(label,count);
333
+ heading.append(button);
334
+ list.append(heading);
335
+ previousDate = dateKey;
336
+ }
337
+ if (grouped) {
338
+ const group = document.createElement('li');
339
+ group.className = 'file-tree-directory-group';
340
+ group.dataset.recentGroup = groupId;
341
+ group.dataset.localDate = dateKey;
342
+ const directoryLabel = first.dataset.directory ? '/'+first.dataset.directory : '/';
343
+ group.title = directoryLabel;
344
+ group.innerHTML = ${JSON.stringify(renderFolderIcon())};
345
+ const name = document.createElement('span');
346
+ name.textContent = directoryLabel;
347
+ group.append(name);
348
+ list.append(group);
349
+ }
350
+ for (; index < end; index++) {
351
+ const file = files[index];
352
+ const date = new Date(file.dataset.modifiedAt);
353
+ file.dataset.localDate = dateKey;
354
+ const time = file.querySelector('time');
355
+ time.textContent = pad(date.getHours())+':'+pad(date.getMinutes());
356
+ if (grouped) {
357
+ file.classList.add('is-grouped');
358
+ file.dataset.recentGroup = groupId;
359
+ }
360
+ list.append(file);
361
+ }
362
+ }
363
+ };
364
+
365
+ for (const tree of trees) rebuildRecentGroups(tree);
366
+
367
+ const firstRecentTree = trees[0];
368
+ const firstRecentFiles = [...firstRecentTree.querySelectorAll('.file-tree-recent-file')];
369
+ const latestRecentDate = firstRecentFiles[0]?.dataset.localDate;
370
+ if (latestRecentDate) openRecentDates.add(latestRecentDate);
371
+ for (const file of firstRecentFiles) {
372
+ if (file.querySelector('a[aria-current="page"]')) openRecentDates.add(file.dataset.localDate);
373
+ }
374
+
375
+ const applyRecentDateState = (searching = false) => {
376
+ for (const tree of trees) {
377
+ const files = [...tree.querySelectorAll('.file-tree-recent-file')];
378
+ for (const heading of tree.querySelectorAll('.file-tree-date')) {
379
+ const expanded = searching
380
+ ? files.some(file => file.dataset.localDate === heading.dataset.date && !file.hidden)
381
+ : openRecentDates.has(heading.dataset.date);
382
+ heading.querySelector('button').setAttribute('aria-expanded', String(expanded));
383
+ for (const item of tree.querySelectorAll('.file-tree-recent-file,.file-tree-directory-group')) {
384
+ if (item.dataset.localDate === heading.dataset.date)
385
+ item.classList.toggle('is-date-collapsed', !expanded);
386
+ }
387
+ }
388
+ }
389
+ };
390
+
391
+ applyRecentDateState();
392
+ for (const tree of trees) {
393
+ for (const button of tree.querySelectorAll('[data-recent-date-toggle]')) {
394
+ button.addEventListener('click', () => {
395
+ if (tree.querySelector('.file-tree-filter-input').value.trim() !== '') return;
396
+ const date = button.closest('.file-tree-date').dataset.date;
397
+ if (openRecentDates.has(date)) openRecentDates.delete(date);
398
+ else openRecentDates.add(date);
399
+ applyRecentDateState();
400
+ });
401
+ }
402
+ }
403
+
404
+ const applyView = (view, sync = true) => {
405
+ activeView = view;
406
+ for (const tree of trees) {
407
+ for (const button of tree.querySelectorAll('[data-file-tree-view]')) {
408
+ const selected = button.dataset.fileTreeView === view;
409
+ button.setAttribute('aria-selected', String(selected));
410
+ button.tabIndex = selected ? 0 : -1;
411
+ }
412
+ for (const panel of tree.querySelectorAll('[data-file-tree-panel]'))
413
+ panel.hidden = panel.dataset.fileTreePanel !== view;
414
+ const selector = view === 'tree' ? '.file-tree-file' : '.file-tree-recent-file';
415
+ const items = [...tree.querySelectorAll(selector)];
416
+ tree.querySelector('.file-tree-filter-empty').hidden = items.length === 0 || items.some(item => !item.hidden);
417
+ }
418
+ if (sync) syncState();
419
+ };
420
+
421
+ for (const tree of trees) {
422
+ const buttons = [...tree.querySelectorAll('[data-file-tree-view]')];
423
+ for (const button of buttons) {
424
+ button.addEventListener('click', () => applyView(button.dataset.fileTreeView));
425
+ button.addEventListener('keydown', (event) => {
426
+ if (!['ArrowLeft', 'ArrowRight'].includes(event.key)) return;
427
+ event.preventDefault();
428
+ const direction = event.key === 'ArrowRight' ? 1 : -1;
429
+ const next = buttons[(buttons.indexOf(button)+direction+buttons.length)%buttons.length];
430
+ applyView(next.dataset.fileTreeView);
431
+ next.focus();
432
+ });
433
+ }
434
+ }
435
+
201
436
  applySidebarState(sidebarPreferenceOpen && !compact.matches);
437
+ setPopoverOpen(pageUrl.searchParams.get(popoverParameter) === 'open' && sidebar.hidden, false, false);
438
+ applyView(activeView, false);
202
439
  syncState();
203
440
 
204
441
  popoverToggle?.addEventListener('click', (event) => {
@@ -208,7 +445,7 @@ export function renderFileTreeScript(enabled) {
208
445
  sidebarOpenButton?.addEventListener('click', () => setSidebarOpen(true));
209
446
  sidebarCloseButton?.addEventListener('click', () => setSidebarOpen(false));
210
447
  document.addEventListener('pointerdown', (event) => {
211
- if (popover.hidden || event.target.closest('.file-navigation,.file-sidebar')) return;
448
+ if (popover.hidden || event.target.closest('.site-header-document-meta,.file-sidebar')) return;
212
449
  setPopoverOpen(false);
213
450
  });
214
451
  document.addEventListener('keydown', (event) => {
@@ -253,50 +490,68 @@ export function renderFileTreeScript(enabled) {
253
490
  }, 1000);
254
491
  });
255
492
 
256
- for (const tree of trees) {
257
- const input = tree.querySelector('.file-tree-filter-input');
493
+ const filterTree = (tree, query) => {
258
494
  const empty = tree.querySelector('.file-tree-filter-empty');
259
495
  const root = tree.querySelector('.file-tree-root');
260
496
  const treeDirectories = [...root.querySelectorAll('.file-tree-directory')];
261
- const filter = () => {
262
- const query = input.value.trim().toLocaleLowerCase();
263
- const files = [...root.querySelectorAll('.file-tree-file')];
264
- let matches = 0;
265
- for (const file of files) {
266
- const name = file.querySelector('a').dataset.fileName.toLocaleLowerCase();
267
- const visible = query === '' || name.includes(query);
268
- file.hidden = !visible;
269
- if (visible) matches += 1;
270
- }
271
- for (const directory of [...treeDirectories].reverse()) {
272
- const details = directory.querySelector(':scope > details');
273
- const hasVisibleFile = [...details.querySelectorAll('.file-tree-file')].some((file) => !file.hidden);
274
- directory.hidden = !hasVisibleFile;
275
- const open = query === '' ? initialOpenState.get(details.dataset.folderId) : hasVisibleFile;
276
- if (details.open !== open) {
277
- ignoredToggles.add(details);
278
- details.open = open;
279
- }
497
+ const files = [...root.querySelectorAll('.file-tree-file')];
498
+ let treeMatches = 0;
499
+ for (const file of files) {
500
+ const name = file.querySelector('a').dataset.fileName.toLocaleLowerCase();
501
+ const visible = query === '' || name.includes(query);
502
+ file.hidden = !visible;
503
+ if (visible) treeMatches += 1;
504
+ }
505
+ for (const directory of [...treeDirectories].reverse()) {
506
+ const details = directory.querySelector(':scope > details');
507
+ const hasVisibleFile = [...details.querySelectorAll('.file-tree-file')].some((file) => !file.hidden);
508
+ directory.hidden = !hasVisibleFile;
509
+ const open = query === '' ? initialOpenState.get(details.dataset.folderId) : hasVisibleFile;
510
+ if (details.open !== open) {
511
+ ignoredToggles.add(details);
512
+ details.open = open;
280
513
  }
281
- empty.hidden = matches !== 0;
282
- };
283
- input.addEventListener('input', filter);
514
+ }
515
+ const recentFiles = [...tree.querySelectorAll('.file-tree-recent-file')];
516
+ let recentMatches = 0;
517
+ for (const file of recentFiles) {
518
+ const visible = query === '' || file.dataset.filePath.toLocaleLowerCase().includes(query);
519
+ file.hidden = !visible;
520
+ if (visible) recentMatches += 1;
521
+ }
522
+ for (const heading of tree.querySelectorAll('.file-tree-date'))
523
+ heading.hidden = !recentFiles.some(file => file.dataset.localDate === heading.dataset.date && !file.hidden);
524
+ for (const heading of tree.querySelectorAll('.file-tree-directory-group'))
525
+ heading.hidden = !recentFiles.some(file => file.dataset.recentGroup === heading.dataset.recentGroup && !file.hidden);
526
+ const matches = activeView === 'tree' ? treeMatches : recentMatches;
527
+ empty.hidden = query === '' || matches !== 0;
528
+ };
529
+
530
+ const applyFilter = (value) => {
531
+ const query = value.trim().toLocaleLowerCase();
532
+ for (const tree of trees) {
533
+ tree.querySelector('.file-tree-filter-input').value = value;
534
+ filterTree(tree, query);
535
+ }
536
+ applyRecentDateState(query !== '');
537
+ };
538
+
539
+ for (const tree of trees) {
540
+ const input = tree.querySelector('.file-tree-filter-input');
541
+ input.addEventListener('input', () => applyFilter(input.value));
284
542
  input.addEventListener('keydown', (event) => {
285
543
  if (event.key !== 'Escape' || input.value === '') return;
286
- input.value = '';
287
- filter();
544
+ applyFilter('');
288
545
  });
289
546
  }
290
547
  })();
291
548
  </script>`;
292
549
  }
293
- export function renderBreadcrumbs(breadcrumbs, modifiedAt) {
294
- const timestamp = renderModifiedAt(modifiedAt);
550
+ export function renderBreadcrumbs(breadcrumbs) {
295
551
  if (!breadcrumbs || breadcrumbs.length === 0)
296
552
  return `<nav class="file-breadcrumbs" aria-label="パンくずリスト">
297
553
  <button type="button" class="file-sidebar-open" data-file-sidebar-open aria-expanded="true" aria-controls="file-sidebar" aria-label="ファイルサイドバーを開く" title="ファイルサイドバーを開く" hidden><svg class="file-sidebar-toggle-icon" viewBox="0 0 16 16" aria-hidden="true"><rect x="1.75" y="2.25" width="12.5" height="11.5" rx="1.5" /><path d="M6 2.5v11M8 5.5L10.5 8 8 10.5" /></svg></button>
298
554
  <button type="button" class="file-tree-popover-toggle" data-file-tree-toggle aria-expanded="false" aria-controls="file-tree-popover" aria-haspopup="true" aria-label="ファイルを開く" title="ファイルを開く"><span>ファイル</span><svg class="panel-toggle-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4" /></svg></button>
299
- ${timestamp}
300
555
  </nav>
301
556
  `;
302
557
  const current = breadcrumbs.find((breadcrumb) => breadcrumb.current);
@@ -322,7 +577,6 @@ ${items}
322
577
  <button type="button" class="file-sidebar-open" data-file-sidebar-open aria-expanded="true" aria-controls="file-sidebar" aria-label="ファイルサイドバーを開く" title="ファイルサイドバーを開く" hidden><svg class="file-sidebar-toggle-icon" viewBox="0 0 16 16" aria-hidden="true"><rect x="1.75" y="2.25" width="12.5" height="11.5" rx="1.5" /><path d="M6 2.5v11M8 5.5L10.5 8 8 10.5" /></svg></button>
323
578
  ${trail} <button type="button" class="file-tree-popover-toggle" data-file-tree-toggle aria-expanded="false" aria-controls="file-tree-popover" aria-haspopup="true" aria-label="ファイルを開く" title="ファイルを開く"><span>${popoverLabel}</span><svg class="panel-toggle-icon" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 6l4 4 4-4" /></svg></button>
324
579
  <button type="button" class="copy-file-path" data-copy-file-path="${escapeHtml(path)}" aria-label="ファイルパスをコピー" title="ファイルパスをコピー">${renderCopyIcon()}</button>
325
- ${timestamp}
326
580
  </nav>
327
581
  `;
328
582
  }
@@ -332,7 +586,7 @@ export function renderModifiedAt(modifiedAt) {
332
586
  const date = new Date(modifiedAt);
333
587
  if (Number.isNaN(date.getTime()))
334
588
  throw new Error(`Invalid modifiedAt timestamp: ${modifiedAt}`);
335
- const label = date.toISOString().slice(0, 19).replace("T", " ");
589
+ const label = date.toISOString().slice(0, 16).replace("T", " ");
336
590
  return `<time class="document-modified" datetime="${date.toISOString()}">更新 ${label}</time>`;
337
591
  }
338
592
  export function renderModifiedAtScript(enabled) {
@@ -342,6 +596,6 @@ export function renderModifiedAtScript(enabled) {
342
596
  const element=document.querySelector('.document-modified');if(!element)return;
343
597
  const date=new Date(element.dateTime);if(Number.isNaN(date.getTime()))return;
344
598
  const pad=value=>String(value).padStart(2,'0');
345
- element.textContent='更新 '+date.getFullYear()+'-'+pad(date.getMonth()+1)+'-'+pad(date.getDate())+' '+pad(date.getHours())+':'+pad(date.getMinutes())+':'+pad(date.getSeconds());
599
+ element.textContent='更新 '+date.getFullYear()+'-'+pad(date.getMonth()+1)+'-'+pad(date.getDate())+' '+pad(date.getHours())+':'+pad(date.getMinutes());
346
600
  })()</script>`;
347
601
  }
@@ -1,4 +1,10 @@
1
1
  import type { DocumentFeature } from "../types.js";
2
2
  /** Assets owned by the generated document header. */
3
3
  export type HeaderFeature = DocumentFeature;
4
- export declare function createHeaderFeature(): HeaderFeature;
4
+ interface HeaderFeatureOptions {
5
+ documentNavigation: string;
6
+ documentMetadata: string;
7
+ fileTree: string;
8
+ }
9
+ export declare function createHeaderFeature(options?: HeaderFeatureOptions): HeaderFeature;
10
+ export {};
@@ -1,18 +1,26 @@
1
- export function createHeaderFeature() {
1
+ export function createHeaderFeature(options = {
2
+ documentNavigation: "",
3
+ documentMetadata: "",
4
+ fileTree: "",
5
+ }) {
2
6
  const markup = `<header class="site-header">
3
- <div class="site-header-brand"><strong>marksites</strong></div>
7
+ <div class="site-header-brand">
8
+ <strong class="site-header-logo">marksites</strong>
9
+ <div class="site-header-document-meta">
10
+ ${options.documentNavigation}${options.documentMetadata ? ` <span class="site-header-metadata-separator" aria-hidden="true"></span>\n <div class="document-metadata">${options.documentMetadata}</div>\n` : ""}${options.fileTree} </div>
11
+ </div>
4
12
  <div class="site-header-actions">
5
13
  <button type="button" class="site-header-action" data-theme-toggle aria-label="ダークモードに切り替え" title="ダークモードに切り替え"><svg data-theme-dark-icon viewBox="0 0 16 16" aria-hidden="true"><path d="M13.5 10.2A5.8 5.8 0 015.8 2.5 5.8 5.8 0 1013.5 10.2z" /></svg><svg data-theme-light-icon viewBox="0 0 16 16" aria-hidden="true" hidden><circle cx="8" cy="8" r="2.5"/><path d="M8 1v1.5M8 13.5V15M1 8h1.5M13.5 8H15M3.05 3.05l1.06 1.06M11.89 11.89l1.06 1.06M12.95 3.05l-1.06 1.06M4.11 11.89l-1.06 1.06"/></svg></button>
6
14
  <button type="button" class="site-header-action language-toggle" data-language-toggle aria-label="英語に切り替え" title="英語に切り替え"><svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6"/><path d="M2 8h12M8 2a9 9 0 010 12M8 2a9 9 0 000 12"/></svg><span data-language-label>JA</span></button>
7
15
  </div>
8
16
  </header>`;
9
17
  const styles = `
10
- .site-header{position:fixed;z-index:40;top:0;right:0;left:0;box-sizing:border-box;display:flex;height:56px;align-items:center;justify-content:space-between;gap:16px;padding:0 16px;color:var(--fgColor-default,#1f2328);background:var(--bgColor-default,#fff);border-bottom:1px solid var(--borderColor-muted,#d8dee4)}body.markdown-body.has-file-tree .site-header{padding-left:56px}
11
- .site-header-brand{display:flex;min-width:0;align-items:center}.site-header-brand strong{font-size:.9375rem}.site-header-actions{display:flex;flex:none;align-items:center;gap:6px}.site-header-action{display:inline-flex;width:34px;height:34px;align-items:center;justify-content:center;gap:3px;padding:0;color:var(--fgColor-muted,#59636e);font:inherit;background:transparent;border:0;border-radius:6px;cursor:pointer}.site-header-action:hover{color:var(--fgColor-default,#1f2328);background:var(--button-default-bgColor-hover,#eaeef2)}.site-header-action:focus-visible{outline:2px solid var(--focus-outlineColor,#0969da);outline-offset:2px}.site-header-action svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.25;stroke-linecap:round;stroke-linejoin:round}.site-header-action svg[hidden]{display:none}.site-header-action.language-toggle{width:auto;min-width:48px;padding:0 7px}.language-toggle span{font-size:.6875rem;font-weight:700;line-height:1}
12
- body.markdown-body,body.markdown-body.has-file-tree,body.markdown-body.has-file-tree.file-sidebar-collapsed{padding-top:88px}.document-sidebar{top:88px;max-height:calc(100vh - 120px)}.file-sidebar{top:56px}.markdown-content :is(h1,h2,h3,h4,h5,h6){scroll-margin-top:88px}
18
+ .site-header{position:fixed;z-index:40;top:0;right:0;left:0;box-sizing:border-box;display:flex;height:56px;align-items:center;gap:16px;padding:0 16px;color:var(--fgColor-default,#1f2328);background:var(--bgColor-default,#fff);border-bottom:1px solid var(--borderColor-muted,#d8dee4)}body.markdown-body.has-file-tree .site-header{padding-left:56px}
19
+ .site-header-brand{display:flex;min-width:0;flex:1;align-items:center;gap:10px}.site-header-logo{display:inline-flex;height:26px;flex:none;align-items:center;padding:0 9px;color:#fff;font-size:.75rem;line-height:26px;background:#24292f;border-radius:6px}.site-header-document-meta{position:relative;display:flex;min-width:0;align-items:center}.site-header-document-meta .file-breadcrumbs{height:28px;min-width:0;align-items:center;padding:0;border:0}.site-header-document-meta .file-breadcrumbs ol{min-height:20px;align-items:center}.site-header-document-meta .file-breadcrumbs li,.site-header-document-meta .file-breadcrumbs a,.site-header-document-meta .file-breadcrumb-separator,.site-header-document-meta .file-tree-popover-toggle{height:20px;font-size:.8125rem;line-height:20px}.site-header-document-meta .file-tree-popover-toggle{top:0}.site-header-document-meta .copy-file-path{width:22px;height:20px}.site-header-metadata-separator{width:1px;height:16px;flex:none;margin:0 10px;background:var(--borderColor-default,#d0d7de)}.site-header-document-meta .document-metadata{display:flex;height:28px;flex:none;align-items:center;margin:0}.site-header-document-meta .document-modified{color:var(--fgColor-muted,#59636e);font-size:.6875rem;line-height:20px;white-space:nowrap}.site-header-actions{display:flex;flex:none;align-items:center;gap:6px}.site-header-action{display:inline-flex;width:34px;height:34px;align-items:center;justify-content:center;gap:3px;padding:0;color:var(--fgColor-muted,#59636e);font:inherit;background:transparent;border:0;border-radius:6px;cursor:pointer}.site-header-action:hover{color:var(--fgColor-default,#1f2328);background:var(--button-default-bgColor-hover,#eaeef2)}.site-header-action:focus-visible{outline:2px solid var(--focus-outlineColor,#0969da);outline-offset:2px}.site-header-action svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.25;stroke-linecap:round;stroke-linejoin:round}.site-header-action svg[hidden]{display:none}.site-header-action.language-toggle{width:auto;min-width:48px;padding:0 7px}.language-toggle span{font-size:.6875rem;font-weight:700;line-height:1}
20
+ body.markdown-body,body.markdown-body.has-file-tree,body.markdown-body.has-file-tree.file-sidebar-collapsed{padding-top:88px}.document-sidebar{top:88px;height:calc(100vh - 120px);max-height:calc(100vh - 120px)}.file-sidebar{top:56px}.markdown-content :is(h1,h2,h3,h4,h5,h6){scroll-margin-top:88px}
13
21
  body.markdown-body[data-theme="dark"]{color-scheme:dark;color:#f0f6fc;background:#0d1117;--fgColor-default:#f0f6fc;--fgColor-muted:#a3abb5;--fgColor-accent:#58a6ff;--fgColor-danger:#ff7b72;--fgColor-attention:#e3b341;--fgColor-success:#56d364;--bgColor-default:#0d1117;--bgColor-muted:#161b22;--bgColor-neutral-muted:#6e768166;--bgColor-accent-muted:#1f6feb40;--bgColor-attention-muted:#bb800940;--bgColor-danger-muted:#67060c;--borderColor-default:#484f58;--borderColor-muted:#484f58cc;--borderColor-accent-emphasis:#388bfd;--button-default-bgColor-rest:#21262d;--button-default-bgColor-hover:#30363d;--focus-outlineColor:#58a6ff;--codeBlock-bgColor:#161b22;--codeBlock-fgColor:#e6edf3;--annotation-highlight-bgColor:#4d3b05;--annotation-highlight-borderColor:#e3b341;--color-prettylights-syntax-comment:#a3abb5;--color-prettylights-syntax-constant:#79c0ff;--color-prettylights-syntax-entity:#d2a8ff;--color-prettylights-syntax-entity-tag:#7ee787;--color-prettylights-syntax-keyword:#ff7b72;--color-prettylights-syntax-string:#a5d6ff;--color-prettylights-syntax-variable:#ffa657}
14
22
  body.markdown-body[data-theme="light"]{color-scheme:light;color:#1f2328;background:#fff;--fgColor-default:#1f2328;--fgColor-muted:#59636e;--fgColor-accent:#0969da;--fgColor-danger:#d1242f;--fgColor-attention:#9a6700;--fgColor-success:#1a7f37;--bgColor-default:#fff;--bgColor-muted:#f6f8fa;--bgColor-neutral-muted:#818b981f;--bgColor-accent-muted:#ddf4ff;--bgColor-attention-muted:#fff8c5;--bgColor-danger-muted:#ffebe9;--borderColor-default:#d0d7de;--borderColor-muted:#d8dee4;--borderColor-accent-emphasis:#0969da;--button-default-bgColor-rest:#f6f8fa;--button-default-bgColor-hover:#eaeef2;--focus-outlineColor:#0969da;--codeBlock-bgColor:#fff;--codeBlock-fgColor:#24292f;--annotation-highlight-bgColor:#fff8c5;--annotation-highlight-borderColor:#bf8700}
15
- @media(max-width:900px){body.markdown-body,body.markdown-body.has-file-tree,body.markdown-body.has-file-tree.file-sidebar-collapsed{padding-top:68px}.document-sidebar{top:68px;max-height:calc(100vh - 80px)}.file-sidebar{top:56px}.markdown-content :is(h1,h2,h3,h4,h5,h6){scroll-margin-top:68px}}
23
+ @media(max-width:900px){.site-header{gap:10px;padding-right:10px}.site-header-document-meta .file-breadcrumbs ol,.site-header-document-meta .file-breadcrumb-separator,.site-header-document-meta .copy-file-path,.site-header-metadata-separator,.site-header-document-meta .document-metadata{display:none}body.markdown-body,body.markdown-body.has-file-tree,body.markdown-body.has-file-tree.file-sidebar-collapsed{padding-top:68px}.document-sidebar{top:68px;height:calc(100vh - 80px);max-height:calc(100vh - 80px)}.file-sidebar{top:56px}.markdown-content :is(h1,h2,h3,h4,h5,h6){scroll-margin-top:68px}}
16
24
  `;
17
25
  const script = `<script>(()=>{
18
26
  const languageParameter='lang',themeParameter='theme',pageUrl=new URL(location.href),textOriginal=new WeakMap(),attributeOriginal=new WeakMap();
@@ -21,7 +29,7 @@ const english=new Map(${JSON.stringify([
21
29
  ["追加", "Add"], ["コピー", "Copy"], ["コメントを追加", "Add comment"], ["コメントをすべてコピー", "Copy all comments"], ["コメントはありません。", "No comments."], ["このページにコメントはありません。", "No comments on this page."],
22
30
  ["コピー", "Copy"], ["AI向けコピー", "Copy for AI"], ["折り返す", "Wrap"], ["コピーしました", "Copied"], ["保存", "Save"], ["キャンセル", "Cancel"],
23
31
  ["引用", "Quote"], ["引用先なし", "Quote unavailable"], ["文書全体", "Whole document"],
24
- ["ファイルを検索", "Filter files"], ["一致するファイルはありません", "No matching files"],
32
+ ["ファイルを検索", "Filter files"], ["一致するファイルはありません", "No matching files"], ["ファイル表示", "File view"], ["ツリー", "Tree"], ["更新順", "Recently updated"], ["更新日時のあるファイルはありません", "No files with update times"],
25
33
  ["ファイルサイドバーを開く", "Open file sidebar"], ["ファイルサイドバーを閉じる", "Close file sidebar"], ["ファイルを開く", "Open files"], ["ファイルを閉じる", "Close files"],
26
34
  ["ファイル名で検索", "Filter files by name"], ["ファイルパスをコピー", "Copy file path"], ["ファイルパスをコピーしました", "File path copied"], ["ファイルパスをコピーできませんでした", "Could not copy file path"],
27
35
  ["選択範囲の操作", "Selection actions"], ["選択範囲をコピー", "Copy selection"], ["AI向けの形式でコピー", "Copy for AI"], ["選択範囲にコメントを追加", "Add comment to selection"], ["文書全体にコメントを追加", "Add comment to document"],
@@ -32,8 +40,8 @@ const english=new Map(${JSON.stringify([
32
40
  ["コードをコピー", "Copy code"], ["長い行を折り返す", "Wrap long lines"], ["折り返しを解除", "Disable line wrapping"], ["コードをコピーしました", "Code copied"], ["コードをコピーできませんでした", "Could not copy code"],
33
41
  ["文書ナビゲーション", "Document navigation"], ["文書サイドバー", "Document sidebar"], ["パンくずリスト", "Breadcrumbs"]
34
42
  ])});
35
- const skipText=node=>{const parent=node.parentElement;if(!parent)return true;if(parent.closest('.file-tree-name,.file-tree summary span,.file-breadcrumbs ol,.table-of-contents a,.annotation-card>p,.annotation-quote,.annotation-source,.code-language'))return true;return Boolean(parent.closest('.markdown-content')&&!parent.closest('.file-navigation,.code-toolbar'))};
36
- const translate=value=>{if(english.has(value))return english.get(value);if(value.startsWith('更新 '))return'Updated '+value.slice(3);if(/^コメント\d+件$/.test(value))return value.replace(/^コメント(\d+)件$/,'$1 comments');return value};
43
+ const skipText=node=>{const parent=node.parentElement;if(!parent)return true;if(parent.closest('.file-tree-name,.file-tree summary span,.file-breadcrumbs ol,.table-of-contents a,.annotation-card>p,.annotation-quote,.annotation-source,.code-language'))return true;return Boolean(parent.closest('.markdown-content')&&!parent.closest('.code-toolbar'))};
44
+ const translate=value=>{if(english.has(value))return english.get(value);if(value.startsWith('更新 '))return'Updated '+value.slice(3);if(/^(\d{4})年(\d{1,2})月(\d{1,2})日$/.test(value))return value.replace(/^(\d{4})年(\d{1,2})月(\d{1,2})日$/,'$1-$2-$3');if(/^コメント\d+件$/.test(value))return value.replace(/^コメント(\d+)件$/,'$1 comments');if(/^ファイル\d+件$/.test(value))return value.replace(/^ファイル(\d+)件$/,'$1 files');return value};
37
45
  function applyNode(node,language){if(node.nodeType===Node.TEXT_NODE){if(skipText(node))return;if(!textOriginal.has(node))textOriginal.set(node,node.data);const original=textOriginal.get(node),trimmed=original.trim(),next=language==='en'?original.replace(trimmed,translate(trimmed)):original;if(node.data!==next)node.data=next;return}if(node.nodeType!==Node.ELEMENT_NODE)return;const element=node,attributes=['aria-label','title','placeholder'];let originals=attributeOriginal.get(element);if(!originals){originals=new Map();attributeOriginal.set(element,originals)}for(const name of attributes){if(!element.hasAttribute(name)&&!originals.has(name))continue;if(!originals.has(name))originals.set(name,element.getAttribute(name));const original=originals.get(name),next=language==='en'?translate(original):original;if(element.getAttribute(name)!==next)element.setAttribute(name,next)}for(const child of element.childNodes)applyNode(child,language)}
38
46
  function applyLanguage(language){document.documentElement.lang=language;document.body.dataset.language=language;for(const root of document.querySelectorAll('.site-header,.file-sidebar,.file-navigation,.document-sidebar,.selection-actions,.code-toolbar'))applyNode(root,language);const button=document.querySelector('[data-language-toggle]'),label=document.querySelector('[data-language-label]'),next=language==='ja'?'en':'ja';label.textContent=language==='ja'?'JA':'EN';button.setAttribute('aria-label',next==='en'?'英語に切り替え':'Switch to Japanese');button.title=button.getAttribute('aria-label')}
39
47
  function syncLinks(){const language=document.body.dataset.language,theme=document.body.dataset.theme;const update=url=>{url.searchParams.delete(languageParameter);if(language==='en')url.searchParams.set(languageParameter,'en');url.searchParams.set(themeParameter,theme);return url};history.replaceState(null,'',update(new URL(location.href)));for(const link of document.querySelectorAll('a[href]')){const raw=link.getAttribute('href');if(!raw||raw.startsWith('#'))continue;const url=new URL(raw,location.href);if(url.protocol===location.protocol&&url.host===location.host&&url.pathname.endsWith('.html'))link.href=update(url).href}}
@@ -0,0 +1,6 @@
1
+ export interface ImageViewerFeature {
2
+ markup: string;
3
+ styles: string;
4
+ script: string;
5
+ }
6
+ export declare function createImageViewerFeature(enabled: boolean): ImageViewerFeature;
@@ -0,0 +1,28 @@
1
+ export function createImageViewerFeature(enabled) {
2
+ if (!enabled)
3
+ return { markup: "", styles: "", script: "" };
4
+ const markup = `<div class="image-viewer" data-image-viewer hidden role="dialog" aria-modal="true" aria-label="画像ビューアー">
5
+ <div class="image-viewer-toolbar">
6
+ <button type="button" data-image-zoom-out aria-label="縮小" title="縮小">−</button>
7
+ <button type="button" data-image-reset aria-label="元の大きさ" title="元の大きさ">100%</button>
8
+ <button type="button" data-image-zoom-in aria-label="拡大" title="拡大">+</button>
9
+ <button type="button" data-image-close aria-label="閉じる" title="閉じる">×</button>
10
+ </div>
11
+ <div class="image-viewer-stage" data-image-stage><img data-image-viewer-image alt=""></div>
12
+ </div>`;
13
+ const styles = `
14
+ .markdown-content img { cursor: zoom-in; }
15
+ .markdown-content img:focus-visible { outline: 2px solid var(--focus-outlineColor, #0969da); outline-offset: 3px; }
16
+ .image-viewer[hidden] { display: none; }
17
+ .image-viewer { position: fixed; z-index: 1000; inset: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); color: #fff; background: rgba(0,0,0,.9); }
18
+ .image-viewer-toolbar { display: flex; justify-content: flex-end; gap: 8px; padding: 12px; }
19
+ .image-viewer-toolbar button { min-width: 40px; height: 40px; padding: 0 10px; color: #fff; font: inherit; font-weight: 600; background: rgba(255,255,255,.12); border: 1px solid rgba(255,255,255,.3); border-radius: 6px; cursor: pointer; }
20
+ .image-viewer-toolbar button:hover { background: rgba(255,255,255,.22); }
21
+ .image-viewer-toolbar button:focus-visible { outline: 2px solid #58a6ff; outline-offset: 2px; }
22
+ .image-viewer-stage { min-width: 0; min-height: 0; overflow: hidden; cursor: grab; touch-action: none; user-select: none; }
23
+ .image-viewer-stage.is-dragging { cursor: grabbing; }
24
+ .image-viewer-stage img { display: block; max-width: none; max-height: none; transform-origin: center; pointer-events: none; }
25
+ body.has-open-image-viewer { overflow: hidden; }`;
26
+ const script = `<script>(()=>{const viewer=document.querySelector('[data-image-viewer]'),stage=viewer?.querySelector('[data-image-stage]'),image=viewer?.querySelector('[data-image-viewer-image]');if(!(viewer instanceof HTMLElement)||!(stage instanceof HTMLElement)||!(image instanceof HTMLImageElement))return;let scale=1,x=0,y=0,drag=null,opener=null;const draw=()=>{image.style.transform='translate('+x+'px,'+y+'px) scale('+scale+')';};const reset=()=>{scale=1;x=(stage.clientWidth-image.naturalWidth)/2;y=(stage.clientHeight-image.naturalHeight)/2;draw();};const close=()=>{viewer.hidden=true;document.body.classList.remove('has-open-image-viewer');if(opener instanceof HTMLElement)opener.focus();};const open=target=>{opener=target;image.src=target.currentSrc||target.src;image.alt=target.alt;viewer.hidden=false;document.body.classList.add('has-open-image-viewer');image.onload=reset;viewer.querySelector('[data-image-close]')?.focus();};document.querySelectorAll('.markdown-content img').forEach(target=>{target.tabIndex=0;target.setAttribute('role','button');target.setAttribute('aria-label',(target.alt||'画像')+'を拡大表示');target.addEventListener('click',()=>open(target));target.addEventListener('keydown',event=>{if(event.key==='Enter'||event.key===' '){event.preventDefault();open(target);}});});viewer.querySelector('[data-image-close]')?.addEventListener('click',close);viewer.querySelector('[data-image-reset]')?.addEventListener('click',reset);const zoom=amount=>{scale=Math.min(8,Math.max(.1,scale*amount));draw();};viewer.querySelector('[data-image-zoom-in]')?.addEventListener('click',()=>zoom(1.25));viewer.querySelector('[data-image-zoom-out]')?.addEventListener('click',()=>zoom(.8));stage.addEventListener('wheel',event=>{event.preventDefault();zoom(event.deltaY<0?1.15:.87);},{passive:false});stage.addEventListener('pointerdown',event=>{drag={id:event.pointerId,x:event.clientX,y:event.clientY,ox:x,oy:y};stage.setPointerCapture(event.pointerId);stage.classList.add('is-dragging');});stage.addEventListener('pointermove',event=>{if(!drag||drag.id!==event.pointerId)return;x=drag.ox+event.clientX-drag.x;y=drag.oy+event.clientY-drag.y;draw();});const end=event=>{if(drag?.id===event.pointerId){drag=null;stage.classList.remove('is-dragging');}};stage.addEventListener('pointerup',end);stage.addEventListener('pointercancel',end);document.addEventListener('keydown',event=>{if(!viewer.hidden&&event.key==='Escape')close();});})();</script>`;
27
+ return { markup, styles, script };
28
+ }
@@ -23,9 +23,9 @@ ${tableOfContents}${annotations}
23
23
  </div>
24
24
  </aside>`;
25
25
  const styles = `
26
- .document-sidebar{grid-area:toc;position:sticky;top:32px;box-sizing:border-box;display:flex;max-height:calc(100vh - 64px);min-height:0;flex-direction:column;border:1px solid var(--borderColor-muted,#d8dee4);border-radius:8px;background:var(--bgColor-default,#fff);overflow:hidden}
26
+ .document-sidebar{grid-area:toc;position:sticky;top:32px;box-sizing:border-box;display:flex;height:calc(100vh - 64px);max-height:calc(100vh - 64px);min-height:0;flex-direction:column;border:1px solid var(--borderColor-muted,#d8dee4);border-radius:8px;background:var(--bgColor-default,#fff);overflow:hidden}
27
27
  .sidebar-toggle{display:none}
28
- .document-sidebar-body{display:flex;min-height:0;flex:0 1 auto;flex-direction:column;overflow:hidden}
28
+ .document-sidebar-body{display:flex;min-height:0;flex:1 1 auto;flex-direction:column;overflow:hidden}
29
29
  .sidebar-tabs{display:flex;flex:none;gap:4px;padding:10px 10px 0;border-bottom:1px solid var(--borderColor-muted,#d8dee4)}
30
30
  .sidebar-tab{position:relative;min-width:0;flex:1;padding:8px 6px 10px;color:var(--fgColor-muted,#59636e);font:inherit;font-size:.8125rem;font-weight:600;line-height:1.25;background:transparent;border:0;cursor:pointer}
31
31
  .sidebar-tab::after{position:absolute;right:4px;bottom:-1px;left:4px;height:2px;background:transparent;content:""}
@@ -34,16 +34,18 @@ ${tableOfContents}${annotations}
34
34
  .sidebar-tab[aria-selected="true"]{color:var(--fgColor-default,#1f2328)}
35
35
  .sidebar-tab[aria-selected="true"]::after{background:var(--borderColor-accent-emphasis,#0969da)}
36
36
  .sidebar-count{display:inline-flex;min-width:18px;height:18px;align-items:center;justify-content:center;margin-left:3px;padding:0 4px;color:var(--fgColor-muted,#59636e);font-size:.6875rem;line-height:18px;background:var(--bgColor-muted,#f6f8fa);border-radius:9px}
37
- .sidebar-panels{display:flex;min-height:0;flex:0 1 auto;align-items:flex-start;overflow:hidden}
38
- .sidebar-panel{box-sizing:border-box;width:100%;max-height:100%;min-height:0;overflow:auto;padding:12px;scrollbar-width:thin;scrollbar-color:var(--borderColor-default,#d0d7de) transparent}
37
+ .sidebar-panels{display:flex;min-height:0;flex:1 1 auto;align-items:stretch;overflow:hidden}
38
+ .sidebar-panel{box-sizing:border-box;width:100%;height:100%;min-height:0;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;padding:12px;scrollbar-width:thin;scrollbar-color:var(--borderColor-default,#d0d7de) transparent}
39
39
  .sidebar-panel[hidden],.document-sidebar-body[hidden]{display:none}
40
40
  @media(max-width:900px){.document-sidebar{z-index:10;top:12px;width:auto;max-height:calc(100vh - 24px);box-shadow:0 4px 12px rgba(31,35,40,.08)}.sidebar-toggle{box-sizing:border-box;display:flex;width:100%;min-height:44px;flex:none;align-items:center;justify-content:space-between;padding:8px 12px;color:var(--fgColor-default,#1f2328);font:inherit;font-size:.9375rem;font-weight:700;background:transparent;border:0;cursor:pointer}.sidebar-toggle:focus-visible{outline:2px solid var(--focus-outlineColor,#0969da);outline-offset:-2px}.sidebar-toggle[aria-expanded="false"] .panel-toggle-icon{transform:rotate(-90deg)}.sidebar-tabs{padding-top:0}}
41
41
  @media(prefers-reduced-motion:reduce){.panel-toggle-icon{transition:none}}`;
42
42
  const script = `<script>(()=>{
43
43
  const sidebar=document.querySelector('.document-sidebar');if(!sidebar)return;
44
44
  const tabs=[...sidebar.querySelectorAll('[data-sidebar-tab]')],panels=[...sidebar.querySelectorAll('.sidebar-panel')],toggle=sidebar.querySelector('.sidebar-toggle'),body=sidebar.querySelector('.document-sidebar-body'),label=sidebar.querySelector('[data-sidebar-toggle-label]'),compact=matchMedia('(max-width: 900px)');
45
- let active=${JSON.stringify(initialPanel)};
46
- function activate(name,focus=false){const tab=tabs.find(item=>item.dataset.sidebarTab===name);if(!tab)return;active=name;for(const item of tabs){const selected=item===tab;item.setAttribute('aria-selected',String(selected));item.tabIndex=selected?0:-1}for(const panel of panels)panel.hidden=panel.id!=='sidebar-panel-'+name;label.textContent=tab.childNodes[0].textContent.trim();if(compact.matches){body.hidden=false;toggle.setAttribute('aria-expanded','true')}if(focus)tab.focus()}
45
+ const tabParameter='sidebar-tab',pageUrl=new URL(location.href),requestedTab=pageUrl.searchParams.get(tabParameter);
46
+ let active=tabs.some(item=>item.dataset.sidebarTab===requestedTab)?requestedTab:${JSON.stringify(initialPanel)};
47
+ function syncActiveTab(){const updateUrl=url=>{url.searchParams.delete(tabParameter);if(active==='comments')url.searchParams.set(tabParameter,active);return url};history.replaceState(null,'',updateUrl(new URL(location.href)));for(const link of document.querySelectorAll('a[href]')){const rawHref=link.getAttribute('href');if(!rawHref||rawHref.startsWith('#'))continue;const url=new URL(rawHref,location.href);if(url.protocol!==location.protocol||url.host!==location.host||!url.pathname.endsWith('.html'))continue;link.href=updateUrl(url).href}}
48
+ function activate(name,focus=false){const tab=tabs.find(item=>item.dataset.sidebarTab===name);if(!tab)return;active=name;for(const item of tabs){const selected=item===tab;item.setAttribute('aria-selected',String(selected));item.tabIndex=selected?0:-1}for(const panel of panels)panel.hidden=panel.id!=='sidebar-panel-'+name;label.textContent=tab.childNodes[0].textContent.trim();if(compact.matches){body.hidden=false;toggle.setAttribute('aria-expanded','true')}syncActiveTab();if(focus)tab.focus()}
47
49
  function setExpanded(expanded){toggle.setAttribute('aria-expanded',String(expanded));body.hidden=!expanded}
48
50
  tabs.forEach((tab,index)=>{tab.addEventListener('click',()=>activate(tab.dataset.sidebarTab));tab.addEventListener('keydown',event=>{if(event.key!=='ArrowLeft'&&event.key!=='ArrowRight')return;event.preventDefault();const step=event.key==='ArrowRight'?1:-1;activate(tabs[(index+step+tabs.length)%tabs.length].dataset.sidebarTab,true)})});
49
51
  toggle.addEventListener('click',()=>setExpanded(toggle.getAttribute('aria-expanded')!=='true'));