softr-vibe-coding 2.4.2 → 2.5.1
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 +2 -1
- package/SKILL.md +51 -0
- package/package.json +1 -1
- package/references/anti-patterns.md +5 -0
- package/references/native-chrome-styling.md +35 -0
- package/references/softr-mcp.md +65 -0
- package/ui-ux-guidelines.md +13 -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.5.1] - 2026-09-09
|
|
8
|
+
- The Action-permission reset has a failure mode that leaves blocks publicly writable — verify, never assume
|
|
9
|
+
|
|
10
|
+
## [2.5.0] - 2026-09-09
|
|
11
|
+
- Cross-block and cross-page consistency: the failure mode where every block looks correct alone
|
|
12
|
+
|
|
7
13
|
## [2.4.2] - 2026-09-01
|
|
8
14
|
- softr-mcp.md: Workflows build/publish loop verified end to end 2026-09-01 (10 production workflows built live via MCP), replacing the not-yet-exercised hedge with build-loop findings: create_workflow instantiates an OLD trigger node version — immediately replace_trigger_node with the same type for current inputs (updateField on SOFTR_TABLES_RECORD_UPDATED only exists at v1.2.0); FILTER conditions set via update_node_inputs inputName "condition" ({operator, conditions} object stored on the outgoing path like the builder); LOOP_ACTION_GROUP loopVariables.items must be a plain array like $.records — [*] projections rejected by the validator, per-item refs inside the loop via {loopActionGroup.<id>:::loopVariables.items.fields.<fieldId>} (bracket form for digit-leading ids); update_node_inputs batches validate against STORED node state so dependent updates split into sequential calls; test-safety doctrine — record-write nodes REAL_ONLY (never test against production), SOFTR_SEND_EMAIL always mode:"mock", triggers/GET_RECORDS REAL_ONLY but read-only-safe. Plus: workspace update_field on SELECT silently drops added choices (echoes old set, no error) — add choices in Studio or let allowToAddNewChoice auto-create on first unknown-label write; joins the allowMultipleEntries silent-ignore
|
|
9
15
|
|
package/README.md
CHANGED
|
@@ -291,7 +291,8 @@ The skill enforces these automatically, but good to know (verified live against
|
|
|
291
291
|
- Create payloads are **flat**; update payloads are `{ recordId, fields: {...} }` — asymmetric by design
|
|
292
292
|
- `mutateAsync` is fully supported — it's the tool for sequential multi-row saves
|
|
293
293
|
- SELECT fields write by option **label string**; linked records write as arrays of record-id strings
|
|
294
|
-
- Every code recompile resets the block's auto-registered Actions to default permissions — tighten permissions after the last redeploy
|
|
294
|
+
- Every code recompile resets the block's auto-registered Actions to default permissions — tighten permissions after the last redeploy, and note there is **no cosmetic-edit exemption**: an edit that changes only a comment resets them too. **Always read the permissions back to confirm** — the default for a create action is `ALL_USERS` (publicly writable), and the MCP call that re-tightens it can fail with no fallback
|
|
295
|
+
- **Blocks cannot import each other**, so two blocks that must look alike will drift — each one looks correct in isolation while the set does not. Repeated page chrome (back button, title, primary action) must sit at the same offset on every page, and a loading skeleton must track the REST state of whatever it stands in for
|
|
295
296
|
- No `import React from 'react'` — use named imports (`import { useState } from "react"`)
|
|
296
297
|
- Must use `export default function Block()`
|
|
297
298
|
- 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
|
package/SKILL.md
CHANGED
|
@@ -222,6 +222,25 @@ Blocks rarely live alone — most Softr pages stack 2–4 blocks vertically, oft
|
|
|
222
222
|
1. **"Should the detail page have a back button?"** — if yes, always wire one. Use the back-navigation pattern in [references/helper-blocks.md](references/helper-blocks.md#breadcrumb--back-navigation): `window.history.back()` for users with history, plus a fallback URL for users who arrived via shared link.
|
|
223
223
|
2. **"What page should the back button fall back to when there's no history?"** — this is a separate question, easy to skip but important. Don't default silently; ask. If the user doesn't have a listing page yet, default to `/` and leave a `// TODO: update fallback when /jobs (or similar) exists` comment so it can be updated later.
|
|
224
224
|
|
|
225
|
+
**Chrome that repeats across pages must land in the SAME place on every page [house].** A back button,
|
|
226
|
+
a page title, a primary action -- anything the user meets on more than one screen -- is a cross-page
|
|
227
|
+
contract, not a per-block decision. Before adding one, open the blocks that already have it and copy
|
|
228
|
+
the exact offset; when you change it, change it everywhere in the same edit. Verified the hard way
|
|
229
|
+
2026-09-09: an item-detail back button carried an extra `mt-6` that the project-header back button did
|
|
230
|
+
not, so it sat 24px lower, and the mismatch only surfaced when a user moved between the two pages in
|
|
231
|
+
one session. **No amount of reading a single block reveals this** -- each block looks correct alone,
|
|
232
|
+
which is exactly why it needs to be a standing rule rather than a review item.
|
|
233
|
+
|
|
234
|
+
Two habits make it survive:
|
|
235
|
+
|
|
236
|
+
- **Let the wrapper's padding be the ONLY thing positioning repeated chrome.** Give the back button
|
|
237
|
+
`mb-4` (space below it) and no top margin, so its offset is the wrapper's top padding and nothing
|
|
238
|
+
else. One number per page then governs the position, and pages can only drift if their wrappers do.
|
|
239
|
+
- **Loading skeletons repeat the chrome too.** A skeleton standing in for a page that has a back button
|
|
240
|
+
needs that button's placeholder at the same offset as the real one, or the button visibly jumps the
|
|
241
|
+
moment the record arrives. Change both in the same edit -- see §12 of
|
|
242
|
+
[ui-ux-guidelines.md](ui-ux-guidelines.md).
|
|
243
|
+
|
|
225
244
|
**Persist the answer as a grep-able comment at the top of the generated file** so future edits know the spacing assumptions and can be updated consistently:
|
|
226
245
|
|
|
227
246
|
```jsx
|
|
@@ -323,6 +342,16 @@ Adjust the top gradient color to complement the user's brand.
|
|
|
323
342
|
|
|
324
343
|
**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).)
|
|
325
344
|
|
|
345
|
+
**A brand colour you use BOTH ways exists twice, and the two copies drift silently.** Arbitrary values
|
|
346
|
+
are resolved at build time, so a class string can never read your `C.accent` constant. A card styled
|
|
347
|
+
with Tailwind (`border-[#3B1F2B] hover:border-[#54594F]`) and its loading skeleton styled inline
|
|
348
|
+
(`style={{ border: "1px solid " + C.accent }}`) therefore hold the same colour in two places that no
|
|
349
|
+
compiler will ever reconcile, and nothing fails when they disagree -- it just looks wrong. Verified
|
|
350
|
+
2026-09-09: a card's rest border was changed and its skeleton's was not, so the whole grid visibly
|
|
351
|
+
re-outlined itself the instant the data arrived. Two habits keep it honest: write the hex-to-token
|
|
352
|
+
mapping in a comment beside the class string (`#3B1F2B = C.accent`), and prefer the runtime token
|
|
353
|
+
wherever inline `style` is already in play, so only one of the two copies is ever a literal.
|
|
354
|
+
|
|
326
355
|
**Font classes:** `font-heading`, `font-sans`, `font-mono`
|
|
327
356
|
|
|
328
357
|
**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.
|
|
@@ -517,6 +546,28 @@ Non-negotiable rules. Most are enforced by the Softr platform (compiler, validat
|
|
|
517
546
|
auto-registered Actions to **default permissions**. Do the Actions-tab permission tightening pass
|
|
518
547
|
only after the LAST redeploy, and re-check it after any future one. Verified live 2026-08-25
|
|
519
548
|
across a 15-block deployment. See [datasources/writing.md](datasources/writing.md#how-actions-work-studios-actions-tab).
|
|
549
|
+
**Any save counts, including one whose only change is a comment** -- there is no "cosmetic edit"
|
|
550
|
+
exemption; a `search_replace` that rewrites nothing but a code comment rebuilds the Actions exactly
|
|
551
|
+
like a full rewrite does (verified live 2026-09-09, on two blocks at once).
|
|
552
|
+
**Re-read the permissions after EVERY push and confirm they actually changed** -- do not assume the
|
|
553
|
+
restore worked. Softr's default for a `genericActions` ADD_RECORD is `ALL_USERS`, i.e. writable by
|
|
554
|
+
logged-OUT visitors, and the MCP call that re-tightens it (`set_vibe_coding_block_action_visibility`)
|
|
555
|
+
can itself fail with no fallback (see the array-argument quirk in
|
|
556
|
+
[references/softr-mcp.md](references/softr-mcp.md#the-array-argument-serialization-quirk-and-why-it-is-a-security-issue)).
|
|
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`, do not publish** -- surface the list and have a human set it
|
|
559
|
+
in Studio.
|
|
560
|
+
22. **Blocks cannot import each other -- cross-block consistency is discipline, not architecture [house]**
|
|
561
|
+
-- Every block compiles standalone. There is no shared module, no design-system import, nothing that
|
|
562
|
+
makes two blocks stay alike. Two blocks that must look the same WILL drift, and the drift usually
|
|
563
|
+
hides in the *mechanism* rather than the value: verified 2026-09-09, a report card and a saved-list
|
|
564
|
+
card that were supposed to be identical had ended up hovering by two different mechanisms (Tailwind
|
|
565
|
+
`hover:` variants on one, React `useState` on the other), so a change to either could never reach
|
|
566
|
+
the other. Neither block looked wrong on its own -- that is the whole problem. When a treatment is
|
|
567
|
+
deliberately shared across blocks: name it in a comment in BOTH files, list the exact tokens that
|
|
568
|
+
are shared, and promise nothing beyond them (the class strings usually differ in layout and padding,
|
|
569
|
+
and do not need to match). The same rule governs repeated page chrome -- see **Block Placement &
|
|
570
|
+
Page Spacing**.
|
|
520
571
|
|
|
521
572
|
## Style Conventions
|
|
522
573
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "softr-vibe-coding",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
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"
|
|
@@ -38,6 +38,8 @@ Run through this catalog before delivering any block. Every row is a violation o
|
|
|
38
38
|
| Wrapping a `useRecordCreate` payload in `{ fields: {...} }` (copied from an update call) | Create payloads are **FLAT** — `createRecord.mutate({ name: "Jane" })`. Only update payloads nest: `{ recordId, fields: {...} }`. The asymmetry is by design (verified 2026-08-25) |
|
|
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
|
+
| 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. **Do not publish** until they read `LOGGED_IN_USERS`; escalate to a human with Studio access |
|
|
41
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 |
|
|
42
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) |
|
|
43
45
|
|
|
@@ -66,6 +68,9 @@ Run through this catalog before delivering any block. Every row is a violation o
|
|
|
66
68
|
| Hardcoded domain in navigation | Relative paths: `/task-details?recordId=...` |
|
|
67
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 |
|
|
68
70
|
| Emojis in UI | lucide-react icons only |
|
|
71
|
+
| 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
|
+
| 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
|
+
| `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 |
|
|
69
74
|
| `[&_svg]:opacity-0` on SelectTrigger | `<style>` + `data-fix-chevron` attribute (Softr bundler limitation) |
|
|
70
75
|
| 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. |
|
|
71
76
|
| 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. **Second exception — DARK brands: see the next row.** |
|
|
@@ -156,6 +156,41 @@ footer [role="list"] > div > div {
|
|
|
156
156
|
}
|
|
157
157
|
```
|
|
158
158
|
|
|
159
|
+
## Relocating footer nodes, not just restyling them
|
|
160
|
+
|
|
161
|
+
CSS alone cannot move an element to a **different parent**. `order` only reorders siblings, so a
|
|
162
|
+
"put the Website / Terms links down on the copyright line" request — where the links and the copyright
|
|
163
|
+
live in different containers — needs a small JS relocation job in the same Custom Code block, not more
|
|
164
|
+
selectors.
|
|
165
|
+
|
|
166
|
+
The shape that works:
|
|
167
|
+
|
|
168
|
+
1. Build one flex row, insert it where the copyright currently sits.
|
|
169
|
+
2. Move the copyright node into it, then the link nodes after it.
|
|
170
|
+
3. Hide the container the links vacated **only if it is now genuinely empty** — check for remaining
|
|
171
|
+
element children rather than assuming, or you will blank a container that still holds something.
|
|
172
|
+
4. Run it BEFORE any job that measures height (a footer-compacting pass, a sticky offset calculation);
|
|
173
|
+
those must see the final arrangement or they measure the old one.
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
/* relocate, then compact — order matters: compactFooter() measures height */
|
|
177
|
+
relocateFooterLinks();
|
|
178
|
+
compactFooter();
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Two details worth copying:
|
|
182
|
+
|
|
183
|
+
- **Truncate rather than wrap.** A relocated row should ellipsise under width pressure
|
|
184
|
+
(`min-width:0` on the flex children plus `text-overflow:ellipsis`), never stack — a footer row that
|
|
185
|
+
reflows to two lines at an awkward width looks broken in a way a clipped label does not.
|
|
186
|
+
- **Scope hover underlines away from logos and icons.** An `::after` underline scaled from
|
|
187
|
+
`transform-origin:50%` gives a centre-out grow; gate it with `:not(:has(svg)):not(:has(img))` so it
|
|
188
|
+
never appears under the logo or the social glyphs, and use `currentColor` so the footer's own colour
|
|
189
|
+
rules keep working untouched.
|
|
190
|
+
|
|
191
|
+
Same caveat as everything else here: **it renders on the published app only.** The Studio editor keeps
|
|
192
|
+
showing the old arrangement, which reads exactly like "the script did not run."
|
|
193
|
+
|
|
159
194
|
## Page background
|
|
160
195
|
|
|
161
196
|
**The trickiest one — Softr paints the SAME fill on FOUR stacked layers:** `html`, `body`, `#page-content` (stable id; classes `content spr-content-root`), AND a deeper **class-less wrapper div** nested a few levels inside `#page-content`. Style any one layer and the ones above cover it — this is why setting `body` alone appears to "do nothing."
|
package/references/softr-mcp.md
CHANGED
|
@@ -83,6 +83,71 @@ Before writing any block code through the MCP, call `get_vibe_coding_docs` — i
|
|
|
83
83
|
|
|
84
84
|
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.
|
|
85
85
|
|
|
86
|
+
### Which edit tool: full replace vs. targeted search-replace
|
|
87
|
+
|
|
88
|
+
`update_vibe_coding_block_code` sends the whole file; `update_vibe_coding_block_code_search_replace`
|
|
89
|
+
sends only the fragments that change. This is not just a bandwidth choice — it changes what can go wrong.
|
|
90
|
+
|
|
91
|
+
**Reach for search-replace when ONE source file is deployed to SEVERAL blocks.** Datasource UUIDs are
|
|
92
|
+
per BLOCK, not per table, so a file living on two pages needs a different `datasource.define()` pair in
|
|
93
|
+
each. A full replace overwrites that pair and forces a manual swap on every single push — the classic
|
|
94
|
+
way to point a project page at the company page's data. Targeted replacements never touch lines you did
|
|
95
|
+
not name, so **each block keeps its own pair and the swap step disappears entirely** (verified
|
|
96
|
+
2026-09-09 across a report block deployed to two pages). It is also the safer option on large files:
|
|
97
|
+
retransmitting ~100KB verbatim to change one class string is its own corruption risk.
|
|
98
|
+
|
|
99
|
+
**Reach for the full replace when the change is structural** — reordering JSX, moving logic between
|
|
100
|
+
components, adding a hook — where being sure of "the exact current text" of a dozen scattered fragments
|
|
101
|
+
is harder than being sure of the whole file. Also use it when the local file is the source of truth and
|
|
102
|
+
has drifted from the deployed block in ways you have not enumerated.
|
|
103
|
+
|
|
104
|
+
### The array-argument serialization quirk, and why it is a security issue
|
|
105
|
+
|
|
106
|
+
**Several tools on this server take an array argument, and some MCP clients serialize it as a JSON
|
|
107
|
+
*string* instead.** The API then rejects it with a Jackson error before it reaches any business logic:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
Cannot deserialize value of type `java.util.ArrayList<java.util.Map<String,Object>>`
|
|
111
|
+
from String value (token `JsonToken.VALUE_STRING`)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Root cause: the server advertises an **empty schema** for its tools (`{"type":"object"}`, no property
|
|
115
|
+
definitions), so a client has no type information to serialize against. Confirmed 2026-09-09 on:
|
|
116
|
+
|
|
117
|
+
| Tool | Array argument | Fallback if it fails |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `update_vibe_coding_block_code_search_replace` | `operations` | Use `update_vibe_coding_block_code` (full replace) |
|
|
120
|
+
| `set_vibe_coding_block_action_visibility` | `updates` | **NONE — a human must fix it in Studio** |
|
|
121
|
+
|
|
122
|
+
It is intermittent, and that is the trap: on 2026-09-09 both tools accepted the array early in a
|
|
123
|
+
session and rejected it an hour later, same shapes, same session. Do not conclude from one success
|
|
124
|
+
that the path is reliable for the rest of your work.
|
|
125
|
+
|
|
126
|
+
**Why the second row is a security problem, not an inconvenience.** Every code push resets the block's
|
|
127
|
+
auto-registered Actions to Softr's defaults, and the default for a `genericActions` **ADD_RECORD is
|
|
128
|
+
`ALL_USERS`** — writable by logged-OUT visitors. The documented remedy is to re-tighten with
|
|
129
|
+
`set_vibe_coding_block_action_visibility`. When that call is the one that fails, a routine cosmetic push
|
|
130
|
+
silently leaves public write access on the block, and nothing in the push result says so: the push
|
|
131
|
+
itself returns `errors: null, warnings: null`. Verified live 2026-09-09 — one push left four ADD_RECORD
|
|
132
|
+
actions open across two blocks.
|
|
133
|
+
|
|
134
|
+
**So treat permission restoration as a step that must be VERIFIED, never assumed:**
|
|
135
|
+
|
|
136
|
+
1. Push the code.
|
|
137
|
+
2. Call `set_vibe_coding_block_action_visibility` for every action that needs tightening.
|
|
138
|
+
3. **Read the permissions back with `get_vibe_coding_block_settings` and confirm each one actually
|
|
139
|
+
changed.** A successful-looking sequence is not evidence; the failure is an argument rejection, so
|
|
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`, **do not publish.** Report the exact list — page, block, action
|
|
142
|
+
type, data source — and have a human set them on the block's Actions tab in Studio.
|
|
143
|
+
|
|
144
|
+
Do not improvise around a rejection. `update_vibe_coding_block_settings` is not a substitute: its schema
|
|
145
|
+
is equally empty, it writes far more than one permission, and guessing its payload risks clobbering the
|
|
146
|
+
block's data source connections. Restoring an older block version is not a substitute either — it
|
|
147
|
+
reverts the code along with the permissions, undoing the change you just pushed.
|
|
148
|
+
|
|
149
|
+
Both edit paths recompile, so both reset Action permissions either way (Hard Constraint 21).
|
|
150
|
+
|
|
86
151
|
## Adopting Studio-AI-generated code
|
|
87
152
|
|
|
88
153
|
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:
|
package/ui-ux-guidelines.md
CHANGED
|
@@ -390,6 +390,15 @@ Vestibular disorders affect ~35% of adults over 40. Always respect `prefers-redu
|
|
|
390
390
|
- Use `<Skeleton />` from shadcn/ui matching the final layout shape.
|
|
391
391
|
- Pulsing animation is included by default.
|
|
392
392
|
- Replace content progressively as data arrives.
|
|
393
|
+
- **Match the component's REST state, not just its shape.** A skeleton stands in for a real card, so it
|
|
394
|
+
must carry that card's *resting* border, background and padding -- not its hover state, and not a
|
|
395
|
+
different border "because it is only a placeholder". When the two disagree the grid visibly re-draws
|
|
396
|
+
itself the instant data lands, which is the one thing a skeleton exists to prevent (observed live
|
|
397
|
+
2026-09-09: skeletons left on the old border colour after the cards moved to a new one).
|
|
398
|
+
- **When an interactive element's resting style changes, change its skeleton in the same edit.** Nothing
|
|
399
|
+
links them -- in a Tailwind + inline-token codebase the colour is literally written twice.
|
|
400
|
+
- **Skeletons repeat page chrome too.** If the page has a back button, breadcrumb or title above the
|
|
401
|
+
content, the skeleton needs those at the SAME offsets, or the chrome jumps when the record arrives.
|
|
393
402
|
|
|
394
403
|
### Perceived Performance:
|
|
395
404
|
- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Use for low-stakes actions (likes, filters); avoid for payments or destructive operations.
|
|
@@ -550,6 +559,7 @@ Avoid the "hero metric layout template" — big number, small label, supporting
|
|
|
550
559
|
- **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.
|
|
551
560
|
- Semantic HTML: `<button>` for actions, `<a>` for navigation, `<input>` for data.
|
|
552
561
|
- **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.
|
|
562
|
+
- **`focus-within:` for containers, `focus-visible:` for the focusable element itself.** A card or row that *holds* buttons is usually a plain `<div>` and never takes focus, so `focus-visible:` on it is dead CSS that reads like an accessibility feature and does nothing. Use `focus-within:` there, so tabbing to a control inside the card lights the same affordance a mouse user gets from `hover:` -- and keep `focus-visible:ring-2` on the button or link itself. Whenever a card has a `hover:` treatment and contains tab stops, it wants the matching `focus-within:` variant.
|
|
553
563
|
- Tables: proper `<thead>`, `<tbody>`, `<th scope="col">`.
|
|
554
564
|
|
|
555
565
|
---
|
|
@@ -699,6 +709,9 @@ Actively check for and reject these fingerprints of generic AI-generated interfa
|
|
|
699
709
|
|
|
700
710
|
- **Smooth transitions:** `transition-all duration-200` on cards and rows
|
|
701
711
|
- **Hover states on rows/cards:** `hover:bg-muted/50` for interactivity indication
|
|
712
|
+
- **Rest and hover compete for the same colour, and the RESTING state wins.** If a card's resting border already uses your strongest colour, hover has nothing left to change to -- the answer is a *third* colour for hover, never demoting the resting state to free one up. A resting border is permanent and seen by everyone; a hover border exists only for whoever has a pointer on it. (Observed 2026-09-09: freeing the strong colour for hover left the cards reading as unbordered at rest.)
|
|
713
|
+
- **Move hover in the same direction as the rest of the hover treatment.** If the background lightens on hover, the border should lighten too -- a border that hardens while the background softens reads as two effects fighting rather than one card lifting.
|
|
714
|
+
- **Check border colours against the surface they actually sit on.** A hairline tuned for contrast *inside* a white card disappears where that card meets a coloured page ground; the same hex that reads correctly between table rows can be invisible at the card's outer edge.
|
|
702
715
|
- **Auto-focus first field** when form opens (`autoFocus`)
|
|
703
716
|
- **Escape to cancel** any modal, dialog, or inline edit
|
|
704
717
|
- **Confirmation animation:** Brief `Check` icon in save button after success
|