@iyulab/canopy-page 0.11.1 → 0.12.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ Notable changes to canopy-page. The format follows
7
7
  The `settings.json` contract is what consuming projects plan their upgrades around, so changes
8
8
  to it — its fields, its validation, and what the checks reject — are what this file is about.
9
9
 
10
+ ## [0.12.0] — 2026-08-22
11
+
12
+ ### Added
13
+
14
+ - **Content images open full-size in a lightbox on click**, closing on a background click,
15
+ <kbd>Esc</kbd>, or its close button. An image already wrapped in a link to its own file — a
16
+ common workaround for the lack of a zoom before this — still opens the lightbox first; a
17
+ middle click or a modifier-held click on it still follows the link, for a reader who wants the
18
+ file itself.
19
+
20
+ ## [0.11.2] — 2026-08-18
21
+
22
+ ### Changed
23
+
24
+ - **Upgraded to canopy 0.11.2** — `**` emphasis now closes correctly when a CJK character
25
+ follows it with no space (a Chinese, Japanese, or Korean particle or full-width punctuation
26
+ mark sitting flush against the closing marker), instead of surviving into the rendered page as
27
+ literal asterisks. See
28
+ [canopy's own changelog](https://github.com/iyulab/canopy/blob/main/CHANGELOG.md#0112--2026-08-18)
29
+ for details.
30
+
10
31
  ## [0.11.1] — 2026-08-17
11
32
 
12
33
  ### Changed
package/README.md CHANGED
@@ -38,6 +38,8 @@ go away.
38
38
  you scroll
39
39
  - **A dark/light toggle** that remembers a reader's choice; without one, pages follow the
40
40
  system setting
41
+ - **Content images open full-size in a lightbox** when clicked, closing on a background click,
42
+ <kbd>Esc</kbd>, or its close button
41
43
  - **Prev/next cards** linking to a page's neighbors in the sidebar's own order, and **backlinks**
42
44
  listing every page that points to it
43
45
  - **Sidebar groups collapse**, open exactly along the path to the page you're on and closed
@@ -0,0 +1,61 @@
1
+ /* Image lightbox — pairs with assets/image-lightbox.js. Reuses canopy's own
2
+ tokens (--bg-primary, --text-normal, --radius-m, --sp-*) rather than
3
+ introducing new ones. z-index 30 sits above search's 10 and the mobile
4
+ nav overlay's 20, since the lightbox can open while either is present. */
5
+
6
+ .canopy-content img {
7
+ cursor: zoom-in;
8
+ }
9
+
10
+ body.canopy-lightbox-open {
11
+ overflow: hidden;
12
+ }
13
+
14
+ .canopy-lightbox-overlay {
15
+ position: fixed;
16
+ inset: 0;
17
+ z-index: 30;
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ padding: var(--sp-8);
22
+ background: rgba(0, 0, 0, 0.85);
23
+ }
24
+
25
+ .canopy-lightbox-overlay[hidden] {
26
+ display: none;
27
+ }
28
+
29
+ .canopy-lightbox-overlay img {
30
+ max-width: 90vw;
31
+ max-height: 90vh;
32
+ object-fit: contain;
33
+ cursor: default;
34
+ border-radius: var(--radius-m);
35
+ }
36
+
37
+ .canopy-lightbox-close {
38
+ position: fixed;
39
+ top: var(--sp-4);
40
+ right: var(--sp-4);
41
+ width: 2.5rem;
42
+ height: 2.5rem;
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ border: 1px solid var(--border);
47
+ border-radius: var(--radius-m);
48
+ background: var(--bg-primary);
49
+ color: var(--text-normal);
50
+ font-size: 1.25rem;
51
+ line-height: 1;
52
+ cursor: pointer;
53
+ }
54
+
55
+ .canopy-lightbox-close::before {
56
+ content: "\00d7";
57
+ }
58
+
59
+ .canopy-lightbox-close:hover {
60
+ background: var(--bg-secondary);
61
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * canopy-page's image lightbox — vanilla JS, no dependencies. Wires every
3
+ * `<img>` inside `.canopy-content` so a click opens it full-size in a
4
+ * dismissible overlay, rather than leaving a reader stuck at whatever width
5
+ * the article column happens to render it at.
6
+ *
7
+ * An image an author already wrapped in `<a href="...">` (a common way to
8
+ * work around the lack of a zoom before this script existed) still opens the
9
+ * lightbox first: the overlay shows the same source either way, so the link
10
+ * added nothing a reader couldn't already get from the image itself. The one
11
+ * case left alone is a click that already carries browser intent to open a
12
+ * new tab/window — middle click, or a modifier-held click — which this
13
+ * script never intercepts, so that intent still reaches the ancestor link.
14
+ *
15
+ * `shouldIntercept` is exposed for tests: it is the one piece of this file
16
+ * with real logic, and it needs no DOM to run.
17
+ */
18
+ var CanopyImageLightbox = (function () {
19
+ "use strict";
20
+
21
+ /** Whether a click should open the lightbox instead of its default action. */
22
+ function shouldIntercept(event) {
23
+ return event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey;
24
+ }
25
+
26
+ function main() {
27
+ var images = document.querySelectorAll(".canopy-content img");
28
+ if (images.length === 0) return;
29
+
30
+ var overlay = document.createElement("div");
31
+ overlay.className = "canopy-lightbox-overlay";
32
+ overlay.hidden = true;
33
+
34
+ var overlayImg = document.createElement("img");
35
+ overlay.appendChild(overlayImg);
36
+
37
+ var closeButton = document.createElement("button");
38
+ closeButton.type = "button";
39
+ closeButton.className = "canopy-lightbox-close";
40
+ closeButton.setAttribute("aria-label", "Close");
41
+ overlay.appendChild(closeButton);
42
+
43
+ var opener = null;
44
+
45
+ function close() {
46
+ overlay.hidden = true;
47
+ document.body.classList.remove("canopy-lightbox-open");
48
+ if (opener) {
49
+ opener.focus();
50
+ opener = null;
51
+ }
52
+ }
53
+
54
+ function open(img) {
55
+ overlayImg.src = img.currentSrc || img.src;
56
+ overlayImg.alt = img.alt;
57
+ overlay.hidden = false;
58
+ document.body.classList.add("canopy-lightbox-open");
59
+ opener = img;
60
+ closeButton.focus();
61
+ }
62
+
63
+ overlay.addEventListener("click", function (event) {
64
+ if (event.target === overlayImg) return;
65
+ close();
66
+ });
67
+ closeButton.addEventListener("click", close);
68
+ document.addEventListener("keydown", function (event) {
69
+ if (!overlay.hidden && event.key === "Escape") close();
70
+ });
71
+
72
+ images.forEach(function (img) {
73
+ img.addEventListener("click", function (event) {
74
+ if (!shouldIntercept(event)) return;
75
+ event.preventDefault();
76
+ event.stopPropagation();
77
+ open(img);
78
+ });
79
+ });
80
+
81
+ document.body.appendChild(overlay);
82
+ }
83
+
84
+ if (typeof document !== "undefined") main();
85
+
86
+ return { shouldIntercept: shouldIntercept };
87
+ })();
@@ -34,11 +34,12 @@ const SEARCH_FAILED_DEFAULT = "Search failed to load.";
34
34
  * asset stays valid, readable JavaScript on its own.
35
35
  */
36
36
  export async function assembleScript(searchFailed) {
37
- const [search, scrollspy, themeToggle, mobileNav] = await Promise.all([
37
+ const [search, scrollspy, themeToggle, mobileNav, imageLightbox] = await Promise.all([
38
38
  readAsset("search.js"),
39
39
  readAsset("scrollspy.js"),
40
40
  readAsset("theme-toggle.js"),
41
41
  readAsset("mobile-nav.js"),
42
+ readAsset("image-lightbox.js"),
42
43
  ]);
43
44
  // A function replacer, not a replacement string: String.replace treats
44
45
  // "$&"/"$'"/"$$" etc. in a replacement string as patterns, and a site
@@ -46,7 +47,7 @@ export async function assembleScript(searchFailed) {
46
47
  const searchWithStrings = searchFailed === undefined
47
48
  ? search
48
49
  : search.replace(JSON.stringify(SEARCH_FAILED_DEFAULT), () => JSON.stringify(searchFailed));
49
- return `${searchWithStrings}\n${scrollspy}\n${themeToggle}\n${mobileNav}`;
50
+ return `${searchWithStrings}\n${scrollspy}\n${themeToggle}\n${mobileNav}\n${imageLightbox}`;
50
51
  }
51
52
  /**
52
53
  * CSS canopy-page contributes on top of a site's own tokens, carried via
@@ -54,7 +55,11 @@ export async function assembleScript(searchFailed) {
54
55
  * already uses, so no new canopy surface is needed for this either.
55
56
  */
56
57
  export async function assembleTokensCss(userTokensCss) {
57
- const [search, scrollspy] = await Promise.all([readAsset("search.css"), readAsset("scrollspy.css")]);
58
- const own = `${search}\n${scrollspy}`;
58
+ const [search, scrollspy, imageLightbox] = await Promise.all([
59
+ readAsset("search.css"),
60
+ readAsset("scrollspy.css"),
61
+ readAsset("image-lightbox.css"),
62
+ ]);
63
+ const own = `${search}\n${scrollspy}\n${imageLightbox}`;
59
64
  return userTokensCss === undefined ? own : `${userTokensCss}\n${own}`;
60
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/canopy-page",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Authoring pipeline for documentation sites: one settings file, integrity checks, and a build.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -53,7 +53,7 @@
53
53
  "vitest": "^4.1.9"
54
54
  },
55
55
  "dependencies": {
56
- "@iyulab/canopy": "^0.11.1",
56
+ "@iyulab/canopy": "^0.11.2",
57
57
  "chokidar": "^5.0.0"
58
58
  }
59
59
  }