softr-vibe-coding 2.13.3 → 2.13.4
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.
- package/CHANGELOG.md +5 -0
- package/README.md +8 -5
- package/SKILL.md +77 -10
- package/package.json +1 -1
- package/references/anti-patterns.md +12 -7
- package/references/browser-checks.md +156 -1
- package/references/common-patterns.md +223 -1
- package/references/dembrandt.md +7 -2
- package/references/native-chrome-styling.md +163 -14
- package/references/quick-reference.md +38 -0
- package/references/searchable-dropdown.md +28 -1
- package/references/static-blocks.md +7 -4
- package/ui-ux-guidelines.md +38 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@ All notable changes to this skill are documented here. Versions follow [Semantic
|
|
|
4
4
|
|
|
5
5
|
Entries from 1.3.1 onward are generated automatically from git commit subjects between version bumps (see `.github/workflows/publish.yml`). Entries before 1.3.1 were backfilled by hand from the existing commit history.
|
|
6
6
|
|
|
7
|
+
## [2.13.4] - 2026-10-06
|
|
8
|
+
- Release 2.13.4
|
|
9
|
+
- Add in-block modal above Softr's bars (shadcn Dialog sits under the top bar)
|
|
10
|
+
- Add app frame and app-page layout for Softr sidebar navigation
|
|
11
|
+
|
|
7
12
|
## [2.13.3] - 2026-10-05
|
|
8
13
|
- Release 2.13.3
|
|
9
14
|
- Anchor CHANGELOG entries at the commit that introduced the published version
|
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ This Claude skill teaches Claude Code how to generate complete, polished Softr V
|
|
|
21
21
|
- **All 14 Softr data sources** — Airtable, Softr Database, Google Sheets, HubSpot, Notion, Coda, monday.com, SmartSuite, ClickUp, Xano, Supabase, BigQuery, SQL Database, and REST API — each with field mapping, rate limits, and gotchas
|
|
22
22
|
- **Helper blocks & cross-block patterns** — Invisible helper blocks for multi-table access via `window` globals + `CustomEvent`, `useWindowData` hook, breadcrumb navigation, saved views architecture
|
|
23
23
|
- **Advanced integrations** — Shadow DOM CSS isolation for third-party libraries (Leaflet, Mapbox, TinyMCE, Quill, FullCalendar)
|
|
24
|
-
- **Native shell styling** — re-skin Softr's native top bar, **footer**, nav, dropdowns, and **page background** via global Custom Code CSS (stable selectors vs. hashed classes, floating "island" header/footer, the dropdown grid fix, the multi-layer page-background stacking, restyle-vs-replace) — distinct from blocks
|
|
24
|
+
- **Native shell styling** — re-skin Softr's native top bar, **footer**, nav, dropdowns, and **page background** via global Custom Code CSS (stable selectors vs. hashed classes, floating "island" header/footer, the dropdown grid fix, the multi-layer page-background stacking, restyle-vs-replace), plus the **app frame** for apps with Softr's sidebar navigation: the bars' colour as one frame around a paper content sheet, with blocks full-bleed, transparent and laid out by their own width — distinct from blocks
|
|
25
25
|
- **UI/UX design guidelines** — 26 sections covering visual hierarchy, color, typography, spacing, motion design, accessibility, responsive patterns, and an AI slop anti-pattern checklist
|
|
26
26
|
- **Self-validation** — Claude checks Softr platform compatibility and house conventions (inline hook options, correct payload shapes, correct imports, container wrappers or a deliberate full-bleed layout, `getFieldValue()` wrapping, hooks ordering) before delivering code
|
|
27
27
|
- **Premium visual baseline** — every app-UI block (dashboards, lists, forms, detail pages) ships polished from v1: gradient backgrounds, card elevation, loading skeletons, empty states, error states; static marketing blocks use the editorial baseline instead
|
|
@@ -202,20 +202,22 @@ softr-vibe-coding/
|
|
|
202
202
|
│ │ # preview cookie, shadow-DOM refs grepped in the
|
|
203
203
|
│ │ # shell, eval measurements, the records-trigger
|
|
204
204
|
│ │ # write guard proven before any click, what a
|
|
205
|
-
│ │ # click sent, screenshots to disk (Oct 1 2026)
|
|
205
|
+
│ │ # click sent, screenshots to disk (Oct 1 2026);
|
|
206
|
+
│ │ # testing Custom Code header CSS (Oct 5 2026)
|
|
206
207
|
│ ├── advanced-integrations.md # Shadow DOM CSS isolation
|
|
207
208
|
│ │ # Leaflet, Mapbox, TinyMCE, Quill, FullCalendar
|
|
208
209
|
│ ├── native-chrome-styling.md # Restyle Softr's native shell (header, footer,
|
|
209
210
|
│ │ # nav, dropdowns, page background) via global
|
|
210
211
|
│ │ # Custom Code CSS — stable selectors, floating
|
|
211
|
-
│ │ # islands, dropdown grid fix, multi-layer page-bg
|
|
212
|
+
│ │ # islands, dropdown grid fix, multi-layer page-bg,
|
|
213
|
+
│ │ # app frame for sidebar apps (Oct 5 2026)
|
|
212
214
|
│ ├── native-block-filters.md # Dynamic date / URL-param filters + custom filter
|
|
213
215
|
│ │ # controls on native List/Grid blocks — wide-range
|
|
214
216
|
│ │ # sentinel, inject into filter row, survive re-renders
|
|
215
217
|
│ ├── anti-patterns.md # Categorized violation catalog
|
|
216
218
|
│ │ # Data access, mutations, hooks, layout,
|
|
217
219
|
│ │ # permissions, editable settings, helper blocks
|
|
218
|
-
│ ├── common-patterns.md # Small reusable patterns (localStorage state, clipboard, nav blocker, drag-to-reorder, create → open, clickable row + inner link, keyboard picker)
|
|
220
|
+
│ ├── common-patterns.md # Small reusable patterns (localStorage state, clipboard, nav blocker, drag-to-reorder, create → open, clickable row + inner link, keyboard picker, measure the block not the window, clear Softr's sticky bars, a modal above Softr's bars)
|
|
219
221
|
│ │ # localStorage cross-page state, clipboard copy,
|
|
220
222
|
│ │ # navigation blocker, scroll-condensing header,
|
|
221
223
|
│ │ # auth-aware CTA, image masks, blobs, dot lists
|
|
@@ -240,7 +242,8 @@ softr-vibe-coding/
|
|
|
240
242
|
│ │ # (Sep 30 2026)
|
|
241
243
|
│ ├── quick-reference.md # Syntax cheat sheet
|
|
242
244
|
│ │ # Imports, hook signatures, mutation shapes,
|
|
243
|
-
│ │ # field mapping, component skeleton
|
|
245
|
+
│ │ # field mapping, component skeleton,
|
|
246
|
+
│ │ # Softr navigation variables, container queries
|
|
244
247
|
│ └── searchable-dropdown.md # THE dropdown pattern for blocks
|
|
245
248
|
│ # why native <select> and shadcn <Select> both
|
|
246
249
|
│ # break in the shadow DOM, composedPath()
|
package/SKILL.md
CHANGED
|
@@ -22,7 +22,7 @@ allowed-tools: Read Write Glob Grep Bash
|
|
|
22
22
|
|
|
23
23
|
You generate complete, production-ready Softr Vibe Coding blocks as TypeScript React files. A Vibe Coding block is a single file with a default-exported React component, compiled by Softr's server and run in the browser inside a Softr app. The current platform compiles TypeScript with modern syntax — optional chaining (`?.`), nullish coalescing (`??`), arrow functions, `const`, generics — plus shadcn/ui from `@/components/ui/*`, lucide-react, sonner, and date-fns (verified live against the builder MCP's `vibe_coding_block_get_docs` and a 15-block production deployment, 2026-08-25).
|
|
24
24
|
|
|
25
|
-
> **Scope note — blocks vs. native chrome.** A block is page *content*, rendered inside a shadow DOM. Softr's global **header / top bar / nav / dropdown menus** are native chrome (configured in Studio, rendered in the main document) — you **cannot** build or replace them as a block. To restyle them, add CSS to Settings → Custom Code → Code inside header. See [references/native-chrome-styling.md](references/native-chrome-styling.md). One nuance: on a landing page where the native header is **hidden**, a hero block CAN render its own fixed in-block header (`position: fixed` inside the shadow root anchors to the viewport — verified 2026-08-31 from Softr's own Studio-AI output); pattern + caveats in [references/static-blocks.md](references/static-blocks.md#block-owned-landing-page-header).
|
|
25
|
+
> **Scope note — blocks vs. native chrome.** A block is page *content*, rendered inside a shadow DOM. Softr's global **header / top bar / sidebar / phone tab bar / nav / dropdown menus** are native chrome (configured in Studio, rendered in the main document) — you **cannot** build or replace them as a block. To restyle them, or to paint them into one app frame around the content, add CSS to Settings → Custom Code → Code inside header. See [references/native-chrome-styling.md](references/native-chrome-styling.md) (app frame: [App frame (navigation layout)](references/native-chrome-styling.md#app-frame-navigation-layout); the blocks that sit inside it: [App pages beside Softr navigation](#app-pages-beside-softr-navigation)). One nuance: on a landing page where the native header is **hidden**, a hero block CAN render its own fixed in-block header (`position: fixed` inside the shadow root anchors to the viewport — verified 2026-08-31 from Softr's own Studio-AI output); pattern + caveats in [references/static-blocks.md](references/static-blocks.md#block-owned-landing-page-header).
|
|
26
26
|
|
|
27
27
|
## Your Workflow
|
|
28
28
|
|
|
@@ -77,6 +77,7 @@ You generate complete, production-ready Softr Vibe Coding blocks as TypeScript R
|
|
|
77
77
|
- `export default function Block()` is present
|
|
78
78
|
- Container + content wrappers present (`<div className="container py-0"><div className="content">`) — OR a deliberate full-bleed layout recorded in the `// BLOCK PLACEMENT:` comment (see "Block Placement & Page Spacing")
|
|
79
79
|
- `// BLOCK PLACEMENT:` comment present at top of file with wrapper classes matching the placement (see "Block Placement & Page Spacing")
|
|
80
|
+
- App page beside Softr's sidebar / top-bar navigation (the header code paints the frame): the block is **full-bleed** with **no background of its own** and no gradient/rounded page panel; every layout threshold is a container query on the block's width (`@container` on a wrapper + `@min-[NNrem]:`), with **no `sm:`/`md:`/`lg:` for layout**; sticky elements clear the top bar with `calc(var(--nav-height, 0px) + …)`. See [App pages beside Softr navigation](#app-pages-beside-softr-navigation)
|
|
80
81
|
- Loading, error, and empty states all handled
|
|
81
82
|
- Mutation calls gated behind `enabled` check (if using mutations)
|
|
82
83
|
- Field access uses `record.fields.alias` (not `record.alias`)
|
|
@@ -90,6 +91,7 @@ You generate complete, production-ready Softr Vibe Coding blocks as TypeScript R
|
|
|
90
91
|
- Sequential multi-row saves use `await hook.mutateAsync(...)` per row, in order, with stop-on-failure + retry state — `mutateAsync` is fully supported on the current platform (verified 2026-08-25; the old ".mutate() only" Action-parser rule is gone — see [datasources/writing.md](datasources/writing.md)). Independent writes to **different tables** may run in parallel via `Promise.all`; drag/reassign UIs should be optimistic with an Undo toast — see [writing.md → Parallel writes across tables](datasources/writing.md#parallel-writes-across-tables-the-one-sanctioned-parallelism)
|
|
91
92
|
- No hardcoded domains in links -- use relative paths (`/page?recordId=...`); same-page anchors written relative too (`/#section`)
|
|
92
93
|
- **No `<select>` and no shadcn `<Select>`** — both break inside a block's shadow DOM (native hands the list to the OS; shadcn portals outside the shadow root and arrives unstyled). Use the `Combo` pattern in [references/searchable-dropdown.md](references/searchable-dropdown.md) — **searchable by default** for every framed filter or form field whatever the option count; `bare` inline editors are click-only; `searchable={false}` only on a short fixed enum the user is setting (a status, a location, a group-by)
|
|
94
|
+
- App page with Softr navigation: **no shadcn `<Dialog>` / `<Sheet>`** — its overlay is z-50, under Softr's top bar (z-index 800), and it portals out of the shadow root. Use the in-block modal (`fixed inset-0 z-[1000]`, rendered as a sibling of the block's `@container` wrapper, with its own Escape, focus trap, scroll lock and focus return) in [references/common-patterns.md → A modal above Softr's bars](references/common-patterns.md#a-modal-above-softrs-bars) (measured live 2026-10-06)
|
|
93
95
|
- No clipping class (`overflow-hidden`, `overflow-*-auto`, `truncate`, `line-clamp-*`) on any element that contains a `Combo` — its panel is absolutely positioned in local DOM, so a clipping `<td>` cuts the menu to the row's height; bound an over-wide chip at the chip (`min-w-0 truncate`), and never `scrollIntoView` inside the panel. See [references/searchable-dropdown.md](references/searchable-dropdown.md#the-four-things-that-will-bite-you), item 4
|
|
94
96
|
- Any **Print** control opens a **new window with its own document** — `window.open` straight from the click, an escaped standalone HTML printout written into it, `print()` once its stylesheets, fonts and images are in, the button disabled until the data has fully loaded. No `window.print()` on the Softr page, no in-page print view (Hard Constraint 28). See [references/printing.md](references/printing.md)
|
|
95
97
|
- Static block: no hardcoded user-visible copy — every string/image/link is an editable setting (see [references/editable-settings.md](references/editable-settings.md#granularity-doctrine-settings-first-static-blocks))
|
|
@@ -124,6 +126,7 @@ When the user describes their block, figure out which of these areas apply and a
|
|
|
124
126
|
- Icon + wordmark (SVG): `https://cdn.brandfetch.io/idytCFzVcY/theme/dark/logo.svg`
|
|
125
127
|
- Icon only (PNG): `https://cdn.brandfetch.io/idytCFzVcY/w/1024/h/1024/theme/dark/icon.png`
|
|
126
128
|
- **Layout and style**: Cards vs. table vs. list? How many columns? Apply the Premium Visual Baseline for app-UI blocks; static marketing blocks use the editorial baseline in [references/static-blocks.md](references/static-blocks.md#editorial-baseline-replaces-the-premium-visual-baseline) instead.
|
|
129
|
+
- **Navigation layout**: Does the app use Softr's **sidebar / top-bar navigation**, and where does the menu live — in the sidebar or in the top bar? Does the app's header code paint an app frame ([native-chrome-styling.md → App frame (navigation layout)](references/native-chrome-styling.md#app-frame-navigation-layout)), or should it? Beside a sidebar, an app page is full-bleed and lays out by its own width — see [App pages beside Softr navigation](#app-pages-beside-softr-navigation). With the menu in the top bar, the sidebar renders as an empty column in the theme colour (seen 2026-10-05); moving the menu into the sidebar is a Studio change (untested).
|
|
127
130
|
- **Interactivity**: Create/edit/delete? Filtering? Sorting? Pagination?
|
|
128
131
|
- **User context**: Does it need to know who's logged in?
|
|
129
132
|
- **Settings**: Should anything be editable by the Softr builder (titles, images, toggle sections)?
|
|
@@ -175,14 +178,14 @@ For advanced patterns beyond data fetching, load the relevant reference when the
|
|
|
175
178
|
| Cross-*block* communication, window globals, breadcrumbs, publishing shared computed state. *(Multi-table reads no longer need a helper — use a second datasource.)* | [references/helper-blocks.md](references/helper-blocks.md) |
|
|
176
179
|
| Embedding third-party libraries with their own CSS (Leaflet, Mapbox, TinyMCE, Quill, FullCalendar) | [references/advanced-integrations.md](references/advanced-integrations.md) |
|
|
177
180
|
| Debugging a broken block, checking patterns before delivery, full violation catalog | [references/anti-patterns.md](references/anti-patterns.md) |
|
|
178
|
-
| Quick syntax check — import paths, hook signatures, mutation call shapes, field mapping | [references/quick-reference.md](references/quick-reference.md) |
|
|
181
|
+
| Quick syntax check — import paths, hook signatures, mutation call shapes, field mapping, Softr navigation variables (`--nav-height` etc.), container-query syntax | [references/quick-reference.md](references/quick-reference.md) |
|
|
179
182
|
| Any **dropdown / picker / combobox** in a block — why shadcn `<Select>` and native `<select>` both fail inside the shadow DOM, the `composedPath()` click-outside, sorting A→Z inside the component, multi-token filtering, **searchable by default** regardless of option count (`bare` inline editors click-only; `searchable={false}` only for a short fixed enum being set), the `bare` inline-editor variant | [references/searchable-dropdown.md](references/searchable-dropdown.md) |
|
|
180
183
|
| **Printing** anything from a block — always a new window/tab holding its own document, never `window.print()` on the page or an in-page print view: the escaped HTML builder, pop-up-safe opening from the click, print-when-ready (stylesheets, fonts and images, capped), Print disabled until the data has loaded, the `?print=1` deep link from another page, paper layout (shared `<colgroup>`, `vertical-align: middle`, tick boxes) | [references/printing.md](references/printing.md) |
|
|
181
|
-
| Small reusable patterns — `localStorage` cross-page state, clipboard copy button | [references/common-patterns.md](references/common-patterns.md) |
|
|
184
|
+
| Small reusable patterns — `localStorage` cross-page state, clipboard copy button, measuring the block's own width (not the window's), clearing Softr's sticky top bar and phone tab bar, an in-block modal above Softr's bars (instead of shadcn `Dialog`) | [references/common-patterns.md](references/common-patterns.md) |
|
|
182
185
|
| Writing Airtable Automation Scripts / Scripting Extension scripts / Airtable formulas — companion to Softr blocks for cross-table cascades and computed values | [references/airtable-automations.md](references/airtable-automations.md) |
|
|
183
186
|
| The official **Softr MCP server** — Softr DB schema + full record/table/field/database CRUD (deletes included), field-level browsing of connected integrations (Airtable / Google Sheets / Notion / Supabase and more), **creating, editing, versioning, and deploying Vibe Coding blocks directly** (`vibe_coding_block_get_docs`, `vibe_coding_block_create`, ...), push verification by `sourceSha256`, app management/scaffolding, the **Softr Workflows** suite (28 tools, 418-node catalog), and **per-application MCP servers**. Tool names changed on 2026-10-01; the file carries the old → new map | [references/softr-mcp.md](references/softr-mcp.md) |
|
|
184
|
-
| **Checking a pushed block in a browser** — rendering and behaviour in a Softr preview with the agent-browser CLI (ask before installing it): the preview cookie, reaching into the block's shadow DOM through accessibility refs, measuring with `eval`, blocking and proving the save endpoint before any click, reading what a click sent, screenshots to disk | [references/browser-checks.md](references/browser-checks.md) |
|
|
185
|
-
| Restyling Softr's **native shell — header / footer / nav / dropdowns / page background** (not a block; it's Softr chrome, done with global Custom Code CSS): stable selectors vs. hashed classes, floating "island" header+footer, the dropdown blank-space grid fix, the multi-layer page-background stacking, restyle-vs-replace | [references/native-chrome-styling.md](references/native-chrome-styling.md) |
|
|
187
|
+
| **Checking a pushed block in a browser** — rendering and behaviour in a Softr preview with the agent-browser CLI (ask before installing it): the preview cookie, reaching into the block's shadow DOM through accessibility refs, measuring with `eval`, blocking and proving the save endpoint before any click, reading what a click sent, screenshots to disk, testing Custom Code header CSS | [references/browser-checks.md](references/browser-checks.md) |
|
|
188
|
+
| Restyling Softr's **native shell — header / footer / nav / dropdowns / page background** (not a block; it's Softr chrome, done with global Custom Code CSS): stable selectors vs. hashed classes, floating "island" header+footer, the dropdown blank-space grid fix, the multi-layer page-background stacking, restyle-vs-replace, and the **app frame** for sidebar apps (top bar + sidebar as one frame in the theme colour, the content as one paper sheet with a pinned rounded corner, blocks transparent, scoped with `:has()` so Log in / 404 keep Softr's colours) | [references/native-chrome-styling.md](references/native-chrome-styling.md) |
|
|
186
189
|
| Adding a **dynamic date filter or custom filter control to a native List/Grid block** (via a Custom Code Static block, not a Vibe block): drive the block's conditional filter with `{URL_PARAM:…}`, the empty-param "match nothing" wide-range sentinel, inject the control into the filter row and keep it alive across Softr's re-renders | [references/native-block-filters.md](references/native-block-filters.md) |
|
|
187
190
|
| **Editable settings deep-dive** — full hook catalog (incl. verified-undocumented `useLongTextSetting` and the `navigation` array-schema type), settings-first granularity doctrine, heading-line-split and `-text`/`-link` pairing patterns, naming conventions, rename-resets-value gotcha, empty-media gating, key-by-index rule | [references/editable-settings.md](references/editable-settings.md) |
|
|
188
191
|
| **Static marketing blocks** — heroes, landing headers, pricing tables, footers: workflow deltas (skip datasources), editorial baseline, full-bleed license, full-viewport sizing, block-owned fixed header + caveats, section anchors | [references/static-blocks.md](references/static-blocks.md) |
|
|
@@ -213,9 +216,12 @@ export default function Block() {
|
|
|
213
216
|
|
|
214
217
|
Wrap the outermost layout in `container` and `content` divs by default — these constrain width to match the Softr app's max width settings so the block aligns with neighboring native blocks. Note this is a **house convention, not platform-enforced**: per the official developer guide the platform default is full width, and the classes are merely "available" to constrain it (verified 2026-08-31 against `vibe_coding_block_get_docs` and a rendering wrapper-free Studio-AI hero).
|
|
215
218
|
|
|
219
|
+
The wrappers also bring **window-based gutters**. Inside a Vibe host, `.container`'s side padding is `--container-x`, which `:host([data-container-padding-x=…])` sets on WINDOW media queries: `regular` is 16px, then 24px from a 576px window (the 768px step keeps 24px), more from 992px (`0 32px` measured on a 744px block at a 1024px window); `tab` 0px, `container` 8px, `none` 0px (read from the block's compiled CSS and measured, 2026-10-05). On a full-width page that is harmless; beside Softr's sidebar the gutter steps on a width the block doesn't have — one reason app pages there drop the wrappers.
|
|
220
|
+
|
|
216
221
|
**Exceptions (omit the wrappers deliberately):**
|
|
217
222
|
- Blocks inside Softr column containers — Softr controls layout.
|
|
218
|
-
- Full-bleed marketing blocks (heroes, banner bands, footers) — backgrounds and decorative shapes run edge-to-edge; the block then owns its own gutters (`px-6 md:px-12 lg:px-16
|
|
223
|
+
- Full-bleed marketing blocks (heroes, banner bands, footers) — backgrounds and decorative shapes run edge-to-edge; the block then owns its own gutters (`px-6 md:px-12 lg:px-16`, window breakpoints: right on a full-width page, wrong beside a sidebar) and inner max-widths, and records the choice in the `// BLOCK PLACEMENT:` comment. See [references/static-blocks.md](references/static-blocks.md#full-bleed-layout-license).
|
|
224
|
+
- App pages beside Softr's sidebar / top-bar navigation, inside a frame the header code paints — the block is full-bleed AND transparent (no background, no page panel), owns container-query gutters (`px-4` → `@min-[40rem]:px-6` → `@min-[64rem]:px-10`) and lays out by its own width. See [App pages beside Softr navigation](#app-pages-beside-softr-navigation).
|
|
219
225
|
|
|
220
226
|
## Block Placement & Page Spacing
|
|
221
227
|
|
|
@@ -228,6 +234,7 @@ Blocks rarely live alone — most Softr pages stack 2–4 blocks vertically, oft
|
|
|
228
234
|
- Is there a Softr header immediately above this block?
|
|
229
235
|
- Is there a Softr footer immediately below this block?
|
|
230
236
|
- Is there a Back button at the top of this block?
|
|
237
|
+
- Does the page show Softr's sidebar / top-bar navigation, with header code painting an app frame? If so, the block follows [App pages beside Softr navigation](#app-pages-beside-softr-navigation) instead of the wrapper table below.
|
|
231
238
|
|
|
232
239
|
**Detail pages — always ask about the back button AND its fallback URL.** A "detail page" is any block that reads a single record by URL recordId (i.e. it calls `useCurrentRecordId()` / `useRecord()`, or the user describes it as the target of a `/page?recordId=...` link). Users almost always want a back button there but rarely think to mention it, and shipping the page without one is the most common UX gap on these screens. So even if every other placement detail is clear, ask both:
|
|
233
240
|
|
|
@@ -271,7 +278,7 @@ The `// BLOCK PLACEMENT:` marker is intentionally stable so it can be grepped an
|
|
|
271
278
|
|
|
272
279
|
### Spacing values (defaults)
|
|
273
280
|
|
|
274
|
-
**Container** (default — omitted by full-bleed blocks and blocks inside column containers; see table below): `<div className="container py-0">`
|
|
281
|
+
**Container** (default — omitted by full-bleed blocks, app pages beside Softr navigation and blocks inside column containers; see table below): `<div className="container py-0">`
|
|
275
282
|
|
|
276
283
|
**Inner wrapper** classes by block position:
|
|
277
284
|
|
|
@@ -281,7 +288,8 @@ The `// BLOCK PLACEMENT:` marker is intentionally stable so it can be grepped an
|
|
|
281
288
|
| Middle block | `py-3 px-8` | 12px + Softr separator + 12px ≈ 24px between blocks |
|
|
282
289
|
| Last block (footer-adjacent) | `pt-3 pb-12 px-8` | 12px top + 48px bottom for footer breathing room |
|
|
283
290
|
| Standalone (only block on page) | `pt-3 pb-12 px-8` | Treat like a last block |
|
|
284
|
-
| Full-bleed (hero / banner / footer) | none — no container/content; block owns gutters `px-6 md:px-12 lg:px-16` | Edge-to-edge backgrounds; see [references/static-blocks.md](references/static-blocks.md#full-bleed-layout-license) |
|
|
291
|
+
| Full-bleed (hero / banner / footer) | none — no container/content; block owns gutters `px-6 md:px-12 lg:px-16` | Edge-to-edge backgrounds on a full-width page; see [references/static-blocks.md](references/static-blocks.md#full-bleed-layout-license) |
|
|
292
|
+
| App page beside Softr navigation (header code paints the frame) | none — no container/content, no background; `@container` root, then `px-4 pt-5 pb-12 @min-[40rem]:px-6 @min-[40rem]:pt-6 @min-[64rem]:px-10 @min-[64rem]:pt-8` | The only block on the page, on the header's paper sheet; gutters follow the block's width, not the window's; see [App pages beside Softr navigation](#app-pages-beside-softr-navigation) |
|
|
285
293
|
|
|
286
294
|
**Back button** (when present at the top of a block — typically on detail pages): wrap in `<div className="mt-6 mb-4">`. The `mt-6` (24px) adds breathing room above the button independent of wrapper padding; `mb-4` (16px) sits between the button and the first card. Apply this regardless of whether the block is first or mid-page.
|
|
287
295
|
|
|
@@ -289,13 +297,70 @@ The `// BLOCK PLACEMENT:` marker is intentionally stable so it can be grepped an
|
|
|
289
297
|
|
|
290
298
|
**Net page rhythm**: between-block gaps (12 + 12 = 24px) match within-block card gaps (`mb-6` = 24px), so the page reads as one consistent vertical rhythm.
|
|
291
299
|
|
|
300
|
+
### App pages beside Softr navigation
|
|
301
|
+
|
|
302
|
+
When the app uses Softr's **sidebar / top-bar navigation** and the app's header code paints the frame (the top bar and sidebar colour around the page, the content as one paper sheet with a rounded corner tucked under them; recipe in [native-chrome-styling.md → App frame (navigation layout)](references/native-chrome-styling.md#app-frame-navigation-layout)), the blocks on its app pages take a different shape. Verified live 2026-10-05 on a three-page HubSpot-backed app (a dashboard, an accounts list + detail, a request form): the header code on the published app and in the preview, the blocks measured in the preview.
|
|
303
|
+
|
|
304
|
+
**Full-bleed, and the block paints nothing behind itself [house].**
|
|
305
|
+
- No `container` / `content` wrappers, no background colour on the block's root, no gradient or rounded panel around the page (Premium Visual Baseline §1 does not apply). A panel on top of the sheet brings back the "card floating on a white page" look the frame exists to remove; Leo rejected exactly that. The block's own cards, borders and dividers stay the block's job, because header CSS cannot reach inside the shadow root.
|
|
306
|
+
- The transparency comes from the header code, not the block. A Vibe host paints the Studio theme background (white by default) through its compiled `:host` rule even when the block sets none; the frame recipe clears it with a main-document rule on the host, `[data-role="vibe-block-root"]` (the `:host` rule isn't `!important`, so it loses). Without that header code, a block that paints nothing still sits on white (verified 2026-10-05).
|
|
307
|
+
- **One block per page, and it owns the page's `h1`**; its panel titles are `h2`. No separate welcome or title block above it. The pattern was built and verified only with one block per page. If a page must hold more, only the first block takes the top padding and only the last takes `pb-12`; the rest use `pt-0` / `pb-0` with the same side gutters (untested).
|
|
308
|
+
- **The same shell on every app page**, so the header lands in the same place (the cross-page chrome rule above):
|
|
309
|
+
|
|
310
|
+
```tsx
|
|
311
|
+
// FONT and C.ink: the brand's UI font and ink colour (DESIGN.md tokens)
|
|
312
|
+
<div ref={rootRef} className="@container" style={{ color: C.ink, fontFamily: FONT }}>
|
|
313
|
+
<div className="px-4 pt-5 pb-12 @min-[40rem]:px-6 @min-[40rem]:pt-6 @min-[64rem]:px-10 @min-[64rem]:pt-8">
|
|
314
|
+
{/* the h1 header row, then the page */}
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Measured padding (top, sides, bottom): `32px 40px 48px` on a 1160px block (a 1440px window minus a 280px sidebar), `20px 16px 48px` on a 390px block.
|
|
320
|
+
- Why the wrappers go: `.container`'s gutters step on the window (see the note under [Code Structure](#code-structure)).
|
|
321
|
+
|
|
322
|
+
**Lay out by the block's width, never the window's.** Softr's sidebar takes 57–360px of the window (280px by default, 57px collapsed, 200–360px when dragged), so `sm:` / `md:` / `lg:` fire on a width the block doesn't have: at a 1024px window the block is 744px wide, and at 768px with the sidebar open it is 488px. Measured 2026-10-05: an `lg:` five-column chart row fired on a 744px block and cut off its bar labels. Below a 768px window Softr swaps the top bar and sidebar for a bottom tab bar (767 = tab bar, 768 = top bar + sidebar).
|
|
323
|
+
- Use Tailwind container queries: `@container` on a wrapper, `@min-[NNrem]:` on what's inside it. Softr's Tailwind compiles them (verified live 2026-10-05). Syntax in [quick-reference.md → Container queries](references/quick-reference.md#container-queries).
|
|
324
|
+
- A container query never resolves against the element that carries `@container`, only against the nearest ancestor container. So `@container` goes on a wrapper, and the `@min-…` classes go on its descendants.
|
|
325
|
+
- Nest another `@container` on a pane (a detail panel, a side column) so its inner rows and strips follow the pane, not the page.
|
|
326
|
+
- Thresholds are BLOCK widths. From that app (the numbers are the project's; the method is the reusable part):
|
|
327
|
+
|
|
328
|
+
| Part | Narrow | Wide | From (block width) |
|
|
329
|
+
|---|---|---|---|
|
|
330
|
+
| Figure strip | 2 columns | 4 columns | `@min-[52rem]:` |
|
|
331
|
+
| Chart row | stacked | 5-column grid, spans 2 + 3 | `@min-[64rem]:` |
|
|
332
|
+
| Form + side list | stacked | `grid-cols-[minmax(0,44rem)_minmax(18rem,22rem)]` | `@min-[56rem]:` |
|
|
333
|
+
| List + detail | list, then detail | side by side | 860px, decided in JS: a 340px list + 20px gap + at least 440px of detail + the shell's padding |
|
|
334
|
+
|
|
335
|
+
Work each threshold out from the parts it has to fit (the list + detail row shows how), then check it at real block widths with the real fonts. A padding change moves where text wraps: after the switch to this shell, a figure-strip skeleton keyed to the old wrap point no longer matched the loaded cells (measured 2026-10-05 across block widths with the real fonts).
|
|
336
|
+
- When CSS can't decide (which component tree to render, how many chart ticks fit), measure the block: [common-patterns.md → Measure the block, not the window](references/common-patterns.md#measure-the-block-not-the-window). Measure in `useLayoutEffect`; a passive `useEffect` paints the first frame at the window's width, and beside a sidebar the layout visibly flips.
|
|
337
|
+
|
|
338
|
+
**Clear Softr's sticky bars with the host's variables.** Softr's top bar is sticky (56px, z-index 800), and on phones so is the tab bar (about 57px as rendered, though Softr's variable says 55px; z-index 800). The Vibe host maps the bar and sidebar sizes onto `--nav-height`, `--sidebar-width` and `--bottombar-height`, each falling back to `0px` (values in [quick-reference.md → Softr navigation variables](references/quick-reference.md#softr-navigation-variables)). Use them only inside CSS `calc()`, e.g. a sticky list pane:
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
<section className="sticky …" style={{ top: "calc(var(--nav-height, 0px) + 16px)", maxHeight: "calc(100dvh - var(--nav-height, 0px) - 32px)" }}>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
A plain `sticky top-4` slid under the top bar; with the variable the pane sits at 72px (measured 2026-10-05). JS that scrolls the window or sizes a popover has to clear the bars too: [common-patterns.md → Clear Softr's sticky bars](references/common-patterns.md#clear-softrs-sticky-bars).
|
|
345
|
+
|
|
346
|
+
**Check it where the header code runs.** Header code applies on the published app and in Softr's preview (verified 2026-10-05, seen after a publish; whether the preview shows a pasted but unpublished change is untested, so inject it instead), not in the Studio editor canvas, so judge the page in the preview: [browser-checks.md → Testing Custom Code header CSS](references/browser-checks.md#testing-custom-code-header-css).
|
|
347
|
+
|
|
348
|
+
**Record it in the placement comment:**
|
|
349
|
+
|
|
350
|
+
```tsx
|
|
351
|
+
// BLOCK PLACEMENT: the only block on <page>; it owns the page's h1. FULL-BLEED by design: no container/content
|
|
352
|
+
// wrappers and no background of its own; the app's header code paints the frame and the paper sheet.
|
|
353
|
+
// Spacing: px-4 pt-5 pb-12, then px-6 pt-6 from a 40rem block and px-10 pt-8 from 64rem (same shell on every page).
|
|
354
|
+
// LAYOUT follows the block's own width (@container queries), never the window.
|
|
355
|
+
```
|
|
356
|
+
|
|
292
357
|
### Full-viewport hero blocks
|
|
293
358
|
|
|
294
359
|
A hero may size itself to the viewport — vh units inside a block resolve against the real window (blocks are shadow DOM in the main document, not iframes). The Studio-verified responsive shape is `min-h-screen lg:min-h-0 lg:h-screen` (natural height on mobile, locked viewport height on desktop). Three rules: (1) `h-screen` fills the window only when the native header is hidden on that page — with a native header above, a 100vh block overflows by the header height; use `min-h-[calc(100vh-<px>)]` when native chrome stays; (2) hard `h-screen` + `overflow-hidden` + centered flex **clips settings-grown content unrecoverably** — prefer `lg:min-h-screen` unless the locked look is explicitly wanted; (3) the standard spacing table above does not apply — the hero owns all its spacing. Extend the placement comment: `// BLOCK PLACEMENT: full-viewport hero, native header hidden, owns all spacing`. Full detail in [references/static-blocks.md](references/static-blocks.md#full-viewport-hero-sizing).
|
|
295
360
|
|
|
296
361
|
## Premium Visual Baseline
|
|
297
362
|
|
|
298
|
-
**Every block must look polished in its first version.** Styling is not a follow-up task — it is a core requirement of every code generation. Apply ALL of the following by default unless the user explicitly requests a minimal/plain style.
|
|
363
|
+
**Every block must look polished in its first version.** Styling is not a follow-up task — it is a core requirement of every code generation. Apply ALL of the following by default unless the user explicitly requests a minimal/plain style (one carve-out: §1's gradient wrapper is dropped on app pages inside a header-painted frame).
|
|
299
364
|
|
|
300
365
|
**Scope: this is the app-UI baseline** — dashboards, lists, forms, detail pages. Static marketing blocks (heroes, landing sections, footers) use the **editorial baseline** in [references/static-blocks.md](references/static-blocks.md#editorial-baseline-replaces-the-premium-visual-baseline) instead — typographic hierarchy and brand-exact values, no gradient wrapper/cards/skeletons/empty states (nothing loads).
|
|
301
366
|
|
|
@@ -309,6 +374,8 @@ Refer to [ui-ux-guidelines.md](ui-ux-guidelines.md) for full design principles.
|
|
|
309
374
|
```
|
|
310
375
|
Adjust the top gradient color to complement the user's brand.
|
|
311
376
|
|
|
377
|
+
**Exception — app pages inside a header-painted frame.** When the app's header code paints the frame and the paper sheet beside Softr's sidebar navigation, drop this wrapper and any rounded page panel: the block paints no background at all. A panel on the sheet recreates the "card floating on a white page" look the frame exists to remove (rejected by Leo, 2026-10-05). The cards inside the block (§3) stay. See [App pages beside Softr navigation](#app-pages-beside-softr-navigation).
|
|
378
|
+
|
|
312
379
|
### 2. Header section
|
|
313
380
|
- Icon in a colored rounded square (`h-10 w-10 rounded-xl` with brand primary, white icon)
|
|
314
381
|
- Title at `text-2xl font-bold`
|
|
@@ -538,7 +605,7 @@ Non-negotiable rules. Most are enforced by the Softr platform (compiler, validat
|
|
|
538
605
|
6. **Array setting icon placement** — Never put `vibeCodingBlockIcon` as first field.
|
|
539
606
|
7. **No nested arrays in settings** — Use text with separator, split in code.
|
|
540
607
|
8. **Default export required** — `export default function Block()`.
|
|
541
|
-
9. **Container wrapping [house]** — Wrap in `<div className="container py-0"><div className="content">` by default so the block's width matches native blocks. NOT platform-enforced: the platform default is full width and the wrappers are officially optional (verified 2026-08-31). Deliberate full-bleed blocks (heroes, banner bands, footers) and blocks inside column containers omit them — see "Block Placement & Page Spacing" and [references/static-blocks.md](references/static-blocks.md#full-bleed-layout-license). Vertical padding lives on the inner wrapper and depends on block placement.
|
|
608
|
+
9. **Container wrapping [house]** — Wrap in `<div className="container py-0"><div className="content">` by default so the block's width matches native blocks. NOT platform-enforced: the platform default is full width and the wrappers are officially optional (verified 2026-08-31). Deliberate full-bleed blocks (heroes, banner bands, footers), app pages beside Softr's sidebar navigation inside a frame the header code paints (full-bleed, no background, laid out by container queries — see [App pages beside Softr navigation](#app-pages-beside-softr-navigation)) and blocks inside column containers omit them — see "Block Placement & Page Spacing" and [references/static-blocks.md](references/static-blocks.md#full-bleed-layout-license). Vertical padding lives on the inner wrapper and depends on block placement.
|
|
542
609
|
10. **Inline options literals for data hooks** — `useRecords` fails to compile when its options
|
|
543
610
|
object is passed through a variable or wrapper function. Build the options object inline at the
|
|
544
611
|
call site; share `q.select` mappings between hooks, not whole options objects. (Same
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "softr-vibe-coding",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.4",
|
|
4
4
|
"description": "Claude Code skill for generating production-ready Softr Vibe Coding blocks (JSX). Installs into ~/.claude/skills/ and auto-updates on each Claude Code session.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"softr-vibe-coding": "bin/cli.js"
|
|
@@ -80,19 +80,24 @@ Run through this catalog before delivering any block. Every row is a violation o
|
|
|
80
80
|
| `overflow-hidden`, `truncate`, `line-clamp-*` or `overflow-*-auto` on a container that holds a dropdown or popover — typically a `<td>` clipped so an over-wide status chip stops at its own column | **Symptom:** the menu opens cut to the height of its row or its scroller: one or two options showing, the rest unreachable by mouse. **Cause:** the `Combo` panel is `position: absolute` in the block's own DOM (a portal would leave the shadow root and lose its styles), and an absolutely positioned box is clipped by every ancestor whose `overflow` is not `visible`. `truncate` and `line-clamp-*` set `overflow: hidden`; `overflow-x-auto` turns `overflow-y` to `auto` as well. **Fix:** no clipping class between the Combo and the scroller it belongs to; bound the chip at the chip (`min-w-0 truncate` on the chip inside the flex trigger); measure the drop-up and the list height against the clipping ancestors, not the window. Hit in production 2026-09-30: three ROSIE item tables clipped the status cell as a 2px backstop, next to a comment claiming the menu was portaled — it had stopped being portaled when the tables moved from shadcn `<Select>` to `Combo`. See [searchable-dropdown.md](searchable-dropdown.md#the-four-things-that-will-bite-you), item 4 |
|
|
81
81
|
| `el.scrollIntoView({ block: "nearest" })` to keep a dropdown's highlighted option in view (or a plain `focus()` on its search box) | **Symptom:** the table or the page jumps when a menu opens near an edge; inside a clipped cell the trigger itself scrolls out of view. **Cause:** `scrollIntoView` scrolls EVERY scrollable ancestor until the element shows, and `overflow: hidden` boxes are still scrollable from script; `focus()` scrolls ancestors the same way. **Fix:** scroll the list element only — compare the option's rect with the list's and adjust `list.scrollTop` — and focus with `{ preventScroll: true }`. Verified in Chromium 2026-09-30: `scrollIntoView` scrolled an `overflow: hidden` cell by 164px, the list-only scroll moved nothing outside the list. See [searchable-dropdown.md](searchable-dropdown.md#the-four-things-that-will-bite-you), item 4, rule 3 |
|
|
82
82
|
| Positioning repeated page chrome (back button, title, primary action) per-block, without checking the pages that already have it | Chrome the user meets on more than one screen is a cross-page contract. Copy the exact offset from the blocks that already ship it, and change every page in one edit. Let the wrapper's padding be the only thing positioning it — `mb-4` and NO top margin on a back button — so one number per page governs it. Verified 2026-09-09: an extra `mt-6` sat one detail page's back button 24px lower than another's, and **each block looked correct in isolation**. See SKILL.md's Block Placement section |
|
|
83
|
-
| A loading skeleton carrying a different border / offset from the component it stands in for | The skeleton must track the component's REST state (border colour, padding, chrome offsets), never its hover state. If they disagree the layout visibly re-draws the instant data lands — the exact thing a skeleton exists to prevent. Verified 2026-09-09 twice in one session: a card grid re-outlined itself on load, and a back button jumped 24px. See [ui-ux-guidelines.md](../ui-ux-guidelines.md) §12 |
|
|
83
|
+
| A loading skeleton carrying a different border / offset from the component it stands in for | The skeleton must track the component's REST state (border colour, padding, chrome offsets), never its hover state. If they disagree the layout visibly re-draws the instant data lands — the exact thing a skeleton exists to prevent. Verified 2026-09-09 twice in one session: a card grid re-outlined itself on load, and a back button jumped 24px. Under container queries, find where a skeleton's text lines wrap by measuring at BLOCK widths with the real fonts: a padding change moves the wrap, and a skeleton keyed to the old width jumped when data landed (measured 2026-10-05 in headless Chromium, blocks 300–1400px wide). See [ui-ux-guidelines.md](../ui-ux-guidelines.md) §12 |
|
|
84
84
|
| `focus-visible:` on a card or row that only *contains* buttons | The container is a plain `<div>` and never takes focus, so it is dead CSS. Use `focus-within:` on the container (pairs with its `hover:` treatment) and keep `focus-visible:ring-2` on the button/link itself |
|
|
85
85
|
| `[&_svg]:opacity-0` on SelectTrigger | `<style>` + `data-fix-chevron` attribute (Softr bundler limitation) |
|
|
86
|
-
| Relying on `custom-code-header.html` (Softr → Settings → Custom Code → Code inside header) to apply brand fonts/colors INSIDE a Vibe Coding block | Vibe Coding blocks render inside a shadow DOM. CSS custom properties (`--brand-*`) pierce that boundary, but `html, body { font-family: ... !important }` rules **do not** — `<html>` and `<body>` don't exist inside the shadow root. Apply brand fonts/colors at the block's **own outermost wrapper** via inline style: `style={{ fontFamily: "'Manrope', system-ui, sans-serif", color: BRAND_INK }}` on the outer `<div>` so every descendant inherits brand defaults. Override per-element with explicit inline `fontFamily` (e.g., `"'Fraunces', Georgia, serif"` on h1/h2). Google `<link>` tags in the page head DO load `@font-face` globally — the fonts are available inside shadow DOM, they just need to be applied. |
|
|
87
|
-
| Painting `backgroundColor: BRAND_CANVAS` on a Vibe Coding block's outer wrapper
|
|
88
|
-
| Relying on the page background on a **dark** brand, and shipping a block whose own canvas is unpainted |
|
|
89
|
-
| Setting only `html, body { background }` in `custom-code-header.html` and expecting the app to change colour | Softr paints the
|
|
86
|
+
| Relying on `custom-code-header.html` (Softr → Settings → Custom Code → Code inside header) to apply brand fonts/colors INSIDE a Vibe Coding block | Vibe Coding blocks render inside a shadow DOM. CSS custom properties (`--brand-*`) pierce that boundary, but `html, body { font-family: ... !important }` rules **do not** — `<html>` and `<body>` don't exist inside the shadow root. Apply brand fonts/colors at the block's **own outermost wrapper** via inline style: `style={{ fontFamily: "'Manrope', system-ui, sans-serif", color: BRAND_INK }}` on the outer `<div>` so every descendant inherits brand defaults. Override per-element with explicit inline `fontFamily` (e.g., `"'Fraunces', Georgia, serif"` on h1/h2). Google `<link>` tags in the page head DO load `@font-face` globally — the fonts are available inside shadow DOM, they just need to be applied. Give the header's font link an id (`<link id="brand-fonts" rel="stylesheet" href="…">`), and have each block append the same link to `document.head` only when `document.getElementById("brand-fonts")` finds nothing (the head is outside the shadow root, so this `document` lookup works). The block then works with or without the header code and never loads the fonts twice. Verified 2026-10-05: the header's link sat in `<head>` and the blocks skipped theirs. |
|
|
87
|
+
| Painting `backgroundColor: BRAND_CANVAS` on a Vibe Coding block's outer wrapper to match a page colour that `custom-code-header.html` paints — or leaving it unset and expecting that page colour to show through | **An unpainted block does not let the page show through.** Its shadow host, `div[data-role="vibe-block-root"]`, paints the Studio theme background through the block's compiled `:host { background-color: var(--background) }` (in `@layer base`; `--background` maps to the theme's hashed background variable). Measured 2026-10-05: the host computes rgb(255,255,255) from its own compiled `:host` rule (read from the block's stylesheet), so it paints white whatever the page behind it is. It turns transparent only when header CSS clears the host: the app-frame rule `#main-content [data-role="vibe-block-root"] { background-color: transparent !important; }` (verified 2026-10-05; it wins because the `:host` rule is not `!important`), or, in a top-bar-only app, the page-background recipe's `#page-content div:not(.softr-topbar)…` clear, which hits the host too ([native-chrome-styling.md → Page background](native-chrome-styling.md#page-background)). The verified app-frame form is unscoped and applies on every page; the scoped form `#page-content:has(.softr-sidebar, .softr-bottombar) [data-role="vibe-block-root"]` is untested. With the host cleared, leave the block's outer wrapper unpainted too, so the header is the one painter of the page colour (older notes reported a seam from painting it twice; its cause was never measured). The header reaches the host but nothing inside the shadow root, so the block still paints its own cards and borders, and sets `fontFamily` and `color` on its wrapper (row above). Exception: a brand-tinted *section* that differs from the page (a card-style admin shell) paints its own container, not the outer wrapper. Recipe: [native-chrome-styling.md → App frame (navigation layout)](native-chrome-styling.md#app-frame-navigation-layout). **Dark brands: see the next row.** |
|
|
88
|
+
| Relying on the page background on a **dark** brand, and shipping a block whose own canvas is unpainted | Same mechanism as the row above: whatever `custom-code-header.html` paints on `body` (`body { background-color: #000 !important }`), the block's host paints the Studio theme background, white by default — so a block with white text, a white-only logo, or a white primary button renders **invisibly on white**. Verified July 2026: a black-canvas feedback form shipped with its wordmark and its Submit button both white-on-white; the button was there and clickable, just unseeable. On a dark brand, paint `backgroundColor` on the block's own outer wrapper AND set the Studio theme background to the same value. The theme background is the no-CSS lever, because it is what the host's `--background` reads (measured 2026-10-05); changing it to fix a block is untested. Two identical pure blacks composite with no seam, so the double-paint concern above doesn't bite there, and painting it in the block also keeps it correct if the custom-code snippet is ever removed. |
|
|
89
|
+
| Setting only `html, body { background }` in `custom-code-header.html` and expecting the app to change colour | Softr paints the page fill on several stacked layers, so styling one gets covered by the ones above it and `body` alone appears to do nothing. Measured 2026-10-05 in an app with Softr's sidebar: `html`, `body`, `#page-content` (Softr's own `.spr-content-root` rule), each Vibe block's shadow host `div[data-role="vibe-block-root"]` (most likely the "class-less wrapper div" of earlier notes, inferred; its `:host` rule paints the theme background, row above) and each native block's outer `<section>`. **Top bar only** (the June 2026 recipe): paint the backdrop on `html`, then clear `body, #page-content { background: transparent }` plus `#page-content div:not(.softr-topbar):not(.softr-topbar *)`. The `:not()` exclusion is required — the nav renders inside `#page-content` and that id's specificity out-ranks `.softr-topbar` rules, so a blanket clear silently flattens the dropdown panel. Full recipe in [native-chrome-styling.md](native-chrome-styling.md#page-background). **With Softr's sidebar or phone tab bar**, don't use that blanket `div` clear: `.softr-sidebar` is a div inside `#page-content` that paints the theme colour, so the clear would strip it too (inferred from the measured DOM, not injected), and it never reaches native blocks' `<section>`s. Name the layers instead, scoped to pages with navigation by `:has(.softr-sidebar, .softr-bottombar)` — see [native-chrome-styling.md → App frame (navigation layout)](native-chrome-styling.md#app-frame-navigation-layout) |
|
|
90
|
+
| On an app page inside a header-painted frame: keeping Softr's `container` / `content` wrappers, a padded outer wrapper, and a rounded panel with its own background (`rounded-[20px] p-8` on the brand surface, or a gradient) | Users read it as cards floating on white: "building blocks on top of each other" (Leo, 2026-10-05, asking for one full-width application instead). Make the block **full-bleed and transparent**: no `container` / `content` wrappers (their gutters step on the WINDOW), no background on any wrapper, and one `@container` shell whose gutters follow the block's width — the shell, its measured paddings and the placement comment are in [SKILL.md → App pages beside Softr navigation](../SKILL.md#app-pages-beside-softr-navigation). Record the full-bleed choice in the `// BLOCK PLACEMENT:` comment and use the same shell on every app page, so the page header lands in the same place. The header code paints the frame and the sheet; the block still paints its own cards and borders. |
|
|
91
|
+
| Window breakpoints (`sm:` / `md:` / `lg:`) for the layout of a block that sits beside Softr's sidebar | The sidebar takes 57px (collapsed) to 360px (dragged; 280px by default) of the window, so the block is far narrower than the window: 744px at a 1024px window, 488px at 768 with the sidebar open. `lg:` still fires at 1024 — measured 2026-10-05: a squeezed five-column chart row with cut-off labels. Lay out by the block's own width: `@container` on a root wrapper plus Tailwind v4 container variants (`@min-[52rem]:grid-cols-4`), which Softr's Tailwind 4.1.13 compiles (verified live 2026-10-05). A container query resolves against the nearest ANCESTOR container, never the element itself, so `@container` goes on a wrapper. For decisions CSS can't make, measure the block in JS — see [common-patterns.md → Measure the block, not the window](common-patterns.md#measure-the-block-not-the-window) |
|
|
92
|
+
| `sticky top-4` (any `top-N`) on an element inside a block, on a page with Softr's top bar | Softr's `#topbar-root` is itself sticky (top 0, z-index 800, 56px tall), so the block's sticky element slides under it. Offset by the nav height the block host exposes, as an inline style: `style={{ top: "calc(var(--nav-height, 0px) + 16px)", maxHeight: "calc(100dvh - var(--nav-height, 0px) - 32px)" }}` (verified 2026-10-05: the list pane stuck at 72px). `--nav-height` is 56px with the top bar and falls back to 0px on phones, where the tab bar is `--bottombar-height`. JS that scrolls the window or fits a popover has to keep the bars clear too (inferred). See [common-patterns.md → Clear Softr's sticky bars](common-patterns.md#clear-softrs-sticky-bars) |
|
|
93
|
+
| Reading the navigation variables in JS — `parseFloat(getComputedStyle(host).getPropertyValue("--bottombar-height"))` | They are unregistered custom properties, so JS gets the token string, not a length: on phones `--bottombar-height` reads `calc(0px + 55px)` (measured 2026-10-05). `parseFloat` of that is NaN (inferred, not run), and a fallback to 0 then hides the tab bar from the code. Use them only inside CSS `calc()`. Where JS needs a number (scroll insets, the room a popover has), take the bars' heights as constants (56px top bar; about 57px phone tab bar as rendered, though Softr's variable says 55px; switching at a 768px window) — see [common-patterns.md → Clear Softr's sticky bars](common-patterns.md#clear-softrs-sticky-bars). The shipped block's 72px (a bar plus about 16px) is a project choice, not a rule |
|
|
90
94
|
| `document.getElementById(...)` / `document.querySelector(...)` to find an element inside the block — for example, a hidden `<input type="file">` triggered by a visible "Upload" button via `getElementById('myInput').click()` | Vibe Coding blocks render inside a shadow DOM. The global `document` traversal stops at the shadow boundary, so id/selector lookups for elements inside the block return `null`. The user-visible symptom is a control that does nothing — no error, no file picker, no focus, no scroll — because the chained `.click()` / `.focus()` / `.scrollIntoView()` was called on `null`. Use a **React `useRef`** instead: `var inputRef = useRef(null)`, then `<input ref={inputRef} />` and `<button onClick={function() { if (inputRef.current) inputRef.current.click(); }}>`. Refs hold direct node references and don't depend on DOM traversal, so they work regardless of which DOM tree the node lives in. This applies to every "trigger a hidden element" pattern: hidden file inputs, programmatic focus, scroll-into-view, `.click()` on a non-visible button. |
|
|
91
95
|
| Inlining brand hexes at every point of use (the signature failure of Studio-AI-generated styling) | Hoist the palette to module-scope constants — `const BRAND = { terracotta: "#B4603D", ink: "#211C18" }` — and reference those. Inlined hexes drift into near-duplicates: observed live 2026-08 in one Studio-emitted hero, `#AE5E3D` vs `#B4603D` for the same terracotta and three near-identical near-blacks. The literals-only static-analysis rule applies ONLY to `datasource.define()` / `q.select()` / data-hook options — style objects and JS expressions use constants freely (the BRAND_INK/BRAND_CANVAS rows above and helper-blocks.md's `BRAND_PRIMARY` are existing house precedent). Constants reach the DOM via inline `style` or by choosing between STATIC class strings — never template-interpolated into arbitrary classes (`` bg-[${BRAND.x}] ``): Tailwind's JIT extracts classes by static source scan (standard-Tailwind inference, not Softr-verified) |
|
|
92
96
|
| Using `window.addEventListener("beforeunload", ...)` as the only unsaved-changes guard in a form block | Softr is a SPA. Internal nav (Softr's nav bar, sidebar links, `<NavigationAction>`) changes the route via the client-side router — `beforeunload` only fires on full page unload (tab close, refresh, external link), so the warning silently misses every in-app navigation. Use `useNavigationBlocker(isDirty)` from `@/lib/use-navigation-blocker` instead; it covers SPA nav AND browser unload with one API. Softr's Vibe Coding bundler often wires this automatically when a form is detected as dirty — you only need to add it manually for advanced cases (multi-step forms, custom dirty tracking, blocking on non-form state). See [common-patterns.md](common-patterns.md#navigation-blocker-for-unsaved-changes). |
|
|
93
|
-
| Targeting Softr's hashed build classes (e.g. `.f8f11e5_m9ntthp`) when restyling the native header/nav from `custom-code-header.html` | Softr regenerates the hash on every deploy, so the rule silently dies. Target stable hooks: `.softr-topbar`, `.softr-nav-link`, `.softr-nav-button`, `.softr-nav-logo`, `#topbar-root`; for dropdown menus (no `softr-*` class) use the Radix/ARIA attrs `[role="menu"]` / `[role="menuitem"]` / `[role="group"]` / `[aria-expanded="true"]`, scoped under `.softr-topbar`. The native header is Softr chrome (main document), not a block — it can't be built as a Vibe Coding block. (A landing page with the native header HIDDEN may instead ship a block-owned in-block header — see [static-blocks.md](static-blocks.md#block-owned-landing-page-header); that's a different pattern, not a rebuild of native chrome.) See [native-chrome-styling.md](native-chrome-styling.md). |
|
|
97
|
+
| Targeting Softr's hashed build classes (e.g. `.f8f11e5_m9ntthp` in June 2026; the navigation block's prefix was `_5f91d6c_` by October) when restyling the native header/nav from `custom-code-header.html` | Softr regenerates the hash on every deploy, so the rule silently dies. Target stable hooks: `.softr-topbar`, `.softr-nav-link`, `.softr-nav-button`, `.softr-nav-logo`, `#topbar-root`; for dropdown menus (no `softr-*` class) use the Radix/ARIA attrs `[role="menu"]` / `[role="menuitem"]` / `[role="group"]` / `[aria-expanded="true"]`, scoped under `.softr-topbar`. The native header is Softr chrome (main document), not a block — it can't be built as a Vibe Coding block. (A landing page with the native header HIDDEN may instead ship a block-owned in-block header — see [static-blocks.md](static-blocks.md#block-owned-landing-page-header); that's a different pattern, not a rebuild of native chrome.) See [native-chrome-styling.md](native-chrome-styling.md). |
|
|
98
|
+
| Taking the frame colour from Softr's hashed theme variables (`var(--_5f91d6c_vnohg20)`) or hashed classes in header CSS | The hash is set per native block package, not once per app — `_5f91d6c_` on the navigation block, `_03ef538_` on the user-accounts block (seen 2026-10-05) — and the theme variables carry the same prefix, so a rule that names one breaks when that package changes. Copy the Studio theme colour of the top bar and sidebar into your own token, e.g. `--app-frame: #A85935`, with a comment saying to update it when the theme changes. Copy the THEME colour (Studio's theme settings, or the rendered sidebar's computed background), not the brand primary from DESIGN.md: they differed (theme `#A85935`, DESIGN.md primary `#B4532A`; measured 2026-10-05). See [native-chrome-styling.md → App frame (navigation layout)](native-chrome-styling.md#app-frame-navigation-layout) |
|
|
94
99
|
| Softr nav dropdown panel shows a tall blank gap below the items, and `height: auto` won't shrink it | The items sit in a CSS grid Softr sets to `grid-auto-flow: column` with pre-sized empty row tracks (`grid-template-rows: 60px 60px…`). Override the flow on `.softr-topbar [role="menu"] [role="group"]`: `grid-auto-flow: row !important; grid-template-rows: none !important; grid-auto-rows: auto !important` (leave `grid-template-columns` to preserve the menu width). Verified June 2026. See [native-chrome-styling.md](native-chrome-styling.md). |
|
|
95
|
-
| Setting the page background on `body` (or any single element) — it appears to do nothing | Softr paints the
|
|
100
|
+
| Setting the page background on `body` (or any single element) — it appears to do nothing | Softr paints the page fill on several stacked layers, so styling one gets covered: `html`, `body`, `#page-content`, each Vibe block's host and each native block's outer `<section>` (measured 2026-10-05; the layer list is in the `html, body` row above). **Top bar only:** paint your backdrop on `html`, then clear the duplicates above it: `body`, `#page-content`, and `#page-content div` — but EXCLUDE the header subtree with `:not(.softr-topbar):not(.softr-topbar *)` (it renders inside `#page-content`, and `#page-content`'s id specificity would otherwise flatten the dropdown panel). Verified June 2026. See [native-chrome-styling.md](native-chrome-styling.md#page-background). **With Softr's sidebar,** that `div` clear would also strip `.softr-sidebar`'s fill (inferred) and never reaches native `<section>`s: use the scoped rules in [native-chrome-styling.md → App frame (navigation layout)](native-chrome-styling.md#app-frame-navigation-layout). |
|
|
96
101
|
|
|
97
102
|
## Printing
|
|
98
103
|
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
How to check a deployed block's rendering and behaviour in a Softr preview with the
|
|
4
4
|
[agent-browser](https://github.com/vercel-labs/agent-browser) CLI. **Verified 2026-10-01** with
|
|
5
5
|
agent-browser v0.38.1 on macOS (Node 22) against a real Softr preview; only the commands under
|
|
6
|
-
[Untested but promising](#untested-but-promising) were not run.
|
|
6
|
+
[Untested but promising](#untested-but-promising) were not run. [Testing Custom Code header
|
|
7
|
+
CSS](#testing-custom-code-header-css) was verified 2026-10-05, except where it says otherwise.
|
|
7
8
|
|
|
8
9
|
## When to use it
|
|
9
10
|
|
|
@@ -156,6 +157,160 @@ ab close # ✓ Browser closed (no process left beh
|
|
|
156
157
|
Give a path; without one, it writes to a temp directory. A saved screenshot costs no tokens until
|
|
157
158
|
someone opens it; one shown inline costs about 1.5k. Left alone, the daemon exits after an hour idle.
|
|
158
159
|
|
|
160
|
+
## Testing Custom Code header CSS
|
|
161
|
+
|
|
162
|
+
CSS in **Settings → Custom Code → Code inside header** applies to every page of the app, and the
|
|
163
|
+
builder usually pastes it, not you. So test it in the preview before it is pasted, then prove what
|
|
164
|
+
went live. **Verified 2026-10-05** on one app with Softr's top bar and sidebar, using the app-frame
|
|
165
|
+
code in [native-chrome-styling.md → App frame (navigation layout)](native-chrome-styling.md#app-frame-navigation-layout)
|
|
166
|
+
with its Vibe-host rule in the unscoped form (see step 4), with agent-browser and the desktop app's
|
|
167
|
+
Browser pane; anything else is marked.
|
|
168
|
+
|
|
169
|
+
**Where header code shows:** on the published app, and in the preview: after a paste and a publish,
|
|
170
|
+
a fresh preview load applied it with nothing injected (verified 2026-10-05). Whether the preview
|
|
171
|
+
shows header code that is pasted but not yet published is untested. The Studio editor canvas is not
|
|
172
|
+
a test surface: header code is not known to render there.
|
|
173
|
+
|
|
174
|
+
### 1. Before pasting: inject it into the preview
|
|
175
|
+
|
|
176
|
+
Open the page as in [step 1](#1-session-preview-cookie-page), as the user whose navigation you are
|
|
177
|
+
styling: on the preview origin run `fetch('/studio/impersonate/<softrUserId>')`, then open the page
|
|
178
|
+
again ([how](softr-mcp.md#testing-as-any-app-user-without-logins--the-preview-as-switcher)). Then
|
|
179
|
+
inject the file exactly as it will be pasted, tagged so that a re-run replaces it:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
node -e '
|
|
183
|
+
const h = require("fs").readFileSync("custom-code-header.html", "utf8");
|
|
184
|
+
process.stdout.write(`(() => {
|
|
185
|
+
document.querySelectorAll("[data-hdr-test]").forEach(n => n.remove());
|
|
186
|
+
const t = document.createElement("template"); t.innerHTML = ${JSON.stringify(h)};
|
|
187
|
+
for (const n of [...t.content.children]) { n.setAttribute("data-hdr-test", ""); document.head.appendChild(n); }
|
|
188
|
+
return "injected";
|
|
189
|
+
})()`);' > inject.js
|
|
190
|
+
ab eval --stdin < inject.js # "injected"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
- The injected copy lasts until the next load: inject again after every `open` or reload. A width
|
|
194
|
+
change keeps it.
|
|
195
|
+
- This tests the `<link>` and `<style>` parts. A `<script>` in the header is out of scope.
|
|
196
|
+
- If an older version is already live, the preview carries it too and the injected copy only adds
|
|
197
|
+
to it, so a rule you deleted still applies. Remove the live `<style>` first, found by a token
|
|
198
|
+
only it contains (inferred, not run).
|
|
199
|
+
- A Browser pane opened on the preview link shows the toolbar shell, with the app in the
|
|
200
|
+
same-origin `#preview-iframe`. Open the direct page URL instead, so that `document` is the app's.
|
|
201
|
+
|
|
202
|
+
### 2. Measure; screenshots are the extra
|
|
203
|
+
|
|
204
|
+
Computed values answer the question; a screenshot only illustrates it. A hidden pane times out on
|
|
205
|
+
screenshots ([why](#tool-choice-and-why)) but measures fine, so measure first and take any
|
|
206
|
+
screenshot with agent-browser, to disk. Save this as `measure.js`:
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
(() => {
|
|
210
|
+
const q = s => document.querySelector(s), bg = e => e ? getComputedStyle(e).backgroundColor : 'none';
|
|
211
|
+
const main = q('#main-content'), sr = q('#sidebar-root'), a = sr ? getComputedStyle(sr, '::after') : null;
|
|
212
|
+
return JSON.stringify({
|
|
213
|
+
w: innerWidth, sidebar: !!q('.softr-sidebar'), tabBar: !!q('.softr-bottombar'),
|
|
214
|
+
html: bg(document.documentElement), body: bg(document.body), page: bg(q('#page-content')),
|
|
215
|
+
main: bg(main), radius: main ? getComputedStyle(main).borderTopLeftRadius : 'none',
|
|
216
|
+
host: bg(q('#main-content [data-role="vibe-block-root"]')),
|
|
217
|
+
corner: a ? a.content + ' @ ' + a.left : 'none',
|
|
218
|
+
overflowX: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
|
219
|
+
});
|
|
220
|
+
})()
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
What the verified live code gave (2026-10-05; its Vibe-host rule was the unscoped
|
|
224
|
+
`#main-content [data-role="vibe-block-root"]`, and the recipe's scoped form is untested):
|
|
225
|
+
|
|
226
|
+
| Window | Navigation | `html`, `body`, `#page-content` | `#main-content` | `corner` |
|
|
227
|
+
|---|---|---|---|---|
|
|
228
|
+
| 768px and wider | top bar + sidebar | frame colour | sheet colour, 24px radius | `"" @ 280px` open, `"" @ 57px` collapsed |
|
|
229
|
+
| 767px and narrower | phone tab bar | sheet colour | transparent (`rgba(0, 0, 0, 0)`), 0px radius; the paper is on `html`, `body` and `#page-content` | `none @ auto` |
|
|
230
|
+
|
|
231
|
+
At the widths checked for it (1440, 1024 and 390/375) the Vibe host was `rgba(0, 0, 0, 0)` and
|
|
232
|
+
there was no sideways scroll. On phones `#sidebar-root` is still in the DOM, empty, 0px wide and
|
|
233
|
+
`position: static` (not sticky), which is why the corner rule is guarded with
|
|
234
|
+
`:has(.softr-sidebar)`. Without the guard the `::after` still renders there and is placed against
|
|
235
|
+
the page: likely off the right edge, adding sideways scroll (seen in a mock, not on Softr).
|
|
236
|
+
|
|
237
|
+
### 3. The width sweep
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
for w in 1440 1280 1024 900 768 767 390; do ab set viewport $w 900; ab wait 1200; ab eval --stdin < measure.js; done
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Then collapse the sidebar (the top bar's "Toggle sidebar" button, a ref from `snapshot -i`; it
|
|
244
|
+
writes nothing) and measure 1024 and 768 again.
|
|
245
|
+
|
|
246
|
+
- **767 / 768 is Softr's switch, to the pixel:** 767 gives the phone tab bar, 768 the top bar and
|
|
247
|
+
sidebar. At 768 with the sidebar open, a block gets 488px.
|
|
248
|
+
- **A plain width change switches the layout live.** `ab set viewport` alone moved between sidebar
|
|
249
|
+
and tab bar; no reload needed.
|
|
250
|
+
- **Reload after leaving a mobile-device emulation.** A pane loaded under a mobile preset (an
|
|
251
|
+
Android user agent and touch points, not just a width) kept the phone layout when widened to
|
|
252
|
+
1440, until a reload (seen 2026-10-05; most likely a device check at load, inferred).
|
|
253
|
+
- **The collapsed sidebar stayed collapsed** at later widths in one headless run (seen once):
|
|
254
|
+
open it again, or expect 57px.
|
|
255
|
+
|
|
256
|
+
### 4. Pages without navigation
|
|
257
|
+
|
|
258
|
+
The recipe scopes its rules with `:has(.softr-sidebar, .softr-bottombar)`, so pages without Softr's
|
|
259
|
+
navigation (log in, sign up, 404) should keep Softr's own colours.
|
|
260
|
+
|
|
261
|
+
- **Before pasting:** inject on a 404 page in the preview (any path that does not exist): `html`
|
|
262
|
+
and `body` stayed rgb(255,255,255). A logged-in preview sends `/login` to the home page, so
|
|
263
|
+
`/login` cannot be checked there.
|
|
264
|
+
- **Once pasted:** open `/login` and a 404 page on the published app, logged out. With the code
|
|
265
|
+
live, `html`, `body` and `#page-content` stayed white on both, with no top bar, sidebar or tab
|
|
266
|
+
bar (verified 2026-10-05).
|
|
267
|
+
- **The Vibe-host rule depends on which form you have.** In the verified live code it was unscoped
|
|
268
|
+
(`#main-content [data-role="vibe-block-root"]`) and applied on these pages too; on a page without
|
|
269
|
+
navigation that holds a Vibe block it is probably invisible, because the page behind the block
|
|
270
|
+
is the same theme colour (inferred). The recipe scopes it with `:has(.softr-sidebar,
|
|
271
|
+
.softr-bottombar)`, so nothing should apply there (untested as written). Either way, no page
|
|
272
|
+
without navigation but with a Vibe block was tested: on one, check that the host keeps the
|
|
273
|
+
theme white.
|
|
274
|
+
|
|
275
|
+
### 5. After the paste: prove what is live
|
|
276
|
+
|
|
277
|
+
**A publish publishes everything.** Header code reaches the published app with a publish, and a
|
|
278
|
+
publish also pushes every unpublished page live (seen 2026-10-05: unfinished pages went live with a
|
|
279
|
+
header-code publish). Before asking anyone to publish header code, check what else is unpublished,
|
|
280
|
+
and say so in the ask.
|
|
281
|
+
|
|
282
|
+
Then fetch the published page and pull the code out. Softr carries it in an inline script as
|
|
283
|
+
`appCustomHeaderCode: "…"`, an unquoted key inside `SoftrPageRenderer.render({…})`: JavaScript, not
|
|
284
|
+
JSON, so read the string literal rather than parsing the object. `json.loads` read Softr's string on
|
|
285
|
+
2026-10-05:
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
curl -sL 'https://<subdomain>.softr.app/' -o pub.html
|
|
289
|
+
python3 - <<'EOF'
|
|
290
|
+
import json, re
|
|
291
|
+
s = open('pub.html').read()
|
|
292
|
+
m = re.search(r'appCustomHeaderCode:\s*("(?:[^"\\]|\\.)*")', s)
|
|
293
|
+
if not m: raise SystemExit('appCustomHeaderCode not found: wrong page, a redirect or an empty body; fetch / again')
|
|
294
|
+
live = json.loads(m.group(1))
|
|
295
|
+
mine = open('custom-code-header.html').read()
|
|
296
|
+
rules = lambda t: re.sub(r'\s+', '', re.sub(r'<!--.*?-->|/\*.*?\*/', '', t, flags=re.S))
|
|
297
|
+
print(len(live), 'bytes live;', 'rules match' if rules(live) == rules(mine) else 'RULES DIFFER')
|
|
298
|
+
EOF
|
|
299
|
+
# 1516 bytes live; rules match
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
- **Compare the rules, not the text.** The pasted copy can lose or shorten comments; it did on
|
|
303
|
+
2026-10-05, and the rules still matched.
|
|
304
|
+
- **`pageCustomHeaderCode`** is the page-level header code, and `appCustomFooterCode` /
|
|
305
|
+
`pageCustomFooterCode` are the footers: check that they are empty, or hold what you expect.
|
|
306
|
+
- **The page source opens with `<!-- Last Published: … -->`.** Check that it moved, so you are not
|
|
307
|
+
reading the previous publish.
|
|
308
|
+
- The key is in every page's source, logged out too: it was the same on Home, `/login` and a 404
|
|
309
|
+
page.
|
|
310
|
+
|
|
311
|
+
Then run the sweep again in a fresh preview with nothing injected, and the pages without navigation
|
|
312
|
+
on the published app, logged out.
|
|
313
|
+
|
|
159
314
|
## Gotchas
|
|
160
315
|
|
|
161
316
|
- **zsh does not word-split.** `AB="agent-browser --session x"; $AB open …` fails with "command not
|