softr-vibe-coding 2.1.1 → 2.2.0

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 CHANGED
@@ -4,6 +4,13 @@ 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.2.0] - 2026-08-31
8
+ - Add editable-settings and static-blocks references: undocumented useLongTextSetting + navigation array-schema type, settings-first doctrine, full-bleed/full-viewport heroes, block-owned fixed headers
9
+
10
+ ## [2.1.2] - 2026-08-26
11
+ - Document four linked-record write traps verified live 2026-08-26
12
+ - Fail the build when main moves ahead of npm
13
+
7
14
  ## [2.1.1] - 2026-08-26
8
15
  - Release 2.1.1
9
16
  - Document external-URL attachment ingestion (copy, not link)
package/README.md CHANGED
@@ -15,13 +15,15 @@
15
15
  This Claude skill teaches Claude Code how to generate complete, polished Softr Vibe Coding blocks as `.tsx` / `.jsx` files (the current platform compiles TypeScript with modern syntax — verified live August 2026). It includes:
16
16
 
17
17
  - **Complete Vibe Coding API reference** — `useRecords`, `q.select()`, mutations, uploads, metrics, charts, editable settings, `useProxyFetch` for REST APIs
18
+ - **Editable settings deep-dive** — full hook catalog including verified-undocumented capabilities (`useLongTextSetting`, the `navigation` array-schema type), settings-first design doctrine so clients edit copy/images/links in Content → Settings without re-prompting
19
+ - **Static marketing blocks** — heroes, landing headers, pricing tables, footers: editorial baseline, full-bleed layouts, full-viewport sizing, block-owned fixed headers with scroll-condensing treatment
18
20
  - **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
19
21
  - **Helper blocks & cross-block patterns** — Invisible helper blocks for multi-table access via `window` globals + `CustomEvent`, `useWindowData` hook, breadcrumb navigation, saved views architecture
20
22
  - **Advanced integrations** — Shadow DOM CSS isolation for third-party libraries (Leaflet, Mapbox, TinyMCE, Quill, FullCalendar)
21
23
  - **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
22
24
  - **UI/UX design guidelines** — 26 sections covering visual hierarchy, color, typography, spacing, motion design, accessibility, responsive patterns, and an AI slop anti-pattern checklist
23
- - **Self-validation** — Claude checks for Softr platform compatibility (inline hook options, correct payload shapes, correct imports, container wrappers, `getFieldValue()` wrapping, hooks ordering) before delivering code
24
- - **Premium visual baseline** — Every block ships polished from v1: gradient backgrounds, card elevation, loading skeletons, empty states, error states
25
+ - **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
26
+ - **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
25
27
  - **Debug utilities** — Field Inspector, API Response Inspector, and User Inspector blocks for diagnosing data source and permissions issues
26
28
  - **Softr MCP integration** — when the [official Softr MCP server](https://docs.softr.io/mcp/overview) is installed (`claude mcp add --transport http softr https://mcp.softr.io/mcp`), Claude reads Softr DB schema, field IDs, and dropdown option UUIDs directly, browses connected Airtable / Google Sheets / Notion / Supabase integrations down to field level, and can even create and deploy Vibe Coding blocks straight into your app — no more pasting `tablespace-with-tables` JSON or copy-pasting code into Studio. See `references/softr-mcp.md`.
27
29
 
@@ -161,20 +163,20 @@ Create a contact form that creates records in our Airtable Contacts table
161
163
 
162
164
  ```
163
165
  softr-vibe-coding/
164
- ├── SKILL.md # Main skill (330 lines)
166
+ ├── SKILL.md # Main skill
165
167
  │ # Workflow, code structure, visual baseline,
166
168
  │ # components, settings, 21 hard constraints
167
169
 
168
- ├── ui-ux-guidelines.md # Design reference (746 lines)
170
+ ├── ui-ux-guidelines.md # Design reference
169
171
  │ # 26 sections: hierarchy, color, typography,
170
172
  │ # spacing, motion, accessibility, AI slop checklist
171
173
 
172
174
  ├── references/ # Advanced patterns (loaded on demand)
173
- │ ├── helper-blocks.md # Cross-block communication (370 lines)
175
+ │ ├── helper-blocks.md # Cross-block communication
174
176
  │ │ # Invisible helper blocks, window globals,
175
177
  │ │ # CustomEvent, useWindowData hook, breadcrumbs,
176
178
  │ │ # saved views, companion field helpers
177
- │ ├── airtable-automations.md # Airtable automation scripting (350 lines)
179
+ │ ├── airtable-automations.md # Airtable automation scripting
178
180
  │ │ # "Run a script" automation action vs.
179
181
  │ │ # Scripting Extension, cross-table cascades,
180
182
  │ │ # batch update gotchas, field-ID discipline,
@@ -183,7 +185,7 @@ softr-vibe-coding/
183
185
  │ │ # tools (create/edit/version/deploy), integrations
184
186
  │ │ # browsing (Airtable/Sheets/Notion/Supabase),
185
187
  │ │ # Softr DB schema + record tools, auth, permissions
186
- │ ├── advanced-integrations.md # Shadow DOM CSS isolation (69 lines)
188
+ │ ├── advanced-integrations.md # Shadow DOM CSS isolation
187
189
  │ │ # Leaflet, Mapbox, TinyMCE, Quill, FullCalendar
188
190
  │ ├── native-chrome-styling.md # Restyle Softr's native shell (header, footer,
189
191
  │ │ # nav, dropdowns, page background) via global
@@ -192,13 +194,22 @@ softr-vibe-coding/
192
194
  │ ├── native-block-filters.md # Dynamic date / URL-param filters + custom filter
193
195
  │ │ # controls on native List/Grid blocks — wide-range
194
196
  │ │ # sentinel, inject into filter row, survive re-renders
195
- │ ├── anti-patterns.md # Categorized violation catalog (86 lines)
197
+ │ ├── anti-patterns.md # Categorized violation catalog
196
198
  │ │ # Data access, mutations, hooks, layout,
197
- │ │ # permissions, helper blocks
198
- │ ├── common-patterns.md # Small reusable patterns (102 lines)
199
- │ │ # localStorage cross-page state,
200
- │ │ # clipboard copy button
201
- └── quick-reference.md # Syntax cheat sheet (207 lines)
199
+ │ │ # permissions, editable settings, helper blocks
200
+ │ ├── common-patterns.md # Small reusable patterns
201
+ │ │ # localStorage cross-page state, clipboard copy,
202
+ │ │ # navigation blocker, scroll-condensing header,
203
+ │ # auth-aware CTA, image masks, blobs, dot lists
204
+ │ ├── editable-settings.md # Settings deep-dive: full hook catalog incl.
205
+ │ │ # verified-undocumented useLongTextSetting +
206
+ │ │ # "navigation" array-schema type, granularity
207
+ │ │ # doctrine, naming, rename-resets gotcha
208
+ │ ├── static-blocks.md # Static marketing archetype: heroes, landing
209
+ │ │ # headers, pricing, footers — workflow deltas,
210
+ │ │ # editorial baseline, full-bleed + full-viewport,
211
+ │ │ # block-owned header, section anchors
212
+ │ └── quick-reference.md # Syntax cheat sheet
202
213
  │ # Imports, hook signatures, mutation shapes,
203
214
  │ # field mapping, component skeleton
204
215
 
@@ -212,11 +223,11 @@ softr-vibe-coding/
212
223
  ├── multi-datasource.md # Several data sources in ONE block: datasource.define(),
213
224
  │ # the from: parameter, getting the datasource UUIDs
214
225
  ├── reading.md # useRecords, filtering, sorting, pagination,
215
- │ # metrics, charts, current user (198 lines)
226
+ │ # metrics, charts, current user
216
227
  ├── writing.md # Mutations, sequential write queues, uploads,
217
228
  │ # linked record format, cross-table writes
218
229
  ├── fields.md # getFieldValue(), field type shapes, record
219
- │ # structure, debug utilities (160 lines)
230
+ │ # structure, debug utilities
220
231
  ├── rest-api.md # useProxyFetch + useQuery (full docs)
221
232
  ├── softr-database.md # Native DB — field IDs, no rate limits
222
233
  ├── airtable.md # Column names, PAT vs OAuth, rate limits
@@ -235,7 +246,7 @@ softr-vibe-coding/
235
246
 
236
247
  ### How context loading works
237
248
 
238
- Only `SKILL.md` loads into Claude's context when the skill triggers (~330 lines). The data source guides, reference files, and UI/UX guidelines load **on demand** — Claude reads only the files relevant to your specific block. This keeps context lean even with 24 files totaling 3,100+ lines.
249
+ Only `SKILL.md` loads into Claude's context when the skill triggers. The data source guides, reference files, and UI/UX guidelines load **on demand** — Claude reads only the files relevant to your specific block. This keeps context lean even with 26 files totaling 6,500+ lines.
239
250
 
240
251
  ---
241
252
 
@@ -272,7 +283,7 @@ The skill enforces these automatically, but good to know (verified live against
272
283
  - Every code recompile resets the block's auto-registered Actions to default permissions — tighten permissions after the last redeploy
273
284
  - No `import React from 'react'` — use named imports (`import { useState } from "react"`)
274
285
  - Must use `export default function Block()`
275
- - Must wrap layout in `<div className="container py-0"><div className="content">`
286
+ - Wrap layout in `<div className="container py-0"><div className="content">` for app/content blocks (house convention for width alignment with native blocks) — the platform default is actually full width, so full-bleed marketing blocks (heroes, banners, footers) legitimately omit the wrappers and own their gutters
276
287
  - Only ONE `useRecords` call per **datasource** — but a block can connect to several sources; declare them with `datasource.define()` and pass `from:` on every hook
277
288
  - `fetchNextPage` never in the render body (infinite loop) — call it from an event handler (Load More `onClick`) or a guarded `useEffect`
278
289
  - All hooks declared before any conditional `return` — React error #310
package/SKILL.md CHANGED
@@ -21,7 +21,7 @@ allowed-tools: Read Write Glob Grep Bash
21
21
 
22
22
  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 `get_vibe_coding_docs` and a 15-block production deployment, 2026-08-25).
23
23
 
24
- > **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).
24
+ > **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
25
 
26
26
  ## Your Workflow
27
27
 
@@ -51,24 +51,24 @@ You generate complete, production-ready Softr Vibe Coding blocks as TypeScript R
51
51
 
52
52
  Do not silently default to Softr's brand. The user must opt in to defaults explicitly.
53
53
 
54
- 2. **Understand what the user wants to build.** They will describe it in plain language. Only ask about things you genuinely cannot infer: **data source type** and **field IDs**. For everything else, make sensible defaults and flag your assumptions.
54
+ 2. **Understand what the user wants to build — and fork on archetype.** They will describe it in plain language. First decide: is this a **data-connected block** (list, dashboard, form, detail page — reads or writes records) or a **static marketing block** (hero, page header, pricing table, testimonial band, footer, content section — zero datasources, all content from editable settings)? For static blocks, read [references/static-blocks.md](references/static-blocks.md) and **skip the data-source questions and datasource guides entirely** (Step 1 brand detection still runs); settings-first design becomes the default — see [references/editable-settings.md](references/editable-settings.md#granularity-doctrine-settings-first-static-blocks). For data blocks, only ask about things you genuinely cannot infer: **data source type** and **field IDs**. For everything else, make sensible defaults and flag your assumptions.
55
55
 
56
56
  3. **Apply defaults for the rest, don't ask.** Infer these from context instead of asking:
57
57
  - **Project folder**: Derive from the block description (e.g., "partner-portal", "client-dashboard"). If the user has already specified a folder in this session, reuse it.
58
58
  - **Brand colors**: Use whatever was chosen in Step 1 — DESIGN.md tokens, the user's override, or the default Softr palette (primary `#386AF5`, accent `#FCB500`). Never silently fall back to defaults.
59
59
  - **Filename**: Derive from the block purpose (e.g., `partner-invite.tsx`, `team-directory.tsx`). The user can rename later.
60
60
 
61
- 4. **Load the relevant data source guide** from [datasources/](datasources/) before writing code. Read the specific guide for the user's data source type.
61
+ 4. **Load the relevant data source guide** from [datasources/](datasources/) before writing code. Read the specific guide for the user's data source type. (Static marketing blocks: skip this step — read [references/static-blocks.md](references/static-blocks.md) instead.)
62
62
 
63
63
  5. **Write the complete block file** (`.tsx` preferred; `.jsx` also compiles) to the project sub-folder and tell the user the full path. Create the sub-folder if it doesn't exist yet. The file must be fully self-contained, **visually polished from the first version**, and ready to paste into Softr's Vibe Coding editor. Styling is not an afterthought -- it ships in v1. **Never deliver code inline in chat.** Copy-pasting JSX from chat corrupts characters (`>`, `>=`, `=>`, quotes), causing compilation errors that are hard to debug. Always write to a file.
64
64
 
65
65
  **Delivery path:** if the official Softr MCP server is connected with Applications & Forms full access, offer to deploy the block directly after writing the file — `create_vibe_coding_block` (or `update_vibe_coding_block_code` for edits) plus `connect_vibe_coding_block_data_source` to wire the data. The local `.tsx` file stays the source of truth. Remember: every code push resets the block's Action permissions to defaults (Hard Constraint 21), and a block whose data source isn't connected saves fine but errors at page load. Details in [references/softr-mcp.md](references/softr-mcp.md).
66
66
 
67
- 6. **Self-validate before delivering.** Before presenting the code as complete, verify:
67
+ 6. **Self-validate before delivering.** Before presenting the code as complete, verify. (Data-hook items apply only to data-connected blocks; static marketing blocks swap in the checklist deltas from [references/static-blocks.md](references/static-blocks.md#workflow-deltas).)
68
68
  - Every data hook is called with an **inline options object literal** — `useRecords({ ... })` written through a variable or wrapper function fails to compile (verified live 2026-08-25). Share `q.select` mappings between hooks, never whole options objects
69
69
  - All imports use named imports (no `import React from 'react'`)
70
70
  - `export default function Block()` is present
71
- - Container + content wrappers present (`<div className="container py-0"><div className="content">`)
71
+ - 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")
72
72
  - `// BLOCK PLACEMENT:` comment present at top of file with wrapper classes matching the placement (see "Block Placement & Page Spacing")
73
73
  - Loading, error, and empty states all handled
74
74
  - Mutation calls gated behind `enabled` check (if using mutations)
@@ -81,7 +81,10 @@ You generate complete, production-ready Softr Vibe Coding blocks as TypeScript R
81
81
  - Mutations use `recordId` (not `id`) and call `refetch()` in `onSuccess`
82
82
  - `useRecordUpdate` payload is `{ recordId, fields: { ... } }` — nested. `useRecordCreate` payload is **flat** (no `fields` wrapper). The two shapes are asymmetric by design (verified live 2026-08-25)
83
83
  - 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))
84
- - No hardcoded domains in links -- use relative paths (`/page?recordId=...`)
84
+ - No hardcoded domains in links -- use relative paths (`/page?recordId=...`); same-page anchors written relative too (`/#section`)
85
+ - 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))
86
+ - Array-setting rows keyed by **index**, never by a builder-editable field value
87
+ - Media settings that may start empty (`src: ""`) gated with a conditional render or placeholder — never an unconditional `<img src={setting.src}>`
85
88
 
