Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
423b01ee98 | ||
|
|
93b62fa185 | ||
|
|
fc47dab20b | ||
|
|
58a936c706 | ||
|
|
72f94d1754 | ||
|
|
af6117ad5d | ||
|
|
540c71982f | ||
|
|
daa71a144e | ||
|
|
a64a4c6958 | ||
|
|
c0b3ba960a | ||
|
|
aaf6aba621 | ||
|
|
80ef9ca6a7 | ||
|
|
c744f8740b | ||
|
|
859f921361 | ||
|
|
6acf9d048e | ||
|
|
cf18b30be9 | ||
|
|
eec0218b1d | ||
|
|
526ba0a684 | ||
|
|
23b6ff1b5e | ||
|
|
00c6863e54 | ||
|
|
4863035a05 | ||
|
|
a30162ea63 | ||
|
|
24eaa97fde | ||
|
|
767b152423 | ||
|
|
10e7669f70 | ||
|
|
76c4f2d225 | ||
|
|
3a851e911f | ||
|
|
2912d637b5 | ||
|
|
5c070ccf23 | ||
|
|
b979067215 | ||
|
|
4440b459e5 | ||
|
|
09b8046337 | ||
|
|
0d67335775 | ||
|
|
867b9977d5 | ||
|
|
80092098e0 | ||
|
|
feab03e049 | ||
|
|
9713da222a | ||
|
|
82e617741e | ||
|
|
9de0707070 | ||
|
|
1343930ae0 | ||
|
|
5424e429e4 | ||
|
|
9149eaecd7 | ||
|
|
64278e9e2c | ||
|
|
1531acb575 | ||
|
|
6796e006d9 | ||
|
|
45f05dd3c6 | ||
|
|
5a2cd45fb8 | ||
|
|
757671b389 | ||
|
|
8a2029c079 | ||
|
|
b2688dd368 | ||
|
|
13de114f39 | ||
|
|
9016982394 | ||
|
|
cad2cd366c | ||
|
|
a5820e68d7 | ||
|
|
aff939a6d8 | ||
|
|
5e725fbd08 | ||
|
|
1cb9e6f65b | ||
|
|
7a843962e8 | ||
|
|
51220a4942 | ||
|
|
981daa3c2d | ||
|
|
9781c2bb6b | ||
|
|
a7c349835c | ||
|
|
9a48eac3e6 | ||
|
|
9082c684ed | ||
|
|
84235fbc9f | ||
|
|
8accd6221a | ||
|
|
c37eebb59f | ||
|
|
5b0bd2589b | ||
|
|
231e5e50cd | ||
|
|
423b2e1c3e | ||
|
|
b6c5f7c18e | ||
|
|
74320e607f | ||
|
|
d87e3b3b5f | ||
|
|
c4153be8bc | ||
|
|
b5a16a2680 | ||
|
|
f6870da310 | ||
|
|
6c8cda5d7a | ||
|
|
7331d9320a | ||
|
|
dccdcaab96 | ||
|
|
99c6a88f90 | ||
|
|
da4f26197d | ||
|
|
02361b7c99 | ||
|
|
d6bbea05ca | ||
|
|
65d3c2ab67 | ||
|
|
327f213f56 | ||
|
|
05c24cc9e0 | ||
|
|
6e31ae52c4 | ||
|
|
7830444c87 | ||
|
|
9ace9d3bb3 | ||
|
|
a994e86ba5 | ||
|
|
e776116162 | ||
|
|
1e814af1d4 | ||
|
|
f3ed77deb0 | ||
|
|
d5245aa496 | ||
|
|
e4a62d6e90 | ||
|
|
dcc57f29c8 | ||
|
|
d87d085842 | ||
|
|
3930617518 | ||
|
|
deaf1f08a6 | ||
|
|
76ba1e4ffa | ||
|
|
62728dc562 | ||
|
|
6454c7f37d | ||
|
|
a4f73a88b7 | ||
|
|
e5658ed441 | ||
|
|
6bfaaf788f | ||
|
|
07da1c27a3 | ||
|
|
35462324bb | ||
|
|
2c1149815d | ||
|
|
c9b600d186 | ||
|
|
ca14fad3ce | ||
|
|
c4fa6557e1 | ||
|
|
1bb08bab36 | ||
|
|
cccafe86bc | ||
|
|
6fb4a52051 | ||
|
|
5f42c4bb01 | ||
|
|
37b8ac78a7 | ||
|
|
26b86f3f9d | ||
|
|
8d50d5a065 | ||
|
|
ce257b429a | ||
|
|
7e95c50f39 | ||
|
|
61ecc4d2ee | ||
|
|
391f43df95 | ||
|
|
60d8ab021b | ||
|
|
dfab8d68fd | ||
|
|
8167cb86c6 | ||
|
|
44bc689348 | ||
|
|
007db344d7 | ||
|
|
3fb2621030 | ||
|
|
39ca9a5f0d | ||
|
|
e64e8a95e0 | ||
|
|
69e888c7fe | ||
|
|
ce809c432d | ||
|
|
cdf900c6b7 | ||
|
|
d5b5b7751e | ||
|
|
351712172c | ||
|
|
f99aded40e | ||
|
|
f631e5fc1b | ||
|
|
26cee1551e | ||
|
|
82e2a40791 | ||
|
|
1c158930cd | ||
|
|
f812ea8716 | ||
|
|
3dc0a95379 | ||
|
|
f8244f582d | ||
|
|
69fbdfdc4a | ||
|
|
ef3272a31b | ||
|
|
b0f3ef2698 | ||
|
|
1711e7f7d9 | ||
|
|
ff6cdf3b6a | ||
|
|
28517a1999 | ||
|
|
af9c207ddf | ||
|
|
88045bcf4e | ||
|
|
5147710fad | ||
|
|
9c45cff09a | ||
|
|
b4848c0424 | ||
|
|
3a46b78361 | ||
|
|
75bbc6eb5f | ||
|
|
da638aea99 | ||
|
|
7c37b479db | ||
|
|
fa473c5ada | ||
|
|
1e6ce04e4f | ||
|
|
5cd5b119e7 | ||
|
|
186138cc3c | ||
|
|
98c7ca980a | ||
|
|
b2470b962a | ||
|
|
2a705bc6d1 | ||
|
|
5d44831757 | ||
|
|
456880501b | ||
|
|
63f7ae12e1 | ||
|
|
fa69ffefd2 | ||
|
|
f78bc82cc4 | ||
|
|
dfacf6a206 | ||
|
|
b2f14c1e2e | ||
|
|
03d7a311bf | ||
|
|
cd5119278b | ||
|
|
6ab3608a44 | ||
|
|
c0539af368 | ||
|
|
c6363140f4 | ||
|
|
187105b729 | ||
|
|
b983c0f11f | ||
|
|
c7cee33889 | ||
|
|
d9d73b8aec | ||
|
|
48455b9b72 | ||
|
|
af798c0bf3 | ||
|
|
ed714c9d65 | ||
|
|
832b401630 | ||
|
|
8814b1ce56 | ||
|
|
e9eedb2241 | ||
|
|
d3a35c307a |
No files matched your search
@@ -1,31 +1,30 @@
|
||||
name: Problem report
|
||||
description: A game or FramePort doesn't work (FramePort → "Report a problem…" fills this in and saves a diagnostics zip).
|
||||
title: "[Problem] "
|
||||
labels: ["bug"]
|
||||
name: Game report
|
||||
description: 'A game doesn''t run right on the Frame, or FramePort itself misbehaves (FramePort fills this in: "Report a problem…").'
|
||||
title: "[Game report] "
|
||||
labels: ["game-compatibility"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please attach the **diagnostics zip** FramePort saved (drag it into the "Diagnostics" box below). It contains
|
||||
logs, the recipe and device details with personal data removed — no game files. Without the app, it can be
|
||||
read with `frameport diag inspect <zip>`.
|
||||
Please attach the **diagnostics zip** FramePort saved: drag it into the "Diagnostics" box below. It holds logs
|
||||
and device details with personal data removed, and no game files.
|
||||
- type: input
|
||||
id: game
|
||||
attributes:
|
||||
label: Game
|
||||
description: Title — package id (version, engine / XR API); empty for app problems
|
||||
description: Title and package name; leave empty for problems with FramePort itself
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: What happens?
|
||||
description: What you did, what you expected, what you saw (in the headset, if it started)
|
||||
description: What you did, what you expected and what happened
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: findings
|
||||
attributes:
|
||||
label: Launch test / triage
|
||||
description: Filled in by FramePort from the last launch test
|
||||
label: Launch test
|
||||
description: Filled in by FramePort
|
||||
- type: textarea
|
||||
id: recipe
|
||||
attributes:
|
||||
@@ -39,6 +38,6 @@ body:
|
||||
id: logs
|
||||
attributes:
|
||||
label: Diagnostics
|
||||
description: Drag the FramePort-diag-*.zip here (max 25 MB)
|
||||
description: "Drag the FramePort-diag-*.zip here (max 25 MB). If the box won't take it, submit the report and attach the zip in a comment."
|
||||
validations:
|
||||
required: true
|
||||
@@ -1 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Questions and ideas
|
||||
url: https://github.com/spoopyghosty0/frameport/discussions
|
||||
about: Setup help, how-to questions and feature ideas go to Discussions.
|
||||
- name: Docs and FAQ
|
||||
url: https://frameport.app/docs/
|
||||
about: Install steps, game folder layouts and common questions.
|
||||
@@ -1,32 +1,32 @@
|
||||
name: Working configuration
|
||||
description: Submit a recipe that works on the Steam Frame (FramePort → game menu → "Share working config…" fills this in).
|
||||
title: "[Working config] "
|
||||
name: Working recipe
|
||||
description: 'Share a recipe that works on the Steam Frame (FramePort fills this in: game menu → "Share working recipe…").'
|
||||
title: "[Working recipe] "
|
||||
labels: ["working-config"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks! A maintainer reviews the recipe; once it's labelled `catalog-accepted`, a pull request adding it to
|
||||
`catalog/games/` is opened automatically. Please don't paste game files or links to them.
|
||||
Thanks! Once a maintainer accepts the recipe, it becomes built-in for everyone. Please don't share game files
|
||||
or links to them.
|
||||
- type: input
|
||||
id: game
|
||||
attributes:
|
||||
label: Game
|
||||
description: Title — package id (platform, version, engine / XR API)
|
||||
description: Title and package name (e.g. Lucky's Tale — com.playful.LuckysTale)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: result
|
||||
attributes:
|
||||
label: Result
|
||||
description: works or issues (+ the last headless launch test)
|
||||
description: works or issues
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: recipe
|
||||
attributes:
|
||||
label: Recipe
|
||||
description: The catalog entry (catalog/games/<package>.yaml). Don't edit the package line.
|
||||
description: Filled in by FramePort. Don't edit the package line.
|
||||
render: yaml
|
||||
validations:
|
||||
required: true
|
||||
@@ -34,16 +34,16 @@ body:
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: FramePort, tool and SteamOS versions
|
||||
description: FramePort and SteamOS versions
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Notes
|
||||
description: What you checked in the headset, known issues, anything unusual
|
||||
description: What you checked in the headset and any known issues
|
||||
- type: checkboxes
|
||||
id: confirm
|
||||
attributes:
|
||||
label: Confirmation
|
||||
options:
|
||||
- label: I played the game in the headset with this recipe (headless launch tests can't check the picture).
|
||||
- label: I played the game in the headset with this recipe.
|
||||
required: true
|
||||
@@ -150,6 +150,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write # starts the pages workflow
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: astral-sh/setup-uv@v10.2.0
|
||||
@@ -185,6 +186,11 @@ jobs:
|
||||
echo; cat packaging/release-footer.md; } > "$notes" # short: the update dialog shows these notes
|
||||
gh release delete "$tag" --yes 2>/dev/null || true
|
||||
gh release create "$tag" out/* packaging/FramePort-selfsigned.cer --title "FramePort ${tag}" --notes-file "$notes"
|
||||
- name: Refresh the website's changelog (a release made with github.token starts no other workflow)
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: gh workflow run pages.yml --ref main
|
||||
|
||||
dev-release:
|
||||
# "Run workflow" with dev ticked: replaces the rolling `dev` pre-release (never shown by the automatic update check;
|
||||
@@ -194,6 +200,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write # starts the pages workflow
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
@@ -225,3 +232,8 @@ jobs:
|
||||
gh release delete dev --yes --cleanup-tag 2>/dev/null || true
|
||||
gh release create dev out/* packaging/FramePort-selfsigned.cer --prerelease --target "$GITHUB_SHA" \
|
||||
--title "FramePort dev build ${VERSION}" --notes-file "$notes"
|
||||
- name: Refresh the website's changelog (a release made with github.token starts no other workflow)
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: gh workflow run pages.yml --ref main
|
||||
@@ -1,5 +1,5 @@
|
||||
name: catalog-from-issue
|
||||
# A maintainer adds the label `catalog-accepted` to a "Working configuration" issue → this opens a PR adding the
|
||||
# A maintainer adds the label `catalog-accepted` to a "Working recipe" issue → this opens a PR adding the
|
||||
# recipe to catalog/games/. Never runs for unlabelled issues; the issue text is only read by the validating script
|
||||
# (never interpolated into shell commands). Needs Settings → Actions → "Allow GitHub Actions to create pull requests".
|
||||
on:
|
||||
@@ -40,7 +40,8 @@ jobs:
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git checkout -b "$branch"
|
||||
git add "catalog/games/$PKG.yaml"
|
||||
uv run python scripts/patch_docs.py # docs/PATCHES.md lists the catalog games per patch
|
||||
git add "catalog/games/$PKG.yaml" docs/PATCHES.md site/src/data/patches.json
|
||||
git commit -m "catalog: $PKG (from #$ISSUE)"
|
||||
git push --force origin "$branch"
|
||||
if ! gh pr view "$branch" >/dev/null 2>&1; then
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# The project page (site/, Astro) on GitHub Pages: https://frameport.app/
|
||||
# Rebuilt when the site, the docs it renders, the catalog or the showcase renders change on main, when a release is
|
||||
# published, and once a day (contributors, releases and the games board stay fresh). The games board also refreshes
|
||||
# itself from the catalog on main in the visitor's browser, so a catalog change shows before the next build.
|
||||
# One-time setup: Settings → Pages → Source: GitHub Actions; custom domain frameport.app (kept in the settings, not a
|
||||
# CNAME file: GitHub ignores that file for Actions deployments).
|
||||
name: pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'site/**'
|
||||
- 'docs/*.md'
|
||||
- 'catalog/games/**'
|
||||
- 'bootstrap/setup.sh'
|
||||
- 'docs/images/**'
|
||||
- 'docs/media/**'
|
||||
- 'docs/badges/**'
|
||||
- 'src/frameport/ui/icons/**'
|
||||
- '.github/workflows/pages.yml'
|
||||
release: # the changelog page shows every release (CI's own releases start this through workflow_dispatch)
|
||||
types: [published]
|
||||
schedule:
|
||||
- cron: '17 5 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: site
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: site/package-lock.json
|
||||
- uses: actions/configure-pages@v6
|
||||
- run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends ffmpeg # the video tiles' short previews
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
- run: npm run build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # the contributors list (higher API limit)
|
||||
# analytics (Settings → Secrets and variables → Actions → Variables); unset = none on the site
|
||||
PUBLIC_CF_ANALYTICS_TOKEN: ${{ vars.CF_ANALYTICS_TOKEN }}
|
||||
PUBLIC_GA_ID: ${{ vars.GA_MEASUREMENT_ID }}
|
||||
- uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: site/dist
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- id: deployment
|
||||
uses: actions/deploy-pages@v5
|
||||
@@ -20,3 +20,13 @@ CLAUDE.local.md
|
||||
REPORT.md
|
||||
.ruff_cache/
|
||||
.claude/worktrees/
|
||||
|
||||
# project page (site/)
|
||||
site/node_modules/
|
||||
site/dist/
|
||||
site/.astro/
|
||||
site/public/media/
|
||||
site/src/assets/media/
|
||||
site/media-hq/
|
||||
site/public/s
|
||||
site/public/setup.sh
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
target: site
|
||||
total_score: 28
|
||||
max_score: 40
|
||||
na_heuristics:
|
||||
p0_count: 0
|
||||
p1_count: 3
|
||||
target_identity: "file:/mnt/d/Backup Data/Misc/emus/VR_META_QUEST/frameport/.claude/worktrees/impeccable-init/site/src/pages/index.astro"
|
||||
target_fingerprint: "sha256:d6703ab52ed142060a06d3e159634338b66605ed54aa67cd8a942cfcc3918f65"
|
||||
target_path: /mnt/d/Backup Data/Misc/emus/VR_META_QUEST/frameport/.claude/worktrees/impeccable-init/site/src/pages/index.astro
|
||||
timestamp: 2026-10-12T14-19-41Z
|
||||
slug: site-src-pages-index-astro
|
||||
closed: true
|
||||
---
|
||||
# Critique: frameport.app (site/src: home, download, install, setup, docs, changelog)
|
||||
Method: dual-agent (A: design review · B: detector + overlay)
|
||||
|
||||
## Design Health Score: 28/40 (Good)
|
||||
| # | Heuristic | Score | Key Issue |
|
||||
|---|---|---|---|
|
||||
| 1 | Visibility of System Status | 3 | TransitLog rows dimmed to 1.8:1 until reached; read as disabled on a quick scroll |
|
||||
| 2 | Match System / Real World | 3 | Home leads with VrApi, OVRPort patch ids, AXRB, a launch-test release card |
|
||||
| 3 | User Control and Freedom | 3 | /download/ starts a download on arrival |
|
||||
| 4 | Consistency and Standards | 2 | Nav "Install" -> /#install, /install/ is the link handler; "Install" vs "Setup"; three step-number styles |
|
||||
| 5 | Error Prevention | 3 | Phone detection, checksums, 4-digit Allow |
|
||||
| 6 | Recognition Rather Than Recall | 3 | No nav at all under 760px |
|
||||
| 7 | Flexibility and Efficiency | 3 | Docs search "/", page keys, ?os= links |
|
||||
| 8 | Aesthetic and Minimalist Design | 2 | 10.8k px home (13.7k mobile), app shown 3x in section 02, effect stack |
|
||||
| 9 | Error Recovery | 3 | Install-link error is plain with a next step |
|
||||
| 10 | Help and Documentation | 3 | Good docs; hub is a uniform card grid, "93 min read" |
|
||||
|
||||
## Design Specificity Verdict
|
||||
Mixed. Content and hero are authored (real package -> game crossing the portal, TransitLog with real recipe chips, live CompatBoard incl. "Doesn't run", /setup Konsole preview). The ambient effect layer (interactions.ts: cursorLight, clickRipples, word-rise headings + pointer-lit gradient letters, scrambling labels, countUp, global .reveal fade-ups, heroScroll) and the section scaffolding (numbered "01 ·" mono eyebrows on 8 sections, statement + dim kicker headlines, Compare bento with icon tiles, uniform docs card hub, avatar wall, glows + warp field on /download/) are the template tells. Detector agrees: kicker-above-heading x9, gradient-text x2, icon-tile-stack, radial-spotlight-glow, dark-glow, bounce-easing, marquee; plus real a11y: --text-3 #717583 below 4.5:1 for small text (~18 hits), docs search placeholders 3.6-3.9:1, 10.5px text in Compare/.pip, heading-rhythm on 4 section heads, 114-char notice line. False positives: AppMock Roboto/nested cards/grid bg (imitates the app), Carousel lightbox img, purple palette (per-game hues), Crossing svg occlusion.
|
||||
|
||||
## Priority Issues
|
||||
- [P1] Ambient effect stack reads as generated. Fix: keep motion only where something crosses (hero, TransitLog, setup Konsole, walkthrough); delete cursorLight, clickRipples, heading word-split + .lit, label scramble; limit .reveal. Command: /impeccable quieter
|
||||
- [P1] No navigation on phones (Nav.astro:45-46 nav display:none under 760px, no menu). Fix: compact menu; phone CTA = send/copy link to PC. Command: /impeccable adapt
|
||||
- [P1] Home too long and repetitive. Fix: keep the mock, fold carousel+videos, move "For game makers" to docs, release list to /changelog, shorten Compare. Command: /impeccable distill + /impeccable layout
|
||||
- [P2] Walkthrough Downloads mock shows "?os=linux" + always ZIP/FramePort.exe (InstallWalkthrough.astro:272). Fix: map OS -> real asset names. Command: /impeccable polish
|
||||
- [P2] No self-signed/SmartScreen reassurance at the download; "Install" overloaded; --text-3 contrast. Fix: one plain line per OS next to the SHA; rename nav item; raise --text-3 for small text. Command: /impeccable clarify + /impeccable onboard
|
||||
|
||||
## Persona Red Flags
|
||||
Jordan: pipeline jargon right below "one click"; Install vs Setup; truncated walkthrough tabs. Riley: /install/ direct = error page; ?os=linux; 93 min read. Casey: no nav; three.js hero + warp canvas on a phone; 13.7k px. Reddit Frame owner: no "send to my PC" from home; curl|bash and Developer Mode first appear in section 05 without /setup's reassurance.
|
||||
|
||||
## Minor Observations
|
||||
Hero meta line wraps; Compare tinted gradient cards break flat panels; green glow in Compare outside the palette; changelog version pills scroll without affordance; blockquote side border; --ease-back overshoot; marquee reduced-motion unverified.
|
||||
|
||||
## Questions to Consider
|
||||
- If the crossing is the brand's one motion, why does every heading animate too?
|
||||
- Who is the home page for after the hero: owner or tinkerer? Could the pipeline be "How it works ->"?
|
||||
- Would sections named by what the visitor does beat numbered eyebrows?
|
||||
@@ -0,0 +1,578 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"generatedAt": "2026-10-12T15:37:55+00:00",
|
||||
"title": "Design System: FramePort",
|
||||
"extensions": {
|
||||
"colorMeta": {
|
||||
"frame-orange": {
|
||||
"role": "primary",
|
||||
"displayName": "Frame Orange",
|
||||
"canonical": "#FF8A1F",
|
||||
"oklch": "oklch(74.8% 0.176 55.4)",
|
||||
"tonalRamp": [
|
||||
"#170600",
|
||||
"#3B1A00",
|
||||
"#633002",
|
||||
"#8E4801",
|
||||
"#C16300",
|
||||
"#F58108",
|
||||
"#FEB98A",
|
||||
"#FEEADD"
|
||||
]
|
||||
},
|
||||
"frame-orange-soft": {
|
||||
"role": "primary",
|
||||
"displayName": "Frame Orange Soft",
|
||||
"canonical": "#3A2512",
|
||||
"oklch": "oklch(28.6% 0.044 61.7)",
|
||||
"tonalRamp": [
|
||||
"#150700",
|
||||
"#331F0C",
|
||||
"#513A27",
|
||||
"#705844",
|
||||
"#947B65",
|
||||
"#BA9F89",
|
||||
"#E0C5AE",
|
||||
"#FEEBDB"
|
||||
]
|
||||
},
|
||||
"ember-ink": {
|
||||
"role": "primary",
|
||||
"displayName": "Ember Ink",
|
||||
"canonical": "#160C03",
|
||||
"oklch": "oklch(16.5% 0.027 67.8)",
|
||||
"tonalRamp": [
|
||||
"#130902",
|
||||
"#2D2216",
|
||||
"#493D31",
|
||||
"#685B4E",
|
||||
"#8B7E70",
|
||||
"#B0A293",
|
||||
"#D7C8B8",
|
||||
"#FBECDC"
|
||||
]
|
||||
},
|
||||
"pc-blue": {
|
||||
"role": "secondary",
|
||||
"displayName": "Pc Blue",
|
||||
"canonical": "#3AA8FF",
|
||||
"oklch": "oklch(71.0% 0.160 246.9)",
|
||||
"tonalRamp": [
|
||||
"#000C1B",
|
||||
"#002643",
|
||||
"#01426F",
|
||||
"#04619F",
|
||||
"#0485D7",
|
||||
"#46ABFE",
|
||||
"#9FD0FE",
|
||||
"#E2F1FE"
|
||||
]
|
||||
},
|
||||
"pc-blue-soft": {
|
||||
"role": "secondary",
|
||||
"displayName": "Pc Blue Soft",
|
||||
"canonical": "#0F2A40",
|
||||
"oklch": "oklch(27.6% 0.052 245.9)",
|
||||
"tonalRamp": [
|
||||
"#000C1A",
|
||||
"#0B263C",
|
||||
"#27425A",
|
||||
"#45617A",
|
||||
"#66849E",
|
||||
"#8AA9C4",
|
||||
"#AFCFEB",
|
||||
"#E2F1FE"
|
||||
]
|
||||
},
|
||||
"void": {
|
||||
"role": "neutral",
|
||||
"displayName": "Void",
|
||||
"canonical": "#0D0E12",
|
||||
"oklch": "oklch(16.5% 0.009 274.3)",
|
||||
"tonalRamp": [
|
||||
"#0A0B0F",
|
||||
"#232428",
|
||||
"#3E3F44",
|
||||
"#5C5D63",
|
||||
"#7E8085",
|
||||
"#A3A4AA",
|
||||
"#C9CAD0",
|
||||
"#ECEEF4"
|
||||
]
|
||||
},
|
||||
"sidebar-night": {
|
||||
"role": "neutral",
|
||||
"displayName": "Sidebar Night",
|
||||
"canonical": "#111217",
|
||||
"oklch": "oklch(18.3% 0.010 276.4)",
|
||||
"tonalRamp": [
|
||||
"#0A0B10",
|
||||
"#232429",
|
||||
"#3E3F45",
|
||||
"#5C5D64",
|
||||
"#7E8087",
|
||||
"#A3A4AB",
|
||||
"#C8CAD1",
|
||||
"#ECEEF6"
|
||||
]
|
||||
},
|
||||
"panel": {
|
||||
"role": "neutral",
|
||||
"displayName": "Panel",
|
||||
"canonical": "#16181E",
|
||||
"oklch": "oklch(21.0% 0.012 270.8)",
|
||||
"tonalRamp": [
|
||||
"#090B10",
|
||||
"#22242A",
|
||||
"#3D4046",
|
||||
"#5B5D65",
|
||||
"#7D8088",
|
||||
"#A1A4AD",
|
||||
"#C7CAD3",
|
||||
"#EBEEF7"
|
||||
]
|
||||
},
|
||||
"panel-raised": {
|
||||
"role": "neutral",
|
||||
"displayName": "Panel Raised",
|
||||
"canonical": "#1D1F27",
|
||||
"oklch": "oklch(24.1% 0.016 274.1)",
|
||||
"tonalRamp": [
|
||||
"#090B12",
|
||||
"#22242C",
|
||||
"#3D3F48",
|
||||
"#5B5D67",
|
||||
"#7D808A",
|
||||
"#A1A4AF",
|
||||
"#C7CAD5",
|
||||
"#EBEEF9"
|
||||
]
|
||||
},
|
||||
"panel-well": {
|
||||
"role": "neutral",
|
||||
"displayName": "Panel Well",
|
||||
"canonical": "#252832",
|
||||
"oklch": "oklch(27.8% 0.019 272.0)",
|
||||
"tonalRamp": [
|
||||
"#080B13",
|
||||
"#21242D",
|
||||
"#3C3F4A",
|
||||
"#5A5D69",
|
||||
"#7C808C",
|
||||
"#A0A4B1",
|
||||
"#C6CAD7",
|
||||
"#EAEEFC"
|
||||
]
|
||||
},
|
||||
"hairline": {
|
||||
"role": "neutral",
|
||||
"displayName": "Hairline",
|
||||
"canonical": "#2A2D37",
|
||||
"oklch": "oklch(29.8% 0.019 272.1)",
|
||||
"tonalRamp": [
|
||||
"#080B13",
|
||||
"#21242D",
|
||||
"#3C3F4A",
|
||||
"#5A5D68",
|
||||
"#7C808C",
|
||||
"#A0A4B1",
|
||||
"#C6CAD7",
|
||||
"#EAEEFB"
|
||||
]
|
||||
},
|
||||
"hairline-strong": {
|
||||
"role": "neutral",
|
||||
"displayName": "Hairline Strong",
|
||||
"canonical": "#393D49",
|
||||
"oklch": "oklch(36.1% 0.021 270.8)",
|
||||
"tonalRamp": [
|
||||
"#080B14",
|
||||
"#20242F",
|
||||
"#3B3F4B",
|
||||
"#595D6A",
|
||||
"#7B808D",
|
||||
"#9FA4B3",
|
||||
"#C5CAD9",
|
||||
"#E9EEFD"
|
||||
]
|
||||
},
|
||||
"text-bright": {
|
||||
"role": "neutral",
|
||||
"displayName": "Text Bright",
|
||||
"canonical": "#EDEEF2",
|
||||
"oklch": "oklch(94.9% 0.005 275.0)",
|
||||
"tonalRamp": [
|
||||
"#0A0B0D",
|
||||
"#232427",
|
||||
"#3F4043",
|
||||
"#5D5D61",
|
||||
"#7F8083",
|
||||
"#A3A4A8",
|
||||
"#C9CACE",
|
||||
"#EDEEF2"
|
||||
]
|
||||
},
|
||||
"text-dim": {
|
||||
"role": "neutral",
|
||||
"displayName": "Text Dim",
|
||||
"canonical": "#A7ABB7",
|
||||
"oklch": "oklch(74.2% 0.018 271.2)",
|
||||
"tonalRamp": [
|
||||
"#080B13",
|
||||
"#21242D",
|
||||
"#3C3F49",
|
||||
"#5A5D68",
|
||||
"#7C808B",
|
||||
"#A0A4B0",
|
||||
"#C6CAD7",
|
||||
"#EAEEFB"
|
||||
]
|
||||
},
|
||||
"text-faint": {
|
||||
"role": "neutral",
|
||||
"displayName": "Text Faint",
|
||||
"canonical": "#717583",
|
||||
"oklch": "oklch(56.4% 0.022 273.0)",
|
||||
"tonalRamp": [
|
||||
"#080A14",
|
||||
"#20232F",
|
||||
"#3C3F4C",
|
||||
"#595D6A",
|
||||
"#7C808E",
|
||||
"#A0A4B3",
|
||||
"#C5CAD9",
|
||||
"#E9EEFE"
|
||||
],
|
||||
"siteValue": "#868a96"
|
||||
},
|
||||
"ok-green": {
|
||||
"role": "status",
|
||||
"displayName": "Ok Green",
|
||||
"canonical": "#3DD68C",
|
||||
"oklch": "oklch(77.9% 0.165 157.3)",
|
||||
"tonalRamp": [
|
||||
"#000F06",
|
||||
"#002D18",
|
||||
"#004D2D",
|
||||
"#017043",
|
||||
"#03995D",
|
||||
"#1AC27A",
|
||||
"#57EA9F",
|
||||
"#C4FFDA"
|
||||
]
|
||||
},
|
||||
"warn-amber": {
|
||||
"role": "status",
|
||||
"displayName": "Warn Amber",
|
||||
"canonical": "#F2C94C",
|
||||
"oklch": "oklch(85.0% 0.146 90.5)",
|
||||
"tonalRamp": [
|
||||
"#100A00",
|
||||
"#2E2301",
|
||||
"#4F3D02",
|
||||
"#725B05",
|
||||
"#9C7C01",
|
||||
"#C79F05",
|
||||
"#EFC648",
|
||||
"#FFEEBE"
|
||||
]
|
||||
},
|
||||
"error-coral": {
|
||||
"role": "status",
|
||||
"displayName": "Error Coral",
|
||||
"canonical": "#FF6166",
|
||||
"oklch": "oklch(69.8% 0.193 21.8)",
|
||||
"tonalRamp": [
|
||||
"#1F0002",
|
||||
"#4B000A",
|
||||
"#7C0118",
|
||||
"#B10227",
|
||||
"#DB3F49",
|
||||
"#FE7173",
|
||||
"#FEB5B1",
|
||||
"#FFE8E7"
|
||||
]
|
||||
},
|
||||
"info-cyan": {
|
||||
"role": "status",
|
||||
"displayName": "Info Cyan",
|
||||
"canonical": "#5CD0E6",
|
||||
"oklch": "oklch(79.9% 0.108 212.0)",
|
||||
"tonalRamp": [
|
||||
"#000E12",
|
||||
"#012930",
|
||||
"#014853",
|
||||
"#066978",
|
||||
"#0290A4",
|
||||
"#3EB6CC",
|
||||
"#6ADDF4",
|
||||
"#D1F6FE"
|
||||
]
|
||||
}
|
||||
},
|
||||
"typographyMeta": {
|
||||
"display": {
|
||||
"displayName": "Display",
|
||||
"purpose": "Site section headings and doc titles (Sora 800)."
|
||||
},
|
||||
"headline": {
|
||||
"displayName": "Headline",
|
||||
"purpose": "App page titles and the game hero title."
|
||||
},
|
||||
"title": {
|
||||
"displayName": "Title",
|
||||
"purpose": "App section and card headings."
|
||||
},
|
||||
"body": {
|
||||
"displayName": "Body",
|
||||
"purpose": "Running text (13px app, 16px site)."
|
||||
},
|
||||
"label": {
|
||||
"displayName": "Label",
|
||||
"purpose": "Pills, meta lines, counts."
|
||||
},
|
||||
"section-label": {
|
||||
"displayName": "Section label",
|
||||
"purpose": "Mono uppercase labels followed by the portal hairline."
|
||||
}
|
||||
},
|
||||
"shadows": [
|
||||
{
|
||||
"name": "art-rest",
|
||||
"value": "0 8px 22px 1px rgba(0,0,0,.55)",
|
||||
"purpose": "Cover and shelf cards, screenshots."
|
||||
},
|
||||
{
|
||||
"name": "art-hover",
|
||||
"value": "0 16px 36px 2px rgba(0,0,0,.75), 0 4px 30px -2px rgba(255,138,31,.22)",
|
||||
"purpose": "A pointed-at cover: deeper, with the orange glow."
|
||||
},
|
||||
{
|
||||
"name": "float",
|
||||
"value": "0 18px 40px rgba(0,0,0,.55)",
|
||||
"purpose": "Menus, popovers, term cards, toasts."
|
||||
},
|
||||
{
|
||||
"name": "portal-glow",
|
||||
"value": "radial-gradient(85% 85% at 50% 37%, rgba(58,168,255,.10) 0%, rgba(255,138,31,.05) 45%, rgba(13,14,18,0) 100%)",
|
||||
"purpose": "Soft light behind empty states and the site hero."
|
||||
}
|
||||
],
|
||||
"gradients": [
|
||||
{
|
||||
"name": "portal",
|
||||
"value": "linear-gradient(90deg, #3AA8FF, #FF8A1F)",
|
||||
"purpose": "The seam: sidebar edge, selection fades (22 %), section hairlines, progress, link underlines."
|
||||
}
|
||||
],
|
||||
"motion": [
|
||||
{
|
||||
"name": "ease-out",
|
||||
"value": "cubic-bezier(0.2, 0.8, 0.2, 1)",
|
||||
"purpose": "Default for state changes, reveals and the cover lift."
|
||||
},
|
||||
{
|
||||
"name": "ease-settle",
|
||||
"value": "cubic-bezier(0.25, 1, 0.5, 1)",
|
||||
"purpose": "Presses, pops and arrivals: they land without overshoot."
|
||||
},
|
||||
{
|
||||
"name": "view-fade",
|
||||
"value": "150ms",
|
||||
"purpose": "App route change (AnimatedSwitcher)."
|
||||
},
|
||||
{
|
||||
"name": "quiet-reveal",
|
||||
"value": "120ms opacity",
|
||||
"purpose": "Row actions that appear on hover/focus."
|
||||
},
|
||||
{
|
||||
"name": "cover-lift",
|
||||
"value": "220ms ease-out; art zoom 600ms",
|
||||
"purpose": "Cover card hover."
|
||||
},
|
||||
{
|
||||
"name": "reduce-motion",
|
||||
"value": "all of the above off",
|
||||
"purpose": "ui.reduce_motion / prefers-reduced-motion."
|
||||
}
|
||||
],
|
||||
"breakpoints": [
|
||||
{
|
||||
"name": "site-narrow",
|
||||
"value": "760px",
|
||||
"purpose": "Docs diagrams and file trees switch to scroll / stacked layout."
|
||||
},
|
||||
{
|
||||
"name": "site-page",
|
||||
"value": "1180px",
|
||||
"purpose": "Site content column max width."
|
||||
}
|
||||
],
|
||||
"themes": [
|
||||
{
|
||||
"name": "portal",
|
||||
"dual": true,
|
||||
"note": "Default."
|
||||
},
|
||||
{
|
||||
"name": "portal_oled",
|
||||
"dual": true,
|
||||
"note": "True black BG/sidebar; panels lift off it."
|
||||
},
|
||||
{
|
||||
"name": "original",
|
||||
"dual": false,
|
||||
"note": "Violet accent #8B7CFF; no gradient touches."
|
||||
}
|
||||
]
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"name": "Primary Button",
|
||||
"kind": "button",
|
||||
"refersTo": "button-primary",
|
||||
"description": "The one main action of a view (Install on Frame, Play on Frame).",
|
||||
"html": "<button class=\"ds-btn-primary\"><svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M8 5v14l11-7z\"/></svg>Play on Frame</button>",
|
||||
"css": ".ds-btn-primary{display:inline-flex;align-items:center;gap:8px;padding:12px 16px;border:0;border-radius:8px;background:var(--accent,#FF8A1F);color:var(--on-accent,#160C03);font:600 13px/18px 'Segoe UI',Roboto,sans-serif;cursor:pointer;transition:filter 220ms cubic-bezier(.2,.8,.2,1),transform 320ms cubic-bezier(.25,1,.5,1)}.ds-btn-primary:hover{filter:brightness(1.08)}.ds-btn-primary:active{transform:scale(.97)}.ds-btn-primary:focus-visible{outline:2px solid var(--accent,#FF8A1F);outline-offset:3px}"
|
||||
},
|
||||
{
|
||||
"name": "Secondary Button",
|
||||
"kind": "button",
|
||||
"refersTo": "button-secondary",
|
||||
"description": "Secondary actions: the blue portal outline next to the orange primary.",
|
||||
"html": "<button class=\"ds-btn-secondary\"><svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M3 12a9 9 0 0 1 15-6.7L21 8M21 3v5h-5M21 12a9 9 0 0 1-15 6.7L3 16M3 21v-5h5\"/></svg>Reinstall on Frame</button>",
|
||||
"css": ".ds-btn-secondary{display:inline-flex;align-items:center;gap:8px;padding:12px 14px;border:1px solid rgba(58,168,255,.55);border-radius:8px;background:transparent;color:var(--text,#EDEEF2);font:600 13px/18px 'Segoe UI',Roboto,sans-serif;cursor:pointer;transition:background-color 220ms cubic-bezier(.2,.8,.2,1),transform 320ms cubic-bezier(.25,1,.5,1)}.ds-btn-secondary svg{color:var(--secondary,#3AA8FF)}.ds-btn-secondary:hover{background:rgba(58,168,255,.08)}.ds-btn-secondary:active{transform:scale(.97)}.ds-btn-secondary:focus-visible{outline:2px solid var(--accent,#FF8A1F);outline-offset:3px}"
|
||||
},
|
||||
{
|
||||
"name": "Ghost Button",
|
||||
"kind": "button",
|
||||
"refersTo": "button-ghost",
|
||||
"description": "Quiet actions: Close, Cancel, inline links.",
|
||||
"html": "<button class=\"ds-btn-ghost\">Cancel</button>",
|
||||
"css": ".ds-btn-ghost{padding:10px 12px;border:0;border-radius:8px;background:transparent;color:var(--text-2,#A7ABB7);font:600 13px/18px 'Segoe UI',Roboto,sans-serif;cursor:pointer;transition:color 200ms,background-color 200ms}.ds-btn-ghost:hover{color:var(--text,#EDEEF2);background:rgba(237,238,242,.06)}.ds-btn-ghost:focus-visible{outline:2px solid var(--accent,#FF8A1F);outline-offset:3px}"
|
||||
},
|
||||
{
|
||||
"name": "Danger Button",
|
||||
"kind": "button",
|
||||
"refersTo": "button-danger",
|
||||
"description": "The final destructive button of a confirm dialog (Uninstall).",
|
||||
"html": "<button class=\"ds-btn-danger\">Uninstall</button>",
|
||||
"css": ".ds-btn-danger{padding:12px 16px;border:0;border-radius:8px;background:var(--error,#FF6166);color:var(--on-accent,#160C03);font:600 13px/18px 'Segoe UI',Roboto,sans-serif;cursor:pointer;transition:filter 220ms,transform 320ms cubic-bezier(.25,1,.5,1)}.ds-btn-danger:hover{filter:brightness(1.08)}.ds-btn-danger:active{transform:scale(.97)}.ds-btn-danger:focus-visible{outline:2px solid var(--error,#FF6166);outline-offset:3px}"
|
||||
},
|
||||
{
|
||||
"name": "Search Field",
|
||||
"kind": "input",
|
||||
"refersTo": "input",
|
||||
"description": "Filled well, no border until focus, then orange.",
|
||||
"html": "<label class=\"ds-search\"><svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"/><path d=\"m20 20-4-4\"/></svg><input placeholder=\"Type to search games and tags\"></label>",
|
||||
"css": ".ds-search{display:flex;align-items:center;gap:10px;width:260px;padding:8px 12px;border:1px solid transparent;border-radius:8px;background:var(--surface-3,#252832);color:var(--text-3,#717583);transition:border-color 160ms}.ds-search:focus-within{border-color:var(--accent,#FF8A1F)}.ds-search input{flex:1;min-width:0;border:0;outline:0;background:transparent;color:var(--text,#EDEEF2);font:13px 'Segoe UI',Roboto,sans-serif;caret-color:var(--accent,#FF8A1F)}.ds-search input::placeholder{color:var(--text-2,#A7ABB7)}"
|
||||
},
|
||||
{
|
||||
"name": "Status Pills",
|
||||
"kind": "chip",
|
||||
"refersTo": "pill",
|
||||
"description": "Color plus a word: status, install state, platform.",
|
||||
"html": "<span class=\"ds-pill ds-ok\">Works</span> <span class=\"ds-pill ds-warn\">Works with issues</span> <span class=\"ds-pill ds-solid\">On Frame</span> <span class=\"ds-pill ds-pc\">PC VR</span>",
|
||||
"css": ".ds-pill{display:inline-flex;align-items:center;gap:6px;padding:3px 9px 3px 8px;border-radius:20px;font:600 11px/16px 'Segoe UI',Roboto,sans-serif;white-space:nowrap}.ds-pill::before{content:'';width:7px;height:7px;border-radius:50%;background:currentColor}.ds-ok{color:#3DD68C;background:rgba(61,214,140,.12)}.ds-warn{color:#F2C94C;background:rgba(242,201,76,.12)}.ds-pc{color:#3AA8FF;background:rgba(58,168,255,.12)}.ds-solid{color:#160C03;background:#3DD68C}"
|
||||
},
|
||||
{
|
||||
"name": "Card",
|
||||
"kind": "card",
|
||||
"refersTo": "card",
|
||||
"description": "Flat panel with a hairline: no shadow.",
|
||||
"html": "<div class=\"ds-card\"><div class=\"ds-card-h\">Where it's installed</div><div class=\"ds-card-b\">Steam Frame · Installed</div><div class=\"ds-card-m\">Last launch test: passed</div></div>",
|
||||
"css": ".ds-card{width:320px;padding:16px;border:1px solid var(--border,#2A2D37);border-radius:12px;background:var(--surface,#16181E);font-family:'Segoe UI',Roboto,sans-serif;transition:background-color 160ms}.ds-card:hover{background:var(--surface-2,#1D1F27)}.ds-card-h{font:600 16px/1.3 inherit;color:var(--text,#EDEEF2);margin-bottom:8px}.ds-card-b{font-size:13px;color:#3DD68C}.ds-card-m{font-size:12px;color:var(--text-2,#A7ABB7);margin-top:4px}"
|
||||
},
|
||||
{
|
||||
"name": "Sidebar Navigation",
|
||||
"kind": "nav",
|
||||
"refersTo": "nav-item-selected",
|
||||
"description": "Selected row = the blue→orange portal fade; the sidebar ends in the portal seam.",
|
||||
"html": "<nav class=\"ds-nav\"><a class=\"on\" href=\"#\">Library</a><a href=\"#\">Steam Frame <i></i></a><a href=\"#\">Files</a><a href=\"#\">Monitor</a></nav>",
|
||||
"css": ".ds-nav{display:grid;gap:2px;width:220px;padding:12px;background:var(--sidebar,#111217);border-right:1px solid;border-image:linear-gradient(180deg,#3AA8FF,#FF8A1F) 1;font:500 14px/1 'Segoe UI',Roboto,sans-serif}.ds-nav a{display:flex;align-items:center;justify-content:space-between;padding:12px;border-radius:8px;color:var(--text-2,#A7ABB7);text-decoration:none;transition:background-color 160ms,color 160ms}.ds-nav a:hover{background:rgba(237,238,242,.05);color:var(--text,#EDEEF2)}.ds-nav a.on{background:linear-gradient(90deg,rgba(58,168,255,.22),rgba(255,138,31,.22));color:var(--text,#EDEEF2);font-weight:600}.ds-nav a:focus-visible{outline:2px solid var(--accent,#FF8A1F);outline-offset:2px}.ds-nav i{width:8px;height:8px;border-radius:50%;background:#3DD68C}"
|
||||
},
|
||||
{
|
||||
"name": "Callout",
|
||||
"kind": "custom",
|
||||
"refersTo": "callout",
|
||||
"description": "Guidance in a status color: 8 % fill, 35 % border.",
|
||||
"html": "<div class=\"ds-callout\"><svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#5CD0E6\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 11v5M12 8h.01\"/></svg><span>Large data set (~27 GiB) incl. all language packs.</span></div>",
|
||||
"css": ".ds-callout{display:flex;gap:12px;align-items:flex-start;max-width:520px;padding:12px 14px;border:1px solid rgba(92,208,230,.35);border-radius:8px;background:rgba(92,208,230,.08);color:var(--text,#EDEEF2);font:13px/1.5 'Segoe UI',Roboto,sans-serif}.ds-callout svg{flex:none;margin-top:1px}"
|
||||
},
|
||||
{
|
||||
"name": "Cover Card",
|
||||
"kind": "custom",
|
||||
"refersTo": "card",
|
||||
"description": "Signature: art-led card that lifts, zooms its art and glows orange underneath on hover.",
|
||||
"html": "<div class=\"ds-cover\" tabindex=\"0\"><div class=\"ds-art\"></div><span class=\"ds-tag\">Quest</span><div class=\"ds-cap\"><b>4XVR Video Player</b><span>Works</span></div></div>",
|
||||
"css": ".ds-cover{position:relative;width:180px;height:290px;border-radius:12px;overflow:hidden;border:1px solid var(--border,#2A2D37);box-shadow:0 8px 22px 1px rgba(0,0,0,.55);transition:transform 220ms cubic-bezier(.2,.8,.2,1),box-shadow 220ms;font-family:'Segoe UI',Roboto,sans-serif}.ds-art{position:absolute;inset:0;background:radial-gradient(60% 40% at 40% 45%,#3AA8FF 0 40%,transparent 41%),radial-gradient(55% 38% at 62% 47%,#FF8A1F 0 40%,transparent 41%),linear-gradient(160deg,#1a2c5a,#0d0e12);transition:transform 600ms cubic-bezier(.2,.8,.2,1)}.ds-cover:hover,.ds-cover:focus-visible{transform:translateY(-4px) scale(1.02);box-shadow:0 16px 36px 2px rgba(0,0,0,.75),0 4px 30px -2px rgba(255,138,31,.22);outline:none}.ds-cover:hover .ds-art{transform:scale(1.07)}.ds-tag{position:absolute;left:10px;top:10px;padding:3px 9px;border-radius:20px;background:rgba(0,0,0,.62);color:#EDEEF2;font:600 11px/16px inherit}.ds-cap{position:absolute;inset:auto 0 0 0;padding:40px 12px 12px;background:linear-gradient(transparent,rgba(13,14,18,.92));display:grid;gap:4px}.ds-cap b{color:#EDEEF2;font-size:15px}.ds-cap span{color:#A7ABB7;font-size:12px}@media (prefers-reduced-motion:reduce){.ds-cover,.ds-art{transition:box-shadow 220ms}.ds-cover:hover{transform:none}.ds-cover:hover .ds-art{transform:none}}"
|
||||
}
|
||||
],
|
||||
"narrative": {
|
||||
"northStar": "The Portal Between",
|
||||
"overview": "FramePort is about crossing. On one side is the PC, in blue; on the other is the Frame, in orange. Between them is the\nseam where a game passes through: a horizontal gradient from blue to orange, the same one the logo's tilted portal\nmakes. Everything else is dark space, so the crossing is what glows. The desktop app and frameport.app share this\nsystem with the same color values. The site states it outright (\"FramePort Portal, for the web\").\n\nSurfaces are flat, dark, tonal steps a few lightness points apart, with hairline borders, and the chrome stays quiet.\nColor comes from three places only: cover art, the orange that marks the one thing to do now, and the blue portal\ntouches that show where you are or what's moving. Density is comfortable desktop density, built for a mouse and a\nwindow you resize. The interface scales as a whole through one scale factor, never by reflowing into a phone layout.\n\nComponents are tactile and confident. A cover lifts and its art zooms when pointed at. A button settles when pressed.\nOn the website, motion is kept for things that cross: the hero's package-to-game crossing, the install log turning\nfrom PC rows into Frame rows, the download going through the portal. Nothing moves just because the pointer or the\nscroll passed by (no cursor light, no word-by-word headings, no scroll fade-ups).\nPlay sends two rings out of the button, blue then orange. A new view fades in. Every one of these motions disappears\nunder Reduce motion, and the static state must still read on its own.",
|
||||
"keyCharacteristics": [
|
||||
"Dark only: a near-black void (`void`) with three tonal panel steps above it.",
|
||||
"Orange is the action, blue is the place: the portal dichotomy is the color doctrine.",
|
||||
"The blue→orange gradient is the system's only decoration.",
|
||||
"Artwork carries the color; the chrome stays neutral.",
|
||||
"Flat at rest. Artwork and floating layers lift.",
|
||||
"Motion is physical (lift, spring, ripple) and always optional."
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"name": "The Portal Rule",
|
||||
"body": "Orange is the Frame and the action; blue is the PC and the secondary. Don't swap them, and don't use a third hue for an action.",
|
||||
"section": "colors"
|
||||
},
|
||||
{
|
||||
"name": "The One Orange Rule",
|
||||
"body": "A view has one filled orange button: the thing to do now. Other actions are blue outlines or ghosts.",
|
||||
"section": "colors"
|
||||
},
|
||||
{
|
||||
"name": "The Seam Rule",
|
||||
"body": "The blue→orange gradient (`--portal`, `C.portal_gradient`) is the only decoration. It always runs left to right (or top to bottom), blue first. It appears on the sidebar edge, the selected nav item and segment, the wordmark, section-label hairlines, link underlines on the site and progress fills. Dual themes only.",
|
||||
"section": "colors"
|
||||
},
|
||||
{
|
||||
"name": "The Dark-Only Rule",
|
||||
"body": "No light mode. Installed theme files whose background is too light, or whose text falls below 4.5:1 on a panel, are refused.",
|
||||
"section": "colors"
|
||||
},
|
||||
{
|
||||
"name": "The Scale-Token Rule",
|
||||
"body": "In the app every size goes through `T.T_*` tokens or `T.px()`, so the user's UI scale (75–300 %) applies everywhere. A bare pixel number is a bug.",
|
||||
"section": "typography"
|
||||
},
|
||||
{
|
||||
"name": "The Sentence-Case Rule",
|
||||
"body": "Headings and buttons use sentence case. Only the mono section labels and pill-sized overlays are uppercase.",
|
||||
"section": "typography"
|
||||
},
|
||||
{
|
||||
"name": "The Lift-Is-Earned Rule",
|
||||
"body": "A panel never gets a shadow just to look important. If it isn't artwork and it doesn't float over other content, it's flat with a hairline.",
|
||||
"section": "elevation"
|
||||
}
|
||||
],
|
||||
"dos": [
|
||||
"Do read colors as theme tokens when building (`T.ACCENT`, `var(--accent)`), so all three built-in themes and user theme files work.",
|
||||
"Do keep exactly one filled orange action per view; make everything else blue outline or ghost.",
|
||||
"Do put the blue→orange gradient only where something crosses or is selected: seams, selection fades, progress, link underlines.",
|
||||
"Do let cover art carry the color and keep surrounding chrome neutral.",
|
||||
"Do pair every status color with a word or glyph (pills, callouts, checklists).",
|
||||
"Do give motion a static fallback: under Reduce motion (`ui.reduce_motion`, `prefers-reduced-motion`) the state must still read without the animation.",
|
||||
"Do scale every app size through `T.T_*` / `T.px()` and give icon-only controls a tooltip."
|
||||
],
|
||||
"donts": [
|
||||
"Don't build a light theme or a light surface; FramePort is dark only.",
|
||||
"Don't swap the poles: no orange secondary buttons, no blue primary actions.",
|
||||
"Don't add shadows to ordinary panels or cards; only artwork and floating layers lift.",
|
||||
"Don't use Warn Amber or another orange-adjacent hue for a warning that could be mistaken for the accent.",
|
||||
"Don't use the tilted portal silhouette as general ornament; it belongs to the logo, the wordmark and empty states.",
|
||||
"Don't put image bytes into controls or recreate clickable controls on progress ticks; update them in place."
|
||||
]
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 169 KiB |
@@ -0,0 +1 @@
|
||||
Baseline: the site as it was before the redesign steps (critique 28/40).
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 170 KiB |
@@ -0,0 +1 @@
|
||||
Quieter: removed the cursor light, click ripples, word-by-word headings and gradient letters, label scramble, count-ups, scroll fade-ups, the hero's scroll fly-away, film grain, icon hover tricks, the download warp field and breathing glow, Compare's tilt/pointer light/tinted cards, the contributor dock magnification; springs no longer overshoot.
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 171 KiB |
@@ -0,0 +1 @@
|
||||
Clarify: section labels name what you do there (See how it works, Try the app, See what runs, Compare the tools, Get started, What's new, Credits) instead of 01-07; plain headlines (What happens when you click Install on Frame; FramePort patches the Quest games that don't run as they are); the hero's 'For the Steam Frame' eyebrow removed; nav Install -> Get started, Compared -> Compare; /download/ explains why Windows and macOS warn (self-signed) and where the SHA-256 sums are.
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 148 KiB |
@@ -0,0 +1 @@
|
||||
Distill + layout: the app tour's 9-slide carousel and videos fold into one 'Screenshots and videos' disclosure; the game-maker block is one row (demo badge, one sentence, docs link; the HTML snippet lives in the docs); What's new is one line; Compare loses its icon tiles; calmer section padding, labels sit closer to their headings, the notice sits between sections at 72ch, bigger hit areas on the hero's platform links.
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1 @@
|
||||
Adapt: below 1120px the eight nav links move into a Menu panel (48px rows, Esc/outside click closes, a link closes it) instead of wrapping (1024) or vanishing (phones); on phones the download buttons say 'Get it on your PC' and lead to the download page's open-on-your-PC state; no three.js on data saver or 2G/3G (the CSS scene shows); the install log drops its technical detail column under 600px. Phone shots now use a phone user agent.
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1 @@
|
||||
Onboard: Get started step 3 (the setup line) now says what running it does: it finds FramePort on your PC and waits, nothing changes on the Frame until you click Allow, then Developer Mode turns on; no password, no root; with a link to read the script. New shot: desktop-walkthrough-step3.
|
||||
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1 @@
|
||||
Polish: the walkthrough's Downloads scene shows each platform's real file and program (FramePort-linux-x64.tar.gz / TAR.GZ / FramePort; .app on macOS) instead of '?os=linux' and FramePort.exe; the site's dim text is #868a96 (4.77:1+, was 3.6-3.9:1) and placeholders use the secondary text colour; install-log stages not reached yet stay readable (only their icon fades); 10.5px labels are 11px; the docs blockquote loses its side bar; the changelog version is solid orange instead of gradient text; the notice reads at 64ch.
|
||||
@@ -0,0 +1,128 @@
|
||||
"""Specimen screenshots of frameport.app for the redesign steps, and the gallery that compares them.
|
||||
|
||||
python capture.py <NN-name> "<what changed>" [--url http://127.0.0.1:4391]
|
||||
|
||||
Writes <NN-name>/*.webp + note.txt next to this file and rebuilds index.html (every step, side by side per shot).
|
||||
Math.random is seeded, so the hero picks the same game every run; the page is scrolled through first so
|
||||
scroll-triggered reveals have played; Astro's dev toolbar is removed."""
|
||||
import argparse
|
||||
import html
|
||||
import io
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
SEED = """(() => { let a = 42; Math.random = () => { a |= 0; a = a + 0x6D2B79F5 | 0;
|
||||
let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
||||
return ((t ^ t >>> 14) >>> 0) / 4294967296; }; })();"""
|
||||
# (name, path, viewport, full page?, scale of the saved image)
|
||||
SHOTS = [
|
||||
("desktop-home-fold", "/", (1440, 900), False, 1.0),
|
||||
("desktop-home-full", "/", (1440, 900), True, 0.5),
|
||||
("desktop-download", "/download/?os=windows", (1440, 900), True, 0.6),
|
||||
("desktop-setup", "/setup/", (1440, 900), True, 0.6),
|
||||
("desktop-docs", "/docs/", (1440, 900), True, 0.6),
|
||||
("mobile-home-fold", "/", (390, 844), False, 1.0),
|
||||
("mobile-home-full", "/", (390, 844), True, 0.6),
|
||||
("mobile-download", "/download/?os=windows", (390, 844), True, 0.8),
|
||||
("desktop-walkthrough-step3", "/#step3", (1440, 900), False, 1.0), # Get started, step 3 (the setup line)
|
||||
("mobile-menu", "/#menu", (390, 844), False, 1.0), # the nav opened (from step 04 on: there was none before)
|
||||
]
|
||||
PHONE_UA = ("Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/126.0 Mobile Safari/537.36")
|
||||
|
||||
|
||||
def settle(page):
|
||||
page.evaluate("""async () => {
|
||||
for (let y = 0; y < document.body.scrollHeight; y += 400) { window.scrollTo(0, y); await new Promise(r => setTimeout(r, 250)); }
|
||||
window.scrollTo(0, 0);
|
||||
document.querySelectorAll('astro-dev-toolbar').forEach(e => e.remove());
|
||||
// the settled look of every section (a full-page shot can't wait for each reveal)
|
||||
document.querySelectorAll('.reveal').forEach(e => e.classList.add('in'));
|
||||
const s = document.createElement('style');
|
||||
s.textContent = '.reveal,.reveal *,.words .w{opacity:1!important;transform:none!important;transition:none!important}';
|
||||
document.head.append(s);
|
||||
}""")
|
||||
time.sleep(1.5)
|
||||
|
||||
|
||||
def capture(step: str, note: str, base: str) -> None:
|
||||
out = HERE / step
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
for name, path, (w, h), full, scale in SHOTS:
|
||||
ctx = browser.new_context(viewport={"width": w, "height": h}, device_scale_factor=1,
|
||||
accept_downloads=False,
|
||||
is_mobile=w < 500, has_touch=w < 500,
|
||||
user_agent=PHONE_UA if w < 500 else None)
|
||||
ctx.add_init_script(SEED)
|
||||
page = ctx.new_page()
|
||||
page.goto(base + path.replace("#menu", "").replace("#step3", ""), wait_until="networkidle")
|
||||
time.sleep(1.0)
|
||||
if path.endswith("#menu"):
|
||||
if not page.is_visible(".menu-btn"):
|
||||
ctx.close()
|
||||
continue
|
||||
page.tap(".menu-btn")
|
||||
if path.endswith("#step3"):
|
||||
page.click("[data-wt] [data-go='2']")
|
||||
page.evaluate("document.querySelector('[data-wt]').scrollIntoView({block: 'center'})")
|
||||
if full:
|
||||
settle(page)
|
||||
else:
|
||||
page.evaluate("document.querySelectorAll('astro-dev-toolbar').forEach(e => e.remove())")
|
||||
time.sleep(2.5) # the hero's first crossing
|
||||
img = Image.open(io.BytesIO(page.screenshot(full_page=full)))
|
||||
if scale != 1.0:
|
||||
img = img.resize((round(img.width * scale), round(img.height * scale)), Image.LANCZOS)
|
||||
img.save(out / f"{name}.webp", "WEBP", quality=72, method=6)
|
||||
ctx.close()
|
||||
browser.close()
|
||||
(out / "note.txt").write_text(note.strip() + "\n", encoding="utf-8")
|
||||
build_index()
|
||||
|
||||
|
||||
def build_index() -> None:
|
||||
steps = sorted(d for d in HERE.iterdir() if d.is_dir() and (d / "note.txt").exists())
|
||||
names = [s[0] for s in SHOTS]
|
||||
rows = []
|
||||
for name in names:
|
||||
cells = "".join(
|
||||
f'<figure><a href="{s.name}/{name}.webp"><img loading="lazy" src="{s.name}/{name}.webp" alt=""></a>'
|
||||
f"<figcaption>{html.escape(s.name)}</figcaption></figure>"
|
||||
for s in steps if (s / f"{name}.webp").exists())
|
||||
rows.append(f'<section><h2>{html.escape(name)}</h2><div class="row">{cells}</div></section>')
|
||||
notes = "".join(f"<li><b>{html.escape(s.name)}</b> — {html.escape((s / 'note.txt').read_text(encoding='utf-8').strip())}</li>"
|
||||
for s in steps)
|
||||
(HERE / "index.html").write_text(f"""<!doctype html><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Site specimens</title>
|
||||
<style>
|
||||
:root {{ color-scheme: dark; }}
|
||||
body {{ margin: 0; padding: 24px 16px; background: #0d0e12; color: #edeef2; font: 15px/1.5 "Segoe UI", Roboto, sans-serif; }}
|
||||
h1 {{ font-size: 22px; margin: 0 0 8px; }} h2 {{ font-size: 15px; color: #a7abb7; margin: 32px 0 8px; font-weight: 600; }}
|
||||
ul {{ max-width: 90ch; padding-left: 20px; color: #c9ccd6; }} b {{ color: #ff8a1f; }}
|
||||
.row {{ display: flex; gap: 16px; overflow-x: auto; align-items: flex-start; padding-bottom: 8px; }}
|
||||
figure {{ margin: 0; flex: 0 0 auto; width: min(560px, 85vw); }}
|
||||
figure img {{ width: 100%; border: 1px solid #2a2d37; border-radius: 8px; display: block; }}
|
||||
figcaption {{ font-size: 13px; color: #a7abb7; margin-top: 6px; }}
|
||||
a {{ color: #3aa8ff; }}
|
||||
</style>
|
||||
<h1>frameport.app: step-by-step specimens</h1>
|
||||
<p>Each row is one shot; each column is a step, oldest on the left. Click an image for full size.</p>
|
||||
<ul>{notes}</ul>
|
||||
{''.join(rows)}
|
||||
""", encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("step")
|
||||
ap.add_argument("note")
|
||||
ap.add_argument("--url", default="http://127.0.0.1:4391")
|
||||
a = ap.parse_args()
|
||||
capture(a.step, a.note, a.url)
|
||||
@@ -0,0 +1,18 @@
|
||||
<!doctype html><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Site specimens</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
body { margin: 0; padding: 24px 16px; background: #0d0e12; color: #edeef2; font: 15px/1.5 "Segoe UI", Roboto, sans-serif; }
|
||||
h1 { font-size: 22px; margin: 0 0 8px; } h2 { font-size: 15px; color: #a7abb7; margin: 32px 0 8px; font-weight: 600; }
|
||||
ul { max-width: 90ch; padding-left: 20px; color: #c9ccd6; } b { color: #ff8a1f; }
|
||||
.row { display: flex; gap: 16px; overflow-x: auto; align-items: flex-start; padding-bottom: 8px; }
|
||||
figure { margin: 0; flex: 0 0 auto; width: min(560px, 85vw); }
|
||||
figure img { width: 100%; border: 1px solid #2a2d37; border-radius: 8px; display: block; }
|
||||
figcaption { font-size: 13px; color: #a7abb7; margin-top: 6px; }
|
||||
a { color: #3aa8ff; }
|
||||
</style>
|
||||
<h1>frameport.app: step-by-step specimens</h1>
|
||||
<p>Each row is one shot; each column is a step, oldest on the left. Click an image for full size.</p>
|
||||
<ul><li><b>00-before</b> — Baseline: the site as it was before the redesign steps (critique 28/40).</li><li><b>01-quieter</b> — Quieter: removed the cursor light, click ripples, word-by-word headings and gradient letters, label scramble, count-ups, scroll fade-ups, the hero's scroll fly-away, film grain, icon hover tricks, the download warp field and breathing glow, Compare's tilt/pointer light/tinted cards, the contributor dock magnification; springs no longer overshoot.</li><li><b>02-clarify</b> — Clarify: section labels name what you do there (See how it works, Try the app, See what runs, Compare the tools, Get started, What's new, Credits) instead of 01-07; plain headlines (What happens when you click Install on Frame; FramePort patches the Quest games that don't run as they are); the hero's 'For the Steam Frame' eyebrow removed; nav Install -> Get started, Compared -> Compare; /download/ explains why Windows and macOS warn (self-signed) and where the SHA-256 sums are.</li><li><b>03-distill-layout</b> — Distill + layout: the app tour's 9-slide carousel and videos fold into one 'Screenshots and videos' disclosure; the game-maker block is one row (demo badge, one sentence, docs link; the HTML snippet lives in the docs); What's new is one line; Compare loses its icon tiles; calmer section padding, labels sit closer to their headings, the notice sits between sections at 72ch, bigger hit areas on the hero's platform links.</li><li><b>04-adapt</b> — Adapt: below 1120px the eight nav links move into a Menu panel (48px rows, Esc/outside click closes, a link closes it) instead of wrapping (1024) or vanishing (phones); on phones the download buttons say 'Get it on your PC' and lead to the download page's open-on-your-PC state; no three.js on data saver or 2G/3G (the CSS scene shows); the install log drops its technical detail column under 600px. Phone shots now use a phone user agent.</li><li><b>05-onboard</b> — Onboard: Get started step 3 (the setup line) now says what running it does: it finds FramePort on your PC and waits, nothing changes on the Frame until you click Allow, then Developer Mode turns on; no password, no root; with a link to read the script. New shot: desktop-walkthrough-step3.</li><li><b>06-polish</b> — Polish: the walkthrough's Downloads scene shows each platform's real file and program (FramePort-linux-x64.tar.gz / TAR.GZ / FramePort; .app on macOS) instead of '?os=linux' and FramePort.exe; the site's dim text is #868a96 (4.77:1+, was 3.6-3.9:1) and placeholders use the secondary text colour; install-log stages not reached yet stay readable (only their icon fades); 10.5px labels are 11px; the docs blockquote loses its side bar; the changelog version is solid orange instead of gradient text; the notice reads at 64ch.</li></ul>
|
||||
<section><h2>desktop-home-fold</h2><div class="row"><figure><a href="00-before/desktop-home-fold.webp"><img loading="lazy" src="00-before/desktop-home-fold.webp" alt=""></a><figcaption>00-before</figcaption></figure><figure><a href="01-quieter/desktop-home-fold.webp"><img loading="lazy" src="01-quieter/desktop-home-fold.webp" alt=""></a><figcaption>01-quieter</figcaption></figure><figure><a href="02-clarify/desktop-home-fold.webp"><img loading="lazy" src="02-clarify/desktop-home-fold.webp" alt=""></a><figcaption>02-clarify</figcaption></figure><figure><a href="03-distill-layout/desktop-home-fold.webp"><img loading="lazy" src="03-distill-layout/desktop-home-fold.webp" alt=""></a><figcaption>03-distill-layout</figcaption></figure><figure><a href="04-adapt/desktop-home-fold.webp"><img loading="lazy" src="04-adapt/desktop-home-fold.webp" alt=""></a><figcaption>04-adapt</figcaption></figure><figure><a href="05-onboard/desktop-home-fold.webp"><img loading="lazy" src="05-onboard/desktop-home-fold.webp" alt=""></a><figcaption>05-onboard</figcaption></figure><figure><a href="06-polish/desktop-home-fold.webp"><img loading="lazy" src="06-polish/desktop-home-fold.webp" alt=""></a><figcaption>06-polish</figcaption></figure></div></section><section><h2>desktop-home-full</h2><div class="row"><figure><a href="00-before/desktop-home-full.webp"><img loading="lazy" src="00-before/desktop-home-full.webp" alt=""></a><figcaption>00-before</figcaption></figure><figure><a href="01-quieter/desktop-home-full.webp"><img loading="lazy" src="01-quieter/desktop-home-full.webp" alt=""></a><figcaption>01-quieter</figcaption></figure><figure><a href="02-clarify/desktop-home-full.webp"><img loading="lazy" src="02-clarify/desktop-home-full.webp" alt=""></a><figcaption>02-clarify</figcaption></figure><figure><a href="03-distill-layout/desktop-home-full.webp"><img loading="lazy" src="03-distill-layout/desktop-home-full.webp" alt=""></a><figcaption>03-distill-layout</figcaption></figure><figure><a href="04-adapt/desktop-home-full.webp"><img loading="lazy" src="04-adapt/desktop-home-full.webp" alt=""></a><figcaption>04-adapt</figcaption></figure><figure><a href="05-onboard/desktop-home-full.webp"><img loading="lazy" src="05-onboard/desktop-home-full.webp" alt=""></a><figcaption>05-onboard</figcaption></figure><figure><a href="06-polish/desktop-home-full.webp"><img loading="lazy" src="06-polish/desktop-home-full.webp" alt=""></a><figcaption>06-polish</figcaption></figure></div></section><section><h2>desktop-download</h2><div class="row"><figure><a href="00-before/desktop-download.webp"><img loading="lazy" src="00-before/desktop-download.webp" alt=""></a><figcaption>00-before</figcaption></figure><figure><a href="01-quieter/desktop-download.webp"><img loading="lazy" src="01-quieter/desktop-download.webp" alt=""></a><figcaption>01-quieter</figcaption></figure><figure><a href="02-clarify/desktop-download.webp"><img loading="lazy" src="02-clarify/desktop-download.webp" alt=""></a><figcaption>02-clarify</figcaption></figure><figure><a href="03-distill-layout/desktop-download.webp"><img loading="lazy" src="03-distill-layout/desktop-download.webp" alt=""></a><figcaption>03-distill-layout</figcaption></figure><figure><a href="04-adapt/desktop-download.webp"><img loading="lazy" src="04-adapt/desktop-download.webp" alt=""></a><figcaption>04-adapt</figcaption></figure><figure><a href="05-onboard/desktop-download.webp"><img loading="lazy" src="05-onboard/desktop-download.webp" alt=""></a><figcaption>05-onboard</figcaption></figure><figure><a href="06-polish/desktop-download.webp"><img loading="lazy" src="06-polish/desktop-download.webp" alt=""></a><figcaption>06-polish</figcaption></figure></div></section><section><h2>desktop-setup</h2><div class="row"><figure><a href="00-before/desktop-setup.webp"><img loading="lazy" src="00-before/desktop-setup.webp" alt=""></a><figcaption>00-before</figcaption></figure><figure><a href="01-quieter/desktop-setup.webp"><img loading="lazy" src="01-quieter/desktop-setup.webp" alt=""></a><figcaption>01-quieter</figcaption></figure><figure><a href="02-clarify/desktop-setup.webp"><img loading="lazy" src="02-clarify/desktop-setup.webp" alt=""></a><figcaption>02-clarify</figcaption></figure><figure><a href="03-distill-layout/desktop-setup.webp"><img loading="lazy" src="03-distill-layout/desktop-setup.webp" alt=""></a><figcaption>03-distill-layout</figcaption></figure><figure><a href="04-adapt/desktop-setup.webp"><img loading="lazy" src="04-adapt/desktop-setup.webp" alt=""></a><figcaption>04-adapt</figcaption></figure><figure><a href="05-onboard/desktop-setup.webp"><img loading="lazy" src="05-onboard/desktop-setup.webp" alt=""></a><figcaption>05-onboard</figcaption></figure><figure><a href="06-polish/desktop-setup.webp"><img loading="lazy" src="06-polish/desktop-setup.webp" alt=""></a><figcaption>06-polish</figcaption></figure></div></section><section><h2>desktop-docs</h2><div class="row"><figure><a href="00-before/desktop-docs.webLine truncated
|
||||
@@ -0,0 +1,255 @@
|
||||
<title>frameport.app Redesign</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||
<style>
|
||||
/* Layout: one column; a comparer (two steps, one shot) leads, the step list follows. FramePort's own Portal look:
|
||||
dark only (the product is dark only), orange = the action and the current choice, blue = the secondary and the PC. */
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #0d0e12;
|
||||
--panel: #16181e;
|
||||
--raised: #1d1f27;
|
||||
--well: #252832;
|
||||
--line: #2a2d37;
|
||||
--line-strong: #393d49;
|
||||
--text: #edeef2;
|
||||
--dim: #a7abb7;
|
||||
--faint: #868a96;
|
||||
--orange: #ff8a1f;
|
||||
--orange-ink: #160c03;
|
||||
--blue: #3aa8ff;
|
||||
--blue-line: rgba(58, 168, 255, .55);
|
||||
--ok: #3dd68c;
|
||||
--portal: linear-gradient(90deg, #3aa8ff, #ff8a1f);
|
||||
--fade: linear-gradient(90deg, rgba(58, 168, 255, .22), rgba(255, 138, 31, .22));
|
||||
--display: Sora, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--sans: "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--mono: ui-monospace, "Cascadia Mono", "SF Mono", Menlo, Consolas, monospace;
|
||||
--r: 12px; --r-sm: 8px;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { background: var(--bg); color: var(--text); font: 400 15px/1.6 var(--sans); }
|
||||
.page { max-width: 1240px; margin: 0 auto; padding-inline: clamp(16px, 4vw, 40px); padding-block: 40px 72px; display: grid; gap: 48px; }
|
||||
a { color: var(--orange); }
|
||||
:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
|
||||
h1, h2 { font-family: var(--display); margin: 0; text-wrap: balance; letter-spacing: -0.02em; }
|
||||
h1 { font-size: clamp(30px, 4.6vw, 48px); line-height: 1.08; font-weight: 800; }
|
||||
h2 { font-size: 22px; font-weight: 700; }
|
||||
p { margin: 0; text-wrap: pretty; }
|
||||
code { font-family: var(--mono); font-size: .9em; }
|
||||
|
||||
/* ---- head */
|
||||
.head { display: grid; gap: 18px; }
|
||||
.site { display: flex; align-items: center; gap: 10px; font: 600 13px/1 var(--mono); color: var(--dim); }
|
||||
.site::after { content: ""; flex: 1; height: 2px; background: var(--portal); opacity: .7; }
|
||||
.lead { color: var(--dim); max-width: 64ch; font-size: 16.5px; }
|
||||
.facts { display: flex; flex-wrap: wrap; gap: 10px; margin: 0; padding: 0; list-style: none; }
|
||||
.facts li { display: grid; gap: 2px; padding: 10px 14px; border: 1px solid var(--line); border-radius: var(--r-sm); background: var(--panel); min-width: 0; }
|
||||
.facts dt, .facts .k { font-size: 12.5px; color: var(--dim); }
|
||||
.facts .v { font: 600 15px/1.3 var(--mono); font-variant-numeric: tabular-nums; }
|
||||
.facts .v s { color: var(--faint); text-decoration-thickness: 1px; }
|
||||
.facts .v b { color: var(--ok); font-weight: 600; }
|
||||
|
||||
/* ---- comparer */
|
||||
.cmp { display: grid; gap: 16px; }
|
||||
.bar { display: flex; flex-wrap: wrap; align-items: end; gap: 12px 20px; }
|
||||
.pick { display: grid; gap: 4px; }
|
||||
.pick label { font-size: 12.5px; color: var(--dim); }
|
||||
select { appearance: none; min-height: 40px; padding: 0 34px 0 12px; border-radius: var(--r-sm); border: 1px solid var(--line-strong);
|
||||
background: var(--well) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23a7abb7' stroke-width='1.6'/%3E%3C/svg%3E") no-repeat right 12px center;
|
||||
color: var(--text); font: 500 14px/1 var(--sans); cursor: pointer; }
|
||||
.arrow { color: var(--faint); padding-bottom: 10px; }
|
||||
.seg { display: inline-flex; padding: 3px; border-radius: 999px; border: 1px solid var(--line-strong); margin-left: auto; }
|
||||
.seg button { all: unset; padding: 7px 14px; border-radius: 999px; font: 600 13px/1 var(--sans); color: var(--dim); cursor: pointer; }
|
||||
.seg button[aria-pressed="true"] { background: var(--fade); color: var(--text); }
|
||||
.seg button:focus-visible { outline: 2px solid var(--orange); outline-offset: 1px; }
|
||||
.shots { display: grid; gap: 8px; }
|
||||
.grp { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
||||
.grp .dev { width: 64px; font: 600 11px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; color: var(--faint); }
|
||||
.chip { all: unset; padding: 7px 12px; border-radius: 999px; border: 1px solid var(--line-strong); font: 500 13px/1 var(--sans); color: var(--dim); cursor: pointer; }
|
||||
.chip:hover { color: var(--text); }
|
||||
.chip[aria-pressed="true"] { border-color: transparent; background: var(--fade); color: var(--text); }
|
||||
.chip:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
|
||||
|
||||
.stage { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; }
|
||||
.pane { display: grid; gap: 8px; min-width: 0; }
|
||||
.cap { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; font-size: 13.5px; }
|
||||
.cap b { font-weight: 600; }
|
||||
.cap code { color: var(--faint); }
|
||||
.cap .tag { font: 600 11px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase; }
|
||||
.pane.a .tag { color: var(--blue); }
|
||||
.pane.b .tag { color: var(--orange); }
|
||||
.view { max-height: 76vh; overflow: auto; border: 1px solid var(--line); border-radius: var(--r); background: #050608; overscroll-behavior: contain; }
|
||||
.view img { display: block; width: 100%; height: auto; }
|
||||
.phone .view img { max-width: 390px; margin-inline: auto; }
|
||||
.none { display: grid; place-items: center; min-height: 240px; padding: 24px; text-align: center; color: var(--dim); font-size: 14px; }
|
||||
|
||||
.swipe { position: relative; touch-action: pan-y; cursor: ew-resize; }
|
||||
.swipe .after { position: absolute; inset: 0; clip-path: inset(0 0 0 var(--x, 50%)); }
|
||||
.swipe .knob { position: absolute; top: 0; bottom: 0; left: var(--x, 50%); width: 2px; margin-left: -1px; background: var(--portal); box-shadow: 0 0 0 1px rgba(0, 0, 0, .5); pointer-events: none; }
|
||||
.swipe-bar { display: flex; align-items: center; gap: 12px; font-size: 13px; color: var(--dim); }
|
||||
.swipe-bar input { flex: 1; accent-color: var(--orange); }
|
||||
.swipe-bar .l { color: var(--blue); } .swipe-bar .r { color: var(--orange); }
|
||||
|
||||
/* ---- steps */
|
||||
.steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
|
||||
.step { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 20px; align-items: start; padding: 16px; border: 1px solid var(--line);
|
||||
border-radius: var(--r); background: var(--panel); }
|
||||
.step img { width: 100%; height: auto; border-radius: var(--r-sm); border: 1px solid var(--line); display: block; }
|
||||
.step .body { display: grid; gap: 8px; min-width: 0; }
|
||||
.step .top { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 12px; }
|
||||
.step .n { font: 600 13px/1 var(--mono); color: var(--faint); font-variant-numeric: tabular-nums; }
|
||||
.step h3 { margin: 0; font: 700 18px/1.25 var(--display); }
|
||||
.step .top code { color: var(--faint); font-size: 12.5px; }
|
||||
.step p { color: var(--dim); max-width: 75ch; }
|
||||
.step button { all: unset; justify-self: start; padding: 8px 14px; border-radius: var(--r-sm); border: 1px solid var(--blue-line); color: var(--text);
|
||||
font: 600 13.5px/1 var(--sans); cursor: pointer; }
|
||||
.step button:hover { background: rgba(58, 168, 255, .08); }
|
||||
.step button:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
|
||||
.note { font-size: 13px; color: var(--faint); }
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.stage { grid-template-columns: minmax(0, 1fr); }
|
||||
.view { max-height: 62vh; }
|
||||
.seg { margin-left: 0; }
|
||||
.step { grid-template-columns: minmax(0, 1fr); }
|
||||
.step img { max-width: 360px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<main class="page">
|
||||
<header class="head">
|
||||
<p class="site">frameport.app</p>
|
||||
<h1>The site, one step at a time</h1>
|
||||
<p class="lead">Six passes after the 2026-10-12 critique (28/40), each committed on its own. Pick two steps and a
|
||||
shot to compare them; the screenshots come from the same capture script each time.</p>
|
||||
<ul class="facts" aria-label="Before and after">
|
||||
<li><span class="k">Detector findings, home (desktop)</span><span class="v"><s>90</s> → <b>46</b></span></li>
|
||||
<li><span class="k">Detector findings, home (phone)</span><span class="v"><s>59</s> → <b>33</b></span></li>
|
||||
<li><span class="k">Home page height</span><span class="v"><s>10.8k px</s> → <b>8.4k px</b></span></li>
|
||||
<li><span class="k">Dim text on cards</span><span class="v"><s>3.87:1</s> → <b>5.15:1</b></span></li>
|
||||
<li><span class="k">Navigation on phones</span><span class="v"><s>none</s> → <b>Menu</b></span></li>
|
||||
</ul>
|
||||
</header>
|
||||
|
||||
<section class="cmp" id="compare" aria-labelledby="cmp-h">
|
||||
<h2 id="cmp-h">Compare</h2>
|
||||
<div class="bar">
|
||||
<div class="pick"><label for="sel-a">Before</label><select id="sel-a"></select></div>
|
||||
<span class="arrow" aria-hidden="true">→</span>
|
||||
<div class="pick"><label for="sel-b">After</label><select id="sel-b"></select></div>
|
||||
<div class="seg" role="group" aria-label="View">
|
||||
<button type="button" id="mode-side" aria-pressed="true">Side by side</button>
|
||||
<button type="button" id="mode-swipe" aria-pressed="false">Swipe</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shots" id="shots" role="group" aria-label="Shot"></div>
|
||||
<div id="stage"></div>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="steps-h" style="display:grid;gap:16px">
|
||||
<h2 id="steps-h">The steps</h2>
|
||||
<ol class="steps" id="steps"></ol>
|
||||
<p class="note">Each step is one commit on the branch <code>worktree-impeccable-init</code>. Shots are seeded so the
|
||||
hero shows the same game; scroll-reveal sections are shown settled.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const DATA = {"steps": [{"id": "00-before", "name": "Before", "commit": "daa71a1", "note": "The site as the critique found it: 28/40, with a cursor light, word-by-word headings, scrambling labels, scroll fade-ups and no navigation on phones.", "shots": ["desktop-home-fold", "desktop-home-full", "desktop-download", "desktop-setup", "desktop-docs", "mobile-home-fold", "mobile-home-full", "mobile-download"]}, {"id": "01-quieter", "name": "Quieter", "commit": "540c719", "note": "Removed the ambient effects (cursor light, click ripples, rising and lit headings, label scramble, count-ups, scroll fade-ups, film grain, the download warp field, Compare's tilt, the avatar magnification). Motion stays where something crosses: the hero, the install log, the download portal.", "shots": ["desktop-home-fold", "desktop-home-full", "desktop-download", "desktop-setup", "desktop-docs", "mobile-home-fold", "mobile-home-full", "mobile-download"]}, {"id": "02-clarify", "name": "Clarify", "commit": "af6117a", "note": "Section labels name what you do there instead of 01-07. Plain headlines. The nav's \"Install\" became \"Get started\". The download page says why Windows and macOS warn.", "shots": ["desktop-home-fold", "desktop-home-full", "desktop-download", "desktop-setup", "desktop-docs", "mobile-home-fold", "mobile-home-full", "mobile-download"]}, {"id": "03-distill-layout", "name": "Distill + layout", "commit": "72f94d1", "note": "Screenshots and videos fold behind one disclosure, the game-maker block is one row, What's new is one line, Compare loses its icon tiles. Calmer spacing. The home page went from 10.8k to 8.4k px.", "shots": ["desktop-home-fold", "desktop-home-full", "desktop-download", "desktop-setup", "desktop-docs", "mobile-home-fold", "mobile-home-full", "mobile-download"]}, {"id": "04-adapt", "name": "Adapt", "commit": "58a936c", "note": "Below 1120px the nav links move into a Menu panel. Phones get \"Get it on your PC\". No 3D download on slow connections. Phone shots use a phone browser from here on.", "shots": ["desktop-home-fold", "desktop-home-full", "desktop-download", "desktop-setup", "desktop-docs", "mobile-home-fold", "mobile-home-full", "mobile-menu", "mobile-download"]}, {"id": "05-onboard", "name": "Onboard", "commit": "fc47dab", "note": "The setup-line step says nothing changes on the Frame until you click Allow, and links the script.", "shots": ["desktop-home-fold", "desktop-home-full", "desktop-walkthrough-step3", "desktop-download", "desktop-setup", "desktop-docs", "mobile-home-fold", "mobile-home-full", "mobile-menu", "mobile-download"]}, {"id": "06-polish", "name": "Polish", "commit": "93b62fa", "note": "Real file names per platform in the walkthrough, readable dim text (4.77:1, was 3.6:1), themed placeholders, no labels under 11px, no gradient text or side-bar quotes left.", "shots": ["desktop-home-fold", "desktop-home-full", "desktop-walkthrough-step3", "desktop-download", "desktop-setup", "desktop-docs", "mobile-home-fold", "mobile-home-full", "mobile-menu", "mobile-download"]}], "shots": [{"id": "desktop-home-fold", "label": "Home, first screen", "device": "desktop"}, {"id": "desktop-home-full", "label": "Home, whole page", "device": "desktop"}, {"id": "desktop-walkthrough-step3", "label": "Get started, step 3", "device": "desktop"}, {"id": "desktop-download", "label": "Download page", "device": "desktop"}, {"id": "desktop-setup", "label": "Setup page", "device": "desktop"}, {"id": "desktop-docs", "label": "Docs", "device": "desktop"}, {"id": "mobile-home-fold", "label": "Home, first screen", "device": "phone"}, {"id": "mobile-home-full", "label": "Home, whole page", "device": "phone"}, {"id": "mobile-menu", "label": "Menu", "device": "phone"}, {"id": "mobile-download", "label": "Download page", "device": "phone"}]};
|
||||
const $ = (s) => document.querySelector(s);
|
||||
const step = (id) => DATA.steps.find((s) => s.id === id);
|
||||
const shot = (id) => DATA.shots.find((s) => s.id === id);
|
||||
const WHY = {
|
||||
'mobile-menu': 'There was no menu on phones before Adapt (04).',
|
||||
'desktop-walkthrough-step3': 'This shot was added with Onboard (05).',
|
||||
};
|
||||
const state = { a: DATA.steps[0].id, b: DATA.steps[DATA.steps.length - 1].id, shot: 'desktop-home-fold', mode: 'side', x: 50 };
|
||||
|
||||
function fillSelects() {
|
||||
for (const sel of [$('#sel-a'), $('#sel-b')]) {
|
||||
sel.innerHTML = DATA.steps.map((s, i) => `<option value="${s.id}">${String(i).padStart(2, '0')} · ${s.name}</option>`).join('');
|
||||
}
|
||||
$('#sel-a').value = state.a; $('#sel-b').value = state.b;
|
||||
$('#sel-a').addEventListener('change', (e) => { state.a = e.target.value; render(); });
|
||||
$('#sel-b').addEventListener('change', (e) => { state.b = e.target.value; render(); });
|
||||
}
|
||||
|
||||
function renderShots() {
|
||||
const devices = [...new Set(DATA.shots.map((s) => s.device))];
|
||||
$('#shots').innerHTML = devices.map((d) => `<div class="grp"><span class="dev">${d}</span>${DATA.shots.filter((s) => s.device === d)
|
||||
.map((s) => `<button type="button" class="chip" data-shot="${s.id}" aria-pressed="${s.id === state.shot}">${s.label}</button>`).join('')}</div>`).join('');
|
||||
$('#shots').querySelectorAll('.chip').forEach((b) => b.addEventListener('click', () => { state.shot = b.dataset.shot; render(); }));
|
||||
}
|
||||
|
||||
const src = (stepId) => step(stepId).shots.includes(state.shot) ? `${stepId}/${state.shot}.webp` : null;
|
||||
const label = (stepId) => { const i = DATA.steps.findIndex((s) => s.id === stepId); return `${String(i).padStart(2, '0')} · ${step(stepId).name}`; };
|
||||
const missing = (stepId) => `<div class="none">${WHY[state.shot] ?? `No ${shot(state.shot).label.toLowerCase()} shot in ${step(stepId).name}.`}</div>`;
|
||||
const caption = (cls, tag, stepId) => `<div class="cap"><span class="tag">${tag}</span><b>${label(stepId)}</b><code>${step(stepId).commit}</code></div>`;
|
||||
|
||||
function render() {
|
||||
$('#shots').querySelectorAll('.chip').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.shot === state.shot)));
|
||||
$('#mode-side').setAttribute('aria-pressed', String(state.mode === 'side'));
|
||||
$('#mode-swipe').setAttribute('aria-pressed', String(state.mode === 'swipe'));
|
||||
const phone = shot(state.shot).device === 'phone';
|
||||
const a = src(state.a), b = src(state.b);
|
||||
const stage = $('#stage');
|
||||
stage.className = phone ? 'phone' : '';
|
||||
if (state.mode === 'swipe' && a && b) {
|
||||
stage.innerHTML = `<div class="pane">
|
||||
<div class="cap"><span class="tag" style="color:var(--blue)">Left</span><b>${label(state.a)}</b>
|
||||
<span aria-hidden="true">·</span><span class="tag" style="color:var(--orange)">Right</span><b>${label(state.b)}</b></div>
|
||||
<div class="view"><div class="swipe" style="--x:${state.x}%">
|
||||
<img src="${a}" alt="${shot(state.shot).label}, ${label(state.a)}">
|
||||
<div class="after"><img src="${b}" alt="${shot(state.shot).label}, ${label(state.b)}"></div>
|
||||
<i class="knob"></i></div></div>
|
||||
<div class="swipe-bar"><span class="l">${step(state.a).name}</span>
|
||||
<input type="range" id="swipe-x" min="0" max="100" value="${state.x}" aria-label="Divider position">
|
||||
<span class="r">${step(state.b).name}</span></div></div>`;
|
||||
const sw = stage.querySelector('.swipe'), range = stage.querySelector('#swipe-x');
|
||||
const set = (x) => { state.x = Math.max(0, Math.min(100, x)); sw.style.setProperty('--x', `${state.x}%`); range.value = state.x; };
|
||||
range.addEventListener('input', () => set(Number(range.value)));
|
||||
let drag = false;
|
||||
const at = (e) => { const r = sw.getBoundingClientRect(); set(((e.clientX - r.left) / r.width) * 100); };
|
||||
sw.addEventListener('pointerdown', (e) => { if (e.pointerType === 'touch') return; drag = true; sw.setPointerCapture(e.pointerId); at(e); });
|
||||
sw.addEventListener('pointermove', (e) => { if (drag) at(e); });
|
||||
sw.addEventListener('pointerup', () => { drag = false; });
|
||||
return;
|
||||
}
|
||||
stage.innerHTML = `<div class="stage">
|
||||
<div class="pane a">${caption('a', 'Before', state.a)}<div class="view">${a ? `<img src="${a}" alt="${shot(state.shot).label}, ${label(state.a)}">` : missing(state.a)}</div></div>
|
||||
<div class="pane b">${caption('b', 'After', state.b)}<div class="view">${b ? `<img src="${b}" alt="${shot(state.shot).label}, ${label(state.b)}">` : missing(state.b)}</div></div>
|
||||
</div>`;
|
||||
// the two views scroll together (by proportion: the pages differ in length)
|
||||
const views = [...stage.querySelectorAll('.view')];
|
||||
let lock = false;
|
||||
views.forEach((v, i) => v.addEventListener('scroll', () => {
|
||||
if (lock) return;
|
||||
const o = views[1 - i], max = v.scrollHeight - v.clientHeight, omax = o.scrollHeight - o.clientHeight;
|
||||
if (max <= 0 || omax <= 0) return;
|
||||
lock = true; o.scrollTop = (v.scrollTop / max) * omax; requestAnimationFrame(() => { lock = false; });
|
||||
}, { passive: true }));
|
||||
}
|
||||
|
||||
function renderSteps() {
|
||||
$('#steps').innerHTML = DATA.steps.map((s, i) => `<li class="step">
|
||||
<img src="${s.id}/desktop-home-fold.webp" alt="Home, first screen, after ${s.name}" loading="lazy">
|
||||
<div class="body">
|
||||
<div class="top"><span class="n">${String(i).padStart(2, '0')}</span><h3>${s.name}</h3><code>${s.commit}</code></div>
|
||||
<p>${s.note}</p>
|
||||
${i ? `<button type="button" data-cmp="${i}">Compare with ${DATA.steps[i - 1].name}</button>` : ''}
|
||||
</div></li>`).join('');
|
||||
$('#steps').querySelectorAll('[data-cmp]').forEach((b) => b.addEventListener('click', () => {
|
||||
const i = Number(b.dataset.cmp);
|
||||
state.a = DATA.steps[i - 1].id; state.b = DATA.steps[i].id;
|
||||
$('#sel-a').value = state.a; $('#sel-b').value = state.b;
|
||||
render();
|
||||
$('#compare').scrollIntoView({ behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' });
|
||||
}));
|
||||
}
|
||||
|
||||
$('#mode-side').addEventListener('click', () => { state.mode = 'side'; render(); });
|
||||
$('#mode-swipe').addEventListener('click', () => { state.mode = 'swipe'; render(); });
|
||||
fillSelects(); renderShots(); renderSteps(); render();
|
||||
</script>
|
||||
@@ -68,7 +68,11 @@ Read `docs/PLAYBOOK.md` (symptom → fix) before debugging a game, and `docs/FRA
|
||||
mouse drags/right-clicks.
|
||||
Live Frame data comes from one app-owned `app.monitor_hub` (`frame/monitor_hub.py`): subscribers name modules +
|
||||
interval, the hub runs one agent `_monitor` stream (union of modules, fastest interval; per-module collection
|
||||
needs agent v65), reconnects after a lost stream and stops it when nobody subscribes; `_poll` retries an "error".
|
||||
needs agent v73), reconnects after a lost stream and stops it when nobody subscribes; `_poll` retries an "error".
|
||||
Games in a sample (agent v81): FramePort's own (launch.sh / container / Proton folder), any other game Steam's
|
||||
reaper runs (`steam:<appid>`, title from the appmanifest, e.g. Job Simulator from Steam's store; not SteamVR, not
|
||||
devkit entries; End game = Steam's Exit game), overlay apps flagged `overlay` and listed last; newest first. The
|
||||
UI takes `frame/monitor.main_game(sample)` (never an overlay app alone). The agent's own workers never count.
|
||||
Subscribers: "monitor" (the Monitor tab, everything, only while shown) and "card" = the sidebar's live Frame card
|
||||
(`ui/frame_card.py` helpers: battery ring, "now playing" row with fps + 2-min sparkline → click opens Monitor;
|
||||
games + battery every 5 s, paused while a job's stage is "Upload…", only while connected and setting
|
||||
@@ -154,7 +158,11 @@ Read `docs/PLAYBOOK.md` (symptom → fix) before debugging a game, and `docs/FRA
|
||||
doesn't do). `VD.bat` is Virtual Desktop's launcher: ignore it except as an exe-location hint. Library migration
|
||||
`rift_run_direct` resets existing recipes.
|
||||
Auto launch-test is skipped on PC installs (it would start the game on the user's desktop). Launch tests collect the Unreal
|
||||
game log + crash summaries from the Proton prefix; triage `unreal-crash`. Lies Beneath via Proton without Revive
|
||||
game log + crash summaries from the Proton prefix; triage `unreal-crash`. Agent v67 adds Unity's logs (LocalLow/<Company>/<Product>/Player(-prev).log via `<Name>_Data/app.info`,
|
||||
`output_log.txt`, Temp/…/Crashes/*/error.log; `unity_logs`) to launch tests and diagnostics; triage `unity-vr-init` /
|
||||
`unity-crash`. A catalog recipe verified with another build (catalog `xr` ≠ the build's) keeps the build's own
|
||||
`pcvr.launch_args` (`engine._other_build`; GitHub #105: SUPERHOT VR's Oculus+OpenVR build needs `-vrmode OpenVR`,
|
||||
the catalog's is the OpenXR build); Electron launchers next to a game rank −30 (`rift.is_electron`). Lies Beneath via Proton without Revive
|
||||
crashed (UE 4.23 "Unhandled exception").
|
||||
- Rift scanning: `sources/rift_dump.scan` = the scanned folder's subfolders are games (one per folder; a folder is a
|
||||
game if all candidate exes sit under one child), recursing into collections; `analysis/rift.py` walks once,
|
||||
@@ -207,7 +215,14 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
|
||||
`FRAMEPORT_DESKTOP=1` skips the Steam-parent watchdog and Steam's display; ensure_host_fixes refreshes entries
|
||||
(older installs, stale ones removed), uninstall/purge remove them. Per app: library entry field `desktop_entry`
|
||||
(default on; patches don't apply to Linux apps) → game page switch → agent `desktop_entry`. Untested on device.
|
||||
- Scanning: SideQuest backups (`<package>/<time>/apk|obb|data`) and AXRB downloads (`AXRB/<app id>/<binary id>/base.apk`
|
||||
+ OBBs/assets; its `patched/`/`*-axrb.apk` PC builds are skipped) are recognised (`sources/quest_dump.py`, FAQ).
|
||||
- Quest/Rift twins stay separate entries, shown and named in Steam "Title (Quest)"/"(Rift)" (`core/titles.py`).
|
||||
- Rename… (game menu "Name and artwork", Library right-click; CLI `frameport rename <pkg> "<name>"|--reset`):
|
||||
`pipeline.rename_game` sets `title` + `title_locked` (+ `auto_title` = the automatic name for reset; rescans, Rift
|
||||
store matches and links then only update `auto_title`; a locked name gets no twin suffix); `sync_title` → Frame
|
||||
agent v77 `rename` (deployment.json title, same shortcut appid: upsert matches by Exe) + art + one Steam restart, PC
|
||||
`PcReviveTarget.rename`; an unreachable Frame → `steam_name_stale` (game page "Update name on Frame", next install).
|
||||
- `Recipe.as_is` = install unchanged (pre-patched libraries): `pipeline.prepare_as_is`; auto for APKs that already
|
||||
contain FrameBridge (`frame_patched`). For Rift it changes nothing (the dump is never modified; the Frame copy
|
||||
still gets launch fixes like the crash-reporter rename — `-nocrashreports` alone doesn't stop UE 4.23) — it does
|
||||
@@ -245,8 +260,12 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
|
||||
- `diag/` — user feedback without tokens (docs/DIAGNOSTICS.md): `redact.py` (every file/issue text: IPs, hosts,
|
||||
home dirs, Steam ids, dump folders → placeholders), `bundle.py` (redacted diagnostics zip; agent v21
|
||||
`collect_diag`; `frameport diag collect|inspect|report`), `issue.py` (prefilled GitHub issue-form links, ≤7.5k
|
||||
chars). "Share working config" → `working-config.yml` issue → maintainer label `catalog-accepted` →
|
||||
`catalog-from-issue.yml` workflow (`scripts/catalog_from_issue.py` validates) opens a catalog PR. App log:
|
||||
chars; `*_links` = `IssueLinks(form, plain, body)`: body = the Markdown the form writes (`### <label>`, recipe in
|
||||
a ```yaml block; labels checked against the templates), plain = `?title=&body=` link. GitHub #167: some (Flatpak)
|
||||
browsers opened the form with empty fields → `app.open_issue` copies the body to the clipboard and offers "Form
|
||||
empty? Open a plain issue"; keep the templates valid YAML, quote `: ` in descriptions). "Share working config" →
|
||||
`working-config.yml` issue → maintainer label `catalog-accepted` (a plain issue needs `working-config` added by
|
||||
hand) → `catalog-from-issue.yml` workflow (`scripts/catalog_from_issue.py` validates) opens a catalog PR. App log:
|
||||
`core/applog.py` (`<data>/logs/app.log`, finished GUI jobs in `<data>/logs/jobs/`).
|
||||
- Self-update (docs/ARCHITECTURE.md "Self-update", docs/INSTALL.md "Updating"): `_version.py` = the only version
|
||||
(`frameport.__version__`; pyproject reads it via hatch `dynamic`; app log, diagnostics, User-Agent, Settings use
|
||||
@@ -277,7 +296,17 @@ Rick and Morty runs on the Frame via its catalog recipe (OpenVR, no Revive),
|
||||
by hand: the "Update now" button path (same apply(), called from the running app). macOS: CI smoke only.
|
||||
- `agent/frameport_agent.py` — runs **on the Frame** (python3 stdlib only), JSON over SSH. Owns the install layout,
|
||||
launch.sh template, Steam shortcuts (binary VDF), launch tests. Bump `AGENT_VERSION` when changing it.
|
||||
- `bootstrap/bootstrap.sh` — one-time Frame setup served by the pairing server (sshd, app key, avahi service, Lepton).
|
||||
- `bootstrap/bootstrap.sh` — one-time Frame setup served by the pairing server (app key, podman fix, Developer Mode, Lepton).
|
||||
`bootstrap/setup.sh` — the setup URL (`curl -fsSL https://frameport.app/s | bash`, copied to the site by
|
||||
`site/scripts/sync-media.mjs`): finds the PC (stdlib mDNS on 5353 for `_frameport-pair._tcp`, which the pairing
|
||||
server announces while open; else the USB address and a /24 scan of `/ping`), `/hello` → the user clicks Allow (4
|
||||
digits on both sides, `ask_digits`) → `/wait` hands over the code → runs bootstrap.sh exactly like the typed line.
|
||||
Checked on the dev Frame 2026-10-10 (mDNS and scan paths) with a stub bootstrap; tests/test_setup_url.py.
|
||||
Auto-pairing (`frame/autopair.py`, `app._start_auto_pair`, setting `frame.auto_pair`, default on; off with
|
||||
FRAMEPORT_HOME/FRAMEPORT_NO_AUTO_PAIR): while no Frame is connected, Frames in Developer Mode (devkit mDNS, else a
|
||||
30 s scan of the /24s for :32000/login-name, deduped by host key) are connected if FramePort's key works, else
|
||||
offered the key via devkit.register every 4 s (refused at once until Pair new host is open; 45 s back-off after an
|
||||
unanswered request). This WSL dev PC never hears the Frame's mDNS (Hyper-V firewall): the scan finds it.
|
||||
- `catalog/games/<package>.yaml` (installed apps also fetch these from GitHub `main`, see "Catalog updates") — 38 recipes (34 verified 2026-09-28; Deadpool VR, 4XVR, NEX Player and AC Nexus's
|
||||
90 Hz default confirmed later by the owner); `catalog/triage.yaml` — log signatures → fixes.
|
||||
- `native/` — sources of the prebuilt binaries in `artifacts/` (adapter, VrApi bridge patches, GL shim, stubs).
|
||||
@@ -298,8 +327,17 @@ Games tests: `pytest -m games` (FRAMEPORT_GAMES=<downloads dir>); on-device chec
|
||||
native layer test: `FRAMEPORT_NATIVE_TESTS=1 pytest -m native` (compiles with the NDK, ~2 min on NTFS).
|
||||
Repo is on an NTFS drive (`core.fileMode=false`); line endings are LF (`.gitattributes`).
|
||||
- `FRAMEPORT_HOME=<dir>` isolates all app data (tests use it); `FRAMEPORT_JAVA/_OVERPORT_JAR/_APKSIGNER_JAR` override
|
||||
managed tools. Real app data (WSL): `~/.local/share/frameport` (tools, overport workspace + game keystores, library,
|
||||
artwork, frames.json, the app's SSH key which the dev Frame authorizes).
|
||||
managed tools. **The owner's real app data is the Windows copy's: `%LOCALAPPDATA%\FramePort`** (= `/mnt/c/Users/
|
||||
<user>/AppData/Local/FramePort`; tools, overport workspace + game keystores, library, artwork, frames.json, the app's
|
||||
SSH key which the dev Frame authorizes; moved from WSL's `~/.local/share/frameport` on 2026-10-11, which is now only
|
||||
a stale backup). The desktop shortcut "FramePort (dev)" runs the source checkout natively on Windows
|
||||
(`%LOCALAPPDATA%\FramePort-dev\launch-frameport-dev.vbs`: venv `FramePort-dev\venv` via `UV_PROJECT_ENVIRONMENT`,
|
||||
`uv sync --frozen`, log `FramePort\logs\dev-gui.log`). **WSL runs on that data need `FRAMEPORT_WINDOWS_DATA=1`**
|
||||
(set in `.claude/settings.local.json`; `paths.shares_windows_data`): the data folder becomes the Windows one, saved
|
||||
paths from the other side are translated on load (`paths.localize`: /mnt/d/X <-> D:\X in library.json, tools.json,
|
||||
PC deployment records), Java/Cpp2IL keep one copy per OS (tools.json `java-<os>-<arch>`), and the link handler isn't
|
||||
registered (it would point the Windows copy's links at WSL). Don't run a WSL GUI and the Windows GUI at once
|
||||
(the gui.lock flock/msvcrt locks don't see each other; only the gui.alive heartbeat does).
|
||||
- Device checks: `frameport test <pkg>` / `frameport parity-device --results <parity.json> --baseline <launch.txt>
|
||||
[--test-only]`; the pre-FramePort baseline is `PATCHED/_known-good-2026-09-28/_frame-state/baseline-launch.txt`.
|
||||
- Docs screenshots + videos (docs/SHOWCASE.md): never by hand. `uv run python scripts/showcase/render_docs.py`
|
||||
@@ -315,7 +353,7 @@ Repo is on an NTFS drive (`core.fileMode=false`); line endings are LF (`.gitattr
|
||||
give new icon-only controls a tooltip. The `showcase` workflow renders on UI pushes to main (+ the videos whose
|
||||
storyboard changed, or the ones named on dispatch) and opens PR `showcase/update`; releases attach every video as
|
||||
`FramePort-<name>.mp4`. `scripts/scrub_library.py` (the owner's own library) is only for private screenshots now.
|
||||
README rules (owner): states the project is a proof of concept, provides no piracy tools, credits the wrapped
|
||||
README rules (owner): says not every game runs (no longer "proof of concept" since 1.0), provides no piracy tools, credits the wrapped
|
||||
projects (most functionality is theirs); neutral technical wording; no Quest2Frame mentions anywhere.
|
||||
- GUI smoke test: `uv pip install flet-web playwright && playwright install chromium`, then
|
||||
`FRAMEPORT_HOME=<test dir> python scripts/ui_smoke.py --out <dir> [--game <pkg>] [--frame steamos@<host>] [--update]` (`--update` = fake release: banner, dialog, Settings → Updates) and look
|
||||
@@ -327,6 +365,11 @@ Repo is on an NTFS drive (`core.fileMode=false`); line endings are LF (`.gitattr
|
||||
`T.px(n)`, Material defaults through the theme's text_theme; never hardcode a bare pixel number in ui/.
|
||||
`scripts/ui_smoke.py --scale 1.5 --viewport 2560x1440` renders it. Flet draws a grey box for invalid layouts (e.g.
|
||||
an `expand` child in a `wrap=True` Row).
|
||||
- One GUI per data folder (`core/instance.py`, 2026-10-11): `<data>/gui.lock` (flock/msvcrt, pid inside); a second
|
||||
start asks the open window to come to the front (`gui.focus`, read by `_watch_links`) and exits; a lock held with no
|
||||
window (no `gui.alive`) and older than 30 s is a leftover FramePort process and is ended. `main()` ends the process
|
||||
(`os._exit`) once `ft.run` returns: background threads (re-analysis, session triage) used to keep a windowless
|
||||
process running. Session triage shows one pop-up for all games of a burst (`_announce_sessions`).
|
||||
- Stopping the GUI: `pkill -f` patterns match your own shell — use `pgrep -f "[b]in/frameport-gui|[f]let-desktop-light"`.
|
||||
|
||||
## Round-2 polish (2026-10-02)
|
||||
@@ -604,7 +647,10 @@ shortcuts.vdf and writes once more if Steam put its old copy back (`shortcuts_lo
|
||||
auto-repair (`_add_then_play`) crashed (its job got a Job, not a reporter). Uploads resume on a transient OSError.
|
||||
Lepton's Android 11 has no clipboard service (134 services; checked with `podman exec … service check clipboard`):
|
||||
SDL/LÖVE apps crashed at start → `frame.sdl_clipboard` (`apk/dex.py`: in-place dex edit, nops one invoke, fixes
|
||||
the header checksum/signature; verified with LÖVE for Android 11.5, GitHub #24 Dramatic Shape). 2D apps (vr_kind none)
|
||||
the header checksum/signature; verified with LÖVE for Android 11.5, GitHub #24 Dramatic Shape). Godot 4.2-4.4 (Kotlin `as ClipboardManager`,
|
||||
GitHub #134 EndoparasiticVR) → `frame.godot_clipboard` (nops the `Intrinsics.checkNotNull` before that check-cast; Godot's
|
||||
clipboard methods return at once; analysis `godot_clipboard`, ANALYSIS_VERSION 10; checked on real godot-lib/templates
|
||||
4.2.2-4.4.1, not on the device; 4.1 and 4.5+ don't throw). 2D apps (vr_kind none)
|
||||
keep every suggested patch that isn't about VR (`needs_vr = False`), not a fixed list. FramePort on the Frame:
|
||||
`frame/local.py` (127.0.0.1, own key authorized; "This Frame (experimental)"; app data in
|
||||
~/.local/share/frameport-app because ~/.local/share/frameport is the agent's) and Steam library changes wait while
|
||||
@@ -694,6 +740,8 @@ FrameBridge `snapshot=N` (`snapshot_gl.c`, GLES): every N s the left-eye image t
|
||||
own context and saved as `files/fb_snap_0-7.ppm` (quarter size) — headless launches show a black headset view, this
|
||||
shows what the game draws. Vader: Lucasfilm logo (an OBB mp4: video works), then its loading card (portrait + segmented
|
||||
bar) that never advances; the async loader thread sleeps and OBB reads stop (~168 MB of a 2.7 GB pak).
|
||||
**SteamVR overlay apps (agent v75, 2026-10-10):** host OpenVR overlays (VRApplication_Overlay) are composited over Lepton (Quest) games on the Frame (probe checker seen over 4XVR in the headset view, `native/vroverlay_probe`); Windows ones register through Proton's vrclient too (Temporal Reality's Windows build created its overlays; fpsVR never reaches OpenVR under Proton). Detection `analysis/vroverlay.py` (bundled `.vrmanifest` with `is_dashboard_overlay`, else OpenVR + IVROverlay + self-registration markers, engine Other; Rift fingerprint `RIFT_ANALYSIS`), patch `pcvr.vr_overlay` (param autostart), Linux entry fields `vr_overlay`/`vr_overlay_autostart` (game page switch); agent `register_vr_overlay`/`unregister_vr_overlay`/`launch_vr_overlay` (IVRApplications via ctypes, live; manifest `<anchor>/frameport-overlay.vrmanifest`, the app's own key, binary = launch.sh). **SteamVR on the Frame reads only `binary_path_linux_arm`** (binary_path_linux alone is skipped). Overlay apps: no launch test, no Linux Steam-parent watchdog (`FRAMEPORT_OVERLAY`), not a "running game". A Steam overlay-app shortcut and a game shortcut run together. SteamVR's SetApplicationAutoLaunch didn't persist on the dev Frame (the watch never started by itself) → **agent v76: FramePort's own autostart**: the deployment's `overlay.autostart` is the source of truth; `ensure_host_fixes` (`ensure_overlay_service`) keeps the user service `frameport-vr-overlays.service` (`_vr_overlay_watch`, 5 s polls of the known vrserver's /proc stat) only while an overlay app has autostart on (register/unregister, uninstall, purge and the kill switch `~/.local/share/frameport/vr-overlays.disabled` / `FRAMEPORT_NO_OVERLAY_AUTOSTART` update it); a new vrserver (pid + start time, kept in `~/.cache/frameport-vr-overlays.json` across agent updates) → child `_vr_overlay_round`: wait for IVRApplications, 15 s grace, `launch_vr_overlay` for apps not running (folder process or GetApplicationProcessId); log `~/.local/share/frameport/vr-overlays.log`. Dev Frame: adopted the running vrserver, skipped the already running watch; a start after a real SteamVR start/boot is not yet seen. Details: docs/FRAME_RUNTIME.md "Overlay apps".
|
||||
**Lepton's logcat mirror dies (agent v67, 2026-10-09):** occasionally launch.log ends with `logcat: Unexpected EOF!` right after the game starts (about 1 launch in 50, Lepton 2.8.14 and 3.0.5): no dashboard auto-hide, no launch-test result. launch.sh's `_logcat_keeper` then reads `podman exec lepton-steamlaunch-<appid> logcat` itself into launch.log (up to 5 restarts while the game runs).
|
||||
**Double launch (agent v57, 2026-10-06):** a second Play while Lepton still boots (~10 s with nothing to see) made
|
||||
the second Lepton stop the first one's container ("Waiting for steamlaunch-<appid> (PID …) to exit", exit 137
|
||||
"(starting)", "Clearing baked app data due to early exit") and both died (Vader, BattleSisters). launch.sh now takes
|
||||
@@ -721,6 +769,99 @@ head-pose deadline ends VR mode without a worn headset (not a game bug).
|
||||
|
||||
**Language packs (merged from PR #20, 2026-10-04):** overport's `libovrplatformloader.so` is a dispatcher that `dlopen`s Meta's own loader (`libovrplatformloader_meta.so` / `_meta_q1.so`, also `libpxrplatformloader.so`) and keeps its own message queue; `ovr_LanguagePack_GetCurrent/SetCurrent` are 8-byte `return 0` stubs in it, `ovr_AssetFile_GetList` forwards to Meta's loader. `frame.langpacks` (opt-in, `native/langpack`) serves `<tag>.lang` files from the game's data; `elf.hide_exports` marks the loader's exports STB_LOCAL (bionic and glibc only match GLOBAL/WEAK; verified for glibc, bionic's `is_symbol_global_and_defined` is from memory). `libfp_langpack.so` is built here (`python native/build.py --only langpack`) and committed. Owner-verified 2026-10-04: Deadpool VR with only `en.lang` and the patch on plays English dialogue and runs normally (headless launch tests show 2-6 fps while it loads: not a regression sign). A dispatcher answer that arrives after we answered the timed-out GetList is dropped (one answer per request); games already in a library get `lang_packs` filled after an app update (`library._refresh_data_fields`). The library logs to logcat under the tag `fp_langpack` (dirs looked in, packs found, every language-pack call), so it shows up in the game's `launch.log`; an `ovr_AssetFile_GetList` the dispatcher leaves unanswered for 1.5 s is answered with our packs alone. Deadpool VR (Unreal) accepts a pack only when its `Metadata` equals the game's own version string (`ULanguagePacksSubsystem` compares it with `%s.%s.%s.%s.%s` built from the build info, e.g. `1.0.40.356975.Quest` = versionName; found by disassembling `libUE4.so`): the patch writes the APK's versionName into the library (`@FPMETA@` slot, `with_metadata`), `FRAMEPORT_LANGPACK_META` overrides it. Deadpool VR (2026-10-04, headset): German became selectable with that Metadata, but dialogue stayed silent (even English once reported as a pack) while the path was spelled `/sdcard/Android/obb/<pkg>/x.lang`; with the `/storage/emulated/0/Android/obb/<pkg>/x.lang` spelling (Unreal's own, now listed first in `scan_dirs`) German dialogue plays. `FRAMEPORT_LANGPACK_SKIP=<tags>` or a file `fp_langpack_skip` in the obb folder leaves packs out of the list (experiments).
|
||||
|
||||
**Cube swapchains (GitHub #107, 2026-10-09):** the Frame's runtime refuses `faceCount=6` swapchains (-2,
|
||||
XR_ERROR_RUNTIME_FAILURE); OVRPlugin ignores that ("CreateSwapchain for eye 0: 0x0, 0 stages") and crashes in
|
||||
ovrp_EndFrame4 (memset). FrameBridge `cube_standin` (default on, `native/adapter/cube_standin.c`) serves a refused cube
|
||||
swapchain as one GL cube-map texture in the app's context (GLES only) and drops its layers. Verified headless with
|
||||
Budget Cuts Ultimate (2048² sRGB, 12 mips; runs on at ~70 fps); what the cube layer showed is simply missing.
|
||||
**Hardware video decoding (PR #96, PR #128 by Lucas-Mathieu, adapted 2026-10-10, agent v71):** `native/hevc` = one
|
||||
OMX plugin `OMX.frameport.{avc,hevc,vp9}.decoder` (FFmpeg v4l2m2m on Iris /dev/video-dec0, FFmpeg software decoders as
|
||||
fallback inside the component, Vulkan copy for big native surfaces). The connection installs it once per Frame
|
||||
(`ensure_video_codec` → agent `video_codec_status`/`install_video_codec`: `~/.local/share/frameport/video-codec/versions/
|
||||
<manifest sha>` + `current` symlink). A Frame keeps the same or a newer `revision` (no flip-flop between PCs) → **bump
|
||||
`revision` in native/hevc/build.py with every artifact change**; a failed install isn't retried during the connection.
|
||||
Per game: only launchers of games whose recipe has `frame.hw_video_decode` (install stage, no APK change; suggested
|
||||
from analysis `media_codec`, ANALYSIS_VERSION 8; deployment.json `hw_video_decode`) get the codec line → the Podman
|
||||
wrapper (native/hevc/podman.py) mounts the plugin into that container (merged media_codecs.xml in
|
||||
$XDG_RUNTIME_DIR/frameport-video). Off for every game: Settings → Installing (library `video.hw_decode` → agent
|
||||
`video_codec_switch` = video-codec/disabled); one game: `FRAMEPORT_NO_HW_VIDEO=1 %command%`. `upgrade_launchers`
|
||||
converts agent ≤70 launchers (Batman's `<base>/frameport-codec` → keeps the line, folder then removed). Batman: catalog
|
||||
+ migration `batman_video_patches` (`frame.hw_video_decode` + hidden adapter setting `surface_native`). Iris: 8K
|
||||
refused (ENOMEM) while any other decoder session is open; SteamVR's vrlinkrunthread holds one ~12 s at game start →
|
||||
refused opens retry until 20 s after the plugin loaded (else 2 s), then software; VP9 7680x3840 never returned a
|
||||
picture → VP9 ≤4096x2304; hidden VP9 frames come back as empty capture buffers (bytesused 0, no LAST):
|
||||
FFmpeg's wrapper ended the EOS drain at the first one (two-pass VP9 lost its last ~25 frames) → build.py requeues
|
||||
them (codec revision 8; drain ends at LAST, or 1 s after a skipped empty buffer; dev Frame 2026-10-10: 600/600 =
|
||||
OMX.google.vp9 Y hashes). Rebuilds are deterministic (ext4 + NTFS path
|
||||
with spaces, two NDK copies → same; rev 8 141de01f…; PR #128's own sources → its 27a2d749…). Not yet run on the device.
|
||||
**Vulkan shader dump (GitHub #140, 2026-10-10):** adapter `vk_shader_dump=1` (Vulkan shim, `native/vkshim/shader_dump.h`;
|
||||
applies where vk_sanitize can: Unreal/Other arm64) writes each distinct SPIR-V module once to
|
||||
`files/fp_vk_shaders/<size>_<sha256>.spv` (tmp + rename) and one `index.txt` line per vkCreateShaderModule (`<seq> <ms>
|
||||
<unix ms> <name> new|known|again|failed`, O_APPEND) to find the module behind a GPU hang. Agent v72 `collect_diag`
|
||||
returns `shaders` (newest modules of fp_vk_shaders + fp_spirv, ≤4 MB each, base64) → bundle
|
||||
`games/<pkg>/target/shaders/`. Triage `gpu-hang` suggests both dumps; `triage.graphics_api` (FrameBridge's
|
||||
xrCreateSwapchain formats: <0x1000 Vulkan, else GL) keeps only the session's API's (`API_ONLY`). Host-tested only.
|
||||
Agent v74: a reporter's hang came ~10 s after the last new module, so the culprit wasn't among the newest written →
|
||||
with an index naming modules, `shader_dumps` sends every module of the index's **newest session** (after the last
|
||||
`# start`), deduplicated, last used first, each ≤4 MB, ≤30 MB in all (`session` = how many it names) + that session's
|
||||
index lines; no index (fp_spirv) = the old newest-by-mtime 4 MB. One JSON line (~40 MB base64) over the SSH channel
|
||||
is read whole by `Frame.agent` (fine at that size); `_Writer.fit` drops the least recently used modules last, after
|
||||
cutting logs, to stay under the 24 MB zip limit.
|
||||
**Unreal OBB check (GitHub #159, 2026-10-10):** Epic's DownloaderActivity checks OBB name+size (OBBData), then with `bVerifyOBBOnStartUp=true` CRCs the whole OBB; its screens are invisible in Lepton (stuck at `Displayed …/.DownloaderActivity`, triage `unreal-obb-check-stuck`) → `frame.unreal_skip_obb_check` (analysis `unreal_verify_obb`, ANALYSIS_VERSION 11) flips the manifest boolean (`axml.set_meta_data_bool`); skips only the CRC pass. Host-tested only.
|
||||
**Meta user id (GitHub #182, 2026-10-11):** OVRPort's platform loader (runtimes 3.4.3-23204ea and -aa54c3f) answers
|
||||
`ovr_GetLoggedInUserID` (and two other callers) without a Meta account with a made-up id from a helper (`stp; mov x29,
|
||||
sp; bl random; …; mov/movk x9 = 0x53444835ec580000 (6e18); add x0, x8, x9; ldp; ret`), kept only in memory → a new id
|
||||
every start; Walkabout Mini Golf files saves under `Profiles/Oculus/<id>/` and lost all progress. `frame.ovr_stable_user`
|
||||
(default_on, detect = the game has libovrplatformloader.so) rewrites the helper to `mov x0, <6e18 + 32 bits of
|
||||
sha256(package, keystore)>; ret`: stable per game and per user (keystores are personal), so online games never see two
|
||||
FramePort users as one. Found in every PATCHED build checked. Host-tested only; upstream candidate.
|
||||
**Vivox API 31 (GitHub #101, 2026-10-09):** newer Vivox builds (Green Hell VR) call Android 12 AudioManager
|
||||
communication-device methods from `com.vivox.sdk.AudioChangeListener` with no SDK check → NoSuchMethodError on Lepton's
|
||||
Android 11. `frame.vivox_audio_route` (analysis `vivox_api31`, ANALYSIS_VERSION 4) makes every such method return at
|
||||
once (`Dex.return_early`: return-void / `const/4 v0,0; return v0`; nopping the invoke would leave a move-result the
|
||||
verifier rejects). Older Vivox (Eleven Table Tennis, BattleSisters) lacks that code and isn't matched. Verified
|
||||
headless: Vivox initialises, 150 s at ~65-70 fps.
|
||||
**Steam Input gamepad for 2D apps (GitHub #162, agent v72, 2026-10-10):** Lepton's Android only has the Wayland seat's `wayland_touch/keyboard/pointer`. Steam makes its virtual pad (uinput, `/devices/virtual/input`, 28de:11ff, "Microsoft X-Box 360 pad N", ACL for steamos) only while the Frame's controllers are on (controller.txt "Steam Controller reserving XInput slot 0"; gone when they sleep), also with no game running; steamos-manager's 28de:0000 keys device isn't a pad (no BTN_SOUTH). Lepton's container root (system_server too) = steamos (`keep-id:uid=0`), so a bind-mounted `/dev/input/eventN` opens. Opt-in `device.steam_gamepad` (vr_kind none; suggested for `android.hardware.gamepad`/`LEANBACK_LAUNCHER`, analysis `gamepad`, ANALYSIS_VERSION 9): deployment.json `steam_gamepad` → launcher `GAMEPAD_LINE` (after the codec line: exports FRAMEPORT_GAMEPAD + LEPTON_ENV_SDL_GAMECONTROLLER_ALLOW_STEAM_VIRTUAL_GAMEPAD, puts `agent/bin` first on PATH) → `PODMAN_WRAPPER` (written by ensure_host_fixes; non-`run` calls go straight on; `run` imports the agent's `podman_run_args`; hands on to the next Podman after its own PATH entry with itself removed from PATH, so it chains to the codec wrapper either way round) adds the pads + `Vendor_28de_Product_<pid>.kl` (Xbox 360 layout). Verified headless with a uinput stand-in pad (Stremio's container: EventHub `classes=0x80000141`, our key layout, KeyEvent BUTTON_A dispatched); with Steam's real pad and a game: not yet. Pads made after the start aren't seen until the next one.
|
||||
**Multiview programs on flat framebuffers (GitHub #77, Doom3Quest, 2026-10-09, prototype):** Mesa enforces OVR_multiview's
|
||||
"program num_views == draw framebuffer views" rule (`draw_validate.c`, the draw is dropped silently); Qualcomm doesn't.
|
||||
Doom3Quest compiles every VS with `layout(num_views=2) in;` and draws its HUD/PDA into 2D-texture FBOs → black. Opt-in
|
||||
`frame.gl_multiview_fbo` (analysis `gl_multiview_libs`, ANALYSIS_VERSION 6; own-engine GLES only): `native/glmv` =
|
||||
`libfpglmv.so` (12 chars = "libGLESv3.so": libdoom3.so's one `.rodata` dlopen string is rewritten in place, its qgl*
|
||||
table comes from dlsym on that handle; also first DT_NEEDED for its direct gl*/egl* imports). Such draws use a lazily
|
||||
built single-view twin (view 0, uniforms copied per draw); eglMakeCurrent resets the per-thread cache. Host-tested only
|
||||
(rewriter on all 19 Doom3Quest shaders + glmv.c against a stand-in GL, `tests/test_gl_multiview_fbo.py`); the host's
|
||||
Mesa 23.2 llvmpipe has no OVR_multiview. Headset-verified by Xandrix1987 (2026-10-09: HUD/PDA shown, no fps drop) -> in the Doom3Quest catalog recipe.
|
||||
**BlazeRush (GitHub #57, Klownicle's verified guide, 2026-10-10; FramePort's version headless-tested only):**
|
||||
`frame.blazerush` (1.0.349 only: exec-segment SHA + every word checked, offsets from PT_LOAD) NOPs the avatar-readiness
|
||||
wait (0x4b2dbc), RETs the avatar renderer (0x4b2adc), renames `cache/shader_cache` → `cache/br_shader_v1`. GL shim
|
||||
(now also exports glShaderSource/glLinkProgram/glProgramBinary/glDeleteProgram/glDraw*: pass-through unless a setting
|
||||
is on) `gl_int_attribs` (integer inputs fed via glVertexAttribPointer re-set with glVertexAttribIPointer per draw,
|
||||
reflection cached per EGL context + program, dropped on link/binary/delete) and `gl_highp_markers` (vertex shaders with
|
||||
every '|' marker: mediump→highp except on in/out/uniform lines); host test `tests/test_blazerush.py` with a stand-in
|
||||
GL. FOV: the game reads VrApi props 7/8 right after vrapi_Initialize (no session; the Frame has no
|
||||
XR_EPIC_view_configuration_fov) → the bridge saves the live FOV (also located before tracking, headless: 118.87×120.04
|
||||
on the dev Frame) in `files/framebridge-vrapi-fov.txt` and answers with it at the next start (log `FOV properties: …
|
||||
(saved by an earlier session)`). `device.config_sync` (catalog field `config_sync`, agent v78): user_config.xml
|
||||
r_3dwidth/height, r_width/height from the runtime eye size (FrameBridge logs `runtime recommended eye size`) × scale,
|
||||
written at finalize, set_settings and by launch.sh before Lepton starts (`_config_sync`). Headless runs at scale 1.7
|
||||
end in `zink: DEVICE LOST` right after the bridge's 30 s no-tracking deadline tears the session down (gpu fault at the
|
||||
teardown; not at scale 1.0): a headless-only artifact, the headset never hits that deadline. Launch tests count an exit/crash after
|
||||
such a line as a pass (triage.yaml `headless_end`, finding `headless-end`; GitHub #57 "Failed to launch" on install).
|
||||
**Session triage (agent v70, 2026-10-09):** launch tests never reach FOCUSED, so real play sessions are triaged
|
||||
too. list_installed gives each game `last_play` {start, end, test} from `<anchor>/plays.log` (launch tests write a
|
||||
`test <unix>` line first → test sessions are only marked); `session_log` returns the newest session's launch.log
|
||||
(`<base>/session.log`, ≤4 MB: head + tail + FrameBridge/crash lines between) + that session's logcat-crash.log +
|
||||
`journalctl -k` GPU lines ("kernel: …"). The GUI's connection refresh (`app._check_sessions` → `pipeline.sessions_due`,
|
||||
background thread; sessions >7 days old are skipped) runs `pipeline.triage_session` = `validate/session.analyze`
|
||||
(triage.yaml signatures incl. `space-warp-used` (info, `question:`, never auto-applied), `gpu-hang` (`report: true`)
|
||||
+ computed `slow-frames` (pacing windows vs the nearest refresh rate) and `focus-dips` (FrameBridge always logs
|
||||
`focus: lost` / `focus: back after N ms`, session_fixes.c)) → library `last_session` / `last_session_checked` → game
|
||||
page "Last session" callout (Try this fix / Rebuild with this fix / Yes-No question / Report / Dismiss). Suggestions may
|
||||
be values (`adapter.scale=0.85`, `triage.split_suggestion`); adapter-only fixes are pushed live (`apply_suggestions_live`
|
||||
→ agent set_settings, like the Game settings dialog). CLI `frameport session <pkg> [--apply]`. PC VR (Proton) launchers
|
||||
log no session end, so they aren't triaged yet. pac_hints stays triage-only: a survey of all 68 dump APKs found unpaired
|
||||
PAC hints in 21 libraries of 18 games (OpenSSL's 38/40 in most UE4 libUE4.so and libEOSSDK.so, UE5 libUnreal.so ~400
|
||||
vs +2-3, libass, libopencv, …), most of them games that work, so default-on would rewrite many working builds.
|
||||
**Unresolved (as of 2026-09-28):** Arcsmith (right-eye distortion) and Time Stall (both eyes) — swap, tracking, Valve
|
||||
layers, depth, pacing ruled out. Sniper Elite VR (DEVICE LOST), Espire 1 (Mesa GL upload crash), HITMAN 3 (freedreno
|
||||
crash): use PC versions.
|
||||
@@ -758,6 +899,10 @@ up to 4 s for the link file to be taken before trusting the heartbeat, the windo
|
||||
`gui.alive`, and the Windows command runs under `conhost.exe --headless` (plain `-WindowStyle Hidden` flashed a
|
||||
console). Screens: `scripts/ui_smoke.py --links --fake-frame --game <pkg>` (tall pages: `--viewport
|
||||
1280x7000`; Flutter's popup menu ignores Escape).
|
||||
Demo link: the homepage's example button = `deeplink.DEMO_MANIFEST` (https://frameport.app/demo/cool-game.json, published
|
||||
from site/public/demo/); every form (https button page, frameport://, framedrop://, pasted) → `InstallRequest.demo`,
|
||||
no network (fetch_manifest returns `demo_manifest()`, download refuses), `link_dialog.show_demo` (Install =
|
||||
`easter.demo_install`: Cool Game's cover out of the portal; eggs off → "Nice, it works!"), CLI just says so.
|
||||
|
||||
## Releases, CI, GitHub
|
||||
Maintainer-only notes (accounts, credentials, key locations) live in the git-ignored `CLAUDE.local.md`.
|
||||
@@ -848,5 +993,7 @@ the unresolved eye distortion, and testing the release bundles on real Windows/m
|
||||
fallback (`core/cache.py`). Don't hardcode what can be discovered (e.g. Lepton path via appmanifests).
|
||||
- Keep APK edits minimal and byte-stable (parity depends on it). Every new fix: a patch module + a triage signature +
|
||||
a PLAYBOOK row + a unit test.
|
||||
- New or changed patch, game setting, catalog recipe or triage signature → run `python scripts/patch_docs.py`
|
||||
(regenerates docs/PATCHES.md + site/src/data/patches.json, the site's id → anchor map; a test runs `--check`).
|
||||
- Known-good backups of every working APK and the signing keys: `PATCHED/_known-good-2026-09-28/` (don't delete).
|
||||
- Your development Frame: `frameport frame info` (remembered in `<user data>/frames.json`); SSH as `steamos@<frame>`.
|
||||
@@ -0,0 +1,365 @@
|
||||
---
|
||||
name: FramePort
|
||||
description: Quest games, Android apps, Linux apps and PC VR games on the Valve Steam Frame, set up and installed from your PC.
|
||||
colors:
|
||||
frame-orange: "#FF8A1F"
|
||||
frame-orange-soft: "#3A2512"
|
||||
ember-ink: "#160C03"
|
||||
pc-blue: "#3AA8FF"
|
||||
pc-blue-soft: "#0F2A40"
|
||||
void: "#0D0E12"
|
||||
sidebar-night: "#111217"
|
||||
panel: "#16181E"
|
||||
panel-raised: "#1D1F27"
|
||||
panel-well: "#252832"
|
||||
hairline: "#2A2D37"
|
||||
hairline-strong: "#393D49"
|
||||
text-bright: "#EDEEF2"
|
||||
text-dim: "#A7ABB7"
|
||||
text-faint: "#717583"
|
||||
ok-green: "#3DD68C"
|
||||
warn-amber: "#F2C94C"
|
||||
error-coral: "#FF6166"
|
||||
info-cyan: "#5CD0E6"
|
||||
typography:
|
||||
display:
|
||||
fontFamily: "Sora, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif"
|
||||
fontSize: "clamp(30px, 4.2vw, 44px)"
|
||||
fontWeight: 800
|
||||
lineHeight: 1.1
|
||||
letterSpacing: "-0.02em"
|
||||
headline:
|
||||
fontFamily: "Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif"
|
||||
fontSize: "28px"
|
||||
fontWeight: 700
|
||||
lineHeight: 1.2
|
||||
title:
|
||||
fontFamily: "Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif"
|
||||
fontSize: "16px"
|
||||
fontWeight: 600
|
||||
lineHeight: 1.3
|
||||
body:
|
||||
fontFamily: "Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif"
|
||||
fontSize: "13px"
|
||||
fontWeight: 400
|
||||
lineHeight: 1.5
|
||||
label:
|
||||
fontFamily: "Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif"
|
||||
fontSize: "11px"
|
||||
fontWeight: 600
|
||||
lineHeight: 1.3
|
||||
section-label:
|
||||
fontFamily: "ui-monospace, Cascadia Mono, SF Mono, Menlo, Consolas, monospace"
|
||||
fontSize: "12px"
|
||||
fontWeight: 600
|
||||
lineHeight: 1
|
||||
letterSpacing: "0.14em"
|
||||
rounded:
|
||||
xs: "6px"
|
||||
sm: "8px"
|
||||
md: "12px"
|
||||
pill: "20px"
|
||||
spacing:
|
||||
s1: "4px"
|
||||
s2: "8px"
|
||||
s3: "12px"
|
||||
s4: "16px"
|
||||
s5: "24px"
|
||||
s6: "32px"
|
||||
components:
|
||||
button-primary:
|
||||
backgroundColor: "{colors.frame-orange}"
|
||||
textColor: "{colors.ember-ink}"
|
||||
typography: "{typography.title}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: "12px 16px"
|
||||
button-primary-big:
|
||||
backgroundColor: "{colors.frame-orange}"
|
||||
textColor: "{colors.ember-ink}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: "18px 22px"
|
||||
button-primary-disabled:
|
||||
backgroundColor: "{colors.panel-well}"
|
||||
textColor: "{colors.text-faint}"
|
||||
rounded: "{rounded.sm}"
|
||||
button-secondary:
|
||||
backgroundColor: "transparent"
|
||||
textColor: "{colors.text-bright}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: "12px 14px"
|
||||
button-ghost:
|
||||
backgroundColor: "transparent"
|
||||
textColor: "{colors.text-dim}"
|
||||
rounded: "{rounded.sm}"
|
||||
button-danger:
|
||||
backgroundColor: "{colors.error-coral}"
|
||||
textColor: "{colors.ember-ink}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: "12px 16px"
|
||||
input:
|
||||
backgroundColor: "{colors.panel-well}"
|
||||
textColor: "{colors.text-bright}"
|
||||
typography: "{typography.body}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: "10px 12px"
|
||||
pill:
|
||||
textColor: "{colors.text-dim}"
|
||||
typography: "{typography.label}"
|
||||
rounded: "{rounded.pill}"
|
||||
padding: "3px 9px 3px 8px"
|
||||
card:
|
||||
backgroundColor: "{colors.panel}"
|
||||
textColor: "{colors.text-bright}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: "16px"
|
||||
callout:
|
||||
textColor: "{colors.text-bright}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: "12px 14px"
|
||||
nav-item-selected:
|
||||
textColor: "{colors.text-bright}"
|
||||
rounded: "{rounded.sm}"
|
||||
tooltip:
|
||||
backgroundColor: "{colors.panel-well}"
|
||||
textColor: "{colors.text-bright}"
|
||||
rounded: "{rounded.xs}"
|
||||
padding: "8px 12px"
|
||||
---
|
||||
|
||||
# Design System: FramePort
|
||||
|
||||
## Overview
|
||||
|
||||
**Creative North Star: "The Portal Between"**
|
||||
|
||||
FramePort is about crossing. On one side is the PC, in blue; on the other is the Frame, in orange. Between them is the
|
||||
seam where a game passes through: a horizontal gradient from blue to orange, the same one the logo's tilted portal
|
||||
makes. Everything else is dark space, so the crossing is what glows. The desktop app and frameport.app share this
|
||||
system with the same color values. The site states it outright ("FramePort Portal, for the web").
|
||||
|
||||
Surfaces are flat, dark, tonal steps a few lightness points apart, with hairline borders, and the chrome stays quiet.
|
||||
Color comes from three places only: cover art, the orange that marks the one thing to do now, and the blue portal
|
||||
touches that show where you are or what's moving. Density is comfortable desktop density, built for a mouse and a
|
||||
window you resize. The interface scales as a whole through one scale factor, never by reflowing into a phone layout.
|
||||
|
||||
Components are tactile and confident. A cover lifts and its art zooms when pointed at. A button settles when pressed.
|
||||
On the website, motion is kept for things that cross: the hero's package-to-game crossing, the install log turning
|
||||
from PC rows into Frame rows, the download going through the portal. Nothing moves just because the pointer or the
|
||||
scroll passed by (no cursor light, no word-by-word headings, no scroll fade-ups).
|
||||
Play sends two rings out of the button, blue then orange. A new view fades in. Every one of these motions disappears
|
||||
under Reduce motion, and the static state must still read on its own.
|
||||
|
||||
**Key Characteristics:**
|
||||
- Dark only: a near-black void (`void`) with three tonal panel steps above it.
|
||||
- Orange is the action, blue is the place: the portal dichotomy is the color doctrine.
|
||||
- The blue→orange gradient is the system's only decoration.
|
||||
- Artwork carries the color; the chrome stays neutral.
|
||||
- Flat at rest. Artwork and floating layers lift.
|
||||
- Motion is physical (lift, spring, ripple) and always optional.
|
||||
|
||||
## Colors
|
||||
|
||||
A cool, near-black neutral ramp with two saturated poles, the orange Frame and the blue PC, plus four status colors
|
||||
tuned so none of them reads as the accent.
|
||||
|
||||
### Primary
|
||||
- **Frame Orange** (`frame-orange`): the main action of a view (Install on Frame, Play on Frame, Add games), the
|
||||
current selection in single-color themes, text cursors, focused input borders and links. In the wordmark it is
|
||||
"Frame".
|
||||
- **Frame Orange Soft** (`frame-orange-soft`): the tinted backing for orange in small areas, such as selected rows in
|
||||
the `original` theme and the accent wash behind the portal scene.
|
||||
- **Ember Ink** (`ember-ink`): text and glyphs on orange (and on the coral danger button). Never pure black and never
|
||||
white.
|
||||
|
||||
### Secondary
|
||||
- **PC Blue** (`pc-blue`): secondary (outlined) buttons, progress, switch tracks, anything "on this PC" or PC VR, and
|
||||
"Port" in the wordmark. Outlines use it at 55 % opacity.
|
||||
- **PC Blue Soft** (`pc-blue-soft`): tinted backing for blue in small areas, and the round icon wells on feature cards.
|
||||
|
||||
### Neutral
|
||||
- **Void** (`void`): the window and page background.
|
||||
- **Sidebar Night** (`sidebar-night`): the sidebar and table headers, one step above the void.
|
||||
- **Panel** (`panel`): cards, sections and the game page's info blocks.
|
||||
- **Panel Raised** (`panel-raised`): hover, menus, code blocks and popovers.
|
||||
- **Panel Well** (`panel-well`): inputs, chips, tooltips and disabled buttons.
|
||||
- **Hairline** (`hairline`) / **Hairline Strong** (`hairline-strong`): 1px card borders, dividers, menu and popover
|
||||
outlines.
|
||||
- **Text Bright** (`text-bright`), **Text Dim** (`text-dim`), **Text Faint** (`text-faint`): primary text, secondary
|
||||
text and meta / disabled text. The website uses a lighter Text Faint (#868a96): the app's value is below 4.5:1 for
|
||||
the small print the site carries (timestamps, captions). Placeholders use Text Dim there, never the browser gray.
|
||||
|
||||
### Status
|
||||
- **OK Green** (`ok-green`): Works, Connected, Installed, the battery ring, passed checks.
|
||||
- **Warn Amber** (`warn-amber`): Works with issues, warnings. Deliberately yellow, so it never reads as Frame Orange.
|
||||
- **Error Coral** (`error-coral`): failures and destructive actions.
|
||||
- **Info Cyan** (`info-cyan`): info callouts and a second data series next to PC Blue in Monitor charts.
|
||||
|
||||
### Named Rules
|
||||
**The Portal Rule.** Orange is the Frame and the action; blue is the PC and the secondary. Don't swap them, and don't
|
||||
use a third hue for an action.
|
||||
|
||||
**The One Orange Rule.** A view has one filled orange button: the thing to do now. Other actions are blue outlines or
|
||||
ghosts.
|
||||
|
||||
**The Seam Rule.** The blue→orange gradient (`--portal`, `C.portal_gradient`) is the only decoration. It always runs
|
||||
left to right (or top to bottom), blue first. It appears on the sidebar edge, the selected nav item and segment, the
|
||||
wordmark, section-label hairlines, link underlines on the site and progress fills. Dual themes only.
|
||||
|
||||
**The Dark-Only Rule.** No light mode. Installed theme files whose background is too light, or whose text falls below
|
||||
4.5:1 on a panel, are refused.
|
||||
|
||||
## Typography
|
||||
|
||||
**Display Font:** Sora 600–800 (website only; self-hosted variable woff2), falling back to Segoe UI / Roboto
|
||||
**Body Font:** the platform sans (Segoe UI on Windows, Roboto via Flutter, Helvetica Neue / Arial)
|
||||
**Label/Mono Font:** ui-monospace / Cascadia Mono / SF Mono / Menlo / Consolas
|
||||
|
||||
**Character:** the app speaks in its platform's sans, so it feels like a native tool. The site adds Sora's geometric
|
||||
heavy weights for headings, which echo the rounded wordmark. Monospace appears where something is literally typed or
|
||||
counted: the setup line, logs, section labels.
|
||||
|
||||
### Hierarchy
|
||||
- **Display** (Sora 800, clamp(30px, 4.2vw, 44px), 1.1, −0.02em): site section headings and doc titles. In the app,
|
||||
empty-state and setup headings use 700 at 22px.
|
||||
- **Headline** (700, 28px): the app's page titles (Library, the game title on its hero).
|
||||
- **Title** (600, 16px): section headings in the app (Where it's installed, About this game) and card headings.
|
||||
Buttons use 600 at 13px (15px when big).
|
||||
- **Body** (400, 13px app / 16px at 1.6–1.7 on the site): running text. Site prose and leads stay at 62ch or less.
|
||||
- **Label** (600, 11–12px): pills, meta lines, counts.
|
||||
- **Section label** (mono 600, 12px, 0.14em, uppercase): site section labels followed by the portal hairline. They
|
||||
name what the visitor does there ("See what runs", "Get started"), never a number ("02 · The app" was removed: the
|
||||
order carries no information). The app's "ALL GAMES" divider is the same idea.
|
||||
|
||||
### Named Rules
|
||||
**The Scale-Token Rule.** In the app every size goes through `T.T_*` tokens or `T.px()`, so the user's UI scale
|
||||
(75–300 %) applies everywhere. A bare pixel number is a bug.
|
||||
|
||||
**The Sentence-Case Rule.** Headings and buttons use sentence case. Only the mono section labels and pill-sized
|
||||
overlays are uppercase.
|
||||
|
||||
## Layout
|
||||
|
||||
The app is a fixed-width left sidebar (navigation, then the activity card, the live Frame card and the power bar at
|
||||
the bottom) next to a scrolling content column. The sidebar ends in a 1px vertical blue→orange seam. Content uses an
|
||||
8-point spacing scale (4/8/12/16/24/32), with 24–32 between sections and 12 between a heading and its body. Pages
|
||||
start with a title row (title and count on the left, search and actions on the right, the orange action alone at the
|
||||
far right), then filter rows of segmented controls and dropdowns, then content.
|
||||
|
||||
Library covers sit in a wrapping grid of portrait cards, with a horizontal "On your Frame" shelf of landscape cards
|
||||
above. The game page opens with a full-width art hero holding breadcrumb, meta line, title, pills and the action row.
|
||||
Dialogs come in three widths (440 / 600 / 880). Selection bars sit **below** a list, never above it, so items don't
|
||||
jump during a drag.
|
||||
|
||||
The site uses a centered 1180px column with a fluid gutter (`clamp(16px, 4vw, 40px)`) and section padding of
|
||||
`clamp(56px, 7vw, 96px)`. On phones, wide doc tables and diagrams scroll sideways inside their box, never the page.
|
||||
|
||||
## Elevation & Depth
|
||||
|
||||
The system is flat at rest. Depth comes from tonal steps (void → sidebar → panel → raised → well) and 1px hairlines.
|
||||
Shadows are reserved for two things: **artwork**, which sits on the page like a physical cover, and **floating
|
||||
layers** (menus, popovers, term cards, toasts, dialogs), which have to separate from whatever is beneath them.
|
||||
Hovering a cover deepens its shadow and adds an orange glow, the one place the accent becomes light.
|
||||
|
||||
### Shadow Vocabulary
|
||||
- **Art at rest** (`box-shadow: 0 8px 22px 1px rgba(0,0,0,.55)`): cover and shelf cards, screenshots.
|
||||
- **Art hover** (`0 16px 36px 2px rgba(0,0,0,.75), 0 4px 30px -2px rgba(255,138,31,.22)`): a pointed-at cover, with
|
||||
the orange glow underneath.
|
||||
- **Float** (`0 18px 40px rgba(0,0,0,.55)`, app menus: elevation 8 on black): menus, popovers, term cards, the
|
||||
search-result bar.
|
||||
- **Portal glow** (radial: blue 10 % → orange 5 % → transparent): the soft light behind empty states and the site
|
||||
hero. Not a shadow, but the only other source of depth.
|
||||
|
||||
### Named Rules
|
||||
**The Lift-Is-Earned Rule.** A panel never gets a shadow just to look important. If it isn't artwork and it doesn't
|
||||
float over other content, it's flat with a hairline.
|
||||
|
||||
## Shapes
|
||||
|
||||
Gently rounded and consistent: 12px for cards, dialogs and the hero; 8px for buttons, inputs, callouts, thumbnails and
|
||||
code blocks; 6px for tooltips and small chips; fully round (20px+) for pills, segmented tracks, the cover buttons and
|
||||
toasts. Borders are 1px hairlines; a focused input or a site `:focus-visible` gets a 2px orange edge instead. Cover art
|
||||
is clipped inside its card, so the art can zoom on hover without spilling. The portal itself is a tilted oval (18°,
|
||||
`--tilt`), and that silhouette is used only in the logo, the wordmark and the empty-state portal scene.
|
||||
|
||||
## Components
|
||||
|
||||
### Buttons
|
||||
Tactile and confident: clear fills, a firm press.
|
||||
- **Shape:** gently rounded (8px).
|
||||
- **Primary:** Frame Orange fill, Ember Ink 600 label, 12px × 16px (18px × 22px for the hero's big Play/Install). The
|
||||
glyph switches to its `-solid` variant. Disabled turns to Panel Well with Text Faint.
|
||||
- **Hover / Press:** the site brightens 8 % on hover and settles to 0.97 on press (ease-out-quart, no overshoot). The
|
||||
app uses Material's overlay. Focus shows a 2px orange outline, 3px offset.
|
||||
- **Secondary:** transparent with a 1px blue outline at 55 %, a blue glyph, a bright label and a faint blue wash on
|
||||
hover. Single-color themes use a neutral Hairline Strong outline.
|
||||
- **Ghost:** a Text Dim label only, for Close, Cancel and inline links (orange when it's a link).
|
||||
- **Danger:** the primary's shape in Error Coral. The outlined variant is for entries that ask first; the filled one
|
||||
is the dialog's final button.
|
||||
- **Icon button:** 20px glyph in Text Dim, always with a tooltip.
|
||||
|
||||
### Pills
|
||||
- **Style:** fully round, 11px 600 label, colored text on a 12 % tint of the same color. A leading dot (site) or glyph
|
||||
(app) means the color is never the only signal.
|
||||
- **Variants:** status (Works / Works with issues / Can't run), install state (solid fill + Ember Ink, e.g. "On
|
||||
Frame"), platform (Quest / PC VR). Overlay pills on artwork sit on 62 % black.
|
||||
|
||||
### Cards / Containers
|
||||
- **Corner Style:** 12px.
|
||||
- **Background:** Panel on Void, with a 1px Hairline border.
|
||||
- **Shadow Strategy:** none; see Elevation. Cover cards are the exception.
|
||||
- **Internal Padding:** 16px.
|
||||
|
||||
### Cover cards (signature)
|
||||
The game is the hero. A portrait (grid) or landscape (shelf) cover, clipped at 12px, has a bottom fade for its title
|
||||
and status line and pills over the top-left corner. Hover lifts the card (scale 1.02, up 1.5 %, 220ms ease-out), zooms
|
||||
the art inside the clip (1.07, 600ms), fades in a radial scrim and deepens the shadow into Art hover. A frosted round
|
||||
**cover button** (10px blur, 60 % white ring) springs in (scale 0.72 → 1, ease-out-back) and fills with orange under
|
||||
the pointer. Play sends out two rings, blue then orange, and turns into a "Starting on Frame…" pill for 8 s. Reduce
|
||||
motion keeps only the shadow and scrim changes.
|
||||
|
||||
### Inputs / Fields
|
||||
- **Style:** filled Panel Well, no border at rest, 8px corners, 13px text, dense 10px × 12px padding. Search adds a
|
||||
leading magnifier.
|
||||
- **Focus:** the border appears in Frame Orange; the cursor is orange.
|
||||
- **Labels:** Text Dim. Dropdowns share the same filled look.
|
||||
|
||||
### Callouts
|
||||
A line of guidance that belongs to the page: an 18px status glyph, then text. The status color is used at 8 % for the
|
||||
fill and 35 % for the 1px border, with 8px corners. Kinds: info (cyan), warn, error, ok, pc (blue). Buttons inside a
|
||||
callout make it a bar.
|
||||
|
||||
### Navigation
|
||||
- **Sidebar:** glyph + label rows with Text Dim at rest. The selected row gets the portal fade (blue→orange at 22 %)
|
||||
with a bright icon and label (single-color themes: the orange soft tint and an orange icon). The Steam Frame row
|
||||
carries a green connected dot.
|
||||
- **Segmented controls:** a round track. The selected segment takes the same portal fade.
|
||||
- **Site nav:** the wordmark (orange "Frame", the portal glyph, blue "Port"), ghost links, one small primary
|
||||
"Download FramePort" button at the right.
|
||||
|
||||
### Empty states (signature)
|
||||
The portal with what the screen is about coming out of it, on a soft portal light. Then a 22px heading, one centered
|
||||
sentence, the action that fills the screen, and up to three feature cards (round blue icon well, 14px heading, one
|
||||
sentence) that say what will appear here.
|
||||
|
||||
## Do's and Don'ts
|
||||
|
||||
### Do:
|
||||
- **Do** read colors as theme tokens when building (`T.ACCENT`, `var(--accent)`), so all three built-in themes and
|
||||
user theme files work.
|
||||
- **Do** keep exactly one filled orange action per view; make everything else blue outline or ghost.
|
||||
- **Do** put the blue→orange gradient only where something crosses or is selected: seams, selection fades, progress,
|
||||
link underlines.
|
||||
- **Do** let cover art carry the color and keep surrounding chrome neutral.
|
||||
- **Do** pair every status color with a word or glyph (pills, callouts, checklists).
|
||||
- **Do** give motion a static fallback: under Reduce motion (`ui.reduce_motion`, `prefers-reduced-motion`) the state
|
||||
must still read without the animation.
|
||||
- **Do** scale every app size through `T.T_*` / `T.px()` and give icon-only controls a tooltip.
|
||||
|
||||
### Don't:
|
||||
- **Don't** build a light theme or a light surface; FramePort is dark only.
|
||||
- **Don't** swap the poles: no orange secondary buttons, no blue primary actions.
|
||||
- **Don't** add shadows to ordinary panels or cards; only artwork and floating layers lift.
|
||||
- **Don't** use Warn Amber or another orange-adjacent hue for a warning that could be mistaken for the accent.
|
||||
- **Don't** use the tilted portal silhouette as general ornament; it belongs to the logo, the wordmark and empty
|
||||
states.
|
||||
- **Don't** put image bytes into controls or recreate clickable controls on progress ticks; update them in place.
|
||||
@@ -0,0 +1,121 @@
|
||||
# Product
|
||||
|
||||
<!-- impeccable:product-schema 1 -->
|
||||
|
||||
## Platform
|
||||
|
||||
web
|
||||
|
||||
Two surfaces share this record:
|
||||
|
||||
- **The desktop app:** Python + Flet 1.0 (Flutter), shipped as bundles for Windows x64, macOS arm64 and Linux x64, plus
|
||||
a CLI wheel. It is not a website, but it renders through Flutter rather than native OS widgets, so it counts as `web`
|
||||
here. Desktop window, mouse and keyboard first; no phone layout.
|
||||
- **The website** [frameport.app](https://frameport.app): Astro 7 (`site/`), with pages for home, download, install,
|
||||
setup, docs, changelog and privacy. It hosts the setup line (`/s`) and the demo install link.
|
||||
|
||||
## Users
|
||||
|
||||
The primary user is a **Steam Frame owner who isn't technical**. They have Quest games (often from SideQuest backups)
|
||||
or PC VR games and want them in the Frame's Steam library without learning about APKs, patches or Proton. They work
|
||||
from their PC with the headset nearby, and they may switch to the headset to play or to type the setup line in
|
||||
Konsole.
|
||||
|
||||
Defaults, copy and the main paths serve that user. Depth is there for a second group: **tinkerers and recipe
|
||||
contributors** who change patches and game settings, read launch tests and session triage, use Monitor and share
|
||||
working recipes back to the catalog. Their tools sit behind Customize, "Show technical details" and the game's "…"
|
||||
menu, and stay out of the main path.
|
||||
|
||||
## Product Purpose
|
||||
|
||||
FramePort installs Quest games, Android apps, Linux apps, PC VR games and Windows programs on the **Valve Steam
|
||||
Frame**. It sets up the Frame once (one setup line, no root, no password), patches each game so it runs in Valve's
|
||||
Lepton (Android) or Proton (Windows), uploads it, adds it to Steam with artwork, and tests that it starts.
|
||||
|
||||
Success means the user picks a game, clicks **Install on Frame**, and plays it from the Frame's Steam library. When a
|
||||
game can't run, FramePort says so plainly and gives a next step instead of failing silently.
|
||||
|
||||
## Positioning
|
||||
|
||||
Other tools (FrameDrop, Valve's own tools) install an app as it is. FramePort **converts and patches** Quest games that
|
||||
don't run on the Frame as they are. It uses community-tested recipes (131 catalog entries in `catalog/games/`) and
|
||||
suggests patches for every other game, each explained in plain words. It is free and open source (GPL-3.0), and it
|
||||
manages the Frame from the PC: Files, Screenshots, Live view, Type on Frame and Monitor.
|
||||
|
||||
## Operating Context
|
||||
|
||||
- **First run:** download, unpack, start FramePort, then **Steam Frame → Start setup**. On the Frame, run the setup
|
||||
line in Konsole and click **Allow** when both sides show the same 4-digit code. Wi-Fi or a USB cable.
|
||||
- **Daily use:** Add games (scan a folder, install link, Linux app, Windows program), then install, play, update and
|
||||
uninstall from the Library. The game page holds the main action; patches are under Customize.
|
||||
- **While playing:** the PC shows the Frame's state. The sidebar Frame card has battery and "now playing" fps, plus the
|
||||
Monitor, Live view (in the browser), Screenshots and Files tabs.
|
||||
- **When something breaks:** launch tests, session triage callouts ("Last session"), Report a problem (redacted
|
||||
diagnostics, prefilled GitHub issue) and Share working config.
|
||||
- Long jobs (uploads of several GiB) run in a background queue that can be cancelled and resumed. The window stays
|
||||
responsive and shows progress in the sidebar activity cards.
|
||||
|
||||
## Capabilities and Constraints
|
||||
|
||||
- Terms and voice follow `docs/STYLE.md`: Steam Frame / the Frame, Quest game, PC VR game, patch, recipe, Install /
|
||||
Update / Reinstall / Play, Set up / connect, This PC, sentence case, US spelling, short labels (four words at most).
|
||||
- Every GUI text goes through `tr()` (`src/frameport/i18n.py`), so layouts must tolerate longer translated strings.
|
||||
- GUI scale is user-set (`ui.scale`, auto under WSL); sizes go through theme tokens and `T.px()`, never bare pixels.
|
||||
- Performance rules are binding: no image bytes in controls (artwork by URL thumbnails), no I/O in render paths, and
|
||||
clickable controls are updated in place, never recreated on progress ticks.
|
||||
- Reduce motion (`ui.reduce_motion`) turns off every animation.
|
||||
- Docs screenshots and videos are generated by `scripts/showcase/` from a demo library, never by hand. New icon-only
|
||||
controls need a tooltip so the showcase steps can find them.
|
||||
- Headless tests prove that a game starts, never what it looks like. Tracking only works with the headset worn.
|
||||
- Not every game runs. Some need Meta's services or hardware the Frame lacks (e.g. 32-bit-only APKs, Oculus Platform
|
||||
SDK games). The product must say this, not hide it.
|
||||
|
||||
## Brand Commitments
|
||||
|
||||
- **Logo:** a monitor coming out of a tilted portal as the Frame (`src/frameport/ui/icons/logo.svg`,
|
||||
`logo-solid.svg`; `docs/brand/`). The wordmark carries the portal.
|
||||
- **Portal dichotomy:** orange = the main action and selection, blue = secondary actions, progress and the PC side.
|
||||
`portal` is the default theme (`portal_oled` and `original` also exist).
|
||||
- **Dark only.** No light mode; new themes are dark variants. User theme files that are light or unreadable are
|
||||
refused.
|
||||
- **Voice:** plain, technical wording that says what happens ("Steam restarts once"). No marketing adjectives
|
||||
(`docs/STYLE.md`).
|
||||
- **Credit** the projects FramePort wraps (OVRPort, Revive, Proton, Lepton, SteamVR and others): most of the
|
||||
functionality is theirs.
|
||||
- **No piracy tools.** FramePort doesn't download, share or unlock games and never bypasses entitlement checks.
|
||||
- **No Quest2Frame mentions** anywhere.
|
||||
- **Easter eggs** exist (`ui/easter.py`) and are never mentioned in the UI or docs.
|
||||
|
||||
## Evidence on Hand
|
||||
|
||||
- Generated screenshots in `docs/images/` (library, game, game settings, patches, install progress, frame, files,
|
||||
screenshots, screenshot viewer, live view, monitor, type on Frame, appearance) and diagrams (`architecture.svg`,
|
||||
`self-update.svg`).
|
||||
- Videos: `docs/media/frameport-tour.mp4` and `docs/media/frameport-install.mp4` (about 90 s each), teaser
|
||||
`docs/images/tour-teaser.webp`.
|
||||
- Install button badges in `docs/badges/` (`docs/INSTALL_BUTTON.md`); brand icon and social preview in `docs/brand/`.
|
||||
- Tested games list `docs/GAMES.md` and the recipe catalog `catalog/games/`. Real GitHub release, download and
|
||||
contributor data feed the site (`site/scripts/releases.mjs`, `contributors.mjs`).
|
||||
- **Absent, do not fabricate:** testimonials, user counts, press quotes, reviews or performance claims beyond what
|
||||
the docs record.
|
||||
|
||||
## Product Principles
|
||||
|
||||
1. **One click for the owner, every switch for the tinkerer.** The main path never asks a question the recipe can
|
||||
answer; depth lives one step away and is explained in plain words.
|
||||
2. **Say what happens, including failure.** Every state, from setup to launch test to "can't run on the Frame", is
|
||||
stated plainly with a next step.
|
||||
3. **The Frame is a place you reach from your PC.** Files, screenshots, live view, typing and monitoring treat the
|
||||
headset as connected, not remote.
|
||||
4. **Never lose the user's work.** Saves, signing keys, picked artwork, renamed titles and interrupted uploads are
|
||||
kept and resumable.
|
||||
5. **Open and honest.** Credit the upstream projects, no piracy, no personal data in reports, and admit that not every
|
||||
game runs.
|
||||
|
||||
## Accessibility & Inclusion
|
||||
|
||||
- Reduce motion turns off every animation, and the easter eggs too.
|
||||
- User-adjustable GUI scale for high-DPI screens.
|
||||
- Every text is translatable (`tr()`).
|
||||
- Icon-only controls carry tooltips. These also feed Flutter's semantics tree, which the showcase uses.
|
||||
- No formal WCAG target has been set (open decision).
|
||||
@@ -7,137 +7,127 @@
|
||||

|
||||

|
||||
|
||||
Install games that target the Meta Quest, Android, or general PCVR onto your **Valve Steam Frame**. FramePort handles everything from uploading game files, setting up your Frame, injecting compatibility patches, and adding shortcuts to your Steam library. FramePort aims to be as simple as possible by taking advantage of the fact that the Steam Frame runs on Linux.
|
||||
Install Quest games, Android apps, Linux apps and PC VR games on the **Valve Steam Frame**. FramePort sets up the
|
||||
Frame, patches each game so it runs there, uploads it and adds it to your Steam library with artwork.
|
||||
|
||||
[](docs/media/frameport-tour.mp4)
|
||||
|
||||
▶ [Watch the full tour](docs/media/frameport-tour.mp4) · New to FramePort? [Watch the install tutorial](docs/media/frameport-install.mp4)
|
||||
(about 90 seconds each, MP4; also attached to every release)
|
||||
(about 90 seconds each)
|
||||
|
||||
> **Notice:** FramePort explicitly does NOT download, share, or unlock games. You must provide legally obtained game
|
||||
> executables. Core features of FramePort simply download and wrap other published tools (see [Built on](#built-on))
|
||||
> with patches provided by FramePort adding a hardware compatibility layer. This enables users to use games/apps legally
|
||||
> purchased on sites like [SideQuest](https://sidequestvr.com/).
|
||||
> **Notice:** FramePort doesn't download, share or unlock games. Use games you got legally, for example from
|
||||
> [SideQuest](https://sidequestvr.com/). FramePort downloads published open-source tools (see [Built on](#built-on))
|
||||
> and adds its own patches so games run on the Frame.
|
||||
>
|
||||
> Not every game runs on the Frame: recipes are tested by the community, and some games need Meta's services or
|
||||
> hardware the Frame lacks.
|
||||
|
||||
## Features
|
||||
|
||||

|
||||
|
||||
- **Painless setup:** one short command on the Frame. No root, no `sudo`, no password.
|
||||
[What it changes](docs/FRAME_SETUP.md).
|
||||
- **Type on Frame:** use your computer's keyboard on the Frame: in VR apps, Android apps, Steam and the desktop.
|
||||
[More](#type-on-frame).
|
||||
- **One click per game:** convert, patch, sign, upload, add to Steam with artwork, launch test. Artwork that can't be found automatically can be picked from the stores or replaced with your own images.
|
||||
- **Per-game recipes:** a tested catalog plus detection rules; every patch explained in plain words.
|
||||
- **FrameBridge:** FramePort's OpenXR adapter emulates what the Frame natively lacks (passthrough, room, controller models,
|
||||
curved and 360° layers); game settings as simple switches.
|
||||
- **Beyond Quest:** Android apps as windows, PC VR via Proton or Revive, Windows (non-VR) games via Proton, a Files
|
||||
tab with drag and drop.
|
||||
- **Linux apps:** install Linux apps (AppImage, a folder, or a zip/tar archive) on the Frame with a Steam
|
||||
shortcut; arm64 builds run natively on SteamOS, x86_64 builds through Valve's FEX translator.
|
||||
- **Install links:** "Install with FrameDrop" buttons on web pages (the one-click protocol of the FrameDrop
|
||||
sideloader) and pasted links open in FramePort, which asks, downloads, adds and installs the build.
|
||||
- **Screenshots tab:** the screenshots you took in the headset, sorted by game (matched by play time) and day;
|
||||
view them and download them to your computer.
|
||||
- **Live view tab:** watch what the headset shows, with sound, in a browser window on your computer.
|
||||
- **Monitor tab:** the running game's frame rate, the Frame's load, temperatures, power and battery live, and its
|
||||
processes, which you can end. [More](#monitor).
|
||||
- **Self-updating** releases, redacted diagnostics, one-click problem reports and working-config sharing.
|
||||
- **Easy setup:** one line in the Frame's terminal. No root, no password. [What it changes](docs/FRAME_SETUP.md).
|
||||
- **One click per game:** patch, upload, add to Steam with artwork and test that it starts.
|
||||
- **Recipes:** tested patches and settings for 120+ games; other games get suggested patches, each explained in
|
||||
plain words.
|
||||
- **Game settings:** sharpness, refresh rate, controllers, 360° video and mixed reality as simple switches.
|
||||
- **More than Quest games:** Android apps in a window, Linux apps, PC VR games and Windows programs.
|
||||
- **Install links:** "Install with FramePort" buttons on websites ([frameport.app](https://frameport.app) links) open
|
||||
in FramePort.
|
||||
- **Your Frame from your PC:** Type on Frame (your keyboard on the Frame), Files, Screenshots, Live view (what the
|
||||
headset shows, in your browser) and Monitor (frame rate, temperatures, battery, processes).
|
||||
- **Updates itself**, and reports problems without personal data.
|
||||
|
||||
## Quick start
|
||||
|
||||
1. [Download](https://github.com/spoopyghosty0/frameport/releases/latest) and unzip the build for Windows, macOS
|
||||
(Apple Silicon) or Linux, then start FramePort.
|
||||
2. **Connect the Frame** (once):
|
||||
1. In FramePort click **Steam Frame → Show setup command**. Keep FramePort open; the Frame and your computer must
|
||||
be on the same Wi-Fi.
|
||||
2. On the Frame open the **SteamVR dashboard → Launch a program → Desktop**: the Linux desktop opens on a virtual
|
||||
screen.
|
||||
3. Open the app menu (bottom-left corner of that desktop) → **System → Konsole** (or search for Konsole).
|
||||
4. Type the command FramePort shows exactly as shown (on-screen keyboard or any USB/Bluetooth keyboard) and press
|
||||
**Enter**. This will run the following [bash setup script](bootstrap/bootstrap.sh).
|
||||
5. After a few seconds the desktop closes by itself (Steam restarts once); that's expected. If
|
||||
Steam asks to install **Lepton**, confirm it. FramePort shows the Frame as connected within a minute. No
|
||||
password needed.
|
||||
3. **Add games → Scan a folder** with your game backups (APK + OBB, or PC VR game folders).
|
||||
4. Open a game → **Install on Frame**, then play it from the Frame's Steam library.
|
||||
|
||||
Full guide, firewalls and troubleshooting: [docs/INSTALL.md](docs/INSTALL.md). Questions (e.g. how to lay out games
|
||||
with OBB files): [docs/FAQ.md](docs/FAQ.md).
|
||||
|
||||
## Type on Frame
|
||||
|
||||
Typing in VR is painful, so FramePort turns your computer's keyboard into a keyboard for the Frame. Open **Type on
|
||||
Frame** (its own tab in the sidebar), select a text field in the headset and type: searches, logins, chat, in any
|
||||
app, in Steam or on the desktop. Paste longer text to type it in one go. Nothing to install: FramePort adds a virtual
|
||||
keyboard on the Frame while the tab is open, without root.
|
||||
|
||||

|
||||
|
||||
Unity apps whose text fields close the moment you select them on the Frame (no system keyboard there) get a per-game
|
||||
fix, so Steam's on-screen keyboard and Type on Frame work in them too.
|
||||
[Details](docs/INSTALL.md#typing-on-the-frame).
|
||||
|
||||
## Screenshots
|
||||
|
||||
Screenshots you take in the headset show up in FramePort's **Screenshots** tab, grouped by day and matched to the game
|
||||
you were playing. Open one full size, step through them, and download single shots, a selection or all of them to
|
||||
your computer.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Live view
|
||||
|
||||
The **Live view** tab streams what the headset shows, with its sound, to your computer: click **Start live view** and
|
||||
it opens in your default web browser (full screen with a double-click; click **Sound on** to hear it, as browsers start
|
||||
videos muted). Pick 360p to 1080p, or the headset view's full size. The picture comes from SteamVR's built-in headset
|
||||
view on the Frame and is encoded there while you watch (about one CPU core), so stop it when you're done. It's black
|
||||
while the headset sleeps.
|
||||
|
||||
## Monitor
|
||||
|
||||
The **Monitor** tab shows what the Frame is doing while it's open: the running game with its frame rate against the
|
||||
display's refresh rate, CPU, graphics chip, memory, the hottest temperature with the fan speed, power draw and battery
|
||||
time left, each with a 2-minute chart. **Show details** adds every CPU core, all temperature sensors, where the power
|
||||
goes and the network. Below, the game's processes (or Steam's, or all of them) with their CPU, GPU and memory use:
|
||||
right-click one to end it, or end the whole game. Programs that Steam, SteamVR or the desktop need are marked and ask
|
||||
again. The Frame sends the numbers itself (about 1 % of one CPU core) and stops when you leave the tab.
|
||||
Details for each: [Install and first steps](docs/INSTALL.md).
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Quick start
|
||||
|
||||
1. [Download](https://github.com/spoopyghosty0/frameport/releases/latest) the build for Windows, macOS (Apple
|
||||
Silicon) or Linux, unpack it and start FramePort.
|
||||
2. In FramePort click **Steam Frame → Start setup**. The Frame and your PC must be on the same Wi-Fi (or use a USB
|
||||
cable).
|
||||
3. On the Frame open the **SteamVR dashboard → Launch a program → Desktop**, then the app menu → **System →
|
||||
Konsole**, and run:
|
||||
|
||||
```
|
||||
curl -fsSL https://frameport.app/s | bash
|
||||
```
|
||||
|
||||
4. Click **Allow** in FramePort when it shows the same 4-digit code as the Frame. Steam restarts once; if it asks to
|
||||
install **Lepton** (Valve's Android runtime), confirm it.
|
||||
5. **Add games → Scan a folder…** with your games, open one and click **Install on Frame**. Play it from the Frame's
|
||||
Steam library.
|
||||
|
||||
Full guide, firewalls and troubleshooting: [Install and first steps](docs/INSTALL.md). How to lay out game folders:
|
||||
[FAQ](docs/FAQ.md).
|
||||
|
||||
## Compatibility
|
||||
|
||||
If a game has already been tested with FramePort, it will automatically use the optimal game config. Otherwise, FramePort
|
||||
will attempt to guess key patches. If you find a new config that works for an app you are testing, please consider submitting it to the community!
|
||||
Tested games use their recipe (the patches and settings that work for them) automatically; for other games FramePort
|
||||
suggests patches. See the [list of tested games](docs/GAMES.md). Got a game working? Share its recipe from the game's
|
||||
**…** menu: [how](docs/INSTALL.md#share-a-recipe-or-report-a-problem).
|
||||
|
||||
**[List of tested games](docs/GAMES.md)**
|
||||
## Compared with other tools
|
||||
|
||||
**Tested something? Share it.** In FramePort open the game → **…** → **Share working recipe…** (it fills in the
|
||||
recipe for you) or **Report a problem…** (attaches a diagnostics zip with personal data removed). Without the app:
|
||||
[share a working config](https://github.com/spoopyghosty0/frameport/issues/new?template=working-config.yml) · [report a problem](https://github.com/spoopyghosty0/frameport/issues/new?template=bug-report.yml). Shared configs become built-in recipes for everyone.
|
||||
FrameDrop and Valve's own tools install an app as it is. FramePort differs in four ways:
|
||||
|
||||
- **Free and open source** (GPL-3.0). FrameDrop is free (donationware) without published source.
|
||||
- **Quest games that don't run on the Frame as they are** get converted and patched, with a tested recipe for 120+
|
||||
games. The others install the game unchanged.
|
||||
- **Windows, macOS and Linux.** FrameDrop is for Windows.
|
||||
- **Wi-Fi or a USB cable**, and no **Pair new host** step.
|
||||
|
||||
<details>
|
||||
<summary>All differences</summary>
|
||||
|
||||
| | **FramePort** | **FrameDrop** | **By hand with Valve's tools** |
|
||||
|---|---|---|---|
|
||||
| Cost and source code | Free, open source (GPL-3.0) | Free (donationware), source not published | Free, from Valve |
|
||||
| Runs on | Windows, macOS, Linux | Windows | Depends on the tool |
|
||||
| First connection | One line in the Frame's terminal; it turns on Developer Mode itself. No password | Turn on Developer Mode, then **Pair new host** | Turn on Developer Mode and pair, or use Android's debug tool (adb) |
|
||||
| Wireless or cable | Wi-Fi, the Frame's hotspot or a USB cable | Same Wi-Fi network | Wi-Fi, or adb |
|
||||
| Quest games that don't run as they are | Converted and patched for the Frame | Installed as they are | Installed as they are |
|
||||
| Knows which patches a game needs | Tested recipes for 120+ games, updated without an app update | Not stated | No |
|
||||
| In the Steam library | Yes, with artwork and tags | As a "Devkit Game" shortcut | As "Devkit Game: <title>"; not with adb |
|
||||
| Android apps, Linux apps, Windows programs | All three; Proton (runs Windows programs) is installed for you | All three; install Proton first | Yes; 2D Android apps need an extra file |
|
||||
| PC VR games | On your PC; some also on the Frame | Not supported | Not supported |
|
||||
| "Install with …" buttons on websites | Yes ([frameport.app](https://frameport.app) links) | Its own | No |
|
||||
| A game doesn't start | A launch test reads the logs and suggests a patch | A log viewer | No help |
|
||||
| Use the Frame from your PC | Live view, Monitor, Files, Screenshots, Type on Frame | Not stated | No |
|
||||
|
||||
</details>
|
||||
|
||||
Other tools as described on their own pages, checked 2026-10-09:
|
||||
[FrameDrop about](https://framedropvr.com/about) · [how-to](https://framedropvr.com/how-to) ·
|
||||
[Valve: loading games on Steam Frame](https://partner.steamgames.com/doc/steamhardware/steamframe/loadgames).
|
||||
Out of date? [Report a problem](https://github.com/spoopyghosty0/frameport/issues/new).
|
||||
|
||||
## Built on
|
||||
|
||||
Most of the work is done by these projects:
|
||||
[OVRPort](https://github.com/Android-XR-Bridge/OVRPort) (Quest → OpenXR, originally
|
||||
[ovrport/app](https://github.com/ovrport/app)) · Valve Lepton, Proton and SteamVR ·
|
||||
[Revive](https://github.com/LibreVR/Revive) · Mesa (Zink) · [Khronos OpenXR SDK](https://github.com/KhronosGroup/OpenXR-SDK)
|
||||
· Eclipse Temurin, Android apksigner and NDK · [Flet](https://flet.dev) · OculusDB and Steam store data.
|
||||
What FramePort adds itself: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
||||
What FramePort adds: [Architecture](docs/ARCHITECTURE.md).
|
||||
|
||||
## Documentation
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| [INSTALL.md](docs/INSTALL.md) | Install, connect, update, PC VR, files, problem reports |
|
||||
| [FRAME_SETUP.md](docs/FRAME_SETUP.md) | What setup changes on the Frame, networks and firewalls, undoing it |
|
||||
| [COMPATIBILITY.md](docs/COMPATIBILITY.md) | What runs and how well |
|
||||
| [GAMES.md](docs/GAMES.md) | Tested games and how well they run |
|
||||
| [PLAYBOOK.md](docs/PLAYBOOK.md) | Symptoms and fixes per game |
|
||||
| [FRAME_RUNTIME.md](docs/FRAME_RUNTIME.md) | Steam Frame runtime facts |
|
||||
| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | How the code is organised |
|
||||
| [CONTRIBUTING.md](CONTRIBUTING.md) | Code, recipes, translations |
|
||||
| [Install and first steps](docs/INSTALL.md) | Install, connect, update, PC VR games, files, problem reports |
|
||||
| [What the setup changes](docs/FRAME_SETUP.md) | What setup changes on the Frame, networks and firewalls, undoing it |
|
||||
| [Compatibility](docs/COMPATIBILITY.md) | What runs and how well |
|
||||
| [Tested games](docs/GAMES.md) | Tested games and how well they run |
|
||||
| [FAQ](docs/FAQ.md) | Game folders and common questions |
|
||||
| [Porting playbook](docs/PLAYBOOK.md) | Symptoms and fixes per game |
|
||||
| [Steam Frame runtime reference](docs/FRAME_RUNTIME.md) | Facts about the Frame's runtime |
|
||||
| [Architecture](docs/ARCHITECTURE.md) | How the code is organized |
|
||||
| [Contributing](CONTRIBUTING.md) | Code, recipes, translations |
|
||||
|
||||
## Development
|
||||
|
||||
@@ -148,8 +138,9 @@ uv run frameport --help # command line
|
||||
uv run pytest # tests
|
||||
```
|
||||
|
||||
## AI Usage Notice
|
||||
While I would like to program everything manually, I no longer have much free time for personal projects. As a result I make use of AI tools to make it significantly quicker to debug compatibility issues.
|
||||
## AI usage
|
||||
|
||||
I don't have much free time for this project, so I use AI tools to debug compatibility problems faster.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ FramePort is GPL-3.0-only (see `LICENSE`). It includes or downloads the followin
|
||||
| Android NDK runtime (statically linked libc++) | native Android libraries | Apache-2.0 with LLVM exception, plus legacy notices (`native/vrapi-bridge/licenses/ANDROID-NDK.txt`) |
|
||||
| Flet and Flutter (desktop app runtime) | release bundles | Apache-2.0 / BSD-3-Clause |
|
||||
| flet-dropzone / desktop_drop (drag-and-drop) | release bundles | Apache-2.0 / MIT |
|
||||
| FFmpeg 7.1.1 hardware HEVC wrapper (LGPL configuration, without GPL/nonfree components) | `artifacts/hevc/libstagefrighthw.so`; source/rebuild instructions in `native/hevc/build.py` and `native/hevc/README.md` | LGPL-2.1-or-later (`artifacts/hevc/COPYING.FFmpeg`); unmodified source: https://ffmpeg.org/releases/ffmpeg-7.1.1.tar.xz |
|
||||
| AOSP Android 11 native media/utility headers | `native/hevc/platform/` | Apache-2.0; copyright/license notices retained in the headers |
|
||||
| Python packages (paramiko, zeroconf, psutil, pyelftools, capstone, UnityPy, PyYAML, requests, typer, pyaxmlparser, Pillow, cryptography, …) | release bundles | their own licenses (see each package's metadata) |
|
||||
|
||||
FramePort's own native code (the FrameBridge adapter, GL/Vulkan/OpenXR shims, the Windows helpers) is GPL-3.0-only.
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
5becb96ee86fbda0ce98c0e0fd064f8ba092314aceaa400e3dcc785705dc3c23 ./arm64-v8a/libVkLayer_fp_shaderfix.so
|
||||
e7554c6343ee2989b0a273ee6230e65c25bfe6499eefd00de19f5f0fee58754f ./arm64-v8a/libfp_langpack.so
|
||||
321da502e0f8f46f0880aad39fe6b84bdef88f925164568814cf704a1b31a577 ./arm64-v8a/libfp_ovrp.so
|
||||
40defdaddcda53bd2649eb48076fae1622bfc2bfc88e9e03c94fc8b45af2a4f2 ./arm64-v8a/libfp_ovrtrace.so
|
||||
7522a7cfb236e48d3442d418c4820cd37760a9627c6bdef80fb0df82df5fd826 ./arm64-v8a/libfp_vk.so
|
||||
e8c0965c188665a4e565e4a3af983ef1866d1cf9225665cc90d8df286bc96ef3 ./arm64-v8a/libfp_vk.so
|
||||
383054f8b3b41dde76d062c71856cd3163655e50009c08454bc1793f393ead65 ./arm64-v8a/libfpg.so
|
||||
5432674a59d02f411cd853a5dc57e3fa547d1ac518f25bf433fc54c43677177a ./arm64-v8a/libfpglmv.so
|
||||
8a8f6b1da8952cb3933a5b53558424a5fc56043b2527a16bb6dd62a00d2de01a ./arm64-v8a/libframe_xrshim.so
|
||||
fc559dbe0b87eecc3641256837293260cefb7c2ce7b21d808d10edd2a2dba9fc ./arm64-v8a/libglshim.so
|
||||
cc5b493cadefe76ad13fd096d3f8494a140ba593e0e1692db12d284bc548c1cf ./arm64-v8a/libopenxr_loader_generic.so
|
||||
19ba5ed3c4b780f5ae46251f6958f552acfc5f13c8bb39f995579593fa8990c6 ./arm64-v8a/libglshim.so
|
||||
449e13bbe58de6718d4e530177924e09552a95d748a082739899144e2cb3ccf3 ./arm64-v8a/libopenxr_loader_generic.so
|
||||
1feaeafad467c4cafdf2b018a4d84b0bee200c3f711697b4ce97e66a3ba256ca ./arm64-v8a/libovrplatformcompat.so
|
||||
aa4dc0020c77e12d41ef6ce80b23ad90c9cb7e882338eddf2261efe8645cf38d ./arm64-v8a/libvrapi.so
|
||||
c10412dd76a65a1d587b7ed226367d1a6f2bec238c733c97ee67eb8870c868e8 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
1d83eb94ec1df44f29a5f525dce4d5b608dbbb13a9c7a5efeb80a4b1dc54a721 ./arm64-v8a/libvrapi.so
|
||||
47f736dfeffce6aa56be2fc049c2d11b87b0d1f7389db7ee1737fcded9457811 ./armeabi-v7a/libopenxr_loader_generic.so
|
||||
1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex
|
||||
b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe ./hevc/COPYING.FFmpeg
|
||||
141de01f0d40b97d4f9a720db5ab8ea442f6aa35db5272048899292884994a1c ./hevc/libstagefrighthw.so
|
||||
4516971bbb2b635e19b14d2d37b9353962876f7de02043e7457327052c1fcbc0 ./hevc/manifest.json
|
||||
46c3dcad2c43cc30bea3b3680b362ed84a99c15d60714bbbbb422c80ecf90405 ./hevc/media_codecs_frameport.xml
|
||||
529ea35d5d82474afe9e09ad1216d269317f56dd24a05fb100fced52b7b4dbca ./hevc/podman.py.txt
|
||||
1aa733117cf57ccff7cf23f0425dbfd73b0332a3ef1c905ceeca0ccd0449c4e0 ./linux-arm64/XR_APILAYER_FRAMEPORT_timefix.json
|
||||
28c2430a02bbd8902c5bfb9562c6fd0e318654f9e05c095bfeb94b0450ab1b07 ./linux-arm64/libxr_frameport_timefix.so
|
||||
b10b3a5c10c3339fc63fd9f6cb4d01ea29be11a863d0ebad1389ea69fd471940 ./linux-arm64-bin/fp_venc
|
||||
|
||||
@@ -0,0 +1,502 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
not price. Our General Public Licenses are designed to make sure that
|
||||
you have the freedom to distribute copies of free software (and charge
|
||||
for this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
possible use to the public, we recommend making it free software that
|
||||
everyone can redistribute and change. You can do so by permitting
|
||||
redistribution under these terms (or, alternatively, under the terms of the
|
||||
ordinary General Public License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is
|
||||
safest to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the library's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1990
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"revision": 8,
|
||||
"runtime_sha256": "456e912c75cd389abcf6a63bc80e2a53bdc334371d00b200c93680388ae955e2",
|
||||
"files": {
|
||||
"libstagefrighthw.so": "141de01f0d40b97d4f9a720db5ab8ea442f6aa35db5272048899292884994a1c",
|
||||
"podman.py": "529ea35d5d82474afe9e09ad1216d269317f56dd24a05fb100fced52b7b4dbca",
|
||||
"media_codecs_frameport.xml": "46c3dcad2c43cc30bea3b3680b362ed84a99c15d60714bbbbb422c80ecf90405",
|
||||
"COPYING.FFmpeg": "b634ab5640e258563c536e658cad87080553df6f34f62269a21d554844e58bfe"
|
||||
},
|
||||
"codecs": [
|
||||
"video/hevc",
|
||||
"video/avc",
|
||||
"video/x-vnd.on2.vp9"
|
||||
],
|
||||
"build": {
|
||||
"ndk_revision": "27.2.12479018",
|
||||
"ffmpeg_source_sha256": "733984395e0dbbe5c046abda2dc49a5544e7e0e1e2366bba849222ae9e3a03b1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<MediaCodecs>
|
||||
<Decoders>
|
||||
<MediaCodec name="OMX.frameport.hevc.decoder" type="video/hevc" rank="64">
|
||||
<Limit name="size" min="128x128" max="8192x8192" />
|
||||
<Limit name="alignment" value="2x2" />
|
||||
<Limit name="block-size" value="16x16" />
|
||||
<Limit name="block-count" range="1-138240" />
|
||||
<Limit name="blocks-per-second" range="1-7864320" />
|
||||
<Limit name="bitrate" range="1-245000000" />
|
||||
<Limit name="concurrent-instances" max="1" />
|
||||
</MediaCodec>
|
||||
<MediaCodec name="OMX.frameport.avc.decoder" type="video/avc" rank="64">
|
||||
<Limit name="size" min="128x128" max="8192x8192" />
|
||||
<Limit name="alignment" value="2x2" />
|
||||
<Limit name="block-size" value="16x16" />
|
||||
<Limit name="block-count" range="1-138240" />
|
||||
<Limit name="blocks-per-second" range="1-7864320" />
|
||||
<Limit name="bitrate" range="1-245000000" />
|
||||
<Limit name="concurrent-instances" max="1" />
|
||||
</MediaCodec>
|
||||
<MediaCodec name="OMX.frameport.vp9.decoder" type="video/x-vnd.on2.vp9" rank="64">
|
||||
<Limit name="size" min="128x128" max="4096x2304" />
|
||||
<Limit name="alignment" value="2x2" />
|
||||
<Limit name="block-size" value="16x16" />
|
||||
<Limit name="block-count" range="1-36864" />
|
||||
<Limit name="blocks-per-second" range="1-2211840" />
|
||||
<Limit name="bitrate" range="1-245000000" />
|
||||
<Limit name="concurrent-instances" max="1" />
|
||||
</MediaCodec>
|
||||
</Decoders>
|
||||
</MediaCodecs>
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/python3
|
||||
"""Add shared codecs to Lepton containers without editing their shared rootfs."""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
|
||||
def mounts(directory, config, args):
|
||||
if not args or args[0] != "run":
|
||||
return []
|
||||
name = None
|
||||
for i, arg in enumerate(args):
|
||||
if arg == "--name" and i + 1 < len(args):
|
||||
name = args[i + 1]
|
||||
elif arg.startswith("--name="):
|
||||
name = arg.partition("=")[2]
|
||||
if config.get("scope") == "shared":
|
||||
# The common launcher supplies these values. Do not intercept arbitrary
|
||||
# Podman containers or depend on a game's package/modified APK.
|
||||
appid = os.environ.get("SteamAppId", "")
|
||||
if not re.fullmatch(r"[0-9]+", appid) or name != f"lepton-steamlaunch-{appid}":
|
||||
return []
|
||||
app = Path(os.environ.get("STEAM_COMPAT_INSTALL_PATH", ""))
|
||||
if app.name != "lepton-app" or not app.is_dir():
|
||||
return []
|
||||
root_arg = None
|
||||
for i, arg in enumerate(args):
|
||||
if arg == "--rootfs" and i + 1 < len(args):
|
||||
root_arg = args[i + 1]
|
||||
elif arg.startswith("--rootfs="):
|
||||
root_arg = arg.partition("=")[2]
|
||||
if not root_arg or not root_arg.endswith(":O"):
|
||||
return []
|
||||
root = Path(root_arg[:-2]).resolve()
|
||||
else: # old per-game deployments remain compatible during migration
|
||||
if name != f"lepton-steamlaunch-{config['appid']}":
|
||||
return []
|
||||
root = Path(config["lepton"]).resolve().parent / "images" / "rootfs"
|
||||
expected_root = str(root) + ":O"
|
||||
if not any(arg in (expected_root, "--rootfs=" + expected_root) for arg in args):
|
||||
return []
|
||||
device = Path("/dev/video-dec0")
|
||||
runtime = root / "vendor/lib64/libstagefright_softomx.so"
|
||||
upstream_plugin = (root / "vendor/lib64/libstagefrighthw.so").exists()
|
||||
for i, arg in enumerate(args):
|
||||
if arg == "--mount" and i + 1 < len(args):
|
||||
fields = dict(item.split("=", 1) for item in args[i + 1].split(",") if "=" in item)
|
||||
target = fields.get("destination", fields.get("target"))
|
||||
if target == "/vendor/lib64/libstagefrighthw.so":
|
||||
upstream_plugin = True
|
||||
if target == "/vendor/lib64/libstagefright_softomx.so" and fields.get("source"):
|
||||
runtime = Path(fields["source"])
|
||||
if upstream_plugin:
|
||||
print("FramePort video: using the runtime's hardware codec plugin", file=sys.stderr)
|
||||
return []
|
||||
if not device.exists() or hashlib.sha256(runtime.read_bytes()).hexdigest() != config["runtime_sha256"]:
|
||||
print("FramePort video: device or runtime ABI differs; retaining the stock codecs", file=sys.stderr)
|
||||
return []
|
||||
xml = ET.parse(root / "vendor/etc/media_codecs.xml")
|
||||
if not any(node.get("href") == "media_codecs_frameport.xml" for node in xml.getroot().findall("Include")):
|
||||
ET.SubElement(xml.getroot(), "Include", href="media_codecs_frameport.xml")
|
||||
# Immutable plugin versions can serve simultaneous app launches and
|
||||
# different Lepton installations. Never share a temporary XML filename.
|
||||
# The version directory stays as the agent verified it: the merged list goes to the user's runtime dir.
|
||||
root_key = hashlib.sha256(str(root).encode()).hexdigest()[:16]
|
||||
merged = merged_dir() / f"media_codecs.{root_key}.xml"
|
||||
temporary = merged.with_suffix(f".{os.getpid()}.tmp")
|
||||
xml.write(temporary, encoding="utf-8", xml_declaration=True)
|
||||
temporary.replace(merged)
|
||||
# Lepton supplies its own /dev tmpfs. A Podman --device node disappears
|
||||
# beneath it; a bind mount matches Lepton's existing GPU/sound device setup.
|
||||
result = ["--mount", f"type=bind,source={device},destination=/dev/video-dec0,rw"]
|
||||
for path, target in (
|
||||
(directory / "libstagefrighthw.so", "/vendor/lib64/libstagefrighthw.so"),
|
||||
(merged, "/vendor/etc/media_codecs.xml"),
|
||||
(directory / "media_codecs_frameport.xml", "/vendor/etc/media_codecs_frameport.xml"),
|
||||
):
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"missing video codec mount: {path}")
|
||||
result += ["--mount", f"type=bind,source={path},destination={target},ro"]
|
||||
print("FramePort video: loading the Iris hardware codec plugin for this container", file=sys.stderr)
|
||||
return result
|
||||
|
||||
|
||||
def merged_dir():
|
||||
runtime = os.environ.get("XDG_RUNTIME_DIR", "")
|
||||
base = Path(runtime) if runtime and Path(runtime).is_dir() else Path.home() / ".cache"
|
||||
path = base / "frameport-video"
|
||||
path.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def switched_off(directory):
|
||||
"""FRAMEPORT_NO_HW_VIDEO=1 (e.g. in a game's Steam launch options) or the Frame-wide switch (FramePort's
|
||||
setting; the agent writes video-codec/disabled) leave every container with Android's stock codecs."""
|
||||
if os.environ.get("FRAMEPORT_NO_HW_VIDEO", "") not in ("", "0"):
|
||||
return True
|
||||
return directory.parent.name == "versions" and (directory.parent.parent / "disabled").exists()
|
||||
|
||||
|
||||
def real_podman(directory):
|
||||
"""Find Podman independently of deployment.json, without recursing into this wrapper."""
|
||||
own_bin = (directory / "bin").resolve()
|
||||
own_script = Path(__file__).resolve()
|
||||
# Lepton runs its `podman exec` calls (boot wait, app pid, logcat mirror) with the Android guest's PATH
|
||||
# (/product/bin:/system/bin:...), which has no host Podman: the system folders come after PATH. Failing there
|
||||
# broke Lepton's logcat mirror and app-pid checks, and the container was stopped early.
|
||||
entries = os.environ.get("PATH", os.defpath).split(os.pathsep) + ["/usr/local/bin", "/usr/bin", "/bin"]
|
||||
for entry in entries:
|
||||
folder = Path(entry or os.curdir).resolve()
|
||||
if folder == own_bin:
|
||||
continue
|
||||
found = shutil.which("podman", path=str(folder))
|
||||
if found:
|
||||
executable = Path(found).resolve()
|
||||
if executable.parent != own_bin and executable != own_script:
|
||||
return str(executable)
|
||||
raise RuntimeError("no real Podman executable found outside the codec wrapper directory")
|
||||
|
||||
|
||||
def main():
|
||||
directory = Path(__file__).resolve().parent.parent
|
||||
args = sys.argv[1:]
|
||||
fallback = real_podman(directory)
|
||||
if switched_off(directory):
|
||||
os.execv(fallback, [fallback, *args])
|
||||
try:
|
||||
config = json.loads((directory / "deployment.json").read_text())
|
||||
podman = Path(config.get("podman", fallback)).resolve()
|
||||
if podman.parent == (directory / "bin").resolve() or podman == Path(__file__).resolve():
|
||||
raise ValueError("configured Podman points to the codec wrapper")
|
||||
extra = mounts(directory, config, args)
|
||||
launch_args = [args[0], *extra, *args[1:]] if extra else args
|
||||
os.execv(str(podman), [str(podman), *launch_args])
|
||||
except Exception as exc: # a codec/configuration failure must never prevent the stock container from starting
|
||||
print(f"FramePort video: retaining stock codecs: {exc}", file=sys.stderr)
|
||||
os.execv(fallback, [fallback, *args])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -30,13 +30,13 @@ user_systemd() {
|
||||
|
||||
say "FramePort setup for $(hostname) ($(. /etc/os-release; echo "$NAME $VERSION_ID"))"
|
||||
|
||||
say "Authorizing the FramePort app's key"
|
||||
say "Letting FramePort log in"
|
||||
key=$(curl -fsS "$PC_URL/key?code=$PAIR_CODE")
|
||||
[[ "$key" == ssh-ed25519\ * ]] || { echo "Could not fetch the app's key from $PC_URL (is the app still open?)"; exit 1; }
|
||||
[[ "$key" == ssh-ed25519\ * ]] || { echo "Couldn't reach FramePort at $PC_URL. Is it still open?"; exit 1; }
|
||||
mkdir -p ~/.ssh && chmod 700 ~/.ssh && touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys
|
||||
grep -qxF "$key" ~/.ssh/authorized_keys || echo "$key" >> ~/.ssh/authorized_keys
|
||||
|
||||
say "Configuring podman for Lepton"
|
||||
say "Setting up Lepton's containers"
|
||||
# rootless podman leaks one kernel keyring per container start; ~200 game launches would exhaust the quota
|
||||
mkdir -p ~/.config/containers
|
||||
grep -qs '^ *keyring *=' ~/.config/containers/containers.conf || printf '[containers]\nkeyring = false\n' >> ~/.config/containers/containers.conf
|
||||
@@ -117,7 +117,7 @@ JOB
|
||||
if [[ -f /etc/steamos-devkit-enabled ]]; then
|
||||
say "Developer Mode is on"
|
||||
bash "$JOB" finish "$PC_URL" "$PAIR_CODE" "$STEAM_CONFIG" "$DEVKIT_HELPER" 2>&1 | tee "$LOG"
|
||||
say "Done. Return to FramePort on your PC: this Frame should now appear as connected."
|
||||
say "Done. FramePort on your PC shows this Frame as connected."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -126,9 +126,8 @@ if [[ ! -x "$DEVKIT_HELPER" || ! -f "$STEAM_CONFIG" ]] || \
|
||||
! user_systemd systemd-run --user --collect --quiet --unit="frameport-setup-$$" \
|
||||
bash -c 'bash "$0" "$@" >"$HOME/.cache/frameport-setup.log" 2>&1' \
|
||||
"$JOB" devmode "$PC_URL" "$PAIR_CODE" "$STEAM_CONFIG" "$DEVKIT_HELPER"; then
|
||||
echo "Couldn't turn it on automatically. Turn it on in Settings > System > Developer, then run this command again."
|
||||
echo "Couldn't turn it on. Turn it on in Settings → System → Enable Developer Mode, then run this again."
|
||||
exit 1
|
||||
fi
|
||||
echo "Steam restarts to turn it on. That closes the desktop in a few seconds and the Frame returns to its"
|
||||
echo "normal view; setup finishes on its own. If Steam asks to install Lepton, confirm it."
|
||||
echo "Then return to FramePort on your PC: the Frame appears as connected within a minute."
|
||||
echo "Steam restarts and the desktop closes. Confirm Lepton if asked."
|
||||
echo "Then check FramePort on your PC."
|
||||