@mindful-web/marko-web-digital-edition 1.95.0

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.
Files changed (41) hide show
  1. package/.eslintignore +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +121 -0
  4. package/components/ad.marko +81 -0
  5. package/components/ad.marko.js +102 -0
  6. package/components/article-body.marko +134 -0
  7. package/components/article-body.marko.js +186 -0
  8. package/components/author.marko +30 -0
  9. package/components/author.marko.js +59 -0
  10. package/components/gate.marko +158 -0
  11. package/components/gate.marko.js +147 -0
  12. package/components/ld-json.marko +5 -0
  13. package/components/ld-json.marko.js +29 -0
  14. package/components/marko.json +78 -0
  15. package/components/reflow.marko +67 -0
  16. package/components/reflow.marko.js +106 -0
  17. package/components/replica.marko +86 -0
  18. package/components/replica.marko.js +128 -0
  19. package/components/scroll.marko +134 -0
  20. package/components/scroll.marko.js +23 -0
  21. package/components/theme.marko +73 -0
  22. package/components/theme.marko.js +30 -0
  23. package/components/toc.marko +28 -0
  24. package/components/toc.marko.js +53 -0
  25. package/components/toolbar.marko +47 -0
  26. package/components/toolbar.marko.js +54 -0
  27. package/components/tracking.marko +211 -0
  28. package/components/tracking.marko.js +60 -0
  29. package/lib/authors.js +125 -0
  30. package/lib/entities.js +97 -0
  31. package/lib/index.js +453 -0
  32. package/marko.json +6 -0
  33. package/package.json +33 -0
  34. package/routes/index.js +129 -0
  35. package/scss/index.scss +679 -0
  36. package/templates/article.marko +108 -0
  37. package/templates/article.marko.js +238 -0
  38. package/templates/hub.marko +94 -0
  39. package/templates/hub.marko.js +219 -0
  40. package/templates/partial.marko +17 -0
  41. package/templates/partial.marko.js +44 -0