86
89
  ## What to Clarify
87
90
 
@@ -108,7 +111,7 @@ When the user describes their block, figure out which of these areas apply and a
108
111
  Softr logo assets (for blocks that need Softr branding):
109
112
  - Icon + wordmark (SVG): `https://cdn.brandfetch.io/idytCFzVcY/theme/dark/logo.svg`
110
113
  - Icon only (PNG): `https://cdn.brandfetch.io/idytCFzVcY/w/1024/h/1024/theme/dark/icon.png`
111
- - **Layout and style**: Cards vs. table vs. list? How many columns? Apply the Premium Visual Baseline regardless.
114
+ - **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.
112
115
  - **Interactivity**: Create/edit/delete? Filtering? Sorting? Pagination?
113
116
  - **User context**: Does it need to know who's logged in?
114
117
  - **Settings**: Should anything be editable by the Softr builder (titles, images, toggle sections)?
@@ -166,6 +169,8 @@ For advanced patterns beyond data fetching, load the relevant reference when the
166
169
  | The official **Softr MCP server** — schema discovery and record reads for Softr DB, field-level browsing of connected Airtable / Google Sheets / Notion / Supabase integrations, and **creating, editing, versioning, and deploying Vibe Coding blocks directly** (`get_vibe_coding_docs`, `create_vibe_coding_block`, `connect_vibe_coding_block_data_source`, ...) | [references/softr-mcp.md](references/softr-mcp.md) |
167
170
  | 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) |
168
171
  | 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) |
172
+ | **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) |
173
+ | **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) |
169
174
 
170
175
  ## Code Structure
171
176
 
@@ -190,9 +195,11 @@ export default function Block() {
190
195
  }
191
196
  ```
192
197
 
193
- Always wrap the outermost layout in `container` and `content` divs — these constrain width to match the Softr app's max width settings.
198
+ 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 `get_vibe_coding_docs` and a rendering wrapper-free Studio-AI hero).
194
199
 
195
- **Exception:** Blocks inside Softr column containers — omit wrappers so Softr controls layout.
200
+ **Exceptions (omit the wrappers deliberately):**
201
+ - Blocks inside Softr column containers — Softr controls layout.
202
+ - 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`) 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).
196
203
 
197
204
  ## Block Placement & Page Spacing
198
205
 
@@ -229,7 +236,7 @@ The `// BLOCK PLACEMENT:` marker is intentionally stable so it can be grepped an
229
236
 
230
237
  ### Spacing values (defaults)
231
238
 
232
- **Container** (always): `<div className="container py-0">`
239
+ **Container** (default — omitted by full-bleed blocks and blocks inside column containers; see table below): `<div className="container py-0">`
233
240
 
234
241
  **Inner wrapper** classes by block position:
235
242
 
@@ -239,6 +246,7 @@ The `// BLOCK PLACEMENT:` marker is intentionally stable so it can be grepped an
239
246
  | Middle block | `py-3 px-8` | 12px + Softr separator + 12px ≈ 24px between blocks |
240
247
  | Last block (footer-adjacent) | `pt-3 pb-12 px-8` | 12px top + 48px bottom for footer breathing room |
241
248
  | Standalone (only block on page) | `pt-3 pb-12 px-8` | Treat like a last block |
249
+ | 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) |
242
250
 
243
251
  **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.
244
252
 
@@ -246,10 +254,16 @@ The `// BLOCK PLACEMENT:` marker is intentionally stable so it can be grepped an
246
254
 
247
255
  **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.
248
256
 
257
+ ### Full-viewport hero blocks
258
+
259
+ 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).
260
+
249
261
  ## Premium Visual Baseline
250
262
 
251
263
  **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.
252
264
 
265
+ **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).
266
+
253
267
  Refer to [ui-ux-guidelines.md](ui-ux-guidelines.md) for full design principles.
254
268
 
255
269
  ### 1. Gradient background wrapper
@@ -303,9 +317,11 @@ Adjust the top gradient color to complement the user's brand.
303
317
  **Tailwind CSS** is pre-configured. **Semantic color tokens** preferred:
304
318
  `bg-background`, `bg-card`, `bg-primary`, `bg-secondary`, `bg-muted`, `bg-accent`, `bg-destructive`, `border`, `border-input`
305
319
 
320
+ **Arbitrary values compile in full** — the platform's Tailwind build is JIT, so the whole arbitrary-value syntax works, including opacity modifiers on arbitrary hex (`bg-[#FAF5EC]/85`), variant + arbitrary + opacity combined (`hover:bg-[#6E7A5C]/10`), negative arbitrary values (`-top-[22%]`, `hover:-translate-y-[1px]`), arbitrary object-position (`object-[62%_25%]`), arbitrary z (`z-[1]`), and vw sizing (verified 2026-08-31 from rendering Studio-AI output). Classes must be **static source strings** — never template-interpolate (`` bg-[${x}] ``); JIT extracts classes by static scan (standard-Tailwind inference, not Softr-verified). When to reach for them vs. the scale: see the editorial lane in [ui-ux-guidelines.md](ui-ux-guidelines.md) §7. (This covers arbitrary *values* and standard variants; arbitrary *selector* variants like `[&_svg]:` have at least one known bundler failure — see the SelectTrigger row in [references/anti-patterns.md](references/anti-patterns.md#layout--styling).)
321
+
306
322
  **Font classes:** `font-heading`, `font-sans`, `font-mono`
307
323
 
308
- **Conditional classNames:** `import { cn } from "@/lib/utils";`
324
+ **Conditional classNames:** `import { cn } from "@/lib/utils";` — template-literal conditionals (`` className={`base ${cond ? "a" : "b"}`} ``) are equally valid (Studio AI emits them); prefer `cn()` when merging many groups or de-duplicating conflicting classes.
309
325
 
310
326
  **DO NOT USE:** CSS modules, styled-components, or CSS file imports.
311
327
 
@@ -376,6 +392,19 @@ When the action navigates to a record-specific page, pass the runtime record id
376
392
 
377
393
  For on-brand custom styling (matching DESIGN.md inline-style conventions instead of shadcn Button), wrap the same pattern with a styled `<button>` — `<NavigationAction>` will render its children into the button's slot. The `asChild` attribute on Button is what enables this slot composition; without it shadcn renders its own native button and ignores `<NavigationAction>`.
378
394
 
