softr-vibe-coding 1.5.1 → 1.6.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,12 @@ 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
|
+
## [1.6.0] - 2026-06-03
|
|
8
|
+
- Document useNavigationBlocker for form-dirty navigation guards in Softr SPA mode
|
|
9
|
+
|
|
10
|
+
## [1.5.2] - 2026-05-27
|
|
11
|
+
- Document BLANK-guard convention for Airtable formulas — Airtable surfaces #ERROR! / #NaN! when arithmetic, date, or string operations touch a blank field and propagates the error through every downstream formula; add a quick-rule bullet calling out the failure modes (multiply/divide-by-blank, DATEADD on blank date), show the IF({field}, <expr>, BLANK()) and AND()-guarded shapes, note why explicit guards beat catch-all IFERROR (don't mask typos), and re-render the common-patterns block with guards applied to the date examples; bump to 1.5.2
|
|
12
|
+
|
|
7
13
|
## [1.5.1] - 2026-05-21
|
|
8
14
|
- Expand the "formulas in create/update" anti-pattern to cover every read-only field type (rollup / aiText / lookup / createdTime / lastModifiedTime / autoNumber, not just formulas), document the silent all-or-nothing parser failure (Actions tab empty, enabled stays false, every other writable field in the same q.select is lost), cite the verified 2026-05-22 wig-details-page.jsx case (Wig Tag ID formula + Total client/worker rollups + Instrucciones aiText all shared with useRecordUpdate), and point at the bisection diagnostic; bump to 1.5.1
|
|
9
15
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "softr-vibe-coding",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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"
|
|
@@ -321,22 +321,31 @@ For Automation Scripts, your ONLY user-visible surfaces are `console.log()` (run
|
|
|
321
321
|
For formula fields inside Airtable itself (NOT scripts). Quick rules:
|
|
322
322
|
|
|
323
323
|
- **NEVER add comments inside Airtable formulas.** Airtable's formula engine doesn't have a comment syntax — anything that looks like a comment will fail to compile.
|
|
324
|
+
- **Guard against blank inputs by default.** Airtable surfaces `#ERROR!` (or `#NaN!` for divide-by-blank) when arithmetic, date, or many string operations touch a blank field — `{A} * {B}` errors when either is blank, `{A} / {B}` errors when `{B}` is blank, `DATEADD({Start At}, 20, 'minutes')` errors when `{Start At}` is blank, and one upstream `#ERROR!` propagates through every downstream formula that references it. Wrap any formula whose inputs could be empty with an `IF()` guard returning `BLANK()` in the empty branch. Prefer explicit `IF()` / `AND()` guards over a catch-all `IFERROR(<expr>, BLANK())` — explicit guards keep intent readable and don't mask unrelated bugs (a typo'd field name silently returns blank instead of failing loudly). Reach for `IFERROR()` only when inputs come from genuinely uncontrolled sources.
|
|
325
|
+
|
|
326
|
+
```
|
|
327
|
+
IF({Start At}, DATEADD({Start At}, 20, 'minutes'), BLANK())
|
|
328
|
+
IF(AND({Quantity}, {Price}), {Quantity} * {Price}, BLANK())
|
|
329
|
+
IF({Divisor}, {Numerator} / {Divisor}, BLANK())
|
|
330
|
+
```
|
|
324
331
|
- Reference fields by name with curly braces: `{Field Name}` (single field) or `{Other Table}` is not valid — formulas are scoped to the current table only.
|
|
325
332
|
- Functions are case-insensitive (`IF` = `if`) but field names ARE case-sensitive.
|
|
326
333
|
- Use `&` for concatenation, NOT `+` (the latter is numeric).
|
|
327
334
|
- For conditional logic, prefer `IF()` nested or `SWITCH()` for many branches.
|
|
328
335
|
|
|
329
|
-
Common patterns:
|
|
336
|
+
Common patterns (blank-guards applied where inputs could be empty):
|
|
330
337
|
|
|
331
338
|
```
|
|
332
339
|
IF({Status} = "Paid", "✓", "")
|
|
333
340
|
|
|
334
341
|
CONCATENATE({First Name}, " ", {Last Name})
|
|
335
342
|
|
|
336
|
-
DATETIME_FORMAT({Created}, 'YYYY-MM-DD')
|
|
343
|
+
IF({Created}, DATETIME_FORMAT({Created}, 'YYYY-MM-DD'), BLANK())
|
|
337
344
|
|
|
338
345
|
IF(AND({Amount} > 100, {Paid} = TRUE()), "VIP", "Standard")
|
|
339
346
|
|
|
347
|
+
IF({Start At}, DATEADD({Start At}, 20, 'minutes'), BLANK())
|
|
348
|
+
|
|
340
349
|
SWITCH({Tier},
|
|
341
350
|
"gold", "Premium",
|
|
342
351
|
"silver", "Standard",
|
|
@@ -62,6 +62,7 @@ Run through this catalog before delivering any block. Every row is a violation o
|
|
|
62
62
|
| Relying on `custom-code-header.html` (Softr → Settings → Custom Code → Code inside header) to apply brand fonts/colors INSIDE a Vibe Coding block | Vibe Coding blocks render inside a shadow DOM. CSS custom properties (`--brand-*`) pierce that boundary, but `html, body { font-family: ... !important }` rules **do not** — `<html>` and `<body>` don't exist inside the shadow root. Apply brand fonts/colors at the block's **own outermost wrapper** via inline style: `style={{ fontFamily: "'Manrope', system-ui, sans-serif", color: BRAND_INK }}` on the outer `<div>` so every descendant inherits brand defaults. Override per-element with explicit inline `fontFamily` (e.g., `"'Fraunces', Georgia, serif"` on h1/h2). Google `<link>` tags in the page head DO load `@font-face` globally — the fonts are available inside shadow DOM, they just need to be applied. |
|
|
63
63
|
| Painting `backgroundColor: BRAND_CANVAS` on a Vibe Coding block's outer wrapper when `custom-code-header.html` already sets `body { background-color: var(--brand-canvas) !important }` | Don't double-paint. If the body bg is already the brand canvas, the block leaves its own backgroundColor unset and the page bg shows through. Painting the same color twice produces a visible seam — Softr's content wrapper sits between `<body>` and the Vibe Coding block, and the two backgrounds composite slightly differently due to sub-pixel rendering, transparency stacking, or wrapper paddings. Set fontFamily and color on the block's wrapper (those don't inherit cleanly through shadow DOM), but **leave backgroundColor unset** — let the page bg flow through. The exception: if the block needs a brand-tinted *section* (e.g., a card-style admin shell that's different from the page bg), paint that bg explicitly on its specific container, not on the outer wrapper. |
|
|
64
64
|
| `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. |
|
|
65
|
+
| 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). |
|
|
65
66
|
|
|
66
67
|
## Permissions
|
|
67
68
|
|
|
@@ -6,6 +6,7 @@ Small reusable patterns that come up across Vibe Coding blocks but don't warrant
|
|
|
6
6
|
|
|
7
7
|
- [Cross-Page State with localStorage + URL Parameters](#cross-page-state-with-localstorage--url-parameters)
|
|
8
8
|
- [Clipboard Copy Button](#clipboard-copy-button)
|
|
9
|
+
- [Navigation Blocker for Unsaved Changes](#navigation-blocker-for-unsaved-changes)
|
|
9
10
|
|
|
10
11
|
## Cross-Page State with localStorage + URL Parameters
|
|
11
12
|
|
|
@@ -100,3 +101,63 @@ Usage:
|
|
|
100
101
|
```
|
|
101
102
|
|
|
102
103
|
The `aria-label` is required because the button has no visible text, only an icon. Without it the button is not screen-reader accessible.
|
|
104
|
+
|
|
105
|
+
## Navigation Blocker for Unsaved Changes
|
|
106
|
+
|
|
107
|
+
Softr apps use SPA-mode client-side navigation — when a user clicks a link in Softr's nav bar, sidebar, or any `<NavigationAction>`, the route changes without a full page reload. The browser's standard `beforeunload` event only fires for tab close / refresh / browser back-forward / external nav, so the classic dirty-form warning misses every internal Softr click.
|
|
108
|
+
|
|
109
|
+
Softr's `useNavigationBlocker` hook intercepts BOTH internal SPA navigation AND browser-level unload with a single API. Import it from `@/lib/use-navigation-blocker`.
|
|
110
|
+
|
|
111
|
+
**Boolean form — simplest case:**
|
|
112
|
+
|
|
113
|
+
```jsx
|
|
114
|
+
import { useState } from "react";
|
|
115
|
+
import { useNavigationBlocker } from "@/lib/use-navigation-blocker";
|
|
116
|
+
|
|
117
|
+
export default function Block() {
|
|
118
|
+
var [isDirty, setIsDirty] = useState(false);
|
|
119
|
+
|
|
120
|
+
useNavigationBlocker(isDirty);
|
|
121
|
+
|
|
122
|
+
function handleFieldChange(newValue) {
|
|
123
|
+
setIsDirty(true);
|
|
124
|
+
/* ... update form state ... */
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* ... form rendering ... */
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Callback form — when you need to read a ref without re-running on every render:**
|
|
132
|
+
|
|
133
|
+
```jsx
|
|
134
|
+
import { useRef } from "react";
|
|
135
|
+
import { useNavigationBlocker } from "@/lib/use-navigation-blocker";
|
|
136
|
+
|
|
137
|
+
export default function Block() {
|
|
138
|
+
var dirtyRef = useRef(false);
|
|
139
|
+
|
|
140
|
+
useNavigationBlocker(function() { return dirtyRef.current; });
|
|
141
|
+
|
|
142
|
+
function handleFieldChange() {
|
|
143
|
+
dirtyRef.current = true;
|
|
144
|
+
/* ... update local state without re-rendering the hook ... */
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ... rest ... */
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The hook automatically handles:
|
|
152
|
+
|
|
153
|
+
- Browser's "Leave site?" dialog on tab close / refresh / external nav.
|
|
154
|
+
- Softr's in-app confirmation modal when the user clicks an internal Softr link or `<NavigationAction>`.
|
|
155
|
+
- Letting navigation through if the user confirms; cancelling if they decline.
|
|
156
|
+
|
|
157
|
+
**Most form blocks don't need to wire this manually** — Softr's Vibe Coding bundler often adds the blocker automatically when it detects form dirty state. You only need to add it explicitly for advanced cases:
|
|
158
|
+
|
|
159
|
+
- Multi-step forms where the dirty state spans several panels.
|
|
160
|
+
- Manual dirty tracking that doesn't go through standard form-state hooks.
|
|
161
|
+
- Blocks where you want to block on something other than form dirtiness (e.g., a pending background upload).
|
|
162
|
+
|
|
163
|
+
**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.
|
|
@@ -18,6 +18,9 @@ import { useTextSetting, useImageSetting, useVideoSetting, useArraySetting,
|
|
|
18
18
|
useVibeCodingBlockIconSetting, useNavigationSetting,
|
|
19
19
|
useBooleanSetting } from "@/lib/editable-settings";
|
|
20
20
|
|
|
21
|
+
// NAVIGATION GUARD (form unsaved-changes warning that also works on Softr's SPA nav)
|
|
22
|
+
import { useNavigationBlocker } from "@/lib/use-navigation-blocker";
|
|
23
|
+
|
|
21
24
|
// REACT
|
|
22
25
|
import { useState, useEffect, useMemo, useCallback, useRef } from "react";
|
|
23
26
|
|
|
@@ -185,6 +188,18 @@ useEffect(function() {
|
|
|
185
188
|
}, [result.hasNextPage, result.isFetchingNextPage, result.status, result.fetchNextPage]);
|
|
186
189
|
```
|
|
187
190
|
|
|
191
|
+
## Navigation Blocker (unsaved-changes warning)
|
|
192
|
+
|
|
193
|
+
```jsx
|
|
194
|
+
// Boolean form: simplest case
|
|
195
|
+
useNavigationBlocker(isDirty);
|
|
196
|
+
|
|
197
|
+
// Callback form: reads from a ref without re-running on every render
|
|
198
|
+
useNavigationBlocker(function() { return dirtyRef.current; });
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Catches BOTH Softr's in-app SPA navigation (nav bar, sidebar, `<NavigationAction>`) AND browser unload (tab close, refresh, external links). A plain `window.addEventListener("beforeunload", ...)` does NOT catch Softr's in-app nav. See [common-patterns.md](common-patterns.md#navigation-blocker-for-unsaved-changes).
|
|
202
|
+
|
|
188
203
|
## Component Skeleton
|
|
189
204
|
|
|
190
205
|
```jsx
|