softr-vibe-coding 2.13.3 → 2.13.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,17 @@
1
- # Styling Softr's Native Shell (Header · Footer · Page Background) via Custom Code
1
+ # Styling Softr's Native Shell (Header · Sidebar · Footer · Page Background · App Frame) via Custom Code
2
2
 
3
- **This is NOT about Vibe Coding blocks.** Softr's top bar, navigation, dropdown menus, footer, and the page background are part of the *native app shell* — configured in Softr Studio and rendered in the **main document**, not inside a block's shadow DOM. You **cannot** build or replace the native chrome itself as a Vibe Coding block. To re-skin it, add **CSS to Settings → Custom Code → Code inside header** (the same place brand fonts/tokens live — house convention keeps that CSS in a `custom-code-header.html` file in the project folder and pastes it into the setting; you author it from the project's DESIGN.md tokens, since dembrandt supplies the palette and webfont URLs but generates no Softr-ready CSS — see [dembrandt.md](dembrandt.md#authoring-custom-code-headerhtml-from-designmd)). Pure CSS — no markup, no JS — and the native chrome stays in place, so Softr's auth-aware nav (account menu, sign-out, user-group gating) keeps working. (Separate pattern, different problem: a landing page with the native header **hidden** can carry a block-owned in-block header — see [Restyle vs. replace vs. block-owned header](#restyle-vs-replace-vs-block-owned-header).)
3
+ **This is NOT about Vibe Coding blocks.** Softr's top bar, sidebar, phone tab bar, navigation, dropdown menus, footer, and the page background are part of the *native app shell* — configured in Softr Studio and rendered in the **main document**, not inside a block's shadow DOM. You **cannot** build or replace the native chrome itself as a Vibe Coding block. To re-skin it, add **CSS to Settings → Custom Code → Code inside header** (the same place brand fonts/tokens live — house convention keeps that CSS in a `custom-code-header.html` file in the project folder and pastes it into the setting; you author it from the project's DESIGN.md tokens, since dembrandt supplies the palette and webfont URLs but generates no Softr-ready CSS — see [dembrandt.md](dembrandt.md#authoring-custom-code-headerhtml-from-designmd)). Pure CSS — no markup, no JS — and the native chrome stays in place, so Softr's auth-aware nav (account menu, sign-out, user-group gating) keeps working. (Separate pattern, different problem: a landing page with the native header **hidden** can carry a block-owned in-block header — see [Restyle vs. replace vs. block-owned header](#restyle-vs-replace-vs-block-owned-header).)
4
4
 
5
- This doc covers the **header / nav / dropdowns**, the **footer**, the **floating "island" treatment** for both, and the **page background** — which is trickier than it looks, because Softr stacks the same fill on several layers.
5
+ This doc covers the **header / nav / dropdowns**, the **footer**, the **floating "island" treatment** for both, the **page background** — which is trickier than it looks, because Softr stacks the same fill on several layers — and the **app frame**: in an app with Softr's sidebar navigation, painting around the top bar and sidebar so the content reads as one sheet inside them ([App frame](#app-frame-navigation-layout)).
6
6
 
7
- > **Mirror of the block rule.** Global `custom-code-header.html` CSS reaches native chrome (main document) but **not** blocks (shadow DOM). Inside a block you apply brand styles inline; for native chrome you apply them with this global CSS. (See [anti-patterns.md](anti-patterns.md) for the block side.)
7
+ > **Mirror of the block rule.** Global `custom-code-header.html` CSS reaches native chrome (main document) but **not** blocks (shadow DOM). Inside a block you apply brand styles inline; for native chrome you apply them with this global CSS. (See [anti-patterns.md](anti-patterns.md) for the block side.) Of a Vibe block it reaches only the **host `<div>`**, which sits in the main document — that is how the app frame makes a block's background transparent — never anything inside the shadow root. Native Softr blocks render in the main document, so it reaches them whole.
8
8
 
9
9
  ## Selector discipline — the #1 rule
10
10
 
11
- Softr's rendered markup carries two kinds of classes:
11
+ Softr's rendered markup carries two kinds of names:
12
12
 
13
13
  - **Hashed build classes** like `f8f11e5_m9ntthp` — **NEVER target these.** Softr regenerates the hash on every deploy, so your rules silently die.
14
+ - **Hashed CSS variables** like `--_5f91d6c_vnohg20` — same rule: **never target them and never `var()` them.** Softr's theme colours reach the page through these (the sidebar fill, the theme background), so when you need a theme value, copy it by hand and keep it in your own token ([App frame](#app-frame-navigation-layout) does this). The prefix belongs to a **native block package**, not to the app: on 2026-10-05 the prefixes carried a leading underscore, `_5f91d6c_` on the navigation and 404 blocks and `_03ef538_` on the Account settings (user-accounts) block; the June 2026 notes recorded `f8f11e5_` without one. Don't hard-code a prefix either; it regenerates with the hash.
14
15
  - **Stable hooks** — target these instead:
15
16
 
16
17
  | Element | Stable selector |
@@ -22,8 +23,24 @@ Softr's rendered markup carries two kinds of classes:
22
23
  | Nav buttons / dropdown triggers | `.softr-nav-button` |
23
24
  | Overflow "…" trigger | `.softr-nav-category` |
24
25
  | Active / current link | `.softr-nav-link[data-active="true"]` |
26
+ | Active-link underline | `.softr-nav-link::before` — a 2px bar 41px down the 56px bar (verified live 2026-10-05) |
25
27
  | Open dropdown trigger | `.softr-nav-button[aria-expanded="true"]` |
26
28
 
29
+ **Navigation-layout shell** — apps whose Studio navigation puts a sidebar beside the content (seen in one such app's `featureFlags` as `navigationLayout: true`; whether the flag marks sidebar apps is untested, so test for `.softr-sidebar` instead). All verified live 2026-10-05:
30
+
31
+ | Element | Stable selector | Notes |
32
+ |---|---|---|
33
+ | Page grid | `#page-content` (classes `content spr-content-root`) | CSS grid: `grid-template-areas: "topbar topbar" "sidebar main" "bottombar bottombar"`, `grid-template-columns: auto minmax(0, 1fr)`. Children: the three roots below, the navigation placeholder and `#main-content` |
34
+ | Top bar root | `#topbar-root` | grid-area `topbar`; sticky, top 0, z-index 800; the bar is 56px tall |
35
+ | Sidebar root | `#sidebar-root` | sticky, top 56px, z-index 1 (static on phones). **Also present on phones, empty and 0px wide** — scope rules on `.softr-sidebar`, never on this id |
36
+ | Sidebar | `.softr-sidebar[data-testid="sidebar"]` | Paints the Studio theme colour. `[data-open="true"]` 280px by default; `[data-open="false"]` 57px, collapsed from the top-bar toggle |
37
+ | Sidebar resize handle | `.softr-sidebar > [role="separator"]` | Drag range 200–360px (`aria-valuemin` / `aria-valuemax`). Softr already keeps it at opacity 0 until hover — no CSS needed to hide it |
38
+ | Sidebar toggle | the top-bar `<button>` holding a visually hidden span "Toggle sidebar" | No `aria-label` and no `softr-*` class; find it by that text in scripts and tests |
39
+ | Phone tab bar root | `#bottombar-root` | sticky (not fixed), z-index 800. Also present on desktop, 0px tall |
40
+ | Phone tab bar | `ul.softr-bottombar[data-testid="bottombar"]` | Only below a 768px window. White; items are `a.softr-nav-link[data-active]`, plus buttons that open dialogs (`aria-haspopup="dialog"`) |
41
+ | Content column | `main#main-content` | grid-area `main`; `display: flex; flex-direction: column; min-height: 100dvh`; transparent. Every block's wrapper is a child of it |
42
+ | Navigation placeholder | `.spr-navigation-placeholder` | The nav block's own node, 0×0; its UI renders into the three roots, so there is nothing to style here |
43
+
27
44
  **Dropdown menus have NO `softr-*` class** — they're **Radix UI**, so target ARIA / Radix attributes (stable across deploys):
28
45
 
29
46
  | Element | Stable selector |
@@ -75,7 +92,8 @@ Scope dropdown rules under `.softr-topbar` (Softr renders the header menu *insid
75
92
 
76
93
  - **Nav font defaults to Inter.** Your brand `@font-face`/`<link>` loads globally, but the bar's `font-family` is set on Softr's classes — you must target `.softr-nav-link` / `.softr-nav-button` to change it.
77
94
  - **Icon + label color** comes from Softr's classes, so a plain `color:` on the link often doesn't take — use the `* { color: inherit !important }` trick above (SVGs use `currentColor`, so they follow too).
78
- - **Custom code renders on the PUBLISHED app only — not in the Studio editor.** The header looks unchanged in the builder; always verify on the live app.
95
+ - **Header custom code renders on the published app AND in Softr's preview — not in the Studio editor.** The preview half is verified live 2026-10-05: a fresh preview load, with nothing injected, applied the app-level header code (seen after a publish; whether the preview shows a pasted but unpublished change is untested — inject it instead, see [browser-checks.md](browser-checks.md#testing-custom-code-header-css)). The editor canvas is still not known to render it: the header looks unchanged in the builder, so check in preview or on the live app.
96
+ - **Confirm the code is live from the page source (verified live 2026-10-05).** The published page's HTML carries the app-level code as `appCustomHeaderCode: "…"` inside `SoftrPageRenderer.render({…})` — an unquoted key in an inline script, not a JSON key, so search for `appCustomHeaderCode:` without quotes around the key. `appCustomHeaderCode: ""` means nothing is published. Every page carries it, `/login` and a 404 page too, so a logged-out fetch of the published app is enough. `pageCustomHeaderCode` sits beside it: **page-level** header code also exists, so check it too when a page behaves differently from the rest. To test CSS before it is pasted, see [browser-checks.md](browser-checks.md#testing-custom-code-header-css).
79
97
  - **Account avatar on a dark bar:** Softr's logged-in account button can blend into a dark bar — give it a contrasting ring if you darken the surface.
80
98
 
81
99
  ## Gotcha: dropdown panel has a tall blank gap below the items
@@ -188,14 +206,26 @@ Two details worth copying:
188
206
  never appears under the logo or the social glyphs, and use `currentColor` so the footer's own colour
189
207
  rules keep working untouched.
190
208
 
191
- Same caveat as everything else here: **it renders on the published app only.** The Studio editor keeps
192
- showing the old arrangement, which reads exactly like "the script did not run."
209
+ Same caveat as everything else here: **it does not render in the Studio editor.** The editor keeps
210
+ showing the old arrangement, which reads exactly like "the script did not run." Check on the published
211
+ app (Softr's preview applies header code too; verified for CSS after a publish on 2026-10-05, not for a script).
193
212
 
194
213
  ## Page background
195
214
 
196
- **The trickiest one — Softr paints the SAME fill on FOUR stacked layers:** `html`, `body`, `#page-content` (stable id; classes `content spr-content-root`), AND a deeper **class-less wrapper div** nested a few levels inside `#page-content`. Style any one layer and the ones above cover it — this is why setting `body` alone appears to "do nothing."
215
+ **The trickiest one — Softr paints the SAME fill, the Studio theme background, on several stacked layers.** Measured live 2026-10-05:
216
+
217
+ | Layer | What paints it |
218
+ |---|---|
219
+ | `html`, `body` | the theme background |
220
+ | `#page-content` (stable id; classes `content spr-content-root`) | Softr's `.spr-content-root { background-color: … }` rule |
221
+ | every Vibe block host, `div[data-role="vibe-block-root"]` — it has no class: most likely the "class-less wrapper div" the June 2026 notes found (inferred; that app was not re-measured) | the block's compiled `@layer base { :host { background-color: var(--background) } }`, where `--background` maps to the theme background through a hashed variable. Not `!important`, so a main-document rule on the host wins |
222
+ | a native block's outer `<section>` | an inline hashed variable holding the theme background |
223
+
224
+ `main#main-content` itself is transparent. Style any one layer and the ones above cover it — this is why setting `body` alone appears to "do nothing", and why a block that sets no background still shows the theme white over a coloured `body`.
225
+
226
+ > **App with Softr's sidebar navigation? Use [App frame](#app-frame-navigation-layout), not this recipe.** The clear below hits every `div` inside `#page-content`. `.softr-sidebar` is one of them and paints the theme colour, so it would lose its fill too (inferred from the DOM and specificity, not injected). And native blocks' outer `<section>`s are not divs, so they keep painting the theme white.
197
227
 
198
- **Pattern: paint the backdrop on the bottom layer (`html`), then clear the duplicate fills off everything stacked above it.**
228
+ **Pattern (top-bar apps): paint the backdrop on the bottom layer (`html`), then clear the duplicate fills off everything stacked above it.**
199
229
 
200
230
  ```css
201
231
  /* 1. Paint the backdrop on the bottom layer. A layered "combo" reads premium:
@@ -212,8 +242,8 @@ html {
212
242
  }
213
243
 
214
244
  /* 2. Clear the duplicate fills stacked above <html> so the backdrop shows through —
215
- but EXCLUDE the header subtree (see gotcha). The inner content wrapper is
216
- class-less and nested deep, so clear ALL divs inside #page-content. */
245
+ but EXCLUDE the header subtree (see gotcha). The block hosts are class-less
246
+ divs nested inside #page-content, so clear ALL divs inside it. */
217
247
  body,
218
248
  #page-content { background-color: transparent !important; background-image: none !important; }
219
249
  #page-content div:not(.softr-topbar):not(.softr-topbar *) {
@@ -222,7 +252,122 @@ body,
222
252
  }
223
253
  ```
224
254
 
225
- **Gotcha — don't clear the header into oblivion.** The header (`#topbar-root` → `.softr-topbar`, *including its dropdown panel*) renders **inside** `#page-content`, so a blanket `#page-content div { background: transparent }` flattens the dropdown's white panel too. And `#page-content`'s **id specificity (1,0,1) out-specifies** class/attr rules like `.softr-topbar [role="menu"]` (0,2,0) — so the clear wins silently and your earlier menu styling vanishes. Always exclude the header subtree: `:not(.softr-topbar):not(.softr-topbar *)`. (Cards are shadow-DOM blocks → their backgrounds are untouched; the footer is a `<footer>`, not a div → safe.)
255
+ **Gotcha — don't clear the header into oblivion.** The header (`#topbar-root` → `.softr-topbar`, *including its dropdown panel*) renders **inside** `#page-content`, so a blanket `#page-content div { background: transparent }` flattens the dropdown's white panel too. And `#page-content`'s **id specificity (1,0,1) out-specifies** class/attr rules like `.softr-topbar [role="menu"]` (0,2,0) — so the clear wins silently and your earlier menu styling vanishes. Always exclude the header subtree: `:not(.softr-topbar):not(.softr-topbar *)`.
256
+
257
+ **What the div clear reaches.** Each Vibe block's **host** div — that is what removes the block's theme white — but nothing inside its shadow root, so a block's own cards and panels keep the fills the block sets. It never reaches native blocks' outer `<section>`s, which keep painting the theme background; the outer-section rule in [App frame](#app-frame-navigation-layout) is the fix (untested outside a sidebar app). The footer is a `<footer>`, not a div → safe.
258
+
259
+ ## App frame (navigation layout)
260
+
261
+ **When:** the app uses Softr's **sidebar navigation** (top bar + sidebar from a 768px window, a tab bar below it) and should read as **one application**, not as blocks stacked on a white page. The bars' theme colour wraps the content as a frame, and the content becomes one paper **sheet** whose top-left corner curves in under the top bar and the sidebar.
262
+
263
+ **The method: the header CSS paints the frame and the sheet; blocks paint nothing.** Leave the top bar and sidebar exactly as the theme draws them. Nothing needs hiding: the top bar has no border or shadow, and the sidebar's inner border is transparent (verified live 2026-10-05). The CSS extends the bars' colour to `html`, `body` and `#page-content`, turns `#main-content` into the sheet, and makes block hosts transparent. Each block on such a page is full-bleed, sets no background of its own and lays out by its own width — the block side is in [SKILL.md](../SKILL.md#app-pages-beside-softr-navigation). The header CSS cannot reach inside a block, so a block's own cards and borders are still set in the block.
264
+
265
+ ### DOM and variables (verified live 2026-10-05)
266
+
267
+ The shell selectors are in the [navigation-layout table](#selector-discipline--the-1-rule) above. Below `#main-content`:
268
+
269
+ - A Vibe block: `#main-content > div[data-block="vibe-coding-…"] > div[data-block-id] > div[data-role="vibe-block-root"]`. The last div is the **host**, with an open shadow root.
270
+ - A native block: `#main-content > div#<block id> > div > section` for the Account settings block; the 404 block is one level shallower (see the caveats).
271
+
272
+ Variables Softr sets on `:root`:
273
+
274
+ | Variable | Top bar + sidebar (window ≥ 768px) | Phone (window < 768px) |
275
+ |---|---|---|
276
+ | `--sticky-nav-height` | `56px` | empty |
277
+ | `--softr-sidebar-width` | `280px`; `57px` collapsed (drag handle range 200–360) | empty |
278
+ | `--softr-bottombar-height` | empty (blocks read 0px) | `calc(0px + 55px)` — the raw token, not a computed length |
279
+
280
+ Each Vibe host maps them for the block: `--nav-height: var(--sticky-nav-height, 0px)`, `--sidebar-width: var(--softr-sidebar-width, 0px)`, `--bottombar-height: var(--softr-bottombar-height, 0px)` (the block-side table: [quick-reference.md → Softr navigation variables](quick-reference.md#softr-navigation-variables)). Blocks use those inside CSS `calc()` ([common-patterns.md](common-patterns.md#clear-softrs-sticky-bars)). The variable says 55px for the tab bar; the rendered bar measured 57px tall.
281
+
282
+ **The layout switches on the window width, exactly at 768px:** 767px gives the phone tab bar, 768px gives the top bar and sidebar.
283
+
284
+ ### The recipe
285
+
286
+ Paste into Settings → Custom Code → Code inside header (every page). That setting takes HTML, so the rules go inside a `<style>` element, as below. Rule order matters: keep it as written.
287
+
288
+ ```html
289
+ <style>
290
+ /* App frame. Pages with Softr's navigation: the bars' colour becomes a frame around one content sheet.
291
+ Pages without it (Log in, Sign up, 404) match none of these rules and keep Softr's own colours. */
292
+ :root {
293
+ --app-frame: #A85935; /* a COPY of the Studio theme colour of the top bar + sidebar (hashed vars can't be
294
+ referenced). Change it by hand whenever the theme colour changes in Studio. */
295
+ --app-sheet: #FBF8F3; /* the app's paper colour (e.g. the DESIGN.md surface) */
296
+ --app-radius: 24px; /* the corner where the sheet meets the frame */
297
+ }
298
+
299
+ /* 1. Any page with Softr navigation (sidebar OR phone tab bar): one paper surface. */
300
+ html:has(.softr-sidebar, .softr-bottombar),
301
+ html:has(.softr-sidebar, .softr-bottombar) body,
302
+ #page-content:has(.softr-sidebar, .softr-bottombar) {
303
+ background-color: var(--app-sheet) !important;
304
+ }
305
+
306
+ /* ...and blocks stop painting the theme background on top of it:
307
+ - Vibe block hosts: their compiled :host { background-color: var(--background) } is not !important.
308
+ This scoped form is UNTESTED as written. The line verified live was the unscoped
309
+ #main-content [data-role="vibe-block-root"], which also applies on pages without navigation.
310
+ - Native blocks: the OUTER <section> only, so their nested cards, list items and form groups keep their
311
+ fills. Nesting depth differs per native block; check yours (see the caveats). */
312
+ #page-content:has(.softr-sidebar, .softr-bottombar) [data-role="vibe-block-root"],
313
+ #page-content:has(.softr-sidebar, .softr-bottombar) #main-content > div > div > section {
314
+ background-color: transparent !important;
315
+ }
316
+
317
+ /* 2. With the sidebar (window ≥ 768px): the frame colour behind everything.
318
+ Same specificity as rule 1, so it MUST come after it; phones (tab bar only) keep the paper. */
319
+ html:has(.softr-sidebar),
320
+ html:has(.softr-sidebar) body,
321
+ #page-content:has(.softr-sidebar) {
322
+ background-color: var(--app-frame) !important;
323
+ }
324
+
325
+ /* The content column becomes the sheet. Softr's own min-height: 100dvh fills short pages. */
326
+ #page-content:has(.softr-sidebar) > #main-content {
327
+ background-color: var(--app-sheet) !important;
328
+ border-top-left-radius: var(--app-radius);
329
+ }
330
+
331
+ /* 3. The pinned inverse corner. #main-content's own radius scrolls away with the page; this one rides
332
+ the sticky #sidebar-root (its containing block), so it stays under the top bar while the content
333
+ scrolls and follows the sidebar's width. Guarded by :has(.softr-sidebar) because #sidebar-root
334
+ also exists on phones, empty and 0px wide. */
335
+ #sidebar-root:has(.softr-sidebar)::after {
336
+ content: "";
337
+ position: absolute;
338
+ top: 0;
339
+ left: 100%;
340
+ width: var(--app-radius);
341
+ height: var(--app-radius);
342
+ background: radial-gradient(circle at 100% 100%,
343
+ transparent calc(var(--app-radius) - 0.5px), /* the -0.5px stop: anti-aliased edge, no seam */
344
+ var(--app-frame) var(--app-radius));
345
+ pointer-events: none; /* never blocks clicks on the sheet */
346
+ }
347
+ </style>
348
+ ```
349
+
350
+ ### Caveats
351
+
352
+ - **Decide where the menu lives first.** If the Studio menu items sit in the top bar, `.softr-sidebar` renders as an empty coloured column, and the frame reads as a blank strip (seen live 2026-10-05). For the full "app" look, put the menu in the sidebar in Studio; that variant was not built or seen, so check it.
353
+ - **The frame colour is a hand copy.** The bars paint the Studio theme colour (e.g. `#A85935`) through hashed variables, so `--app-frame` cannot reference it. Read it off the page, `getComputedStyle(document.querySelector('.softr-sidebar')).backgroundColor`, rather than taking the DESIGN.md primary: the two can differ, and in the verified app they did. If the theme colour changes in Studio and `--app-frame` does not, the frame and the bars split.
354
+ - **Rule order.** Rule 2 has the same specificity as rule 1. Put it first and sidebar pages lose the frame.
355
+ - **The Vibe-host rule's scope.** The live code used the unscoped `#main-content [data-role="vibe-block-root"]`, which applies on every page. That is harmless where the page behind the block is the same theme background (inferred; no page without navigation but with a Vibe block was tested). The recipe scopes it like every other rule; that exact form is untested.
356
+ - **Native blocks' outer section.** The child path `#main-content > div > div > section` matches the DOM measured on the Account settings block, but it was never re-injected in that form: the descendant form `#main-content section` was injected there and turned the section transparent. Nesting depth differs per native block: the 404 block's section sits one level shallower (`#main-content > div > section`), which does not matter there (no navigation). For each native block type on a navigation page, check the depth first; fall back to the descendant form only after checking the block holds no nested `<section>`s. **Side effect:** on navigation pages this overrides a background colour set on purpose in a native block's Style settings. A no-CSS alternative, setting the Studio theme background to the sheet colour, is untested.
357
+ - **Phones (window < 768px).** Softr renders no top bar and no sidebar, only the tab bar: white, with the active item in the theme colour. Only rule 1 matches, so the page is paper throughout with no frame and no corner (the guarded `::after` computes `content: none`); `#main-content` stays transparent and the paper is on `html`, `body` and `#page-content`. Without the guard the `::after` still renders on phones, where `#sidebar-root` is static, so it is placed against the page: likely off the right edge, adding sideways scroll (seen in a mock, not on Softr). Restyling the tab bar to match the frame is untested.
358
+ - **Tablets.** The bars appear from a 768px window. With the sidebar open, the content column there is only 488px wide (711px collapsed), so a block's window breakpoints fire for a column far narrower than the window. Blocks must size by their own width: [SKILL.md](../SKILL.md#app-pages-beside-softr-navigation), [common-patterns.md](common-patterns.md#measure-the-block-not-the-window).
359
+ - **Collapsed and resized sidebar.** The top-bar toggle collapses the sidebar to 57px (`data-open="false"`, `--softr-sidebar-width: 57px`), and the corner follows it (measured at left 280px open, 57px collapsed). Drag-resizing (200–360px) was not tested; that the corner follows is inferred from `left: 100%`. In one headless run the collapsed state carried over to later checks (seen once; it may persist per browser).
360
+ - **Stacking.** `#topbar-root` and `#bottombar-root` sit at z-index 800 and `#sidebar-root` at 1; block content scrolls under the top bar. A block's own sticky pane or header must clear the bars: [common-patterns.md](common-patterns.md#clear-softrs-sticky-bars). A block's modal must sit above them: no stacking context lies between a block and the page, so a fixed layer in a block at z-index 50 (shadcn's `Dialog` overlay) stays under the top bar, and one at 801 or more covers the top bar and sidebar (measured live 2026-10-06); use the in-block modal in [common-patterns.md → A modal above Softr's bars](common-patterns.md#a-modal-above-softrs-bars). Softr already sets `#main-content [data-block] { scroll-margin-top: var(--sticky-nav-height, 0px) }`, so a fragment jump to a block's outer wrapper (`div[data-block]`, with a page-assigned id such as `ai1`; the host sits two levels inside it) clears the top bar (the rule is read from Softr's CSS; a jump was not tested). Softr's floating "Made with Softr" badge (`div.made-with-softr`, fixed, z-index 1, 296px from the left beside a 280px sidebar) floats over the bottom left of the content on desktop and phones (seen in the preview). It is controlled by the app's badge setting (`showMadeWithBadge` in the page source; `false` after a later publish of the same app), so turn it off there rather than with CSS.
361
+ - **`:has()` support.** Every scoped rule needs `:has()`: Safari 15.4+, Chrome 105+, Firefox 121+ (a reviewer's judgement, not tested on each browser). A browser without it drops those rules and shows Softr's default colours (inferred).
362
+
363
+ ### How the frame was verified
364
+
365
+ Verified 2026-10-05 on a demo app with Softr's navigation layout. First by injecting the project's `<style>` (the recipe above, except that its Vibe-host rule was the unscoped form) into the preview and measuring computed backgrounds, the sheet's radius and the `::after` position at 1440, 1280, 1024, 900, 768, 767 and 390px windows, with the sidebar open and collapsed; the corner was cropped at rest and scrolled, with no seam. Then live, after the code was pasted into Code inside header and published:
366
+
367
+ - **Preview, fresh load, nothing injected.** At 1024px with the sidebar open: frame on `html`, `body` and `#page-content`; `#main-content` the sheet with a 24px corner; Vibe hosts transparent; the corner `::after` at left 280px. At 375px (after a mobile emulation and a reload): tab bar only, paper throughout, no corner. No sideways scroll at either width.
368
+ - **Published, logged out.** `/login` and a 404 page loaded the code and kept `html`, `body` and `#page-content` white, with no top bar, sidebar or tab bar. This is the real test of the `:has()` scoping.
369
+
370
+ How to run these checks: [browser-checks.md](browser-checks.md#testing-custom-code-header-css).
226
371
 
227
372
  ## Finding the element to target
228
373
 
@@ -247,12 +392,16 @@ body,
247
392
  })();
248
393
  ```
249
394
 
395
+ In an app with Softr's sidebar, lower the `1200`: the content column is the window minus the sidebar (1160px at a 1440px window), so block hosts and native sections drop out of the list otherwise.
396
+
250
397
  ## Restyle vs. replace vs. block-owned header
251
398
 
252
399
  **Restyle the native bar (recommended):** robust, global, keeps Softr's auth-aware nav (account menu, user-group-gated items) and stays editable in Studio.
253
400
 
401
+ **Paint around the bars (apps with sidebar navigation):** leave the top bar and sidebar exactly as the Studio theme draws them, and extend their colour around a content sheet: `html`, `body` and `#page-content` take the bars' colour, `#main-content` becomes one paper sheet with a rounded corner tucked under both bars, and blocks paint nothing behind themselves. Choose it when the app uses Softr's top bar + sidebar and should read as one application rather than blocks on a white page. It keeps everything restyling keeps (auth-aware nav, Studio editing) and touches no nav markup. It can sit beside a restyle only if `--app-frame` matches the colour the restyled bars then paint (untested combination). Recipe, scoping and caveats: [App frame](#app-frame-navigation-layout).
402
+
254
403
  **Replace it** (hide `#topbar-root`, inject a fully custom HTML/JS header globally): only if you need structure the native nav can't do — e.g. multi-column mega-menus with icon cards. It's **fragile**: you lose Softr's logged-in account menu + user-group gating, you must re-init the JS on every SPA route change (Softr swaps pages without a full reload), and the custom header won't render in the Studio editor. Steer users to restyle unless the structure genuinely requires replacement.
255
404
 
256
405
  **Block-owned header (landing pages only):** on a marketing/landing page where the native header is **hidden in Studio**, a full-bleed hero block can render its own `<header>` with `position: fixed` — fixed elements inside a block's shadow root still anchor to the viewport, and window scroll listeners work from block code. Proven by Softr Studio AI's own hero output (2026-08-31), and the official user guide lists "a page header" as a supported static layout. How the replace-option caveats transfer: **per-page only** and **no auth-aware nav / user-group gating** carry over (same losses as replacing globally — it's for public landing pages, not logged-in app pages); **"won't render in the Studio editor" does NOT** (a Vibe-block header renders in Studio like any block); **"manual SPA re-init" does not apply** (React owns the block's lifecycle). Two caveats of its own: don't ship it on a page where the native `#topbar-root` is still visible (the z-index contest between the block's header and the native sticky bar is untested — hide one), and it exists only on pages containing the block. Full pattern, mobile-nav requirement, and caveat set: [static-blocks.md](static-blocks.md#block-owned-landing-page-header).
257
406
 
258
- Decision order: restyle when the native structure suffices → block-owned header for landing pages that hide native chrome → global replacement only when a logged-in app needs structure the native nav can't do.
407
+ Decision order: restyle when the native structure suffices (or paint around the bars when a sidebar app should read as one application) → block-owned header for landing pages that hide native chrome → global replacement only when a logged-in app needs structure the native nav can't do.
@@ -255,6 +255,42 @@ useNavigationBlocker(function() { return dirtyRef.current; });
255
255
 
256
256
  Catches BOTH Softr's in-app SPA navigation (nav bar, sidebar, `<NavigationAction>`) AND browser unload (tab close, refresh, external links). A plain `window.addEventListener("beforeunload", ...)` does NOT catch Softr's in-app nav. See [common-patterns.md](common-patterns.md#navigation-blocker-for-unsaved-changes).
257
257
 
258
+ ## Softr navigation variables
259
+
260
+ Softr sets these on the page's `:root` when the app uses its sidebar / top-bar navigation; the Vibe host's `:host` maps them for the block, each with a `0px` fallback (measured live 2026-10-05; phone = below a 768px window).
261
+
262
+ | Page (`:root`, header CSS) | Block (Vibe host) | Desktop / tablet (≥ 768px window) | Phone (< 768px window) |
263
+ |---|---|---|---|
264
+ | `--sticky-nav-height` | `--nav-height` | `56px` (sticky top bar) | not set → host `0px` |
265
+ | `--softr-sidebar-width` | `--sidebar-width` | `280px` open, `57px` collapsed (drag handle 200–360px) | not set → host `0px` |
266
+ | `--softr-bottombar-height` | `--bottombar-height` | host `0px` | `calc(0px + 55px)` (sticky tab bar; the rendered bar measured 57px) |
267
+
268
+ ```tsx
269
+ style={{ top: "calc(var(--nav-height, 0px) + 16px)", maxHeight: "calc(100dvh - var(--nav-height, 0px) - 32px)" }}
270
+ ```
271
+
272
+ - **Use them only inside CSS `calc()`.** In JS, `getComputedStyle(...).getPropertyValue("--bottombar-height")` hands back the token string (`calc(0px + 55px)` on phones), so `parseFloat` gives `NaN` (the string is measured; the `NaN` is reasoned, not run).
273
+ - The same `:host` rule maps `--background`, `--font-family-sans` / `--font-family-serif` and `--container-max-width` onto Softr's hashed theme variables (`--_5f91d6c_…`), which is why an unpainted block still paints the Studio theme background (verified 2026-10-05). Never reference the hashed names: the prefix is per Softr block package and has changed before.
274
+ - How blocks use them: [SKILL.md → App pages beside Softr navigation](../SKILL.md#app-pages-beside-softr-navigation) and [common-patterns.md → Clear Softr's sticky bars](common-patterns.md#clear-softrs-sticky-bars). The page side (header CSS, the `:root` values): [native-chrome-styling.md → App frame (navigation layout)](native-chrome-styling.md#app-frame-navigation-layout).
275
+
276
+ ## Container queries
277
+
278
+ Lay a block out by its OWN width when it sits beside Softr's sidebar (57–360px of the window). Softr's Tailwind compiles container variants (verified live 2026-10-05):
279
+
280
+ ```tsx
281
+ <div className="@container"> {/* the container: a wrapper */}
282
+ <div className="px-4 @min-[40rem]:px-6 @min-[64rem]:px-10"> {/* resolves against the wrapper's width */}
283
+ <div className="grid grid-cols-2 @min-[52rem]:grid-cols-4">…</div>
284
+ <section className="@container …"> {/* a pane: its children follow the pane */}
285
+ <div className="text-[20px] @min-[24rem]:text-[24px]">…</div>
286
+ </section>
287
+ </div>
288
+ </div>
289
+ ```
290
+
291
+ - A container query never resolves against the element that carries `@container`, only against the nearest ancestor container — so put `@container` on a wrapper.
292
+ - Beside a sidebar, no `sm:` / `md:` / `lg:` for layout: they fire on the window. Full rules and worked thresholds: [SKILL.md → App pages beside Softr navigation](../SKILL.md#app-pages-beside-softr-navigation). When CSS can't decide, [measure the block](common-patterns.md#measure-the-block-not-the-window).
293
+
258
294
  ## Component Skeleton
259
295
 
260
296
  ```jsx
@@ -277,3 +313,5 @@ export default function Block() {
277
313
  );
278
314
  }
279
315
  ```
316
+
317
+ App pages beside Softr's sidebar navigation, inside a frame the header code paints, drop these wrappers for the full-bleed `@container` shell: [SKILL.md → App pages beside Softr navigation](../SKILL.md#app-pages-beside-softr-navigation).
@@ -151,6 +151,32 @@ the hidden part); a picker at the bottom of a dialog body opens up, inside the d
151
151
  row at the window's bottom edge opens up, as before. All four, plus a scroller outside the
152
152
  shadow root, checked in Chromium on 2026-09-30 on a test page — not yet in a deployed block.
153
153
 
154
+ **On app pages with Softr navigation, the strip doesn't reach the window's edges either.**
155
+ Softr's bars sit over the page, outside the block: from a 768px window, a 56px sticky top bar;
156
+ below it, a sticky tab bar at the bottom, 57px as rendered (Softr's variable says 55px; both bars
157
+ z-index 800, measured live 2026-10-05). The walk above starts the strip at `0` and
158
+ `window.innerHeight`, so a trigger just under the top bar
159
+ can open its menu up and under the bar, and on a phone a menu can open down behind the tab bar.
160
+ That clash is inferred from the measurements, not seen. Start the strip inside the bars (an
161
+ untested variant):
162
+
163
+ ```jsx
164
+ var SOFTR_TOP_BAR = 56; // Softr's sticky top bar, window 768px and up
165
+ var SOFTR_TAB_BAR = 57; // Softr's phone tab bar, window below 768px: measured 57px; its variable says 55px
166
+
167
+ function comboClipBox(node) {
168
+ var phone = window.innerWidth < 768; // Softr's own switch: 767px = tab bar, 768px = top bar
169
+ var top = phone ? 0 : SOFTR_TOP_BAR;
170
+ var bottom = window.innerHeight - (phone ? SOFTR_TAB_BAR : 0);
171
+ // … the ancestor walk, unchanged
172
+ }
173
+ ```
174
+
175
+ On a page without Softr navigation (log in, a landing page) the top offset only makes the
176
+ panel open upward a little less readily. The same bar heights apply to any other room check
177
+ or window scroll in a block:
178
+ [common-patterns.md → Clear Softr's sticky bars](common-patterns.md#clear-softrs-sticky-bars).
179
+
154
180
  Rule 2 does not rescue a clipped cell. The cell is the height of its row, so neither side has
155
181
  room, the list falls to its 120px floor and is clipped anyway. Rule 1 is not optional.
156
182
 
@@ -345,7 +371,8 @@ Everything else — the trigger, the card, the rows — stays flat.
345
371
  - [ ] No overflow-clipping class (`overflow-hidden`, `overflow-*-auto`, `truncate`,
346
372
  `line-clamp-*`) between the Combo and the scroller it belongs to; an over-wide chip
347
373
  bounded at the chip (`min-w-0 truncate`)
348
- - [ ] Drop-up and list `maxHeight` measured against the clipping ancestors, not the window
374
+ - [ ] Drop-up and list `maxHeight` measured against the clipping ancestors, not the window, and
375
+ on app pages inside Softr's top bar and phone tab bar
349
376
  - [ ] Keyboard: ↑ ↓ Enter Esc Tab; the active row kept visible by scrolling the list only
350
377
  (never `scrollIntoView`), and the search box focused with `preventScroll`
351
378
  - [ ] `aria-haspopup="listbox"`, `aria-expanded`, `role="listbox"` / `role="option"`,
@@ -50,8 +50,8 @@ map below was checked against the tool lists the server delivered on 2026-09-30
50
50
  **The Workflows tools have since followed** (when exactly is not known; first seen 2026-10-05). The 2026-10-05 roster delivered all 28 as `workflow_*`:
51
51
  `workflow_create`, `workflow_get`, `workflow_list`, `workflow_publish`, `workflow_update_node_inputs`,
52
52
  `workflow_get_node_specifications`, `workflow_list_node_types`, `workflow_test_node` and the rest,
53
- i.e. area first, then the old verb and object. [Workflows](#workflows) below still lists the
54
- pre-rename names. Translate them that way. That roster had no `get_workspace_integrations`;
53
+ i.e. area first, then the old verb and object. [Workflows](#workflows) below uses the new names
54
+ (re-checked against the live roster 2026-10-06: all 28 present). That roster had no `get_workspace_integrations`;
55
55
  `integration_list` covers it.
56
56
 
57
57
  Most new names are the old words reordered. These are the ones you would not guess:
@@ -195,7 +195,7 @@ For block-building work you need **Applications & Forms: Full access** (to creat
195
195
 
196
196
  ## Vibe coding block tools
197
197
 
198
- Before writing any block code through the MCP, call `vibe_coding_block_get_docs` — it returns the current version of the [Vibe Coding Developer Guide](https://docs.softr.io/vibe-coding-developer-guide), which is the authority on hook signatures if it and this skill ever disagree.
198
+ Before writing any block code through the MCP, call `vibe_coding_block_get_docs` — it returns the current version of the [Vibe Coding Developer Guide](https://docs.softr.io/vibe-coding-developer-guide), which is the authority on hook signatures if it and this skill ever disagree. On runtime *behaviour* the guide's prose can lag a live capture, and where it does this skill says so: the guide still describes `useRecords({ enabled })` as a way to defer loading (checked 2026-10-06), while a 2026-09-18 network capture showed `useRecords` fetching anyway ([reading.md](../datasources/reading.md#userecords-ignores-enabled-false)). Trust the capture until a newer one says otherwise.
199
199
 
200
200
  | Group | Tools |
201
201
  |---|---|
@@ -250,9 +250,13 @@ context. Keep the local mirror in step mechanically rather than by hand:
250
250
  sent, which is what makes it usable here: on this path you never see the merged file yourself.
251
251
 
252
252
  One encoding trap: JSON `\uXXXX` escapes inside the ops are **decoded to the real characters** on
253
- Softr's side (`"—"` is stored as `—`). The mirror must therefore hold raw UTF-8 — apply the
253
+ Softr's side (`"\u2014"` is stored as `—`). The mirror must therefore hold raw UTF-8 — apply the
254
254
  ops to it *after* JSON-decoding them, never as the escaped text, or the final comparison
255
255
  fails on every non-ASCII character.
256
+ The same decoding happens to an agent's own tool-call arguments: a `\u2014` typed into a
257
+ file-writing tool lands on disk as `—` (it put a wrong example into this very paragraph
258
+ twice, 2026-09-18 and 2026-10-06). When a file must hold a literal backslash-u sequence, build the
259
+ backslash at runtime (`chr(92)` in Python) and check the bytes afterwards.
256
260
 
257
261
  **Reach for the full replace when the change is structural** — reordering JSX, moving logic between
258
262
  components, adding a hook — where being sure of "the exact current text" of a dozen scattered fragments
@@ -421,13 +425,17 @@ preserves explicitly set permissions across a recompile, so every recompile need
421
425
  the call errors rather than lying, but an agent that batches calls can easily miss which one failed.
422
426
  5. If any action is still broader than intended (typically ADD_RECORD at `ALL_USERS`), **report it
423
427
  and let the builder decide.** Check the page's own VIEW permission first with
424
- `application_page_get_permissions`, because that is what sets the severity:
428
+ `application_page_get_permissions`, because that is what sets the severity, and report the
429
+ block's own Visibility with it (`vibe_coding_block_get_settings`): it gates the block's reads
430
+ and sets ADD_RECORD's default (2026-10-05), but whether it refuses writes on its own is untested
431
+ ([below](#what-the-server-enforces-on-a-blocks-data-endpoints)):
425
432
  - **Page VIEW is gated** (e.g. `LOGGED_IN_USERS`) — an anonymous visitor cannot load the page at
426
433
  all, so exploiting the open action means calling its endpoint directly, and the realistic worst
427
434
  case is junk records rather than data exposure or deletion. Housekeeping: worth fixing on the
428
435
  next Studio pass, not worth holding a release for.
429
- - **Page VIEW is `ALL_USERS`** — the action permission is the only gate left. That is a genuine
430
- hole and deserves to be called one.
436
+ - **Page VIEW is `ALL_USERS`** — the action permission is the only gate known to hold on writes
437
+ (the block's Visibility may also refuse them; untested). That is a genuine hole and deserves
438
+ to be called one.
431
439
 
432
440
  Report page, block, action type, data source and current group; say which of the two cases applies;
433
441
  note that a human sets them on the block's Actions tab in Studio. Then stop — **do not unilaterally
@@ -438,8 +446,10 @@ preserves explicitly set permissions across a recompile, so every recompile need
438
446
  **is** enforced on the block's datasource **records** endpoint (verified live 2026-09-18 — a
439
447
  viewer who cannot view the page gets a 403 whose message names "block/action visibility rules";
440
448
  see [below](#what-the-server-enforces-on-a-blocks-data-endpoints)). The *action* (write) endpoint
441
- was not exercised separately; the message wording suggests the same gate covers it, but that part
442
- is inference. So the reason to treat a gated page as low-severity is still the practical
449
+ refuses outsiders too: on 2026-10-05 a replayed PATCH from outside the block's group got 403
450
+ ([hubspot.md](../datasources/hubspot.md#writing)). The block and its actions were both limited to
451
+ that group, so which rule refused it is not known, and whether page VIEW alone gates writes is
452
+ untested. So the reason to treat a gated page as low-severity is still the practical
443
453
  difficulty and low blast radius — and note that "gated to logged-in users" keeps out anonymous
444
454
  visitors only: any logged-in user can view that page, and therefore reach its endpoints. Say that
445
455
  plainly rather than implying the action is safe.
@@ -462,14 +472,14 @@ Both edit paths recompile, so both reset Action permissions either way (Hard Con
462
472
  *Verified live 2026-09-18 (Softr Database; draft preview, "Preview as" different users, requests
463
473
  captured from the app iframe); the block-visibility row verified 2026-10-05 (HubSpot; preview link,
464
474
  impersonated users, direct POSTs).* A block's data lives behind per-connection endpoints —
465
- `/blocks/<blockId>/datasources/<dataSourceId>/records` for lists, `/records/<id>` for one record —
466
- and these are the gates that actually exist on them:
475
+ `/blocks/<blockId>/datasources/<connection>/records` for lists, `/records/<id>` for one record —
476
+ and these are the gates that actually exist on them (`<connection>` was recorded as the connection's id in the 2026-09-18 Softr Database capture and seen as its alias in a 2026-10-05 HubSpot capture; unresolved, and it only matters when reading a network log):
467
477
 
468
478
  | Gate | Enforced server-side? |
469
479
  |---|---|
470
480
  | **Page VIEW permission** | **Yes.** A viewer who cannot view the page gets **403** ("block/action visibility rules…") from the block's datasource endpoint — crafting the request by hand does not get around it |
471
481
  | **The block's Visibility** (`predefinedUserGroup` + `customUserGroupIds`; `vibe_coding_block_set_visibility`) | **Yes.** A viewer outside the block's group gets the same **403**, on list and by-id, even where the page lets them in. The body reads like a write error on a read: "You cannot add or edit a record because either the block/action visibility rules, user group conditions, or the user/record data in the datasource has changed." Per block: the same table on an ungated block stays open. Five code pushes left the setting intact |
472
- | **The connection's Source conditions** (Source tab / `vibe_coding_block_set_data_source_record_filters`) | **Yes — and they are the only server-side ROW gate** |
482
+ | **The connection's Source conditions** (Source tab / `vibe_coding_block_set_data_source_record_filters`) | **Yes — and they are the only server-side ROW gate.** A by-id request for a record the condition excludes answers differently per backend: **HTTP 200 with an empty body** on Softr Database (2026-09-18), **404** on HubSpot (2026-10-05). Treat both as "not found" |
473
483
  | A `where` filter in the block's code | No — it is a request parameter the caller controls |
474
484
  | Which fields the block *renders*, a second / conditional `q.select`, `enabled: false` on `useRecords` | No — the endpoint returns the union of the connection's read selects to anyone allowed to call it (see [multi-datasource.md](../datasources/multi-datasource.md#one-connection--one-read-payload-the-union-of-its-selects)) |
475
485
 
@@ -506,7 +516,8 @@ verified on HubSpot on 2026-10-05, the same way.* There are two forms, and **the
506
516
  field in this form (Leo set it in the Source tab, and it read back that way), and the same form
507
517
  works when written with `vibe_coding_block_set_data_source_record_filters`.
508
518
  **It fails closed:** a user whose field is empty, or who has no record in the users' data source,
509
- gets 0 rows, and a by-id request for a record outside the condition returns 404.
519
+ gets 0 rows, and a by-id request for a record outside the condition returns 404 (on HubSpot;
520
+ Softr Database answers HTTP 200 with an empty body).
510
521
  - **Use AND between rules.** With one rule OR and AND behave the same, but a second rule added
511
522
  under OR widens access (2026-10-05).
512
523
  - **The braced user-field spellings fail.** On 2026-09-18, on Softr Database, eleven spellings were
@@ -726,13 +737,13 @@ Softr Workflows are automations built from trigger + action nodes, and the MCP c
726
737
 
727
738
  | Group | Tools |
728
739
  |---|---|
729
- | Workflow lifecycle | `create_workflow`, `get_workflow`, `get_workflow_url`, `list_workflows`, `rename_workflow`, `update_workflow_configuration`, `publish_workflow`, `unpublish_workflow`, `test_workflow` |
730
- | Node management | `add_node`, `add_branch_node`, `create_branch`, `delete_node`, `duplicate_node`, `rename_node`, `reorder_node`, `reorder_multiple_nodes`, `replace_node`, `replace_trigger_node`, `update_node_inputs`, `update_node_note`, `update_node_continue_on_error`, `update_node_retry` |
731
- | Discovery / testing | `list_node_types`, `get_node_specifications`, `get_dynamic_input_options`, `test_node`, `get_node_output` |
740
+ | Workflow lifecycle | `workflow_create`, `workflow_get`, `workflow_get_url`, `workflow_list`, `workflow_rename`, `workflow_update_configuration`, `workflow_publish`, `workflow_unpublish`, `workflow_test` |
741
+ | Node management | `workflow_add_node`, `workflow_add_branch_node`, `workflow_create_branch`, `workflow_delete_node`, `workflow_duplicate_node`, `workflow_rename_node`, `workflow_reorder_node`, `workflow_reorder_multiple_nodes`, `workflow_replace_node`, `workflow_replace_trigger_node`, `workflow_update_node_inputs`, `workflow_update_node_note`, `workflow_update_node_continue_on_error`, `workflow_update_node_retry` |
742
+ | Discovery / testing | `workflow_list_node_types`, `workflow_get_node_specifications`, `workflow_get_dynamic_input_options`, `workflow_test_node`, `workflow_get_node_output` |
732
743
 
733
- These are the names as of 2026-10-01. By 2026-10-05 the server delivered them as `workflow_*`
734
- (`create_workflow` → `workflow_create`, `update_node_inputs` → `workflow_update_node_inputs`); see
735
- [the rename note](#tool-names--the-2026-10-01-rename).
744
+ These are the current names (live roster, 2026-10-06). Until 2026-10-01 they had no `workflow_`
745
+ prefix (`create_workflow` → `workflow_create`), and older notes, including project docs, still use
746
+ those; see [the rename note](#tool-names--the-2026-10-01-rename).
736
747
 
737
748
  **The node catalog is huge** — live-enumerated 2026-08-31: **418 node types (58 triggers + 360 actions) across 56 applications.** The parts that matter most for this skill:
738
749
 
@@ -756,13 +767,13 @@ These are the names as of 2026-10-01. By 2026-10-05 the server delivered them as
756
767
 
757
768
  **Build-loop findings (verified live 2026-09-01, first end-to-end production build — 10 workflows):**
758
769
 
759
- - **`create_workflow` instantiates an OLD version of the trigger node.** Immediately call `replace_trigger_node` with the **same trigger type** — the replacement lands at the current version with the current inputs. Example: `updateField` on `SOFTR_TABLES_RECORD_UPDATED` (fire only when a specific field changed) only exists at v1.2.0; the version `create_workflow` instantiates doesn't have it.
760
- - **FILTER node conditions are set via `update_node_inputs` with inputName `"condition"`** — the value is an `{operator, conditions: [...]}` object. The condition is stored on the FILTER node's **outgoing path**, the same way the Studio builder wires it.
761
- - **The official MCP docs disagree:** "Branch and filter conditions can't be set through MCP yet ... deciding what sends a run down each path is something you finish in the builder" (docs.softr.io/mcp/workflows, checked 2026-10-05).
762
- - **What stands on each side:** our 2026-09-01 build did set them this way. The FILTER spec today declares `inputs: {}`, but live FILTER nodes still keep their condition on the outgoing path, so the empty spec does not refute the mechanism.
763
- - **Until it's re-checked:** after setting a condition over MCP, read the workflow back (`workflow_get`) and confirm the path condition is there. If it isn't, finish the condition in the builder.
770
+ - **`workflow_create` instantiates an OLD version of the trigger node.** Immediately call `workflow_replace_trigger_node` with the **same trigger type** — the replacement lands at the current version with the current inputs. Example: `updateField` on `SOFTR_TABLES_RECORD_UPDATED` (fire only when a specific field changed) only exists at v1.2.0; the version `workflow_create` instantiates doesn't have it.
771
+ - **A FILTER condition written over MCP is inert — set it in Studio** (corrected 2026-10-06; this bullet used to say the MCP writes it). `workflow_update_node_inputs` with inputName `"condition"` accepts an `{operator, conditions: [...]}` object and stores it in the node's `inputs.condition`, a field the engine does not read. The engine evaluates the condition on the FILTER node's **outgoing path** (the `paths` entry whose `fromActionId` is the filter), and only the Studio builder writes that. **A filter built over MCP passes every run.** A 2026-09-19 audit of this very build showed it three ways: the one workflow actually running had empty `inputs` and its whole condition on the path; two others, edited in Studio afterwards, held one condition in `inputs.condition` and a different one on the path, so Studio reads and writes only the path.
772
+ - **The official MCP docs agree:** "Branch and filter conditions can't be set through MCP yet ... deciding what sends a run down each path is something you finish in the builder" (docs.softr.io/mcp/workflows, checked 2026-10-05), and the FILTER spec declares `inputs: {}`. BRANCH conditions were not tested separately here; treat them the same way.
773
+ - **How to build one:** add the FILTER over MCP if that is convenient, then open it in Studio, set its clauses and save. Never trust `inputs.condition` as a record of what the filter does; it can disagree with the path.
774
+ - **How to check one:** read the workflow back with `workflow_get` and find the `paths` entry whose `fromActionId` is the filter; the condition must be there. FILTER nodes cannot be run with `workflow_test_node`, so this read-back is the only check before a real run.
764
775
  - **`LOOP_ACTION_GROUP`'s `loopVariables.items` must reference a plain array**, e.g. `$.records` — a `[*]` projection (e.g. `$.records[*].fields.X`) is rejected by the validator. Per-item references **inside** the loop use `{loopActionGroup.<id>:::loopVariables.items.fields.<fieldId>}` (use the bracket form for ids that start with a digit).
765
- - **`update_node_inputs` batches validate against the STORED node state**, not the batch-in-progress — an update that depends on another update in the same batch fails validation. Split dependent updates into sequential calls.
776
+ - **`workflow_update_node_inputs` batches validate against the STORED node state**, not the batch-in-progress — an update that depends on another update in the same batch fails validation. Split dependent updates into sequential calls.
766
777
  - **Test-safety rules** (which `testRunMode` means what in practice):
767
778
  - Record-**write** nodes (`SOFTR_TABLES_UPDATE_RECORD` etc.) are `REAL_ONLY` — **never test them against a production workspace**; the test performs the real write.
768
779
  - `SOFTR_SEND_EMAIL` is `MOCK_AND_REAL` — **always pass `mode: "mock"`**.
@@ -37,9 +37,11 @@ SKILL.md's Premium Visual Baseline (gradient wrapper, icon-in-rounded-square hea
37
37
 
38
38
  **`container`/`content` wrappers are optional platform behavior, not platform-enforced.** Per the official developer guide: "By default, block occupies full width of the page but special classes - `container` and `content` are **available** to constrain the width [of content to match app's max width settings]" (emphasis added). The wrappers remain the strong default for app/content blocks that sit next to native blocks (width consistency — that's their whole purpose). Static marketing blocks legitimately skip them so backgrounds, images, and decorative shapes run edge-to-edge.
39
39
 
40
+ App pages are the other full-bleed case. Beside Softr's sidebar / top-bar navigation, inside a frame the app's header code paints, an app block drops the wrappers too — but it also paints no background and lays out by its own width with container queries. That case has its own rules: [SKILL.md → App pages beside Softr navigation](../SKILL.md#app-pages-beside-softr-navigation).
41
+
40
42
  When you go full-bleed:
41
43
 
42
- - **Own your horizontal gutters** — the responsive padding ramp `px-6 md:px-12 lg:px-16` on the content container is the Studio-verified shape.
44
+ - **Own your horizontal gutters** — the responsive padding ramp `px-6 md:px-12 lg:px-16` on the content container is the Studio-verified shape. It steps on the WINDOW width, which suits a full-width marketing page and not a block beside Softr's sidebar: there the block is 57–360px narrower than the window (744px wide at a 1024px window with the sidebar open, measured 2026-10-05), so use the container-query shell from the app-page section instead.
43
45
  - **Own your inner max-widths** — constrain the copy column yourself (`lg:max-w-[46%]`, `max-w-[430px]` on body text).
44
46
  - **`overflow-hidden` on the block root** if decorative shapes offset off-canvas (prevents horizontal scroll).
45
47
  - **Record the choice** in the placement comment so future edits and reviews know it's deliberate:
@@ -77,7 +79,7 @@ The caveat set — state these in any block that ships its own header:
77
79
 
78
80
  1. **Per-page only.** The header exists solely on pages containing this block. Every page of a multi-page site needs either this block or the native header, or navigation disappears.
79
81
  2. **No auth-aware nav logic.** You forfeit Softr's account menu and user-group-gated items — nav items are plain `<NavigationAction>`s. Fine for public landing pages; wrong for logged-in app pages. For the login CTA, use the auth-aware swap (`useCurrentUser()` → "Sign in" vs "Dashboard") from [common-patterns.md](common-patterns.md#auth-aware-header-cta).
80
- 3. **Don't ship both headers on one page.** The z-index outcome against a visible native `#topbar-root` (sticky, main document) is untested — hide the native header on this page, or don't use the pattern.
82
+ 3. **Don't ship both headers on one page.** The z-index outcome against a visible native `#topbar-root` (sticky, z-index 800 in the main document — measured 2026-10-05) is untested — hide the native header on this page, or don't use the pattern.
81
83
  4. **Keep transforms/filters off the header's ancestors.** `position: fixed` anchors to the viewport ONLY while no ancestor establishes a containing block — a `transform`, `filter`, `perspective`, or `will-change` on the block root (e.g. an entrance animation) silently converts the "fixed" header to absolute and lets `overflow-hidden` clip it. Leaf-element transforms (`hover:-translate-y-[1px]` on buttons) are fine. Verify in the **published app**, not just the Studio canvas.
82
84
  5. **Mobile nav is mandatory.** `hidden md:flex` on the nav with no fallback means nav items simply don't exist on phones (Studio AI ships exactly this bug). Pair the pattern with a shadcn `Sheet`-based mobile menu (hamburger → drawer listing the same `navItems` array) — the component is on the platform roster.
83
85
  6. **Landmark hygiene.** Shadow DOM does NOT hide landmarks from assistive tech, and Softr's native chrome uses semantic elements. On a page with native chrome visible, a block's own `<header>`/`<main>` creates duplicate banner/main landmarks — use plain `<div>`s there. Reserve `<header>`/`<main>` for pages where the native chrome is hidden and the block genuinely IS the page chrome.
@@ -89,8 +91,9 @@ The wider decision (restyle native vs replace globally vs block-owned) is laid o
89
91
  Heroes pair with same-page sections via hash links (nav "Capabilities" → `#capabilities` further down).
90
92
 
91
93
  - **Write anchor destinations RELATIVE**, per Hard Constraint 20: `/#capabilities` or `/page#section`. A Studio-generated hero shipped its CTA configured as the absolute self-domain form (`https://<app>.softr.app/#capabilities`, observed in the Settings pane 2026-08-31) — rewrite that form; hardcoded domains break on custom-domain publish and between staging/production.
92
- - **A URL fragment cannot target an element inside a Vibe block's shadow root** — fragment lookup stops at the shadow boundary (same family as the documented `getElementById` anti-pattern). Anchors must target something in the main document: the block/section host. Softr has a native anchor-link mechanism targeting blocks — **verify live which fragment a Vibe Coding block answers to** before promising exact syntax to a user.
93
- - **In-block scrolling** (scroll to a section inside the same block) uses refs + `scrollIntoView` with the `setTimeout(fn, 0)` rule (Hard Constraint 17), not fragments.
94
+ - **A URL fragment cannot target an element inside a Vibe block's shadow root** — fragment lookup stops at the shadow boundary (same family as the documented `getElementById` anti-pattern). Anchors must target something in the main document: the block/section host. Softr has a native anchor-link mechanism targeting blocks — **verify live which fragment a Vibe Coding block answers to** before promising exact syntax to a user. (Seen 2026-10-05: a Vibe block's outer wrapper in the main document is `#main-content > div[data-block="vibe-coding-…"]` with a page-assigned id such as `ai1`; whether a fragment for that id jumps to it is untested.)
95
+ - **The sticky top bar is already cleared for a jump to a block's outer wrapper.** Softr's page CSS gives that wrapper (`#main-content > div[data-block]`, the element with the page-assigned id, previous bullet) `#main-content [data-block] { scroll-margin-top: var(--sticky-nav-height, 0px) }`, so a fragment jump to it should stop below the 56px top bar (rule read from the page CSS 2026-10-05; the jump itself untested). The Vibe host two levels inside it carries no `data-block`, and nothing does that for content inside the block.
96
+ - **In-block scrolling** (scroll to a section inside the same block) uses refs + `scrollIntoView` with the `setTimeout(fn, 0)` rule (Hard Constraint 17), not fragments. It must add the top-bar offset itself, or the target lands under the sticky bar — for example `scroll-margin-top: calc(var(--nav-height, 0px) + 16px)` on the target (standard CSS, untested in a block). See [common-patterns.md → Clear Softr's sticky bars](common-patterns.md#clear-softrs-sticky-bars).
94
97
 
95
98
  ## Harvesting Studio-AI marketing blocks
96
99