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 +3 -0
- package/README.md +8 -3
- package/SKILL.md +2 -0
- package/package.json +1 -1
- package/references/searchable-dropdown.md +149 -0
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
|
-
│
|
|
224
|
-
│
|
|
225
|
-
│
|
|
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.
|
|
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
|