softr-vibe-coding 2.6.0 → 2.7.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,9 @@ 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.7.0] - 2026-09-09
8
+ - Add the searchable dropdown pattern (Combo) as a reference
9
+
7
10
  ## [2.6.0] - 2026-09-09
8
11
  - Drag-to-reorder: the pattern, the shadow-DOM hit-test trap, and two affordance rules
9
12
 
package/README.md CHANGED
@@ -220,9 +220,14 @@ softr-vibe-coding/
220
220
  │ │ # MCP/CLI install (@latest npx + browser step),
221
221
  │ │ # extract → poll → findings → generate → write
222
222
  │ │ # flow, DESIGN.md anatomy, drift QA
223
- └── quick-reference.md # Syntax cheat sheet
224
- # Imports, hook signatures, mutation shapes,
225
- # field mapping, component skeleton
223
+ ├── quick-reference.md # Syntax cheat sheet
224
+ # Imports, hook signatures, mutation shapes,
225
+ # field mapping, component skeleton
226
+ │ └── searchable-dropdown.md # THE dropdown pattern for blocks
227
+ │ # why native <select> and shadcn <Select> both
228
+ │ # break in the shadow DOM, composedPath()
229
+ │ # click-outside, A-Z inside the component,
230
+ │ # multi-token filter, bare inline-editor variant
226
231
 
227
232
  ├── tools/ # Bundled CLI scripts (run, not read)
228
233
  │ ├── get-airtable-base # Full Airtable base schema export (bash + jq)
package/SKILL.md CHANGED
@@ -85,6 +85,7 @@ You generate complete, production-ready Softr Vibe Coding blocks as TypeScript R
85
85
  - `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)
86
86
  - 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)
87
87
  - No hardcoded domains in links -- use relative paths (`/page?recordId=...`); same-page anchors written relative too (`/#section`)
