From 05d197a367d77b6718488bda255bbdfc4eb556b5 Mon Sep 17 00:00:00 2001 From: Masterofnone <247910333+enonforetsam@users.noreply.github.com> Date: Sun, 23 Aug 2026 11:49:17 +0800 Subject: [PATCH 01/31] App shell for phones: a bottom nav, one section as a sheet, the art keeps half the screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Danial: "what if we made it more like an app — nav buttons to open sections, so the design always occupies at least half the screen." Below 880px in portrait (phones and iPad mini upright; landscape keeps this week's side-by-side split) the studio now is that app: the artwork, chip bar and tray fill the screen above a five-tab nav — Canvas (Source + Size), Colour, Engine, Look, Export — and a tab opens its panel section as a sheet between art and nav. The sheet is 42dvh, capped at 50dvh minus the nav so the stage can never drop under half the screen (on a 667px phone 42dvh alone left 49.6%); it scrolls inside; the same tab, the ✕, or a pull on its grip closes it. The art stays fully live with a sheet open — that is what the half is for — so a tap on it does not dismiss the sheet. It is pure state. `data-sheet` on .wrap names the open section; CSS shows that one section and sizes stage and sheet from it, the canvas re-measures on the change, and the tray folds if the card would drop under its floor. Nothing is re-wired: every control keeps its boot binding, Source and Size share the Canvas sheet by sharing a data-nav, a collapsed section (Output boots that way) is expanded by the shell because inert is not a CSS matter, the chip-bar docks and the section sheet are mutually exclusive, and the colour editor and docks sit on the nav instead of the screen edge. revealSection opens the right sheet first, so a tray layer row still jumps to the controls. The status line, which had no home left, speaks once as a toast over the art when its message changes. The drag grip and its clamps are retired here; the stage height is a constant of the layout now. The shell's media query — `(max-width:879px) and (not ((orientation:landscape) and (min-aspect-ratio:4/3)))`, level-4 `not` — is one string in CSS and JS, pinned equal by tests/app-shell.test.js along with nav↔section parity, so a tab can never point at nothing and the shell can never switch on in JS where its CSS did not. A browser that cannot parse it keeps the stacked layout. Verified headless with touch at 390x844, 375x667 and 744x1133 (iPad mini): boot with the art full, toast on shuffle, every tab opens only its section, stage ≥ 50% with a sheet open (51.4 / 50.0 / 53.1%), tile taps select, a slider drags, Export expands the collapsed Output, pull-down and ✕ close, Canvas shows both sections, a dock closes the sheet and sits on the nav, a tray row opens Engine, the colour editor sits above the nav, rotation leaves and re-enters the shell cleanly; landscape and 1280x900 unchanged. 140 tests. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LcPnaMoccEmKYGA4nrZXd6 --- docs/MAP.md | 4 +- index.html | 315 +++++++++++++++++++++++++++++++++++--- tests/app-shell.test.js | 52 +++++++ tests/section-map.test.js | 2 + 4 files changed, 348 insertions(+), 25 deletions(-) create mode 100644 tests/app-shell.test.js diff --git a/docs/MAP.md b/docs/MAP.md index ab6035a..4e9b7f5 100644 --- a/docs/MAP.md +++ b/docs/MAP.md @@ -51,6 +51,7 @@ Zones, in file order: | pickers | the thumbnail tiles, the swipeable strips they live in, and the aspect fader | `---------- pickers:` | | shelves and footer | the look shelf, status line, signature and credits | `---------- shelves and footer:` | | mobile layout | artwork pinned to the top, controls scrolling below, drag-to-resize grip | `---------- mobile layout:` | +| app shell layout | portrait phones + small tablets — a bottom nav, one section as a sheet, the art keeps at least half the screen | `---------- app shell layout:` | | panel themes | METAL (default, above) + carbon/phosphor/paper/neon | `---------- panel themes:` | | per-theme materials | each theme builds its own keys and knobs on top of the shared flat base above | `---------- per-theme materials:` | | per-theme dial rings, pucks and the aspect slider | the two remaining chrome parts | `---------- per-theme dial rings, pucks and the aspect slider:` | @@ -126,6 +127,7 @@ Zones, in file order: | clear | drop the image source and go back to a pure field | `---------- clear:` | | source disclosure | the 00 Source panel opens itself the moment an image lands | `---------- source disclosure:` | | mobile panel sheet | drag the grip to trade panel height for artwork | `---------- mobile panel sheet:` | +| app shell | bottom nav + one section at a time on portrait phones — the art keeps at least half the screen | `---------- app shell:` | | share links | #p= is comma-joined numbers in an EXACT, append-only order | `---------- share links:` | | persistence | the URL hash and localStorage are rewritten on every change | `---------- persistence:` | | local saved designs | private saves that stay in this browser, hash plus thumbnail | `---------- local saved designs:` | @@ -175,5 +177,5 @@ Zones, in file order: --- -127 sections. Anything unbannered is a gap in the map: add a banner in the +129 sections. Anything unbannered is a gap in the map: add a banner in the surrounding style rather than growing an orphan block. diff --git a/index.html b/index.html index 77ef87e..eed4184 100644 --- a/index.html +++ b/index.html @@ -1310,6 +1310,101 @@ #modePill .modeOpt:last-child{border-radius:0 7px 7px 0} } + /* ---------- app shell layout: portrait phones + small tablets — a bottom nav, one section as a sheet, the art keeps at least half the screen ---------- + Below 880px and not landscape (landscape keeps the side-by-side split above): the stage + fills everything above a five-tab nav; a tab opens ITS panel section as a sheet of 42dvh, + and the stage shrinks above it rather than being covered — the artwork is the point of the + app, so it never drops under half the screen. Same DOM, same ids, same wiring: the nav only + decides which section is visible. The drag grip and its clamps are retired here (the sheet + height is a constant), the phone title/meta/status rows are gone (status speaks as a toast), + and the docks + colour editor sit above the nav instead of on the screen's edge. + The query string is mirrored VERBATIM in the shell JS (APP_Q) and pinned by a test. */ + #appNav,.sheetHead,#toast{display:none} + @media (max-width:879px) and (not ((orientation:landscape) and (min-aspect-ratio:4/3))){ + .wrap{--navH:calc(56px + env(safe-area-inset-bottom, 0px));--sheetH:0px} + /* 42dvh, but never so tall that the art drops under half the screen once the nav's 56px + is counted — on a 667px phone 42dvh + 56 left 49.6% */ + .wrap[data-sheet]{--sheetH:min(42vh, calc(50vh - var(--navH)));--sheetH:min(42dvh, calc(50dvh - var(--navH)))} + html:not(.embed) .wrap .stage{ + flex:none; + height:calc(100vh - var(--navH) - var(--sheetH));height:calc(100dvh - var(--navH) - var(--sheetH)); + --stageH:calc(100vh - var(--navH) - var(--sheetH) - 24px);--stageH:calc(100dvh - var(--navH) - var(--sheetH) - 24px); + transition:none; + } + html:not(.embed) .wrap .panel{ + flex:none;height:var(--sheetH);min-height:0;overflow:hidden; + display:flex;flex-direction:column; + padding:0 var(--panel-pad) 0;border-top:1px solid var(--line); + } + html:not(.embed) .wrap:not([data-sheet]) .panel{display:none} + /* the stage is a screen now, not a box: chip bar at the top, tray at the bottom, the art + centred in what is left (auto margins absorb the slack a square piece leaves) */ + html:not(.embed) .wrap .stageInner{height:100%} + html:not(.embed) .wrap .card{margin:auto} + #panelHandle{display:none} + .mobTitle,.panelMobileMeta,.panel > header,.panel footer{display:none !important} + .sheetHead{ + display:flex;align-items:center;gap:10px;flex:none; + padding:10px 0 9px;border-bottom:1px solid var(--line); + touch-action:none;-webkit-user-select:none;user-select:none; + } + .sheetGrip{width:36px;height:4px;border-radius:3px;background:var(--line);flex:none} + .sheetTitle{flex:1;min-width:0;font-size:10px;letter-spacing:.18em;text-transform:uppercase;font-weight:700;color:var(--ink)} + #sheetClose{ + flex:none;min-width:36px;min-height:32px;padding:0;font-size:20px;line-height:1;letter-spacing:0; + background-image:none;box-shadow:none;background-color:transparent;border:1px solid var(--line);color:var(--soft); + } + .panelScroll{flex:1;min-height:0;overflow-y:auto;padding-bottom:14px} + .panel section{display:none;border-top:0;box-shadow:none;padding-top:10px} + .panel section .sec-title{display:none} + .wrap[data-sheet="canvas"] .panel section[data-nav="canvas"], + .wrap[data-sheet="colour"] .panel section[data-nav="colour"], + .wrap[data-sheet="engine"] .panel section[data-nav="engine"], + .wrap[data-sheet="look"] .panel section[data-nav="look"], + .wrap[data-sheet="export"] .panel section[data-nav="export"]{display:block} + /* a sheet is never a collapsed section: the JS expands it (for inert); this is the belt */ + .wrap[data-sheet] .panel section.collapsed .sec-body{grid-template-rows:1fr} + .wrap[data-sheet] .panel section.collapsed .sec-inner{opacity:1;pointer-events:auto} + #appNav{ + display:flex;align-items:stretch;flex:none;position:relative;z-index:35; + height:var(--navH);padding:0 4px env(safe-area-inset-bottom, 0px); + background:var(--panel-metal);border-top:1px solid var(--line); + } + .navBtn{ + position:relative;flex:1 1 0;min-width:0; + display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px; + padding:6px 2px;border:0;border-radius:0; + background:transparent;background-image:none;box-shadow:none;text-shadow:none; + color:var(--soft);font-size:8px;letter-spacing:.12em;text-transform:uppercase; + cursor:pointer;touch-action:manipulation; + } + .navBtn:active{transform:none;background-image:none;box-shadow:none} + .navBtn svg{width:22px;height:22px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round} + /* the global button.on (a pressed, ink-filled key) must not reach the nav: an active tab is + a lit icon and a bar, not a filled block */ + .navBtn.on{color:var(--ink);background-color:transparent;background-image:none;box-shadow:none;transform:none;text-shadow:none} + .navBtn.on::before{content:'';position:absolute;top:-1px;left:50%;width:28px;margin-left:-14px;height:2px;background:var(--ink);border-radius:0 0 2px 2px} + html.embed #appNav{display:none} + /* the docks and the colour editor sit on the nav, not under it */ + #cursorDock,#frameDock,#textDock,#designDock{ + bottom:var(--navH); + max-height:min(50vh, calc(100vh - var(--navH) - 80px)); + max-height:min(50dvh, calc(100dvh - var(--navH) - 80px)); + } + #colourMore{bottom:calc(var(--navH) + 8px)} + #toast{ + display:block;position:absolute;left:8px;right:8px;top:40px;z-index:6;pointer-events:none; + text-align:center;font-size:9px;letter-spacing:.12em;text-transform:uppercase;line-height:1.4; + background:rgba(6,6,10,.84);color:#e8e8e4;padding:7px 10px;border-radius:7px; + opacity:0;transform:translateY(-4px);transition:opacity .18s ease,transform .18s ease; + } + #toast.show{opacity:1;transform:none} + @media(prefers-reduced-motion:reduce){#toast{transition:none}} + /* a tablet gets a centred sheet, not a full-width slab */ + @media (min-width:600px){ + html:not(.embed) .wrap .panel{width:min(100%, 600px);margin:0 auto;border-left:1px solid var(--line);border-right:1px solid var(--line);border-radius:14px 14px 0 0} + } + } /* ---------- panel themes: METAL (default, above) + carbon/phosphor/paper/neon. A theme is a personal preference (localStorage fluid:theme), never part of the share hash. Each theme re-scopes the .panel palette variables and flattens the @@ -1867,6 +1962,8 @@
+ +
+ +
+ + Section + +

Fluid v3

Generative backgrounds, wallpapers & OG images. Design, export, or embed.
@@ -2133,7 +2237,7 @@

Fluid v3 -
+
00 — Source
@@ -2169,7 +2273,7 @@

Fluid v3 -
+
01 — Size