diff --git a/.gitignore b/.gitignore
index cc239aa..d9466bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,5 @@ node_modules
dist
.next
out
-.vercel
\ No newline at end of file
+.vercel
+public/examples
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8f6bf56..5ed9363 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -41,6 +41,8 @@ echo("Hello World");
This is a [pull request](https://github.com/recho-dev/recho/pull/82) you can refer to.
+If you prefer a `snapshot thumbnail`, create an image file named `[SKETCH-NAME].snap.[FORMAT]` in `app/examples`, such as [sin-wave-radios.snap.png](./app/examples/sin-wave-radios.snap.png) for [sin-wave-radios.recho.js](/app/examples/sin-wave-radios.recho.js).
+
## Editor & Runtime Development
If you want to contribute to the editor or runtime, make sure to add test files in `test/js/` and register them in `test/js/index.js`.
diff --git a/app/Thumbnail.jsx b/app/Thumbnail.jsx
index 2d02e65..e95b26a 100644
--- a/app/Thumbnail.jsx
+++ b/app/Thumbnail.jsx
@@ -6,7 +6,7 @@ function removeEmptyLines(string) {
return first + rest.join("\n");
}
-export function Thumbnail({html, outputStartLine = null}) {
+export function Thumbnail({html, outputStartLine = null, snap = null}) {
if (outputStartLine === null) return
;
const $ = load(html);
const lines = $("span.line");
@@ -15,5 +15,10 @@ export function Thumbnail({html, outputStartLine = null}) {
$(lines[i]).remove();
}
}
+ if (snap) {
+ return (
+
+ );
+ }
return ;
}
diff --git a/app/examples/page.jsx b/app/examples/page.jsx
index b917694..d43ce14 100644
--- a/app/examples/page.jsx
+++ b/app/examples/page.jsx
@@ -19,7 +19,7 @@ export default function Page() {
diff --git a/app/examples/sin-wave-radios.recho.js b/app/examples/sin-wave-radios.recho.js
new file mode 100644
index 0000000..55d094d
--- /dev/null
+++ b/app/examples/sin-wave-radios.recho.js
@@ -0,0 +1,51 @@
+/**
+ * @title Sin Wave Radios
+ * @author Bairui Su
+ * @created 2025-09-27
+ * @pull_request 128
+ * @github pearmini
+ */
+
+/**
+ * ============================================================================
+ * = Sin Wave Radios =
+ * ============================================================================
+ *
+ * This example explores using inputs to create some visual patterns in Recho.
+ * For now, we can only manually set the values of the inputs to simulate the
+ * sine wave, based on the computed values. In the future, inputs values can be
+ * programmatically updated.
+ */
+
+recho.radio(4, ['','','','','','','','','']);
+recho.radio(5, ['','','','','','','','','']);
+recho.radio(6, ['','','','','','','','','']);
+recho.radio(7, ['','','','','','','','','']);
+recho.radio(8, ['','','','','','','','','']);
+recho.radio(8, ['','','','','','','','','']);
+recho.radio(8, ['','','','','','','','','']);
+recho.radio(7, ['','','','','','','','','']);
+recho.radio(6, ['','','','','','','','','']);
+recho.radio(5, ['','','','','','','','','']);
+recho.radio(4, ['','','','','','','','','']);
+recho.radio(3, ['','','','','','','','','']);
+recho.radio(2, ['','','','','','','','','']);
+recho.radio(1, ['','','','','','','','','']);
+recho.radio(0, ['','','','','','','','','']);
+recho.radio(0, ['','','','','','','','','']);
+recho.radio(0, ['','','','','','','','','']);
+recho.radio(1, ['','','','','','','','','']);
+recho.radio(2, ['','','','','','','','','']);
+recho.radio(3, ['','','','','','','','','']);
+recho.radio(4, ['','','','','','','','','']);
+
+//➜ [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ]
+//➜ [ 0, 0.3141592653589793, 0.6283185307179586, 0.9424777960769379, 1.2566370614359172, 1.5707963267948966, 1.8849555921538759, 2.199114857512855, 2.5132741228718345, 2.827433388230814, 3.141592653589793…
+//➜ [ 4, 5, 6, 7, 8, 8, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 0, 1, 2, 3, 4 ]
+{
+ const n = 20;
+ const m = 8;
+ const I = echo([...Array.from({length: n}, (_, i) => i), n]);
+ const X = echo(I.map(i => i / n * Math.PI * 2));
+ echo(X.map(x => Math.round(Math.sin(x) * m / 2 + m / 2)));
+}
\ No newline at end of file
diff --git a/app/examples/sin-wave-radios.snap.png b/app/examples/sin-wave-radios.snap.png
new file mode 100644
index 0000000..ee821fb
Binary files /dev/null and b/app/examples/sin-wave-radios.snap.png differ
diff --git a/app/utils.js b/app/utils.js
index 4a1887e..da376fa 100644
--- a/app/utils.js
+++ b/app/utils.js
@@ -18,6 +18,22 @@ export function removeJSMeta(content) {
return content.replace(/^\/\*\*([\s\S]*?)\*\//, "").trimStart();
}
+function ensureSnapImageInPublic(snapFile, slug) {
+ if (!snapFile) return null;
+ const publicExamplesDir = path.join(process.cwd(), "public", "examples");
+ const publicSnapPath = path.join(publicExamplesDir, snapFile);
+ if (!fs.existsSync(publicExamplesDir)) {
+ fs.mkdirSync(publicExamplesDir, {recursive: true});
+ }
+ if (!fs.existsSync(publicSnapPath)) {
+ const sourcePath = path.join(process.cwd(), "app", "examples", snapFile);
+ if (fs.existsSync(sourcePath)) {
+ fs.copyFileSync(sourcePath, publicSnapPath);
+ }
+ }
+ return snapFile;
+}
+
export function getAllJSDocs() {
const dir = path.join(process.cwd(), "app/docs/");
const files = fs.readdirSync(dir);
@@ -39,15 +55,19 @@ export function getAllJSExamples() {
const content = fs.readFileSync(path.join(dir, file), "utf8");
const meta = parseJSMeta(content);
const {startLine, endLine} = findFirstOutputRange(content);
+ const slug = file.replace(".recho.js", "");
+ const snap = files.find((f) => f.startsWith(slug + ".snap"));
+ const publicSnap = ensureSnapImageInPublic(snap, slug);
if (!meta) {
throw new Error(`No meta found in ${file}`);
}
return {
...meta,
content,
- slug: file.replace(".recho.js", ""),
+ slug,
outputStartLine: meta.thumbnail_start ?? startLine,
outputEndLine: endLine,
+ snap: publicSnap,
};
});
}