88
+ - **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)
88
89
  - Static block: no hardcoded user-visible copy — every string/image/link is an editable setting (see [references/editable-settings.md](references/editable-settings.md#granularity-doctrine-settings-first-static-blocks))
89
90
  - Array-setting rows keyed by **index**, never by a builder-editable field value
90
91
  - Media settings that may start empty (`src: ""`) gated with a conditional render or placeholder — never an unconditional `<img src={setting.src}>`
@@ -167,6 +168,7 @@ For advanced patterns beyond data fetching, load the relevant reference when the
167
168
  | Embedding third-party libraries with their own CSS (Leaflet, Mapbox, TinyMCE, Quill, FullCalendar) | [references/advanced-integrations.md](references/advanced-integrations.md) |
168
169
  | Debugging a broken block, checking patterns before delivery, full violation catalog | [references/anti-patterns.md](references/anti-patterns.md) |
169
170
  | Quick syntax check — import paths, hook signatures, mutation call shapes, field mapping | [references/quick-reference.md](references/quick-reference.md) |
171
+ | 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, the `bare` inline-editor variant | [references/searchable-dropdown.md](references/searchable-dropdown.md) |
170
172
  | Small reusable patterns — `localStorage` cross-page state, clipboard copy button | [references/common-patterns.md](references/common-patterns.md) |
171
173
  | 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) |
172
174
  | The official **Softr MCP server** — Softr DB schema + full record/table/field/database CRUD (deletes included), field-level browsing of connected Airtable / Google Sheets / Notion / Supabase integrations, **creating, editing, versioning, and deploying Vibe Coding blocks directly** (`get_vibe_coding_docs`, `create_vibe_coding_block`, ...), app management/scaffolding, the **Softr Workflows** suite (26 tools, 418-node catalog), and **per-application MCP servers** | [references/softr-mcp.md](references/softr-mcp.md) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "softr-vibe-coding",
3
- "version": "2.6.0",
3
+ "version": "2.7.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"
@@ -0,0 +1,149 @@
1
+ # The searchable dropdown (`Combo`)
2
+
3
+ **Use this instead of shadcn's `<Select>` and instead of a native `<select>`, always.**
4
+ A Vibe Coding block renders inside a **shadow DOM**, and that one fact rules out both of
5
+ the obvious choices:
6
+
7
+ | Option | Why it fails in a block |
8
+ |---|---|
9
+ | Native `<select>` | Hands the list to the OS. No keyword filter, none of your styling, and on macOS it paints a grey slab over the page. Fine for 5 options, unusable at 90. |
10
+ | shadcn `<Select>` / `<Command>` | **Portals to `document.body`, which is outside the block's shadow root**, so the styles arrive stripped. It also cannot be searched. |
11
+ | `Combo` (below) | Local DOM, brand-styled, keyword filter, A→Z, keyboard, create-new, drop-up. |
12
+
13
+ Copy the component into the block. A Vibe block is one self-contained file — there is no
14
+ shared module to import, so each block carries its own copy. Keep one canonical copy in the
15
+ project (e.g. `Assets/Softr App/Shared/combo.jsx`) and port changes from there.
16
+
17
+ ## The three things that will bite you
18
+
19
+ **1. Click-outside must use `composedPath()`, not `contains()`.**
20
+ By the time a click reaches `document`, the shadow DOM has *retargeted* its `target` to the
21
+ shadow **host**. So the usual `root.contains(e.target)` test calls the panel's own rows
22
+ "outside" and closes the panel before the click can land on one — the dropdown appears to
23
+ ignore every selection.
24
+
25
+ ```jsx
26
+ function onDown(e) {
27
+ var root = rootRef.current;
28
+ if (!root) return;
29
+ var path = e.composedPath ? e.composedPath() : [];
30
+ for (var i = 0; i < path.length; i++) {
31
+ if (path[i] === root) return; // inside — leave it open
32
+ }
33
+ if (path.length === 0 && e.target && root.contains(e.target)) return; // fallback
34
+ setOpen(false);
35
+ }
36
+ document.addEventListener("mousedown", onDown, true);
37
+ ```
38
+
39
+ **2. Define it at MODULE scope, never inside `Block()`.**
40
+ A component redefined inside `Block()` gets a fresh identity every render, so React unmounts
41
+ and remounts the `<input>` and the search box loses focus after one keystroke. This is the
42
+ single most common Vibe Coding bug and it looks like a platform fault.
43
+
44
+ **3. `onMouseDown` on a row must `preventDefault()`.**
45
+ Otherwise focus leaves the search input before the click resolves.
46
+
47
+ ## Sort A→Z *inside* the component
48
+
49
+ Sorting at the call site gets forgotten. Do it in the component, with an opt-out:
50
+
51
+ ```jsx
52
+ function comboSortLabels(a, b) {
53
+ return String(a.label || "").localeCompare(String(b.label || ""), undefined, {
54
+ sensitivity: "base", // case-insensitive
55
+ numeric: true, // "Item 2" before "Item 10" — a plain compare gets this backwards
56
+ });
57
+ }
58
+ ```
59
+
60
+ `autoSort` defaults to **true**. Pass `autoSort={false}` only where the given order *is* the
61
+ meaning — a pipeline of statuses (Not ordered → Ordered → … → Delivered), a physical
62
+ journey, a curated short list. Alphabetising a workflow puts "Damaged" first and buries the
63
+ starting state in the middle, which is worse than not sorting at all.
64
+
65
+ Grouped lists sort **within** a group and keep first-seen group order, so a "this project's
66
+ rooms first, everyone else after" grouping survives being alphabetised.
67
+
68
+ ## Filter on every token, in any order
69
+
70
+ ```jsx
71
+ var tokens = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
72
+ var filtered = options.filter(function (o) {
73
+ if (tokens.length === 0) return true;
74
+ var hay = String(o.label || "").toLowerCase();
75
+ for (var i = 0; i < tokens.length; i++) {
76
+ if (hay.indexOf(tokens[i]) === -1) return false;
77
+ }
78
+ return true;
79
+ });
80
+ ```
81
+
82
+ So `rural art` finds *The Rural Art Company*. A single `indexOf(query)` would not.
83
+
84
+ ## Show the search box only when it earns its place
85
+
86
+ ```jsx
87
+ var searchable = props.onCreate
88
+ ? true
89
+ : props.searchable === true || (props.searchable !== false && options.length >= 8);
90
+ ```
91
+
92
+ Under eight options a search box is noise — the picker should still read as a picker.
93
+
94
+ ## One flat row list for the keyboard
95
+
96
+ Build `rows` as a single array — the optional *clear* row, then the filtered options, then
97
+ the optional *create* row — so arrow-key navigation has one index to walk. Clamp the active
98
+ index (`Math.min(active, rows.length - 1)`): filtering shrinks the list under the highlight.
99
+
100
+ ## Drop up near the fold
101
+
102
+ ```jsx
103
+ var r = rootRef.current.getBoundingClientRect();
104
+ var below = window.innerHeight - r.bottom;
105
+ setDropUp(below < 300 && r.top > below);
106
+ ```
107
+
108
+ A filter row near the bottom of the viewport otherwise opens into nothing.
109
+
110
+ ## Variants worth having
111
+
112
+ - **`bare`** — inline-editor mode. No border, no fill; `triggerContent` (a status chip, a
113
+ cell's text) *is* the trigger. Lets a table cell become editable without every row growing
114
+ a form control.
115
+ ⚠ If any column width in your table is derived from the trigger's chrome, keep the
116
+ chevron the SAME size in both variants. Shrinking it in `bare` silently changes those
117
+ widths in a different file.
118
+ - **`triggerStyle`** — merged over the defaults, for a trigger that is part of the design
119
+ (a chip painted in its own status colour) rather than a plain field.
120
+ - **`onCreate(text)`** — offers `Add "<typed>"` when nothing matches. If creating the record
121
+ and *linking* it happen at different times, say so in the toast: the two halves landing
122
+ separately is exactly what gets reported as "it didn't save".
123
+ - **`emptyLabel`** — a row that clears the selection. Call sites that pass a leading
124
+ `{ value: "" }` "any / none" option should have it lifted into `emptyLabel` rather than
125
+ rendered twice.
126
+
127
+ ## The one shadow you are allowed
128
+
129
+ DESIGN-system rules that ban shadows are about cards. A white panel floating over a white
130
+ card with only a hairline between them reads as part of the card, so the menu gets a shadow:
131
+
132
+ ```js
133
+ boxShadow: "0 10px 30px rgba(0, 0, 0, 0.16)"
134
+ ```
135
+
136
+ Everything else — the trigger, the card, the rows — stays flat.
137
+
138
+ ## Checklist before shipping a dropdown
139
+
140
+ - [ ] Defined at module scope
141
+ - [ ] `composedPath()` click-outside
142
+ - [ ] Sorted A→Z inside the component, with `autoSort={false}` only where order is meaning
143
+ - [ ] Multi-token filter
144
+ - [ ] Search box hidden under 8 options
145
+ - [ ] Keyboard: ↑ ↓ Enter Esc Tab, active row scrolled into view
146
+ - [ ] `aria-haspopup="listbox"`, `aria-expanded`, `role="listbox"` / `role="option"`,
147
+ `aria-selected`, and an `aria-label` on the trigger
148
+ - [ ] Loading and empty states (`"Nothing matches that."`)
149
+ - [ ] No `@/components/ui/select` import anywhere in the file