@@ -0,0 +1,86 @@
1
+ import { pageImageUrl, pageSrcset, pageAspect } from "../lib";
2
+ import { adEntity, entityAttr } from "../lib/entities";
3
+
4
+ $ const { manifest, page } = input;
5
+ $ // Hotspot regions carry only id/href/label; the ad item behind each one has the
6
+ $ // advertiser and company the entity needs.
7
+ $ const adItemFor = (r) => manifest.items.find((i) => i.id === r.id) || { id: r.id, advertiserName: r.label, landingUrl: r.href };
8
+ $ const canAccess = input.canAccess !== false;
9
+ $ const previewPages = input.previewPages || 6;
10
+ $ const pageCount = manifest.pdf.pageCount;
11
+ $ const aspect = pageAspect(manifest);
12
+ $ // Unentitled visitors get a cover-and-first-few-pages preview rather than nothing,
13
+ $ // so the "Print view" toggle stays honest for signed-out readers.
14
+ $ const pages = canAccess ? manifest.pages : manifest.pages.slice(0, previewPages);
15
+
16
+ <!--
17
+ Replica view: imgix-rendered page images with transparent anchors positioned over
18
+ the ad regions the ingest pipeline identified. Fractional bboxes become percentage
19
+ offsets, so a hotspot tracks the image at every breakpoint without JS.
20
+
21
+ Only the current page and its neighbours are eagerly loaded; the rest lazy-load as
22
+ the reader scrolls. The whole issue is 80 pages, so rendering all <img> tags up
23
+ front with loading="lazy" is cheaper than a JS pager and keeps deep links working
24
+ with no client code.
25
+ -->
26
+ <div
27
+ class="edition-replica"
28
+ data-edition-issue-id=manifest.issueId
29
+ data-edition-page-count=pageCount
30
+ data-edition-start-page=page
31
+ >
32
+ <div class="edition-replica__pages">
33
+ <for|p| of=pages>
34
+ $ const near = Math.abs(p.n - page) <= 1;
35
+ $ const hotspots = p.regions.filter((r) => r.href);
36
+ <figure class="edition-replica__page" id=`page-${p.n}` data-page=p.n>
37
+ <div class="edition-replica__frame" style=`aspect-ratio: ${aspect};`>
38
+ <img
39
+ class="edition-replica__img"
40
+ src=pageImageUrl(manifest, p.n, { w: near ? 1200 : 800 })
41
+ srcset=pageSrcset(manifest, p.n)
42
+ sizes="(min-width: 1100px) 1000px, 100vw"
43
+ alt=`${manifest.publication} ${manifest.name}, page ${p.folio}`
44
+ width=manifest.pdf.baseWidth
45
+ height=manifest.pdf.baseHeight
46
+ loading=(near ? "eager" : "lazy")
47
+ decoding="async"
48
+ />
49
+ <for|r| of=hotspots>
50
+ $ const adItem = adItemFor(r);
51
+ $ const entity = adEntity(manifest, { ...adItem, printPage: p.n }, { placement: "replica" });
52
+ <!--
53
+ The hotspot doubles as the ad's tracking element in the replica: the
54
+ impression / active-view observer in tracking.marko picks up anything
55
+ with data-edition-ad-id, and the p1events link listener reads data-p1-*.
56
+ -->
57
+ <a
58
+ class="edition-replica__hotspot"
59
+ href=r.href
60
+ target="_blank"
61
+ rel="nofollow noopener sponsored"
62
+ title=(r.label || "Advertisement")
63
+ aria-label=`Advertisement: ${r.label || "advertiser"}`
64
+ style=`left:${(r.bbox[0] * 100).toFixed(3)}%;top:${(r.bbox[1] * 100).toFixed(3)}%;width:${(r.bbox[2] * 100).toFixed(3)}%;height:${(r.bbox[3] * 100).toFixed(3)}%;`
65
+ data-edition-ad-id=r.id
66
+ data-edition-page=p.n
67
+ data-edition-placement="replica"
68
+ data-advertiser=(r.label || "")
69
+ data-edition-entity=JSON.stringify(entity)
70
+ data-p1-category="Digital Edition Ad Unit"
71
+ data-p1-action="Click"
72
+ data-p1-label=(r.label || "")
73
+ data-p1-entity=entityAttr(entity)
74
+ ></a>
75
+ </for>
76
+ </div>
77
+ <figcaption class="edition-replica__folio">Page ${p.folio}</figcaption>
78
+ </figure>
79
+ </for>
80
+ <if(!canAccess && pageCount > pages.length)>
81
+ <p class="edition-replica__more">
82
+ Showing ${pages.length} of ${pageCount} pages.
83
+ </p>
84
+ </if>
85
+ </div>
86
+ </div>
@@ -0,0 +1,128 @@
1
+ // Compiled using marko@4.20.2 - DO NOT EDIT
2
+ "use strict";
3
+
4
+ var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
+ marko_componentType = "/@mindful-web/marko-web-digital-edition$1.95.0/components/replica.marko",
6
+ marko_renderer = require("marko/dist/runtime/components/renderer"),
7
+ module_lib_module = require("../lib"),
8
+ lib_module = module_lib_module.default || module_lib_module,
9
+ pageImageUrl = module_lib_module.pageImageUrl,
10
+ pageSrcset = module_lib_module.pageSrcset,
11
+ pageAspect = module_lib_module.pageAspect,
12
+ module_entities_module = require("../lib/entities"),
13
+ entities_module = module_entities_module.default || module_entities_module,
14
+ adEntity = module_entities_module.adEntity,
15
+ entityAttr = module_entities_module.entityAttr,
16
+ marko_forOf = require("marko/dist/runtime/helpers/for-of"),
17
+ marko_attr = require("marko/dist/runtime/html/helpers/attr"),
18
+ marko_styleAttr = require("marko/dist/runtime/html/helpers/style-attr"),
19
+ helpers_escape_xml = require("marko/dist/runtime/html/helpers/escape-xml"),
20
+ marko_escapeXml = helpers_escape_xml.x;
21
+
22
+ function render(input, out, __component, component, state) {
23
+ var data = input;
24
+
25
+ const { manifest, page } = input;
26
+
27
+ // Hotspot regions carry only id/href/label; the ad item behind each one has the
28
+
29
+ // advertiser and company the entity needs.
30
+
31
+ const adItemFor = (r) => manifest.items.find((i) => i.id === r.id) || { id: r.id, advertiserName: r.label, landingUrl: r.href };
32
+
33
+ const canAccess = input.canAccess !== false;
34
+
35
+ const previewPages = input.previewPages || 6;
36
+
37
+ const pageCount = manifest.pdf.pageCount;
38
+
39
+ const aspect = pageAspect(manifest);
40
+
41
+ // Unentitled visitors get a cover-and-first-few-pages preview rather than nothing,
42
+
43
+ // so the "Print view" toggle stays honest for signed-out readers.
44
+
45
+ const pages = canAccess ? manifest.pages : manifest.pages.slice(0, previewPages);
46
+
47
+ out.w("<div class=\"edition-replica\"" +
48
+ marko_attr("data-edition-issue-id", manifest.issueId) +
49
+ marko_attr("data-edition-page-count", pageCount) +
50
+ marko_attr("data-edition-start-page", page) +
51
+ "><div class=\"edition-replica__pages\">");
52
+
53
+ var $for$0 = 0;
54
+
55
+ marko_forOf(pages, function(p) {
56
+ const near = Math.abs(p.n - page) <= 1;
57
+
58
+ const hotspots = p.regions.filter((r) => r.href);
59
+
60
+ var $keyScope$0 = "[" + (($for$0++) + "]");
61
+
62
+ out.w("<figure class=\"edition-replica__page\"" +
63
+ marko_attr("id", "page-" + p.n) +
64
+ marko_attr("data-page", p.n) +
65
+ "><div class=\"edition-replica__frame\"" +
66
+ marko_styleAttr(("aspect-ratio: " + aspect) + ";") +
67
+ "><img class=\"edition-replica__img\"" +
68
+ marko_attr("src", pageImageUrl(manifest, p.n, {
69
+ w: near ? 1200 : 800
70
+ })) +
71
+ marko_attr("srcset", pageSrcset(manifest, p.n)) +
72
+ " sizes=\"(min-width: 1100px) 1000px, 100vw\"" +
73
+ marko_attr("alt", (((manifest.publication + " ") + manifest.name) + ", page ") + p.folio) +
74
+ marko_attr("width", manifest.pdf.baseWidth) +
75
+ marko_attr("height", manifest.pdf.baseHeight) +
76
+ marko_attr("loading", near ? "eager" : "lazy") +
77
+ " decoding=\"async\">");
78
+
79
+ var $for$1 = 0;
80
+
81
+ marko_forOf(hotspots, function(r) {
82
+ const adItem = adItemFor(r);
83
+
84
+ const entity = adEntity(manifest, { ...adItem, printPage: p.n }, { placement: "replica" });
85
+
86
+ var $keyScope$1 = "[" + ((($for$1++) + $keyScope$0) + "]");
87
+
88
+ out.w("<a class=\"edition-replica__hotspot\"" +
89
+ marko_attr("href", r.href) +
90
+ " target=\"_blank\" rel=\"nofollow noopener sponsored\"" +
91
+ marko_attr("title", r.label || "Advertisement") +
92
+ marko_attr("aria-label", "Advertisement: " + (r.label || "advertiser")) +
93
+ marko_styleAttr(((((((("left:" + (r.bbox[0] * 100).toFixed(3)) + "%;top:") + (r.bbox[1] * 100).toFixed(3)) + "%;width:") + (r.bbox[2] * 100).toFixed(3)) + "%;height:") + (r.bbox[3] * 100).toFixed(3)) + "%;") +
94
+ marko_attr("data-edition-ad-id", r.id) +
95
+ marko_attr("data-edition-page", p.n) +
96
+ " data-edition-placement=\"replica\"" +
97
+ marko_attr("data-advertiser", r.label || "") +
98
+ marko_attr("data-edition-entity", JSON.stringify(entity)) +
99
+ " data-p1-category=\"Digital Edition Ad Unit\" data-p1-action=\"Click\"" +
100
+ marko_attr("data-p1-label", r.label || "") +
101
+ marko_attr("data-p1-entity", entityAttr(entity)) +
102
+ "></a>");
103
+ });
104
+
105
+ out.w("</div><figcaption class=\"edition-replica__folio\">Page " +
106
+ marko_escapeXml(p.folio) +
107
+ "</figcaption></figure>");
108
+ });
109
+
110
+ if ((!canAccess) && (pageCount > pages.length)) {
111
+ out.w("<p class=\"edition-replica__more\">Showing " +
112
+ marko_escapeXml(pages.length) +
113
+ " of " +
114
+ marko_escapeXml(pageCount) +
115
+ " pages.</p>");
116
+ }
117
+
118
+ out.w("</div></div>");
119
+ }
120
+
121
+ marko_template._ = marko_renderer(render, {
122
+ d_: true,
123
+ e_: marko_componentType
124
+ });
125
+
126
+ marko_template.meta = {
127
+ id: "/@mindful-web/marko-web-digital-edition$1.95.0/components/replica.marko"
128
+ };
@@ -0,0 +1,134 @@
1
+ $ const { manifest } = input;
2
+
3
+ <!--
4
+ Continuous-scroll reader for the hub.
5
+
6
+ Progressive enhancement only: the hub is complete and fully navigable without this.
7
+ When it runs, it appends the next article as an HTML fragment (`?partial=1`) as the
8
+ reader nears the end, and pushStates to that article's real URL — the same URL that
9
+ serves the article standalone, so what the reader sees and what a crawler indexes
10
+ never diverge.
11
+
12
+ History: each appended article gets one pushState. Scrolling back up replaces the
13
+ URL (no new history entries) so Back leaves the reader rather than stepping through
14
+ every article the reader passed.
15
+ -->
16
+ <script>
17
+ $!{`
18
+ (function () {
19
+ var root = document.querySelector('.edition-reflow');
20
+ if (!root || !window.IntersectionObserver || !window.fetch || !history.pushState) return;
21
+
22
+ var flow = root.querySelector('[data-edition-flow]');
23
+ var queueEl = root.querySelector('[data-edition-queue]');
24
+ if (!flow || !queueEl) return;
25
+
26
+ var queue = [].map.call(
27
+ queueEl.querySelectorAll('[data-edition-queue-slug]'),
28
+ function (a) { return { slug: a.getAttribute('data-edition-queue-slug'), href: a.getAttribute('href') }; }
29
+ );
30
+ if (!queue.length) return;
31
+
32
+ // The queue is the no-JS fallback; once we take over, hide it.
33
+ queueEl.hidden = true;
34
+
35
+ var loading = false;
36
+ var done = false;
37
+ var current = null;
38
+
39
+ // An article entering the reader counts as a View of that article — the same
40
+ // event the standalone article page fires on load (see tracking.marko), with the
41
+ // entity the server stamped on the article element.
42
+ function track(el, href) {
43
+ if (!window.p1events) return;
44
+ var entity = null;
45
+ try { entity = JSON.parse(el.getAttribute('data-edition-entity')); } catch (e) { return; }
46
+ if (!entity) return;
47
+ p1events('track', {
48
+ action: 'View',
49
+ category: 'Digital Edition Article',
50
+ label: el.getAttribute('data-edition-article-slug') || '',
51
+ entity: entity,
52
+ props: { path: href, via: 'scroll' }
53
+ });
54
+ }
55
+
56
+ function setUrl(el, href, slug, push) {
57
+ if (current === slug) return;
58
+ current = slug;
59
+ try { history[push ? 'pushState' : 'replaceState']({ slug: slug }, '', href); } catch (e) { return; }
60
+ if (push) track(el, href);
61
+ }
62
+
63
+ // Report the article currently filling the viewport.
64
+ var seen = Object.create(null);
65
+ var spy = new IntersectionObserver(function (entries) {
66
+ entries.forEach(function (e) {
67
+ if (!e.isIntersecting) return;
68
+ var el = e.target;
69
+ var slug = el.getAttribute('data-edition-article-slug');
70
+ var href = el.getAttribute('data-edition-article-href');
71
+ if (!slug || !href) return;
72
+ var isNew = !seen[slug];
73
+ seen[slug] = 1;
74
+ setUrl(el, href, slug, isNew);
75
+ });
76
+ }, { threshold: 0.25, rootMargin: '-20% 0px -60% 0px' });
77
+
78
+ [].forEach.call(flow.querySelectorAll('[data-edition-article-slug]'), function (el) { spy.observe(el); });
79
+
80
+ function appendNext() {
81
+ if (loading || done) return;
82
+ var next = queue.shift();
83
+ if (!next) { done = true; return; }
84
+ loading = true;
85
+ var url = next.href + (next.href.indexOf('?') === -1 ? '?' : '&') + 'partial=1';
86
+ fetch(url, { credentials: 'same-origin', headers: { 'x-requested-with': 'fetch' } })
87
+ .then(function (r) { return r.ok ? r.text() : Promise.reject(new Error(String(r.status))); })
88
+ .then(function (html) {
89
+ var holder = document.createElement('div');
90
+ holder.innerHTML = html;
91
+ // Appending removes the node from the holder, so iterating a live
92
+ // HTMLCollection with forEach skips every other child — drain instead.
93
+ while (holder.firstElementChild) {
94
+ var child = holder.firstElementChild;
95
+ flow.appendChild(child);
96
+ if (child.getAttribute && child.getAttribute('data-edition-article-slug')) spy.observe(child);
97
+ }
98
+ // Let the impression tracker pick up the ads inside the new article.
99
+ try { document.dispatchEvent(new CustomEvent('edition:content-appended')); } catch (e) { /* older browsers: impressions only */ }
100
+ loading = false;
101
+ // Keep going if the sentinel is still on screen after this insert.
102
+ if (sentinelVisible) appendNext();
103
+ })
104
+ .catch(function () {
105
+ // Put it back and stand down; the visible queue link still works.
106
+ queue.unshift(next);
107
+ loading = false;
108
+ done = true;
109
+ queueEl.hidden = false;
110
+ });
111
+ }
112
+
113
+ var sentinel = document.createElement('div');
114
+ sentinel.className = 'edition-reflow__sentinel';
115
+ sentinel.setAttribute('aria-hidden', 'true');
116
+ flow.parentNode.insertBefore(sentinel, queueEl);
117
+
118
+ var sentinelVisible = false;
119
+ new IntersectionObserver(function (entries) {
120
+ sentinelVisible = entries[0].isIntersecting;
121
+ if (sentinelVisible) appendNext();
122
+ }, { rootMargin: '600px 0px' }).observe(sentinel);
123
+
124
+ // Clicking a contents or queue link should behave normally (a real navigation) —
125
+ // only the scroll position drives the reader.
126
+ window.addEventListener('popstate', function (e) {
127
+ var slug = e.state && e.state.slug;
128
+ if (!slug) return;
129
+ var el = flow.querySelector('[data-edition-article-slug="' + slug + '"]');
130
+ if (el) { current = slug; el.scrollIntoView(); }
131
+ });
132
+ }());
133
+ `}
134
+ </script>
@@ -0,0 +1,23 @@
1
+ // Compiled using marko@4.20.2 - DO NOT EDIT
2
+ "use strict";
3
+
4
+ var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
+ marko_componentType = "/@mindful-web/marko-web-digital-edition$1.95.0/components/scroll.marko",
6
+ marko_renderer = require("marko/dist/runtime/components/renderer");
7
+
8
+ function render(input, out, __component, component, state) {
9
+ var data = input;
10
+
11
+ const { manifest } = input;
12
+
13
+ out.w("<script>\n \n (function () {\n var root = document.querySelector('.edition-reflow');\n if (!root || !window.IntersectionObserver || !window.fetch || !history.pushState) return;\n\n var flow = root.querySelector('[data-edition-flow]');\n var queueEl = root.querySelector('[data-edition-queue]');\n if (!flow || !queueEl) return;\n\n var queue = [].map.call(\n queueEl.querySelectorAll('[data-edition-queue-slug]'),\n function (a) { return { slug: a.getAttribute('data-edition-queue-slug'), href: a.getAttribute('href') }; }\n );\n if (!queue.length) return;\n\n // The queue is the no-JS fallback; once we take over, hide it.\n queueEl.hidden = true;\n\n var loading = false;\n var done = false;\n var current = null;\n\n // An article entering the reader counts as a View of that article — the same\n // event the standalone article page fires on load (see tracking.marko), with the\n // entity the server stamped on the article element.\n function track(el, href) {\n if (!window.p1events) return;\n var entity = null;\n try { entity = JSON.parse(el.getAttribute('data-edition-entity')); } catch (e) { return; }\n if (!entity) return;\n p1events('track', {\n action: 'View',\n category: 'Digital Edition Article',\n label: el.getAttribute('data-edition-article-slug') || '',\n entity: entity,\n props: { path: href, via: 'scroll' }\n });\n }\n\n function setUrl(el, href, slug, push) {\n if (current === slug) return;\n current = slug;\n try { history[push ? 'pushState' : 'replaceState']({ slug: slug }, '', href); } catch (e) { return; }\n if (push) track(el, href);\n }\n\n // Report the article currently filling the viewport.\n var seen = Object.create(null);\n var spy = new IntersectionObserver(function (entries) {\n entries.forEach(function (e) {\n if (!e.isIntersecting) return;\n var el = e.target;\n var slug = el.getAttribute('data-edition-article-slug');\n var href = el.getAttribute('data-edition-article-href');\n if (!slug || !href) return;\n var isNew = !seen[slug];\n seen[slug] = 1;\n setUrl(el, href, slug, isNew);\n });\n }, { threshold: 0.25, rootMargin: '-20% 0px -60% 0px' });\n\n [].forEach.call(flow.querySelectorAll('[data-edition-article-slug]'), function (el) { spy.observe(el); });\n\n function appendNext() {\n if (loading || done) return;\n var next = queue.shift();\n if (!next) { done = true; return; }\n loading = true;\n var url = next.href + (next.href.indexOf('?') === -1 ? '?' : '&') + 'partial=1';\n fetch(url, { credentials: 'same-origin', headers: { 'x-requested-with': 'fetch' } })\n .then(function (r) { return r.ok ? r.text() : Promise.reject(new Error(String(r.status))); })\n .then(function (html) {\n var holder = document.createElement('div');\n holder.innerHTML = html;\n // Appending removes the node from the holder, so iterating a live\n // HTMLCollection with forEach skips every other child — drain instead.\n while (holder.firstElementChild) {\n var child = holder.firstElementChild;\n flow.appendChild(child);\n if (child.getAttribute && child.getAttribute('data-edition-article-slug')) spy.observe(child);\n }\n // Let the impression tracker pick up the ads inside the new article.\n try { document.dispatchEvent(new CustomEvent('edition:content-appended')); } catch (e) { /* older browsers: impressions only */ }\n loading = false;\n // Keep going if the sentinel is still on screen after this insert.\n if (sentinelVisible) appendNext();\n })\n .catch(function () {\n // Put it back and stand down; the visible queue link still works.\n queue.unshift(next);\n loading = false;\n done = true;\n queueEl.hidden = false;\n });\n }\n\n var sentinel = document.createElement('div');\n sentinel.className = 'edition-reflow__sentinel';\n sentinel.setAttribute('aria-hidden', 'true');\n flow.parentNode.insertBefore(sentinel, queueEl);\n\n var sentinelVisible = false;\n new IntersectionObserver(function (entries) {\n sentinelVisible = entries[0].isIntersecting;\n if (sentinelVisible) appendNext();\n }, { rootMargin: '600px 0px' }).observe(sentinel);\n\n // Clicking a contents or queue link should behave normally (a real navigation) —\n // only the scroll position drives the reader.\n window.addEventListener('popstate', function (e) {\n var slug = e.state && e.state.slug;\n if (!slug) return;\n var el = flow.querySelector('[data-edition-article-slug=\"' + slug + '\"]');\n if (el) { current = slug; el.scrollIntoView(); }\n });\n }());\n \n</script>");
14
+ }
15
+
16
+ marko_template._ = marko_renderer(render, {
17
+ d_: true,
18
+ e_: marko_componentType
19
+ });
20
+
21
+ marko_template.meta = {
22
+ id: "/@mindful-web/marko-web-digital-edition$1.95.0/components/scroll.marko"
23
+ };
@@ -0,0 +1,73 @@
1
+ $ const defaultTheme = input.defaultTheme || "dark";
2
+ $ const view = input.view === "print" ? "print" : "read";
3
+
4
+ <!--
5
+ Reader theme.
6
+
7
+ Rendered into the page <head> so the attribute is set before first paint — a
8
+ server-rendered default plus a client-stored override cannot be done without an
9
+ inline script, and doing it in the body would flash the wrong theme.
10
+
11
+ BOTH views default to DARK, deliberately: the toggle is offered in both, and the
12
+ stored preference is shared, so a per-view default would flip the theme on a
13
+ first-time visitor who merely switched Read -> Print without touching the toggle.
14
+ In the replica, dark mode darkens ONLY the canvas the page images float on —
15
+ the chrome keeps the light treatment, which is how a PDF reader behaves. The
16
+ CSS needs to know the view to scope that, so the same pre-paint script also
17
+ stamps `data-edition-view` on the root.
18
+
19
+ An explicit choice is remembered per viewer and wins over the default.
20
+ localStorage is wrapped because it throws outright in some contexts (private
21
+ windows, blocked site data) rather than just returning null.
22
+
23
+ The click handler is delegated from `document`, so it is safe to attach here
24
+ before the toolbar button exists.
25
+ -->
26
+ <script>
27
+ $!{`
28
+ (function () {
29
+ var KEY = 'edition:theme';
30
+ var DEFAULT = ${JSON.stringify(defaultTheme)};
31
+ var root = document.documentElement;
32
+ root.setAttribute('data-edition-view', ${JSON.stringify(view)});
33
+
34
+ function read() {
35
+ try { return window.localStorage.getItem(KEY); } catch (e) { return null; }
36
+ }
37
+ function write(v) {
38
+ try { window.localStorage.setItem(KEY, v); } catch (e) { /* non-persistent is fine */ }
39
+ }
40
+ function apply(theme) {
41
+ root.setAttribute('data-edition-theme', theme);
42
+ var btn = document.querySelector('[data-edition-theme-toggle]');
43
+ if (btn) {
44
+ btn.setAttribute('aria-label', theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme');
45
+ }
46
+ }
47
+
48
+ var stored = read();
49
+ apply(stored === 'dark' || stored === 'light' ? stored : DEFAULT);
50
+
51
+ document.addEventListener('click', function (e) {
52
+ var btn = e.target && e.target.closest && e.target.closest('[data-edition-theme-toggle]');
53
+ if (!btn) return;
54
+ e.preventDefault();
55
+ var next = root.getAttribute('data-edition-theme') === 'dark' ? 'light' : 'dark';
56
+ apply(next);
57
+ write(next);
58
+ if (window.p1events) {
59
+ p1events('track', {
60
+ action: 'Theme Toggle',
61
+ category: 'Digital Edition',
62
+ label: next,
63
+ });
64
+ }
65
+ });
66
+
67
+ // Label the button correctly once it exists, even if the theme never changes.
68
+ document.addEventListener('DOMContentLoaded', function () {
69
+ apply(root.getAttribute('data-edition-theme') || DEFAULT);
70
+ });
71
+ }());
72
+ `}
73
+ </script>
@@ -0,0 +1,30 @@
1
+ // Compiled using marko@4.20.2 - DO NOT EDIT
2
+ "use strict";
3
+
4
+ var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
+ marko_componentType = "/@mindful-web/marko-web-digital-edition$1.95.0/components/theme.marko",
6
+ marko_renderer = require("marko/dist/runtime/components/renderer"),
7
+ helpers_escape_xml = require("marko/dist/runtime/html/helpers/escape-xml"),
8
+ marko_escapeXml = helpers_escape_xml.x,
9
+ marko_escapeScript = require("marko/dist/runtime/html/helpers/escape-script-placeholder");
10
+
11
+ function render(input, out, __component, component, state) {
12
+ var data = input;
13
+
14
+ const defaultTheme = input.defaultTheme || "dark";
15
+
16
+ const view = input.view === "print" ? "print" : "read";
17
+
18
+ out.w("<script>" +
19
+ marko_escapeScript(("\n " + (((("\n (function () {\n var KEY = 'edition:theme';\n var DEFAULT = " + JSON.stringify(defaultTheme)) + ";\n var root = document.documentElement;\n root.setAttribute('data-edition-view', ") + JSON.stringify(view)) + ");\n\n function read() {\n try { return window.localStorage.getItem(KEY); } catch (e) { return null; }\n }\n function write(v) {\n try { window.localStorage.setItem(KEY, v); } catch (e) { /* non-persistent is fine */ }\n }\n function apply(theme) {\n root.setAttribute('data-edition-theme', theme);\n var btn = document.querySelector('[data-edition-theme-toggle]');\n if (btn) {\n btn.setAttribute('aria-label', theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme');\n }\n }\n\n var stored = read();\n apply(stored === 'dark' || stored === 'light' ? stored : DEFAULT);\n\n document.addEventListener('click', function (e) {\n var btn = e.target && e.target.closest && e.target.closest('[data-edition-theme-toggle]');\n if (!btn) return;\n e.preventDefault();\n var next = root.getAttribute('data-edition-theme') === 'dark' ? 'light' : 'dark';\n apply(next);\n write(next);\n if (window.p1events) {\n p1events('track', {\n action: 'Theme Toggle',\n category: 'Digital Edition',\n label: next,\n });\n }\n });\n\n // Label the button correctly once it exists, even if the theme never changes.\n document.addEventListener('DOMContentLoaded', function () {\n apply(root.getAttribute('data-edition-theme') || DEFAULT);\n });\n }());\n ")) + "\n") +
20
+ "</script>");
21
+ }
22
+
23
+ marko_template._ = marko_renderer(render, {
24
+ d_: true,
25
+ e_: marko_componentType
26
+ });
27
+
28
+ marko_template.meta = {
29
+ id: "/@mindful-web/marko-web-digital-edition$1.95.0/components/theme.marko"
30
+ };
@@ -0,0 +1,28 @@
1
+ import { editionPath } from "../lib";
2
+
3
+ $ const { manifest, items, currentSlug } = input;
4
+
5
+ <!--
6
+ Contents. These are real links to per-article URLs, which is what makes each story
7
+ crawlable — the scroll reader intercepts them for a smoother read, but they work
8
+ fully without JS.
9
+ -->
10
+ <nav class="edition-toc" aria-label="In this issue">
11
+ <h2 class="edition-toc__title">In this issue</h2>
12
+ <ol class="edition-toc__list">
13
+ <for|item| of=items>
14
+ $ const href = editionPath(manifest.issueId, { slug: item.slug });
15
+ $ const isCurrent = currentSlug && item.slug === currentSlug;
16
+ <li class="edition-toc__item">
17
+ <a
18
+ class=`edition-toc__link ${isCurrent ? "is-current" : ""}`
19
+ href=href
20
+ aria-current=(isCurrent ? "page" : null)
21
+ >
22
+ <span class="edition-toc__headline">${item.headline || "Untitled"}</span>
23
+ <span class="edition-toc__page">p.&nbsp;${item.printPage}</span>
24
+ </a>
25
+ </li>
26
+ </for>
27
+ </ol>
28
+ </nav>
@@ -0,0 +1,53 @@
1
+ // Compiled using marko@4.20.2 - DO NOT EDIT
2
+ "use strict";
3
+
4
+ var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
+ marko_componentType = "/@mindful-web/marko-web-digital-edition$1.95.0/components/toc.marko",
6
+ marko_renderer = require("marko/dist/runtime/components/renderer"),
7
+ module_lib_module = require("../lib"),
8
+ lib_module = module_lib_module.default || module_lib_module,
9
+ editionPath = module_lib_module.editionPath,
10
+ marko_forOf = require("marko/dist/runtime/helpers/for-of"),
11
+ helpers_escape_xml = require("marko/dist/runtime/html/helpers/escape-xml"),
12
+ marko_escapeXml = helpers_escape_xml.x,
13
+ marko_classAttr = require("marko/dist/runtime/html/helpers/class-attr"),
14
+ marko_attr = require("marko/dist/runtime/html/helpers/attr");
15
+
16
+ function render(input, out, __component, component, state) {
17
+ var data = input;
18
+
19
+ const { manifest, items, currentSlug } = input;
20
+
21
+ out.w("<nav class=\"edition-toc\" aria-label=\"In this issue\"><h2 class=\"edition-toc__title\">In this issue</h2><ol class=\"edition-toc__list\">");
22
+
23
+ var $for$0 = 0;
24
+
25
+ marko_forOf(items, function(item) {
26
+ const href = editionPath(manifest.issueId, { slug: item.slug });
27
+
28
+ const isCurrent = currentSlug && item.slug === currentSlug;
29
+
30
+ var $keyScope$0 = "[" + (($for$0++) + "]");
31
+
32
+ out.w("<li class=\"edition-toc__item\"><a" +
33
+ marko_classAttr("edition-toc__link " + (isCurrent ? "is-current" : "")) +
34
+ marko_attr("href", href) +
35
+ marko_attr("aria-current", isCurrent ? "page" : null) +
36
+ "><span class=\"edition-toc__headline\">" +
37
+ marko_escapeXml(item.headline || "Untitled") +
38
+ "</span><span class=\"edition-toc__page\">p.&nbsp;" +
39
+ marko_escapeXml(item.printPage) +
40
+ "</span></a></li>");
41
+ });
42
+
43
+ out.w("</ol></nav>");
44
+ }
45
+
46
+ marko_template._ = marko_renderer(render, {
47
+ d_: true,
48
+ e_: marko_componentType
49
+ });
50
+
51
+ marko_template.meta = {
52
+ id: "/@mindful-web/marko-web-digital-edition$1.95.0/components/toc.marko"
53
+ };
@@ -0,0 +1,47 @@
1
+ import { editionPath } from "../lib";
2
+
3
+ $ const { manifest, view } = input;
4
+ $ const readHref = editionPath(manifest.issueId);
5
+ $ const printHref = editionPath(manifest.issueId, { view: "print" });
6
+ $ const showThemeToggle = input.showThemeToggle !== false;
7
+
8
+ <div class="edition-toolbar">
9
+ <div class="edition-toolbar__title">
10
+ <span class="edition-toolbar__pub">${manifest.publication}</span>
11
+ <span class="edition-toolbar__issue">${manifest.name}</span>
12
+ </div>
13
+ <div class="edition-toolbar__actions">
14
+ <if(showThemeToggle)>
15
+ <!--
16
+ Both labels are rendered server-side and CSS shows the one matching the
17
+ active theme, so there is no flash of the wrong label and the button needs
18
+ no state of its own. The click handler lives in theme.marko.
19
+ -->
20
+ <button
21
+ type="button"
22
+ class="edition-toolbar__theme"
23
+ data-edition-theme-toggle
24
+ aria-live="polite"
25
+ >
26
+ <span class="edition-toolbar__theme__icon" aria-hidden="true">
27
+ <span data-when="dark">☀</span><span data-when="light">☾</span>
28
+ </span>
29
+ <span class="edition-toolbar__theme__label">
30
+ <span data-when="dark">Light</span><span data-when="light">Dark</span>
31
+ </span>
32
+ </button>
33
+ </if>
34
+ <nav class="edition-toolbar__views" aria-label="Reading view">
35
+ <a
36
+ class=`edition-toolbar__view ${view === "read" ? "is-active" : ""}`
37
+ href=readHref
38
+ aria-current=(view === "read" ? "page" : null)
39
+ >Read</a>
40
+ <a
41
+ class=`edition-toolbar__view ${view === "print" ? "is-active" : ""}`
42
+ href=printHref
43
+ aria-current=(view === "print" ? "page" : null)
44
+ >Print view</a>
45
+ </nav>
46
+ </div>
47
+ </div>