heroshot 0.13.0 → 0.14.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 (29) hide show
  1. package/README.md +13 -7
  2. package/dist/cli/cli.js +3 -2
  3. package/dist/index.d.ts +44 -0
  4. package/dist/index.js +49 -0
  5. package/dist/integrations/next/index.d.ts +3 -0
  6. package/dist/integrations/next/index.js +134 -0
  7. package/dist/integrations/shared/docusaurusPlugin.cjs +1 -1
  8. package/dist/integrations/shared/docusaurusPlugin.js +1 -1
  9. package/dist/integrations/shared/getManifest-BMMzQAE9.cjs +1 -0
  10. package/dist/integrations/shared/{getManifest-CZSH5yc7.js → getManifest-DfRVQbEx.js} +25 -22
  11. package/dist/integrations/shared/nextPlugin.cjs +5 -0
  12. package/dist/integrations/shared/nextPlugin.d.ts +50 -0
  13. package/dist/integrations/shared/nextPlugin.js +41 -0
  14. package/dist/integrations/shared/virtual.d.ts +23 -0
  15. package/dist/integrations/shared/vitePlugin.cjs +2 -2
  16. package/dist/integrations/shared/vitePlugin.js +29 -29
  17. package/dist/integrations/svelte/components/Heroshot.svelte +197 -0
  18. package/dist/integrations/svelte/index.d.ts +25 -0
  19. package/dist/integrations/svelte/index.js +4 -0
  20. package/dist/integrations/svelte/shared.js +43 -0
  21. package/dist/mcp/index.js +1 -1
  22. package/dist/{snippet-Bwn8XcVs.js → snippet-B6Lg_Ant.js} +16 -9
  23. package/editor/dist/editor.js +23 -5
  24. package/package.json +27 -1
  25. package/dist/integrations/shared/getManifest-ClS1WrNU.cjs +0 -1
  26. /package/dist/integrations/react/{react/src/components → components}/Heroshot.d.ts +0 -0
  27. /package/dist/integrations/react/{react/src/index.d.ts → index.d.ts} +0 -0
  28. /package/dist/integrations/vue/{vue/src/components → components}/Heroshot.vue.d.ts +0 -0
  29. /package/dist/integrations/vue/{vue/src/index.d.ts → index.d.ts} +0 -0
package/README.md CHANGED
@@ -9,26 +9,28 @@
9
9
  <p align="center"><em>👆 This hero shot of <a href="https://heroshot.sh">heroshot.sh</a> is <a href="https://github.com/omachala/heroshot/blob/main/.github/workflows/update-screenshots.yml#L17">taken</a> by heroshot ⚡️</em></p>
10
10
 
11
11
  <p align="center">
12
+ <a href="https://www.npmjs.com/package/heroshot"><img src="https://img.shields.io/npm/dt/heroshot?style=for-the-badge" alt="npm downloads"></a>
12
13
  <a href="https://www.npmjs.com/package/heroshot"><img src="https://img.shields.io/npm/v/heroshot?style=for-the-badge&logo=npm" alt="npm version"></a>
13
- <a href="https://github.com/omachala/heroshot/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/heroshot?style=for-the-badge" alt="license"></a>
14
14
  <a href="https://codecov.io/gh/omachala/heroshot"><img src="https://img.shields.io/codecov/c/github/omachala/heroshot?style=for-the-badge" alt="coverage"></a>
15
15
  <a href="https://sonarcloud.io/summary/new_code?id=omachala_heroshot"><img src="https://img.shields.io/sonar/quality_gate/omachala_heroshot?server=https%3A%2F%2Fsonarcloud.io&style=for-the-badge&logo=sonarcloud" alt="quality gate"></a>
16
16
  <a href="https://heroshot.sh"><img src="https://img.shields.io/badge/docs-heroshot.sh-blue?style=for-the-badge" alt="docs"></a>
17
17
  </p>
18
18
 
19
+ <p align="center">Like Heroshot? Share your story — <a href="https://x.com/intent/tweet?url=https%3A%2F%2Fheroshot.sh">X/Twitter</a> · <a href="https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fheroshot.sh">LinkedIn</a> · <a href="https://www.reddit.com/submit?url=https%3A%2F%2Fheroshot.sh&title=Heroshot%20%E2%80%93%20Define%20screenshots%20once%2C%20update%20them%20forever">Reddit</a> ❤️</p>
20
+
19
21
  Your app changes constantly. New features, design tweaks, bug fixes. Meanwhile, the screenshots in your README and docs quietly become lies.
20
22
 
21
23
  The manual fix is tedious: open browser, navigate, log in, screenshot, crop, save, commit. Now do that for every image. Now do it again next month.
22
24
 
23
- **Heroshot fixes this.** Define your screenshots once - point and click, no CSS selectors - and regenerate them with one command whenever you need.
25
+ **Heroshot fixes this.** Define your screenshots once - point and click, no CSS selectors. Style them with the visual editor, add annotations to highlight what matters, and regenerate everything with one command.
24
26
 
25
27
  ```bash
26
28
  npx heroshot
27
29
  ```
28
30
 
29
- First run opens a browser with a visual picker. Click what you want, name it, done. Screenshots land in `heroshots/`, config saves to `.heroshot/config.json`. Next run regenerates everything headlessly.
31
+ First run opens a browser with a visual editor. Pick elements, adjust padding, style borders, edit text, and add annotations (arrows, rectangles, callouts). Screenshots land in `heroshots/`, config saves to `.heroshot/config.json`. Next run regenerates everything headlessly.
30
32
 
31
- https://github.com/user-attachments/assets/f35600a6-9220-4bd2-a8c6-a6b4ee8a33d9
33
+ https://github.com/user-attachments/assets/1636d404-1e5f-4151-9aba-d5676ed3ff2a
32
34
 
33
35
  ## Use in Your Docs
34
36
 
