heroshot 0.13.1 → 0.14.1

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 +1 -1
  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 +138 -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 +1 -0
  12. package/dist/integrations/shared/nextPlugin.d.ts +50 -0
  13. package/dist/integrations/shared/nextPlugin.js +32 -0
  14. package/dist/integrations/shared/virtual.d.ts +23 -0
  15. package/dist/integrations/shared/vitePlugin.cjs +4 -3
  16. package/dist/integrations/shared/vitePlugin.js +35 -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-CqBg-092.js → snippet-B6Lg_Ant.js} +12 -7
  23. package/editor/dist/editor.js +23 -5
  24. package/package.json +24 -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,5 +1,5 @@
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-CqBg-092.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
5
  import { fileURLToPath } from "node:url";
@@ -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,138 @@
1
+ "use client";
2
+ import y from "@heroshot/manifest";
3
+ import { jsx as h, jsxs as I } from "react/jsx-runtime";
4
+ import $, { createContext as j, useContext as C, useMemo as g, useState as L, useEffect as z } from "react";
5
+ let x = null;
6
+ function N(e) {
7
+ x = e;
8
+ }
9
+ function O() {
10
+ return x;
11
+ }
12
+ function R(e, t) {
13
+ return e.screenshots[t];
14
+ }
15
+ function V(e, t = {}) {
16
+ const { slug: a, format: s } = e, { viewport: r, colorScheme: c } = t, n = [a];
17
+ r && n.push(r), c && n.push(c);
18
+ const i = s === "jpeg" ? "jpg" : "png";
19
+ return `${n.join("-")}.${i}`;
20
+ }
21
+ function A(e, t, a = {}) {
22
+ const s = V(t, a);
23
+ return `${e.outputDirectory}/${s}`;
24
+ }
25
+ function H(e, t, a) {
26
+ const { colorSchemes: s } = t, r = s.length > 0, c = (i) => A(e, t, { viewport: a, colorScheme: i }), n = {
27
+ default: r ? c("light") : c()
28
+ };
29
+ return s.includes("light") && (n.light = c("light")), s.includes("dark") && (n.dark = c("dark")), n;
30
+ }
31
+ function F(e, t) {
32
+ const a = H(e, t), s = {};
33
+ for (const r of t.viewports)
34
+ s[r] = H(e, t, r);
35
+ return {
36
+ ...a,
37
+ viewports: s
38
+ };
39
+ }
40
+ const D = j(null);
41
+ function G({
42
+ manifest: e,
43
+ children: t
44
+ }) {
45
+ return /* @__PURE__ */ h(D.Provider, { value: e, children: t });
46
+ }
47
+ function S() {
48
+ if (globalThis.window === void 0) return { isDark: !1, hasThemeHandling: !1 };
49
+ const { theme: e } = document.documentElement.dataset;
50
+ 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 };
51
+ }
52
+ function W() {
53
+ const e = $.useRef(!1), [t, a] = L(() => S().isDark);
54
+ return z(() => {
55
+ const s = S();
56
+ e.current = s.hasThemeHandling;
57
+ const r = () => {
58
+ const { theme: u } = document.documentElement.dataset;
59
+ 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;
60
+ }, c = new MutationObserver(() => {
61
+ e.current = !0, a(r());
62
+ });
63
+ c.observe(document.documentElement, {
64
+ attributes: !0,
65
+ attributeFilter: ["data-theme", "class"]
66
+ });
67
+ const n = globalThis.matchMedia?.("(prefers-color-scheme: dark)"), i = () => {
68
+ a(r());
69
+ };
70
+ return n?.addEventListener("change", i), () => {
71
+ c.disconnect(), n?.removeEventListener("change", i);
72
+ };
73
+ }, []), t;
74
+ }
75
+ const k = {
76
+ mobile: 430,
77
+ // iPhone 15/16 Pro Max viewport
78
+ tablet: 768,
79
+ desktop: 1280
80
+ };
81
+ function J({
82
+ name: e,
83
+ alt: t = "",
84
+ manifest: a,
85
+ className: s
86
+ }) {
87
+ const r = W(), c = C(D), n = a ?? c ?? O(), i = g(() => n ? R(n, e) : null, [n, e]), u = g(() => !i || !n ? null : F(n, i), [n, i]), v = g(() => {
88
+ if (!u) return "";
89
+ const { light: o, dark: f } = u;
90
+ return r && f ? f : !r && o ? o : u.default;
91
+ }, [u, r]), T = g(() => {
92
+ if (!u || !i) return [];
93
+ const { viewports: o } = u, f = Object.keys(o);
94
+ if (f.length === 0) return [];
95
+ const b = [...f].sort((l, m) => {
96
+ const d = k[l] || Number.parseInt(l.split("x")[0] || "1280", 10), p = k[m] || Number.parseInt(m.split("x")[0] || "1280", 10);
97
+ return d - p;
98
+ });
99
+ return b.map((l, m) => {
100
+ const d = o[l];
101
+ if (!d) return null;
102
+ 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;
103
+ return {
104
+ viewport: l,
105
+ srcset: r ? E : p,
106
+ width: w,
107
+ media: M ? void 0 : `(max-width: ${w}px)`
108
+ };
109
+ }).filter((l) => l !== null);
110
+ }, [u, i, r]), P = T.length > 0;
111
+ if (!n) {
112
+ const o = "Heroshot: No manifest found. Add heroshot() plugin to vite config, use HeroshotProvider, or pass manifest prop.";
113
+ return typeof console < "u" && console.warn(o), /* @__PURE__ */ h("span", { style: { color: "red", fontSize: "12px" }, children: o });
114
+ }
115
+ if (!i) {
116
+ const o = `Heroshot: Screenshot "${e}" not found in config`;
117
+ return typeof console < "u" && console.warn(o), /* @__PURE__ */ h("span", { style: { color: "red", fontSize: "12px" }, children: o });
118
+ }
119
+ return P ? /* @__PURE__ */ I("picture", { className: s, children: [
120
+ T.map((o) => /* @__PURE__ */ h(
121
+ "source",
122
+ {
123
+ srcSet: o.srcset,
124
+ media: o.media
125
+ },
126
+ `${o.viewport}-${r}`
127
+ )),
128
+ /* @__PURE__ */ h("img", { src: v, alt: t, loading: "lazy" })
129
+ ] }) : /* @__PURE__ */ h("img", { src: v, alt: t, className: s, loading: "lazy" });
130
+ }
131
+ (function() {
132
+ N(y);
133
+ })();
134
+ export {
135
+ J as Heroshot,
136
+ G as HeroshotProvider,
137
+ N as setManifest
138
+ };
@@ -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 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const n=require("node:fs"),i=require("node:path"),c=require("./getManifest-BMMzQAE9.cjs");function d(e){return e.startsWith("public/")?e.slice(7):e}function m(e={},l={}){return{...e,webpack(o,h){const r=process.cwd();let t=null;l.config?t=i.resolve(r,l.config):t=c.findConfig(r);let u=c.emptyManifest();if(t&&n.existsSync(t)){const s=c.loadManifest(t);s&&(u={...s,outputDirectory:d(s.outputDirectory)},console.log(`[heroshot] Loaded config from ${t}`))}else console.log("[heroshot] No config found, using empty manifest");const a=i.join(r,"node_modules",".heroshot");n.mkdirSync(a,{recursive:!0});const f=i.join(a,"manifest.json");return n.writeFileSync(f,JSON.stringify(u,null,2)),o.resolve??={},o.resolve.alias??={},o.resolve.alias["@heroshot/manifest"]=f,typeof e.webpack=="function"?e.webpack(o,h):o}}}exports.withHeroshot=m;
@@ -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,32 @@
1
+ import { existsSync as u, mkdirSync as m, writeFileSync as p } from "node:fs";
2
+ import { resolve as h, join as c } from "node:path";
3
+ import { f as d, l as y, e as w } from "./getManifest-DfRVQbEx.js";
4
+ function g(e) {
5
+ return e.startsWith("public/") ? e.slice(7) : e;
6
+ }
7
+ function D(e = {}, i = {}) {
8
+ return {
9
+ ...e,
10
+ webpack(t, f) {
11
+ const r = process.cwd();
12
+ let o = null;
13
+ i.config ? o = h(r, i.config) : o = d(r);
14
+ let n = w();
15
+ if (o && u(o)) {
16
+ const s = y(o);
17
+ s && (n = {
18
+ ...s,
19
+ outputDirectory: g(s.outputDirectory)
20
+ }, console.log(`[heroshot] Loaded config from ${o}`));
21
+ } else
22
+ console.log("[heroshot] No config found, using empty manifest");
23
+ const l = c(r, "node_modules", ".heroshot");
24
+ m(l, { recursive: !0 });
25
+ const a = c(l, "manifest.json");
26
+ return p(a, JSON.stringify(n, null, 2)), t.resolve ??= {}, t.resolve.alias ??= {}, t.resolve.alias["@heroshot/manifest"] = a, typeof e.webpack == "function" ? e.webpack(t, f) : t;
27
+ }
28
+ };
29
+ }
30
+ export {
31
+ D as withHeroshot
32
+ };
@@ -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,5 @@
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';
2
- const manifest = ${JSON.stringify(r,null,2)};
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const d=require("node:fs"),c=require("node:path"),i=require("./getManifest-BMMzQAE9.cjs");function h(n){return n.startsWith("public/")||n.startsWith("static/")?n.slice(7):n}const r="virtual:heroshot-manifest",f="\0"+r;function m(n={}){let t=null,l=i.emptyManifest(),a="heroshot/vue";return{name:"heroshot",configResolved(e){const o=e.root;if(e.plugins.some(s=>s.name.includes("svelte"))&&(a="heroshot/svelte"),n.config?t=c.resolve(o,n.config):t=i.findConfig(o),t&&d.existsSync(t)){const s=i.loadManifest(t);s&&(l={...s,outputDirectory:h(s.outputDirectory)},console.log(`[heroshot] Loaded config from ${t}`))}else console.log("[heroshot] No config found, using empty manifest")},transform(e,o){if(!(o.includes("\0")||o.includes("node_modules"))&&(e.includes("from 'heroshot/")||e.includes('from "heroshot/')))return e.includes(r)?void 0:`import '${r}';
2
+ ${e}`},resolveId(e){if(e===r)return f},load(e){if(e===f)return`import { setManifest } from '${a}';
3
+ const manifest = ${JSON.stringify(l,null,2)};
3
4
  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;
5
+ export default manifest;`},handleHotUpdate({file:e,server:o}){if(t&&e===t){const u=i.loadManifest(t);if(u){l=u,console.log("[heroshot] Config updated");const s=o.moduleGraph.getModuleById(f);s&&(o.moduleGraph.invalidateModule(s),o.ws.send({type:"full-reload"}))}}}}}exports.heroshot=m;
@@ -1,44 +1,50 @@
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 d } from "node:fs";
2
+ import { resolve as m } from "node:path";
3
+ import { e as c, f as h, l as a } from "./getManifest-DfRVQbEx.js";
4
+ function p(n) {
5
+ return n.startsWith("public/") || n.startsWith("static/") ? n.slice(7) : n;
6
6
  }
