softr-vibe-coding 2.1.2 → 2.3.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 +6 -0
- package/README.md +33 -19
- package/SKILL.md +62 -19
- package/datasources/rest-api.md +1 -1
- package/datasources/softr-database.md +2 -0
- package/datasources/writing.md +2 -1
- package/package.json +1 -1
- package/references/airtable-automations.md +3 -2
- package/references/anti-patterns.md +7 -1
- package/references/common-patterns.md +132 -1
- package/references/editable-settings.md +238 -0
- package/references/native-chrome-styling.md +6 -2
- package/references/quick-reference.md +19 -8
- package/references/softr-mcp.md +96 -9
- package/references/static-blocks.md +97 -0
- package/ui-ux-guidelines.md +18 -2
|
@@ -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
|
|
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 (workflow-side receiving end by name: the "Run Custom Workflow action triggered" trigger — name-based match, not wired live; see [softr-mcp.md](softr-mcp.md#workflows))
|
|
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
|
|
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
|
|
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-
|
|
255
|
-
if (result.status === "error") return <div className="container py-
|
|
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-
|
|
269
|
+
<div className="container py-0">
|
|
261
270
|
<div className="content">
|
|
262
|
-
|
|
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
|
);
|