eleven / messenger Public

server+web: member apps get border-box sizing and the full pane (#1413)

* server+web: member apps get border-box sizing and the full pane

Two sizing fixes for sandboxed member apps:

- The /app-sandbox document now carries the same box-sizing:border-box
  reset as the parent app (style.css). App CSS is written against that
  environment, so under the default content-box a common idiom like
  `height:100%; padding:16px` made the app's root 32px taller than the
  frame, cutting off its bottom edge (buttons ~16px below the fold).

- The app-pane's 16px inset (apps.js framework CSS) no longer applies to
  sandbox mounts: .sandbox-pane on the pane zeroes it, so a member app's
  surface runs edge-to-edge and owns its own padding. renderApp clears
  the tag when a built-in app reuses the pane, so the padding drop never
  leaks onto non-sandbox mounts.

Co-Authored-By: Claude <noreply@anthropic.com>

* web: native-embed keeps only the bar insets around sandbox mounts

The Mac/iOS webview pads #app-pane by 16px + the native bar insets so an
app's UI starts clear of the floating bars — right for DOM-rendered apps,
but around a sandboxed member-app iframe the 16px reads as bare strips
above and below the surface. Sandbox mounts now keep only the insets.

Co-Authored-By: Claude <noreply@anthropic.com>

---------
Co-authored-by: Claude <noreply@anthropic.com>
James Adam dfc8b402e149cc63dc80b4bc47064055ca7c9850 parent 1ece1ee
5 files changed, +31 −1
  • changelog/2026-08-04-member-apps-fill-the-pane.md +5 −0
  • memberapps.go +5 −1
  • web/static/apps.js +4 −0
  • web/static/sandbox.js +5 −0
  • web/static/style.css +12 −0
diff --git a/changelog/2026-08-04-member-apps-fill-the-pane.md b/changelog/2026-08-04-member-apps-fill-the-pane.md
new file mode 100644
index 0000000..5b0b6b0
--- /dev/null
+++ b/changelog/2026-08-04-member-apps-fill-the-pane.md
@@ -0,0 +1,5 @@
+🤖 Member-built apps now fill their whole pane. Apps you've asked a bot to
+build used to sit inside a small margin, and some had their bottom edge —
+buttons included — cut off just below the window. Both are fixed: an app's
+surface now runs edge-to-edge and everything fits on screen, with no changes
+needed to the apps themselves.
diff --git a/memberapps.go b/memberapps.go
index e135b4c..174cd6c 100644
--- a/memberapps.go
+++ b/memberapps.go
@@ -295,9 +295,13 @@ func (s *Server) serveAppSandbox(w http.ResponseWriter, r *http.Request) {
// embedded in — NOT --bg (the page/sidebar colour). applyTheme forwards
// --panel/--ink and color-scheme (so the light-dark() resolves right); with no
// theme yet it falls back to transparent, never the black UA canvas.
+ // box-sizing matches the parent app's global reset (style.css) — app CSS is
+ // written against that environment, and under content-box a common idiom
+ // like `height:100%; padding:16px` overflows the frame by its padding.
w.Write([]byte(`<!doctype html><html><head><meta charset="utf-8">` +
`<meta name="referrer" content="no-referrer">` +
- `<style>html,body{margin:0;padding:0;height:100%;background:var(--panel);color:var(--ink)}</style></head><body>` +
+ `<style>*,*::before,*::after{box-sizing:border-box}` +
+ `html,body{margin:0;padding:0;height:100%;background:var(--panel);color:var(--ink)}</style></head><body>` +
`<script type="module">` + string(runtime) +
`</script></body></html>`))
}
diff --git a/web/static/apps.js b/web/static/apps.js
index dbd0d53..ab29616 100644
--- a/web/static/apps.js
+++ b/web/static/apps.js
@@ -2545,6 +2545,10 @@ export function renderApp(threadId, appName, el) {
const app = appByName(appName);
if (!app || !app.render || !el) return;
runCleanups(threadId, app.name);
+ // A member-app mount tags the pane .sandbox-pane (sandbox.js), which now
+ // strips the pane's padding; the tag must not survive onto a built-in app
+ // reusing the same pane. The sandbox render re-adds it every time.
+ if (!app.memberApp) el.classList.remove("sandbox-pane");
if (!app.incremental) el.textContent = "";
// A mini-app's render must NEVER take down the host. This surface can be the
// whole thread, and it's reached on boot (reopening your last thread) — an
diff --git a/web/static/sandbox.js b/web/static/sandbox.js
index 3409057..c5e4cfe 100644
--- a/web/static/sandbox.js
+++ b/web/static/sandbox.js
@@ -251,6 +251,11 @@ function injectSandboxStyles() {
st.id = "sandbox-style";
st.textContent = `
.sandbox-pane { display: flex; flex-direction: column; height: 100%; }
+/* Sandboxed apps own their full surface edge-to-edge — drop the app-pane's
+ 16px inset (apps.js), which is meant for built-in apps rendering DOM
+ directly into the pane. The app adds its own padding inside the frame.
+ (render() puts .sandbox-pane on the pane element itself.) */
+.thread-view.app-mode .app-pane.sandbox-pane { padding: 0; gap: 0; }
.sandbox-note { padding: 8px 12px; font-size: 13px; color: var(--muted); border-bottom: 1px solid var(--border); }
.sandbox-note.err { color: var(--danger); }
.sandbox-frame { flex: 1 1 auto; width: 100%; border: 0; display: block; }
diff --git a/web/static/style.css b/web/static/style.css
index dc69fa7..ae62725 100644
--- a/web/static/style.css
+++ b/web/static/style.css
@@ -2865,6 +2865,18 @@ html.native .thread-head {
flex: none;
height: calc(16px + var(--native-bottom-inset, 0px));
}
+/* A sandboxed member app (.sandbox-pane — the pane itself is tagged) is an
+ iframe surface that owns its own padding, so the 16px breathing room above
+ reads as a bare strip around it. Keep only the native-bar insets: the app
+ still starts clear of the floating bars, but runs edge-to-edge otherwise. */
+.native-embed #interface-pane.sandbox-pane,
+.native-embed #app-pane.sandbox-pane {
+ padding-top: var(--native-top-inset, 0px);
+}
+.native-embed #interface-pane.sandbox-pane::after,
+.native-embed #app-pane.sandbox-pane::after {
+ height: var(--native-bottom-inset, 0px);
+}
/* Command-host embed (embed.mode === "command"): the page the native Mac shell
keeps beside each thread purely to run slash commands — everything except a