@dogsbay/plugin-image-zoom 0.2.0-beta.81

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/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @dogsbay/plugin-image-zoom
2
+
3
+ Click-to-zoom for docs images. Drop-in for any Dogsbay site.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @dogsbay/plugin-image-zoom
9
+ ```
10
+
11
+ ## Use
12
+
13
+ ```yaml
14
+ # dogsbay.config.yml
15
+ plugins:
16
+ - "@dogsbay/plugin-image-zoom"
17
+ ```
18
+
19
+ With options:
20
+
21
+ ```yaml
22
+ plugins:
23
+ - name: "@dogsbay/plugin-image-zoom"
24
+ options:
25
+ background: "rgba(0,0,0,0.92)"
26
+ margin: 32
27
+ ```
28
+
29
+ ## Options
30
+
31
+ | Option | Default | Description |
32
+ |---|---|---|
33
+ | `selector` | `"article img"` | CSS selector for which images get the zoom UX. |
34
+ | `background` | `"rgba(0,0,0,0.85)"` | Overlay backdrop. |
35
+ | `margin` | `20` | Pixels of margin around the zoomed image. |
36
+ | `scrollOffset` | `40` | Scroll-to-close threshold. |
37
+ | `excludeInsideLinks` | `true` | Skip images inside `<a>` (the link is the click target). |
38
+
39
+ ## How it works
40
+
41
+ 1. **Build time** — the plugin walks every page's TreeNode AST and
42
+ adds `data-zoomable="true"` to images that aren't inside `<a>`
43
+ links.
44
+ 2. **Run time** — a small client module attaches
45
+ [medium-zoom](https://github.com/francoischalifour/medium-zoom)
46
+ handlers to every `[data-zoomable]` element. The handlers are
47
+ re-bound on every `astro:page-load` so view transitions don't
48
+ leave stale state.
49
+
50
+ The runtime only attaches handlers to pre-tagged elements — no DOM
51
+ scan, no observer, nothing past the click handler itself.
52
+
53
+ ## License
54
+
55
+ MIT.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":""}
package/dist/client.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Client runtime for `@dogsbay/plugin-image-zoom`.
3
+ *
4
+ * Imported via the auto-generated `src/lib/plugin-runtime.ts`
5
+ * entry point in the user's Astro project. The build-time walker
6
+ * has already tagged article images with `data-zoomable="true"`,
7
+ * so this module only needs to (re)bind medium-zoom on every
8
+ * navigation event.
9
+ *
10
+ * Astro view transitions: every `astro:page-load` event causes a
11
+ * detach + reattach. `astro:before-swap` detaches before the new
12
+ * DOM lands so we don't carry stale references.
13
+ */
14
+ import mediumZoom from "medium-zoom";
15
+ // Vite alias — emitted by `dogsbay site build` via
16
+ // `astro.config.plugins.mjs`. Resolves at build time to the
17
+ // JSON-serialized options the plugin baked in.
18
+ // @ts-expect-error: virtual module resolved by Vite alias.
19
+ import config from "virtual:dogsbay-plugin-config/@dogsbay/plugin-image-zoom";
20
+ const runtimeConfig = config;
21
+ let zoom = null;
22
+ function attach() {
23
+ if (zoom) {
24
+ zoom.detach();
25
+ zoom = null;
26
+ }
27
+ zoom = mediumZoom("[data-zoomable]", {
28
+ background: runtimeConfig.background,
29
+ margin: runtimeConfig.margin,
30
+ scrollOffset: runtimeConfig.scrollOffset,
31
+ });
32
+ }
33
+ function detach() {
34
+ if (zoom) {
35
+ zoom.detach();
36
+ zoom = null;
37
+ }
38
+ }
39
+ if (typeof document !== "undefined") {
40
+ if (document.readyState === "loading") {
41
+ document.addEventListener("DOMContentLoaded", attach);
42
+ }
43
+ else {
44
+ attach();
45
+ }
46
+ document.addEventListener("astro:page-load", attach);
47
+ document.addEventListener("astro:before-swap", detach);
48
+ }
49
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,UAAU,MAAM,aAAa,CAAC;AACrC,mDAAmD;AACnD,4DAA4D;AAC5D,+CAA+C;AAC/C,2DAA2D;AAC3D,OAAO,MAAM,MAAM,0DAA0D,CAAC;AAQ9E,MAAM,aAAa,GAAkB,MAAuB,CAAC;AAE7D,IAAI,IAAI,GAAyC,IAAI,CAAC;AAEtD,SAAS,MAAM;IACb,IAAI,IAAI,EAAE,CAAC;QACT,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,IAAI,GAAG,IAAI,CAAC;IACd,CAAC;IACD,IAAI,GAAG,UAAU,CAAC,iBAAiB,EAAE;QACnC,UAAU,EAAE,aAAa,CAAC,UAAU;QACpC,MAAM,EAAE,aAAa,CAAC,MAAM;QAC5B,YAAY,EAAE,aAAa,CAAC,YAAY;KACzC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,MAAM;IACb,IAAI,IAAI,EAAE,CAAC;QACT,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,IAAI,GAAG,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;IACpC,IAAI,QAAQ,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QACtC,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,MAAM,CAAC,CAAC;IACxD,CAAC;SAAM,CAAC;QACN,MAAM,EAAE,CAAC;IACX,CAAC;IACD,QAAQ,CAAC,gBAAgB,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;IACrD,QAAQ,CAAC,gBAAgB,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;AACzD,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { DogsbayPlugin } from "@dogsbay/types";
2
+ import { type ImageZoomOptions } from "./schema.js";
3
+ export type { ImageZoomOptions } from "./schema.js";
4
+ export default function imageZoom(input?: ImageZoomOptions): DogsbayPlugin;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,aAAa,EAAuB,MAAM,gBAAgB,CAAC;AACzE,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAclE,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,KAAK,CAAC,EAAE,gBAAgB,GAAG,aAAa,CA+BzE"}
package/dist/index.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * `@dogsbay/plugin-image-zoom` — adds click-to-enlarge to docs
3
+ * images. Build-time TreeNode walk tags `<img>` nodes; runtime
4
+ * client module attaches medium-zoom handlers to the tagged
5
+ * elements. View-transition aware (rebinds on `astro:page-load`).
6
+ *
7
+ * Usage:
8
+ *
9
+ * # dogsbay.config.yml
10
+ * plugins:
11
+ * - "@dogsbay/plugin-image-zoom"
12
+ *
13
+ * # or with options
14
+ * plugins:
15
+ * - name: "@dogsbay/plugin-image-zoom"
16
+ * options:
17
+ * background: "rgba(0,0,0,0.92)"
18
+ * margin: 32
19
+ *
20
+ * See plans/plugin-api.md for the API contract.
21
+ */
22
+ import path from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+ import { parseOptions } from "./schema.js";
25
+ import { tagImagesInTree } from "./walker.js";
26
+ // __dirname equivalent for an ESM-published package. Resolves the
27
+ // directory the built `dist/index.js` lives in so we can hand
28
+ // absolute paths to the Dogsbay plugin runtime.
29
+ const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url));
30
+ // __dirname is `dist/`; the runtime emits absolute paths to the
31
+ // built `client.js` and the (un-built) `styles.css` shipped at the
32
+ // package root under `src/styles.css`.
33
+ const PACKAGE_ROOT = path.resolve(PLUGIN_DIR, "..");
34
+ const CLIENT_PATH = path.join(PLUGIN_DIR, "client.js");
35
+ const STYLES_PATH = path.join(PACKAGE_ROOT, "src", "styles.css");
36
+ export default function imageZoom(input) {
37
+ const opts = parseOptions(input);
38
+ return {
39
+ name: "@dogsbay/plugin-image-zoom",
40
+ onContentImported(pages, nav) {
41
+ const tagged = pages.map((page) => ({
42
+ ...page,
43
+ tree: tagImagesInTree(page.tree, opts),
44
+ }));
45
+ return { pages: tagged, nav };
46
+ },
47
+ clientModules() {
48
+ return [CLIENT_PATH];
49
+ },
50
+ styles() {
51
+ return [STYLES_PATH];
52
+ },
53
+ defineClientConfig() {
54
+ // Only the runtime-relevant subset. JSON-safe.
55
+ return {
56
+ background: opts.background,
57
+ margin: opts.margin,
58
+ scrollOffset: opts.scrollOffset,
59
+ };
60
+ },
61
+ };
62
+ }
63
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,YAAY,EAAyB,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,kEAAkE;AAClE,8DAA8D;AAC9D,gDAAgD;AAChD,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAChE,gEAAgE;AAChE,mEAAmE;AACnE,uCAAuC;AACvC,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;AACpD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;AACvD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC;AAIjE,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,KAAwB;IACxD,MAAM,IAAI,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IAEjC,OAAO;QACL,IAAI,EAAE,4BAA4B;QAElC,iBAAiB,CAAC,KAAmB,EAAE,GAAc;YACnD,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;gBAClC,GAAG,IAAI;gBACP,IAAI,EAAE,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC;aACvC,CAAC,CAAC,CAAC;YACJ,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;QAChC,CAAC;QAED,aAAa;YACX,OAAO,CAAC,WAAW,CAAC,CAAC;QACvB,CAAC;QAED,MAAM;YACJ,OAAO,CAAC,WAAW,CAAC,CAAC;QACvB,CAAC;QAED,kBAAkB;YAChB,+CAA+C;YAC/C,OAAO;gBACL,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,YAAY,EAAE,IAAI,CAAC,YAAY;aAChC,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Options schema for `@dogsbay/plugin-image-zoom`.
3
+ *
4
+ * Validation is hand-rolled to avoid a Zod dep — the option set is
5
+ * small enough that the boilerplate is in noise. The `parseOptions`
6
+ * function returns a fully-resolved options object with every field
7
+ * set to either the user value or the documented default.
8
+ */
9
+ export interface ImageZoomOptions {
10
+ /**
11
+ * CSS selector matched against TreeNode image nodes during the
12
+ * build-time walk. The walker tags matching `<img>` elements
13
+ * with `data-zoomable="true"` so the runtime doesn't need to
14
+ * scan the DOM. Default: every `<img>` inside `<article>`.
15
+ *
16
+ * NOTE: Currently informational — the build-time walker tags
17
+ * every image node that isn't inside a link. The selector
18
+ * primarily documents intent and may drive a future
19
+ * predicate-based filter. Use `excludeInsideLinks` and the
20
+ * runtime selector to control the runtime side.
21
+ */
22
+ selector?: string;
23
+ /** Background colour of the zoom overlay. Default rgba(0,0,0,.85). */
24
+ background?: string;
25
+ /** Pixels of margin around the zoomed image. Default 20. */
26
+ margin?: number;
27
+ /** Pixels of scroll before the overlay closes. Default 40. */
28
+ scrollOffset?: number;
29
+ /**
30
+ * When true (default), images inside `<a>` links are not
31
+ * tagged — the click already navigates somewhere. Setting to
32
+ * false zooms every image regardless.
33
+ */
34
+ excludeInsideLinks?: boolean;
35
+ }
36
+ export interface ResolvedImageZoomOptions {
37
+ selector: string;
38
+ background: string;
39
+ margin: number;
40
+ scrollOffset: number;
41
+ excludeInsideLinks: boolean;
42
+ }
43
+ /**
44
+ * Validate + resolve user options. Throws structured errors with
45
+ * the offending field path so users see "options.margin must be a
46
+ * non-negative integer" rather than a cryptic stack.
47
+ */
48
+ export declare function parseOptions(input: unknown): ResolvedImageZoomOptions;
49
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAUD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,wBAAwB,CAyDrE"}
package/dist/schema.js ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Options schema for `@dogsbay/plugin-image-zoom`.
3
+ *
4
+ * Validation is hand-rolled to avoid a Zod dep — the option set is
5
+ * small enough that the boilerplate is in noise. The `parseOptions`
6
+ * function returns a fully-resolved options object with every field
7
+ * set to either the user value or the documented default.
8
+ */
9
+ const DEFAULTS = {
10
+ selector: "article img",
11
+ background: "rgba(0, 0, 0, 0.85)",
12
+ margin: 20,
13
+ scrollOffset: 40,
14
+ excludeInsideLinks: true,
15
+ };
16
+ /**
17
+ * Validate + resolve user options. Throws structured errors with
18
+ * the offending field path so users see "options.margin must be a
19
+ * non-negative integer" rather than a cryptic stack.
20
+ */
21
+ export function parseOptions(input) {
22
+ if (input === undefined || input === null)
23
+ return { ...DEFAULTS };
24
+ if (typeof input !== "object" || Array.isArray(input)) {
25
+ throw new Error(`@dogsbay/plugin-image-zoom: options must be an object, got ${describe(input)}.`);
26
+ }
27
+ const opts = input;
28
+ const out = { ...DEFAULTS };
29
+ if (opts.selector !== undefined) {
30
+ if (typeof opts.selector !== "string" || opts.selector.trim() === "") {
31
+ throw new Error(`@dogsbay/plugin-image-zoom: options.selector must be a non-empty string.`);
32
+ }
33
+ out.selector = opts.selector;
34
+ }
35
+ if (opts.background !== undefined) {
36
+ if (typeof opts.background !== "string" || opts.background.trim() === "") {
37
+ throw new Error(`@dogsbay/plugin-image-zoom: options.background must be a non-empty string.`);
38
+ }
39
+ out.background = opts.background;
40
+ }
41
+ if (opts.margin !== undefined) {
42
+ if (typeof opts.margin !== "number" || !Number.isInteger(opts.margin) || opts.margin < 0) {
43
+ throw new Error(`@dogsbay/plugin-image-zoom: options.margin must be a non-negative integer.`);
44
+ }
45
+ out.margin = opts.margin;
46
+ }
47
+ if (opts.scrollOffset !== undefined) {
48
+ if (typeof opts.scrollOffset !== "number" ||
49
+ !Number.isInteger(opts.scrollOffset) ||
50
+ opts.scrollOffset < 0) {
51
+ throw new Error(`@dogsbay/plugin-image-zoom: options.scrollOffset must be a non-negative integer.`);
52
+ }
53
+ out.scrollOffset = opts.scrollOffset;
54
+ }
55
+ if (opts.excludeInsideLinks !== undefined) {
56
+ if (typeof opts.excludeInsideLinks !== "boolean") {
57
+ throw new Error(`@dogsbay/plugin-image-zoom: options.excludeInsideLinks must be a boolean.`);
58
+ }
59
+ out.excludeInsideLinks = opts.excludeInsideLinks;
60
+ }
61
+ return out;
62
+ }
63
+ function describe(value) {
64
+ if (value === null)
65
+ return "null";
66
+ if (Array.isArray(value))
67
+ return "array";
68
+ return typeof value;
69
+ }
70
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Build-time TreeNode walker — tags every page image with
3
+ * `data-zoomable="true"` so the runtime can skip its own DOM
4
+ * scan and bind handlers only to pre-marked elements.
5
+ *
6
+ * Skips images inside link nodes when `excludeInsideLinks` is on
7
+ * (the link click already does something — zooming would be
8
+ * surprising).
9
+ *
10
+ * Tolerates the cross-format TreeNode shape variations:
11
+ * - inline images attached to a paragraph via `inline: [{type: "image"}]`
12
+ * - block-level images as `{type: "image"}` children
13
+ * - `link` nodes by either `type: "link"` or `type: "a"`
14
+ */
15
+ import type { TreeNode } from "@dogsbay/types";
16
+ import type { ResolvedImageZoomOptions } from "./schema.js";
17
+ export declare function tagImagesInTree(nodes: TreeNode[], opts: ResolvedImageZoomOptions): TreeNode[];
18
+ //# sourceMappingURL=walker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"walker.d.ts","sourceRoot":"","sources":["../src/walker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAc,MAAM,gBAAgB,CAAC;AAC3D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAE5D,wBAAgB,eAAe,CAC7B,KAAK,EAAE,QAAQ,EAAE,EACjB,IAAI,EAAE,wBAAwB,GAC7B,QAAQ,EAAE,CAEZ"}
package/dist/walker.js ADDED
@@ -0,0 +1,71 @@
1
+ export function tagImagesInTree(nodes, opts) {
2
+ return nodes.map((n) => walkNode(n, opts, false));
3
+ }
4
+ function walkNode(node, opts, insideLink) {
5
+ const isLink = node.type === "link" || node.type === "a";
6
+ const recurseInsideLink = insideLink || isLink;
7
+ // Block-level image — tag it.
8
+ if (node.type === "image") {
9
+ if (insideLink && opts.excludeInsideLinks)
10
+ return node;
11
+ return {
12
+ ...node,
13
+ props: { ...(node.props ?? {}), "data-zoomable": "true" },
14
+ };
15
+ }
16
+ let next = node;
17
+ // Recurse into block children.
18
+ if (node.children?.length) {
19
+ next = {
20
+ ...next,
21
+ children: node.children.map((c) => walkNode(c, opts, recurseInsideLink)),
22
+ };
23
+ }
24
+ // Recurse into inline children. Inline images live as an entry in
25
+ // the inline array; tag them via an inline-side helper.
26
+ if (node.inline?.length) {
27
+ next = {
28
+ ...next,
29
+ inline: walkInline(node.inline, opts, recurseInsideLink),
30
+ };
31
+ }
32
+ // Special-case the "paragraph with only one inline image" shape
33
+ // — the format-astro serializer emits a single <BlockImage /> for
34
+ // that. We tag the paragraph itself so BlockImage emission can
35
+ // forward `data-zoomable`.
36
+ if (next.type === "paragraph" && next.inline?.length) {
37
+ const meaningful = next.inline.filter((n) => !(n.type === "text" && !("text" in n && n.text.trim())));
38
+ if (meaningful.length === 1 &&
39
+ meaningful[0].type === "image" &&
40
+ !(insideLink && opts.excludeInsideLinks)) {
41
+ next = {
42
+ ...next,
43
+ props: { ...(next.props ?? {}), "data-zoomable": "true" },
44
+ };
45
+ }
46
+ }
47
+ return next;
48
+ }
49
+ function walkInline(nodes, opts, insideLink) {
50
+ return nodes.map((n) => {
51
+ if (n.type === "image") {
52
+ if (insideLink && opts.excludeInsideLinks)
53
+ return n;
54
+ // InlineImage doesn't have a typed `data-*` slot; we attach
55
+ // via a meta-style cast. The astro serializer reads the
56
+ // resulting attribute when emitting the <img> element.
57
+ return {
58
+ ...n,
59
+ // The serializer maps unknown extra props onto the <img>
60
+ // tag — see packages/format-astro/src/serialize.ts. Casting
61
+ // through unknown to keep the InlineNode union types clean.
62
+ ...{ "data-zoomable": "true" },
63
+ };
64
+ }
65
+ if (n.type === "link" && n.children?.length) {
66
+ return { ...n, children: walkInline(n.children, opts, true) };
67
+ }
68
+ return n;
69
+ });
70
+ }
71
+ //# sourceMappingURL=walker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"walker.js","sourceRoot":"","sources":["../src/walker.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@dogsbay/plugin-image-zoom",
3
+ "version": "0.2.0-beta.81",
4
+ "description": "Click-to-zoom for images in Dogsbay docs sites.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./client": "./dist/client.js",
14
+ "./styles.css": "./src/styles.css"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src/styles.css",
19
+ "src/client.ts",
20
+ "README.md"
21
+ ],
22
+ "dependencies": {
23
+ "medium-zoom": "^1.1.0",
24
+ "@dogsbay/types": "0.2.0-beta.81"
25
+ },
26
+ "devDependencies": {
27
+ "typescript": "^5.7.0",
28
+ "vitest": "^3.0.0",
29
+ "@types/node": "^22.0.0"
30
+ },
31
+ "license": "MIT",
32
+ "keywords": [
33
+ "dogsbay",
34
+ "dogsbay-plugin",
35
+ "documentation",
36
+ "image-zoom"
37
+ ],
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/dogsbay/dogsbay.git",
41
+ "directory": "packages/plugin-image-zoom"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest"
47
+ }
48
+ }
package/src/client.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Client runtime for `@dogsbay/plugin-image-zoom`.
3
+ *
4
+ * Imported via the auto-generated `src/lib/plugin-runtime.ts`
5
+ * entry point in the user's Astro project. The build-time walker
6
+ * has already tagged article images with `data-zoomable="true"`,
7
+ * so this module only needs to (re)bind medium-zoom on every
8
+ * navigation event.
9
+ *
10
+ * Astro view transitions: every `astro:page-load` event causes a
11
+ * detach + reattach. `astro:before-swap` detaches before the new
12
+ * DOM lands so we don't carry stale references.
13
+ */
14
+ import mediumZoom from "medium-zoom";
15
+ // Vite alias — emitted by `dogsbay site build` via
16
+ // `astro.config.plugins.mjs`. Resolves at build time to the
17
+ // JSON-serialized options the plugin baked in.
18
+ // @ts-expect-error: virtual module resolved by Vite alias.
19
+ import config from "virtual:dogsbay-plugin-config/@dogsbay/plugin-image-zoom";
20
+
21
+ interface RuntimeConfig {
22
+ background: string;
23
+ margin: number;
24
+ scrollOffset: number;
25
+ }
26
+
27
+ const runtimeConfig: RuntimeConfig = config as RuntimeConfig;
28
+
29
+ let zoom: ReturnType<typeof mediumZoom> | null = null;
30
+
31
+ function attach(): void {
32
+ if (zoom) {
33
+ zoom.detach();
34
+ zoom = null;
35
+ }
36
+ zoom = mediumZoom("[data-zoomable]", {
37
+ background: runtimeConfig.background,
38
+ margin: runtimeConfig.margin,
39
+ scrollOffset: runtimeConfig.scrollOffset,
40
+ });
41
+ }
42
+
43
+ function detach(): void {
44
+ if (zoom) {
45
+ zoom.detach();
46
+ zoom = null;
47
+ }
48
+ }
49
+
50
+ if (typeof document !== "undefined") {
51
+ if (document.readyState === "loading") {
52
+ document.addEventListener("DOMContentLoaded", attach);
53
+ } else {
54
+ attach();
55
+ }
56
+ document.addEventListener("astro:page-load", attach);
57
+ document.addEventListener("astro:before-swap", detach);
58
+ }
package/src/styles.css ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Styles shipped by @dogsbay/plugin-image-zoom.
3
+ *
4
+ * Imported into the user's `src/styles/global.css` by `dogsbay
5
+ * site build` via the plugin runtime emitter.
6
+ */
7
+
8
+ [data-zoomable] {
9
+ cursor: zoom-in;
10
+ transition: transform 200ms ease;
11
+ }
12
+
13
+ .medium-zoom-overlay {
14
+ z-index: 100;
15
+ }
16
+
17
+ .medium-zoom-image {
18
+ cursor: zoom-out;
19
+ z-index: 101;
20
+ }
21
+
22
+ .medium-zoom--opened [data-zoomable] {
23
+ cursor: default;
24
+ }