@@ -104,11 +106,15 @@ One component/macro, all variants - light/dark mode switches automatically, resp
104
106
  | **CI/CD Setup** | [Automated updates](https://heroshot.sh/docs/guide/automated-updates) |
105
107
  | **CLI Reference** | [All commands & flags](https://heroshot.sh/docs/cli) |
106
108
 
107
- ## Contributing
109
+ ## Support the Project
110
+
111
+ Your suggestions and feedback are highly appreciated. Please feel free to [start a discussion](https://github.com/omachala/heroshot/discussions) or [create an issue](https://github.com/omachala/heroshot/issues) to share your experience with the tool or to discuss a feature/issue.
108
112
 
109
- This is a community project aiming to solve screenshot automation end-to-end and any feedback is valuable. Open an [issue](https://github.com/omachala/heroshot/issues) for bugs, questions, or feature requests. Pull requests are more than welcome.
113
+ If you find heroshot useful, saves you a lot of work, and lets you sleep much better, then consider supporting the project by any of the following means:
110
114
 
111
- If you like it, give the repo a ⭐
115
+ - **Star the repo** — it helps others discover heroshot
116
+ - **Spread the word** — share the project on social media or with friends
117
+ - **Report bugs or propose solutions** — open an [issue](https://github.com/omachala/heroshot/issues) or [pull request](https://github.com/omachala/heroshot/pulls)
112
118
 
113
119
  ## License
114
120
 
package/dist/cli/cli.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { C as intro, D as setVerbose, E as outro, O as spinner, S as error, T as note, _ as loadConfig, a as launchBrowser, c as generateSessionKey, d as loadSession, f as saveLocalKey, g as getConfigPath, h as ensureHeroshotDirectory, k as verbose, l as getSessionPath, m as sessionExists, n as snippetAction, o as DEFAULT_VIEWPORT, p as saveSession, r as sync, s as EDITOR_DIR, u as loadLocalKey, v as saveConfig, w as log, x as generateUid, y as VIEWPORT_PRESETS } from "../snippet-Bwn8XcVs.js";
2
+ import { C as intro, D as setVerbose, E as outro, O as spinner, S as error, T as note, _ as loadConfig, a as launchBrowser, c as generateSessionKey, d as loadSession, f as saveLocalKey, g as getConfigPath, h as ensureHeroshotDirectory, k as verbose, l as getSessionPath, m as sessionExists, n as snippetAction, o as DEFAULT_VIEWPORT, p as saveSession, r as sync, s as EDITOR_DIR, u as loadLocalKey, v as saveConfig, w as log, x as generateUid, y as VIEWPORT_PRESETS } from "../snippet-B6Lg_Ant.js";
3
3
  import { existsSync, readFileSync, rmSync } from "node:fs";
4
4
  import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
5
6
  import { Command } from "commander";
6
7
  import { z } from "zod";
7
8
 
@@ -721,7 +722,7 @@ function listAction(options, globalOptions) {
721
722
  function getVersion() {
722
723
  if (typeof HEROSHOT_VERSION !== "undefined") return HEROSHOT_VERSION;
723
724
  try {
724
- const packageJsonPath = path.join(import.meta.dirname, "..", "..", "package.json");
725
+ const packageJsonPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
725
726
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
726
727
  if (packageJson && typeof packageJson === "object" && "version" in packageJson) return String(packageJson.version);
727
728
  } catch {}
@@ -0,0 +1,44 @@
1
+
2
+ //#region integrations/shared/types.d.ts
3
+ /**
4
+ * Shared types for heroshot framework integrations.
5
+ */
6
+ /** Screenshot info needed by components */
7
+ type ScreenshotInfo = {
8
+ /** Slugified base filename (without extension) */slug: string; /** Viewport variants (empty array if none) */
9
+ viewports: string[]; /** Color schemes captured */
10
+ colorSchemes: ('light' | 'dark')[]; /** Output format */
11
+ format: 'png' | 'jpeg';
12
+ };
13
+ /** Manifest structure (derived from config.json) */
14
+ type Manifest = {
15
+ /** Manifest version for compatibility */version: 1; /** Output directory relative to project root */
16
+ outputDirectory: string; /** Screenshots indexed by name */
17
+ screenshots: Record<string, ScreenshotInfo>;
18
+ };
19
+ //#endregion
20
+ //#region integrations/shared/configTransform.d.ts
21
+ /**
22
+ * Raw config.json structure (subset of fields we need)
23
+ */
24
+ interface ConfigJson {
25
+ outputDirectory?: string;
26
+ outputFormat?: 'png' | 'jpeg';
27
+ browser?: {
28
+ colorScheme?: 'light' | 'dark';
29
+ };
30
+ screenshots: Array<{
31
+ id: string;
32
+ name: string;
33
+ viewports?: string[];
34
+ }>;
35
+ }
36
+ /**
37
+ * Transform config.json into manifest structure.
38
+ *
39
+ * This is a pure function - no file system access needed.
40
+ * Use it when you already have the config object in memory.
41
+ */
42
+ declare function configToManifest(config: ConfigJson): Manifest;
43
+ //#endregion
44
+ export { ConfigJson, type Manifest, type ScreenshotInfo, configToManifest };
package/dist/index.js ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ //#region integrations/shared/configTransform.ts
3
+ /**
4
+ * Slugify a single path segment for use in filenames
5
+ */
6
+ function slugifySegment(text) {
7
+ return text.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/(?:^-|-$)/g, "");
8
+ }
9
+ /**
10
+ * Slugify a string for use in filenames.
11
+ * Preserves forward slashes to support subdirectory output paths.
12
+ */
13
+ function slugify(text) {
14
+ return text.split("/").map(slugifySegment).filter(Boolean).join("/");
15
+ }
16
+ /**
17
+ * Determine color schemes from config
18
+ */
19
+ function getColorSchemes(colorScheme) {
20
+ if (colorScheme === "light") return ["light"];
21
+ if (colorScheme === "dark") return ["dark"];
22
+ return ["light", "dark"];
23
+ }
24
+ /**
25
+ * Transform config.json into manifest structure.
26
+ *
27
+ * This is a pure function - no file system access needed.
28
+ * Use it when you already have the config object in memory.
29
+ */
30
+ function configToManifest(config) {
31
+ const colorSchemes = getColorSchemes(config.browser?.colorScheme);
32
+ const format = config.outputFormat ?? "png";
33
+ const outputDirectory = config.outputDirectory ?? "heroshots";
34
+ const screenshots = {};
35
+ for (const screenshot of config.screenshots) screenshots[screenshot.name] = {
36
+ slug: slugify(screenshot.name),
37
+ viewports: screenshot.viewports ?? [],
38
+ colorSchemes,
39
+ format
40
+ };
41
+ return {
42
+ version: 1,
43
+ outputDirectory,
44
+ screenshots
45
+ };
46
+ }
47
+
48
+ //#endregion
49
+ export { configToManifest };
@@ -0,0 +1,3 @@
1
+ export { Heroshot, HeroshotProvider } from '../../react/src/components/Heroshot';
2
+ export { setManifest } from '../../shared/manifestStore';
3
+ export type { Manifest } from '../../shared/types';
@@ -0,0 +1,134 @@
1
+ "use client";
2
+ import { jsx as h, jsxs as y } from "react/jsx-runtime";
3
+ import I, { createContext as $, useContext as j, useMemo as g, useState as C, useEffect as L } from "react";
4
+ function z(e, t) {
5
+ return e.screenshots[t];
6
+ }
7
+ function N(e, t = {}) {
8
+ const { slug: a, format: s } = e, { viewport: r, colorScheme: c } = t, n = [a];
9
+ r && n.push(r), c && n.push(c);
10
+ const i = s === "jpeg" ? "jpg" : "png";
11
+ return `${n.join("-")}.${i}`;
12
+ }
13
+ function O(e, t, a = {}) {
14
+ const s = N(t, a);
15
+ return `${e.outputDirectory}/${s}`;
16
+ }
17
+ function H(e, t, a) {
18
+ const { colorSchemes: s } = t, r = s.length > 0, c = (i) => O(e, t, { viewport: a, colorScheme: i }), n = {
19
+ default: r ? c("light") : c()
20
+ };
21
+ return s.includes("light") && (n.light = c("light")), s.includes("dark") && (n.dark = c("dark")), n;
22
+ }
23
+ function R(e, t) {
24
+ const a = H(e, t), s = {};
25
+ for (const r of t.viewports)
26
+ s[r] = H(e, t, r);
27
+ return {
28
+ ...a,
29
+ viewports: s
30
+ };
31
+ }
32
+ let x = null;
33
+ function Q(e) {
34
+ x = e;
35
+ }
36
+ function V() {
37
+ return x;
38
+ }
39
+ const D = $(null);
40
+ function _({
41
+ manifest: e,
42
+ children: t
43
+ }) {
44
+ return /* @__PURE__ */ h(D.Provider, { value: e, children: t });
45
+ }
46
+ function S() {
47
+ if (globalThis.window === void 0) return { isDark: !1, hasThemeHandling: !1 };
48
+ const { theme: e } = document.documentElement.dataset;
49
+ return e ? { isDark: e === "dark", hasThemeHandling: !0 } : document.documentElement.classList.contains("dark") ? { isDark: !0, hasThemeHandling: !0 } : document.documentElement.classList.length > 0 ? { isDark: !1, hasThemeHandling: !0 } : globalThis.matchMedia?.("(prefers-color-scheme: dark)").matches ? { isDark: !0, hasThemeHandling: !1 } : { isDark: !1, hasThemeHandling: !1 };
50
+ }
51
+ function A() {
52
+ const e = I.useRef(!1), [t, a] = C(() => S().isDark);
53
+ return L(() => {
54
+ const s = S();
55
+ e.current = s.hasThemeHandling;
56
+ const r = () => {
57
+ const { theme: u } = document.documentElement.dataset;
58
+ return u ? (e.current = !0, u === "dark") : document.documentElement.classList.contains("dark") ? (e.current = !0, !0) : e.current ? !1 : globalThis.matchMedia?.("(prefers-color-scheme: dark)").matches ?? !1;
59
+ }, c = new MutationObserver(() => {
60
+ e.current = !0, a(r());
61
+ });
62
+ c.observe(document.documentElement, {
63
+ attributes: !0,
64
+ attributeFilter: ["data-theme", "class"]
65
+ });
66
+ const n = globalThis.matchMedia?.("(prefers-color-scheme: dark)"), i = () => {
67
+ a(r());
68
+ };
69
+ return n?.addEventListener("change", i), () => {
70
+ c.disconnect(), n?.removeEventListener("change", i);
71
+ };
72
+ }, []), t;
73
+ }
74
+ const k = {
75
+ mobile: 430,
76
+ // iPhone 15/16 Pro Max viewport
77
+ tablet: 768,
78
+ desktop: 1280
79
+ };
80
+ function q({
81
+ name: e,
82
+ alt: t = "",
83
+ manifest: a,
84
+ className: s
85
+ }) {
86
+ const r = A(), c = j(D), n = a ?? c ?? V(), i = g(() => n ? z(n, e) : null, [n, e]), u = g(() => !i || !n ? null : R(n, i), [n, i]), v = g(() => {
87
+ if (!u) return "";
88
+ const { light: o, dark: f } = u;
89
+ return r && f ? f : !r && o ? o : u.default;
90
+ }, [u, r]), T = g(() => {
91
+ if (!u || !i) return [];
92
+ const { viewports: o } = u, f = Object.keys(o);
93
+ if (f.length === 0) return [];
94
+ const b = [...f].sort((l, m) => {
95
+ const d = k[l] || Number.parseInt(l.split("x")[0] || "1280", 10), p = k[m] || Number.parseInt(m.split("x")[0] || "1280", 10);
96
+ return d - p;
97
+ });
98
+ return b.map((l, m) => {
99
+ const d = o[l];
100
+ if (!d) return null;
101
+ const p = d.light || d.default, E = d.dark || d.light || d.default, w = k[l] || Number.parseInt(l.split("x")[0] || "1280", 10), M = m === b.length - 1;
102
+ return {
103
+ viewport: l,
104
+ srcset: r ? E : p,
105
+ width: w,
106
+ media: M ? void 0 : `(max-width: ${w}px)`
107
+ };
108
+ }).filter((l) => l !== null);
109
+ }, [u, i, r]), P = T.length > 0;
110
+ if (!n) {
111
+ const o = "Heroshot: No manifest found. Add heroshot() plugin to vite config, use HeroshotProvider, or pass manifest prop.";
112
+ return typeof console < "u" && console.warn(o), /* @__PURE__ */ h("span", { style: { color: "red", fontSize: "12px" }, children: o });
113
+ }
114
+ if (!i) {
115
+ const o = `Heroshot: Screenshot "${e}" not found in config`;
116
+ return typeof console < "u" && console.warn(o), /* @__PURE__ */ h("span", { style: { color: "red", fontSize: "12px" }, children: o });
117
+ }
118
+ return P ? /* @__PURE__ */ y("picture", { className: s, children: [
119
+ T.map((o) => /* @__PURE__ */ h(
120
+ "source",
121
+ {
122
+ srcSet: o.srcset,
123
+ media: o.media
124
+ },
125
+ `${o.viewport}-${r}`
126
+ )),
127
+ /* @__PURE__ */ h("img", { src: v, alt: t, loading: "lazy" })
128
+ ] }) : /* @__PURE__ */ h("img", { src: v, alt: t, className: s, loading: "lazy" });
129
+ }
130
+ export {
131
+ q as Heroshot,
132
+ _ as HeroshotProvider,
133
+ Q as setManifest
134
+ };
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=require("node:fs"),n=require("node:path"),s=require("./getManifest-ClS1WrNU.cjs");function h(t){return t.startsWith("static/")?t.slice(7):t}function g(t={}){return function(l){const{siteDir:a,generatedFilesDir:c}=l;let e=null;t.config?e=n.resolve(a,t.config):e=s.findConfig(a);let f=s.emptyManifest();if(e&&o.existsSync(e)){const r=s.loadManifest(e);r&&(f={...r,outputDirectory:h(r.outputDirectory)},console.log(`[heroshot] Loaded config from ${e}`))}else console.log("[heroshot] No config found, using empty manifest");const i=n.join(c,"heroshot-manifest.json");o.mkdirSync(n.dirname(i),{recursive:!0}),o.writeFileSync(i,JSON.stringify(f,null,2));const u=n.join(c,"heroshot-client.js");return o.writeFileSync(u,`import { setManifest } from 'heroshot/docusaurus';
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=require("node:fs"),n=require("node:path"),s=require("./getManifest-BMMzQAE9.cjs");function h(t){return t.startsWith("static/")?t.slice(7):t}function g(t={}){return function(l){const{siteDir:a,generatedFilesDir:c}=l;let e=null;t.config?e=n.resolve(a,t.config):e=s.findConfig(a);let f=s.emptyManifest();if(e&&o.existsSync(e)){const r=s.loadManifest(e);r&&(f={...r,outputDirectory:h(r.outputDirectory)},console.log(`[heroshot] Loaded config from ${e}`))}else console.log("[heroshot] No config found, using empty manifest");const i=n.join(c,"heroshot-manifest.json");o.mkdirSync(n.dirname(i),{recursive:!0}),o.writeFileSync(i,JSON.stringify(f,null,2));const u=n.join(c,"heroshot-client.js");return o.writeFileSync(u,`import { setManifest } from 'heroshot/docusaurus';
2
2
  import manifest from '@heroshot/manifest';
3
3
  setManifest(manifest);
4
4
  `),{name:"heroshot",configureWebpack(){return{resolve:{alias:{"@heroshot/manifest":i}}}},getClientModules(){return[u]},getPathsToWatch(){return e?[e]:[]}}}}exports.heroshot=g;
@@ -1,6 +1,6 @@
1
1
  import { existsSync as l, mkdirSync as h, writeFileSync as a } from "node:fs";
2
2
  import { resolve as m, join as c, dirname as g } from "node:path";
3
- import { f as d, l as p, e as y } from "./getManifest-CZSH5yc7.js";
3
+ import { f as d, l as p, e as y } from "./getManifest-DfRVQbEx.js";
4
4
  function M(t) {
5
5
  return t.startsWith("static/") ? t.slice(7) : t;
6
6
  }
@@ -0,0 +1 @@
1
+ "use strict";const s=require("node:fs"),c=require("node:path");function u(o){return o.toLowerCase().replaceAll(/[^a-z0-9]+/g,"-").replaceAll(/(?:^-|-$)/g,"")}function f(o){return o.split("/").map(u).filter(Boolean).join("/")}function l(o){return o==="light"?["light"]:o==="dark"?["dark"]:["light","dark"]}function a(o){const n=l(o.browser?.colorScheme),t=o.outputFormat??"png",i=o.outputDirectory??"heroshots",r={};for(const e of o.screenshots)r[e.name]={slug:f(e.name),viewports:e.viewports??[],colorSchemes:n,format:t};return{version:1,outputDirectory:i,screenshots:r}}const h=["heroshot.config.json","heroshots/config.json",".heroshot/config.json"];function g(o){for(const n of h){const t=c.resolve(o,n);if(s.existsSync(t))return t}return null}function p(o){try{const n=s.readFileSync(o,"utf-8"),t=JSON.parse(n);return!t.screenshots||!Array.isArray(t.screenshots)?(console.warn("[heroshot] Invalid config: missing screenshots array"),null):a(t)}catch(n){return console.warn("[heroshot] Failed to load config:",n),null}}function d(){return{version:1,outputDirectory:"heroshots",screenshots:{}}}exports.emptyManifest=d;exports.findConfig=g;exports.loadManifest=p;
@@ -1,36 +1,39 @@
1
1
  import { existsSync as c, readFileSync as i } from "node:fs";
2
- import { resolve as u } from "node:path";
3
- function a(o) {
2
+ import { resolve as l } from "node:path";
3
+ function u(o) {
4
4
  return o.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/(?:^-|-$)/g, "");
5
5
  }
6
- function l(o) {
7
- return o === "light" ? ["light"] : o === "dark" ? ["dark"] : ["light", "dark"];
6
+ function a(o) {
7
+ return o.split("/").map(u).filter(Boolean).join("/");
8
8
  }
9
- const f = ["heroshot.config.json", "heroshots/config.json", ".heroshot/config.json"];
10
- function m(o) {
11
- for (const t of f) {
12
- const r = u(o, t);
13
- if (c(r))
14
- return r;
15
- }
16
- return null;
9
+ function f(o) {
10
+ return o === "light" ? ["light"] : o === "dark" ? ["dark"] : ["light", "dark"];
17
11
  }
18
12
  function h(o) {
19
- const t = l(o.browser?.colorScheme), r = o.outputFormat ?? "png", s = o.outputDirectory ?? "heroshots", n = {};
20
- for (const e of o.screenshots)
21
- n[e.name] = {
22
- slug: a(e.name),
23
- viewports: e.viewports ?? [],
13
+ const t = f(o.browser?.colorScheme), r = o.outputFormat ?? "png", s = o.outputDirectory ?? "heroshots", e = {};
14
+ for (const n of o.screenshots)
15
+ e[n.name] = {
16
+ slug: a(n.name),
17
+ viewports: n.viewports ?? [],
24
18
  colorSchemes: t,
25
19
  format: r
26
20
  };
27
21
  return {
28
22
  version: 1,
29
23
  outputDirectory: s,
30
- screenshots: n
24
+ screenshots: e
31
25
  };
32
26
  }
27
+ const g = ["heroshot.config.json", "heroshots/config.json", ".heroshot/config.json"];
33
28
  function y(o) {
29
+ for (const t of g) {
30
+ const r = l(o, t);
31
+ if (c(r))
32
+ return r;
33
+ }
34
+ return null;
35
+ }
36
+ function d(o) {
34
37
  try {
35
38
  const t = i(o, "utf-8"), r = JSON.parse(t);
36
39
  return !r.screenshots || !Array.isArray(r.screenshots) ? (console.warn("[heroshot] Invalid config: missing screenshots array"), null) : h(r);
@@ -38,7 +41,7 @@ function y(o) {
38
41
  return console.warn("[heroshot] Failed to load config:", t), null;
39
42
  }
40
43
  }
41
- function d() {
44
+ function S() {
42
45
  return {
43
46
  version: 1,
44
47
  outputDirectory: "heroshots",
@@ -46,7 +49,7 @@ function d() {
46
49
  };
47
50
  }
48
51
  export {
49
- d as e,
50
- m as f,
51
- y as l
52
+ S as e,
53
+ y as f,
54
+ d as l
52
55
  };
@@ -0,0 +1,5 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=require("node:fs"),i=require("node:path"),l=require("./getManifest-BMMzQAE9.cjs");function d(e){return e.startsWith("public/")?e.slice(7):e}function p(e={},c={}){return{...e,webpack(t,m){const n=process.cwd();let o=null;c.config?o=i.resolve(n,c.config):o=l.findConfig(n);let f=l.emptyManifest();if(o&&s.existsSync(o)){const a=l.loadManifest(o);a&&(f={...a,outputDirectory:d(a.outputDirectory)},console.log(`[heroshot] Loaded config from ${o}`))}else console.log("[heroshot] No config found, using empty manifest");const r=i.join(n,"node_modules",".heroshot");s.mkdirSync(r,{recursive:!0});const u=i.join(r,"manifest.json");s.writeFileSync(u,JSON.stringify(f,null,2));const h=i.join(r,"client.js");return s.writeFileSync(h,`import { setManifest } from 'heroshot/next';
2
+ import manifest from './manifest.json';
3
+ setManifest(manifest);
4
+ export default manifest;
5
+ `),t.resolve??={},t.resolve.alias??={},t.resolve.alias["@heroshot/manifest"]=u,t.resolve.alias["virtual:heroshot-manifest"]=h,typeof e.webpack=="function"?e.webpack(t,m):t}}}exports.withHeroshot=p;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Next.js plugin for heroshot framework integration.
3
+ *
4
+ * Provides a config wrapper that injects manifest via webpack alias.
5
+ *
6
+ * Usage in next.config.js (webpack mode):
7
+ * ```js
8
+ * const { withHeroshot } = require('heroshot/plugins/next');
9
+ *
10
+ * module.exports = withHeroshot({
11
+ * // ... your Next.js config
12
+ * });
13
+ * ```
14
+ *
15
+ * For Turbopack mode (Next.js 15+ default), use manual setup:
16
+ * ```tsx
17
+ * // app/layout.tsx
18
+ * import { setManifest } from 'heroshot/next';
19
+ * import { configToManifest } from 'heroshot';
20
+ * import config from '../.heroshot/config.json';
21
+ * setManifest(configToManifest(config));
22
+ * ```
23
+ */
24
+ export interface WithHeroshotOptions {
25
+ /**
26
+ * Path to config.json (relative to project root)
27
+ * @default auto-detected from heroshot.config.json, heroshots/config.json, etc.
28
+ */
29
+ config?: string;
30
+ }
31
+ interface WebpackConfig {
32
+ resolve?: {
33
+ alias?: Record<string, string>;
34
+ };
35
+ }
36
+ interface WebpackOptions {
37
+ isServer: boolean;
38
+ }
39
+ interface NextConfig {
40
+ webpack?: (config: WebpackConfig, options: WebpackOptions) => WebpackConfig;
41
+ [key: string]: unknown;
42
+ }
43
+ /**
44
+ * Heroshot Next.js config wrapper
45
+ *
46
+ * Wraps your Next.js config to inject manifest via webpack alias.
47
+ * Works with webpack mode. For Turbopack, use manual setManifest().
48
+ */
49
+ export declare function withHeroshot(nextConfig?: NextConfig, options?: WithHeroshotOptions): NextConfig;
50
+ export {};
@@ -0,0 +1,41 @@
1
+ import { existsSync as h, mkdirSync as p, writeFileSync as m } from "node:fs";
2
+ import { resolve as d, join as n } from "node:path";
3
+ import { f as y, l as v, e as w } from "./getManifest-DfRVQbEx.js";
4
+ function M(e) {
5
+ return e.startsWith("public/") ? e.slice(7) : e;
6
+ }
7
+ function k(e = {}, a = {}) {
8
+ return {
9
+ ...e,
10
+ webpack(t, u) {
11
+ const s = process.cwd();
12
+ let o = null;
13
+ a.config ? o = d(s, a.config) : o = y(s);
14
+ let f = w();
15
+ if (o && h(o)) {
16
+ const i = v(o);
17
+ i && (f = {
18
+ ...i,
19
+ outputDirectory: M(i.outputDirectory)
20
+ }, console.log(`[heroshot] Loaded config from ${o}`));
21
+ } else
22
+ console.log("[heroshot] No config found, using empty manifest");
23
+ const r = n(s, "node_modules", ".heroshot");
24
+ p(r, { recursive: !0 });
25
+ const l = n(r, "manifest.json");
26
+ m(l, JSON.stringify(f, null, 2));
27
+ const c = n(r, "client.js");
28
+ return m(
29
+ c,
30
+ `import { setManifest } from 'heroshot/next';
31
+ import manifest from './manifest.json';
32
+ setManifest(manifest);
33
+ export default manifest;
34
+ `
35
+ ), t.resolve ??= {}, t.resolve.alias ??= {}, t.resolve.alias["@heroshot/manifest"] = l, t.resolve.alias["virtual:heroshot-manifest"] = c, typeof e.webpack == "function" ? e.webpack(t, u) : t;
36
+ }
37
+ };
38
+ }
39
+ export {
40
+ k as withHeroshot
41
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Type declarations for heroshot virtual modules.
3
+ *
4
+ * Users should add this to their project's tsconfig.json:
5
+ * {
6
+ * "compilerOptions": {
7
+ * "types": ["heroshot/virtual"]
8
+ * }
9
+ * }
10
+ */
11
+
12
+ declare module 'virtual:heroshot-manifest' {
13
+ import type { Manifest } from 'heroshot/vue';
14
+ const manifest: Manifest;
15
+ export default manifest;
16
+ }
17
+
18
+ // Docusaurus plugin alias
19
+ declare module '@heroshot/manifest' {
20
+ import type { Manifest } from 'heroshot/docusaurus';
21
+ const manifest: Manifest;
22
+ export default manifest;
23
+ }
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const u=require("node:fs"),d=require("node:path"),s=require("./getManifest-ClS1WrNU.cjs");function c(t){return t.startsWith("public/")?t.slice(7):t}const a="virtual:heroshot-manifest",f="\0"+a;function h(t={}){let e=null,r=s.emptyManifest();return{name:"heroshot",configResolved(o){const n=o.root;if(t.config?e=d.resolve(n,t.config):e=s.findConfig(n),e&&u.existsSync(e)){const i=s.loadManifest(e);i&&(r={...i,outputDirectory:c(i.outputDirectory)},console.log(`[heroshot] Loaded config from ${e}`))}else console.log("[heroshot] No config found, using empty manifest")},resolveId(o){if(o===a)return f},load(o){if(o===f)return`import { setManifest } from 'heroshot/vitepress';
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("node:fs"),d=require("node:path"),i=require("./getManifest-BMMzQAE9.cjs");function h(t){return t.startsWith("public/")||t.startsWith("static/")?t.slice(7):t}const u="virtual:heroshot-manifest",f="\0"+u;function m(t={}){let e=null,r=i.emptyManifest(),a="heroshot/vue";return{name:"heroshot",configResolved(o){const s=o.root;if(o.plugins.some(n=>n.name.includes("svelte"))&&(a="heroshot/svelte"),t.config?e=d.resolve(s,t.config):e=i.findConfig(s),e&&c.existsSync(e)){const n=i.loadManifest(e);n&&(r={...n,outputDirectory:h(n.outputDirectory)},console.log(`[heroshot] Loaded config from ${e}`))}else console.log("[heroshot] No config found, using empty manifest")},resolveId(o){if(o===u)return f},load(o){if(o===f)return`import { setManifest } from '${a}';
2
2
  const manifest = ${JSON.stringify(r,null,2)};
3
3
  setManifest(manifest);
4
- export default manifest;`},handleHotUpdate({file:o,server:n}){if(e&&o===e){const i=s.loadManifest(e);if(i){r=i,console.log("[heroshot] Config updated");const l=n.moduleGraph.getModuleById(f);l&&(n.moduleGraph.invalidateModule(l),n.ws.send({type:"full-reload"}))}}}}}exports.heroshot=h;
4
+ export default manifest;`},handleHotUpdate({file:o,server:s}){if(e&&o===e){const l=i.loadManifest(e);if(l){r=l,console.log("[heroshot] Config updated");const n=s.moduleGraph.getModuleById(f);n&&(s.moduleGraph.invalidateModule(n),s.ws.send({type:"full-reload"}))}}}}}exports.heroshot=m;
@@ -1,44 +1,44 @@
1
- import { existsSync as u } from "node:fs";
2
- import { resolve as d } from "node:path";
3
- import { e as c, f as m, l } from "./getManifest-CZSH5yc7.js";
4
- function h(e) {
5
- return e.startsWith("public/") ? e.slice(7) : e;
1
+ import { existsSync as c } from "node:fs";
2
+ import { resolve as m } from "node:path";
3
+ import { e as d, f as h, l as a } from "./getManifest-DfRVQbEx.js";
4
+ function p(e) {
5
+ return e.startsWith("public/") || e.startsWith("static/") ? e.slice(7) : e;
6
6
  }
7
- const a = "virtual:heroshot-manifest", r = "\0" + a;
7
+ const u = "virtual:heroshot-manifest", l = "\0" + u;
8
8
  function D(e = {}) {
9
- let o = null, s = c();
9
+ let t = null, i = d(), f = "heroshot/vue";
10
10
  return {
11
11
  name: "heroshot",
12
- configResolved(t) {
13
- const n = t.root;
14
- if (e.config ? o = d(n, e.config) : o = m(n), o && u(o)) {
15
- const i = l(o);
16
- i && (s = {
17
- ...i,
18
- outputDirectory: h(i.outputDirectory)
19
- }, console.log(`[heroshot] Loaded config from ${o}`));
12
+ configResolved(o) {
13
+ const s = o.root;
14
+ if (o.plugins.some((n) => n.name.includes("svelte")) && (f = "heroshot/svelte"), e.config ? t = m(s, e.config) : t = h(s), t && c(t)) {
15
+ const n = a(t);
16
+ n && (i = {
17
+ ...n,
18
+ outputDirectory: p(n.outputDirectory)
19
+ }, console.log(`[heroshot] Loaded config from ${t}`));
20
20
  } else
21
21
  console.log("[heroshot] No config found, using empty manifest");
22
22
  },
23
- resolveId(t) {
24
- if (t === a)
25
- return r;
23
+ resolveId(o) {
24
+ if (o === u)
25
+ return l;
26
26
  },
27
- load(t) {
28
- if (t === r)
29
- return `import { setManifest } from 'heroshot/vitepress';
30
- const manifest = ${JSON.stringify(s, null, 2)};
27
+ load(o) {
28
+ if (o === l)
29
+ return `import { setManifest } from '${f}';
30
+ const manifest = ${JSON.stringify(i, null, 2)};
31
31
  setManifest(manifest);
32
32
  export default manifest;`;
33
33
  },
34
34
  // Hot reload on config changes
35
- handleHotUpdate({ file: t, server: n }) {
36
- if (o && t === o) {
37
- const i = l(o);
38
- if (i) {
39
- s = i, console.log("[heroshot] Config updated");
40
- const f = n.moduleGraph.getModuleById(r);
41
- f && (n.moduleGraph.invalidateModule(f), n.ws.send({ type: "full-reload" }));
35
+ handleHotUpdate({ file: o, server: s }) {
36
+ if (t && o === t) {
37
+ const r = a(t);
38
+ if (r) {
39
+ i = r, console.log("[heroshot] Config updated");
40
+ const n = s.moduleGraph.getModuleById(l);
41
+ n && (s.moduleGraph.invalidateModule(n), s.ws.send({ type: "full-reload" }));
42
42
  }
43
43
  }
44
44
  }
@@ -0,0 +1,197 @@
1
+ <script lang="ts">
2
+ import {
3
+ getScreenshot,
4
+ getVariantPaths,
5
+ getManifest,
6
+ type Manifest,
7
+ type VariantPaths,
8
+ } from '../shared.js';
9
+
10
+ interface Props {
11
+ /** Screenshot name (as defined in heroshot config) */
12
+ name: string;
13
+ /** Alt text for accessibility */
14
+ alt?: string;
15
+ /** Manifest data - optional if using heroshot vite plugin */
16
+ manifest?: Manifest;
17
+ /** CSS class to apply to the image */
18
+ class?: string;
19
+ }
20
+
21
+ const { name, alt = '', manifest: manifestProp, class: className }: Props = $props();
22
+
23
+ // Use prop manifest or fall back to global manifest (set by plugin)
24
+ const activeManifest: Manifest | null = $derived(manifestProp ?? getManifest());
25
+
26
+ // Dark mode detection
27
+ let isDark = $state(false);
28
+
29
+ // Track if site has explicit theme handling (detected on mount)
30
+ let siteHasThemeHandling = false;
31
+
32
+ /**
33
+ * Dark mode detection priority:
34
+ * 1. Site theme (.dark class) - SvelteKit/frameworks set this based on user choice
35
+ * 2. System preference (prefers-color-scheme) - for sites without framework theme handling
36
+ * 3. Default to light
37
+ */
38
+ function detectDarkMode(): boolean {
39
+ if (globalThis.window === undefined) return false;
40
+
41
+ // 1. Check site theme (.dark class) - explicit user/framework choice
42
+ if (document.documentElement.classList.contains('dark')) {
43
+ return true;
44
+ }
45
+
46
+ // If site has theme handling (detected previously), absence of .dark = light mode
47
+ if (siteHasThemeHandling) {
48
+ return false;
49
+ }
50
+
51
+ // 2. Fall back to system preference for sites without theme handling
52
+ if (globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches) {
53
+ return true;
54
+ }
55
+
56
+ // 3. Default to light
57
+ return false;
58
+ }
59
+
60
+ $effect(() => {
61
+ // Detect if site has theme handling by checking for class management
62
+ siteHasThemeHandling =
63
+ document.documentElement.classList.length > 0 ||
64
+ document.documentElement.dataset.theme !== undefined;
65
+
66
+ isDark = detectDarkMode();
67
+
68
+ // Watch for class changes on documentElement (site theme toggle)
69
+ const observer = new MutationObserver(() => {
70
+ // Once we see a class change, we know the site has theme handling
71
+ siteHasThemeHandling = true;
72
+ isDark = document.documentElement.classList.contains('dark');
73
+ });
74
+ observer.observe(document.documentElement, {
75
+ attributes: true,
76
+ attributeFilter: ['class'],
77
+ });
78
+
79
+ // Listen for system preference changes
80
+ const mediaQuery = globalThis.matchMedia?.('(prefers-color-scheme: dark)');
81
+ const handleMediaChange = () => {
82
+ isDark = detectDarkMode();
83
+ };
84
+ mediaQuery?.addEventListener('change', handleMediaChange);
85
+
86
+ return () => {
87
+ observer.disconnect();
88
+ mediaQuery?.removeEventListener('change', handleMediaChange);
89
+ };
90
+ });
91
+
92
+ // Get screenshot from manifest
93
+ const screenshot = $derived(activeManifest ? getScreenshot(activeManifest, name) : undefined);
94
+
95
+ // Get all variant paths
96
+ const paths: VariantPaths | null = $derived(
97
+ screenshot && activeManifest ? getVariantPaths(activeManifest, screenshot) : null
98
+ );
99
+
100
+ // Current theme-based src (fallback for no viewports)
101
+ const themeSrc = $derived.by(() => {
102
+ if (!paths) return '';
103
+ const { light, dark } = paths;
104
+ if (isDark && dark) return dark;
105
+ if (!isDark && light) return light;
106
+ return paths.default;
107
+ });
108
+
109
+ // Viewport width mapping for media queries
110
+ const VIEWPORT_WIDTHS: Record<string, number> = {
111
+ mobile: 430, // iPhone 15/16 Pro Max viewport
112
+ tablet: 768,
113
+ desktop: 1280,
114
+ };
115
+
116
+ interface SourceEntry {
117
+ viewport: string;
118
+ srcset: string;
119
+ width: number;
120
+ media: string | undefined;
121
+ }
122
+
123
+ // Generate sources for <picture> element (sorted by width ascending)
124
+ const sources: SourceEntry[] = $derived.by(() => {
125
+ if (!paths || !screenshot) return [];
126
+ const { viewports } = paths;
127
+ const viewportNames = Object.keys(viewports);
128
+
129
+ if (viewportNames.length === 0) return [];
130
+
131
+ // Sort viewports by width ascending (smallest first - browser picks FIRST matching source)
132
+ const sorted = [...viewportNames].sort((a, b) => {
133
+ const widthA = VIEWPORT_WIDTHS[a] || Number.parseInt(a.split('x')[0] || '1280', 10);
134
+ const widthB = VIEWPORT_WIDTHS[b] || Number.parseInt(b.split('x')[0] || '1280', 10);
135
+ return widthA - widthB;
136
+ });
137
+
138
+ return sorted
139
+ .map((viewport, index): SourceEntry | null => {
140
+ const vpPaths = viewports[viewport];
141
+ if (!vpPaths) return null;
142
+
143
+ const lightSrc = vpPaths.light || vpPaths.default;
144
+ const darkSrc = vpPaths.dark || vpPaths.light || vpPaths.default;
145
+ const width =
146
+ VIEWPORT_WIDTHS[viewport] || Number.parseInt(viewport.split('x')[0] || '1280', 10);
147
+
148
+ // Last (largest) viewport doesn't need a max-width constraint - it's the fallback
149
+ const isLast = index === sorted.length - 1;
150
+
151
+ // Use isDark to pick the correct src (JavaScript-based theme detection)
152
+ const currentSrc = isDark ? darkSrc : lightSrc;
153
+
154
+ return {
155
+ viewport,
156
+ srcset: currentSrc,
157
+ width,
158
+ media: isLast ? undefined : `(max-width: ${width}px)`,
159
+ };
160
+ })
161
+ .filter((s): s is SourceEntry => s !== null);
162
+ });
163
+
164
+ // Check if we have viewport variants
165
+ const hasViewports = $derived(sources.length > 0);
166
+
167
+ // Warning message
168
+ const warning = $derived.by(() => {
169
+ if (!activeManifest) {
170
+ return 'Heroshot: No manifest found. Add heroshot() plugin to vite config or pass manifest prop.';
171
+ }
172
+ if (!screenshot) {
173
+ return `Heroshot: Screenshot "${name}" not found in config`;
174
+ }
175
+ return null;
176
+ });
177
+
178
+ if (warning && typeof console !== 'undefined') {
179
+ console.warn(warning);
180
+ }
181
+ </script>
182
+
183
+ <!-- Use <picture> for responsive viewport switching -->
184
+ <!-- Theme switching is JS-based (isDark reactive), viewport switching is CSS-based (media queries) -->
185
+ {#if screenshot && hasViewports}
186
+ <picture class={className}>
187
+ {#each sources as source (`${source.viewport}-${isDark}`)}
188
+ <source srcset={source.srcset} media={source.media} />
189
+ {/each}
190
+ <img src={themeSrc} {alt} loading="lazy" />
191
+ </picture>
192
+ <!-- Fallback to simple img for no viewports -->
193
+ {:else if screenshot}
194
+ <img src={themeSrc} {alt} class={className} loading="lazy" />
195
+ {:else}
196
+ <span style="color: red; font-size: 12px">{warning}</span>
197
+ {/if}
@@ -0,0 +1,25 @@
1
+ import { SvelteComponent } from 'svelte';
2
+
3
+ export interface Manifest {
4
+ version: 1;
5
+ outputDirectory: string;
6
+ screenshots: Record<string, {
7
+ slug: string;
8
+ viewports: string[];
9
+ colorSchemes: ('light' | 'dark')[];
10
+ format: 'png' | 'jpeg';
11
+ }>;
12
+ }
13
+
14
+ export interface HeroshotProps {
15
+ name: string;
16
+ alt?: string;
17
+ manifest?: Manifest;
18
+ class?: string;
19
+ }
20
+
21
+ export declare class Heroshot extends SvelteComponent<HeroshotProps> {}
22
+ export default Heroshot;
23
+
24
+ export declare function setManifest(manifest: Manifest): void;
25
+ export declare function getManifest(): Manifest | null;
@@ -0,0 +1,4 @@
1
+ // Svelte integration - re-exports component + shared utilities
2
+ // The .svelte file is compiled by the consumer's bundler (SvelteKit/Vite)
3
+ export { default as Heroshot, default } from './components/Heroshot.svelte';
4
+ export { setManifest, getManifest } from './shared.js';
@@ -0,0 +1,43 @@
1
+ function h(t, n) {
2
+ return t.screenshots[n];
3
+ }
4
+ function l(t, n = {}) {
5
+ const { slug: o, format: e } = t, { viewport: r, colorScheme: i } = n, s = [o];
6
+ r && s.push(r), i && s.push(i);
7
+ const c = e === "jpeg" ? "jpg" : "png";
8
+ return `${s.join("-")}.${c}`;
9
+ }
10
+ function f(t, n, o = {}) {
11
+ const e = l(n, o);
12
+ return `${t.outputDirectory}/${e}`;
13
+ }
14
+ function a(t, n, o) {
15
+ const { colorSchemes: e } = n, r = e.length > 0, i = (c) => f(t, n, { viewport: o, colorScheme: c }), s = {
16
+ default: r ? i("light") : i()
17
+ };
18
+ return e.includes("light") && (s.light = i("light")), e.includes("dark") && (s.dark = i("dark")), s;
19
+ }
20
+ function g(t, n) {
21
+ const o = a(t, n), e = {};
22
+ for (const r of n.viewports)
23
+ e[r] = a(t, n, r);
24
+ return {
25
+ ...o,
26
+ viewports: e
27
+ };
28
+ }
29
+ let u = null;
30
+ function p(t) {
31
+ u = t;
32
+ }
33
+ function d() {
34
+ return u;
35
+ }
36
+ export {
37
+ l as generateFilename,
38
+ f as generatePath,
39
+ d as getManifest,
40
+ h as getScreenshot,
41
+ g as getVariantPaths,
42
+ p as setManifest
43
+ };
package/dist/mcp/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { _ as loadConfig, b as screenshotSchema, g as getConfigPath, i as filterScreenshots, r as sync, t as generateSnippets, v as saveConfig, x as generateUid } from "../snippet-Bwn8XcVs.js";
2
+ import { _ as loadConfig, b as screenshotSchema, g as getConfigPath, i as filterScreenshots, r as sync, t as generateSnippets, v as saveConfig, x as generateUid } from "../snippet-B6Lg_Ant.js";
3
3
  import { z } from "zod";
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { intro, log, note, outro, spinner } from "@clack/prompts";
5
6
  import { z } from "zod";
6
7
  import { createCipheriv, createDecipheriv, createHash, randomBytes, scryptSync } from "node:crypto";
@@ -387,7 +388,8 @@ function parseConfig(input) {
387
388
  //#region src/configFile.ts
388
389
  const HEROSHOT_DIRECTORY_NAME = ".heroshot";
389
390
  const CONFIG_FILENAME = "config.json";
390
- const README_TEMPLATE_PATH = path.join(import.meta.dirname, "templates", "heroshotReadme.txt");
391
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
392
+ const README_TEMPLATE_PATH = path.join(__dirname, "templates", "heroshotReadme.txt");
391
393
  /**
392
394
  * Get the .heroshot directory path for a project
393
395
  */
@@ -576,7 +578,7 @@ function findPackageRoot(startDirectory) {
576
578
  return startDirectory;
577
579
  }
578
580
  /** Path to editor directory */
579
- const EDITOR_DIR = path.join(findPackageRoot(import.meta.dirname), "editor");
581
+ const EDITOR_DIR = path.join(findPackageRoot(path.dirname(fileURLToPath(import.meta.url))), "editor");
580
582
 
581
583
  //#endregion
582
584
  //#region src/browser/browserDetect.ts
@@ -847,21 +849,25 @@ function parseViewport(variant) {
847
849
  //#endregion
848
850
  //#region src/utils/screenshotPath.ts
849
851
  /**
850
- * Slugify a string for use in filenames
852
+ * Slugify a single path segment for use in filenames
851
853
  */
852
- function slugify(text) {
854
+ function slugifySegment(text) {
853
855
  return text.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/(?:^-|-$)/g, "");
854
856
  }
855
857
  /**
856
- * Generate screenshot filename
858
+ * Generate screenshot filename.
859
+ * Supports subdirectory paths via forward slashes in the name (e.g., "registry/login-01").
857
860
  */
858
861
  function generateScreenshotFilename(options) {
859
862
  const { name, viewport, colorScheme, format = "png" } = options;
860
- const parts = [slugify(name)];
863
+ const segments = name.split("/").map(slugifySegment).filter(Boolean);
864
+ const directory = segments.length > 1 ? segments.slice(0, -1).join("/") : "";
865
+ const parts = [segments.at(-1) ?? ""];
861
866
  if (viewport) parts.push(viewport);
862
867
  if (colorScheme) parts.push(colorScheme);
863
868
  const extension = format === "jpeg" ? "jpg" : "png";
864
- return `${parts.join("-")}.${extension}`;
869
+ const filename = `${parts.join("-")}.${extension}`;
870
+ return directory ? `${directory}/${filename}` : filename;
865
871
  }
866
872
 
867
873
  //#endregion
@@ -2124,12 +2130,13 @@ function loadEncryptedSession(sessionKeyOption) {
2124
2130
  */
2125
2131
  /**
2126
2132
  * Get list of existing screenshot files in output directory.
2127
- * Returns only .png and .jpg files.
2133
+ * Scans recursively to support subdirectory output paths.
2134
+ * Returns relative paths (e.g., "registry/login-01-light.png").
2128
2135
  */
2129
2136
  function getExistingFiles(outputDirectory) {
2130
2137
  if (!existsSync(outputDirectory)) return [];
2131
2138
  try {
2132
- return readdirSync(outputDirectory).filter((file) => file.endsWith(".png") || file.endsWith(".jpg"));
2139
+ return readdirSync(outputDirectory, { recursive: true }).map((file) => typeof file === "string" ? file : file.toString()).filter((file) => file.endsWith(".png") || file.endsWith(".jpg"));
2133
2140
  } catch {
2134
2141
  return [];
2135
2142
  }
@@ -4069,7 +4069,17 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
4069
4069
  function isElementFill(value) {
4070
4070
  return value === "original" || value === "solid" || value === "transparent";
4071
4071
  }
4072
- let barStyle = /* @__PURE__ */ user_derived(() => `left:${$$props.position.x + get(dragOffset).x}px;top:${$$props.position.y + get(dragOffset).y}px;transform:translateX(-50%);`);
4072
+ let barStyle = /* @__PURE__ */ user_derived(() => {
4073
+ const x = $$props.position.x + get(dragOffset).x;
4074
+ const y = $$props.position.y + get(dragOffset).y;
4075
+ if ($$props.position.placement === "right") {
4076
+ return `left:${x}px;top:${y}px;transform:translateY(-50%);`;
4077
+ }
4078
+ if ($$props.position.placement === "left") {
4079
+ return `left:${x}px;top:${y}px;transform:translate(-100%,-50%);`;
4080
+ }
4081
+ return `left:${x}px;top:${y}px;transform:translateX(-50%);`;
4082
+ });
4073
4083
  var fragment = comment();
4074
4084
  var node = first_child(fragment);
4075
4085
  {
@@ -5994,9 +6004,13 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
5994
6004
  const typeHandler = getAnnotationType(get(selectedAnnotation).type);
5995
6005
  if (!typeHandler) return null;
5996
6006
  const bbox = typeHandler.getBBox(get(selectedAnnotation));
6007
+ const bboxCenterX = $$props.elementRect.left + (bbox.minX + bbox.maxX) / 2;
6008
+ const placeRight = bboxCenterX < globalThis.innerWidth / 2;
6009
+ const bboxCenterY = $$props.elementRect.top + (bbox.minY + bbox.maxY) / 2;
5997
6010
  return {
5998
- x: $$props.elementRect.left + (bbox.minX + bbox.maxX) / 2,
5999
- y: $$props.elementRect.top + bbox.maxY + 12
6011
+ x: placeRight ? $$props.elementRect.left + bbox.maxX + 12 : $$props.elementRect.left + bbox.minX - 12,
6012
+ y: bboxCenterY,
6013
+ placement: placeRight ? "right" : "left"
6000
6014
  };
6001
6015
  }
6002
6016
  function updateStyle(newStyle) {
@@ -7422,9 +7436,13 @@ var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "acce
7422
7436
  return null;
7423
7437
  }
7424
7438
  if (get(selectionContext).type === "element" && get(pickerExpandedRect)) {
7439
+ const viewportWidth = globalThis.innerWidth;
7440
+ const elementCenterX = get(pickerExpandedRect).left + get(pickerExpandedRect).width / 2;
7441
+ const placeRight = elementCenterX < viewportWidth / 2;
7425
7442
  return {
7426
- x: get(pickerExpandedRect).left + get(pickerExpandedRect).width / 2,
7427
- y: get(pickerExpandedRect).top + get(pickerExpandedRect).height + 12
7443
+ x: placeRight ? get(pickerExpandedRect).left + get(pickerExpandedRect).width + 12 : get(pickerExpandedRect).left - 12,
7444
+ y: get(pickerExpandedRect).top + get(pickerExpandedRect).height / 2,
7445
+ placement: placeRight ? "right" : "left"
7428
7446
  };
7429
7447
  }
7430
7448
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heroshot",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Define your screenshots once, update them forever with one command",
5
5
  "type": "module",
6
6
  "author": "Ondrej Machala",
@@ -9,6 +9,9 @@
9
9
  "type": "git",
10
10
  "url": "https://github.com/omachala/heroshot"
11
11
  },
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
12
15
  "publishConfig": {
13
16
  "provenance": true,
14
17
  "access": "public"
@@ -58,6 +61,18 @@
58
61
  "types": "./dist/integrations/react/index.d.ts",
59
62
  "import": "./dist/integrations/react/index.js"
60
63
  },
64
+ "./svelte": {
65
+ "types": "./dist/integrations/svelte/index.d.ts",
66
+ "import": "./dist/integrations/svelte/index.js"
67
+ },
68
+ "./sveltekit": {
69
+ "types": "./dist/integrations/svelte/index.d.ts",
70
+ "import": "./dist/integrations/svelte/index.js"
71
+ },
72
+ "./next": {
73
+ "types": "./dist/integrations/next/index.d.ts",
74
+ "import": "./dist/integrations/next/index.js"
75
+ },
61
76
  "./plugins/vite": {
62
77
  "types": "./dist/integrations/shared/vitePlugin.d.ts",
63
78
  "import": "./dist/integrations/shared/vitePlugin.js"
@@ -67,6 +82,11 @@
67
82
  "import": "./dist/integrations/shared/docusaurusPlugin.js",
68
83
  "require": "./dist/integrations/shared/docusaurusPlugin.cjs"
69
84
  },
85
+ "./plugins/next": {
86
+ "types": "./dist/integrations/shared/nextPlugin.d.ts",
87
+ "import": "./dist/integrations/shared/nextPlugin.js",
88
+ "require": "./dist/integrations/shared/nextPlugin.cjs"
89
+ },
70
90
  "./virtual": {
71
91
  "types": "./dist/integrations/shared/virtual.d.ts"
72
92
  }
@@ -134,6 +154,7 @@
134
154
  },
135
155
  "peerDependencies": {
136
156
  "react": ">=18.0.0",
157
+ "svelte": ">=5.0.0",
137
158
  "vitepress": ">=1.0.0",
138
159
  "vue": ">=3.0.0"
139
160
  },
@@ -141,6 +162,9 @@
141
162
  "react": {
142
163
  "optional": true
143
164
  },
165
+ "svelte": {
166
+ "optional": true
167
+ },
144
168
  "vue": {
145
169
  "optional": true
146
170
  },
@@ -157,6 +181,8 @@
157
181
  "build:integrations": "pnpm --filter './integrations/*' build",
158
182
  "build:integrations:vue": "pnpm --filter './integrations/vue' build",
159
183
  "build:integrations:react": "pnpm --filter './integrations/react' build",
184
+ "build:integrations:svelte": "pnpm --filter './integrations/svelte' build",
185
+ "build:integrations:next": "pnpm --filter './integrations/next' build",
160
186
  "test:integrations": "pnpm --filter './integrations/*' test:run",
161
187
  "lint:integrations": "pnpm --filter './integrations/react' lint",
162
188
  "typecheck:integrations": "pnpm --filter './integrations/vue' --filter './integrations/react' typecheck",
@@ -1 +0,0 @@
1
- "use strict";const s=require("node:fs"),i=require("node:path");function u(o){return o.toLowerCase().replaceAll(/[^a-z0-9]+/g,"-").replaceAll(/(?:^-|-$)/g,"")}function a(o){return o==="light"?["light"]:o==="dark"?["dark"]:["light","dark"]}const f=["heroshot.config.json","heroshots/config.json",".heroshot/config.json"];function l(o){for(const r of f){const t=i.resolve(o,r);if(s.existsSync(t))return t}return null}function h(o){const r=a(o.browser?.colorScheme),t=o.outputFormat??"png",c=o.outputDirectory??"heroshots",n={};for(const e of o.screenshots)n[e.name]={slug:u(e.name),viewports:e.viewports??[],colorSchemes:r,format:t};return{version:1,outputDirectory:c,screenshots:n}}function g(o){try{const r=s.readFileSync(o,"utf-8"),t=JSON.parse(r);return!t.screenshots||!Array.isArray(t.screenshots)?(console.warn("[heroshot] Invalid config: missing screenshots array"),null):h(t)}catch(r){return console.warn("[heroshot] Failed to load config:",r),null}}function d(){return{version:1,outputDirectory:"heroshots",screenshots:{}}}exports.emptyManifest=d;exports.findConfig=l;exports.loadManifest=g;