softr-vibe-coding 2.14.2 → 2.14.4
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 +9 -0
- package/SKILL.md +2 -2
- package/datasources/hubspot.md +12 -2
- package/datasources/rest-api.md +39 -1
- package/package.json +1 -1
- package/references/anti-patterns.md +1 -0
- package/references/searchable-dropdown.md +58 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ 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.14.4] - 2026-10-07
|
|
8
|
+
- Release 2.14.4
|
|
9
|
+
- Add the Safari focus rule to the Combo reference
|
|
10
|
+
|
|
11
|
+
## [2.14.3] - 2026-10-07
|
|
12
|
+
- Release 2.14.3
|
|
13
|
+
- Document that the REST API proxy is not access control
|
|
14
|
+
- Run the publish workflow one at a time and pass a duplicate release run
|
|
15
|
+
|
|
7
16
|
## [2.14.2] - 2026-10-06
|
|
8
17
|
- Release 2.14.2
|
|
9
18
|
- Check every relative Markdown link in CI before publishing
|
package/SKILL.md
CHANGED
|
@@ -94,7 +94,7 @@ You generate complete, production-ready Softr Vibe Coding blocks as TypeScript R
|
|
|
94
94
|
- `useRecordUpdate` payload is `{ recordId, fields: { ... } }` — nested. `useRecordCreate` payload is **flat** (no `fields` wrapper). The two shapes are asymmetric by design (verified live 2026-08-25)
|
|
95
95
|
- Sequential multi-row saves use `await hook.mutateAsync(...)` per row, in order, with stop-on-failure + retry state — `mutateAsync` is fully supported on the current platform (verified 2026-08-25; the old ".mutate() only" Action-parser rule is gone — see [datasources/writing.md](datasources/writing.md)). Independent writes to **different tables** may run in parallel via `Promise.all`; drag/reassign UIs should be optimistic with an Undo toast — see [writing.md → Parallel writes across tables](datasources/writing.md#parallel-writes-across-tables-the-one-sanctioned-parallelism)
|
|
96
96
|
- No hardcoded domains in links -- use relative paths (`/page?recordId=...`); same-page anchors written relative too (`/#section`)
|
|
97
|
-
- **No `<select>` and no shadcn `<Select>`** — both break inside a block's shadow DOM (native hands the list to the OS; shadcn portals outside the shadow root and arrives unstyled). Use the `Combo` pattern in [references/searchable-dropdown.md](references/searchable-dropdown.md) — **searchable by default** for every framed filter or form field whatever the option count; `bare` inline editors are click-only; `searchable={false}` only on a short fixed enum the user is setting (a status, a location, a group-by)
|
|
97
|
+
- **No `<select>` and no shadcn `<Select>`** — both break inside a block's shadow DOM (native hands the list to the OS; shadcn portals outside the shadow root and arrives unstyled). Use the `Combo` pattern in [references/searchable-dropdown.md](references/searchable-dropdown.md) — **searchable by default** for every framed filter or form field whatever the option count; `bare` inline editors are click-only; `searchable={false}` only on a short fixed enum the user is setting (a status, a location, a group-by). Opening a Combo moves focus into it (the search box, or the trigger on a click-only list), because Safari does not focus a clicked button. Escape on an open list closes only the list ([Move focus into the Combo when it opens](references/searchable-dropdown.md#move-focus-into-the-combo-when-it-opens))
|
|
98
98
|
- App page with Softr navigation: **no shadcn `<Dialog>` / `<Sheet>`** — its overlay is z-50, under Softr's top bar (z-index 800), and it portals out of the shadow root. Use the in-block modal (`fixed inset-0 z-[1000]`, rendered as a sibling of the block's `@container` wrapper, with its own Escape, focus trap, scroll lock and focus return) in [references/common-patterns.md → A modal above Softr's bars](references/common-patterns.md#a-modal-above-softrs-bars) (measured live 2026-10-06)
|
|
99
99
|
- No clipping class (`overflow-hidden`, `overflow-*-auto`, `truncate`, `line-clamp-*`) on any element that contains a `Combo` — its panel is absolutely positioned in local DOM, so a clipping `<td>` cuts the menu to the row's height; bound an over-wide chip at the chip (`min-w-0 truncate`), and never `scrollIntoView` inside the panel. See [references/searchable-dropdown.md](references/searchable-dropdown.md#the-four-things-that-will-bite-you), item 4
|
|
100
100
|
- Any **Print** control opens a **new window with its own document** — `window.open` straight from the click, an escaped standalone HTML printout written into it, `print()` once its stylesheets, fonts and images are in, the button disabled until the data has fully loaded. No `window.print()` on the Softr page, no in-page print view (Hard Constraint 28). See [references/printing.md](references/printing.md)
|
|
@@ -183,7 +183,7 @@ For advanced patterns beyond data fetching, load the relevant reference when the
|
|
|
183
183
|
| Embedding third-party libraries with their own CSS (Leaflet, Mapbox, TinyMCE, Quill, FullCalendar) | [references/advanced-integrations.md](references/advanced-integrations.md) |
|
|
184
184
|
| Debugging a broken block, checking patterns before delivery, full violation catalog | [references/anti-patterns.md](references/anti-patterns.md) |
|
|
185
185
|
| Quick syntax check — import paths, hook signatures, mutation call shapes, field mapping, Softr navigation variables (`--nav-height` etc.), container-query syntax | [references/quick-reference.md](references/quick-reference.md) |
|
|
186
|
-
| Any **dropdown / picker / combobox** in a block — why shadcn `<Select>` and native `<select>` both fail inside the shadow DOM, the `composedPath()` click-outside, sorting A→Z inside the component, multi-token filtering, **searchable by default** regardless of option count (`bare` inline editors click-only; `searchable={false}` only for a short fixed enum being set), the `bare` inline-editor variant | [references/searchable-dropdown.md](references/searchable-dropdown.md) |
|
|
186
|
+
| Any **dropdown / picker / combobox** in a block — why shadcn `<Select>` and native `<select>` both fail inside the shadow DOM, the `composedPath()` click-outside, sorting A→Z inside the component, multi-token filtering, **searchable by default** regardless of option count (`bare` inline editors click-only; `searchable={false}` only for a short fixed enum being set), the `bare` inline-editor variant, focus moved into the Combo on open (Safari does not focus a clicked button) and Escape that closes only the list | [references/searchable-dropdown.md](references/searchable-dropdown.md) |
|
|
187
187
|
| **Printing** anything from a block — always a new window/tab holding its own document, never `window.print()` on the page or an in-page print view: the escaped HTML builder, pop-up-safe opening from the click, print-when-ready (stylesheets, fonts and images, capped), Print disabled until the data has loaded, the `?print=1` deep link from another page, paper layout (shared `<colgroup>`, `vertical-align: middle`, tick boxes) | [references/printing.md](references/printing.md) |
|
|
188
188
|
| Small reusable patterns — `localStorage` cross-page state, clipboard copy button, measuring the block's own width (not the window's), clearing Softr's sticky top bar and phone tab bar, an in-block modal above Softr's bars (instead of shadcn `Dialog`) | [references/common-patterns.md](references/common-patterns.md) |
|
|
189
189
|
| Writing Airtable Automation Scripts / Scripting Extension scripts / Airtable formulas — companion to Softr blocks for cross-table cascades and computed values | [references/airtable-automations.md](references/airtable-automations.md) |
|
package/datasources/hubspot.md
CHANGED
|
@@ -287,7 +287,9 @@ to set a client's links server-side.
|
|
|
287
287
|
|
|
288
288
|
- **The step:** a **Run custom code** step (`CUSTOM_CODE` v1.2.0) with the HubSpot integration
|
|
289
289
|
attached. `fetch()` calls to `api.hubapi.com` then carry that integration's credentials, with no
|
|
290
|
-
token in the code.
|
|
290
|
+
token in the code. Those credentials carry only the scopes granted when HubSpot was connected.
|
|
291
|
+
A call to an object Softr's connector doesn't support yet may get HubSpot's 403 (Softr engineer,
|
|
292
|
+
2026-10-07; the scope list is not published).
|
|
291
293
|
- **Plan and testing:** the step needs a paid Softr plan, and it is `REAL_ONLY`, so a test run
|
|
292
294
|
writes for real.
|
|
293
295
|
- **Limits:** about 2 minutes per run, and up to 20 fetches per second.
|
|
@@ -310,7 +312,9 @@ to set a client's links server-side.
|
|
|
310
312
|
HubSpot itself. The cost is trigger latency (unknown, see below) and a run for every new ticket.
|
|
311
313
|
- **Why the block can't do it itself:** `useProxyFetch` is documented for REST API sources only.
|
|
312
314
|
Call API authenticates only REST_API integrations and needs Professional or higher, so it would
|
|
313
|
-
need a HubSpot private-app token stored as a REST integration (inferred).
|
|
315
|
+
need a HubSpot private-app token stored as a REST integration (inferred). Any user who can call
|
|
316
|
+
the proxy could then use that token for any path on HubSpot's API
|
|
317
|
+
([why](rest-api.md#the-proxy-is-not-access-control)).
|
|
314
318
|
- **HubSpot-side route:** a HubSpot workflow's "Create associations" action needs Pro or
|
|
315
319
|
Enterprise, and ticket-based workflows need Service Hub Pro or Enterprise (documented). It
|
|
316
320
|
matches records by exact, case-sensitive property value.
|
|
@@ -322,6 +326,12 @@ request parameter the caller controls, not access control. See
|
|
|
322
326
|
[../references/softr-mcp.md](../references/softr-mcp.md#what-the-server-enforces-on-a-blocks-data-endpoints)
|
|
323
327
|
(verified 2026-09-18 on Softr Database; on HubSpot 2026-10-05, below).
|
|
324
328
|
|
|
329
|
+
A **REST API source pointed at HubSpot** (`useProxyFetch` with a private-app token) has no row gate
|
|
330
|
+
at all. The proxy forwards any path on `api.hubapi.com` that the browser sends (Softr engineers,
|
|
331
|
+
2026-10-07); see [rest-api.md](rest-api.md#the-proxy-is-not-access-control). Serve client-facing
|
|
332
|
+
rows from the native connection. Objects it lacks, such as conversations and feedback submissions,
|
|
333
|
+
need one of the server-side routes listed there.
|
|
334
|
+
|
|
325
335
|
The **block's Visibility** is enforced on the same endpoints, all or nothing (verified 2026-10-05 on
|
|
326
336
|
HubSpot). A block gated to an "Account managers" condition group returned every deal and ticket to
|
|
327
337
|
members from connections with no Source condition, and 403 on list and by-id to clients, a
|
package/datasources/rest-api.md
CHANGED
|
@@ -98,10 +98,47 @@ export default function Block() {
|
|
|
98
98
|
|
|
99
99
|
- `useProxyFetch` returns a fetch function that routes requests through Softr's proxy
|
|
100
100
|
- Softr injects the authentication headers configured in the data source automatically
|
|
101
|
-
- API keys are **never exposed** in client-side code
|
|
101
|
+
- API keys are **never exposed** in client-side code. That protects the key, not the data: see [The proxy is not access control](#the-proxy-is-not-access-control)
|
|
102
102
|
- The response is the raw API JSON -- access fields directly (e.g., `item.name`, not `record.fields.name`)
|
|
103
103
|
- **The proxy only supports text payloads** -- streams, `FormData`, and file uploads won't work. Serialize request bodies as JSON/text.
|
|
104
104
|
|
|
105
|
+
### The proxy is not access control
|
|
106
|
+
|
|
107
|
+
*Softr engineers, asked 2026-10-07; not tested here.* The proxy does two things:
|
|
108
|
+
|
|
109
|
+
- It sends requests only to the **origin** set when the REST API data source was created, e.g.
|
|
110
|
+
`https://api.hubapi.com`, so they can't be redirected to another host.
|
|
111
|
+
- It adds the data source's stored secrets on Softr's server, so the token never reaches the browser.
|
|
112
|
+
|
|
113
|
+
It checks nothing else. The path, query, method and body come from the browser. A user can copy a
|
|
114
|
+
proxy request from DevTools' Network tab, change the endpoint or record id, and resend it. Softr
|
|
115
|
+
forwards it as is, and its engineers advise against relying on the proxy for security. So:
|
|
116
|
+
|
|
117
|
+
- **Anyone who can call the proxy can read everything the token can read on that origin.**
|
|
118
|
+
Filtering in block code, or a URL built from `useCurrentUser().email`, decides what the block
|
|
119
|
+
shows, not what a user can fetch.
|
|
120
|
+
- **Source conditions don't apply.** The connection's record filters cover the record hooks
|
|
121
|
+
(`useRecords` and the rest), not `proxyFetch`. This came from a less certain answer in the same
|
|
122
|
+
thread.
|
|
123
|
+
- **Block Visibility and page permissions may not apply either.** That same answer said "only
|
|
124
|
+
the record hooks" for them too, and it is unconfirmed. Until it is tested, assume any logged-in
|
|
125
|
+
user can call the proxy.
|
|
126
|
+
- **`{LOGGED_IN_USER: …}` placeholders don't help.** They are reportedly filled in on the server,
|
|
127
|
+
but a replayed request can simply leave the placeholder out.
|
|
128
|
+
- **The token's scopes set the blast radius.** Grant only the narrowest scopes the block needs.
|
|
129
|
+
|
|
130
|
+
Per-user data needs a gate on the server instead:
|
|
131
|
+
|
|
132
|
+
1. **A native connector with a logged-in-user Source condition.** This is verified on Softr
|
|
133
|
+
Database and HubSpot; see
|
|
134
|
+
[softr-mcp.md](../references/softr-mcp.md#what-the-server-enforces-on-a-blocks-data-endpoints).
|
|
135
|
+
Softr's engineers asked first why a builder would pick REST over the native integration.
|
|
136
|
+
2. **For data no native connector covers**, copy it with a workflow into a table that a native
|
|
137
|
+
connector reads, such as a Softr Database table keyed by the user's email, and gate that table
|
|
138
|
+
with a Source condition. This design is inferred, not built.
|
|
139
|
+
3. **Otherwise keep REST proxy blocks to data that everyone who can reach the page may see**, or
|
|
140
|
+
to internal tools whose users are trusted with everything the token reads.
|
|
141
|
+
|
|
105
142
|
### Multiple datasources
|
|
106
143
|
|
|
107
144
|
When the block has more than one datasource, `useProxyFetch` needs to know which source to route through. Unlike the record hooks (which take a `from:` option), it takes the alias as its **function argument**:
|
|
@@ -184,6 +221,7 @@ fetch("https://workflows-api.softr.io/v1/workflows/WORKFLOW_ID/executions/EXECUT
|
|
|
184
221
|
| Pagination | Built-in `fetchNextPage` | Manual via URL params + cursor |
|
|
185
222
|
| Filtering | `q.text()`, `q.number()`, etc. | API query params or client-side |
|
|
186
223
|
| Auth | Handled by Softr | Proxied through Softr (key hidden) |
|
|
224
|
+
| Per-user rows | Source conditions, enforced on the server | None: the browser picks the URL ([why](#the-proxy-is-not-access-control)) |
|
|
187
225
|
| Mutations | `useRecordCreate/Update/Delete` | Direct `fetch()` or `proxyFetch()` |
|
|
188
226
|
|
|
189
227
|
## Limitations
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "softr-vibe-coding",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.4",
|
|
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"
|
|
@@ -14,6 +14,7 @@ Run through this catalog before delivering any block. Every row is a violation o
|
|
|
14
14
|
| `useRecords` with REST API source | Use `useProxyFetch` + `useQuery` |
|
|
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
|
+
| Limiting a REST API block to the user's own records by building the `proxyFetch` URL from `useCurrentUser()` or by filtering the response | **This is not access control.** The proxy forwards any path on the source's origin with the stored token, and a user can replay the request from DevTools with another endpoint or record id (Softr engineers, 2026-10-07). Per-user rows need a native connector with a Source condition. See [datasources/rest-api.md](../datasources/rest-api.md#the-proxy-is-not-access-control) |
|
|
17
18
|
| 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
19
|
| 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
20
|
| 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. `useProxyFetch` has the same multi-datasource requirement but takes the alias as its **argument** — `useProxyFetch(ds.store)` — not as `from:` |
|
|
@@ -329,6 +329,58 @@ the optional *create* row — so arrow-key navigation has one index to walk. Cla
|
|
|
329
329
|
index (`Math.min(active, rows.length - 1)`): filtering shrinks the list under the highlight.
|
|
330
330
|
Keep the highlighted row in view by scrolling the list only (rule 3 of item 4 above).
|
|
331
331
|
|
|
332
|
+
## Move focus into the Combo when it opens
|
|
333
|
+
|
|
334
|
+
When the panel opens, the Combo moves focus into itself: to the search box if it has one,
|
|
335
|
+
otherwise to the trigger. Do not count on the click to do it. **Safari (macOS and iPadOS) and
|
|
336
|
+
Firefox on macOS do not focus a `<button>` when it is clicked** (Chrome does), so on a click-only
|
|
337
|
+
Combo focus stays in whatever field had it, usually the text input above it in a form. Two things then
|
|
338
|
+
break at once: the keys the user types land in that input, and Escape goes past the Combo to the
|
|
339
|
+
next listener. Inside the [in-block modal](common-patterns.md#a-modal-above-softrs-bars), that
|
|
340
|
+
listener closes the modal or asks to discard the typed input, with the list still open.
|
|
341
|
+
|
|
342
|
+
```jsx
|
|
343
|
+
useEffect(
|
|
344
|
+
function () {
|
|
345
|
+
if (!open) return;
|
|
346
|
+
if (searchable) {
|
|
347
|
+
if (inputRef.current) inputRef.current.focus({ preventScroll: true });
|
|
348
|
+
} else if (triggerRef.current) {
|
|
349
|
+
triggerRef.current.focus({ preventScroll: true });
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
[open, searchable]
|
|
353
|
+
);
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
A click-only list is then driven from the trigger: ↑ ↓ move the highlight, Enter picks, Escape
|
|
357
|
+
closes. Give that trigger `role="combobox"` with `aria-activedescendant` pointing at the
|
|
358
|
+
highlighted row, because `aria-activedescendant` is not valid on a plain button.
|
|
359
|
+
`preventScroll` is there for the same reason as on the search box (rule 3 of item 4).
|
|
360
|
+
|
|
361
|
+
**Escape closes the list and nothing else.** While the list is open, the Combo's keydown
|
|
362
|
+
handler calls `e.preventDefault()` and `e.stopPropagation()`, closes the list and puts focus
|
|
363
|
+
back on the trigger. React's handler on the Combo root runs inside the shadow root, before the
|
|
364
|
+
event reaches `document`. The in-block modal's document listener returns early on
|
|
365
|
+
`e.defaultPrevented`, so the modal stays open and clean. Either guard alone covers that modal,
|
|
366
|
+
but keep both: another document-level listener may not check `defaultPrevented`.
|
|
367
|
+
|
|
368
|
+
**To reproduce Safari in any browser,** focus the text field before the Combo, call `.click()`
|
|
369
|
+
on the trigger from the console (a synthetic click does not move focus either), and read
|
|
370
|
+
`activeElement` on the block's shadow root. It must be the search box or the trigger. Test
|
|
371
|
+
typing with real key events. A browser-automation "type" action that inserts text without key
|
|
372
|
+
events drops it into the last focused text field, which makes a correct Combo look broken.
|
|
373
|
+
|
|
374
|
+
**The incident: Lane County Diaper Bank, 2026-10-07.** A browser check reported that in B4's New
|
|
375
|
+
partner modal, letters typed after opening the click-only "Partner type" list went into
|
|
376
|
+
Organization name, and Escape then showed the modal's "Discard your changes?" strip while the
|
|
377
|
+
list stayed open. Part of that report came from the test tool, whose "type" action wrote into the
|
|
378
|
+
last focused text field. But the code audit it prompted found the real gap: every copy of the
|
|
379
|
+
component moved focus on open only when the list was searchable. On Safari, where a click leaves
|
|
380
|
+
focus where it was, a click-only list never got the keyboard. Eight of the fifteen blocks had
|
|
381
|
+
it. Chrome hid it, because there the click itself focuses the trigger. The effect above fixed
|
|
382
|
+
all eight, rechecked on every page with a synthetic click and real key presses.
|
|
383
|
+
|
|
332
384
|
## Variants worth having
|
|
333
385
|
|
|
334
386
|
- **`bare`** — inline-editor mode. No border, no fill; `triggerContent` (a status chip, a
|
|
@@ -375,7 +427,12 @@ Everything else — the trigger, the card, the rows — stays flat.
|
|
|
375
427
|
on app pages inside Softr's top bar and phone tab bar
|
|
376
428
|
- [ ] Keyboard: ↑ ↓ Enter Esc Tab; the active row kept visible by scrolling the list only
|
|
377
429
|
(never `scrollIntoView`), and the search box focused with `preventScroll`
|
|
430
|
+
- [ ] Opening moves focus into the Combo: the search box, or the trigger on a click-only
|
|
431
|
+
list, because Safari does not focus a clicked button. Escape on an open list calls
|
|
432
|
+
`preventDefault` + `stopPropagation` and closes only the list
|
|
433
|
+
([Move focus into the Combo when it opens](#move-focus-into-the-combo-when-it-opens))
|
|
378
434
|
- [ ] `aria-haspopup="listbox"`, `aria-expanded`, `role="listbox"` / `role="option"`,
|
|
379
|
-
`aria-selected`, and an `aria-label` on the trigger
|
|
435
|
+
`aria-selected`, and an `aria-label` on the trigger; a click-only trigger that carries
|
|
436
|
+
`aria-activedescendant` also gets `role="combobox"`
|
|
380
437
|
- [ ] Loading and empty states (`"Nothing matches that."`)
|
|
381
438
|
- [ ] No `@/components/ui/select` import anywhere in the file
|