softr-vibe-coding 1.1.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/LICENSE +21 -0
- package/README.md +262 -0
- package/SKILL.md +480 -0
- package/bin/cli.js +140 -0
- package/datasources/airtable.md +91 -0
- package/datasources/bigquery.md +36 -0
- package/datasources/clickup.md +82 -0
- package/datasources/coda.md +44 -0
- package/datasources/fields.md +203 -0
- package/datasources/google-sheets.md +46 -0
- package/datasources/hubspot.md +51 -0
- package/datasources/monday.md +52 -0
- package/datasources/notion.md +56 -0
- package/datasources/overview.md +41 -0
- package/datasources/reading.md +222 -0
- package/datasources/rest-api.md +206 -0
- package/datasources/shared-patterns.md +9 -0
- package/datasources/smartsuite.md +39 -0
- package/datasources/softr-database.md +47 -0
- package/datasources/sql-database.md +48 -0
- package/datasources/supabase.md +38 -0
- package/datasources/writing.md +256 -0
- package/datasources/xano.md +37 -0
- package/package.json +40 -0
- package/references/advanced-integrations.md +69 -0
- package/references/airtable-automations.md +350 -0
- package/references/anti-patterns.md +86 -0
- package/references/common-patterns.md +102 -0
- package/references/helper-blocks.md +370 -0
- package/references/quick-reference.md +207 -0
- package/ui-ux-guidelines.md +746 -0
package/SKILL.md
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: softr-vibe-coding
|
|
3
|
+
description: >
|
|
4
|
+
Generate custom Softr Vibe Coding blocks as complete JSX components. Use this skill whenever the user
|
|
5
|
+
mentions Softr, Vibe Coding, Softr blocks, or wants to build a custom UI component for a Softr app.
|
|
6
|
+
Also trigger when the user asks to create cards, lists, forms, dashboards, charts, detail pages,
|
|
7
|
+
or any interactive block intended for Softr — even if they don't say "Vibe Coding" explicitly.
|
|
8
|
+
If the user mentions Softr in the context of building a custom UI component, creating a JSX block,
|
|
9
|
+
or vibe coding, use this skill.
|
|
10
|
+
when_to_use: >
|
|
11
|
+
Triggers on "build me a Softr block", "create a card component", "make a dashboard",
|
|
12
|
+
"vibe code this", "custom block for Softr", "JSX component for Softr app",
|
|
13
|
+
"create a form block", "build a list view", "make a portal page",
|
|
14
|
+
"Softr custom component", "vibe coding block".
|
|
15
|
+
effort: max
|
|
16
|
+
allowed-tools: Read Write Glob Grep Bash
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Softr Vibe Coding Block Generator
|
|
20
|
+
|
|
21
|
+
You generate complete, production-ready Softr Vibe Coding blocks as JSX files. A Vibe Coding block is a JavaScript file with a default-exported React component that runs exclusively in the browser inside a Softr app.
|
|
22
|
+
|
|
23
|
+
## Your Workflow
|
|
24
|
+
|
|
25
|
+
1. **Detect the brand source (always run first, before any block work).** Check if a `./DESIGN.md` file exists in the project folder you're about to work in.
|
|
26
|
+
|
|
27
|
+
- **If `./DESIGN.md` is found:** Read its frontmatter and confirm with the user:
|
|
28
|
+
|
|
29
|
+
> "I found a DESIGN.md in this project (brand: `<name>`, source: `<source>`, extracted: `<date>`). Use its brand tokens for this block?
|
|
30
|
+
> 1. Yes — use this DESIGN.md
|
|
31
|
+
> 2. No — use the default Softr style instead
|
|
32
|
+
> 3. No — I'll paste a different brand override"
|
|
33
|
+
|
|
34
|
+
If (1), load every relevant section: `colors`, `typography`, `rounded`, `elevation`, `components`, and especially the `Application Patterns` scaffold. Apply those tokens throughout the block. Honour the `tech_stack` block — it may pin specific shadcn variants or note bundler quirks.
|
|
35
|
+
|
|
36
|
+
If (2), proceed with the default Softr style (see Step 3).
|
|
37
|
+
|
|
38
|
+
If (3), accept the override and apply it.
|
|
39
|
+
|
|
40
|
+
- **If `./DESIGN.md` is NOT found:** Tell the user:
|
|
41
|
+
|
|
42
|
+
> "No DESIGN.md found in this project. Three options:
|
|
43
|
+
> A. **Set up a brand foundation first** — run `building-design-md` to extract brand tokens from the client's website or a brand guide, then come back here. (Recommended for client work.)
|
|
44
|
+
> B. **Quick brand override** — paste the brand's primary color, accent color, and font name now. I'll apply just those.
|
|
45
|
+
> C. **Use the default Softr style** — primary `#386AF5`, accent `#FCB500`, Inter font."
|
|
46
|
+
|
|
47
|
+
Wait for their pick. If (A), end the skill — the user will run `building-design-md` and then re-invoke this skill. If (B) or (C), record their choice for Step 3 and continue.
|
|
48
|
+
|
|
49
|
+
Do not silently default to Softr's brand. The user must opt in to defaults explicitly.
|
|
50
|
+
|
|
51
|
+
2. **Understand what the user wants to build.** They will describe it in plain language. Only ask about things you genuinely cannot infer: **data source type** and **field IDs**. For everything else, make sensible defaults and flag your assumptions.
|
|
52
|
+
|
|
53
|
+
3. **Apply defaults for the rest, don't ask.** Infer these from context instead of asking:
|
|
54
|
+
- **Project folder**: Derive from the block description (e.g., "partner-portal", "client-dashboard"). If the user has already specified a folder in this session, reuse it.
|
|
55
|
+
- **Brand colors**: Use whatever was chosen in Step 1 — DESIGN.md tokens, the user's override, or the default Softr palette (primary `#386AF5`, accent `#FCB500`). Never silently fall back to defaults.
|
|
56
|
+
- **Filename**: Derive from the block purpose (e.g., `partner-invite.jsx`, `team-directory.jsx`). The user can rename later.
|
|
57
|
+
|
|
58
|
+
4. **Load the relevant data source guide** from [datasources/](datasources/) before writing code. Read the specific guide for the user's data source type.
|
|
59
|
+
|
|
60
|
+
5. **Write the complete `.jsx` file** to the project sub-folder and tell the user the full path. Create the sub-folder if it doesn't exist yet. The file must be fully self-contained, **visually polished from the first version**, and ready to paste into Softr's Vibe Coding editor. Styling is not an afterthought -- it ships in v1. **Never deliver code inline in chat.** Copy-pasting JSX from chat corrupts characters (`>`, `>=`, `=>`, quotes), causing compilation errors that are hard to debug. Always write to a file.
|
|
61
|
+
|
|
62
|
+
6. **Self-validate before delivering.** Before presenting the code as complete, verify:
|
|
63
|
+
- No optional chaining (`?.`) or nullish coalescing (`??`)
|
|
64
|
+
- All imports use named imports (no `import React from 'react'`)
|
|
65
|
+
- `export default function Block()` is present
|
|
66
|
+
- Container + content wrappers present (`<div className="container py-0"><div className="content">`)
|
|
67
|
+
- `// BLOCK PLACEMENT:` comment present at top of file with wrapper classes matching the placement (see "Block Placement & Page Spacing")
|
|
68
|
+
- Loading, error, and empty states all handled
|
|
69
|
+
- Mutation calls gated behind `enabled` check (if using mutations)
|
|
70
|
+
- Field access uses `record.fields.alias` (not `record.alias`)
|
|
71
|
+
- Every field rendered in JSX wrapped in `getFieldValue()` -- prevents React error #31
|
|
72
|
+
- All hooks declared before any conditional `return` -- prevents React error #310
|
|
73
|
+
- Sub-components (FieldLabel, TextInput, ChipButton, SectionCard, etc.) defined at **module scope**, NOT inside `Block()` -- prevents inputs losing focus after one keystroke (each render creates a new component identity, React unmounts/remounts the `<input>`)
|
|
74
|
+
- When a custom DESIGN.md is in use, brand `fontFamily` (and any non-inherited brand defaults) set as an **inline style on the block's outermost wrapper** `<div>`, not relied on from `custom-code-header.html` -- Vibe Coding blocks render inside a shadow DOM and `html, body` rules don't cross that boundary. Per-element overrides (e.g. Fraunces serif on h1) still set inline at the element.
|
|
75
|
+
- `fetchNextPage` only inside `useEffect`, never in render body
|
|
76
|
+
- Mutations use `recordId` (not `id`) and call `refetch()` in `onSuccess`
|
|
77
|
+
- `useRecordUpdate` calls use `.mutate(payload, { onSuccess, onError })` — **NOT** `.mutateAsync(...).then(...).catch(...)`. Softr's Action parser only recognizes the `.mutate(` token; `.mutateAsync(` is invisible to it and the Action never gets derived (`enabled` stays `false`, Actions tab shows "No actions used in this block yet")
|
|
78
|
+
- `useRecordUpdate` payload is `{ recordId, fields: { ... } }` — nested, not flat. Field references inside flat payloads are invisible to the parser, same silent-failure mode as the `.mutateAsync` issue
|
|
79
|
+
- No hardcoded domains in links -- use relative paths (`/page?recordId=...`)
|
|
80
|
+
|
|
81
|
+
## What to Clarify
|
|
82
|
+
|
|
83
|
+
When the user describes their block, figure out which of these areas apply and ask about anything you're missing:
|
|
84
|
+
|
|
85
|
+
- **Data source type**: Is it Airtable, Softr Database, REST API, or another source? This determines the data fetching approach. **Load the relevant data source guide** from the [datasources/](datasources/) directory before writing code.
|
|
86
|
+
- **Data source fields**: For Airtable/Softr Database, you need actual field IDs. For REST APIs, you access the raw API response directly. If the user doesn't know field IDs:
|
|
87
|
+
- For **Softr Database**, ask the user to paste the `tablespace-with-tables` network response (DevTools -> Network -> filter that string while on Studio's Data tab). The JSON contains every field ID, type, and dropdown option UUID -- the most reliable way to receive accurate schema without transcription errors. See [datasources/fields.md](datasources/fields.md#field-inspector-block).
|
|
88
|
+
- For **Airtable** and other sources where empty `q.select({})` works, suggest the Field Inspector block.
|
|
89
|
+
- **Brand colors**: Already resolved in Step 1 (Detect the brand source). Don't re-ask. The brand source is one of:
|
|
90
|
+
- **Project's `./DESIGN.md`** (recommended for client work — produced by the `building-design-md` skill)
|
|
91
|
+
- **User's quick override** (paste of primary + accent + font)
|
|
92
|
+
- **Default Softr palette** (only when the user explicitly opted in — never as a silent fallback):
|
|
93
|
+
|
|
94
|
+
| Color | Hex | Name | Use |
|
|
95
|
+
|---|---|---|---|
|
|
96
|
+
| Primary | `#386AF5` | Mariner (blue) | CTAs, links, active states |
|
|
97
|
+
| Accent | `#FCB500` | Yellow Sea | Highlights, badges, sparkle accents |
|
|
98
|
+
| Destructive | `#F53878` | Cabaret (pink) | Errors, destructive actions, required markers |
|
|
99
|
+
| Text | `#030712` | Revolver (near-black) | Body text, headings |
|
|
100
|
+
| Background | `#FFFFFF` | White | Page and card backgrounds |
|
|
101
|
+
|
|
102
|
+
Softr logo assets (for blocks that need Softr branding):
|
|
103
|
+
- Icon + wordmark (SVG): `https://cdn.brandfetch.io/idytCFzVcY/theme/dark/logo.svg`
|
|
104
|
+
- Icon only (PNG): `https://cdn.brandfetch.io/idytCFzVcY/w/1024/h/1024/theme/dark/icon.png`
|
|
105
|
+
- **Layout and style**: Cards vs. table vs. list? How many columns? Apply the Premium Visual Baseline regardless.
|
|
106
|
+
- **Interactivity**: Create/edit/delete? Filtering? Sorting? Pagination?
|
|
107
|
+
- **User context**: Does it need to know who's logged in?
|
|
108
|
+
- **Settings**: Should anything be editable by the Softr builder (titles, images, toggle sections)?
|
|
109
|
+
|
|
110
|
+
Don't over-ask. If the user gives a clear description, fill in sensible defaults and note your assumptions.
|
|
111
|
+
|
|
112
|
+
## Examples (Decision Traces)
|
|
113
|
+
|
|
114
|
+
**User:** "Build a team directory with cards showing name, role, and photo from Airtable"
|
|
115
|
+
**Claude:** Reads `datasources/airtable.md` -> uses `q.select()` with Airtable column names (e.g., `"Full Name"`, `"Role"`, `"Headshot"`) -> card grid layout with `repeat(auto-fit, minmax(280px, 1fr))` -> avatar with brand-color fallback initials -> loading skeleton matching card shape -> empty state with "No team members yet"
|
|
116
|
+
|
|
117
|
+
**User:** "I need a form that pulls events from the Luma API and sends a webhook"
|
|
118
|
+
**Claude:** Reads `datasources/rest-api.md` -> uses `useProxyFetch` + `useQuery` (NOT `useRecords`) -> accesses raw API response directly (`event.name`, not `record.fields.name`) -> Select dropdown with event name + formatted date -> webhook via regular `fetch()` (not proxied) -> loading/error/empty states
|
|
119
|
+
|
|
120
|
+
## Data Sources
|
|
121
|
+
|
|
122
|
+
Softr supports 14 data sources. **Before writing any data-fetching code, read the relevant guide:**
|
|
123
|
+
|
|
124
|
+
| Data Source | Guide | Approach |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| Softr Databases | [datasources/softr-database.md](datasources/softr-database.md) | `useRecords` + `q.select()` |
|
|
127
|
+
| Airtable | [datasources/airtable.md](datasources/airtable.md) | `useRecords` + `q.select()` |
|
|
128
|
+
| Google Sheets | [datasources/google-sheets.md](datasources/google-sheets.md) | `useRecords` + `q.select()` |
|
|
129
|
+
| HubSpot | [datasources/hubspot.md](datasources/hubspot.md) | `useRecords` + `q.select()` |
|
|
130
|
+
| Notion | [datasources/notion.md](datasources/notion.md) | `useRecords` + `q.select()` |
|
|
131
|
+
| Coda | [datasources/coda.md](datasources/coda.md) | `useRecords` + `q.select()` |
|
|
132
|
+
| monday.com | [datasources/monday.md](datasources/monday.md) | `useRecords` + `q.select()` |
|
|
133
|
+
| SmartSuite | [datasources/smartsuite.md](datasources/smartsuite.md) | `useRecords` + `q.select()` |
|
|
134
|
+
| ClickUp | [datasources/clickup.md](datasources/clickup.md) | `useRecords` + `q.select()` |
|
|
135
|
+
| Xano | [datasources/xano.md](datasources/xano.md) | `useRecords` + `q.select()` |
|
|
136
|
+
| Supabase | [datasources/supabase.md](datasources/supabase.md) | `useRecords` + `q.select()` |
|
|
137
|
+
| BigQuery | [datasources/bigquery.md](datasources/bigquery.md) | `useRecords` + `q.select()` |
|
|
138
|
+
| SQL Database | [datasources/sql-database.md](datasources/sql-database.md) | `useRecords` + `q.select()` |
|
|
139
|
+
| REST API | [datasources/rest-api.md](datasources/rest-api.md) | `useProxyFetch` + `useQuery` |
|
|
140
|
+
|
|
141
|
+
For shared data fetching patterns (useRecords, mutations, uploads, metrics, charts), see [datasources/shared-patterns.md](datasources/shared-patterns.md).
|
|
142
|
+
|
|
143
|
+
For data source comparison and selection guidance, see [datasources/overview.md](datasources/overview.md).
|
|
144
|
+
|
|
145
|
+
## Reference Guides
|
|
146
|
+
|
|
147
|
+
For advanced patterns beyond data fetching, load the relevant reference when the task needs it:
|
|
148
|
+
|
|
149
|
+
| If the task involves... | Load reference |
|
|
150
|
+
|---|---|
|
|
151
|
+
| Cross-block communication, multi-table data access, invisible helper blocks, window globals, breadcrumbs | [references/helper-blocks.md](references/helper-blocks.md) |
|
|
152
|
+
| Embedding third-party libraries with their own CSS (Leaflet, Mapbox, TinyMCE, Quill, FullCalendar) | [references/advanced-integrations.md](references/advanced-integrations.md) |
|
|
153
|
+
| Debugging a broken block, checking patterns before delivery, full violation catalog | [references/anti-patterns.md](references/anti-patterns.md) |
|
|
154
|
+
| Quick syntax check — import paths, hook signatures, mutation call shapes, field mapping | [references/quick-reference.md](references/quick-reference.md) |
|
|
155
|
+
| Small reusable patterns — `localStorage` cross-page state, clipboard copy button | [references/common-patterns.md](references/common-patterns.md) |
|
|
156
|
+
| 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) |
|
|
157
|
+
|
|
158
|
+
## Code Structure
|
|
159
|
+
|
|
160
|
+
Every block follows this shape:
|
|
161
|
+
|
|
162
|
+
```jsx
|
|
163
|
+
// imports at the top
|
|
164
|
+
import { ... } from "@/lib/datasource";
|
|
165
|
+
// ...other imports
|
|
166
|
+
|
|
167
|
+
export default function Block() {
|
|
168
|
+
// hooks, state, logic
|
|
169
|
+
return (
|
|
170
|
+
<div className="container py-0">
|
|
171
|
+
<div className="content">
|
|
172
|
+
<div className="py-3 px-8">
|
|
173
|
+
{/* block content — wrapper padding depends on placement; see "Block Placement & Page Spacing" */}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Always wrap the outermost layout in `container` and `content` divs — these constrain width to match the Softr app's max width settings.
|
|
182
|
+
|
|
183
|
+
**Exception:** Blocks inside Softr column containers — omit wrappers so Softr controls layout.
|
|
184
|
+
|
|
185
|
+
## Block Placement & Page Spacing
|
|
186
|
+
|
|
187
|
+
Blocks rarely live alone — most Softr pages stack 2–4 blocks vertically, often between a header and a footer. Spacing must be set per-block based on **where the block sits on the page**, so adjacent blocks don't double up padding or leave inconsistent gaps.
|
|
188
|
+
|
|
189
|
+
**General rule:** the inner wrapper (the `<div>` directly inside `<div className="content">`) owns all vertical spacing. The outer `container` is always `py-0`. Top and bottom padding on the wrapper change based on what's above and below the block (another block, a header, a footer, or nothing).
|
|
190
|
+
|
|
191
|
+
**When generating a new block, if the placement is not clear from the user's description, ASK before writing code:**
|
|
192
|
+
- Where will this block sit on the page? (top / middle / bottom / standalone)
|
|
193
|
+
- Is there a Softr header immediately above this block?
|
|
194
|
+
- Is there a Softr footer immediately below this block?
|
|
195
|
+
- Is there a Back button at the top of this block?
|
|
196
|
+
|
|
197
|
+
**Detail pages — always ask about the back button AND its fallback URL.** A "detail page" is any block that reads a single record by URL recordId (i.e. it calls `useCurrentRecordId()` / `useRecord()`, or the user describes it as the target of a `/page?recordId=...` link). Users almost always want a back button there but rarely think to mention it, and shipping the page without one is the most common UX gap on these screens. So even if every other placement detail is clear, ask both:
|
|
198
|
+
|
|
199
|
+
1. **"Should the detail page have a back button?"** — if yes, always wire one. Use the back-navigation pattern in [references/helper-blocks.md](references/helper-blocks.md#breadcrumb--back-navigation): `window.history.back()` for users with history, plus a fallback URL for users who arrived via shared link.
|
|
200
|
+
2. **"What page should the back button fall back to when there's no history?"** — this is a separate question, easy to skip but important. Don't default silently; ask. If the user doesn't have a listing page yet, default to `/` and leave a `// TODO: update fallback when /jobs (or similar) exists` comment so it can be updated later.
|
|
201
|
+
|
|
202
|
+
**Persist the answer as a grep-able comment at the top of the generated file** so future edits know the spacing assumptions and can be updated consistently:
|
|
203
|
+
|
|
204
|
+
```jsx
|
|
205
|
+
// BLOCK PLACEMENT: <position on page>, <header/footer adjacency>, <back button y/n>
|
|
206
|
+
// Spacing: <wrapper classes; back-button container if present>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
|
|
211
|
+
```jsx
|
|
212
|
+
// BLOCK PLACEMENT: first block on page, header-adjacent, has Back button
|
|
213
|
+
// Spacing: wrapper py-3 px-8; back-button container mt-6 mb-4
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The `// BLOCK PLACEMENT:` marker is intentionally stable so it can be grepped and updated when the block's surroundings change.
|
|
217
|
+
|
|
218
|
+
### Spacing values (defaults)
|
|
219
|
+
|
|
220
|
+
**Container** (always): `<div className="container py-0">`
|
|
221
|
+
|
|
222
|
+
**Inner wrapper** classes by block position:
|
|
223
|
+
|
|
224
|
+
| Position on page | Wrapper classes | Rationale |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| First block (header-adjacent) | `py-3 px-8` | 12px top + 12px bottom; lets the Softr header own its own spacing |
|
|
227
|
+
| Middle block | `py-3 px-8` | 12px + Softr separator + 12px ≈ 24px between blocks |
|
|
228
|
+
| Last block (footer-adjacent) | `pt-3 pb-12 px-8` | 12px top + 48px bottom for footer breathing room |
|
|
229
|
+
| Standalone (only block on page) | `pt-3 pb-12 px-8` | Treat like a last block |
|
|
230
|
+
|
|
231
|
+
**Back button** (when present at the top of a block — typically on detail pages): wrap in `<div className="mt-6 mb-4">`. The `mt-6` (24px) adds breathing room above the button independent of wrapper padding; `mb-4` (16px) sits between the button and the first card. Apply this regardless of whether the block is first or mid-page.
|
|
232
|
+
|
|
233
|
+
**Within-block stacked cards**: each card uses `mb-6` (24px). **Do NOT add `mb-6` to the last card** in a block — the wrapper's bottom padding already handles that buffer. Doubling them produces 32–40px gaps that look bigger than the within-block rhythm.
|
|
234
|
+
|
|
235
|
+
**Net page rhythm**: between-block gaps (12 + 12 = 24px) match within-block card gaps (`mb-6` = 24px), so the page reads as one consistent vertical rhythm.
|
|
236
|
+
|
|
237
|
+
## Premium Visual Baseline
|
|
238
|
+
|
|
239
|
+
**Every block must look polished in its first version.** Styling is not a follow-up task — it is a core requirement of every code generation. Apply ALL of the following by default unless the user explicitly requests a minimal/plain style.
|
|
240
|
+
|
|
241
|
+
Refer to [ui-ux-guidelines.md](ui-ux-guidelines.md) for full design principles.
|
|
242
|
+
|
|
243
|
+
### 1. Gradient background wrapper
|
|
244
|
+
```jsx
|
|
245
|
+
<div className="rounded-2xl p-8" style={{ background: "linear-gradient(180deg, #EEF2FF 0%, #FFFFFF 100%)" }}>
|
|
246
|
+
{/* header + content cards go inside here */}
|
|
247
|
+
</div>
|
|
248
|
+
```
|
|
249
|
+
Adjust the top gradient color to complement the user's brand.
|
|
250
|
+
|
|
251
|
+
### 2. Header section
|
|
252
|
+
- Icon in a colored rounded square (`h-10 w-10 rounded-xl` with brand primary, white icon)
|
|
253
|
+
- Title at `text-2xl font-bold`
|
|
254
|
+
- Optional subtitle in `text-muted-foreground`
|
|
255
|
+
- Primary CTA button with `shadow-md hover:shadow-lg transition-shadow`
|
|
256
|
+
|
|
257
|
+
### 3. Card-based content
|
|
258
|
+
- `bg-white rounded-xl shadow-sm border border-gray-100`
|
|
259
|
+
- Items with `hover:shadow-md hover:border-gray-200 transition-all duration-200`
|
|
260
|
+
- Use `space-y-3` or `gap-3`, never flat separators
|
|
261
|
+
|
|
262
|
+
### 4. Avatar and identity elements
|
|
263
|
+
- Brand primary color as avatar fallback with white initials
|
|
264
|
+
- `h-12 w-12` for list items, `h-28 w-28` for profiles
|
|
265
|
+
- `border-2 border-white shadow-lg` on profile avatars
|
|
266
|
+
|
|
267
|
+
### 5. Interactive feedback
|
|
268
|
+
- Buttons: `shadow-md hover:shadow-lg transition-shadow`
|
|
269
|
+
- Cards: `hover:shadow-md hover:border-gray-200 transition-all duration-200`
|
|
270
|
+
- Active states: blue left border accent (`border-l-4`)
|
|
271
|
+
|
|
272
|
+
### 6. Status and metadata
|
|
273
|
+
- Counts in pill badges: `text-xs font-medium px-2 py-0.5 rounded-full`
|
|
274
|
+
- Dates with icons (Calendar, Mail, Users)
|
|
275
|
+
|
|
276
|
+
### 7. Empty states
|
|
277
|
+
- Large icon in gradient square (`h-20 w-20 rounded-2xl`)
|
|
278
|
+
- Clear heading + explanation + CTA button
|
|
279
|
+
|
|
280
|
+
### 8. Loading states
|
|
281
|
+
- Skeleton shapes matching the final layout, `rounded-xl`
|
|
282
|
+
|
|
283
|
+
### 9. Error states
|
|
284
|
+
- Icon in tinted background, clear message, retry button
|
|
285
|
+
|
|
286
|
+
### 10. Modals and dialogs
|
|
287
|
+
- Icon in dialog title, required field markers, example placeholders
|
|
288
|
+
|
|
289
|
+
## Styling & Components
|
|
290
|
+
|
|
291
|
+
**Tailwind CSS** is pre-configured. **Semantic color tokens** preferred:
|
|
292
|
+
`bg-background`, `bg-card`, `bg-primary`, `bg-secondary`, `bg-muted`, `bg-accent`, `bg-destructive`, `border`, `border-input`
|
|
293
|
+
|
|
294
|
+
**Font classes:** `font-heading`, `font-sans`, `font-mono`
|
|
295
|
+
|
|
296
|
+
**Conditional classNames:** `import { cn } from "@/lib/utils";`
|
|
297
|
+
|
|
298
|
+
**DO NOT USE:** CSS modules, styled-components, or CSS file imports.
|
|
299
|
+
|
|
300
|
+
**shadcn/ui components** at `@/components/ui/[name]`:
|
|
301
|
+
accordion, alert, alert-dialog, aspect-ratio, avatar, badge, button, calendar, card, carousel, chart, checkbox, collapsible, command, context-menu, dialog, drawer, dropdown-menu, empty, hover-card, input, input-group, input-otp, item, kbd, label, menubar, native-select, navigation-menu, pagination, popover, progress, radio-group, resizable, scroll-area, select, separator, sheet, skeleton, slider, sonner, spinner, switch, table, tabs, textarea, toggle, toggle-group, tooltip
|
|
302
|
+
|
|
303
|
+
**Common import patterns:**
|
|
304
|
+
|
|
305
|
+
```jsx
|
|
306
|
+
import { Button } from "@/components/ui/button";
|
|
307
|
+
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
|
308
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
|
309
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
310
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
|
311
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
312
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
|
313
|
+
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
|
|
314
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Lucide Icons:** `import { TrendingUp, User, Settings } from "lucide-react";`
|
|
318
|
+
|
|
319
|
+
**Dynamic icons by name string:** `<DynamicIcon>` from `@/components/dynamic-icon` renders a Lucide icon when you only know its name at runtime (e.g. from an editable setting, a window global, or a `useRecords` row). Pass the kebab-case Lucide name as `name`. Use this — not a manual lookup table — whenever the icon is data-driven.
|
|
320
|
+
|
|
321
|
+
```jsx
|
|
322
|
+
import { DynamicIcon } from "@/components/dynamic-icon";
|
|
323
|
+
<DynamicIcon name="trending-up" className="h-5 w-5" />
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Softr's `<NavigationAction>`** is a built-in component for triggering Softr-native navigation actions from inside a Vibe Coding block. Accepts a `recordId` prop for dynamic record-specific URLs and supports action types `OPEN_URL`, `OPEN_PAGE`, `OPEN_CHAT`, `TRIGGER_CUSTOM_WORKFLOW`. Prefer this over bare `<a href>` when you want Softr's user-group visibility checks and slide-out / modal navigation styles to apply. Bare `<a href>` is still fine for simple in-app links where you don't need that behavior.
|
|
327
|
+
|
|
328
|
+
The canonical pattern pairs a shadcn `<Button asChild>` with `<NavigationAction navigation={...}>` so the button's styling stays on-brand while the navigation behavior is handled by Softr's component:
|
|
329
|
+
|
|
330
|
+
```jsx
|
|
331
|
+
import { Button } from "@/components/ui/button";
|
|
332
|
+
import { NavigationAction } from "@/components/navigation-action";
|
|
333
|
+
import { useNavigationSetting } from "@/lib/editable-settings";
|
|
334
|
+
import { MessageSquare } from "lucide-react";
|
|
335
|
+
|
|
336
|
+
var askAi = useNavigationSetting({
|
|
337
|
+
name: "ask-ai-action",
|
|
338
|
+
label: "Ask AI Action",
|
|
339
|
+
initialValue: { action: "OPEN_CHAT" }, // OPEN_CHAT needs no destination
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
<Button asChild size="lg" className="gap-2">
|
|
343
|
+
<NavigationAction navigation={askAi}>
|
|
344
|
+
<MessageSquare className="h-5 w-5" />
|
|
345
|
+
Ask AI about this
|
|
346
|
+
</NavigationAction>
|
|
347
|
+
</Button>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**Action types — what each one needs in `initialValue`:**
|
|
351
|
+
|
|
352
|
+
- `OPEN_CHAT` — opens Softr's AI chat. **No `destination` or `openIn` needed** — it's the cheapest "Ask AI" button to wire up. **GOTCHA:** Softr's AI pulls context from the block that triggered the chat, NOT from the page. If the block has no data source connected, `chat/prepare` returns HTTP 500 ("Failed to prepare AI assistant") even though the chat panel opens. Fix: in Softr Studio, connect the block to whatever data source the AI should read from — even if the block doesn't read or write any records itself, the connection is what gives the AI context. Verified by direct experiment, May 2026: a button-only helper block with no data source caused this exact failure; connecting it to the same table as the main block fixed it without any code change.
|
|
353
|
+
- `OPEN_URL` — opens an external URL. Needs `destination` (the URL) + `openIn` (`"SELF"` | `"TAB"`).
|
|
354
|
+
- `OPEN_PAGE` — navigates to a Softr page in-app. Needs `destination` (page path) + `openIn` (`"SELF"` | `"TAB"` | `"MODAL"`).
|
|
355
|
+
- `TRIGGER_CUSTOM_WORKFLOW` — runs a Softr workflow. Needs the workflow id in `destination`.
|
|
356
|
+
|
|
357
|
+
When the action navigates to a record-specific page, pass the runtime record id via the `recordId` prop on `<NavigationAction>` (not on the setting) so Softr can resolve dynamic URLs:
|
|
358
|
+
|
|
359
|
+
```jsx
|
|
360
|
+
<NavigationAction navigation={openWigDetails} recordId={wig.id}>
|
|
361
|
+
View wig
|
|
362
|
+
</NavigationAction>
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
For on-brand custom styling (matching DESIGN.md inline-style conventions instead of shadcn Button), wrap the same pattern with a styled `<button>` — `<NavigationAction>` will render its children into the button's slot. The `asChild` attribute on Button is what enables this slot composition; without it shadcn renders its own native button and ignores `<NavigationAction>`.
|
|
366
|
+
|
|
367
|
+
**Any public npm package** auto-installs on import: `import { format } from "date-fns";`
|
|
368
|
+
|
|
369
|
+
### React Hooks — Critical Import Rule
|
|
370
|
+
|
|
371
|
+
```jsx
|
|
372
|
+
// CORRECT
|
|
373
|
+
import { useState, useEffect } from "react";
|
|
374
|
+
|
|
375
|
+
// WRONG — All of these fail:
|
|
376
|
+
const { useState } = React;
|
|
377
|
+
import React from 'react';
|
|
378
|
+
React.useState(null);
|
|
379
|
+
useState(null); // without import = ReferenceError
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Editable Settings
|
|
383
|
+
|
|
384
|
+
Hooks from `@/lib/editable-settings` let Softr builders customize blocks. Each `name` must be unique.
|
|
385
|
+
|
|
386
|
+
```jsx
|
|
387
|
+
import { useTextSetting } from "@/lib/editable-settings";
|
|
388
|
+
var title = useTextSetting({ name: "title", label: "Title", initialValue: "Welcome", required: false });
|
|
389
|
+
// Returns: string
|
|
390
|
+
|
|
391
|
+
import { useImageSetting } from "@/lib/editable-settings";
|
|
392
|
+
var image = useImageSetting({ name: "hero", label: "Hero Image", initialValue: { src: "https://...", alt: "Hero" } });
|
|
393
|
+
// Returns: { src: string, alt: string }
|
|
394
|
+
|
|
395
|
+
import { useVideoSetting } from "@/lib/editable-settings";
|
|
396
|
+
var video = useVideoSetting({ name: "intro", label: "Intro Video", initialValue: { src: "https://..." } });
|
|
397
|
+
// Returns: { src: string }
|
|
398
|
+
|
|
399
|
+
import { useVibeCodingBlockIconSetting } from "@/lib/editable-settings";
|
|
400
|
+
import { DynamicIcon } from "@/components/dynamic-icon";
|
|
401
|
+
var iconSetting = useVibeCodingBlockIconSetting({ name: "icon", label: "Icon", initialValue: { icon: "trending-up" } });
|
|
402
|
+
// Render: <DynamicIcon name={iconSetting.icon} className="w-6 h-6" />
|
|
403
|
+
|
|
404
|
+
import { useNavigationSetting } from "@/lib/editable-settings";
|
|
405
|
+
var nav = useNavigationSetting({ name: "cta", label: "CTA", initialValue: { action: "OPEN_PAGE", destination: "/pricing", openIn: "SELF" } });
|
|
406
|
+
// Returns: { action, destination, openIn }
|
|
407
|
+
// `openIn` MUST be one of: "SELF" (same tab), "TAB" (new tab), or "MODAL". Any other value (e.g. "SAME_TAB", "NEW_TAB") fails Softr's setting validator with: "useNavigationSetting(): The 'initialValue.openIn' property in the 'navigation' setting must be \"SELF\", \"TAB\", or \"MODAL\" if provided".
|
|
408
|
+
|
|
409
|
+
import { useBooleanSetting } from "@/lib/editable-settings";
|
|
410
|
+
var show = useBooleanSetting({ name: "toggle", label: "Show header", initialValue: false });
|
|
411
|
+
// Returns: boolean
|
|
412
|
+
|
|
413
|
+
import { useArraySetting } from "@/lib/editable-settings";
|
|
414
|
+
var features = useArraySetting({
|
|
415
|
+
name: "features", label: "Features",
|
|
416
|
+
schema: {
|
|
417
|
+
title: { type: "text", label: "Title", initialValue: "Feature" },
|
|
418
|
+
description: { type: "text", label: "Description" },
|
|
419
|
+
icon: { type: "vibeCodingBlockIcon", label: "Icon" },
|
|
420
|
+
},
|
|
421
|
+
initialValue: [{ title: "Fast", description: "Blazing fast.", icon: { icon: "zap" } }],
|
|
422
|
+
});
|
|
423
|
+
// Schema types: "text", "image", "video", "vibeCodingBlockIcon"
|
|
424
|
+
// No nested arrays. Don't put vibeCodingBlockIcon as first field.
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## Hard Constraints
|
|
428
|
+
|
|
429
|
+
Non-negotiable rules enforced by the Softr platform:
|
|
430
|
+
|
|
431
|
+
1. **Browser-only** — No server-side code, no Node.js APIs.
|
|
432
|
+
2. **Static field mappings** — `q.select()` keys and values must be string literals.
|
|
433
|
+
3. **Filter nesting limit** — Maximum 2 levels deep with `q.and()` / `q.or()`.
|
|
434
|
+
4. **Check mutation `enabled`** — Gate mutation UI and calls behind the `enabled` boolean.
|
|
435
|
+
5. **Unique setting names** — No two setting hooks can share the same `name`.
|
|
436
|
+
6. **Array setting icon placement** — Never put `vibeCodingBlockIcon` as first field.
|
|
437
|
+
7. **No nested arrays in settings** — Use text with separator, split in code.
|
|
438
|
+
8. **Default export required** — `export default function Block()`.
|
|
439
|
+
9. **Container wrapping** — Always wrap in `<div className="container py-0"><div className="content">`. Vertical padding lives on the inner wrapper and depends on block placement (see "Block Placement & Page Spacing").
|
|
440
|
+
10. **No optional chaining or nullish coalescing** — Softr's bundler fails on `?.` and `??`. Use:
|
|
441
|
+
- `(user && user.email) || ""` instead of `user?.email ?? ""`
|
|
442
|
+
- `(data && data.pages) ? data.pages.flatMap(function(p) { return p.items; }) : []`
|
|
443
|
+
11. **Airtable: use column names, not fld... IDs** — See [datasources/airtable.md](datasources/airtable.md).
|
|
444
|
+
12. **Record fields nested under `fields`** — Access via `record.fields.alias`, not `record.alias`.
|
|
445
|
+
13. **ONE `useRecords` per block** — Filter client-side. Multiple `useMetric` calls OK.
|
|
446
|
+
14. **React functional components only** — No class components.
|
|
447
|
+
15. **Do NOT `import React from 'react'`** — Use named imports for hooks.
|
|
448
|
+
16. **No CSS modules or styled-components** — Tailwind only.
|
|
449
|
+
17. **setTimeout for scroll** -- Wrap programmatic scroll in `setTimeout(fn, 0)`.
|
|
450
|
+
18. **`fetchNextPage` inside `useEffect` only** -- Calling it during render causes infinite re-render loops. The component calls `fetchNextPage`, which updates data, which triggers re-render, which calls `fetchNextPage` again.
|
|
451
|
+
19. **All hooks before any conditional `return`** -- Hooks must be called in the same order every render. A hook declared after a conditional `return` causes React error #310.
|
|
452
|
+
20. **Relative paths in navigation** -- Use `/page-name?recordId=...`, never hardcoded domains like `app.client.com/page`.
|
|
453
|
+
|
|
454
|
+
## Style Conventions (preferred, not enforced)
|
|
455
|
+
|
|
456
|
+
The conventions below improve consistency across the skill's examples but are NOT enforced by the Softr bundler. Softr's AI assistant in Studio outputs `const` and arrow functions, and both compile and run fine. Adopting these conventions makes hand-written blocks visually uniform with the rest of the skill, but blocks generated by Softr's AI work without conversion.
|
|
457
|
+
|
|
458
|
+
- Prefer `var` over `const` / `let`
|
|
459
|
+
- Prefer `function() {}` over arrow functions in JSX callbacks and component props
|
|
460
|
+
- Field-value helper property priority: `label` -> `name` -> `title`
|
|
461
|
+
|
|
462
|
+
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.
|
|
463
|
+
|
|
464
|
+
## Anti-Patterns Checklist
|
|
465
|
+
|
|
466
|
+
Before delivering any block, run through [references/anti-patterns.md](references/anti-patterns.md) — a categorized catalog of every violation observed in production (data access, mutations, hooks, layout, helper blocks).
|
|
467
|
+
|
|
468
|
+
## Code Quality Guidelines
|
|
469
|
+
|
|
470
|
+
- Use `sonner`'s `toast` for notifications. Also: `toast.info()`, `toast.message("Note", { description: "..." })`.
|
|
471
|
+
- Show loading states with `spinner` or `skeleton` when `status === "pending"`.
|
|
472
|
+
- Show error states gracefully when `status === "error"`.
|
|
473
|
+
- After mutations, call `refetch()` before success toast.
|
|
474
|
+
- Use Tailwind for all styling.
|
|
475
|
+
- Prefer shadcn/ui components over raw HTML.
|
|
476
|
+
- Use `date-fns` for date formatting.
|
|
477
|
+
|
|
478
|
+
## UI/UX Guidelines
|
|
479
|
+
|
|
480
|
+
For design best practices, spacing, responsive patterns, and component guidance, see [ui-ux-guidelines.md](ui-ux-guidelines.md).
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
var fs = require('fs');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
var os = require('os');
|
|
6
|
+
|
|
7
|
+
var SKILL_NAME = 'softr-vibe-coding';
|
|
8
|
+
var SKILL_DIR = path.join(os.homedir(), '.claude', 'skills', SKILL_NAME);
|
|
9
|
+
var SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.json');
|
|
10
|
+
var PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
11
|
+
|
|
12
|
+
var SKILL_FILES = ['SKILL.md', 'ui-ux-guidelines.md', 'README.md', 'LICENSE'];
|
|
13
|
+
var SKILL_DIRS = ['references', 'datasources'];
|
|
14
|
+
|
|
15
|
+
var HOOK_COMMAND = 'npx -y --prefer-online ' + SKILL_NAME + '@latest sync';
|
|
16
|
+
|
|
17
|
+
function log(msg) {
|
|
18
|
+
console.log('[' + SKILL_NAME + '] ' + msg);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function copyRecursive(src, dest) {
|
|
22
|
+
if (!fs.existsSync(src)) return;
|
|
23
|
+
var stat = fs.statSync(src);
|
|
24
|
+
if (stat.isDirectory()) {
|
|
25
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
26
|
+
var entries = fs.readdirSync(src);
|
|
27
|
+
for (var i = 0; i < entries.length; i++) {
|
|
28
|
+
copyRecursive(path.join(src, entries[i]), path.join(dest, entries[i]));
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
32
|
+
fs.copyFileSync(src, dest);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function syncSkillFiles() {
|
|
37
|
+
fs.mkdirSync(SKILL_DIR, { recursive: true });
|
|
38
|
+
|
|
39
|
+
for (var i = 0; i < SKILL_FILES.length; i++) {
|
|
40
|
+
var src = path.join(PACKAGE_ROOT, SKILL_FILES[i]);
|
|
41
|
+
if (fs.existsSync(src)) {
|
|
42
|
+
copyRecursive(src, path.join(SKILL_DIR, SKILL_FILES[i]));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (var j = 0; j < SKILL_DIRS.length; j++) {
|
|
47
|
+
var srcDir = path.join(PACKAGE_ROOT, SKILL_DIRS[j]);
|
|
48
|
+
var destDir = path.join(SKILL_DIR, SKILL_DIRS[j]);
|
|
49
|
+
if (fs.existsSync(srcDir)) {
|
|
50
|
+
// Wipe destination first so files removed upstream are also removed locally.
|
|
51
|
+
if (fs.existsSync(destDir)) {
|
|
52
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
copyRecursive(srcDir, destDir);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readSettings() {
|
|
60
|
+
if (!fs.existsSync(SETTINGS_FILE)) return {};
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error('[' + SKILL_NAME + '] ERROR: could not parse ' + SETTINGS_FILE + ': ' + e.message);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writeSettings(settings) {
|
|
70
|
+
fs.mkdirSync(path.dirname(SETTINGS_FILE), { recursive: true });
|
|
71
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + '\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function hookExists(settings) {
|
|
75
|
+
var sessionStart = (settings.hooks && settings.hooks.SessionStart) || [];
|
|
76
|
+
for (var i = 0; i < sessionStart.length; i++) {
|
|
77
|
+
var hooks = sessionStart[i].hooks || [];
|
|
78
|
+
for (var j = 0; j < hooks.length; j++) {
|
|
79
|
+
var h = hooks[j];
|
|
80
|
+
if (h.type === 'command' && typeof h.command === 'string' && h.command.indexOf(SKILL_NAME) !== -1) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function installHook() {
|
|
89
|
+
var settings = readSettings();
|
|
90
|
+
|
|
91
|
+
if (!settings.hooks) settings.hooks = {};
|
|
92
|
+
if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
|
|
93
|
+
|
|
94
|
+
if (hookExists(settings)) {
|
|
95
|
+
log('SessionStart hook already configured — leaving as is.');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
settings.hooks.SessionStart.push({
|
|
100
|
+
hooks: [{ type: 'command', command: HOOK_COMMAND }]
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
writeSettings(settings);
|
|
104
|
+
log('Added SessionStart hook to ' + SETTINGS_FILE);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function cmdInit() {
|
|
108
|
+
log('Installing skill to ' + SKILL_DIR);
|
|
109
|
+
syncSkillFiles();
|
|
110
|
+
log('Skill files installed.');
|
|
111
|
+
installHook();
|
|
112
|
+
log('Done. Start a new Claude Code session to load the skill.');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function cmdSync() {
|
|
116
|
+
syncSkillFiles();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function cmdHelp() {
|
|
120
|
+
console.log('Usage: npx ' + SKILL_NAME + '@latest <command>');
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log('Commands:');
|
|
123
|
+
console.log(' init First-time install. Copies skill into ~/.claude/skills/' + SKILL_NAME + '/');
|
|
124
|
+
console.log(' and adds a SessionStart hook to auto-update on each Claude Code session.');
|
|
125
|
+
console.log(' sync Refresh skill files (called by the SessionStart hook).');
|
|
126
|
+
console.log(' help Show this message.');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
var cmd = process.argv[2];
|
|
130
|
+
if (cmd === 'init') {
|
|
131
|
+
cmdInit();
|
|
132
|
+
} else if (cmd === 'sync') {
|
|
133
|
+
cmdSync();
|
|
134
|
+
} else if (cmd === 'help' || cmd === '--help' || cmd === '-h' || cmd === undefined) {
|
|
135
|
+
cmdHelp();
|
|
136
|
+
} else {
|
|
137
|
+
console.error('[' + SKILL_NAME + '] Unknown command: ' + cmd);
|
|
138
|
+
cmdHelp();
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|