softr-vibe-coding 1.11.2 → 1.12.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,11 @@ 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.12.0] - 2026-07-22
8
+ - Bump to 1.12.0 — multi-datasource blocks, Airtable rating writability, dark-brand canvas + page-background anti-patterns
9
+ - Document the Airtable rating field as writable (plain integer 0-max; 0 clears it, so AVERAGE formulas skip it) — it was absent from the Supported Fields table, which made it the main unknown when building a star-rating form. Add two dark-brand styling anti-patterns: a block on a dark brand MUST paint its own backgroundColor because custom-code-header's body rule doesn't cross the shadow-DOM boundary and a default Softr page is white, so white text/logos/buttons render invisibly on white (the existing don't-double-paint row assumed a light app and inverts here); and setting html,body alone doesn't change the page background because Softr paints the same fill on FOUR stacked layers — html, body, #page-content, and a class-less wrapper div inside it — so paint html then clear the duplicates, excluding the .softr-topbar subtree
10
+ - Add datasources/multi-datasource.md — a block can now connect to SEVERAL data sources: datasource.define({alias: 'uuid'}) plus a from: parameter on every data hook (useRecords, useRecord, useLinkedRecords, useFieldOptions, useMetric, useChartData, useRecordCreate/Update/Delete; not useUpload/useCurrentRecordId, which are app-level). Omitting from: throws once >1 source is connected; with exactly one you can skip define entirely. The define() values must be INLINE STRING LITERALS — hoisting them into constants fails to compile with 'datasource.define() object values must be string literals', same static-analysis rule as q.select(). Ids are plain UUIDs, not the table id and not the ds_id_N placeholder shape in Softr's docs; obtain them by asking Studio's AI chat to WRITE CODE, never to recite a value — asked three times in prose for the same three connected tables it returned three different confidently-worded sets, once recycling another table's uuid. This supersedes the one-table-per-block limit: relax Hard Constraint #13 to one useRecords per DATASOURCE, reframe helper-blocks.md around genuinely cross-BLOCK jobs (triggering another block, sharing computed state) with the old rationale kept as history, and retarget the useLinkedRecords anti-pattern at a second datasource rather than a helper
11
+
7
12
  ## [1.11.2] - 2026-07-08
8
13
  - Bump to 1.11.2 — publish recordId-less useRecord note and input.textAsync correction
9
14
  - Correct input.textAsync in airtable-automations.md — it is not a real method in EITHER Airtable scripting environment; calling it in the Scripting Extension throws TypeError: input.textAsync is not a function. buttonsAsync is the only interactive runtime prompt; free-text values (e.g. an API key) go through an input.config({...}) setting at the top or a hardcoded constant. Also generalize the Automation Scripts bullet to "no interactive prompts".
package/README.md CHANGED
@@ -204,7 +204,9 @@ softr-vibe-coding/
204
204
 
205
205
  └── datasources/ # Data source guides (loaded on demand)
206
206
  ├── overview.md # Comparison matrix, selection guide
207
- ├── shared-patterns.md # Index → reading, writing, fields
207
+ ├── shared-patterns.md # Index → multi-datasource, reading, writing, fields
208
+ ├── multi-datasource.md # Several data sources in ONE block: datasource.define(),
209
+ │ # the from: parameter, getting the datasource UUIDs
208
210
  ├── reading.md # useRecords, filtering, sorting, pagination,
209
211
  │ # metrics, charts, current user (198 lines)
210
212
  ├── writing.md # Mutations, uploads, linked record format,
@@ -263,7 +265,7 @@ The skill enforces these automatically, but good to know:
263
265
  - No arrow functions in JSX callback props — use `function() {}`
264
266
  - Must use `export default function Block()`
265
267
  - Must wrap layout in `<div className="container py-6"><div className="content">`
266
- - Only ONE `useRecords` call per block (use helper blocks for multi-table)
268
+ - Only ONE `useRecords` call per **datasource** — but a block can connect to several sources; declare them with `datasource.define()` and pass `from:` on every hook
267
269
  - `fetchNextPage` only inside `useEffect` — in render body causes infinite loops
268
270
  - All hooks declared before any conditional `return` — React error #310
269
271
  - Every field value rendered in JSX must pass through `getFieldValue()`
package/SKILL.md CHANGED
@@ -141,6 +141,8 @@ Softr supports 14 data sources. **Before writing any data-fetching code, read th
141
141
  | SQL Database | [datasources/sql-database.md](datasources/sql-database.md) | `useRecords` + `q.select()` |
142
142
  | REST API | [datasources/rest-api.md](datasources/rest-api.md) | `useProxyFetch` + `useQuery` |
143
143
 
144
+ **A block can connect to MORE THAN ONE of these at a time.** Declare them with `datasource.define({ alias: "uuid" })` and pass `from: ds.alias` on every data hook — read two tables and write to a third from a single block. Required reading before building anything multi-table: [datasources/multi-datasource.md](datasources/multi-datasource.md). (This replaces the old one-table-per-block limit and the invisible-helper-block workaround.)
145
+
144
146
  For shared data fetching patterns (useRecords, mutations, uploads, metrics, charts), see [datasources/shared-patterns.md](datasources/shared-patterns.md).
145
147
 
146
148
  For data source comparison and selection guidance, see [datasources/overview.md](datasources/overview.md).
@@ -151,7 +153,8 @@ For advanced patterns beyond data fetching, load the relevant reference when the
151
153
 
152
154
  | If the task involves... | Load reference |
153
155
  |---|---|
154
- | Cross-block communication, multi-table data access, invisible helper blocks, window globals, breadcrumbs | [references/helper-blocks.md](references/helper-blocks.md) |
156
+ | Reading/writing **several tables from one block** `datasource.define()`, the `from:` parameter, obtaining the datasource UUIDs (and why Studio's chat invents them) | [datasources/multi-datasource.md](datasources/multi-datasource.md) |
157
+ | Cross-*block* communication, window globals, breadcrumbs, publishing shared computed state. *(Multi-table reads no longer need a helper — use a second datasource.)* | [references/helper-blocks.md](references/helper-blocks.md) |
155
158
  | Embedding third-party libraries with their own CSS (Leaflet, Mapbox, TinyMCE, Quill, FullCalendar) | [references/advanced-integrations.md](references/advanced-integrations.md) |
156
159
  | Debugging a broken block, checking patterns before delivery, full violation catalog | [references/anti-patterns.md](references/anti-patterns.md) |
157
160
  | Quick syntax check — import paths, hook signatures, mutation call shapes, field mapping | [references/quick-reference.md](references/quick-reference.md) |
@@ -446,9 +449,19 @@ Non-negotiable rules enforced by the Softr platform:
446
449
  10. **No optional chaining or nullish coalescing** — Softr's bundler fails on `?.` and `??`. Use:
447
450
  - `(user && user.email) || ""` instead of `user?.email ?? ""`
448
451
  - `(data && data.pages) ? data.pages.flatMap(function(p) { return p.items; }) : []`
452
+
453
+ ⚠️ **Possibly stale — do not relax on this note alone.** July 2026: a block scaffolded by Studio's own
454
+ AI assistant used `peopleData?.pages` and `status?.label` and rendered correctly in a published app,
455
+ which contradicts this rule. That's a single observation and the failure mode is a block that won't
456
+ compile, so the rule stands until someone re-tests it deliberately. Writing `&&` costs nothing and
457
+ works under either behaviour. If you confirm `?.` compiles reliably, update this constraint, the
458
+ self-validation checklist, and the Style Conventions note together.
449
459
  11. **Airtable: use column names, not fld... IDs** — See [datasources/airtable.md](datasources/airtable.md).
450
460
  12. **Record fields nested under `fields`** — Access via `record.fields.alias`, not `record.alias`.
451
- 13. **ONE `useRecords` per block** — Filter client-side. Multiple `useMetric` calls OK.
461
+ 13. **ONE `useRecords` per datasource** — filter client-side rather than issuing several queries against
462
+ the same table. A block CAN connect to multiple data sources and call `useRecords` once per source;
463
+ declare them with `datasource.define()` and pass `from:` on every hook. See
464
+ [datasources/multi-datasource.md](datasources/multi-datasource.md). Multiple `useMetric` calls OK.
452
465
  14. **React functional components only** — No class components.
453
466
  15. **Do NOT `import React from 'react'`** — Use named imports for hooks.
454
467
  16. **No CSS modules or styled-components** — Tailwind only.
@@ -465,7 +478,7 @@ The conventions below improve consistency across the skill's examples but are NO
465
478
  - Prefer `function() {}` over arrow functions in JSX callbacks and component props
466
479
  - Field-value helper property priority: `label` -> `name` -> `title`
467
480
 
468
- If you're editing a block originally generated by Softr's AI assistant, you can convert to skill style for consistency or leave the AI's syntax as-is. Both produce a working block. Only `?.` and `??` (Hard Constraint #10) are actual bundler blockers — verified by direct experiment, April 2026.
481
+ If you're editing a block originally generated by Softr's AI assistant, you can convert to skill style for consistency or leave the AI's syntax as-is. Both produce a working block. Only `?.` and `??` (Hard Constraint #10) are actual bundler blockers — verified by direct experiment, April 2026. *(But see the caveat on constraint #10: a Studio-scaffolded block using `?.` rendered fine in July 2026. Keep avoiding it until that's deliberately re-tested.)*
469
482
 
470
483
  ## Anti-Patterns Checklist
471
484
 
@@ -120,6 +120,7 @@ This script reads only **metadata**, never records. To inspect record contents i
120
120
  | Number | Yes | |
121
121
  | Date | Yes | |
122
122
  | Checkbox | Yes | |
123
+ | Rating | Yes | Star field. **Writable** — send a plain integer `0`–`max`; `0` clears it (Airtable treats a cleared rating as empty, so `AVERAGE` formulas skip it). Reads back as a number. Verified by direct experiment, July 2026: a create Action with three `rating` fields derived cleanly and wrote all three. |
123
124
  | Single Select | Yes | Returns as `{ label, id }` object. Use `getFieldValue()` helper to extract the label. |
124
125
  | Multiple Select | Yes | Array of `{ label, id }` objects |
125
126
  | Attachment | Yes | Array of `{ filename, id, type, url }` objects |
@@ -0,0 +1,106 @@
1
+ # Multiple Data Sources in One Block
2
+
3
+ A Vibe Coding block can connect to **several data sources at once**. Declare them with
4
+ `datasource.define()` and target one per hook with `from:`.
5
+
6
+ This supersedes the old one-table-per-block limit. Blocks that needed a second table used to
7
+ require an invisible helper block publishing to a `window` global — that workaround is no
8
+ longer necessary for plain multi-table reads. See [../references/helper-blocks.md](../references/helper-blocks.md)
9
+ for what helper blocks are still genuinely for.
10
+
11
+ ## The pattern
12
+
13
+ ```jsx
14
+ import { datasource, useRecords, useRecordCreate, q } from "@/lib/datasource";
15
+
16
+ var ds = datasource.define({
17
+ people: "74d2cbfd-f2cb-4f5c-82d9-0d3a0651e531",
18
+ shifts: "ec7a6311-f6c3-4c99-881d-aae308148716",
19
+ feedback: "52461ab9-9912-4e15-bcf4-8838d38c64ea",
20
+ });
21
+
22
+ var peopleSelect = q.select({ email: "Email", firstName: "First name" });
23
+ var shiftSelect = q.select({ jobCode: "Job Code" });
24
+ var feedbackCreateFields = q.select({ comments: "Comments", crewMember: "Crew Member" });
25
+
26
+ export default function Block() {
27
+ var people = useRecords({ from: ds.people, select: peopleSelect, count: 20 });
28
+ var shifts = useRecords({ from: ds.shifts, select: shiftSelect, count: 5 });
29
+
30
+ var createFeedback = useRecordCreate({
31
+ from: ds.feedback,
32
+ fields: feedbackCreateFields,
33
+ onSuccess: function () { /* … */ },
34
+ });
35
+ // …
36
+ }
37
+ ```
38
+
39
+ **`from:` is required on every data hook once a block has more than one source.** Omitting it
40
+ throws. With exactly one source you can skip `datasource.define` and omit `from` entirely —
41
+ the hooks default to that source.
42
+
43
+ Applies to: `useRecords`, `useRecord`, `useLinkedRecords`, `useFieldOptions`, `useMetric`,
44
+ `useChartData`, `useRecordCreate`, `useRecordUpdate`, `useRecordDelete`.
45
+
46
+ Does **not** apply to `useUpload` and `useCurrentRecordId` — those are app-level and take no `from`.
47
+
48
+ ## The values must be inline string literals
49
+
50
+ Softr statically analyses `datasource.define()`, exactly like `q.select()`. Hoisting the ids
51
+ into constants fails to compile:
52
+
53
+ ```jsx
54
+ // WRONG — "datasource.define() object values must be string literals"
55
+ var PEOPLE_DS_ID = "74d2cbfd-…";
56
+ var ds = datasource.define({ people: PEOPLE_DS_ID });
57
+
58
+ // CORRECT — literals, in place
59
+ var ds = datasource.define({ people: "74d2cbfd-…" });
60
+ ```
61
+
62
+ The error text is explicit, so this one fails fast rather than silently — but it's an easy
63
+ reflex to hoist "magic strings" into named constants, and that reflex is wrong here.
64
+
65
+ ## Getting the datasource ids — ask for CODE, never for a value
66
+
67
+ The id is a plain **UUID**. It is *not* the underlying table id (`tbl…` in Airtable), and not
68
+ the `ds_id_1` shape used as a placeholder in Softr's own developer guide.
69
+
70
+ **Studio's AI chat fabricates these when asked in prose.** Verified 2026-07-22: asked three
71
+ times for the ids of the same three connected tables, it returned three different sets, once
72
+ recycling a previously-mentioned table's uuid for a different table. All three answers were
73
+ confidently worded. None were flagged as uncertain.
74
+
75
+ Ask it to **write code** instead:
76
+
77
+ ```
78
+ Write a datasource.define call covering every data source connected to this
79
+ block, plus one useRecords per source. Output code only.
80
+ ```
81
+
82
+ Code generation is bound to the block's real connections, so the ids come out correct — the
83
+ same reason Softr's assistant reliably inlines select-option UUIDs when scaffolding a form but
84
+ invents them when asked to recite one.
85
+
86
+ **Then verify by running it.** The scaffold renders a list per source; if real rows appear
87
+ under each heading, every alias maps to the table you think it does. A wrong uuid fails safe
88
+ (it matches no datasource, so the block errors) — but a *swapped* pair of correct uuids does
89
+ not, and only running it will catch that.
90
+
91
+ ## When you still want a helper block
92
+
93
+ Multi-datasource removes the need for helpers as a *data-access* workaround. They remain the
94
+ right tool for:
95
+
96
+ - **Cross-block communication** — one block triggering or feeding another on the same page.
97
+ - **Publishing computed state** — expensive derivations shared by several consumers.
98
+ - **Rich foreign data via `useLinkedRecords`** — that hook still only returns `{id, title}`
99
+ and silently ignores extra fields in `select`. Reading the foreign table directly with its
100
+ own `from:` is now the simpler fix.
101
+
102
+ ## Worked example
103
+
104
+ `crew-feedback-form.jsx` — a public feedback form that reads a person from **People** by an
105
+ `email` URL param, resolves a **Shifts** record from a job-code param, and writes a row to
106
+ **Feedback** linking both. One block, three sources, no helpers, no `window` globals.
@@ -56,7 +56,9 @@ var isRefetching = result.isRefetching;
56
56
  var items = (data && data.pages) ? data.pages.flatMap(function(p) { return p.items; }) : [];
57
57
  ```
58
58
 
59
- **CRITICAL:** Only ONE `useRecords` call per block. Fetch all data in one call and filter client-side. Multiple `useMetric` calls ARE allowed.
59
+ **CRITICAL:** Only ONE `useRecords` call **per datasource**. Fetch that table's data in one call and filter client-side. Multiple `useMetric` calls ARE allowed.
60
+
61
+ A block can connect to **several data sources** and call `useRecords` once per source — declare them with `datasource.define()` and pass `from:` on every hook. See [multi-datasource.md](multi-datasource.md). (This replaces the old one-table-per-block limit; blocks no longer need an invisible helper block just to read a second table.)
60
62
 
61
63
  ### Loading All Records (Auto-Pagination)
62
64
 
@@ -4,6 +4,7 @@ These patterns apply to all data sources that use `useRecords` + `q.select()` (e
4
4
 
5
5
  | Topic | Guide |
6
6
  |---|---|
7
+ | **Multiple data sources in one block** — `datasource.define()`, the `from:` parameter, getting the datasource UUIDs | [multi-datasource.md](multi-datasource.md) |
7
8
  | Query builder, useRecords, useRecord, useLinkedRecords, filtering, sorting, pagination, current user, metrics, chart data | [reading.md](reading.md) |
8
9
  | Record mutations (create/update/delete), file uploads, linked record format, cross-table REST API writes | [writing.md](writing.md) |
9
10
  | `getFieldValue()` helper, field type shapes, record structure, debug utilities (Field Inspector, User Inspector) | [fields.md](fields.md) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "softr-vibe-coding",
3
- "version": "1.11.2",
3
+ "version": "1.12.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"
@@ -15,6 +15,10 @@ Run through this catalog before delivering any block. Every row is a violation o
15
15
  | `q.select()` for REST API fields | Access raw API response directly |
16
16
  | Hardcoding API keys for connected API | Use `useProxyFetch` -- key stays server-side |
17
17
  | Using `q.select({})` to dump all fields on Softr Database | Returns record IDs with empty `fields: {}`. Look up field IDs in Studio's Data tab, or use the Softr DB REST API with `fieldNames=true` |
18
+ | Building an invisible helper block + `window` globals just to read a second table | A block can connect to **multiple data sources**. Declare them with `datasource.define({ alias: "uuid" })` and pass `from: ds.alias` on every hook. One block instead of two, no page-order dependency, no mount-timing race. See [datasources/multi-datasource.md](../datasources/multi-datasource.md). Helper blocks remain correct for genuinely cross-*block* jobs (triggering another block, sharing computed state) — just not for plain multi-table reads |
19
+ | Omitting `from:` on a hook when the block has more than one datasource | Throws at runtime. `from:` is optional ONLY when exactly one source is connected — then hooks default to it. Applies to `useRecords`, `useRecord`, `useLinkedRecords`, `useFieldOptions`, `useMetric`, `useChartData`, `useRecordCreate`, `useRecordUpdate`, `useRecordDelete`. NOT to `useUpload` / `useCurrentRecordId`, which are app-level |
20
+ | Hoisting datasource ids into constants: `datasource.define({ people: PEOPLE_DS_ID })` | Fails to compile — *"datasource.define() object values must be string literals."* Softr statically analyses the call, same as `q.select()`. Keep the UUIDs **inline**: `datasource.define({ people: "74d2cbfd-…" })`. Fails fast with an explicit message, but hoisting magic strings is a strong reflex — resist it here |
21
+ | Asking Studio's AI chat "what are the datasource IDs?" and pasting the answer | **It fabricates them.** Verified July 2026: asked three times for the same three connected tables, it gave three different UUID sets, once reusing a previously-mentioned table's uuid for a different table — all confidently worded, none hedged. Ask it to **write code** instead (*"write a datasource.define call covering every connected source, plus one useRecords per source, code only"*) — scaffolding is bound to the real connections. Then RUN it: real rows under each heading proves each alias maps where you think. A wrong uuid fails safe (matches nothing → error); a *swapped pair* of valid uuids does not |
18
22
 
19
23
  ## Mutations
20
24
 
@@ -60,7 +64,9 @@ Run through this catalog before delivering any block. Every row is a violation o
60
64
  | Emojis in UI | lucide-react icons only |
61
65
  | `[&_svg]:opacity-0` on SelectTrigger | `<style>` + `data-fix-chevron` attribute (Softr bundler limitation) |
62
66
  | 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
- | 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. |
67
+ | 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.** |
68
+ | Relying on the page background on a **dark** brand, and shipping a block whose own canvas is unpainted | The row above assumes a light app whose page bg is already correct. On a dark brand it inverts: `custom-code-header.html`'s `body { background-color: #000 !important }` does NOT cross the shadow-DOM boundary, and a default Softr page is white — so a block with white text, a white-only logo, or a white primary button renders **invisibly on white**. Verified July 2026: a black-canvas feedback form shipped with its wordmark and its Submit button both white-on-white; the button was there and clickable, just unseeable. On a dark brand, paint `backgroundColor` on the block's own outer wrapper AND set the Softr page background to the same value — two identical pure blacks composite with no seam, so the double-paint concern above doesn't bite. Painting it in the block also keeps it correct if the custom-code snippet is ever removed. |
69
+ | Setting only `html, body { background }` in `custom-code-header.html` and expecting the app to change colour | Softr paints the same page fill on **four stacked layers**: `html`, `body`, `#page-content`, and a **class-less wrapper div** nested inside `#page-content`. Styling one gets covered by the ones above it, so `body` alone appears to do nothing. Paint the backdrop on `html`, then clear the duplicates: `body, #page-content { background: transparent }` plus `#page-content div:not(.softr-topbar):not(.softr-topbar *)`. The `:not()` exclusion is required — the nav renders inside `#page-content` and that id's specificity out-ranks `.softr-topbar` rules, so a blanket clear silently flattens the dropdown panel. Full recipe in [native-chrome-styling.md](native-chrome-styling.md). |
64
70
  | `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
71
  | 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). |
66
72
  | Targeting Softr's hashed build classes (e.g. `.f8f11e5_m9ntthp`) when restyling the native header/nav from `custom-code-header.html` | Softr regenerates the hash on every deploy, so the rule silently dies. Target stable hooks: `.softr-topbar`, `.softr-nav-link`, `.softr-nav-button`, `.softr-nav-logo`, `#topbar-root`; for dropdown menus (no `softr-*` class) use the Radix/ARIA attrs `[role="menu"]` / `[role="menuitem"]` / `[role="group"]` / `[aria-expanded="true"]`, scoped under `.softr-topbar`. The native header is Softr chrome (main document), not a block — it can't be built as a Vibe Coding block. See [native-chrome-styling.md](native-chrome-styling.md). |
@@ -88,4 +94,4 @@ Run through this catalog before delivering any block. Every row is a violation o
88
94
  | Helper publishes only raw records | Also publish computed `filterOptions` as separate globals |
89
95
  | Refactoring helper shape without updating consumers | Version namespace OR update all consumers in same commit |
90
96
  | Helper B placed above A when B depends on A | A must be above B -- Softr renders top-to-bottom |
91
- | Using `useLinkedRecords` for rich foreign data | It only returns `{id, title}` -- use a helper block instead |
97
+ | Using `useLinkedRecords` for rich foreign data | It only returns `{id, title}` and silently ignores extra `select` fields. Connect the foreign table as a **second datasource** and read it with its own `useRecords({ from: ds.x })` — see [multi-datasource.md](../datasources/multi-datasource.md). (A helper block also works and is what older blocks do, but it's now the heavier option.) |
@@ -5,6 +5,8 @@ Cross-block communication via `window` globals, the invisible helper block patte
5
5
  ## Table of Contents
6
6
 
7
7
  - [When You Need Helper Blocks](#when-you-need-helper-blocks)
8
+ - [When you still need a helper block](#when-you-still-need-a-helper-block)
9
+ - [The original rationale (historic)](#the-original-rationale-historic)
8
10
  - [Companion Field Helpers](#companion-field-helpers)
9
11
  - [Breadcrumb / Back Navigation](#breadcrumb--back-navigation)
10
12
  - [Multi-Table Access via Invisible Helper Blocks](#multi-table-access-via-invisible-helper-blocks)
@@ -17,11 +19,39 @@ Cross-block communication via `window` globals, the invisible helper block patte
17
19
 
18
20
  ## When You Need Helper Blocks
19
21
 
20
- A Vibe Coding block can only connect to **one source table**. When your main block needs data from a second table (e.g., a task detail block that also needs the Users table for a team picker), you cannot add another `useRecords` for a different table.
22
+ > ⚠️ **Read this first the main reason for helper blocks is gone.** A Vibe Coding block can now
23
+ > connect to **multiple data sources** and read each one with its own `useRecords({ from: ds.x })`.
24
+ > See [../datasources/multi-datasource.md](../datasources/multi-datasource.md). For a plain
25
+ > "my block also needs data from a second table", **use a second datasource, not a helper block** —
26
+ > it's one block instead of two, no `window` globals, no page-order dependency, no mount-timing races.
27
+ >
28
+ > Everything below still works, and the cross-block sections are still the right tool for the jobs
29
+ > listed under "When you still need a helper block". Historic blocks built on this pattern don't
30
+ > need rewriting.
21
31
 
22
- **Solution:** Drop an invisible helper block on the same Softr page, connected to the second table. It fetches records, publishes them to a `window` global, and dispatches a custom event. The main block reads the global and listens for updates. The helper returns `null` so it renders nothing in the published app.
32
+ ## When you still need a helper block
23
33
 
24
- `useLinkedRecords` always returns `{id, title}` only and silently ignores extra fields in `select`. This is the core reason the helper block pattern exists. If your task involves showing status, due dates, or any non-title field from a linked table, skip `useLinkedRecords` entirely and build a helper -- there is no other way to get those fields.
34
+ Reach for one only when the job is genuinely cross-block, not merely cross-table:
35
+
36
+ - **Cross-block communication** — one block triggering behaviour in another on the same page
37
+ (see [Triggering Actions in Other Blocks](#triggering-actions-in-other-blocks-bi-directional-events)).
38
+ - **Publishing computed state** — an expensive derivation several consumer blocks share, computed once.
39
+ - **Rich filter options** shared across multiple blocks on a page.
40
+
41
+ `useLinkedRecords` still returns `{id, title}` only and silently ignores extra fields in `select`.
42
+ That limitation is unchanged — but the fix is now a second datasource with its own `from:`, not a
43
+ helper block.
44
+
45
+ ## The original rationale (historic)
46
+
47
+ A Vibe Coding block used to connect to **one source table** only. When a block needed data from a
48
+ second table (e.g., a task detail block that also needs the Users table for a team picker), you
49
+ could not add another `useRecords` for a different table.
50
+
51
+ **The workaround:** drop an invisible helper block on the same Softr page, connected to the second
52
+ table. It fetches records, publishes them to a `window` global, and dispatches a custom event. The
53
+ main block reads the global and listens for updates. The helper returns `null` so it renders nothing
54
+ in the published app.
25
55
 
26
56
  Key rules:
27
57
  - The helper block must be on the **same Softr page** as the consumer -- `window` is page-scoped, globals don't cross pages.
@@ -367,4 +397,4 @@ For per-user saved filter views on a list page:
367
397
  | Refactoring helper shape without updating consumers | Version namespace OR update all consumers in same commit |
368
398
  | Helper B placed above A when B depends on A | A must be above B -- Softr renders top-to-bottom |
369
399
  | `useRef` for IDs consumed by `useMemo` | Use `useState` -- ref mutations don't trigger recomputation |
370
- | Using `useLinkedRecords` for rich foreign data | It only returns `{id, title}` -- use a helper block instead |
400
+ | Using `useLinkedRecords` for rich foreign data | It only returns `{id, title}` and silently ignores extra `select` fields. Connect the foreign table as a **second datasource** and read it with its own `useRecords({ from: ds.x })` — see [multi-datasource.md](../datasources/multi-datasource.md). (A helper block also works and is what older blocks do, but it's now the heavier option.) |
@@ -5,8 +5,8 @@ Fast lookup for imports, hook signatures, field mapping syntax, and common patte
5
5
  ## Imports
6
6
 
7
7
  ```jsx
8
- // DATASOURCE
9
- import { useRecords, useRecord, useRecordCreate, useRecordUpdate, useRecordDelete,
8
+ // DATASOURCE (add `datasource` when the block connects to more than one source)
9
+ import { datasource, useRecords, useRecord, useRecordCreate, useRecordUpdate, useRecordDelete,
10
10
  useCurrentRecordId, useLinkedRecords, useUpload, useMetric, useChartData,
11
11
  q, metric } from "@/lib/datasource";
12
12
 
@@ -41,6 +41,20 @@ var updateFields = q.select({ alias: "FIELD_ID" }); // writable only
41
41
  var createFields = q.select({ alias: "FIELD_ID" }); // writable only
42
42
  ```
43
43
 
44
+ ## Multiple datasources (static, outside component)
45
+
46
+ ```jsx
47
+ var ds = datasource.define({ // values MUST be inline string literals
48
+ people: "74d2cbfd-f2cb-4f5c-82d9-0d3a0651e531",
49
+ shifts: "ec7a6311-f6c3-4c99-881d-aae308148716",
50
+ });
51
+
52
+ useRecords({ from: ds.people, select: select }); // `from:` required once >1 source
53
+ ```
54
+
55
+ Ids are plain UUIDs. Get them by asking Studio's AI chat to **write code**, never to recite a
56
+ value — it fabricates them in prose. Full detail: [../datasources/multi-datasource.md](../datasources/multi-datasource.md).
57
+
44
58
  ## Read
45
59
 
46
60
  ```jsx