7
- const a = "virtual:heroshot-manifest", r = "\0" + a;
8
- function D(e = {}) {
9
- let o = null, s = c();
7
+ const i = "virtual:heroshot-manifest", f = "\0" + i;
8
+ function D(n = {}) {
9
+ let o = null, r = c(), u = "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)
12
+ configResolved(e) {
13
+ const t = e.root;
14
+ if (e.plugins.some((s) => s.name.includes("svelte")) && (u = "heroshot/svelte"), n.config ? o = m(t, n.config) : o = h(t), o && d(o)) {
15
+ const s = a(o);
16
+ s && (r = {
17
+ ...s,
18
+ outputDirectory: p(s.outputDirectory)
19
19
  }, console.log(`[heroshot] Loaded config from ${o}`));
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
+ // Auto-inject manifest import into entry modules so users don't need a separate plugin file
24
+ transform(e, t) {
25
+ if (!(t.includes("\0") || t.includes("node_modules")) && (e.includes("from 'heroshot/") || e.includes('from "heroshot/')))
26
+ return e.includes(i) ? void 0 : `import '${i}';
27
+ ${e}`;
26
28
  },
27
- load(t) {
28
- if (t === r)
29
- return `import { setManifest } from 'heroshot/vitepress';
30
- const manifest = ${JSON.stringify(s, null, 2)};
29
+ resolveId(e) {
30
+ if (e === i)
31
+ return f;
32
+ },
33
+ load(e) {
34
+ if (e === f)
35
+ return `import { setManifest } from '${u}';
36
+ const manifest = ${JSON.stringify(r, null, 2)};
31
37
  setManifest(manifest);
32
38
  export default manifest;`;
33
39
  },
34
40
  // 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" }));
41
+ handleHotUpdate({ file: e, server: t }) {
42
+ if (o && e === o) {
43
+ const l = a(o);
44
+ if (l) {
45
+ r = l, console.log("[heroshot] Config updated");
46
+ const s = t.moduleGraph.getModuleById(f);
47
+ s && (t.moduleGraph.invalidateModule(s), t.ws.send({ type: "full-reload" }));
42
48
  }
43
49
  }
44
50
  }
@@ -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-CqBg-092.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";
@@ -849,21 +849,25 @@ function parseViewport(variant) {
849
849
  //#endregion
850
850
  //#region src/utils/screenshotPath.ts
851
851
  /**
852
- * Slugify a string for use in filenames
852
+ * Slugify a single path segment for use in filenames
853
853
  */
854
- function slugify(text) {
854
+ function slugifySegment(text) {
855
855
  return text.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-").replaceAll(/(?:^-|-$)/g, "");
856
856
  }
857
857
  /**
858
- * Generate screenshot filename
858
+ * Generate screenshot filename.
859
+ * Supports subdirectory paths via forward slashes in the name (e.g., "registry/login-01").
859
860
  */
860
861
  function generateScreenshotFilename(options) {
861
862
  const { name, viewport, colorScheme, format = "png" } = options;
862
- 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) ?? ""];
863
866
  if (viewport) parts.push(viewport);
864
867
  if (colorScheme) parts.push(colorScheme);
865
868
  const extension = format === "jpeg" ? "jpg" : "png";
866
- return `${parts.join("-")}.${extension}`;
869
+ const filename = `${parts.join("-")}.${extension}`;
870
+ return directory ? `${directory}/${filename}` : filename;
867
871
  }
868
872
 
869
873
  //#endregion
@@ -2126,12 +2130,13 @@ function loadEncryptedSession(sessionKeyOption) {
2126
2130
  */
2127
2131
  /**
2128
2132
  * Get list of existing screenshot files in output directory.
2129
- * 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").
2130
2135
  */
2131
2136
  function getExistingFiles(outputDirectory) {
2132
2137
  if (!existsSync(outputDirectory)) return [];
2133
2138
  try {
2134
- 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"));
2135
2140
  } catch {
2136
2141
  return [];
2137
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.1",
3
+ "version": "0.14.1",
4
4
  "description": "Define your screenshots once, update them forever with one command",
5
5
  "type": "module",
6
6
  "author": "Ondrej Machala",
@@ -61,6 +61,18 @@
61
61
  "types": "./dist/integrations/react/index.d.ts",
62
62
  "import": "./dist/integrations/react/index.js"
63
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
+ },
64
76
  "./plugins/vite": {
65
77
  "types": "./dist/integrations/shared/vitePlugin.d.ts",
66
78
  "import": "./dist/integrations/shared/vitePlugin.js"
@@ -70,6 +82,11 @@
70
82
  "import": "./dist/integrations/shared/docusaurusPlugin.js",
71
83
  "require": "./dist/integrations/shared/docusaurusPlugin.cjs"
72
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
+ },
73
90
  "./virtual": {
74
91
  "types": "./dist/integrations/shared/virtual.d.ts"
75
92
  }
@@ -137,6 +154,7 @@
137
154
  },
138
155
  "peerDependencies": {
139
156
  "react": ">=18.0.0",
157
+ "svelte": ">=5.0.0",
140
158
  "vitepress": ">=1.0.0",
141
159
  "vue": ">=3.0.0"
142
160
  },
@@ -144,6 +162,9 @@
144
162
  "react": {
145
163
  "optional": true
146
164
  },
165
+ "svelte": {
166
+ "optional": true
167
+ },
147
168
  "vue": {
148
169
  "optional": true
149
170
  },
@@ -160,6 +181,8 @@
160
181
  "build:integrations": "pnpm --filter './integrations/*' build",
161
182
  "build:integrations:vue": "pnpm --filter './integrations/vue' build",
162
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",
163
186
  "test:integrations": "pnpm --filter './integrations/*' test:run",
164
187
  "lint:integrations": "pnpm --filter './integrations/react' lint",
165
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;