softr-vibe-coding 2.14.3 → 2.14.5

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,14 @@ 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.5] - 2026-10-07
8
+ - Release 2.14.5
9
+ - Document condition-based user groups over the MCP
10
+
11
+ ## [2.14.4] - 2026-10-07
12
+ - Release 2.14.4
13
+ - Add the Safari focus rule to the Combo reference
14
+
7
15
  ## [2.14.3] - 2026-10-07
8
16
  - Release 2.14.3
9
17
  - Document that the REST API proxy is not access control
package/README.md CHANGED
@@ -208,7 +208,12 @@ softr-vibe-coding/
208
208
  │ │ # alone, DATETIME create shape, offset paging;
209
209
  │ │ # OAuth grant per ticked workspace, email
210
210
  │ │ # senders, formulas fixed at creation, loop
211
- │ │ # counter, workflow time zone and publish state
211
+ │ │ # counter, workflow time zone and publish state;
212
+ │ │ # Oct 7 2026: condition-based user groups over
213
+ │ │ # MCP (subject USER:<field id>, not USER:::),
214
+ │ │ # list_users omits conditional membership,
215
+ │ │ # reading userGroups in the preview iframe,
216
+ │ │ # what __softr_current_user carries
212
217
  │ ├── browser-checks.md # Checking a pushed block in a browser with
213
218
  │ │ # the agent-browser CLI (ask before installing):
