@dirwell/theme-ledger 0.0.2 → 0.0.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.
@@ -75,7 +75,7 @@
75
75
  let offsets = $derived.by(() => {
76
76
  const values = [0];
77
77
  for (const entry of rows)
78
- values.push(values.at(-1) + (heights.get(entry.id) ?? 76));
78
+ values.push(values.at(-1) + (heights.get(entry.id) ?? 52));
79
79
  return values;
80
80
  });
81
81
  let start = $derived.by(() => {
@@ -100,14 +100,53 @@
100
100
  }
101
101
  return Math.min(rows.length, Math.max(start + 35, low + 5));
102
102
  });
103
+ function stickyLayout(node) {
104
+ const heading = node.querySelector(":scope > header");
105
+ const columns = node.querySelector(".entry-head");
106
+ const update = () => {
107
+ const height = heading.getBoundingClientRect().height;
108
+ const disabled = height > Math.min(innerHeight / 4, 192);
109
+ node.toggleAttribute("data-sticky-header-disabled", disabled);
110
+ node.style.setProperty(
111
+ "--dw-sticky-header-height",
112
+ `${disabled ? 0 : height}px`,
113
+ );
114
+ node.style.setProperty(
115
+ "--dw-sticky-columns-height",
116
+ `${columns.getBoundingClientRect().height}px`,
117
+ );
118
+ node.setAttribute("data-sticky-ready", "");
119
+ };
120
+ update();
121
+ const observer = new ResizeObserver(update);
122
+ observer.observe(heading);
123
+ observer.observe(columns);
124
+ window.addEventListener("resize", update);
125
+ return {
126
+ destroy() {
127
+ observer.disconnect();
128
+ window.removeEventListener("resize", update);
129
+ },
130
+ };
131
+ }
103
132
  function measure(node, id) {
104
133
  if (typeof ResizeObserver === "undefined") return;
105
134
  const observer = new ResizeObserver(() => {
106
135
  const value = node.getBoundingClientRect().height;
107
136
  if (value > 0 && heights.get(id) !== value) {
137
+ const focused = list?.contains(document.activeElement)
138
+ ? document.activeElement
139
+ : null;
140
+ const top = focused?.getBoundingClientRect().top;
108
141
  const next = new Map(heights);
109
142
  next.set(id, value);
110
143
  heights = next;
144
+ // Updating spacer estimates must preserve the focused link's viewport position.
145
+ if (focused)
146
+ tick().then(() => {
147
+ if (focused.isConnected && document.activeElement === focused)
148
+ window.scrollBy(0, focused.getBoundingClientRect().top - top);
149
+ });
111
150
  }
112
151
  });
113
152
  observer.observe(node);
@@ -169,17 +208,6 @@
169
208
  return () => cancelAnimationFrame(frame);
170
209
  });
171
210
  const isDirectory = directoryLike;
172
- function safeHref(record) {
173
- if (
174
- typeof location !== "undefined" &&
175
- location.protocol === "file:" &&
176
- isDirectory(record) &&
177
- !record.exitsExplorer &&
178
- record.href?.endsWith("/")
179
- )
180
- return record.href + "index.html";
181
- return record.href;
182
- }
183
211
  async function hydrateData() {
184
212
  if (complete || !model.manifestHref || location.protocol === "file:")
185
213
  return;
@@ -206,6 +234,7 @@
206
234
  }
207
235
  }
208
236
  async function navigate(event, record) {
237
+ selected = record.id;
209
238
  saveView();
210
239
  const dataHref = record.presentation?.folderDataHref;
211
240
  if (
@@ -330,16 +359,29 @@
330
359
  if (!row) return;
331
360
  selected = row.id;
332
361
  if (virtual) {
333
- scrollTop = Math.max(0, offsets[target] - 100);
362
+ const inset = list
363
+ ? parseFloat(
364
+ getComputedStyle(list).getPropertyValue(
365
+ "--dw-sticky-header-height",
366
+ ),
367
+ ) +
368
+ parseFloat(
369
+ getComputedStyle(list).getPropertyValue(
370
+ "--dw-sticky-columns-height",
371
+ ),
372
+ ) +
373
+ 8
374
+ : 0;
375
+ scrollTop = Math.max(0, offsets[target] - inset);
334
376
  if (list)
335
377
  window.scrollTo({
336
378
  top: list.getBoundingClientRect().top + window.scrollY + scrollTop,
337
379
  });
338
380
  }
339
381
  await tick();
340
- list
341
- ?.querySelector(`[data-order="${index}"] a`)
342
- ?.focus({ preventScroll: true });
382
+ const link = list?.querySelector(`[data-order="${target}"] a`);
383
+ link?.focus({ preventScroll: true });
384
+ link?.closest(".entry")?.scrollIntoView({ block: "nearest" });
343
385
  }
344
386
  function keyboard(event) {
345
387
  if (event.isComposing || event.altKey || event.ctrlKey || event.metaKey)
@@ -636,32 +678,32 @@
636
678
  if (list) scrollTop = Math.max(0, window.scrollY - list.offsetTop);
637
679
  }}
