Move to a pnpm workspace and add the frameloader.com site

- apps/desktop: the Electron app, now bundling all runtime deps into
  dist/main.cjs so packaged builds need no node_modules; Electron pinned
  for electron-builder; screenshot script for the website
- apps/web: static landing page (Vite) and terms of use, using real app
  screenshots in light and dark
- packages/tokens: shared color/type/shape tokens used by both
- Terms of use (TERMS.md, /terms.html), linked from the app's connect
  screen
- Canonical URLs, social tags, robots.txt and sitemap for
  www.frameloader.com

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Zack SheppardandClaude Opus 5.5 committed 2026-10-07 01:04:53 +01:00
1 parent bfcccef92a
commit 2c7afac144
78 files changed
+1140 -154

No files matched your search

+7 -1
View File
@@ -4,8 +4,14 @@
{
"name": "ui-preview",
"runtimeExecutable": "python3",
"runtimeArgs": ["-m", "http.server", "8765", "--directory", "dist"],
"runtimeArgs": ["-m", "http.server", "8765", "--directory", "apps/desktop/dist"],
"port": 8765
},
{
"name": "web",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["--filter", "@frameloader/web", "dev", "--strictPort"],
"port": 5180
}
]
}
+21 -4
View File
@@ -1,5 +1,7 @@
# Frameloader
[www.frameloader.com](https://www.frameloader.com)
Drop an APK, a Windows app, or a Linux ARM64 build onto Frameloader and it shows up in your Steam Frame's library, under Non-Steam, ready to launch. Open source, cross-platform (macOS, Windows, Linux), and built on the same mechanism Valve's own SteamOS Devkit Client uses.
**Drop it in. It's in your library.**
@@ -32,19 +34,34 @@ Logs for Android titles stream straight into the app (row → Logs). The Activit
## Development
This is a pnpm workspace:
| Path | What it is |
|---|---|
| `apps/desktop` | The Electron app (TypeScript, esbuild, electron-builder) |
| `apps/web` | The website (plain HTML and CSS, built with Vite) |
| `packages/tokens` | Shared color, type and shape tokens used by both |
```bash
pnpm install
pnpm start # build and run Electron
pnpm dev # esbuild in watch mode (run `electron .` separately)
pnpm start # build and run the desktop app
pnpm dev:web # website dev server on http://localhost:5180
pnpm typecheck
pnpm test
pnpm build:web # static site in apps/web/dist
pnpm dist:mac # or dist:win / dist:linux
```
The renderer can be previewed in a normal browser with simulated data: serve `dist/` and open `index.html?mock=connected` (also `fresh`, `empty`, `offline`, `nolepton`, `fail`).
The desktop app bundles all of its runtime dependencies into `dist/main.cjs`, so packages contain no `node_modules`.
The renderer can be previewed in a normal browser with simulated data: serve `apps/desktop/dist/` and open `index.html?mock=connected` (also `fresh`, `empty`, `offline`, `nolepton`, `fail`). `pnpm --filter @frameloader/desktop screenshots` regenerates the website's screenshots from that mock, in light and dark.
`pnpm install` must be allowed to run the `electron` and `esbuild` build scripts (see `pnpm-workspace.yaml`). If Electron's binary is missing, run `node node_modules/electron/install.js`.
## Privacy
Frameloader has no analytics, telemetry, crash reporting, accounts or update checks. Its only network connections are to your headset on your local network: SSH, Valve's devkit pairing service on port 32000, and mDNS discovery. A remembered password is encrypted with the operating system's secure storage. The website loads no third-party scripts, fonts or cookies.
## Troubleshooting
- **"Couldn't find frame.local."** Frameloader resolves names itself over mDNS, then the OS resolver, then by browsing for the devkit service, then the last known IP. If all fail: the Frame is asleep (it drops off the network), Developer Mode is off, or the network blocks multicast (guest/isolated Wi-Fi). Typing the IP from Quick Settings always works.
@@ -60,4 +77,4 @@ The renderer can be previewed in a normal browser with simulated data: serve `di
- [Lepton](https://gitlab.steamos.cloud/frame-public/lepton) (MIT) for documenting how Android titles are launched.
- [Frame Control](https://github.com/saphid/frame-control) (MIT) for field notes that verified each runtime on real hardware.
MIT licensed.
MIT licensed. If Frameloader saves you some time, you can [buy me a coffee](https://buymeacoffee.com/travelerdev).
+33
View File
@@ -0,0 +1,33 @@
# Frameloader terms of use
Last updated: October 7, 2026
Frameloader is free, open-source software released under the [MIT License](LICENSE). By using it you accept these terms. They're short on purpose.
## No warranty
Frameloader is provided "as is", without warranty of any kind, express or implied, including any warranty that it is fit for a particular purpose, works with your headset, or is free of bugs.
## Use at your own risk
Frameloader connects to your Steam Frame over your local network using Developer Mode, copies files to it, and registers them with Steam. Sideloaded apps may not run, may misbehave, or may crash your headset. You are responsible for what you install and for backing up anything you care about.
## Limitation of liability
To the fullest extent the law allows, the authors and contributors are not liable for any damage or loss arising from your use of Frameloader, including damage to devices, lost data, or actions taken on your accounts.
## Your responsibilities
Only install software you have the right to use, and follow its license. Steam's and Valve's own terms still apply to your headset and your Steam account.
## Not affiliated with Valve
Frameloader is an independent project. Valve, Steam, and Steam Frame are trademarks of Valve Corporation.
## Privacy
Frameloader collects no data. It has no analytics, telemetry, or accounts, and it only talks to your headset on your local network.
## Changes
These terms may be updated. The current version is always at https://www.frameloader.com/terms.html.
+98
View File
@@ -0,0 +1,98 @@
{
"name": "@frameloader/desktop",
"productName": "Frameloader",
"version": "0.1.0",
"description": "Drop an APK, Windows app, or Linux build onto your Steam Frame and it shows up in your Steam library.",
"license": "MIT",
"author": "Zack <zack@traveler.dev>",
"private": true,
"main": "dist/main.cjs",
"type": "module",
"scripts": {
"build": "node scripts/build.mjs",
"dev": "node scripts/build.mjs --watch",
"start": "pnpm build && electron .",
"typecheck": "tsc -p tsconfig.main.json --noEmit && tsc -p tsconfig.renderer.json --noEmit",
"test": "pnpm build && node --test dist/test/*.test.cjs",
"dist": "pnpm build && electron-builder --publish never",
"dist:mac": "pnpm build && electron-builder --mac --arm64 --publish never",
"dist:win": "pnpm build && electron-builder --win --x64 --publish never",
"dist:linux": "pnpm build && electron-builder --linux --x64 --arm64 --publish never",
"screenshots": "pnpm build && electron scripts/screenshot.cjs"
},
"devDependencies": {
"@frameloader/tokens": "workspace:*",
"@types/multicast-dns": "^7.2.4",
"@types/node": "^24.0.0",
"@types/ssh2": "^1.15.6",
"@types/yauzl": "^2.10.3",
"app-info-parser": "^1.1.6",
"electron": "44.5.1",
"electron-builder": "^26.15.3",
"esbuild": "^0.28.2",
"multicast-dns": "^7.2.5",
"ssh2": "^1.17.0",
"typescript": "^7.0.2",
"yauzl": "^3.2.0"
},
"build": {
"appId": "dev.traveler.frameloader",
"productName": "Frameloader",
"directories": {
"output": "release",
"buildResources": "build"
},
"files": [
"dist/**/*",
"package.json",
"!dist/test/**",
"!dist/**/*.map"
],
"extraResources": [
{
"from": "vendor/devkit-utils",
"to": "devkit-utils",
"filter": [
"**/*",
"!**/__pycache__/**"
]
}
],
"mac": {
"category": "public.app-category.utilities",
"target": [
"dmg",
"zip"
],
"artifactName": "Frameloader-mac-${arch}.${ext}"
},
"win": {
"target": [
"nsis",
"zip"
],
"artifactName": "Frameloader-win-${arch}.${ext}"
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true
},
"linux": {
"target": [
"AppImage",
"deb"
],
"category": "Utility",
"artifactName": "Frameloader-linux-${arch}.${ext}"
},
"electronVersion": "44.5.1"
},
"homepage": "https://www.frameloader.com",
"repository": {
"type": "git",
"url": "https://github.com/travelerdev/frameloader.git",
"directory": "apps/desktop"
},
"dependencies": {}
}
@@ -1,9 +1,13 @@
// Builds main + preload (Node/CJS) and the renderer (browser) with esbuild.
// `node scripts/build.mjs --watch` rebuilds on change.
import { build, context } from "esbuild";
import { cpSync, mkdirSync, readdirSync } from "node:fs";
import { cpSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { join } from "node:path";
const require = createRequire(import.meta.url);
const TOKENS = require.resolve("@frameloader/tokens/tokens.css");
const watch = process.argv.includes("--watch");
const prod = process.env.NODE_ENV === "production";
@@ -14,7 +18,9 @@ const nodeCommon = {
target: "node22",
sourcemap: !prod,
minify: false,
external: ["electron", "ssh2", "cpu-features", "app-info-parser", "yauzl"],
// Everything except Electron is bundled, so the packaged app needs no node_modules.
// ssh2's optional native helpers are loaded in try/catch and fall back to pure JS.
external: ["electron", "cpu-features", "*.node"],
logLevel: "info",
};
@@ -47,7 +53,8 @@ const configs = [
function copyStatic() {
mkdirSync("dist", { recursive: true });
cpSync("src/renderer/index.html", "dist/index.html");
cpSync("src/renderer/styles.css", "dist/styles.css");
// Shared tokens first, then the app's own styles.
writeFileSync("dist/styles.css", readFileSync(TOKENS, "utf8") + "\n" + readFileSync("src/renderer/styles.css", "utf8"));
}
copyStatic();
@@ -59,6 +66,7 @@ if (watch) {
fsWatch("src/renderer", { recursive: true }, (_e, f) => {
if (f && (f.endsWith(".html") || f.endsWith(".css"))) copyStatic();
});
fsWatch(TOKENS, () => copyStatic());
console.log("watching…");
} else {
await Promise.all(configs.map((c) => build(c)));
+56
View File
@@ -0,0 +1,56 @@
// Renders the UI against the mock bridge and saves PNGs for the website.
// Usage: pnpm screenshots (runs `electron scripts/screenshot.cjs` after a build)
const { app, BrowserWindow, nativeTheme } = require("electron");
const { mkdirSync, writeFileSync } = require("node:fs");
const { join } = require("node:path");
const OUT = join(__dirname, "..", "..", "web", "public", "screenshots");
const PAGE = join(__dirname, "..", "dist", "index.html");
const W = 960;
const H = 640;
const shots = [
{ name: "home", query: "mock=connected&shot=1", prepare: "" },
{
name: "review",
query: "mock=connected&shot=1",
prepare: `[...document.querySelectorAll("button")].find(b => b.textContent.includes("Choose file"))?.click();`,
},
];
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// Each shot opens and closes a window; don't let Electron quit in between.
app.on("window-all-closed", () => undefined);
app.whenReady().then(async () => {
mkdirSync(OUT, { recursive: true });
for (const theme of ["light", "dark"]) {
nativeTheme.themeSource = theme;
for (const shot of shots) {
const win = new BrowserWindow({ width: W, height: H, show: false, useContentSize: true, paintWhenInitiallyHidden: true });
const query = Object.fromEntries(new URLSearchParams(shot.query));
try {
await win.loadFile(PAGE, { query });
} catch (e) {
console.error("load failed", shot.name, theme, e.message);
win.destroy();
continue;
}
await sleep(1600);
if (shot.prepare) {
await win.webContents.executeJavaScript(shot.prepare);
await sleep(1200);
}
// Hide the scrollbar and the empty macOS titlebar strip for a clean frame.
await win.webContents.insertCSS("::-webkit-scrollbar{display:none} body{--titlebar:0px !important}");
await sleep(300);
const img = await win.webContents.capturePage(undefined, { stayHidden: true });
const file = join(OUT, `${shot.name}-${theme}.png`);
writeFileSync(file, img.toPNG());
console.log("wrote", file, img.getSize());
win.destroy();
}
}
app.quit();
});
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -37,7 +37,7 @@ function render(s: State): void {
if (s.panel) for (const el of panelView(s)) root.appendChild(el);
if (s.devicesSheet) for (const el of devicesView(s)) root.appendChild(el);
if (s.toast) root.appendChild(h("div", { class: `notice ${s.toast.kind}`, "data-anim": "toast", style: "position:fixed;left:50%;bottom:48px;transform:translateX(-50%);z-index:40;box-shadow:0 8px 24px rgba(16,24,40,.15);max-width:min(560px,90vw)" }, s.toast.text));
if (isMock) root.appendChild(h("div", { class: "chip warn", style: "position:fixed;top:8px;left:50%;transform:translateX(-50%);z-index:50" }, "Preview mode: no headset, simulated data"));
if (isMock && !new URLSearchParams(location.search).has("shot")) root.appendChild(h("div", { class: "chip warn", style: "position:fixed;top:8px;left:50%;transform:translateX(-50%);z-index:50" }, "Preview mode: no headset, simulated data"));
// Entrance animations only when an element first appears, not on every re-render.
root.querySelectorAll<HTMLElement>("[data-anim]").forEach((el) => {
if (!mounted.has(el.dataset.anim!)) el.classList.add("anim-in");
File renamed without changes.
File renamed without changes.
@@ -1,49 +1,5 @@
:root {
--page: #f5f6f8;
--card: #ffffff;
--card-2: #f9fafb;
--text: #1b1f24;
--text-2: #5c6470;
--text-3: #8a919c;
--border: #e3e6ea;
--border-2: #d3d7dd;
--accent: #2e6be6;
--accent-hover: #255cc9;
--accent-soft: #e9f0fd;
--success: #1f9d55;
--success-soft: #e6f6ec;
--warning: #b7791f;
--warning-soft: #fdf3e1;
--danger: #d23f3f;
--danger-soft: #fdeaea;
--mono: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, Roboto, sans-serif;
--radius: 12px;
--radius-sm: 8px;
--shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
--titlebar: 0px;
}
@media (prefers-color-scheme: dark) {
:root {
--page: #121417;
--card: #1b1e23;
--card-2: #20242a;
--text: #e8eaed;
--text-2: #a4abb5;
--text-3: #6f7782;
--border: #2a2f37;
--border-2: #363c46;
--accent: #5b8df0;
--accent-hover: #74a0f3;
--accent-soft: #1d2a45;
--success: #3fbf74;
--success-soft: #16301f;
--warning: #d9a441;
--warning-soft: #332a14;
--danger: #ef6b6b;
--danger-soft: #3a1c1c;
}
}
/* Color, type and shape tokens come from @frameloader/tokens (prepended at build). */
:root { --titlebar: 0px; }
body.mac { --titlebar: 38px; }
* { box-sizing: border-box; }
@@ -1,4 +1,6 @@
import { h, icon } from "../dom";
const TERMS_URL = "https://www.frameloader.com/terms.html";
import { store, type State } from "../state";
import { cancelPair, connectSaved, connectWithPassword, pair, useDiscovered } from "../actions";
@@ -109,6 +111,13 @@ export function connectView(s: State): HTMLElement {
found,
f.mode === "pair" ? pairBlock : passwordBlock,
f.error ? h("div", { class: "notice bad" }, f.error) : null,
h(
"p",
{ class: "help" },
"Frameloader is free software with no warranty. By connecting, you accept the ",
h("a", { href: TERMS_URL, target: "_blank", rel: "noreferrer" }, "terms of use"),
" and use it at your own risk.",
),
h(
"div",
{ class: "row spread wrap" },
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+166
View File
@@ -0,0 +1,166 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Frameloader: sideload apps onto your Steam Frame</title>
<meta name="description" content="Drop an Android app, Windows program, or Linux build onto Frameloader and it shows up in your Steam Frame's library. Free, open source, and completely private." />
<meta name="color-scheme" content="light dark" />
<meta property="og:title" content="Frameloader" />
<meta property="og:description" content="Drag. Drop. Play. Free, open-source sideloading for Steam Frame." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://www.frameloader.com/" />
<meta property="og:image" content="https://www.frameloader.com/screenshots/home-light.png" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="canonical" href="https://www.frameloader.com/" />
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<link rel="stylesheet" href="./src/styles.css" />
</head>
<body>
<header class="nav">
<div class="wrap nav-inner">
<a class="brand" href="#top" aria-label="Frameloader home">
<span class="logo"><svg viewBox="0 0 24 24"><path d="M12 3v12"/><path d="m8 11 4 4 4-4"/><rect x="4" y="15" width="16" height="6" rx="2"/></svg></span>
Frameloader
</a>
<nav class="nav-links" aria-label="Sections">
<a href="#how">How it works</a>
<a href="#privacy">Privacy</a>
<a href="#open-source">Open source</a>
<a class="btn sm" href="https://github.com/travelerdev/frameloader">
<svg viewBox="0 0 24 24"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
GitHub
</a>
</nav>
</div>
</header>
<main id="top">
<section class="hero wrap">
<span class="chip accent">For Steam Frame · Free and open source</span>
<h1>Drag. Drop. Play.</h1>
<p class="lede">
How do you sideload APK Android apps, Windows exe apps, Linux apps, and more to your Steam Frame for free
without installing sketchy closed-source shareware? You use Frameloader.
</p>
<div class="cta">
<a class="btn primary lg" href="https://github.com/travelerdev/frameloader/releases/latest">
<svg viewBox="0 0 24 24"><path d="M12 15V3"/><path d="m7 10 5 5 5-5"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/></svg>
Download
</a>
<a class="btn lg" href="https://github.com/travelerdev/frameloader">View the code</a>
</div>
<p class="small muted">macOS, Windows, and Linux · MIT licensed · Fully LAN</p>
<figure class="window" aria-label="Frameloader showing the apps installed on a Steam Frame">
<div class="window-bar"><span></span><span></span><span></span></div>
<picture>
<source srcset="./screenshots/home-dark.png" media="(prefers-color-scheme: dark)" />
<img src="./screenshots/home-light.png" width="960" height="640" alt="Frameloader's home screen: a drop zone above a list of three apps installed on the headset, each with a Play button." />
</picture>
</figure>
</section>
<section id="how" class="section wrap">
<div class="split">
<div class="steps">
<h2>How to get loading</h2>
<ol>
<li>
<span class="num">1</span>
<div><b>Turn on Developer Mode</b><span>Steam Settings → System → Enable Developer Mode, then set a user password under Developer.</span></div>
</li>
<li>
<span class="num">2</span>
<div><b>Pair</b><span>Open Settings → Developer → Pair new host on the headset, click Pair in Frameloader, and approve.</span></div>
</li>
<li>
<span class="num">3</span>
<div><b>Drop</b><span>From then on it's drag, drop, Install. The title appears under Non-Steam in your library.</span></div>
</li>
</ol>
<p class="muted small">
Frameloader automates the sideloading process that Valve built for developers to test apps on the Frame.
This lets you get apps into the Frame's library without needing to mess around with the terminal yourself every time.
</p>
</div>
<figure class="window small-window">
<div class="window-bar"><span></span><span></span><span></span></div>
<picture>
<source srcset="./screenshots/review-dark.png" media="(prefers-color-scheme: dark)" />
<img src="./screenshots/review-light.png" width="960" height="640" loading="lazy" alt="Reviewing an Android app before installing: its name, details like arm64 and VR app, the Lepton runtime, and an Install to Frame button." />
</picture>
</figure>
</div>
</section>
<section id="privacy" class="section wrap">
<div class="card privacy">
<div class="privacy-head">
<div class="icon big"><svg viewBox="0 0 24 24"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg></div>
<div>
<h2>Actually private - no analytics, no tracking, nothing.</h2>
<p class="muted">Frameloader is a tiny piece of freeware that solves one problem - getting apps from your computer to your Steam Frame.</p>
</div>
</div>
<ul class="checks">
<li><b>No analytics, no telemetry, no crash reporting.</b> Frameloader doesn't count you, profile you, or phone home.</li>
<li><b>No account.</b> Nothing to sign up for and nothing to sign in to.</li>
<li><b>Talks only to your Frame.</b> Its only connections are to the headset on your own network: SSH, Valve's pairing service, and local discovery.</li>
<li><b>This website doesn't track you either.</b> No cookies, no analytics, no third-party fonts or scripts.</li>
</ul>
<p class="small muted">Don't take our word for it: <a href="https://github.com/travelerdev/frameloader">read the code</a>.</p>
</div>
</section>
<section id="open-source" class="section wrap">
<div class="section-head">
<h2>Open source, top to bottom</h2>
<p class="muted">Yes it was written with the help of Claude, but it's also a straight forward bit of code. The whole app (and this site) is MIT licensed and you can read it, fork it, file an issue, send a PR.</p>
</div>
<div class="grid two">
<a class="card link-card" href="https://github.com/travelerdev/frameloader">
<h3>travelerdev/frameloader</h3>
<p class="muted">The desktop app, this website, and the notes on how sideloading to the Frame actually works.</p>
<span class="more">View on GitHub →</span>
</a>
<div class="card link-card">
<h3>Standing on good shoulders</h3>
<p class="muted">
Valve's open-source <a href="https://gitlab.steamos.cloud/devkit/steamos-devkit">SteamOS Devkit Client</a> and
<a href="https://gitlab.steamos.cloud/frame-public/lepton">Lepton</a>, plus the field notes from
<a href="https://github.com/saphid/frame-control">Frame Control</a>.
</p>
</div>
</div>
</section>
<section id="support" class="section wrap">
<div class="card support">
<div class="support-text">
<h2>Frameloader is 100% free and open source</h2>
<p class="muted">
I'm <a href="https://zack.computer">Zack</a> - I made this because I wanted to solve this problem without having to
install sketchy closed-source software or dig into terminal all afternoon.
</p>
</div>
<a class="btn primary lg" href="https://buymeacoffee.com/travelerdev">
<svg viewBox="0 0 24 24"><path d="M10 2v2"/><path d="M14 2v2"/><path d="M16 8a1 1 0 0 1 1 1v8a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1h14a4 4 0 1 1 0 8h-1"/><path d="M6 2v2"/></svg>
Buy me a coffee
</a>
</div>
</section>
</main>
<footer class="footer">
<div class="wrap footer-inner">
<div class="footer-meta">
<span>© 2026 Traveler Dev · <a href="https://github.com/travelerdev/frameloader">GitHub</a> · <a href="https://github.com/travelerdev/frameloader/blob/main/LICENSE">MIT License</a> · <a href="./terms.html">Terms</a></span>
<span class="muted small">Frameloader is provided as is, with no warranty. Use it at your own risk; see the <a href="./terms.html">terms</a>.</span>
<span class="muted small">Not affiliated with Valve. Steam and Steam Frame are trademarks of Valve Corporation.</span>
</div>
</div>
</footer>
</body>
</html>
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@frameloader/web",
"version": "0.1.0",
"private": true,
"description": "The Frameloader website.",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"typecheck": "true"
},
"dependencies": {
"@frameloader/tokens": "workspace:*"
},
"devDependencies": {
"vite": "^8.3.3"
},
"homepage": "https://www.frameloader.com"
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="9" fill="#2E6BE6"/><g fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M16 6v12"/><path d="m11.5 13.5 4.5 4.5 4.5-4.5"/><rect x="8" y="20" width="16" height="6" rx="2"/></g></svg>

After

Width:  |  Height:  |  Size: 325 B

+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://www.frameloader.com/sitemap.xml
Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://www.frameloader.com/</loc></url>
<url><loc>https://www.frameloader.com/terms.html</loc></url>
</urlset>
+127
View File
@@ -0,0 +1,127 @@
@import "@frameloader/tokens/tokens.css";
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font: 16px/1.6 var(--sans);
color: var(--text);
background: var(--page);
-webkit-font-smoothing: antialiased;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
h1, h2, h3 { margin: 0; font-weight: 600; letter-spacing: -0.015em; line-height: 1.2; }
h1 { font-size: clamp(36px, 6vw, 56px); letter-spacing: -0.025em; }
h2 { font-size: clamp(24px, 3.2vw, 30px); }
h3 { font-size: 17px; }
p { margin: 0; }
.muted { color: var(--text-2); }
.small { font-size: 14px; }
svg { width: 18px; height: 18px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.wrap { width: 100%; max-width: 1040px; margin: 0 auto; padding: 0 16px; }
@media (min-width: 720px) { .wrap { padding: 0 24px; } }
/* Nav */
.nav { position: sticky; top: 0; z-index: 10; background: color-mix(in srgb, var(--page) 85%, transparent); backdrop-filter: saturate(1.4) blur(12px); border-bottom: 1px solid var(--border); }
.nav-inner { display: flex; align-items: center; justify-content: space-between; height: 60px; gap: 16px; }
.brand { display: flex; align-items: center; gap: 10px; font-weight: 600; font-size: 16px; color: var(--text); }
.brand:hover { text-decoration: none; }
.logo { width: 28px; height: 28px; border-radius: 8px; background: var(--accent); color: #fff; display: grid; place-items: center; }
.logo svg { width: 17px; height: 17px; stroke-width: 2.2; }
.nav-links { display: flex; align-items: center; gap: 20px; font-size: 15px; }
.nav-links > a:not(.btn) { color: var(--text-2); }
.nav-links > a:not(.btn):hover { color: var(--text); text-decoration: none; }
@media (max-width: 640px) { .nav-links > a:not(.btn) { display: none; } }
/* Buttons and chips */
.btn {
display: inline-flex; align-items: center; gap: 8px; padding: 9px 16px; border-radius: var(--radius-sm);
border: 1px solid var(--border-2); background: var(--card); color: var(--text); font-weight: 500; font-size: 15px; line-height: 1.2;
transition: background 120ms, border-color 120ms;
}
.btn:hover { background: var(--card-2); text-decoration: none; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.btn.sm { padding: 6px 12px; font-size: 14px; }
.btn.lg { padding: 12px 20px; font-size: 16px; }
.chip { display: inline-flex; align-items: center; padding: 4px 12px; border-radius: 999px; font-size: 13.5px; font-weight: 500; }
.chip.accent { color: var(--accent); background: var(--accent-soft); }
/* Hero */
.hero { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 20px; padding-top: 72px; padding-bottom: 24px; }
.lede { font-size: clamp(17px, 2.2vw, 19px); color: var(--text-2); max-width: 620px; }
.cta { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; margin-top: 4px; }
/* App window frame around screenshots */
.window {
margin: 36px 0 0; width: 100%; max-width: 960px; border-radius: 14px; overflow: hidden; background: var(--card);
border: 1px solid var(--border-2); box-shadow: 0 24px 60px -20px rgba(16, 24, 40, 0.25), 0 2px 6px rgba(16, 24, 40, 0.05);
}
.window-bar { height: 34px; display: flex; align-items: center; gap: 8px; padding: 0 14px; border-bottom: 1px solid var(--border); background: var(--card); }
.window-bar span { width: 11px; height: 11px; border-radius: 50%; background: var(--border-2); }
.window img { display: block; width: 100%; height: auto; }
/* Sections */
.section { padding-top: 88px; }
.section-head { display: flex; flex-direction: column; gap: 8px; margin-bottom: 28px; max-width: 640px; }
.card { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); }
.grid { display: grid; gap: 16px; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
.grid.two { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
@media (max-width: 380px) { .grid, .grid.two { grid-template-columns: 1fr; } }
.feature { padding: 22px; display: flex; flex-direction: column; gap: 10px; }
.feature p { color: var(--text-2); font-size: 15px; }
.icon { width: 38px; height: 38px; border-radius: 10px; background: var(--accent-soft); color: var(--accent); display: grid; place-items: center; }
.icon.big { width: 48px; height: 48px; border-radius: 12px; flex: none; }
.icon.big svg { width: 24px; height: 24px; }
/* Steps */
.split { display: grid; gap: 40px; align-items: center; grid-template-columns: 1fr; }
@media (min-width: 880px) { .split { grid-template-columns: 0.9fr 1.1fr; } }
.steps { display: flex; flex-direction: column; gap: 20px; }
.steps ol { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 16px; }
.steps li { display: flex; gap: 14px; align-items: flex-start; }
.steps li b { display: block; }
.steps li span:not(.num) { color: var(--text-2); font-size: 15px; }
.num { width: 28px; height: 28px; border-radius: 50%; background: var(--accent-soft); color: var(--accent); display: grid; place-items: center; font-size: 14px; font-weight: 600; flex: none; margin-top: 1px; }
.small-window { margin: 0; }
/* Privacy */
.privacy { padding: clamp(22px, 4vw, 40px); display: flex; flex-direction: column; gap: 24px; }
.privacy-head { display: flex; gap: 18px; align-items: flex-start; }
.privacy-head > div:last-child { display: flex; flex-direction: column; gap: 6px; }
.checks { list-style: none; margin: 0; padding: 0; display: grid; gap: 14px 32px; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
@media (max-width: 380px) { .checks { grid-template-columns: 1fr; } }
.checks li { position: relative; padding-left: 30px; color: var(--text-2); font-size: 15px; }
.checks li b { color: var(--text); }
.checks li::before {
content: ""; position: absolute; left: 0; top: 3px; width: 20px; height: 20px; border-radius: 50%; background: var(--success-soft);
-webkit-mask: none;
}
.checks li::after {
content: ""; position: absolute; left: 7px; top: 7px; width: 5px; height: 9px; border: solid var(--success); border-width: 0 2px 2px 0; transform: rotate(45deg);
}
/* Open source */
.link-card { padding: 22px; display: flex; flex-direction: column; gap: 10px; color: var(--text); }
a.link-card:hover { text-decoration: none; border-color: var(--accent); }
.link-card p { font-size: 15px; }
.more { color: var(--accent); font-weight: 500; font-size: 15px; margin-top: auto; }
/* Footer */
.footer { margin-top: 88px; border-top: 1px solid var(--border); background: var(--card); }
.footer-inner { padding-top: 40px; padding-bottom: 40px; display: flex; flex-direction: column; gap: 28px; }
.support {
padding: clamp(24px, 4vw, 40px); display: flex; gap: 24px; align-items: center; justify-content: space-between; flex-wrap: wrap;
background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 35%, transparent);
}
.support-text { display: flex; flex-direction: column; gap: 10px; max-width: 620px; }
.support-text p { font-size: 16px; }
.footer-meta { display: flex; flex-direction: column; gap: 6px; font-size: 14px; color: var(--text-2); }
/* Terms page */
.legal { max-width: 720px; padding-top: 56px; display: flex; flex-direction: column; gap: 18px; }
.legal h2 { font-size: 19px; margin-top: 12px; }
.legal p, .legal li { color: var(--text-2); }
.legal ul { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 6px; }
+62
View File
@@ -0,0 +1,62 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Frameloader terms of use</title>
<meta name="description" content="Frameloader is free software provided as is, with no warranty. Use it at your own risk." />
<meta name="color-scheme" content="light dark" />
<link rel="canonical" href="https://www.frameloader.com/terms.html" />
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
<link rel="stylesheet" href="./src/styles.css" />
</head>
<body>
<header class="nav">
<div class="wrap nav-inner">
<a class="brand" href="./index.html" aria-label="Frameloader home">
<span class="logo"><svg viewBox="0 0 24 24"><path d="M12 3v12"/><path d="m8 11 4 4 4-4"/><rect x="4" y="15" width="16" height="6" rx="2"/></svg></span>
Frameloader
</a>
<nav class="nav-links" aria-label="Sections">
<a class="btn sm" href="./index.html">Back to home</a>
</nav>
</div>
</header>
<main class="wrap legal">
<h1 style="font-size: clamp(30px, 5vw, 40px)">Terms of use</h1>
<p class="small">Last updated: October 7, 2026</p>
<p>Frameloader is free, open-source software released under the <a href="https://github.com/travelerdev/frameloader/blob/main/LICENSE">MIT License</a>. By using it you accept these terms. They're short on purpose.</p>
<h2>No warranty</h2>
<p>Frameloader is provided "as is", without warranty of any kind, express or implied, including any warranty that it is fit for a particular purpose, works with your headset, or is free of bugs.</p>
<h2>Use at your own risk</h2>
<p>Frameloader connects to your Steam Frame over your local network using Developer Mode, copies files to it, and registers them with Steam. Sideloaded apps may not run, may misbehave, or may crash your headset. You are responsible for what you install and for backing up anything you care about.</p>
<h2>Limitation of liability</h2>
<p>To the fullest extent the law allows, the authors and contributors are not liable for any damage or loss arising from your use of Frameloader, including damage to devices, lost data, or actions taken on your accounts.</p>
<h2>Your responsibilities</h2>
<p>Only install software you have the right to use, and follow its license. Steam's and Valve's own terms still apply to your headset and your Steam account.</p>
<h2>Not affiliated with Valve</h2>
<p>Frameloader is an independent project. Valve, Steam, and Steam Frame are trademarks of Valve Corporation.</p>
<h2>Privacy</h2>
<p>Frameloader collects no data. It has no analytics, telemetry, or accounts, and it only talks to your headset on your local network.</p>
<h2>Changes</h2>
<p>These terms may be updated. The current version is always at <a href="https://www.frameloader.com/terms.html">frameloader.com/terms.html</a>.</p>
</main>
<footer class="footer">
<div class="wrap footer-inner">
<div class="footer-meta">
<span>© 2026 Traveler Dev · <a href="https://github.com/travelerdev/frameloader">GitHub</a> · <a href="https://github.com/travelerdev/frameloader/blob/main/LICENSE">MIT License</a></span>
<span class="muted small">Not affiliated with Valve. Steam and Steam Frame are trademarks of Valve Corporation.</span>
</div>
</div>
</footer>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig } from "vite";
import { resolve } from "node:path";
export default defineConfig({
// Relative asset URLs so the built site works from any path (GitHub Pages, a subfolder, a file share).
base: "./",
server: { port: 5180 },
build: {
outDir: "dist",
assetsInlineLimit: 0,
rollupOptions: {
input: {
main: resolve(import.meta.dirname, "index.html"),
terms: resolve(import.meta.dirname, "terms.html"),
},
},
},
});
+12 -84
View File
@@ -1,89 +1,17 @@
{
"name": "frameloader",
"productName": "Frameloader",
"version": "0.1.0",
"description": "Drop an APK, Windows app, or Linux build onto your Steam Frame and it shows up in your Steam library.",
"license": "MIT",
"author": "Zack <zack@traveler.dev>",
"name": "frameloader-monorepo",
"private": true,
"main": "dist/main.cjs",
"type": "module",
"license": "MIT",
"packageManager": "pnpm@11.24.0",
"scripts": {
"build": "node scripts/build.mjs",
"dev": "node scripts/build.mjs --watch",
"start": "pnpm build && electron .",
"typecheck": "tsc -p tsconfig.main.json --noEmit && tsc -p tsconfig.renderer.json --noEmit",
"test": "pnpm build && node --test dist/test/*.test.cjs",
"dist": "pnpm build && electron-builder --publish never",
"dist:mac": "pnpm build && electron-builder --mac --arm64 --publish never",
"dist:win": "pnpm build && electron-builder --win --x64 --publish never",
"dist:linux": "pnpm build && electron-builder --linux --x64 --arm64 --publish never"
},
"dependencies": {
"app-info-parser": "^1.1.6",
"multicast-dns": "^7.2.5",
"ssh2": "^1.17.0",
"yauzl": "^3.2.0"
},
"devDependencies": {
"@types/multicast-dns": "^7.2.4",
"@types/node": "^24.0.0",
"@types/ssh2": "^1.15.6",
"@types/yauzl": "^2.10.3",
"electron": "^44.5.1",
"electron-builder": "^26.15.3",
"esbuild": "^0.28.2",
"typescript": "^7.0.2"
},
"build": {
"appId": "dev.traveler.frameloader",
"productName": "Frameloader",
"directories": {
"output": "release",
"buildResources": "build"
},
"files": [
"dist/**/*",
"package.json",
"!dist/test/**"
],
"extraResources": [
{
"from": "vendor/devkit-utils",
"to": "devkit-utils",
"filter": [
"**/*",
"!**/__pycache__/**"
]
}
],
"mac": {
"category": "public.app-category.utilities",
"target": [
"dmg",
"zip"
],
"artifactName": "Frameloader-mac-${arch}.${ext}"
},
"win": {
"target": [
"nsis",
"zip"
],
"artifactName": "Frameloader-win-${arch}.${ext}"
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true
},
"linux": {
"target": [
"AppImage",
"deb"
],
"category": "Utility",
"artifactName": "Frameloader-linux-${arch}.${ext}"
}
"build": "pnpm -r build",
"typecheck": "pnpm -r typecheck",
"test": "pnpm --filter @frameloader/desktop test",
"start": "pnpm --filter @frameloader/desktop start",
"dev:web": "pnpm --filter @frameloader/web dev",
"build:web": "pnpm --filter @frameloader/web build",
"dist:mac": "pnpm --filter @frameloader/desktop dist:mac",
"dist:win": "pnpm --filter @frameloader/desktop dist:win",
"dist:linux": "pnpm --filter @frameloader/desktop dist:linux"
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"name": "@frameloader/tokens",
"version": "0.1.0",
"private": true,
"description": "Shared color, type and shape tokens for the Frameloader app and website.",
"license": "MIT",
"exports": {
"./tokens.css": "./tokens.css"
},
"files": ["tokens.css"]
}
+46
View File
@@ -0,0 +1,46 @@
/* Frameloader design tokens, shared by the desktop app and the website. */
:root {
--page: #f5f6f8;
--card: #ffffff;
--card-2: #f9fafb;
--text: #1b1f24;
--text-2: #5c6470;
--text-3: #8a919c;
--border: #e3e6ea;
--border-2: #d3d7dd;
--accent: #2e6be6;
--accent-hover: #255cc9;
--accent-soft: #e9f0fd;
--success: #1f9d55;
--success-soft: #e6f6ec;
--warning: #b7791f;
--warning-soft: #fdf3e1;
--danger: #d23f3f;
--danger-soft: #fdeaea;
--mono: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, Roboto, sans-serif;
--radius: 12px;
--radius-sm: 8px;
--shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
}
@media (prefers-color-scheme: dark) {
:root {
--page: #121417;
--card: #1b1e23;
--card-2: #20242a;
--text: #e8eaed;
--text-2: #a4abb5;
--text-3: #6f7782;
--border: #2a2f37;
--border-2: #363c46;
--accent: #5b8df0;
--accent-hover: #74a0f3;
--accent-soft: #1d2a45;
--success: #3fbf74;
--success-soft: #16301f;
--warning: #d9a441;
--warning-soft: #332a14;
--danger: #ef6b6b;
--danger-soft: #3a1c1c;
}
}
+424 -15
View File
@@ -6,21 +6,13 @@ settings:
importers:
.:
dependencies:
app-info-parser:
specifier: ^1.1.6
version: 1.1.6
multicast-dns:
specifier: ^7.2.5
version: 7.2.5
ssh2:
specifier: ^1.17.0
version: 1.17.0
yauzl:
specifier: ^3.2.0
version: 3.4.0
.: {}
apps/desktop:
devDependencies:
'@frameloader/tokens':
specifier: workspace:*
version: link:../../packages/tokens
'@types/multicast-dns':
specifier: ^7.2.4
version: 7.2.4
@@ -33,8 +25,11 @@ importers:
'@types/yauzl':
specifier: ^2.10.3
version: 2.10.3
app-info-parser:
specifier: ^1.1.6
version: 1.1.6
electron:
specifier: ^44.5.1
specifier: 44.5.1
version: 44.5.1(supports-color@7.2.0)
electron-builder:
specifier: ^26.15.3
@@ -42,9 +37,30 @@ importers:
esbuild:
specifier: ^0.28.2
version: 0.28.2
multicast-dns:
specifier: ^7.2.5
version: 7.2.5
ssh2:
specifier: ^1.17.0
version: 1.17.0
typescript:
specifier: ^7.0.2
version: 7.0.2
yauzl:
specifier: ^3.2.0
version: 3.4.0
apps/web:
dependencies:
'@frameloader/tokens':
specifier: workspace:*
version: link:../../packages/tokens
devDependencies:
vite:
specifier: ^8.3.3
version: 8.3.3(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0)
packages/tokens: {}
packages:
@@ -271,6 +287,9 @@ packages:
resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==}
engines: {node: '>= 20.19.0'}
'@oxc-project/types@0.152.0':
resolution: {integrity: sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==}
'@peculiar/asn1-schema@2.10.0':
resolution: {integrity: sha512-GhokD41lV4gQrrLYm3wCkHfBOnJrnhDMgt4XeMW8gzfE1UdJqIuSwsE+ggf82XBjUXRJylcm+KIGQFa4utIVLw==}
engines: {node: '>=14'}
@@ -286,6 +305,105 @@ packages:
resolution: {integrity: sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==}
engines: {node: '>=14.18.0'}
'@rolldown/binding-android-arm-eabi@1.2.12':
resolution: {integrity: sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
'@rolldown/binding-android-arm64@1.2.12':
resolution: {integrity: sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.2.12':
resolution: {integrity: sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.2.12':
resolution: {integrity: sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.2.12':
resolution: {integrity: sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.2.12':
resolution: {integrity: sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.2.12':
resolution: {integrity: sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.2.12':
resolution: {integrity: sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.2.12':
resolution: {integrity: sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.2.12':
resolution: {integrity: sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.2.12':
resolution: {integrity: sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.2.12':
resolution: {integrity: sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.2.12':
resolution: {integrity: sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-win32-arm64-msvc@1.2.12':
resolution: {integrity: sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.2.12':
resolution: {integrity: sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/pluginutils@1.0.1':
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
'@sindresorhus/is@4.6.0':
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
engines: {node: '>=10'}
@@ -701,6 +819,10 @@ packages:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
detect-node@2.1.0:
resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
@@ -858,6 +980,11 @@ packages:
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@@ -1016,6 +1143,80 @@ packages:
lazy-val@1.0.5:
resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==}
lightningcss-android-arm64@1.33.0:
resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
lightningcss-darwin-arm64@1.33.0:
resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.33.0:
resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.33.0:
resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.33.0:
resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.33.0:
resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.33.0:
resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.33.0:
resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.33.0:
resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.33.0:
resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.33.0:
resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.33.0:
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
engines: {node: '>= 12.0.0'}
lodash@4.18.1:
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
@@ -1100,6 +1301,11 @@ packages:
nan@2.29.0:
resolution: {integrity: sha512-GlGk3HIvitbvs+LT3g6XUP1kpirKNvmDFwF/bmo6XNWSb/eYEs/O4bfgIEIXCZ+lIOTS5xNwDvSGMw6FJdAhtA==}
nanoid@3.3.20:
resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
node-abi@4.35.0:
resolution: {integrity: sha512-ymk4aIzxdPopw2giv8Fs1Ec6vybGkjmyxUwVqhkI4MCy2tVfXdkOGGWieWVjL0THgH+7a8lRdevyupoYj3Js/Q==}
engines: {node: '>=22.12.0'}
@@ -1169,6 +1375,10 @@ packages:
resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==}
engines: {node: '>=10.4.0'}
postcss@8.5.29:
resolution: {integrity: sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==}
engines: {node: ^10 || ^12 || >=14}
postject@1.0.0-alpha.6:
resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==}
engines: {node: '>=14.0.0'}
@@ -1244,6 +1454,11 @@ packages:
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
engines: {node: '>=8.0'}
rolldown@1.2.12:
resolution: {integrity: sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
safe-buffer@5.1.2:
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
@@ -1297,6 +1512,10 @@ packages:
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
engines: {node: '>=10'}
source-map-js@1.2.2:
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
@@ -1423,6 +1642,49 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
vite@8.3.3:
resolution: {integrity: sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
'@vitejs/devtools': ^0.7.1
esbuild: ^0.27.0 || ^0.28.0
jiti: '>=1.21.0'
less: ^4.0.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
'@vitejs/devtools':
optional: true
esbuild:
optional: true
jiti:
optional: true
less:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
webcrypto-core@1.9.2:
resolution: {integrity: sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==}
@@ -1679,6 +1941,8 @@ snapshots:
'@noble/hashes@2.4.0': {}
'@oxc-project/types@0.152.0': {}
'@peculiar/asn1-schema@2.10.0':
dependencies:
'@peculiar/utils': 2.0.3
@@ -1701,6 +1965,53 @@ snapshots:
tslib: 2.8.1
webcrypto-core: 1.9.2
'@rolldown/binding-android-arm-eabi@1.2.12':
optional: true
'@rolldown/binding-android-arm64@1.2.12':
optional: true
'@rolldown/binding-darwin-arm64@1.2.12':
optional: true
'@rolldown/binding-darwin-x64@1.2.12':
optional: true
'@rolldown/binding-freebsd-x64@1.2.12':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.2.12':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.2.12':
optional: true
'@rolldown/binding-linux-arm64-musl@1.2.12':
optional: true
'@rolldown/binding-linux-ppc64-gnu@1.2.12':
optional: true
'@rolldown/binding-linux-s390x-gnu@1.2.12':
optional: true
'@rolldown/binding-linux-x64-gnu@1.2.12':
optional: true
'@rolldown/binding-linux-x64-musl@1.2.12':
optional: true
'@rolldown/binding-openharmony-arm64@1.2.12':
optional: true
'@rolldown/binding-win32-arm64-msvc@1.2.12':
optional: true
'@rolldown/binding-win32-x64-msvc@1.2.12':
optional: true
'@rolldown/pluginutils@1.0.1': {}
'@sindresorhus/is@4.6.0': {}
'@szmarczak/http-timer@4.0.6':
@@ -2114,6 +2425,8 @@ snapshots:
delayed-stream@1.0.0: {}
detect-libc@2.1.2: {}
detect-node@2.1.0:
optional: true
@@ -2344,6 +2657,9 @@ snapshots:
fs.realpath@1.0.0: {}
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
get-caller-file@2.0.5: {}
@@ -2520,6 +2836,55 @@ snapshots:
lazy-val@1.0.5: {}
lightningcss-android-arm64@1.33.0:
optional: true
lightningcss-darwin-arm64@1.33.0:
optional: true
lightningcss-darwin-x64@1.33.0:
optional: true
lightningcss-freebsd-x64@1.33.0:
optional: true
lightningcss-linux-arm-gnueabihf@1.33.0:
optional: true
lightningcss-linux-arm64-gnu@1.33.0:
optional: true
lightningcss-linux-arm64-musl@1.33.0:
optional: true
lightningcss-linux-x64-gnu@1.33.0:
optional: true
lightningcss-linux-x64-musl@1.33.0:
optional: true
lightningcss-win32-arm64-msvc@1.33.0:
optional: true
lightningcss-win32-x64-msvc@1.33.0:
optional: true
lightningcss@1.33.0:
dependencies:
detect-libc: 2.1.2
optionalDependencies:
lightningcss-android-arm64: 1.33.0
lightningcss-darwin-arm64: 1.33.0
lightningcss-darwin-x64: 1.33.0
lightningcss-freebsd-x64: 1.33.0
lightningcss-linux-arm-gnueabihf: 1.33.0
lightningcss-linux-arm64-gnu: 1.33.0
lightningcss-linux-arm64-musl: 1.33.0
lightningcss-linux-x64-gnu: 1.33.0
lightningcss-linux-x64-musl: 1.33.0
lightningcss-win32-arm64-msvc: 1.33.0
lightningcss-win32-x64-msvc: 1.33.0
lodash@4.18.1: {}
long@3.2.0: {}
@@ -2587,6 +2952,8 @@ snapshots:
nan@2.29.0:
optional: true
nanoid@3.3.20: {}
node-abi@4.35.0:
dependencies:
semver: 7.8.5
@@ -2656,6 +3023,12 @@ snapshots:
base64-js: 1.5.1
xmlbuilder: 15.1.1
postcss@8.5.29:
dependencies:
nanoid: 3.3.20
picocolors: 1.1.1
source-map-js: 1.2.2
postject@1.0.0-alpha.6:
dependencies:
commander: 9.5.0
@@ -2737,6 +3110,27 @@ snapshots:
sprintf-js: 1.1.3
optional: true
rolldown@1.2.12:
dependencies:
'@oxc-project/types': 0.152.0
'@rolldown/pluginutils': 1.0.1
optionalDependencies:
'@rolldown/binding-android-arm-eabi': 1.2.12
'@rolldown/binding-android-arm64': 1.2.12
'@rolldown/binding-darwin-arm64': 1.2.12
'@rolldown/binding-darwin-x64': 1.2.12
'@rolldown/binding-freebsd-x64': 1.2.12
'@rolldown/binding-linux-arm-gnueabihf': 1.2.12
'@rolldown/binding-linux-arm64-gnu': 1.2.12
'@rolldown/binding-linux-arm64-musl': 1.2.12
'@rolldown/binding-linux-ppc64-gnu': 1.2.12
'@rolldown/binding-linux-s390x-gnu': 1.2.12
'@rolldown/binding-linux-x64-gnu': 1.2.12
'@rolldown/binding-linux-x64-musl': 1.2.12
'@rolldown/binding-openharmony-arm64': 1.2.12
'@rolldown/binding-win32-arm64-msvc': 1.2.12
'@rolldown/binding-win32-x64-msvc': 1.2.12
safe-buffer@5.1.2: {}
safer-buffer@2.1.2: {}
@@ -2775,6 +3169,8 @@ snapshots:
dependencies:
semver: 7.8.5
source-map-js@1.2.2: {}
source-map-support@0.5.21:
dependencies:
buffer-from: 1.1.2
@@ -2921,6 +3317,19 @@ snapshots:
util-deprecate@1.0.2: {}
vite@8.3.3(@types/node@24.19.1)(esbuild@0.28.2)(jiti@2.7.0):
dependencies:
lightningcss: 1.33.0
picomatch: 4.0.7
postcss: 8.5.29
rolldown: 1.2.12
tinyglobby: 0.2.17
optionalDependencies:
'@types/node': 24.19.1
esbuild: 0.28.2
fsevents: 2.3.3
jiti: 2.7.0
webcrypto-core@1.9.2:
dependencies:
'@peculiar/asn1-schema': 2.10.0
+5
View File
@@ -1,3 +1,6 @@
packages:
- apps/*
- packages/*
nodeLinker: hoisted
allowBuilds:
electron: true
@@ -5,3 +8,5 @@ allowBuilds:
ssh2: true
cpu-features: true
electron-winstaller: false
minimumReleaseAgeExclude:
- vite@8.3.3