214
219
  │ │ # preview cookie, shadow-DOM refs grepped in the
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) |
@@ -452,7 +452,10 @@ page ([details](multi-datasource.md#one-connection--one-read-payload-the-union-o
452
452
  HubSpot: anyone who can edit that property there can grant the group's access.
453
453
  - **`application_list_users` is not a membership check.** It showed `userGroups: []` for every
454
454
  user, including members of condition groups that demonstrably applied (verified 2026-10-05).
455
- Test membership by what the user can reach, e.g. preview as them against a group-gated block.
455
+ Test membership by what the user can reach, e.g. preview as them against a group-gated block, or
456
+ read their groups in the preview
457
+ ([how](../references/softr-mcp.md#testing-as-any-app-user-without-logins--the-preview-as-switcher);
458
+ same result on Softr Database, 2026-10-07).
456
459
 
457
460
  ## Audit trail
458
461
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "softr-vibe-coding",
3
- "version": "2.14.3",
3
+ "version": "2.14.5",
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"
@@ -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
@@ -19,7 +19,7 @@ The official Softr MCP server (`https://mcp.softr.io/mcp`) gives an AI assistant
19
19
  - [Vibe coding block tools](#vibe-coding-block-tools) — incl. [what the server enforces on a block's data endpoints](#what-the-server-enforces-on-a-blocks-data-endpoints)
20
20
  - [Adopting Studio-AI-generated code](#adopting-studio-ai-generated-code)
21
21
  - [Vibe coding gotchas (official)](#vibe-coding-gotchas-official)
22
- - [Application management tools](#application-management-tools) — incl. [testing as any user via "Preview as"](#testing-as-any-app-user-without-logins--the-preview-as-switcher)
22
+ - [Application management tools](#application-management-tools) — incl. [condition-based user groups](#condition-based-user-groups) and [testing as any user via "Preview as"](#testing-as-any-app-user-without-logins--the-preview-as-switcher)
23
23
  - [Browsing integrations (external data sources)](#browsing-integrations-external-data-sources)
24
24
  - [Softr Database tools](#softr-database-tools)
25
25
  - [Workflows](#workflows)
@@ -536,14 +536,19 @@ verified on HubSpot on 2026-10-05, the same way.* There are two forms, and **the
536
536
  **It fails closed:** a user whose field is empty, or who has no record in the users' data source,
537
537
  gets 0 rows, and a by-id request for a record outside the condition returns 404 (on HubSpot;
538
538
  Softr Database answers HTTP 200 with an empty body).
539
+ - **Do not confuse it with the user-group syntax.** A user group's condition names the user field
540
+ as its **subject**, `USER:<field id>`: one colon, no braces (verified 2026-10-07, see
541
+ [Condition-based user groups](#condition-based-user-groups)). A Source condition names it in the
542
+ **value**, `USER:::<field id>`: three colons. The user-group form returned HTTP 400 in a Source
543
+ condition (below); the Source-condition form has not been tried in a user group.
539
544
  - **Use AND between rules.** With one rule OR and AND behave the same, but a second rule added
540
545
  under OR widens access (2026-10-05).
541
546
  - **The braced user-field spellings fail.** On 2026-09-18, on Softr Database, eleven spellings were
542
547
  tried, including `{USER:::<fieldId>}`, `{USER:<fieldId>}` and `{USER:::FIELD:<fieldId>}`. Each
543
548
  silently matched nothing. All eleven had braces, so the braceless form is untested on Softr
544
549
  Database, not disproved. A subject of `USER:<fieldId>`, the syntax user-group rules use, returned
545
- HTTP 400 "Field not found": the user field goes in the value, never the subject. No token for a
546
- user group has been found.
550
+ HTTP 400 "Field not found": the user field goes in the value, never the subject. No token that
551
+ tests whether the user is in a group has been found.
547
552
  - Studio's conditional-filter UI offers the logged-in user's Email and Email-Domain, plus every
548
553
  users-table field once users sync from a data source (documented). For a value not listed here,
549
554
  pick it in a block's Source tab, save, and read `dataSources[].condition` back with
@@ -641,6 +646,37 @@ Combined with the database tools (`database_create` / `database_create_table` /
641
646
  no zone designator and nine fractional digits (`2026-09-09T22:34:11.157881061`). They were UTC, so
642
647
  read any older logged value as UTC, never as local time.
643
648
 
649
+ ### Condition-based user groups
650
+
651
+ *Verified 2026-10-07 on Softr Database, with users synced from a Softr Database table and the user
652
+ connection's field reference key set to `id`. The HubSpot version (2026-10-05) is in
653
+ [../datasources/hubspot.md](../datasources/hubspot.md#user-sync).*
654
+
655
+ - **`application_update_user_group` sets a group's condition.** It returned the condition exactly as
656
+ sent. This one, on a "Volunteer" group, tests two single-line text fields of the users table:
657
+
658
+ ```json
659
+ {
660
+ "logicalOperator": "AND",
661
+ "expressions": [
662
+ { "subject": { "field": "USER:YB2ot", "type": "TEXT" }, "operator": "IS_NOT_EMPTY", "value": [] },
663
+ { "subject": { "field": "USER:uo0TW", "type": "TEXT" }, "operator": "IS", "value": ["Active"] }
664
+ ]
665
+ }
666
+ ```
667
+
668
+ - **The subject is `USER:<field id>`: one colon, no braces.** A block's Source condition uses a
669
+ different form, `USER:::<field id>` with three colons, and puts it in the value
670
+ ([Logged-in-user values in Source conditions](#logged-in-user-values-in-source-conditions)). Do not
671
+ copy one into the other.
672
+ - **`application_list_users` does not show condition-based membership.** Right after the update, the
673
+ one user whose record matched (login email set, status Active) still listed `userGroups: []`. The
674
+ tool shows only manual memberships, such as a user added to Administrator through `userEmails`.
675
+ Softr evaluates conditions at runtime, so an empty `userGroups` there is not evidence that a
676
+ condition fails. HubSpot behaved the same way.
677
+ - **Check membership by running the app as that user** and reading the groups the app gives them
678
+ ([how](#testing-as-any-app-user-without-logins--the-preview-as-switcher), below).
679
+
644
680
  ### Testing as any app user without logins — the "Preview as" switcher
645
681
 
646
682
  *Verified live 2026-09-18.* The `application_preview` link does not open the app directly: it opens a
@@ -666,6 +702,24 @@ passwords, no test accounts to create:
666
702
  `fetch('/studio/impersonate/<softrUserId>')` makes the preview run as that user. The id is the
667
703
  user's Softr id from `application_list_users`. It works from a fresh preview link, so it needs no
668
704
  Studio session in the browser.
705
+ - **Read a user's groups from the app itself** (verified 2026-10-07, read-only). Mint a fresh
706
+ `application_preview` link and open it, run the impersonate call above, reload the app iframe,
707
+ then read `iframe.contentWindow.__softr_current_user.userGroups`.
708
+ - The link carries `?show-toolbar=true`, so the top document is the toolbar shell and the app runs
709
+ in `document.querySelector('iframe')` (the `#preview-iframe` of
710
+ [browser-checks.md](browser-checks.md)). `window.__softr_current_user` exists only in that
711
+ iframe's `contentWindow`. The top window has no user globals at all.
712
+ - To reload, set the iframe's `src` again with a fresh `t=<timestamp>` query parameter, after the
713
+ impersonate call.
714
+ - Observed group names: a user who matched the Volunteer condition above read
715
+ `["Logged in users", "All users", "Volunteer"]`; a user with no login email read
716
+ `["Logged in users", "All users"]`.
717
+ - **`window.__softr_current_user` carries only `name`, `email`, `avatar` and `userGroups`**
718
+ (verified 2026-10-07). None of the users-table record's other fields were there, on a users table
719
+ with notes, phone and emergency-contact fields. For a privacy review: syncing a users table does
720
+ not by itself expose the record's other fields through this global. A block can still ask for
721
+ them with `useCurrentUser({ properties })`
722
+ ([../datasources/reading.md](../datasources/reading.md#current-user)).
669
723
  - **Press the preview's own `#refresh-button` after a push.** An open preview kept serving the old
670
724
  block version until it was pressed (verified 2026-10-05). Minting a fresh link does too (see the
671
725
  version note above).