395
+ **Third shape — standalone `<NavigationAction>` with `className`** (text-style links, nav items — no Button wrapper). NavigationAction renders its own clickable element and forwards `className` onto it; include UA-style resets since that element ships default chrome. Observed in Studio-AI output 2026-08-31, not in the official developer guide — don't treat className forwarding as officially guaranteed:
396
+
397
+ ```jsx
398
+ <NavigationAction
399
+ navigation={item.link}
400
+ className="text-[15px] text-[#2A2520] hover:text-[#AE5E3D] transition-colors bg-transparent border-none cursor-pointer no-underline"
401
+ >
402
+ {item.label}
403
+ </NavigationAction>
404
+ ```
405
+
406
+ **Brand hex on a shadcn `<Button>` — the middle path.** To keep `<Button asChild>` (slot composition, focus ring, disabled handling) while guaranteeing an exact brand color, set the color as an inline style: `<Button asChild className="... shadow-none" style={{ backgroundColor: "#B4603D" }}>`. The precedence consequence: with an inline `backgroundColor`, **no `hover:bg-*` class can ever repaint it** (inline style outranks all classes, and ui-ux-guidelines' "hover handled by shadcn Button" no longer holds) — do hover feedback with `hover:opacity-90` / `hover:-translate-y-[1px]` instead. Alternative: a static `bg-[#HEX]` class also works (arbitrary values compile, and shadcn's `cn()`/tailwind-merge resolves it against the variant's `bg-primary`), in which case `hover:bg-*` stays available.
407
+
379
408
  **Any public npm package** auto-installs on import: `import { format } from "date-fns";`
380
409
 
381
410
  ### React Hooks — Critical Import Rule
@@ -393,13 +422,18 @@ useState(null); // without import = ReferenceError
393
422
 
394
423
  ## Editable Settings
395
424
 
396
- Hooks from `@/lib/editable-settings` let Softr builders customize blocks. Each `name` must be unique.
425
+ Hooks from `@/lib/editable-settings` let Softr builders customize blocks via **Content → Settings**. Each `name` must be unique — and `name` is the **persistence key**: renaming it in a later edit silently resets the builder's saved value to `initialValue`. Compact signatures below; the deep-dive (granularity doctrine, naming conventions, undocumented hooks/types, empty-media gating) is [references/editable-settings.md](references/editable-settings.md).
397
426
 
398
427
  ```jsx
399
428
  import { useTextSetting } from "@/lib/editable-settings";
400
429
  var title = useTextSetting({ name: "title", label: "Title", initialValue: "Welcome", required: false });
401
430
  // Returns: string
402
431
 
432
+ import { useLongTextSetting } from "@/lib/editable-settings";
433
+ var description = useLongTextSetting({ name: "description", label: "Description", initialValue: "..." });
434
+ // Returns: string — multi-line textarea in the Settings pane. UNDOCUMENTED officially but verified
435
+ // working 2026-08-31 (Studio-AI output). Render with `whitespace-pre-line` or builder line breaks collapse.
436
+
403
437
  import { useImageSetting } from "@/lib/editable-settings";
404
438
  var image = useImageSetting({ name: "hero", label: "Hero Image", initialValue: { src: "https://...", alt: "Hero" } });
405
439
  // Returns: { src: string, alt: string }
@@ -417,6 +451,7 @@ import { useNavigationSetting } from "@/lib/editable-settings";
417
451
  var nav = useNavigationSetting({ name: "cta", label: "CTA", initialValue: { action: "OPEN_PAGE", destination: "/pricing", openIn: "SELF" } });
418
452
  // Returns: { action, destination, openIn }
419
453
  // `openIn` MUST be one of: "SELF" (same tab), "TAB" (new tab), or "MODAL". Any other value (e.g. "SAME_TAB", "NEW_TAB") fails Softr's setting validator with: "useNavigationSetting(): The 'initialValue.openIn' property in the 'navigation' setting must be \"SELF\", \"TAB\", or \"MODAL\" if provided".
454
+ // The validator ACCEPTS an initialValue with no `action` key (Studio AI emits { destination, openIn } alone — verified 2026-08-31). Keep emitting an explicit `action` when generating, but don't flag its absence as a defect when reviewing existing blocks.
420
455
 
421
456
  import { useBooleanSetting } from "@/lib/editable-settings";
422
457
  var show = useBooleanSetting({ name: "toggle", label: "Show header", initialValue: false });
@@ -432,13 +467,19 @@ var features = useArraySetting({
432
467
  },
433
468
  initialValue: [{ title: "Fast", description: "Blazing fast.", icon: { icon: "zap" } }],
434
469
  });
435
- // Schema types: "text", "image", "video", "vibeCodingBlockIcon"
470
+ // Schema types: "text", "image", "video", "vibeCodingBlockIcon" — plus UNDOCUMENTED-but-working
471
+ // "navigation" (verified 2026-08-31 via Studio-AI output; renders per-item link pickers, item values
472
+ // carry the useNavigationSetting shape → pass to <NavigationAction navigation={item.link}>).
473
+ // Give navigation-typed schema entries a per-field initialValue, or guard the render — builder-added
474
+ // rows otherwise start with that field undefined.
436
475
  // No nested arrays. Don't put vibeCodingBlockIcon as first field.
476
+ // Key rendered rows by INDEX (key={index}), never by an editable field — builder-added rows all start
477
+ // at the schema default, so value keys duplicate immediately. See references/editable-settings.md.
437
478
  ```
438
479
 
439
480
  ## Hard Constraints
440
481
 
441
- Non-negotiable rules enforced by the Softr platform:
482
+ Non-negotiable rules. Most are enforced by the Softr platform (compiler, validator, or runtime); the ones marked **[house]** are conventions this skill enforces on itself — the platform accepts violations, but the rule exists for consistency or safety:
442
483
 
443
484
  1. **Browser-only** — No server-side code, no Node.js APIs.
444
485
  2. **Static field mappings** — `q.select()` keys and values must be string literals.
@@ -448,7 +489,7 @@ Non-negotiable rules enforced by the Softr platform:
448
489
  6. **Array setting icon placement** — Never put `vibeCodingBlockIcon` as first field.
449
490
  7. **No nested arrays in settings** — Use text with separator, split in code.
450
491
  8. **Default export required** — `export default function Block()`.
451
- 9. **Container wrapping** — Always wrap in `<div className="container py-0"><div className="content">`. Vertical padding lives on the inner wrapper and depends on block placement (see "Block Placement & Page Spacing").
492
+ 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.
452
493
  10. **Inline options literals for data hooks** — `useRecords` fails to compile when its options
453
494
  object is passed through a variable or wrapper function. Build the options object inline at the
454
495
  call site; share `q.select` mappings between hooks, not whole options objects. (Same
@@ -464,10 +505,10 @@ Non-negotiable rules enforced by the Softr platform:
464
505
  14. **React functional components only** — No class components.
465
506
  15. **Do NOT `import React from 'react'`** — Use named imports for hooks.
466
507
  16. **No CSS modules or styled-components** — Tailwind only.
467
- 17. **setTimeout for scroll** -- Wrap programmatic scroll in `setTimeout(fn, 0)`.
508
+ 17. **setTimeout for scroll [house]** -- Wrap **programmatic** scroll commands (`scrollIntoView`, `scrollTo`) in `setTimeout(fn, 0)`. This is about ISSUING scrolls only — LISTENING to window scroll (`window.addEventListener("scroll", ...)`) needs no wrapper and works normally from block code; see [references/common-patterns.md](references/common-patterns.md#scroll-condensing-fixed-header-landing-page-hero).
468
509
  18. **Never call `fetchNextPage` in the render body** -- render calls `fetchNextPage`, which updates data, which triggers re-render, which calls it again: infinite loop. Call it from an event handler (the official Load More pattern: `<button onClick={() => fetchNextPage()} disabled={isFetching}>`) or from a guarded `useEffect` when intentionally auto-loading all pages.
469
510
  19. **All hooks before any conditional `return`** -- Hooks must be called in the same order every render. A hook declared after a conditional `return` causes React error #310.
470
- 20. **Relative paths in navigation** -- Use `/page-name?recordId=...`, never hardcoded domains like `app.client.com/page`.
511
+ 20. **Relative paths in navigation [house]** -- Use `/page-name?recordId=...`, never hardcoded domains like `app.client.com/page`. The platform accepts absolute self-domain URLs as navigation setting values (a Studio-generated hero shipped its CTA configured as `https://<app>.softr.app/#section` — observed in the Settings pane, 2026-08-31), but they break on custom-domain publish — rewrite them relative, including hash anchors (`/#section`).
471
512
  21. **Recompiles reset Action permissions** -- Every code recompile/redeploy resets the block's
472
513
  auto-registered Actions to **default permissions**. Do the Actions-tab permission tightening pass
473
514
  only after the LAST redeploy, and re-check it after any future one. Verified live 2026-08-25
@@ -481,6 +522,8 @@ History, kept so old guidance elsewhere is recognizable as superseded: until mid
481
522
 
482
523
  Still house conventions: the field-value helper is named `getFieldValue()`, with property priority `label` -> `name` -> `title`.
483
524
 
525
+ **Platform truth sources.** Blocks emitted by Softr's own Studio AI are known-good evidence of what the current platform accepts — they compile and render live, and they surface undocumented hooks and setting types before the official docs catch up (this is how `useLongTextSetting` and the `navigation` array-schema type were discovered, 2026-08-31). Mine them for capabilities, but **never copy them verbatim**: fix React keys on settings-array loops (index, never a builder-editable label), consolidate near-duplicate hardcoded hexes into brand/DESIGN.md tokens, add the missing mobile nav / media-setting guards, and normalize formatting. Functional patterns in Studio output are platform-support evidence; its formatting and code hygiene are not patterns to imitate. (This is distinct from the no-churn rule above — cleaning up a block you're ADOPTING into the repo as source of truth is required; modernizing a deployed working block's syntax is still churn.) Adoption checklist: [references/softr-mcp.md](references/softr-mcp.md#adopting-studio-ai-generated-code). Note the inverse rule stays too: Studio's chat **prose** fabricates facts (datasource UUIDs) — trust its code, not its answers.
526
+
484
527
  ## Anti-Patterns Checklist
485
528
 
486
529
  Before delivering any block, run through [references/anti-patterns.md](references/anti-patterns.md) — a categorized catalog of every violation observed in production (data access, mutations, hooks, layout, helper blocks).
@@ -291,6 +291,43 @@ parentAccount: "RECORD_ID_1"
291
291
  string-array shape is the verified current form on Softr Database; if a linked-record write
292
292
  fails on an Airtable-backed block, try the `[{ id }]` object shape before deeper debugging.
293
293
 
294
+ ### Linked-record write traps (verified live 2026-08-26)
295
+
296
+ Four Softr Database behaviors proven by direct experiment on a live production build — a
297
+ 49-link backfill across a self-referencing parent/child pair. They bite hardest in backfills
298
+ and schema work, and **none of them raises an error**.
299
+
300
+ 1. **A single-valued link pair is enforced ON WRITE — and it clobbers silently.** With
301
+ `allowMultipleEntries: false` on the inverse side, writing child N's link to a parent
302
+ silently unlinks child N−1: no error, the earlier link just vanishes (16 of 49 backfill
303
+ links disappeared this way before it was caught). Pre-existing multi links DO survive a
304
+ flip from multi to single, which is why a scratch-table spike that flips an
305
+ already-linked pair reports the setting as "cosmetic only" and misleads — the
306
+ enforcement only fires on the next write. If one side must hold many links, both sides
307
+ must allow multiple entries; enforce any one-parent rule in the UI, not the schema.
308
+
309
+ 2. **`allowMultipleEntries` is a TOP-LEVEL field property, not part of `options`.** The
310
+ workspace MCP's `update_field` silently ignores it when nested inside `options` — the
311
+ call succeeds and changes nothing. A Tables API `PUT /fields/{id}` with the property at
312
+ top level works.
313
+
314
+ 3. **A Tables API field PUT that omits `options.inverseLinkFieldId` SEVERS the inverse
315
+ pairing** — it comes back `null` and the two sides stop mirroring each other. Always
316
+ echo `inverseLinkFieldId` inside the `options` you send, and re-read BOTH sides after
317
+ any linked-record field PUT to confirm the pairing survived.
318
+
319
+ 4. **An empty multi-entry link reads back as `[]`, which is truthy in JS.** Any
320
+ "already linked — skip" guard written as `if (value)` matches every record after a
321
+ single→multi flip, so a resumable backfill re-processes nothing or skips everything.
322
+ Test presence explicitly:
323
+
324
+ ```jsx
325
+ const hasLink = Array.isArray(v) ? v.length > 0 : !!v;
326
+ ```
327
+
328
+ (The populated read shapes are in [fields.md](fields.md); this is the empty case, and
329
+ it becomes a write trap the moment a backfill uses it as a skip condition.)
330
+
294
331
  ## Writing to Field Types
295
332
 
296
333
  Different Softr field types accept different value shapes in mutation payloads. The shape returned when you READ a field is often different from the shape you must SEND when you WRITE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "softr-vibe-coding",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
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"
@@ -72,8 +72,9 @@ Run through this catalog before delivering any block. Every row is a violation o
72
72
  | Relying on the page background on a **dark** brand, and shipping a block whose own canvas is unpainted | The row above assumes a light app whose page bg is already correct. On a dark brand it inverts: `custom-code-header.html`'s `body { background-color: #000 !important }` does NOT cross the shadow-DOM boundary, and a default Softr page is white — 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 Softr page background to the same value — two identical pure blacks composite with no seam, so the double-paint concern above doesn't bite. Painting it in the block also keeps it correct if the custom-code snippet is ever removed. |
73
73
  | Setting only `html, body { background }` in `custom-code-header.html` and expecting the app to change colour | Softr paints the same page fill on **four stacked layers**: `html`, `body`, `#page-content`, and a **class-less wrapper div** nested inside `#page-content`. Styling one gets covered by the ones above it, so `body` alone appears to do nothing. Paint the backdrop on `html`, then clear the duplicates: `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). |
74
74
  | `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. |
75
+ | 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) |
75
76
  | 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). |
76
- | 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. See [native-chrome-styling.md](native-chrome-styling.md). |
77
+ | 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). |
77
78
  | 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). |
