softr-vibe-coding 2.5.1 → 2.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 +6 -0
- package/SKILL.md +5 -2
- package/package.json +1 -1
- package/references/anti-patterns.md +2 -1
- package/references/common-patterns.md +100 -0
- package/references/softr-mcp.md +23 -2
- package/ui-ux-guidelines.md +2 -0
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
|
+
## [2.6.0] - 2026-09-09
|
|
8
|
+
- Drag-to-reorder: the pattern, the shadow-DOM hit-test trap, and two affordance rules
|
|
9
|
+
|
|
10
|
+
## [2.5.2] - 2026-09-09
|
|
11
|
+
- Permission finding is advisory, not a veto — report severity and leave the call to the builder
|
|
12
|
+
|
|
7
13
|
## [2.5.1] - 2026-09-09
|
|
8
14
|
- The Action-permission reset has a failure mode that leaves blocks publicly writable — verify, never assume
|
|
9
15
|
|
package/SKILL.md
CHANGED
|
@@ -555,8 +555,11 @@ Non-negotiable rules. Most are enforced by the Softr platform (compiler, validat
|
|
|
555
555
|
can itself fail with no fallback (see the array-argument quirk in
|
|
556
556
|
[references/softr-mcp.md](references/softr-mcp.md#the-array-argument-serialization-quirk-and-why-it-is-a-security-issue)).
|
|
557
557
|
A push that returns `errors: null` can still have left public write access on the block.
|
|
558
|
-
**If any action is still `ALL_USERS`,
|
|
559
|
-
in
|
|
558
|
+
**If any action is still `ALL_USERS`, report it WITH its severity and let the builder decide.**
|
|
559
|
+
Check the page's own VIEW permission first (`get_page_permissions`): a page gated to logged-in
|
|
560
|
+
users makes an open action housekeeping, a public page makes it a real hole. Surface the list
|
|
561
|
+
either way -- page, block, action type, data source -- and note that a human sets them on the
|
|
562
|
+
block's Actions tab. Do not unilaterally block a publish; it is not your app.
|
|
560
563
|
22. **Blocks cannot import each other -- cross-block consistency is discipline, not architecture [house]**
|
|
561
564
|
-- Every block compiles standalone. There is no shared module, no design-system import, nothing that
|
|
562
565
|
makes two blocks stay alike. Two blocks that must look the same WILL drift, and the drift usually
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "softr-vibe-coding",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.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"
|
|
@@ -39,7 +39,7 @@ Run through this catalog before delivering any block. Every row is a violation o
|
|
|
39
39
|
| Passing a data hook's options through a variable or wrapper function: `useRecords(buildOpts())` | **Fails to compile** — the options object must be an inline literal at the call site (verified live 2026-08-25; hit in production, fixed by making the wrapper take the hook's *result* instead). Share `q.select` mappings between hooks, never whole options objects |
|
|
40
40
|
| Tightening Actions-tab permissions before the block's final redeploy | Every code recompile **resets the auto-registered Actions to default permissions** (verified live 2026-08-25). Tighten permissions after the LAST redeploy, and re-check after any future one |
|
|
41
41
|
| Assuming a comment-only edit is "safe" and leaves Action permissions alone | There is no cosmetic-edit exemption. Any save recompiles, and every recompile rebuilds the Actions at default visibility — a `search_replace` changing nothing but a code comment resets them exactly like a rewrite (verified live 2026-09-09, on two blocks at once). Re-check after EVERY push, including cosmetic ones |
|
|
42
|
-
| Treating the permission-restore call as done because you issued it | Read the permissions back with `get_vibe_coding_block_settings` and confirm each one changed. `set_vibe_coding_block_action_visibility` can fail outright on the array-argument serialization quirk, and it has **no fallback** — the default for a `genericActions` ADD_RECORD is `ALL_USERS`, so a routine push silently leaves the block publicly writable while returning `errors: null`. Verified live 2026-09-09: four ADD_RECORD actions left open across two blocks.
|
|
42
|
+
| Treating the permission-restore call as done because you issued it | Read the permissions back with `get_vibe_coding_block_settings` and confirm each one changed. `set_vibe_coding_block_action_visibility` can fail outright on the array-argument serialization quirk, and it has **no fallback** — the default for a `genericActions` ADD_RECORD is `ALL_USERS`, so a routine push silently leaves the block publicly writable while returning `errors: null`. Verified live 2026-09-09: four ADD_RECORD actions left open across two blocks. Report the list with its severity — check the page's VIEW permission with `get_page_permissions`, since a logged-in-gated page makes this housekeeping while a public page makes it a real hole — and let the builder decide whether it holds their release. A human sets them on the block's Actions tab |
|
|
43
43
|
| Treating Studio's Actions tab as a separately-managed configuration to keep in sync with code | Actions auto-derive from your `useRecordCreate`/`useRecordUpdate`/`useRecordDelete` + `q.select` on every save. The Actions tab is a read-only inspector; there is no manual delete control. To change an Action, change the code |
|
|
44
44
|
| One alias in a write-side `q.select` referencing a renamed / non-existent Airtable column | Softr's Action parser silently rejects the **entire** create/update Action — not just the bad alias. Symptoms: Studio's Actions tab shows "No actions used in this block yet", `createRecord.enabled` / `updateRecord.enabled` stays `false`, `.mutate()` calls dispatch but resolve immediately to "not yet ready". Every OTHER field in the same `q.select()` is also lost, even the ones that map cleanly. Diagnostic: bisect the `q.select` — strip down to a known-good minimal set, confirm the Action appears in Studio, then add fields back in halves until it drops out. The culprit is in the last half added. Once narrowed to a single field, grep its name against the freshest Airtable schema export to catch the rename / trailing-space / case-mismatch. Verified 2026-05-21: a `"Photos"` column on Wigs was renamed to `"Before Photos"`, the helper that wrote `photos: "Photos"` had its entire Action disabled even though 11 other fields in the same `q.select` were fine. See [datasources/airtable.md](../datasources/airtable.md#maintainability-gotcha) |
|
|
45
45
|
|
|
@@ -68,6 +68,7 @@ Run through this catalog before delivering any block. Every row is a violation o
|
|
|
68
68
|
| Hardcoded domain in navigation | Relative paths: `/task-details?recordId=...` |
|
|
69
69
|
| Placing a `<NavigationAction navigation={{ action: "OPEN_CHAT" }}>` Ask-AI button on a block that has no data source connected | Connect the block to the data source the AI should read from in Studio's Source tab. Softr's AI pulls context from the **block that triggered the chat**, not from the page — a button-only helper block with no data source causes `chat/prepare` → HTTP 500 ("Failed to prepare AI assistant") even though the chat UI opens fine. The block doesn't need to read or write records itself; the connection is purely for AI context. Verified by direct experiment, May 2026 |
|
|
70
70
|
| Emojis in UI | lucide-react icons only |
|
|
71
|
+
| `document.elementFromPoint(x, y)` to hit-test during a drag or custom pointer interaction | It returns the block's shadow **host**, not the element under the cursor — same boundary that stops `getElementById` and URL-fragment lookup. Either call it on the shadow root (`ref.current.getRootNode().elementFromPoint(x, y)`) or, better, keep refs to the candidate elements and compare `getBoundingClientRect()` yourself: rects need no shadow-root plumbing and work identically when the block is later reused elsewhere |
|
|
71
72
|
| Positioning repeated page chrome (back button, title, primary action) per-block, without checking the pages that already have it | Chrome the user meets on more than one screen is a cross-page contract. Copy the exact offset from the blocks that already ship it, and change every page in one edit. Let the wrapper's padding be the only thing positioning it — `mb-4` and NO top margin on a back button — so one number per page governs it. Verified 2026-09-09: an extra `mt-6` sat one detail page's back button 24px lower than another's, and **each block looked correct in isolation**. See SKILL.md's Block Placement section |
|
|
72
73
|
| A loading skeleton carrying a different border / offset from the component it stands in for | The skeleton must track the component's REST state (border colour, padding, chrome offsets), never its hover state. If they disagree the layout visibly re-draws the instant data lands — the exact thing a skeleton exists to prevent. Verified 2026-09-09 twice in one session: a card grid re-outlined itself on load, and a back button jumped 24px. See [ui-ux-guidelines.md](../ui-ux-guidelines.md) §12 |
|
|
73
74
|
| `focus-visible:` on a card or row that only *contains* buttons | The container is a plain `<div>` and never takes focus, so it is dead CSS. Use `focus-within:` on the container (pairs with its `hover:` treatment) and keep `focus-visible:ring-2` on the button/link itself |
|
|
@@ -14,6 +14,7 @@ Small reusable patterns that come up across Vibe Coding blocks but don't warrant
|
|
|
14
14
|
- [Edge-Fade Image Mask (Editorial Hero)](#edge-fade-image-mask-editorial-hero)
|
|
15
15
|
- [Decorative Background Blobs (Editorial Layering)](#decorative-background-blobs-editorial-layering)
|
|
16
16
|
- [Dot-Separated Inline List](#dot-separated-inline-list)
|
|
17
|
+
- [Drag-to-Reorder Rows](#drag-to-reorder-rows)
|
|
17
18
|
|
|
18
19
|
## Cross-Page State with localStorage + URL Parameters
|
|
19
20
|
|
|
@@ -292,3 +293,102 @@ Certifications, feature tags, meta rows: `GMP Manufacturing ● ISO 22716 ● Lo
|
|
|
292
293
|
- **Keep `gap-y-*`** on the container for multi-line rhythm when the list wraps.
|
|
293
294
|
- **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
295
|
- `aria-hidden="true"` on the glyph — screen readers announce `●` as "black circle" otherwise.
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
## Drag-to-Reorder Rows
|
|
299
|
+
|
|
300
|
+
Reordering a list by dragging, written against a Softr block's constraints. Four of the five decisions
|
|
301
|
+
below are non-obvious, and each one is a bug if you get it wrong.
|
|
302
|
+
|
|
303
|
+
```jsx
|
|
304
|
+
var [drag, setDrag] = useState(null); // { from, over } while dragging, else null
|
|
305
|
+
var [optimisticOrder, setOptimisticOrder] = useState(null); // ids, post-drop, pre-refetch
|
|
306
|
+
var rowElsRef = useRef([]);
|
|
307
|
+
|
|
308
|
+
/* The pointer is CAPTURED, so no other element receives enter/leave — rects are the only
|
|
309
|
+
thing that can answer "what is under the cursor". Compare against each row's MIDPOINT so
|
|
310
|
+
the row you are over is the one that yields. */
|
|
311
|
+
function dropIndex(count, clientY) {
|
|
312
|
+
for (var i = 0; i < count; i++) {
|
|
313
|
+
var el = rowElsRef.current[i];
|
|
314
|
+
if (!el) continue;
|
|
315
|
+
var r = el.getBoundingClientRect();
|
|
316
|
+
if (clientY < r.top + r.height / 2) return i;
|
|
317
|
+
}
|
|
318
|
+
return count - 1;
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
The handle — never the whole row, so text selection and the row's own buttons keep working:
|
|
323
|
+
|
|
324
|
+
```jsx
|
|
325
|
+
<span
|
|
326
|
+
role="button"
|
|
327
|
+
aria-label={"Drag to reorder " + row.name}
|
|
328
|
+
className="touch-none select-none" // or the browser scrolls instead of dragging
|
|
329
|
+
style={{ cursor: "grab" }}
|
|
330
|
+
onPointerDown={function (e) {
|
|
331
|
+
e.preventDefault();
|
|
332
|
+
try { e.currentTarget.setPointerCapture(e.pointerId); } catch (err) {}
|
|
333
|
+
setDrag({ from: index, over: index });
|
|
334
|
+
}}
|
|
335
|
+
onPointerMove={function (e) {
|
|
336
|
+
if (!drag) return;
|
|
337
|
+
var over = dropIndex(rows.length, e.clientY);
|
|
338
|
+
if (over !== drag.over) setDrag({ from: drag.from, over: over });
|
|
339
|
+
}}
|
|
340
|
+
onPointerUp={function () {
|
|
341
|
+
if (!drag) return;
|
|
342
|
+
var from = drag.from, to = drag.over;
|
|
343
|
+
setDrag(null);
|
|
344
|
+
if (from !== to) {
|
|
345
|
+
var next = rows.slice();
|
|
346
|
+
next.splice(to, 0, next.splice(from, 1)[0]);
|
|
347
|
+
applyOrder(next);
|
|
348
|
+
}
|
|
349
|
+
}}
|
|
350
|
+
onPointerCancel={function () { setDrag(null); }}
|
|
351
|
+
>
|
|
352
|
+
<GripVertical className="h-3.5 w-3.5" />
|
|
353
|
+
</span>
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**Pointer capture, not mouse events.** Capture makes the handle the target of every move and of the up
|
|
357
|
+
*wherever the pointer travels*, and obliges the browser to send `pointercancel` if it takes the pointer
|
|
358
|
+
away. Without it, a drag released over another application never delivers its up and the row stays
|
|
359
|
+
stuck mid-drag until a reload.
|
|
360
|
+
|
|
361
|
+
**Measure rects, don't listen for `onPointerEnter` on each row.** While the pointer is captured, no
|
|
362
|
+
other element gets enter/leave at all, so per-row handlers silently never fire. And
|
|
363
|
+
`document.elementFromPoint` is not the escape hatch — inside a block it returns the shadow host (see
|
|
364
|
+
[anti-patterns.md](anti-patterns.md#layout--styling)).
|
|
365
|
+
|
|
366
|
+
**Draw the insertion line with an INSET box-shadow, never a border.** A real 2px border grows the row
|
|
367
|
+
by 2px and shoves every row below it down a notch, so the list crawls under the pointer as the target
|
|
368
|
+
changes:
|
|
369
|
+
|
|
370
|
+
```jsx
|
|
371
|
+
style={Object.assign({}, ROW_STYLE, isTarget
|
|
372
|
+
? (drag.over < drag.from
|
|
373
|
+
? { boxShadow: "inset 0 2px 0 0 " + ACCENT } // landing above
|
|
374
|
+
: { boxShadow: "inset 0 -2px 0 0 " + ACCENT }) // landing below
|
|
375
|
+
: null)}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Renumber the whole run — never swap a pair.** A swap cannot express "drop three rows up", and on a
|
|
379
|
+
nullable order field it corrupts the sort: positions start null, so numbering only the two rows that
|
|
380
|
+
moved leaves the rest null, and any "nulls last" comparator then throws every untouched row to the
|
|
381
|
+
bottom the moment the user switches to that sort. Write `position = i + 1` for every row whose slot
|
|
382
|
+
actually changed. The first reorder on a fresh list costs N writes; later ones cost the distance
|
|
383
|
+
travelled.
|
|
384
|
+
|
|
385
|
+
**Hold an optimistic order until the refetch lands.** The position writes are in flight while the
|
|
386
|
+
records still carry their OLD numbers, so re-sorting on those throws the row back to where it was
|
|
387
|
+
dragged from for a beat — which reads as the drag having failed. Apply `optimisticOrder` ahead of both
|
|
388
|
+
sorts and clear it when the refetch resolves. Writes stay sequential (`await mutateAsync` per row, in
|
|
389
|
+
order, stop on first failure — see [../datasources/writing.md](../datasources/writing.md#sequential-multi-row-writes-mutateasync));
|
|
390
|
+
on failure, clear the override and refetch, because a half-applied renumber is worse than none.
|
|
391
|
+
|
|
392
|
+
**Only gate the drag on permissions, not on a sort mode.** If the list has an alternative sort, let the
|
|
393
|
+
drag switch to manual order rather than disabling the handle — see the disabled-control note in
|
|
394
|
+
[../ui-ux-guidelines.md](../ui-ux-guidelines.md#26-finishing-touches).
|
package/references/softr-mcp.md
CHANGED
|
@@ -138,8 +138,29 @@ actions open across two blocks.
|
|
|
138
138
|
3. **Read the permissions back with `get_vibe_coding_block_settings` and confirm each one actually
|
|
139
139
|
changed.** A successful-looking sequence is not evidence; the failure is an argument rejection, so
|
|
140
140
|
the call errors rather than lying, but an agent that batches calls can easily miss which one failed.
|
|
141
|
-
4. If any action is still `ALL_USERS`, **
|
|
142
|
-
|
|
141
|
+
4. If any action is still `ALL_USERS`, **report it and let the builder decide.** Check the page's own
|
|
142
|
+
VIEW permission first with `get_page_permissions`, because that is what sets the severity:
|
|
143
|
+
- **Page VIEW is gated** (e.g. `LOGGED_IN_USERS`) — an anonymous visitor cannot load the page at
|
|
144
|
+
all, so exploiting the open action means calling its endpoint directly, and the realistic worst
|
|
145
|
+
case is junk records rather than data exposure or deletion. Housekeeping: worth fixing on the
|
|
146
|
+
next Studio pass, not worth holding a release for.
|
|
147
|
+
- **Page VIEW is `ALL_USERS`** — the action permission is the only gate left. That is a genuine
|
|
148
|
+
hole and deserves to be called one.
|
|
149
|
+
|
|
150
|
+
Report page, block, action type, data source and current group; say which of the two cases applies;
|
|
151
|
+
note that a human sets them on the block's Actions tab in Studio. Then stop — **do not unilaterally
|
|
152
|
+
block the publish.** It is not your app, and the person whose app it is needs the finding and the
|
|
153
|
+
severity, not a veto.
|
|
154
|
+
|
|
155
|
+
One caveat worth stating: page visibility and action permissions are *separate* gates, and whether
|
|
156
|
+
Softr enforces page VIEW on the action endpoint itself is unverified here. The reason to treat a
|
|
157
|
+
gated page as low-severity is the practical difficulty and low blast radius, not a proof that the
|
|
158
|
+
action is unreachable. Say that plainly rather than implying the action is safe.
|
|
159
|
+
|
|
160
|
+
**Calibration matters.** This guidance read "do not publish" in v2.5.1 and immediately fired at
|
|
161
|
+
maximum severity on a logged-in-gated app where the real exposure was junk records. A warning that
|
|
162
|
+
cannot distinguish housekeeping from a breach gets tuned out, and then it is worth nothing on the
|
|
163
|
+
day it matters.
|
|
143
164
|
|
|
144
165
|
Do not improvise around a rejection. `update_vibe_coding_block_settings` is not a substitute: its schema
|
|
145
166
|
is equally empty, it writes far more than one permission, and guessing its payload risks clobbering the
|
package/ui-ux-guidelines.md
CHANGED
|
@@ -719,6 +719,8 @@ Actively check for and reject these fingerprints of generic AI-generated interfa
|
|
|
719
719
|
- **Relative timestamps:** "2 hours ago" via `date-fns/formatDistanceToNow`
|
|
720
720
|
- **Truncate long text** with `truncate` or `line-clamp-2`, full value in `Tooltip`
|
|
721
721
|
- **Sticky headers** for long tables
|
|
722
|
+
- **Never render an affordance you have not wired.** A grip glyph that does not drag, a chevron that does not sort, a card that looks clickable and is not — the signifier IS the promise, and an unfulfilled one reads as a broken feature, not a missing one. Either wire it or delete it. (Observed 2026-09-09: a `GripVertical` shipped as decoration on every row of a reorderable list; users reported the list as "can't be reordered", not as "missing drag".)
|
|
723
|
+
- **A control that is disabled by default is indistinguishable from a broken one.** If the only explanation lives in a `title` tooltip, nobody reads it — they file a bug. When a control depends on a mode the user has not chosen yet, prefer making the action *switch the mode and proceed* over greying it out. Disable only for genuine impossibility (permissions, first row can't move up), and when you do, say why in visible text rather than on hover. (Same 2026-09-09 report: reorder arrows were disabled until you switched the sort to Manual, which nothing on screen told you.)
|
|
722
724
|
|
|
723
725
|
---
|
|
724
726
|
|