Archives (.zip, .7z, .rar; multi-part sets) are a source on every target, the PS5 included: - New crate ps5-dump-forge-archive: own zip reader, 7z over sevenz-rust2 (decode only), RAR through our wrapper over unrar_sys's UnRAR, with memory, header and link limits. - Conversions read the game straight from the archive (stream::Reader), file by file in the writer's order, with nothing unpacked to disk; solid archives re-decode on a backward read. - Release-site passwords are tried first when an archive needs one; a password counts only when it reads the game's param.json (and an encrypted file when param.json is in the clear). - Truncated RARs, swapped parts and unsafe names are refused; every part is guarded from delete while its job runs. AMPR: the game's ampr_emu version is read from its runtime. Update AMPR (now in the Full verification box) shows only for an older one; Pack keeps a runtime that is not older and reads packs. The LZ4 tab blocks a game without ampr_emu and hides its LZ4 tile. UI: animated dots on waiting texts, shorter LZ4 texts in all 11 languages, new target orders and defaults (Convert: fpkg; LZ4: exfat), "Папка с игрой", no "No preflight findings" line. Server: a source read waits up to 30 s for one of its 2 slots (at most 4 waiting; 409 while stopping). Release zips hold only the app and the CLI; the UnRAR licences live in vendor/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
24 KiB
PS5 Dump Forge UI design system
The rules behind app/src/styles.css: the CSS holds the exact values, this file says when to use what.
The look follows the SpendIQ Dribbble shot (dark cards, pill buttons, inset stat tiles, tinted tags, a
gradient notice bar) and the app icon (near-black navy with a blue edge and an orange glow).
Principles
- Dark only (
color-scheme: dark). A light theme would need a second token set. - One hero. Only the game card (
.card.hero) glows, in colours taken from its cover (glow.ts). - Pills for controls. Buttons, inputs, tags and segmented controls are fully rounded (999px); cards 16px, inset tiles 12px.
- Facts in inset tiles. A value the user decides on (title ID, version, firmware, size) sits in a
darker
.tile: a muted label above a large value. - Colour never works alone. A format has its hue and its label; a rating its tint and an icon (✓ – ✕) and text; a selected segment is tinted and ringed and bold.
- Contrast. Text is at least 4.5:1 on its ground, the hero glow and the alert bar included. The CSS comments give the measured ratio wherever a pair is tight.
- Fits the window. Default 1200×820, minimum 900×620 (
main.rs); the web build down to 360px wide. No horizontal page scroll; long lists scroll inside their own box. - Offline and CSP-safe. System fonts, inline SVG icons, the logo bundled by Vite; no remote assets,
no injected
<style>. The only inline styles are CSS variables set through React'sstyle(CSSOM).
Tokens (:root)
| Token | Value | Use |
|---|---|---|
--page |
#0e0e15 |
Window background, sticky header |
--card |
#17171f |
Cards |
--inset |
#101017 |
Tiles, inputs, folds, table and list wells |
--raise / --raise-hi |
#24242f / #2e2e3b |
Secondary buttons / hover, the selected tab |
--line / --line-hi |
white 7% / 12% | Borders and row dividers / input borders |
--fg / --fg-2 |
#f2f2f7 / #d2d2dc |
Text / secondary text (paths, checks) |
--muted |
#9a9aae |
Labels, meta, unselected segments (≥ 4.5:1 on every ground) |
--accent / --accent-ink |
#3d6bff / #9db6ff |
Switch on, progress, empty-state icon / link text |
--orange |
#ff7a2f |
Glow only (logo shadow, hero and alert-bar edge), never text |
--good / --bad / --warn |
#2fd27a / #ff6b6b / #ffb547 |
Status icons and text |
--focus |
#9db6ff |
2px focus ring on every control (:focus-visible) |
--mono |
ui-monospace, SFMono-Regular, Menlo |
Paths, file names, IDs, param.json, logs |
--tab-lz4, --strap / --strap-hi |
#1c1c26, yellow 12% / 20% |
The LZ4 screen tab's ground and straps |
--lz4-ink / -ink-hi / -soft |
#ffd84d / #ffe27a / #e3c766 |
LZ4 yellow text: the tab, its pickers, Choose source…, LZ4 job tags, the Library's LZ4 badge |
--lz4-tint / -tint-hi / -line |
yellow 14% / 22%, ring 50% | Their tint, hover and ring (--lz4-ink on tint ≥ 9.3:1, --lz4-soft 11.4:1) |
--library-ink / -ink-hi / -line |
#a8ed8b / #c4f6aa / green 50% |
Library green: its header icon and the selected view button only |
--neon-* |
lime, magenta, cyan 12%; ring lime 38%; ink #d6e875 |
A traced source's LZ4 (AMPR) tile |
--pk-* |
green 14%, cyan 12%; ring 42%; ink #86f5b4 |
A packed source's tile and Save as profile |
count badge (.count) |
#2f5bff + white |
Active jobs on the Convert tab, the Library's count (5.2:1); grey in Inspect's sub-tabs |
| primary button | #fff + #0e0e15 |
The one main action per card |
Format hues. .fmt-* sets --h (text), --h-bg (tint) and --h-line (ring); text on tint ≥ 4.5:1.
| Format | Class | Hue |
|---|---|---|
| Folder | fmt-folder |
white #ececf4 |
| exfat | fmt-exfat |
magenta #ffa3e6 |
| ffpkg | fmt-ffpkg |
blue #a3c0ff |
| ffpfs | fmt-ffpfs |
teal #7ee0e0 |
| ffpfsc | fmt-ffpfsc |
amber #ffc580 |
| fpkg | fmt-pkg |
green #8fe0ab (Convert's default target) |
| LZ4 packed folder | fmt-lz4 |
lime #d6e875 (only the LZ4 tab's Pack target picker offers it) |
Semantic tags. Tinted pills for states: .tag.blue Writing, .tag.green Done, .tag.red Failed,
.tag.orange Backport, .tag.violet DLC. A plain .tag is neutral (Emulators, AMPR runtime).
Type. System font (SF Pro on macOS), 14px/1.45 body.
| Size / weight | Use |
|---|---|
| 20/600 | Game title |
| 18/600 | Tile values (tabular numbers) |
| 18/500 | Card titles |
| 16/600 | Empty-state, modal and Library titles |
| 13.5/600 | Job names |
| 13 | Controls and body copy |
| 12.5 | Labels, meta, notes |
| 12 mono | Paths |
| 11.5 | Hints, under-tile notes |
Spacing. Page gutters 28px, content max 1200px, 16px between cards, card padding 20–22px, 14px from card head to body, 12px between groups in a card, 8px between tiles and tags. Two columns from 880px; Inspect splits 5:7 from 1100px.
Components (class names in styles.css)
-
Header (
.topbar, sticky, no bottom border): the 30px logo left, the screen tabs centred (.seg.nav: 14px labels, a 1% white fill over a 28px backdrop blur), the GitHub mark (.gh-link, 32px round,--muted) rightmost; the app opens it in the default browser through the fixed-URLopen_repocommand. No wordmark and no version: the window or page title carries both ("PS5 Dump Forge v…" in the app, "Dump Forge v…" in the web build;APP_NAMEinapi.ts). Under 900px: two rows, logo and actions over the full-width tabs (--top98px); the tab bar scrolls inside itself. At ≤ 420px the tab padding tightens so all five tabs fit at 360px. -
Segmented control (
.seg,.seg-opt,.on): the screen tabs (Convert, Inspect | LZ4 | Library | About: one tablist,.tab-sepdividers drawn only, arrow keys move through every tab), Inspect's sub-tabs, the format picker (.seg.formats) and the LZ4 tab's scenario and action pickers. Native radios orrole="tab"sit underneath for the keyboard.- The LZ4 screen tab (
.seg-opt.tab-lz4): a--tab-lz4pill with four static yellow straps from top right to bottom left and--lz4-inktext; selected,--raise-hi,--strap-hi,--lz4-ink-hi, bold and a--lz4-linering. The Library tab is plain, with the books icon (16px) before its text. - Format picker: the selected segment fills with its format tint and ring, bold; unselected segments are plain muted text. Segments are sized by their labels, then share the spare width.
- Smaller pickers of the same kind (
.seg.formats.inner): the image inside an ffpfsc (shown only for that target) and the fpkg Compression picker (Fast / Balanced / Smallest, 15px bolt, scale and compress icons, the fpkg tint), each with one line under it on what the choice costs. - The ffpfsc Compression level is a native range (
.range,accent-color, 0–9, default 6) with its value in the label row (.range-value, tabular).
- The LZ4 screen tab (
-
Card (
.card, no border:--cardon--pageplus a shadow;.card-head= 18px icon, 18px title, actions right;.card.compact)..card.heroadds the glow: two radial gradients from the top right corner, faded in once its colours are known (.lit). -
Buttons. Every button is a pill and may lead with an icon.
Kind Class Use Primary .primaryWhite. One per card (Build, Pack, Choose folder… on an empty Library). Secondary default Dark. Choose…, Change…, Show in Finder (Explorer, folder), download links ( a.button).Danger .dangerRed tint. Stop job, Stop jobs and quit, the trash buttons, Yes, delete. Small .smallJob rows; .small.icon-onlyis a 30px round icon button.LZ4 .lz4-btnYellow tint, ring and ink: the LZ4 tab's Choose source…. Help .help-btnThe danger colours on a harmless button: How LZ4 works (experimental, read first). Neon .neon-btnSave as profile: the packed tile's static green straps, its label wraps; disabled drops the straps. Link .linkQuiet text action (Compare formats). -
Inputs: pill,
--insetfill, mono for paths. A generated output name shows whole in.out-box, its folder on the.out-dirline below; a file name is never clipped. A typed name that can't work (another image extension, or longer than core's name limit) is refused in.field-errorunder the box, and Build stays off. -
Right-to-left (Arabic,
<html dir="rtl">): the layout mirrors through flexbox and logical properties (margin-inline-*,padding-inline-*,text-align: start/end); the few physical positions (switch knob, menus, the LZ4 badge, the LZ4 job edge, the select's chevron) have[dir="rtl"]overrides at the end of styles.css. Paths, file names and the output name stay left-to-right, isolated from the text around them. -
Language select (
select.lang-select, right in the About card's header after a muted "Language" label,.head-lang): the one dropdown. A native<select>drawn as a small pill (button.small's 30px and 12.5px,--inset,--line-hiborder,--focusring),appearance: nonewith thechevronicon in--mutedas its background; options are System, then each language in its own words (langset), so it holds any number of languages. -
Switch (
.switch): a native checkbox drawn as a switch, blue when on. Disabled, only the track dims; the reason sits under it as a.note-line.warn. Remove backport (only for a source with backport libraries) and Full verification (off by default) each sit in their own inset box (.field.backport,.field.verify:--inset,--lineborder, 12px radius). Convert's Update AMPR switch sits in the Full verification box (a second.switch, with a one-line hint) and shows for an AMPR title, not packed, whose own ampr_emu names a version older than the bundled one; on for each new source. A combination core would refuse disables Build with the reason in a.note-line.blockbeside it. -
LZ4 tab (
Lz4.tsx): a Source card, then an "LZ4 asset packs" card whose head holds a red.small.help-btnHow LZ4 works. The empty Source is.empty.lz4(yellow icon and glow): "Choose a game that uses AMPR", an experimental lead and one.lz4-btnChoose source… with a Game folder… / Image… menu.- The scenario picker is a full-width
.seg.formatsin LZ4 yellow (.lz4-y: unselected--lz4-soft, chosen tint and ring, disabled plain--muted, no icons): Pack/Unpack (first, the default whenever it applies) and Trace. A disabled scenario carries its reason in its tooltip and in one muted "Not for this source: …" line under the picker; a title that neither uses AMPR nor is packed gets one.note-line.warninstead. - Settings sit in
.field.lz4inset boxes. Trace: in the web build, when the source has a journal, a.field.lz4.recordedbox "Recorded traces" with one wrapping download link (a.button,.downloads); then a.seg.formats.innerPatch / Unpatch picker and a box with what it changes, for an image Patch the "Room for the trace" range, and the fakelib warning. A read-only image gets one.note-line.warnand nothing else. Pack: Traces (a zip or a folder; the app's Choose… opens a Zip… / Folder….menu), locked to the source's own traces when they load by themselves (.lock-box: dashed--line-hiborder, a round lock button; unlocked, abutton.link"Use the source's traces" locks it again), a collapseddetails.fold.rules"Use a rules profile instead", then the Target picker: Folder (the LZ4 packed folder, shown as Folder) | exfat (default) | ffpkg | ffpfs. Unpack: Folder | exfat (default) | ffpkg | ffpfs. - Pack and Unpack end like Convert (Full verification, Output, Generate name; shared in
target.tsx). In.build-actions, left of Pack, the.neon-btnSave as profile (download icon, "Saving…" while it runs); the row wraps under the switch when narrow.
- The scenario picker is a full-width
-
Lead word (
Leadintarget.tsx): a note starting with "Recommended" shows that word in--good(.lead-good), one starting with "Experimental" in--warn(.lead-warn). Text colour only (≥ 9:1 on--cardand--inset); the word itself carries the meaning. -
Tags (
.tag,.tag.fmt, semantic colours): 22px pills, 12px/500. -
Format names go without the dot in the UI (ffpkg, exfat, ffpfs, ffpfsc, fpkg; Folder and LZ4 as they are): pills and pickers in their own style, running text in bold (
Proseincommon.tsx, or<b>). Paths and file names keep the extension (game.ffpkg).- Archive pills (
.fmt-archive): zip, 7z and rar share one sand hue (text 8.7:1 on its tint over--card, 9.5:1 over--inset); the label tells them apart. Sources only: the empty state's accepted kinds and an archive source's head, never a target picker.
- Archive pills (
-
Stat tiles (
.tiles,.tile,.value,.note): two per row, four from 680 to 879px; an archive source adds an "in a .7z archive · ".tagto the Size tile everywhere and, in Inspect, a.tile.wideArchive tile: the part count as the value, then "game in " (or "at the archive's root") · solid · password-protected as its note, and the part names as a.pathnote when there are several..tile.widespans the row (theLZ4 (AMPR)tile: Packed, Damaged packs, Traced or Plain). A traced source's tile is.tile.neonwherever it shows: static muted lime, magenta and cyan straps over--inset, a--neon-linering with a faint--neon-glow, the state in--neon-ink(on the brightest strap--fg12.8:1,--muted5.2:1,--neon-ink10.7:1). A packed source takes green straps instead (.tile.neon.packed,--pk-*:--pk-ink10.4:1,--muted5.0:1); damaged packs (.tile.damaged) get no straps and the state in--warn. -
Alert bar (
.alert-bar): one line of notice on a blue-to-orange gradient kept dark under the text, orange only as a glow at the right edge..alert-bar.warnis amber: the fpkg firmware limit. -
Findings (
ul.findings):liwarns (amber),li.blockfails every target (red),li.infois good news (blue). Under Build,.note-line(.warn,.block,.good) says why it is off or what to expect. -
Folds (
details.fold): DLC, backport files, emulators in fakelib, job logs, verified checks, folders checked. Collapsed by default; a list inside that can grow is a scrollable, focusable box. -
Job list (
JobList.tsx,.card.jobs): one card under the other cards on Convert and LZ4, newest first; it fills the rest of the window and scrolls inside itself. A row (.job): format tag, name,frompath, progress bar (progress, blue gradient; it never goes back and stays under 100% until the job succeeds), status tag, meta line (% · speed · time left). An LZ4 tab job (.job.lz4) has a yellow left edge and a.tag.lz4(LZ4 pack, unpack, trace, unpatch); Convert's runtime job a plain.tag"AMPR runtime". Finished: Show in Finder (Explorer on Windows, folder on Linux), the error box, or "Fast (Full) verification passed: N checks". -
Rating chip (
.chip.good/.ok/.bad): the About tab's formats table (Console speed and Size on disk). Tint, icon and text together, every text ≥ 8.9:1. About holds an introduction card (one line, then the features as a bold name plus a few words), in the web build beside a 300px How to connect on any device card (.about-row,.about-connect: a 200px QR code over the address; stacked under 880px), then the table and a.creditsblock (plain selectable text, no links). -
Empty state (
.empty): an inset well with a round blue icon (.empty-icon;.badred for an error), a 16px title, one line of help, the accepted kinds as tags, and its buttons.- Source pickers (
PickButtonsincommon.tsx): Folder (primary, folder icon) and Image/Archive (secondary, archive icon; one file dialog for images, packages and archives,.001/.z01first parts included). Once a source is chosen the same two sit in the Source card's head as.smallbuttons.
- Source pickers (
-
Modal (
.modal-backdrop,.modal): a 440px card on a blurred dark backdrop.Modal+openOverlay(common.tsx) render it in its own root with the whole app behind itinert: Escape and a click outside cancel, Tab stays inside, focus returns to the opener, a quit request closes it first, so dialogs never stack.- How LZ4 works (
.modal.help, 560px,Lz4Help.tsx): the info icon in yellow, a warn line, then Trace and Pack/Unpack as short numbered steps; the body scrolls (focusable), title and Close stay. - How to connect (
.connect-modal,openConnectinFormats.tsx): the 240px QR code over one centred 16px line, one Close. - Password (
.modal.password,password.tsx): opens when inspecting an archive answers "needs a password". The lock icon and "This archive needs a password", the path in an.out-box, one line (muted;.bad"The password is not correct." after a failed try,aria-live), a full-widthtype="password"input (.pw-input, focused, selected again after a wrong try), Cancel and a primary Unlock ("Checking…" while it runs). Cancel leaves the source locked: the Source card shows the red reason and a.smallEnter password (lock icon) in.row.locked. The password stays in memory with that source (inspect, naming and its jobs), never stored or shown. - Delete (
confirmDeleteindelete.tsx,role="alertdialog", shared by Inspect and the Library): the warn icon, "Delete this folder?" / "Delete this image?", the whole path in an.out-box, a muted line (for good, no Trash or undo), and two buttons: No (focused first) and Yes, delete (.danger). Afterwards a muted "Deleted ".
- How LZ4 works (
-
Library tab (
Library.tsx,.library): one card. Head: the green library icon, "Library" (16/600) and its count; right (.library-ctl, wraps): on a computer the chosen folder (mutedPathLine), Refresh, Choose folder…, then the Card / List buttons (.library-views, icon-only,aria-pressed, selected--library-ink-hion--raise-hiwith a--library-linering). On the PS5: the internal storage's free space first (.library-storage: drive icon, "Free 312 GB of 825 GB" in decimal GB,--muted6.5:1 with the free amount in--fg-2), then Refresh and the view buttons. Under the head the scrolling well (.library-well:--inset, 14px padding, 160px minimum, height to the viewport, focusable,aria-label"Games"); the list scrolls inside it, never the page.- Cards (
.library-cards,minmax(240px, 1fr)columns, 16px gaps): each card (--card, 14px radius, no glow, no outline) has a square cover (10px radius, apadding-top: 100%box for older WebKit), the name (600, wraps anywhere), the title ID muted, and a foot with the format pill and the actions; the path is the card's tooltip. List (.library-rows): 48px cover, name and title ID, format pill, path line and actions on one wrapping row. Covers load lazily, one at a time; a placeholder with the disc icon stands in at the same size. - LZ4 badge (
.lz4-badge, an entry with ampr_emu packs): "LZ4" in bold--lz4-inkon a liquid-glass pill: a faint white gradient, a light rim brighter on top, a soft dark text shadow, no colour of its own. On a card it sits on the cover's top-right corner (24px) over a light dark tint with a backdropblur(14px) saturate(160%) brightness(0.55)(-webkit-prefixed for the PS5 browser), so it stays readable on any cover; in a row it follows the title ID (20px, ≥ 7:1 on--card). It reads "LZ4 packed" to a screen reader; the format pill stays the entry's own. - Actions: icon-only buttons with
aria-labelandtitle("Inspect ", "Convert ", "Delete ", the last.danger). Inspect and Convert switch to that tab with the game loaded; Delete opens the shared Delete dialog, then focus moves to the next entry (or Refresh); a failure shows an orange "Can’t delete:" tag + the reason. - States: scanning, a centred spinner and "Scanning …" in the well (
role="status",aria-busy). A computer with no folder yet: a centred.emptypanel with the folder icon, Pick a folder with your library to scan and a primary Choose folder…. A chosen folder refused or failed: the same panel with the red icon, Can’t scan this folder, core's message and Choose another folder…. A failed Refresh keeps the list under an orange.tag"Can’t scan the Library:" + reason. A hit scan limit: an orange tag "Scan limit reached. This list may be incomplete." and a "Skipped locations and files" fold. No roots: "No folders are available to scan."
- Cards (
Web build only (npm run build:http, the PS5 payload's UI)
The web build names the product "Dump Forge" (the PS5 tile's name); the desktop app "PS5 Dump Forge".
- Launcher (
main-http.tsx, before the app while the tile's page starts the payload): a modal card with the spinner and "Starting Dump Forge…" (role="status"); on failure (role="alert") the warn icon, "Can't start Dump Forge" (or "Dump Forge didn't answer"), the reason muted, a primary Retry. - Address (
.address-btn, right of the logo): the server's host:port from the session'surl(never the viewer'slocation) in.address(mono 600 15px--fg-2, ellipsized first). It opens How to connect; no QR code when/api/qrfails. - Header controls (
.top-actions): "Stop Dump Forge" (.small, 14px, power icon) before the GitHub mark; under 900px it shrinks to its icon (the label stays as visually hidden text), as it does at 900–1049px while the orange.tag"Offline, retrying…" (role="status") sits before it. With jobs running it opens the quit modal (Keep running / Stop Dump Forge), then "has stopped" with how to start again; a failed stop turns it into "Couldn't stop Dump Forge" with the reason, Close and Try again (.danger). - Folder and file browser (
.modal.picker, 560px): breadcrumbs (.crumbs, mono 12px: "Drives", then each folder as a.link, the current one plain witharia-current; long paths wrap, never clipped), then an inset list (ul.rows.pick-list, scrolls inside, max 420px): Parent folder (or All drives), folders, files with their size (tabular, muted). File mode lists only the accepted extensions; zip-or-folder mode (LZ4 Pack Traces) lists folders and.zipfiles. Footer: Cancel and, in folder modes, "Choose this folder" (.primary). The next browse opens where the last one ended. - Show path (instead of Show in Finder on a finished job): the output path in an
.out-box(.path-shown), focused and selected, to copy by hand (plain HTTP has no clipboard API).
Icons come from icons.tsx: 24-unit grid, stroke 1.8, currentColor, shown at 18px (15px in small
spots). Add a path there rather than importing an icon set.
Cover-derived glow (glow.ts)
- The cover (a
data:URL) is drawn to a 24×24 canvas; pixels are bucketed by hue into 12 bins of 30°, skipping near-greys, near-blacks and near-whites. - The two most weighted saturated hues (the second at least 60° from the first) become
--glow-1and--glow-2, dimmed until the card's brightest point keeps muted text ≥ 4.9:1 (relative luminance ≤ 0.028). Cached per cover for the session. - No cover, or any failure: the brand glow (blue
rgba(61,107,255,.13)+ orangergba(255,122,47,.08)). - Old WebKit (the PS5's browser): the cover loads through
onload(nodecode(), 5 s timeout), and the::beforelayer uses four offsets (notinset) inside az-index: 0stacking context.
Motion
Only short fades and transforms: the hero glow (0.5s), the switch and fold chevrons (0.15s), the
spinners, and the dots after a waiting text (Dots, .dots: ".", "..", "..." in turn over 1.2s; a job row's
status while its stage has not moved the bar, "Reading the source", "Scanning", the picker's "Loading"). prefers-reduced-motion: reduce turns all of them off. Straps are static.
Copy
- Plain user language: say what a format is for, not its spec. The About tab (
Formats.tsx) explains the formats. - The debug package is labelled fpkg wherever it is shown; files are saved as
.pkg, and the internal id and the CLI flag staypkg. - Errors lead with a sentence a user understands ("Can't build this game as exfat:"), then quote the core's lines unchanged.