78
79
  | Setting the page background on `body` (or any single element) — it appears to do nothing | Softr paints the SAME page fill on `html`, `body`, `#page-content`, AND a deeper class-less wrapper div, stacked — so styling one gets covered. 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). |
79
80
 
@@ -88,6 +89,11 @@ Run through this catalog before delivering any block. Every row is a violation o
88
89
  | Anti-Pattern | Correct Approach |
89
90
  |---|---|
90
91
  | `useNavigationSetting` with `openIn: "SAME_TAB"` / `"NEW_TAB"` / any other string | `openIn` must be exactly `"SELF"`, `"TAB"`, or `"MODAL"` -- Softr's setting validator rejects unknown values at save time with: *"The 'initialValue.openIn' property in the 'navigation' setting must be \"SELF\", \"TAB\", or \"MODAL\" if provided"* |
92
+ | Flagging a navigation initialValue with no `action` key as a defect during review | The validator ACCEPTS `{ destination, openIn }` alone — Studio AI emits this shape and it saves/renders fine (verified 2026-08-31). Keep emitting an explicit `action` when GENERATING (deterministic; click-time default is unverified inference), but don't fail reviews over its absence. See [editable-settings.md](editable-settings.md#usenavigationsetting) |
93
+ | `key={item.label}` (or any builder-editable field) when mapping `useArraySetting` items | `key={index}` (matching the official docs' own example). Array-item fields are builder-editable and NOT unique — the schema gives every newly added row the same default value, so two clicks of Add Item = duplicate keys = broken reorder/update rendering in the Settings live preview. Index keys are safe here: setting rows are stateless display rows fully re-rendered on every settings change. Studio's own AI gets this wrong — don't copy it |
94
+ | A `{ type: "navigation" }` array-schema entry with no per-field `initialValue`, rendered unguarded | Builder-added rows start with that field `undefined` → `<NavigationAction navigation={undefined}>` (behavior unverified; dead element at best). Seed a per-field `initialValue` in the schema, or guard: `{item.link && <NavigationAction ...>}`. See [editable-settings.md](editable-settings.md#usearraysetting) |
95
+ | Unconditional `<img src={setting.src}>` against a media setting that may start empty (`initialValue: { src: "" }`) | Gate it (`{setting.src && <img .../>}`) or render a same-size placeholder — an empty-string src triggers React's re-download warning and shows a broken/empty band at the slot's fixed height until the builder uploads. The empty state is platform-normal (the official docs' own array example seeds `{ src: "", alt: "" }`), so this is the default path, not an edge case |
96
+ | Rendering a `useLongTextSetting` value in a plain `<p>` | Add `whitespace-pre-line` (or split on `\n`) — the Settings pane gives builders a multi-line textarea, and HTML collapses their line breaks to spaces otherwise. See [editable-settings.md](editable-settings.md#text-hooks-usetextsetting-and-uselongtextsetting) |
91
97
 
92
98
  ## Helper Blocks
93
99
 
@@ -1,12 +1,19 @@
1
1
  # Common Patterns
2
2
 
3
- Small reusable patterns that come up across Vibe Coding blocks but don't warrant their own reference file. Each is a copy-pasteable snippet. The snippets below use legacy var-style (`var`, `function() {}`), which remains valid write new blocks in modern TS (see SKILL.md Style Conventions).
3
+ Small reusable patterns that come up across Vibe Coding blocks but don't warrant their own reference file. Each is a copy-pasteable snippet. The first three snippets use legacy var-style (`var`, `function() {}`), which remains valid; the newer patterns use modern TS — both compile (see SKILL.md Style Conventions).
4
+
5
+ **Browser environment note.** Vibe blocks render in a shadow root in the MAIN document — not an iframe — so window-level APIs behave normally from block code: `window.scrollY` reflects the real page scroll, window-level events (scroll, resize, keydown) fire, `localStorage`/`navigator.clipboard`/`window.history` all work. Standard `useEffect` add/remove-listener with cleanup is the right shape for event-driven UI; use `{ passive: true }` for scroll/touch listeners. (SKILL.md Hard Constraint 17's `setTimeout` rule applies to ISSUING programmatic scrolls only, not to listening.)
4
6
 
5
7
  ## Table of Contents
6
8
 
7
9
  - [Cross-Page State with localStorage + URL Parameters](#cross-page-state-with-localstorage--url-parameters)
8
10
  - [Clipboard Copy Button](#clipboard-copy-button)
9
11
  - [Navigation Blocker for Unsaved Changes](#navigation-blocker-for-unsaved-changes)
12
+ - [Scroll-Condensing Fixed Header (Landing-Page Hero)](#scroll-condensing-fixed-header-landing-page-hero)
13
+ - [Auth-Aware Header CTA](#auth-aware-header-cta)
14
+ - [Edge-Fade Image Mask (Editorial Hero)](#edge-fade-image-mask-editorial-hero)
15
+ - [Decorative Background Blobs (Editorial Layering)](#decorative-background-blobs-editorial-layering)
16
+ - [Dot-Separated Inline List](#dot-separated-inline-list)
10
17
 
11
18
  ## Cross-Page State with localStorage + URL Parameters
12
19
 
@@ -161,3 +168,127 @@ The hook automatically handles:
161
168
  - Blocks where you want to block on something other than form dirtiness (e.g., a pending background upload).
162
169
 
163
170
  **Asking Softr to add the blocker automatically:** when generating or refining a form block in the Vibe Coding editor, you can prompt with "Block the navigation when the form is dirty" and Softr will wire `useNavigationBlocker` for you — useful when you don't want to write the import + hook call yourself.
171
+
172
+ ## Scroll-Condensing Fixed Header (Landing-Page Hero)
173
+
174
+ For block-owned landing headers (see [static-blocks.md](static-blocks.md#block-owned-landing-page-header) for when this pattern applies and its caveat set): the header starts tall and transparent, then condenses to a translucent, blurred bar once the page scrolls. Verified pattern from Studio-AI output, 2026-08-31 (renders live; scroll behavior consistent with window-scrolled Softr pages).
175
+
176
+ ```tsx
177
+ import { useState, useEffect } from "react";
178
+
179
+ export default function Block() {
180
+ const [scrolled, setScrolled] = useState(false);
181
+
182
+ useEffect(() => {
183
+ const onScroll = () => setScrolled(window.scrollY > 24);
184
+ onScroll(); // sync immediately — Softr is a SPA, so the block can mount with a restored scroll offset
185
+ window.addEventListener("scroll", onScroll, { passive: true });
186
+ return () => window.removeEventListener("scroll", onScroll);
187
+ }, []);
188
+
189
+ return (
190
+ <header
191
+ className={`fixed top-0 left-0 right-0 z-50 flex items-center justify-between px-6 md:px-12 transition-all duration-300 ${
192
+ scrolled
193
+ ? "py-3 bg-[#FAF5EC]/85 backdrop-blur-md border-b border-[#E7DECD]"
194
+ : "py-6 bg-transparent border-b border-transparent"
195
+ }`}
196
+ >
197
+ {/* logo / nav / CTA */}
198
+ </header>
199
+ );
200
+ }
201
+ ```
202
+
203
+ Notes:
204
+
205
+ - **The initial `onScroll()` call matters** — without it, a block mounting mid-page (SPA back-navigation with restored scroll) renders the transparent state over content.
206
+ - **No throttle/rAF needed** — the handler sets a boolean; React skips re-renders when the value doesn't change.
207
+ - **`backdrop-blur-md` works across the shadow-DOM boundary**: `backdrop-filter` operates on the composited backdrop (everything painted beneath the element in the viewport), so a block's translucent fixed header blurs other blocks' content scrolling under it. Requirements: the element needs a semi-transparent background for the blur to be visible, and `backdrop-filter` must sit on the fixed element itself — on an ancestor it creates a containing block that would re-anchor the fixed header. (Compositing claim is standard CSS; the blurred-over-content visual on a published Softr page is inferred, not screenshot-proven.)
208
+ - This translucent-blur bar is a deliberate, single-surface exception to the anti-glassmorphism taste rule in ui-ux-guidelines.md — don't extend the treatment to cards/panels.
209
+ - **Fixed-position fragility**: `position: fixed` anchors to the viewport only while no ancestor has a `transform`/`filter`/`perspective`/`will-change`. Keep those off the block root and the header's ancestors, and verify in the published app, not just the Studio canvas.
210
+
211
+ ## Auth-Aware Header CTA
212
+
213
+ A landing header's "Sign in" button should swap for a logged-in destination. `useCurrentUser()` returns `null` when logged out (documented in [../datasources/reading.md](../datasources/reading.md)); verify whether it has a transient loading state before adding flicker handling — the docs only document `null`.
214
+
215
+ ```tsx
216
+ import { useCurrentUser } from "@/lib/user";
217
+ import { NavigationAction } from "@/components/navigation-action";
218
+ import { Button } from "@/components/ui/button";
219
+
220
+ // signInLink, dashboardLink: two useNavigationSetting hooks so both destinations stay builder-editable
221
+ const user = useCurrentUser();
222
+
223
+ <Button asChild>
224
+ {user ? (
225
+ <NavigationAction navigation={dashboardLink}>Dashboard</NavigationAction>
226
+ ) : (
227
+ <NavigationAction navigation={signInLink}>Sign in</NavigationAction>
228
+ )}
229
+ </Button>
230
+ ```
231
+
232
+ Default label is "Sign in" (the ui-ux-guidelines.md glossary term), not "Log in". This is the default for any block-owned landing header with a login button — Studio AI omits the swap; add it.
233
+
234
+ ## Edge-Fade Image Mask (Editorial Hero)
235
+
236
+ Fade a photo's edges into the block background (no hard rectangle) with multiple gradient masks — one linear-gradient per edge to fade — combined by intersection. Renders fine inside the block's shadow DOM (verified from Studio output, 2026-08-31).
237
+
238
+ ```tsx
239
+ // Module scope. Left edge fades into the background; bottom edge fades so the photo doesn't butt the block edge.
240
+ const photoMask = {
241
+ WebkitMaskImage:
242
+ "linear-gradient(to right, rgba(0,0,0,0) 0%, rgba(0,0,0,0.35) 6%, rgba(0,0,0,1) 18%), linear-gradient(to bottom, rgba(0,0,0,1) 84%, rgba(0,0,0,0) 100%)",
243
+ WebkitMaskComposite: "source-in",
244
+ maskImage:
245
+ "linear-gradient(to right, rgba(0,0,0,0) 0%, rgba(0,0,0,0.35) 6%, rgba(0,0,0,1) 18%), linear-gradient(to bottom, rgba(0,0,0,1) 84%, rgba(0,0,0,0) 100%)",
246
+ maskComposite: "intersect",
247
+ };
248
+
249
+ <div className="absolute top-0 right-0 h-[92%] w-[58%]" style={photoMask}>
250
+ <img src={image.src} alt={image.alt} className="w-full h-full object-cover object-[62%_25%]" />
251
+ </div>
252
+ ```
253
+
254
+ **The trap: the two composite properties take DIFFERENT keyword vocabularies.** `-webkit-mask-composite` uses Porter-Duff names (`source-in`), standard `mask-composite` uses `intersect`. Setting only one property, or using the wrong vocabulary, silently loses the fade in one browser family — always set both, with each one's own keyword. Pairs naturally with `object-cover` + arbitrary `object-[x%_y%]` for the crop.
255
+
256
+ ## Decorative Background Blobs (Editorial Layering)
257
+
258
+ Large soft shapes behind hero content. Three load-bearing gotchas, then the recipe:
259
+
260
+ 1. **`overflow-hidden` on the block root** — negatively-offset off-canvas shapes otherwise create horizontal scroll (this operationalizes ui-ux-guidelines.md §21's no-horizontal-overflow rule).
261
+ 2. **`pointer-events-none` on every decorative layer** — so they never intercept clicks on content.
262
+ 3. **vw sizing paired with px max-caps** — shapes scale with the viewport but don't balloon on ultrawide.
263
+
264
+ ```tsx
265
+ <div className="relative overflow-hidden ...">
266
+ {/* decoration: z-0 */}
267
+ <div className="pointer-events-none absolute -top-[22%] -right-[10%] w-[62vw] h-[62vw] max-w-[900px] max-h-[900px] rounded-full bg-[#AE5E3D] z-0" />
268
+ {/* art layer (e.g. masked photo): z-[1] */}
269
+ {/* content: z-10 */}
270
+ <main className="relative z-10 ...">...</main>
271
+ </div>
272
+ ```
273
+
274
+ The z-0 / z-[1] / z-10 stack is block-internal layering — it complements (does not replace) the overlay z-scale in ui-ux-guidelines.md §7.
275
+
276
+ ## Dot-Separated Inline List
277
+
278
+ Certifications, feature tags, meta rows: `GMP Manufacturing ● ISO 22716 ● Low MOQs`. Render separators LEADING (never trailing), keep the two gap values identical, and hide the glyphs from screen readers:
279
+
280
+ ```tsx
281
+ <div className="flex flex-wrap items-center gap-x-8 gap-y-3">
282
+ {items.map((item, index) => (
283
+ <span key={index} className="flex items-center gap-x-8">
284
+ {index > 0 && <span aria-hidden="true" className="text-[7px] text-muted-foreground leading-none">●</span>}
285
+ <span>{item.label}</span>
286
+ </span>
287
+ ))}
288
+ </div>
289
+ ```
290
+
291
+ - **Two gap declarations, deliberately equal** — the container's `gap-x-8` spaces item→item, the item span's `gap-x-8` spaces dot→label; symmetry depends on the two values matching, so keep them identical (hoist to a shared constant if you touch them often). The real fix over Studio AI's emitted shape is the **leading**-separator guard (`index > 0`): Studio puts a trailing dot inside each item, which dangles alone at the end of a wrapped line, and its two gap values match only by accident.
292
+ - **Keep `gap-y-*`** on the container for multi-line rhythm when the list wraps.
293
+ - **If the list is expected to wrap often**, drop the dots and let the gap carry the rhythm — any inline separator looks orphaned at a line break.
294
+ - `aria-hidden="true"` on the glyph — screen readers announce `●` as "black circle" otherwise.
@@ -0,0 +1,238 @@
1
+ # Editable Settings — Hook Catalog, Granularity Doctrine, Undocumented Hooks & Types
2
+
3
+ Editable settings are the hooks from `@/lib/editable-settings` that surface a block's content in Studio's **Content → Settings** pane, so builders (and clients) edit text, images, links, and lists **without re-prompting or touching code**. Softr's own pitch: "make simple text and image edits directly without re-prompting." This file is the deep-dive; SKILL.md keeps the compact signatures.
4
+
5
+ **Provenance discipline.** Every behavior below is tagged either **[official]** (in Softr's Vibe Coding Developer Guide, re-fetchable via the MCP's `get_vibe_coding_docs`) or **[verified-undocumented]** (absent from the official guide but proven working — source and date given). Keep the tags when editing this file: they're what stops a future docs-based review from false-positiving working code (the same failure class as the useRecord-via-Studio-binding incident), and what tells you which behaviors could silently change under you since Softr never promised them.
6
+
7
+ ## Contents
8
+
9
+ - [Hook catalog](#hook-catalog)
10
+ - [Text hooks: useTextSetting and useLongTextSetting](#text-hooks-usetextsetting-and-uselongtextsetting)
11
+ - [Media hooks: useImageSetting and useVideoSetting](#media-hooks-useimagesetting-and-usevideosetting)
12
+ - [useVibeCodingBlockIconSetting](#usevibecodingblockiconsetting)
13
+ - [useNavigationSetting](#usenavigationsetting)
14
+ - [useBooleanSetting](#usebooleansetting)
15
+ - [useArraySetting](#usearraysetting)
16
+ - [Granularity doctrine: settings-first static blocks](#granularity-doctrine-settings-first-static-blocks)
17
+ - [Naming conventions and the rename-resets-value gotcha](#naming-conventions-and-the-rename-resets-value-gotcha)
18
+ - [Constraints recap](#constraints-recap)
19
+
20
+ ## Hook catalog
21
+
22
+ | Hook | Returns | Status |
23
+ |---|---|---|
24
+ | `useTextSetting` | `string` | [official] |
25
+ | `useLongTextSetting` | `string` (multi-line textarea in the pane) | [verified-undocumented] Studio-AI output, 2026-08-31 |
26
+ | `useImageSetting` | `{ src, alt }` | [official] |
27
+ | `useVideoSetting` | `{ src }` | [official] |
28
+ | `useVibeCodingBlockIconSetting` | `{ icon }` (Lucide name) | [official] |
29
+ | `useNavigationSetting` | navigation object (see below) | [official] |
30
+ | `useBooleanSetting` | `boolean` | [official] |
31
+ | `useArraySetting` | array of schema-shaped items | [official] — plus one [verified-undocumented] schema type |
32
+
33
+ All hooks share the base options `{ name, label, initialValue }`; `useTextSetting` also takes `required` (optional, default false). `name` must be unique per block (Hard Constraint 5) and is the persistence key (see [the rename gotcha](#naming-conventions-and-the-rename-resets-value-gotcha)).
34
+
35
+ ## Text hooks: useTextSetting and useLongTextSetting
36
+
37
+ ```tsx
38
+ import { useTextSetting, useLongTextSetting } from "@/lib/editable-settings";
39
+
40
+ const title = useTextSetting({
41
+ name: "title",
42
+ label: "Title",
43
+ initialValue: "Welcome",
44
+ required: false, // optional, default false
45
+ });
46
+ // Returns: string — single-line input in the Settings pane
47
+
48
+ const description = useLongTextSetting({
49
+ name: "description",
50
+ label: "Description",
51
+ initialValue: "Formulation, manufacturing and packaging under one roof.",
52
+ });
53
+ // Returns: string — multi-line textarea with an expand control in the Settings pane
54
+ ```
55
+
56
+ **Rule of thumb:** `useTextSetting` for single-line strings (labels, headings, CTA text, URLs); `useLongTextSetting` for paragraph-length copy (descriptions, testimonial bodies, bios).
57
+
58
+ **[verified-undocumented]** `useLongTextSetting` is absent from the official developer guide (checked 2026-08-31) but is emitted by Softr's own Studio AI and renders a working multi-line textarea in the Settings pane (screenshot-verified on a live Studio block). Same options shape as `useTextSetting`; return type `string` is inferred from usage, not a published signature.
59
+
60
+ **Newline gotcha — the one thing that makes it different in practice.** The pane's textarea lets builders enter line breaks, but HTML collapses `\n` to spaces. Render the value with `whitespace-pre-line` (or split on `\n` yourself) or the builder's paragraph breaks silently vanish:
61
+
62
+ ```tsx
63
+ <p className="whitespace-pre-line text-muted-foreground">{description}</p>
64
+ ```
65
+
66
+ Studio AI itself gets this wrong (renders the value in a plain `<p>`), so fix it when adopting Studio-generated code.
67
+
68
+ ## Media hooks: useImageSetting and useVideoSetting
69
+
70
+ ```tsx
71
+ import { useImageSetting, useVideoSetting } from "@/lib/editable-settings";
72
+
73
+ const image = useImageSetting({
74
+ name: "hero-image",
75
+ label: "Hero image",
76
+ initialValue: { src: "https://...", alt: "A hero image" },
77
+ });
78
+ // Returns: { src: string, alt: string }
79
+
80
+ const video = useVideoSetting({
81
+ name: "intro-video",
82
+ label: "Intro video",
83
+ initialValue: { src: "https://example.com/video.mp4" },
84
+ });
85
+ // Returns: { src: string }
86
+ ```
87
+
88
+ **Empty-src gating.** Blocks routinely ship with `initialValue: { src: "" }` so the builder uploads the real asset in the pane — the official docs' own array example seeds `image: { src: "", alt: "" }`, so the empty state is platform-normal. Never render an unconditional `<img src={image.src}>` against a possibly-empty setting: an empty-string `src` triggers React's re-download warning and shows a broken/empty band at whatever fixed height you gave it. Gate it, or render a same-size placeholder so the layout holds pre-upload:
89
+
90
+ ```tsx
91
+ {image.src ? (
92
+ <img src={image.src} alt={image.alt} className="w-full h-[340px] object-cover" />
93
+ ) : (
94
+ <div className="w-full h-[340px] bg-muted" aria-hidden="true" />
95
+ )}
96
+ ```
97
+
98
+ **One setting, several renders.** A settings hook returns a plain value, so one `useImageSetting` may safely feed two sibling `<img>` elements with opposite visibility classes (desktop art-directed crop + mobile full-bleed). The builder still edits ONE image in the pane. See ui-ux-guidelines.md §21 (art-directed responsive images).
99
+
100
+ ## useVibeCodingBlockIconSetting
101
+
102
+ ```tsx
103
+ import { useVibeCodingBlockIconSetting } from "@/lib/editable-settings";
104
+ import { DynamicIcon } from "@/components/dynamic-icon";
105
+
106
+ const { icon } = useVibeCodingBlockIconSetting({
107
+ name: "feature-icon",
108
+ label: "Feature icon",
109
+ initialValue: { icon: "trending-up" }, // kebab-case lucide-react name
110
+ });
111
+
112
+ <DynamicIcon name={icon} className="w-6 h-6" />
113
+ ```
114
+
115
+ Returns `{ icon: string }`. Always render through `<DynamicIcon>` — never a manual lookup table.
116
+
117
+ ## useNavigationSetting
118
+
119
+ ```tsx
120
+ import { useNavigationSetting } from "@/lib/editable-settings";
121
+ import { NavigationAction } from "@/components/navigation-action";
122
+ import { Button } from "@/components/ui/button";
123
+
124
+ const cta = useNavigationSetting({
125
+ name: "primary-cta-link",
126
+ label: "Primary CTA link",
127
+ initialValue: { action: "OPEN_PAGE", destination: "/pricing", openIn: "SELF" },
128
+ });
129
+
130
+ <Button asChild>
131
+ <NavigationAction navigation={cta}>Request your quote</NavigationAction>
132
+ </Button>
133
+ ```
134
+
135
+ Value shapes per action type (full detail in SKILL.md's NavigationAction section; the official docs type `openIn` as `"SELF" | "TAB"` only — `"MODAL"` is validator-verified as a legal value per the error message quoted in [Constraints recap](#constraints-recap), and its restriction to `OPEN_PAGE` is inference from Studio behavior, not documented):
136
+
137
+ - `OPEN_PAGE` — `destination` (page path) + `openIn` (`"SELF"` | `"TAB"` | `"MODAL"`)
138
+ - `OPEN_URL` — `destination` (URL) + `openIn` (`"SELF"` | `"TAB"`)
139
+ - `OPEN_CHAT` — no destination (mind the data-source-context gotcha in [anti-patterns.md](anti-patterns.md))
140
+ - `TRIGGER_CUSTOM_WORKFLOW` — no destination; builder picks the workflow in Studio
141
+
142
+ **[verified-undocumented] `action` is accepted as optional.** Softr's setting validator accepts an initialValue with no `action` key — `{ destination: "/", openIn: "SELF" }` alone — and Studio's own AI emits exactly that shape (verified 2026-08-31: a Studio-generated block with three action-less `useNavigationSetting` initialValues, plus three more action-less link values inside an array setting, saved and rendered its Settings pane). Corroborating in-repo evidence that not every key is mandatory: the validator's own error message for `openIn` ends in *"if provided"* (see [anti-patterns.md](anti-patterns.md#editable-settings)). Two consequences:
143
+
144
+ 1. **When generating, keep emitting an explicit `action`** — deterministic, self-documenting, and click-time resolution of action-less values is unverified (presumably defaults to `OPEN_PAGE` for in-app paths, but that's inference).
145
+ 2. **When reviewing Studio-generated or existing blocks, do NOT flag a missing `action` as a defect.** It saves and renders fine.
146
+
147
+ ## useBooleanSetting
148
+
149
+ ```tsx
150
+ import { useBooleanSetting } from "@/lib/editable-settings";
151
+
152
+ const showHeader = useBooleanSetting({
153
+ name: "toggle-header",
154
+ label: "Toggle header",
155
+ initialValue: false, // official docs note the default is true when omitted
156
+ });
157
+ // Returns: boolean
158
+ ```
159
+
160
+ Use for show/hide sections, layout variants, feature toggles.
161
+
162
+ ## useArraySetting
163
+
164
+ ```tsx
165
+ import { useArraySetting } from "@/lib/editable-settings";
166
+
167
+ const navItems = useArraySetting({
168
+ name: "nav-items",
169
+ label: "Navigation items",
170
+ schema: {
171
+ label: { type: "text", label: "Label", initialValue: "Item" },
172
+ link: { type: "navigation", label: "Link", initialValue: { action: "OPEN_PAGE", destination: "/", openIn: "SELF" } },
173
+ },
174
+ initialValue: [
175
+ { label: "Capabilities", link: { action: "OPEN_PAGE", destination: "/#capabilities", openIn: "SELF" } },
176
+ { label: "Process", link: { action: "OPEN_PAGE", destination: "/#process", openIn: "SELF" } },
177
+ ],
178
+ });
179
+ ```
180
+
181
+ Returns an array of schema-shaped items, rendered in the Settings pane as **reorderable rows** with per-field editors and an "Add item" control. Use for nav menus, feature lists, testimonials, badges, footer link columns, FAQs.
182
+
183
+ ### Schema field types
184
+
185
+ `"text"`, `"image"`, `"video"`, `"vibeCodingBlockIcon"` **[official]** — plus:
186
+
187
+ **[verified-undocumented] `"navigation"`.** A `{ type: "navigation" }` schema entry renders a per-item link picker (page picker / URL / openIn) inside each row, and item values carry the `useNavigationSetting` value shape — pass them straight to `<NavigationAction navigation={item.link}>`. Verified 2026-08-31 via a Studio-AI-generated block whose Settings pane showed nav items as reorderable rows with rendered per-item link pickers. This unlocks fully builder-editable nav menus, footer link lists, and CTA collections; without it you'd be stuck hardcoding links or spawning N separate `useNavigationSetting` hooks. (Click-through of array-item links follows from the value shape being identical to top-level navigation settings consumed by the same component, but wasn't independently click-tested.)
188
+
189
+ ### Initial values: two layers
190
+
191
+ - **Top-level `initialValue` array** seeds the rows the block starts with.
192
+ - **Per-field `initialValue` inside the schema** is the default a field gets when the builder clicks **Add item**. It's optional [official — the docs' own example omits it on some fields], BUT:
193
+
194
+ **Give `navigation`-typed schema entries a per-field `initialValue` (or guard the render).** A schema entry like `link: { type: "navigation", label: "Link" }` with no initialValue means every builder-added row starts with `link: undefined`, which flows straight into `<NavigationAction navigation={undefined}>` — behavior unverified (dead element at best). Either seed it in the schema (as in the example above) or guard: `{item.link && <NavigationAction navigation={item.link}>...</NavigationAction>}`.
195
+
196
+ ### Key array rows by index
197
+
198
+ ```tsx
199
+ {navItems.map((item, index) => (
200
+ <NavigationAction key={index} navigation={item.link}>{item.label}</NavigationAction>
201
+ ))}
202
+ ```
203
+
204
+ Never key by an editable field (`key={item.label}`). Builder-added rows all start at the schema default (`"Item"`, `"Certification"`, ...), so value keys duplicate **immediately** — broken reorder/update rendering in the pane's live preview is the default path, not an edge case. The official docs' own example keys by index; index keys are safe here because setting rows are stateless display rows fully re-rendered on every settings change. Studio's own AI gets this wrong (emits `key={item.label}`) — don't copy that part of Studio output.
205
+
206
+ ## Granularity doctrine: settings-first static blocks
207
+
208
+ **For static/marketing blocks (heroes, page headers, pricing tables, testimonial bands, footers, FAQ sections): every user-visible string, image, and link is a setting by default. Hardcoded copy is the exception and needs a reason.** This is Softr's own generator's revealed philosophy — a Studio-emitted hero ships with 15 settings hooks and zero hardcoded user-visible copy — and it matches the official guidance ("Always use them for any text, images, icons, or lists that might change between block instances"). The payoff is the platform pitch itself: clients edit copy in **Content → Settings** without re-prompting, and one block template re-skins across client apps.
209
+
210
+ The recurring patterns:
211
+
212
+ - **Heading-line split** — one `useTextSetting` per visual line, joined with `<br />`, names suffixed `-line-1` / `-line-2` / `-line-3`. Gives builders line-break control without markup in a text field:
213
+
214
+ ```tsx
215
+ <h1>
216
+ {headingLine1}<br />{headingLine2}<br />{headingLine3}
217
+ </h1>
218
+ ```
219
+
220
+ - **CTA pairing** — every call-to-action is TWO settings: `<x>-text` (`useTextSetting`) + `<x>-link` (`useNavigationSetting`). E.g. `primary-cta-text` / `primary-cta-link`, `login-button-text` / `login-button-link`.
221
+ - **Repeated groups** — any repeated UI group (nav items, badges, certifications, logos) is a `useArraySetting`, not N individual hooks.
222
+
223
+ For data-connected blocks the doctrine relaxes: record data comes from the datasource, and settings cover the frame around it (section headings, empty-state copy, CTA labels/links, toggles).
224
+
225
+ ## Naming conventions and the rename-resets-value gotcha
226
+
227
+ **Convention:** kebab-case names describing the content's role — `logo-text`, `tagline`, `heading-line-1`, `primary-cta-text`, `nav-items`. Matches Studio AI's own output. (These conventions are for **setting names** only — the window-global and CustomEvent naming schemes in [helper-blocks.md](helper-blocks.md) are a different namespace; don't conflate them.)
228
+
229
+ **Gotcha [official]:** the docs annotate `name` with *"unique identifier — changing this resets the value."* `name` is the persistence key: rename a setting in a later code edit and the builder's saved value silently resets to `initialValue`. **Treat setting names as stable API once a block is deployed** — a rename is silent builder-data loss, not a refactor.
230
+
231
+ ## Constraints recap
232
+
233
+ The platform-enforced rules (Hard Constraints 5–7 in SKILL.md):
234
+
235
+ - **Unique names** — no two setting hooks may share a `name`.
236
+ - **No nested arrays in schemas** — for list-like text inside an array item, use a `"text"` field with a separator and split in code.
237
+ - **`vibeCodingBlockIcon` never first** — don't put an icon field as the first field in an array schema.
238
+ - **`openIn`** must be exactly `"SELF"`, `"TAB"`, or `"MODAL"` (validator-enforced — see [anti-patterns.md](anti-patterns.md#editable-settings)).
@@ -1,6 +1,6 @@
1
1
  # Styling Softr's Native Shell (Header · Footer · Page Background) 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 them as a Vibe Coding block. To re-skin them, add **CSS to Settings → Custom Code → Code inside header** (the same place brand fonts/tokens live, i.e. the `custom-code-header.html` produced by `building-design-md`). 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.
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, i.e. the `custom-code-header.html` produced by `building-design-md`). 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
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.
6
6
 
@@ -212,8 +212,12 @@ body,
212
212
  })();
213
213
  ```
214
214
 
215
- ## Restyle vs. replace
215
+ ## Restyle vs. replace vs. block-owned header
216
216
 
217
217
  **Restyle the native bar (recommended):** robust, global, keeps Softr's auth-aware nav (account menu, user-group-gated items) and stays editable in Studio.
218
218
 
219
219
  **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.
220
+
221
+ **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).
222
+
223
+ 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.
@@ -15,8 +15,8 @@ import { datasource, useRecords, useRecord, useRecordCreate, useRecordUpdate, us
15
15
  // USER
16
16
  import { useCurrentUser } from "@/lib/user";
17
17
 
18
- // EDITABLE SETTINGS
19
- import { useTextSetting, useImageSetting, useVideoSetting, useArraySetting,
18
+ // EDITABLE SETTINGS (useLongTextSetting is undocumented officially but verified working 2026-08-31)
19
+ import { useTextSetting, useLongTextSetting, useImageSetting, useVideoSetting, useArraySetting,
20
20
  useVibeCodingBlockIconSetting, useNavigationSetting,
21
21
  useBooleanSetting } from "@/lib/editable-settings";
22
22
 
@@ -201,10 +201,19 @@ var result2 = useMetric({ select: select, metric: metric.count() });
201
201
  ## Editable Settings
202
202
 
203
203
  ```jsx
204
- var title = useTextSetting({ name: "title", label: "Title", initialValue: "Hello" });
205
- var show = useBooleanSetting({ name: "toggle", label: "Show header", initialValue: false });
204
+ var title = useTextSetting({ name: "title", label: "Title", initialValue: "Hello" }); // string
205
+ var body = useLongTextSetting({ name: "body", label: "Body", initialValue: "..." }); // string; multi-line textarea — render with whitespace-pre-line
206
+ var show = useBooleanSetting({ name: "toggle", label: "Show header", initialValue: false }); // boolean
207
+ var items = useArraySetting({ name: "nav-items", label: "Nav items",
208
+ schema: { label: { type: "text", label: "Label", initialValue: "Item" },
209
+ link: { type: "navigation", label: "Link", initialValue: { action: "OPEN_PAGE", destination: "/", openIn: "SELF" } } },
210
+ initialValue: [/* seed rows */] });
211
+ // Array schema types: "text" | "image" | "video" | "vibeCodingBlockIcon" | "navigation" (undocumented, verified 2026-08-31)
212
+ // Key rendered array rows by INDEX, never by an editable field. Renaming a setting's `name` resets its saved value.
206
213
  ```
207
214
 
215
+ Deep-dive (granularity doctrine, all hooks, gotchas): [editable-settings.md](editable-settings.md)
216
+
208
217
  ## Field Value Helper (getFieldValue)
209
218
 
210
219
  ```jsx
@@ -251,15 +260,17 @@ Catches BOTH Softr's in-app SPA navigation (nav bar, sidebar, `<NavigationAction
251
260
  export default function Block() {
252
261
  var result = useRecords({ select: select, count: 25 });
253
262
 
254
- if (result.status === "pending") return <div className="container py-6"><div className="content">Loading...</div></div>;
255
- if (result.status === "error") return <div className="container py-6"><div className="content">Error</div></div>;
263
+ if (result.status === "pending") return <div className="container py-0"><div className="content"><div className="py-3 px-8">Loading...</div></div></div>;
264
+ if (result.status === "error") return <div className="container py-0"><div className="content"><div className="py-3 px-8">Error</div></div></div>;
256
265
 
257
266
  var records = (result.data && result.data.pages) ? result.data.pages.flatMap(function(p) { return p.items; }) : [];
258
267
 
259
268
  return (
260
- <div className="container py-6">
269
+ <div className="container py-0">
261
270
  <div className="content">
262
- {/* UI */}
271
+ <div className="py-3 px-8">
272
+ {/* UI — inner wrapper owns vertical spacing; classes depend on placement (see SKILL.md Block Placement) */}
273
+ </div>
263
274
  </div>
264
275
  </div>
265
276
  );
@@ -12,6 +12,7 @@ The official Softr MCP server (`https://mcp.softr.io/mcp`) gives an AI assistant
12
12
  - [Connection and auth](#connection-and-auth)
13
13
  - [Permissions model](#permissions-model)
14
14
  - [Vibe coding block tools](#vibe-coding-block-tools)
15
+ - [Adopting Studio-AI-generated code](#adopting-studio-ai-generated-code)
15
16
  - [Vibe coding gotchas (official)](#vibe-coding-gotchas-official)
16
17
  - [Browsing integrations (external data sources)](#browsing-integrations-external-data-sources)
17
18
  - [Softr Database tools](#softr-database-tools)
@@ -76,6 +77,18 @@ Before writing any block code through the MCP, call `get_vibe_coding_docs` — i
76
77
 
77
78
  Editable settings via MCP are the same fields as the block's **Content → Settings** panel; sort and record filters are the same as the **Source** tab. Duplicating from a version is the safe way to try an alternative — the original keeps working while you experiment on the copy.
78
79
 
80
+ ## Adopting Studio-AI-generated code
81
+
82
+ When you pull a Studio-AI-generated block via `get_vibe_coding_block_code` to adopt into a project repo as source of truth: its output renders fine but ships with predictable defects. **Functional patterns in Studio output are platform-support evidence** (it surfaces undocumented capabilities before the docs do — see SKILL.md's "Platform truth sources"); **its code hygiene is not a pattern to imitate.** Cleanup pass before committing:
83
+
84
+ - **Run a formatter** — Studio output ships inconsistent indentation (observed: statements at column 0 inside a 4-space-indented component).
85
+ - **Hoist and consolidate brand hexes** into module-scope constants; flag near-duplicate hexes as probable unintended drift (observed: `#AE5E3D` vs `#B4603D` for one terracotta in a single block).
86
+ - **Fix React keys on settings-array loops** — Studio emits `key={item.label}`; use `key={index}` (see [anti-patterns.md](anti-patterns.md#editable-settings)).
87
+ - **Add the guards Studio omits** — conditional render for empty media settings, `whitespace-pre-line` on long-text settings, mobile nav for block-owned headers, `aria-hidden` on decorative glyphs.
88
+ - **Rewrite absolute self-domain URLs relative** (`https://<app>.softr.app/#x` → `/#x`) — observed as a configured setting value on a Studio-generated hero, 2026-08-31.
89
+
90
+ This is distinct from SKILL.md's no-churn rule: cleaning up a block you're ADOPTING into the repo is required; modernizing a deployed working block's syntax is still churn — don't do that.
91
+
79
92
  ## Vibe coding gotchas (official)
80
93
 
81
94
  From the official MCP docs — these hold for MCP-driven and Studio-driven edits alike:
@@ -122,6 +135,13 @@ Known limits and behaviors (per official docs):
122
135
  `{ filename, url }` on an ATTACHMENT field with any publicly reachable URL; Softr fetches it, stores its
123
136
  own copy and generates thumbnails, so backfilling images from another system is one write per record
124
137
  with no upload step. Verified 2026-08-26 — see [../datasources/writing.md](../datasources/writing.md#attachment).
138
+ - **`update_field` silently ignores `allowMultipleEntries` nested inside `options`** — it is a TOP-LEVEL
139
+ field property; the call succeeds and changes nothing (verified 2026-08-26). To flip a LINKED_RECORD
140
+ field between single and multi, `PUT` it via the Tables API with `allowMultipleEntries` at top level —
141
+ and always echo `options.inverseLinkFieldId` in that PUT, because omitting it severs the inverse
142
+ pairing. Full write-up, including the silent on-write clobbering of single-valued link pairs and the
143
+ truthy-`[]` empty-link read shape:
144
+ [../datasources/writing.md](../datasources/writing.md#linked-record-write-traps-verified-live-2026-08-26).
125
145
  - Limits: 100 records per `create_records` call, 200 records per read (silently capped, not an error), 2 group-by fields in `aggregate_data`. For big tables prefer a filter or aggregate over paging.
126
146
 
127
147
  Typical Vibe Coding uses: "list every field on `Wigs` with id, name, type, and dropdown options", "what's the option id for `Payment status` = 'Partially paid'?", "show 3 sample records so we know value shapes", "verify the field id in my `q.select()` exists". This eliminates the field-id-typo / wrong-option-uuid class of bugs entirely.
@@ -0,0 +1,97 @@
1
+ # Static Marketing Blocks — Heroes, Landing Sections, and the Editorial Lane
2
+
3
+ A **static block** is a Vibe Coding block with **zero datasources**: all content comes from editable settings, nothing loads, nothing mutates. Officially blessed — Softr's user guide lists "a static layout like a page header, pricing table, or content section" as a first-class use of the Vibe Coding block. Typical members: hero sections, landing-page headers, pricing tables, testimonial bands, logo walls, FAQ sections, footers, content/feature sections.
4
+
5
+ This archetype inverts most of the skill's data-block defaults, so read this file INSTEAD of the datasource guides when the requested block is static marketing content. (Verified against a Studio-AI-generated hero rendering live in Studio, 2026-08-31.)
6
+
7
+ ## Contents
8
+
9
+ - [Workflow deltas](#workflow-deltas)
10
+ - [Editorial baseline (replaces the Premium Visual Baseline)](#editorial-baseline-replaces-the-premium-visual-baseline)
11
+ - [Full-bleed layout license](#full-bleed-layout-license)
12
+ - [Full-viewport hero sizing](#full-viewport-hero-sizing)
13
+ - [Block-owned landing-page header](#block-owned-landing-page-header)
14
+ - [Section anchors on landing pages](#section-anchors-on-landing-pages)
15
+ - [Harvesting Studio-AI marketing blocks](#harvesting-studio-ai-marketing-blocks)
16
+
17
+ ## Workflow deltas
18
+
19
+ When the block is static marketing content:
20
+
21
+ - **SKIP data-source interrogation entirely** — no data source type question, no field IDs, no datasource guide loading. Step 1 (DESIGN.md brand detection) still runs unchanged.
22
+ - **Settings-first design is the default** — every user-visible string, image, and link becomes an editable setting; see the granularity doctrine in [editable-settings.md](editable-settings.md#granularity-doctrine-settings-first-static-blocks). This is what makes the block a reusable, client-editable template instead of frozen copy.
23
+ - **Self-validation deltas** — these Step 6 checklist items DON'T apply: loading/error/empty states (nothing loads), `getFieldValue()` wrapping (no records), mutation `enabled` gating, `fetchNextPage` rules, container/content wrappers (see full-bleed below). These DO apply, plus extras: hooks before conditional returns, module-scope sub-components, no hardcoded user-visible copy, media settings gated for the empty-src state, array rows keyed by index, `// BLOCK PLACEMENT:` comment present.
24
+ - **Ask-AI buttons still need a data source.** The one place a "static" block touches data: an `OPEN_CHAT` NavigationAction pulls AI context from the block that triggered it — with no data source connected, `chat/prepare` returns HTTP 500. Connect the block to whatever table the AI should read, even though the block renders none of it. See [anti-patterns.md](anti-patterns.md#layout--styling).
25
+
26
+ ## Editorial baseline (replaces the Premium Visual Baseline)
27
+
28
+ SKILL.md's Premium Visual Baseline (gradient wrapper, icon-in-rounded-square header, card grids, skeletons, empty states) is an **app-UI** baseline — applying it to a hero produces a dashboard cosplaying as a landing page. For static marketing blocks, the baseline is editorial instead:
29
+
30
+ 1. **Typographic hierarchy carries the design** — display-size headings, an eyebrow/tagline, generous line-height on body copy. Use the marketing display-type lane in ui-ux-guidelines.md §6.
31
+ 2. **Brand-exact values over theme tokens** — arbitrary Tailwind values (`bg-[#FAF5EC]`, `text-[17px]`, `rounded-[6px]`) and inline-style brand hexes are the correct tool here (see §7's editorial lane in ui-ux-guidelines.md). Hoist repeated hexes to module-scope constants — see [anti-patterns.md](anti-patterns.md#layout--styling).
32
+ 3. **Decorative layers, used deliberately** — background blobs/shapes, edge-fade image masks, art-directed responsive images. Recipes in [common-patterns.md](common-patterns.md).
33
+ 4. **No loading/empty/error scaffolding** — there is nothing to load. The only "empty" state to handle is an un-uploaded media setting ([editable-settings.md](editable-settings.md#media-hooks-useimagesetting-and-usevideosetting)).
34
+ 5. **Restraint still applies** — ui-ux-guidelines.md's AI-slop bans (purple-to-blue gradients, glassmorphism everywhere, gradient text) bind in this lane too. Editorial ≠ decorated.
35
+
36
+ ## Full-bleed layout license
37
+
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
+
40
+ When you go full-bleed:
41
+
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.
43
+ - **Own your inner max-widths** — constrain the copy column yourself (`lg:max-w-[46%]`, `max-w-[430px]` on body text).
44
+ - **`overflow-hidden` on the block root** if decorative shapes offset off-canvas (prevents horizontal scroll).
45
+ - **Record the choice** in the placement comment so future edits and reviews know it's deliberate:
46
+
47
+ ```tsx
48
+ // BLOCK PLACEMENT: full-bleed hero, native header hidden, owns all spacing
49
+ // Spacing: no container/content; gutters px-6 md:px-12 lg:px-16
50
+ ```
51
+
52
+ The standard spacing table (`py-3 px-8` wrappers) does not apply — a full-bleed block owns all of its spacing.
53
+
54
+ ## Full-viewport hero sizing
55
+
56
+ The Studio-verified responsive shape:
57
+
58
+ ```tsx
59
+ <div className="relative min-h-screen lg:min-h-0 lg:h-screen lg:flex lg:flex-col ...">
60
+ <main className="lg:flex-1 lg:flex lg:items-center ...">
61
+ ```
62
+
63
+ Natural height on mobile, locked viewport height on desktop, content vertically centered in the remaining space. Rules and gotchas:
64
+
65
+ - **vh/`h-screen` resolve against the real viewport** (blocks are shadow DOM in the main document — see [Block-owned landing-page header](#block-owned-landing-page-header)). So `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-<headerHeight>px)]` when native chrome stays.
66
+ - **Hard `h-screen` + `overflow-hidden` + centered flex CLIPS settings-grown content — unrecoverably.** Every string in a settings-first hero is builder-lengthenable; on a short laptop viewport, two extra sentences in the description push the CTA row past the clip, and centered-flex overflow clips top AND bottom, so no scrolling reaches it. Prefer `lg:min-h-screen` (grow-with-content) unless the locked-viewport look is explicitly wanted; if locking, flag the tradeoff to the user and keep the copy settings short.
67
+ - **Mobile URL-bar resize**: consider `min-h-[100svh]`/`dvh` variants if the mobile jump matters; the `min-h-screen` mobile default is the safe baseline.
68
+
69
+ ## Block-owned landing-page header
70
+
71
+ The scope rule stays true: Softr's **native** chrome (top bar, auth-aware account menu, user-group-gated nav) cannot be built or replaced as a block — restyling it is global Custom Code territory ([native-chrome-styling.md](native-chrome-styling.md)). But on a landing page where the native header is **hidden in Studio**, a hero block CAN render its own `<header>` — Softr's own Studio AI emits exactly this, and the official user guide lists "a page header" as a supported static layout. Mechanics (rendered live in Studio 2026-08-31; the positioning behavior is standard shadow-DOM/CSS — confirm in the published app):
72
+
73
+ - **`position: fixed` works from inside the block** and anchors to the viewport — blocks render in a shadow root in the MAIN document (not an iframe), and shadow roots don't create a containing block. The header escapes the block's visual bounds and overlays every other block on the page.
74
+ - **Window scroll listeners work normally** — `window.scrollY` reflects the real page. The scroll-condensing treatment (translucent bg + backdrop-blur + border after ~24px) is in [common-patterns.md](common-patterns.md#scroll-condensing-fixed-header-landing-page-hero).
75
+
76
+ The caveat set — state these in any block that ships its own header:
77
+
78
+ 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
+ 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.
81
+ 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
+ 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
+ 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.
84
+
85
+ The wider decision (restyle native vs replace globally vs block-owned) is laid out in [native-chrome-styling.md](native-chrome-styling.md#restyle-vs-replace-vs-block-owned-header).
86
+
87
+ ## Section anchors on landing pages
88
+
89
+ Heroes pair with same-page sections via hash links (nav "Capabilities" → `#capabilities` further down).
90
+
91
+ - **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
+
95
+ ## Harvesting Studio-AI marketing blocks
96
+
97
+ Marketing blocks are where you'll most often mine Studio-AI output for platform truth (it surfaced `useLongTextSetting` and the `navigation` array-schema type before the official docs did). Mine capabilities, never copy verbatim — the standard defects to fix on adoption are listed in SKILL.md's "Platform truth sources" note and [softr-mcp.md](softr-mcp.md#adopting-studio-ai-generated-code) (React keys, hex drift, formatting, missing mobile nav, unconditional media renders).
@@ -192,6 +192,17 @@ Aim for at least **1.25x ratio** between adjacent scale steps. Sizes only 1-2px
192
192
  - Use `text-foreground`, not pure black. The theme token is calibrated for comfortable contrast.
193
193
  - **Use tabular numbers** in data tables for column alignment: add `style={{ fontVariantNumeric: "tabular-nums" }}` to numeric cells.
194
194
 
195
+ ### Marketing / editorial display type (heroes, landing blocks)
196
+
197
+ The scale above is for **app UI** (dashboards, forms, lists). Static marketing blocks get a sanctioned exception lane (patterns verified from rendering Studio-AI hero output, 2026-08-31 — see [references/static-blocks.md](references/static-blocks.md)):
198
+
199
+ - **Display headings 40–60px**, stepped per breakpoint with arbitrary px classes — `text-[40px] md:text-[56px] lg:text-[54px] xl:text-[60px]` — px precision because the Tailwind scale jumps too coarsely up there.
200
+ - **Display line-height 1.05–1.1** for multi-line display headings (`leading-[1.08]`); the leading-snug rule above is for app-scale headings.
201
+ - **Slight negative tracking on large display type**: `tracking-[-0.015em]`.
202
+ - **Eyebrow/tagline pattern**: 13–14px, uppercase, `tracking-[0.15em]`–`tracking-[0.2em]`, `font-medium`, in an accent color, above the heading. (An eyebrow IS a "short label" — the all-caps rule below permits it.)
203
+ - **Wordmark/logotype text**: `font-light`/`font-normal` + wide tracking (`tracking-[0.16em]`) for luxury brands.
204
+ - Body copy in this lane still follows the body rules (16–19px, `leading-[1.6]`-ish, max-width constrained).
205
+
195
206
  ### Typography Anti-Patterns:
196
207
  - **DO NOT** use monospace typography as lazy shorthand for "technical/developer" vibes.
197
208
  - **DO NOT** place large rounded icons above every heading — they rarely add value and look templated.
@@ -220,7 +231,7 @@ All spacing uses **multiples of 4px**. The critical step that pure 8pt systems m
220
231
  - **Vary spacing for hierarchy.** A heading with extra space above reads as more important. Uniform padding everywhere feels monotonous.
221
232
  - **Internal spacing <= external spacing:** Padding *inside* a component must be smaller than the margin *between* components.
222
233
  - **Button height:** 40px (`h-10`) or 48px (`h-12`) — touch-friendly.
223
- - **Never use arbitrary values** like `p-[13px]`. Always use the Tailwind scale.
234
+ - **Stick to the Tailwind scale for spacing in app-UI blocks** — avoid arbitrary spacing values like `p-[13px]` there; scale steps keep rhythm consistent. This is a design-consistency preference, NOT a platform limit: the platform's Tailwind build is JIT and the full arbitrary-value syntax compiles, including opacity-modified arbitrary hex (`bg-[#FAF5EC]/85`), negatives (`-top-[22%]`), arbitrary object-position/z/vw (verified from rendering Studio-AI output, 2026-08-31). In the **editorial/brand-faithful lane** (marketing heroes, luxury type, brand-exact radii — see [references/static-blocks.md](references/static-blocks.md)), px-precise arbitrary values are the correct tool. Elsewhere in this file, `min-h-[44px]` touch targets already use them.
224
235
  - **Generous whitespace** reduces cognitive load — it is not wasted space.
225
236
 
226
237
  ### Depth and Elevation:
@@ -306,7 +317,7 @@ Vestibular disorders affect ~35% of adults over 40. Always respect `prefers-redu
306
317
  | State | Purpose | Implementation |
307
318
  |---|---|---|
308
319
  | **Default** | Ready to use | Standard `Button` component |
309
- | **Hover** | Cursor over it | Handled by shadcn `Button` |
320
+ | **Hover** | Cursor over it | Handled by shadcn `Button` — EXCEPT when an inline `style={{ backgroundColor }}` overrides the variant: inline style beats every `hover:bg-*` class, so use `hover:opacity-90` / transform instead (see SKILL.md's brand-hex Button recipe) |
310
321
  | **Focus** | Keyboard-selected | `focus-visible:ring-2` — never remove |
311
322
  | **Active** | Being pressed | Pressed in, darker |
312
323
  | **Loading** | Action in progress | Replace label with `<Spinner />` + disable |
@@ -535,6 +546,8 @@ Avoid the "hero metric layout template" — big number, small label, supporting
535
546
  ### Keyboard and screen reader:
536
547
  - All interactive elements reachable by keyboard (Tab, Enter, Space, Escape).
537
548
  - `aria-label` on all icon-only buttons.
549
+ - **`aria-hidden="true"` on decorative glyph characters** used as separators or ornaments (●, •, |, →, ✦) — screen readers otherwise announce them literally ("black circle"). Purely decorative empty divs need nothing; decorative `<img>` elements need `alt=""`.
550
+ - **Landmark hygiene when a block ships page chrome**: shadow DOM does NOT hide landmarks from assistive tech, and Softr's native chrome uses semantic elements — so a block's own `<header>`/`<main>` on a page with native chrome creates duplicate banner/main landmarks. Use plain `<div>`s there; reserve `<header>`/`<main>` for pages where the native chrome is hidden.
538
551
  - Semantic HTML: `<button>` for actions, `<a>` for navigation, `<input>` for data.
539
552
  - **Focus rings:** Never `outline: none` without replacement. Always keep `focus-visible:ring-2`. Focus ring must be 2-3px thick, high contrast, offset from the element.
540
553
  - Tables: proper `<thead>`, `<tbody>`, `<th scope="col">`.
@@ -551,6 +564,9 @@ Avoid the "hero metric layout template" — big number, small label, supporting
551
564
  - Form fields: `w-full` on mobile.
552
565
  - Navigation: collapse to hamburger or bottom nav on small screens.
553
566
 
567
+ ### Art-directed responsive images:
568
+ A settings hook returns a plain value, so one `useImageSetting` may safely feed **two sibling `<img>` renders** with opposite visibility classes — desktop: absolute-positioned, masked, cropped via arbitrary `object-[x%_y%]`; mobile: in-flow full-bleed (`hidden lg:block` / `lg:hidden`, same mechanism as the table→cards swap in §18). The builder still edits ONE image in the Settings pane. To bleed the mobile render edge-to-edge against the wrapper's own horizontal padding, use matching negative margins (`-mx-6` against `px-6`, `md:-mx-12` against `md:px-12`).
569
+
554
570
  ### Breakpoint strategy:
555
571
  ```
556
572
  Mobile first: default -> sm (640px) -> md (768px) -> lg (1024px) -> xl (1280px)