heroshot 0.13.1 → 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.
- package/README.md +13 -7
- package/dist/cli/cli.js +1 -1
- package/dist/index.d.ts +44 -0
- package/dist/index.js +49 -0
- package/dist/integrations/next/index.d.ts +3 -0
- package/dist/integrations/next/index.js +134 -0
- package/dist/integrations/shared/docusaurusPlugin.cjs +1 -1
- package/dist/integrations/shared/docusaurusPlugin.js +1 -1
- package/dist/integrations/shared/getManifest-BMMzQAE9.cjs +1 -0
- package/dist/integrations/shared/{getManifest-CZSH5yc7.js → getManifest-DfRVQbEx.js} +25 -22
- package/dist/integrations/shared/nextPlugin.cjs +5 -0
- package/dist/integrations/shared/nextPlugin.d.ts +50 -0
- package/dist/integrations/shared/nextPlugin.js +41 -0
- package/dist/integrations/shared/virtual.d.ts +23 -0
- package/dist/integrations/shared/vitePlugin.cjs +2 -2
- package/dist/integrations/shared/vitePlugin.js +29 -29
- package/dist/integrations/svelte/components/Heroshot.svelte +197 -0
- package/dist/integrations/svelte/index.d.ts +25 -0
- package/dist/integrations/svelte/index.js +4 -0
- package/dist/integrations/svelte/shared.js +43 -0
- package/dist/mcp/index.js +1 -1
- package/dist/{snippet-CqBg-092.js → snippet-B6Lg_Ant.js} +12 -7
- package/editor/dist/editor.js +23 -5
- package/package.json +24 -1
- package/dist/integrations/shared/getManifest-ClS1WrNU.cjs +0 -1
- /package/dist/integrations/react/{react/src/components → components}/Heroshot.d.ts +0 -0
- /package/dist/integrations/react/{react/src/index.d.ts → index.d.ts} +0 -0
- /package/dist/integrations/vue/{vue/src/components → components}/Heroshot.vue.d.ts +0 -0
- /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
|
|
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
|
|
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/
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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";
|
package/dist/index.d.ts
ADDED
|
@@ -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,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-
|
|
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-
|
|
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
|
|
3
|
-
function
|
|
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
|
|
7
|
-
return o
|
|
6
|
+
function a(o) {
|
|
7
|
+
return o.split("/").map(u).filter(Boolean).join("/");
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
|
|
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 =
|
|
20
|
-
for (const
|
|
21
|
-
n
|
|
22
|
-
slug: a(
|
|
23
|
-
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:
|
|
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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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:
|
|
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
|
|
2
|
-
import { resolve as
|
|
3
|
-
import { e as
|
|
4
|
-
function
|
|
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
|
|
7
|
+
const u = "virtual:heroshot-manifest", l = "\0" + u;
|
|
8
8
|
function D(e = {}) {
|
|
9
|
-
let
|
|
9
|
+
let t = null, i = d(), f = "heroshot/vue";
|
|
10
10
|
return {
|
|
11
11
|
name: "heroshot",
|
|
12
|
-
configResolved(
|
|
13
|
-
const
|
|
14
|
-
if (e.config ?
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
...
|
|
18
|
-
outputDirectory:
|
|
19
|
-
}, console.log(`[heroshot] Loaded config from ${
|
|
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(
|
|
24
|
-
if (
|
|
25
|
-
return
|
|
23
|
+
resolveId(o) {
|
|
24
|
+
if (o === u)
|
|
25
|
+
return l;
|
|
26
26
|
},
|
|
27
|
-
load(
|
|
28
|
-
if (
|
|
29
|
-
return `import { setManifest } from '
|
|
30
|
-
const manifest = ${JSON.stringify(
|
|
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:
|
|
36
|
-
if (
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
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-
|
|
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
|
|
852
|
+
* Slugify a single path segment for use in filenames
|
|
853
853
|
*/
|
|
854
|
-
function
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
}
|
package/editor/dist/editor.js
CHANGED
|
@@ -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(() =>
|
|
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 +
|
|
5999
|
-
y:
|
|
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
|
|
7427
|
-
y: get(pickerExpandedRect).top + get(pickerExpandedRect).height
|
|
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.
|
|
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",
|
|
@@ -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;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|