638
680
  />
639
- <header class="page-header">
640
- <div class="wordmark">
641
- {#if header}{@render header({ model })}{:else}{#if ui.brand.logoUrl}<img
642
- src={ui.brand.logoUrl}
643
- alt={ui.brand.logoAlt ?? ""}
644
- width="40"
645
- height="40"
646
- />{/if}
647
- <h1>{ui.brand.name ?? model.title}</h1>
648
- <span>{ui.labels.archive}</span>{/if}
649
- </div>
650
- <nav aria-label={ui.labels.breadcrumb}>
651
- <a
652
- href={model.rootHref}
653
- onclick={(event) =>
654
- navigateLink(event, model.rootHref, model.rootDataHref)}
655
- >{ui.labels.home}</a
656
- >{#each model.breadcrumbs as crumb}
657
- / <a
658
- href={crumb.href}
659
- onclick={(event) => navigateLink(event, crumb.href, crumb.dataHref)}
660
- >{crumb.name}</a
661
- >{/each}
662
- </nav>
663
- </header>
664
- <main data-explorer data-framework="svelte" aria-busy={busy}>
681
+ <main data-explorer data-framework="svelte" aria-busy={busy} use:stickyLayout>
682
+ <header class="page-header">
683
+ <div class="wordmark">
684
+ {#if header}{@render header({ model })}{:else}{#if ui.brand.logoUrl}<img
685
+ src={ui.brand.logoUrl}
686
+ alt={ui.brand.logoAlt ?? ""}
687
+ width="40"
688
+ height="40"
689
+ />{/if}
690
+ <h1>{ui.brand.name ?? model.title}</h1>
691
+ <span>{ui.labels.archive}</span>{/if}
692
+ </div>
693
+ <nav aria-label={ui.labels.breadcrumb}>
694
+ <a
695
+ href={model.rootHref}
696
+ onclick={(event) =>
697
+ navigateLink(event, model.rootHref, model.rootDataHref)}
698
+ >{ui.labels.home}</a
699
+ >{#each model.breadcrumbs as crumb}
700
+ / <a
701
+ href={crumb.href}
702
+ onclick={(event) => navigateLink(event, crumb.href, crumb.dataHref)}
703
+ >{crumb.name}</a
704
+ >{/each}
705
+ </nav>
706
+ </header>
665
707
  <div class="toolbar">
666
708
  <label class="search-field"
667
709
  >{ui.labels.searchDirectory}<input
@@ -765,14 +807,10 @@
765
807
  <div
766
808
  class="entry-head"
767
809
  data-custom-columns={Boolean(model.ui?.columns)}
768
- style:grid-template-columns={model.ui?.columns
810
+ style:--dw-entry-columns={model.ui?.columns
769
811
  ? ui.columns
770
812
  .map((column) =>
771
- column === "name"
772
- ? "minmax(0,1fr)"
773
- : typeof column === "object"
774
- ? "minmax(0,12rem)"
775
- : "minmax(6rem,max-content)",
813
+ column === "name" ? "minmax(0,1fr)" : "minmax(0,12rem)",
776
814
  )
777
815
  .join(" ")
778
816
  : undefined}
@@ -801,12 +839,10 @@
801
839
  data-id={entry.id}
802
840
  data-order={start + index}
803
841
  data-active={selected === entry.id}
804
- style:grid-template-columns={model.ui?.columns
842
+ style:--dw-entry-columns={model.ui?.columns
805
843
  ? ui.columns
806
844
  .map((column) =>
807
- column === "name"
808
- ? "minmax(0,1fr)"
809
- : "minmax(6rem,max-content)",
845
+ column === "name" ? "minmax(0,1fr)" : "minmax(0,12rem)",
810
846
  )
811
847
  .join(" ")
812
848
  : undefined}
@@ -815,7 +851,7 @@
815
851
  {#if column === "name"}<span class="identity"
816
852
  >{#if entry.href}<a
817
853
  class="name"
818
- href={safeHref(entry)}
854
+ href={entry.href}
819
855
  target={entry.exitsExplorer ? "_blank" : undefined}
820
856
  rel={entry.exitsExplorer ? "noopener" : undefined}
821
857
  onclick={(event) => navigate(event, entry)}
@@ -867,7 +903,7 @@
867
903
  })}{/if}
868
904
  {#each ui.actions as action}
869
905
  {#if action === "download" && entry.href && !isDirectory(entry) && !entry.link?.unavailable}<a
870
- href={safeHref(entry)}
906
+ href={entry.href}
871
907
  download>{ui.labels.download}</a
872
908
  >
873
909
  {:else if action !== "download"}<button