@usesidekick/react 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -1,40 +1,55 @@
1
- # Sidekick React SDK
1
+ # @usesidekick/react
2
2
 
3
- Zero-change integration for extensible React applications. Override modules can customize any component by name without modifying the host application.
3
+ Zero-change runtime for extensible React applications. Override modules can wrap, replace, or enhance any component by name without modifying the host app's source code.
4
4
 
5
- ## Quick Start
6
-
7
- ### 1. Install
5
+ ## Installation
8
6
 
9
7
  ```bash
10
8
  npm install @usesidekick/react
11
9
  ```
12
10
 
13
- ### 2. Wrap Your App
11
+ For the full automated setup (installs deps, configures build, creates API routes):
12
+
13
+ ```bash
14
+ npx @usesidekick/cli init
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ### 1. Wrap Your App
14
20
 
15
21
  ```tsx
16
22
  import { SidekickProvider } from '@usesidekick/react';
17
23
 
18
24
  export default function App() {
19
25
  return (
20
- <SidekickProvider>
26
+ <SidekickProvider overridesEndpoint="/api/sidekick/overrides">
21
27
  <YourApp />
22
28
  </SidekickProvider>
23
29
  );
24
30
  }
25
31
  ```
26
32
 
27
- ### 3. (Production) Add Build Plugin
33
+ ### 2. Add the Build Plugin
34
+
35
+ The `babel-plugin-add-react-displayname` plugin preserves component names through minification so overrides can target them in production.
28
36
 
29
- For production builds, add the `babel-plugin-add-react-displayname` plugin to automatically preserve component names through minification.
37
+ ```bash
38
+ npm install -D babel-plugin-add-react-displayname
39
+ ```
30
40
 
31
- **Next.js** (`next.config.js`):
32
- ```js
33
- module.exports = {
34
- // Using Babel
35
- babel: {
36
- plugins: ['add-react-displayname']
37
- }
41
+ **Next.js** (`.babelrc`):
42
+ ```json
43
+ {
44
+ "presets": [
45
+ ["next/babel", {
46
+ "preset-react": {
47
+ "runtime": "automatic",
48
+ "importSource": "@usesidekick/react"
49
+ }
50
+ }]
51
+ ],
52
+ "plugins": ["add-react-displayname"]
38
53
  }
39
54
  ```
40
55
 
@@ -45,41 +60,25 @@ import react from '@vitejs/plugin-react';
45
60
  export default {
46
61
  plugins: [
47
62
  react({
48
- babel: {
49
- plugins: ['add-react-displayname']
50
- }
63
+ jsxImportSource: '@usesidekick/react',
64
+ babel: { plugins: ['add-react-displayname'] }
51
65
  })
52
66
  ]
53
67
  }
54
68
  ```
55
69
 
56
- **Webpack** (`.babelrc`):
57
- ```json
58
- {
59
- "plugins": ["add-react-displayname"]
60
- }
61
- ```
62
-
63
- That's it! Override modules can now customize any component in your app.
64
-
65
- ## How Override Modules Work
66
-
67
- Override modules use the SDK to wrap or replace components by name:
68
-
69
- ### Wrapping Components
70
-
71
- Add behavior around any component without modifying it:
70
+ ### 3. Write an Override
72
71
 
73
72
  ```tsx
74
73
  import { createOverride } from '@usesidekick/react';
75
74
 
76
75
  export default createOverride({
77
- name: 'Custom Task Card Wrapper',
76
+ name: 'Beta Banner',
78
77
  primitives: ['ui.wrap'],
79
78
  activate: (sdk) => {
80
- sdk.ui.wrap('TaskCard', (Original) => (props) => (
81
- <div className="relative">
82
- <span className="badge">New!</span>
79
+ sdk.ui.wrap('TaskBoard', (Original) => (props) => (
80
+ <div>
81
+ <div style={{ background: 'purple', color: 'white', padding: 8 }}>Beta</div>
83
82
  <Original {...props} />
84
83
  </div>
85
84
  ));
@@ -87,159 +86,203 @@ export default createOverride({
87
86
  });
88
87
  ```
89
88
 
90
- ### Replacing Components
91
-
92
- Completely swap out a component:
93
-
94
- ```tsx
95
- import { createOverride } from '@usesidekick/react';
96
- import { MyCustomModal } from './MyCustomModal';
97
-
98
- export default createOverride({
99
- name: 'Custom Modal',
100
- primitives: ['ui.replace'],
101
- activate: (sdk) => {
102
- sdk.ui.replace('TaskModal', MyCustomModal);
103
- },
104
- });
105
- ```
106
-
107
- ### Injecting Styles
108
-
109
- Add CSS without touching any files:
110
-
111
- ```tsx
112
- sdk.ui.addStyles(`
113
- .task-card {
114
- border-radius: 12px;
115
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
116
- }
117
- `);
118
- ```
119
-
120
- ## Component Name Resolution
121
-
122
- The SDK resolves component names in this order:
123
-
124
- 1. **displayName** - Explicit name, survives minification
125
- 2. **function name** - Works in development
126
- 3. **Inner component** - For memo/forwardRef wrapped components
127
-
128
- | Environment | Name Source | Works? |
129
- |------------|-------------|--------|
130
- | Development | function name | Always |
131
- | Production (with build plugin) | auto-added displayName | Always |
132
- | Production (without plugin) | minified name | Breaks |
133
-
134
- For best results, either:
135
- - Use the babel plugin (recommended, zero per-component changes)
136
- - Or manually add `displayName` to components you want to target
137
-
138
- ## API Reference
139
-
140
- ### SidekickProvider
141
-
142
- ```tsx
143
- <SidekickProvider
144
- overridesEndpoint="/api/overrides" // Optional: API endpoint for remote overrides
145
- >
146
- {children}
147
- </SidekickProvider>
148
- ```
149
-
150
- ### SDK Primitives
89
+ ## Override Primitives
151
90
 
152
- #### UI Primitives
91
+ ### UI
153
92
 
154
93
  | Method | Description |
155
94
  |--------|-------------|
156
- | `sdk.ui.wrap(name, wrapper)` | Wrap a component by name |
157
- | `sdk.ui.replace(name, component)` | Replace a component by name |
158
- | `sdk.ui.inject(extensionPointId, component)` | Inject into an ExtensionPoint |
95
+ | `sdk.ui.wrap(name, wrapper)` | Wrap a component with additional markup/logic |
96
+ | `sdk.ui.replace(name, component)` | Replace a component entirely |
97
+ | `sdk.ui.inject(extensionPointId, component)` | Inject into an `<ExtensionPoint>` slot |
159
98
  | `sdk.ui.addStyles(css)` | Inject global CSS |
160
99
  | `sdk.ui.addColumn(tableId, config)` | Add a column to a table |
161
100
  | `sdk.ui.addMenuItem(menuId, config)` | Add a menu item |
162
101
  | `sdk.ui.addTab(tabGroupId, config)` | Add a tab |
163
102
  | `sdk.ui.addAction(actionBarId, config)` | Add an action button |
164
- | `sdk.ui.modifyProps(componentId, modifier)` | Modify component props |
103
+ | `sdk.ui.modifyProps(componentId, modifier)` | Modify a component's props at render time |
165
104
 
166
- #### Data Primitives
105
+ ### Data
167
106
 
168
107
  | Method | Description |
169
108
  |--------|-------------|
170
- | `sdk.data.computed(fieldName, compute)` | Add a computed field |
109
+ | `sdk.data.computed(fieldName, compute)` | Add a computed/derived field |
171
110
  | `sdk.data.addFilter(name, filter)` | Add a data filter |
172
- | `sdk.data.transform(dataKey, transform)` | Transform data |
111
+ | `sdk.data.transform(dataKey, transform)` | Transform data before render |
173
112
  | `sdk.data.intercept(pathPattern, handler)` | Intercept API responses |
174
113
  | `sdk.data.addSortOption(tableId, config)` | Add a sort option |
175
114
  | `sdk.data.addGroupBy(tableId, config)` | Add a group-by option |
176
115
 
177
- #### Behavior Primitives
116
+ ### Behavior
178
117
 
179
118
  | Method | Description |
180
119
  |--------|-------------|
181
- | `sdk.behavior.onEvent(name, handler)` | Handle custom events |
182
- | `sdk.behavior.addKeyboardShortcut(keys, action)` | Add keyboard shortcuts |
120
+ | `sdk.behavior.onEvent(name, handler)` | Listen for custom events |
121
+ | `sdk.behavior.addKeyboardShortcut(keys, action)` | Register keyboard shortcuts |
183
122
  | `sdk.behavior.modifyRoute(pattern, handler)` | Modify routing behavior |
184
123
 
185
- ## ExtensionPoints (Optional)
124
+ ## React Hooks
186
125
 
187
- For fine-grained injection slots, you can still use ExtensionPoints:
126
+ The SDK provides hooks for host apps that want to read override state:
188
127
 
189
128
  ```tsx
190
- import { ExtensionPoint } from '@usesidekick/react';
191
-
192
- function MyComponent() {
193
- return (
194
- <div>
195
- <ExtensionPoint id="header-actions" props={{ user }} />
196
- {/* ... */}
197
- </div>
198
- );
199
- }
129
+ import {
130
+ useAddedColumns,
131
+ useColumnRenames,
132
+ useHiddenColumns,
133
+ useColumnOrder,
134
+ useMenuItems,
135
+ useTabs,
136
+ useActions,
137
+ useValidations,
138
+ usePropsModifier,
139
+ useComputedField,
140
+ useFilter,
141
+ useSortOptions,
142
+ useGroupByOptions,
143
+ useKeyboardShortcuts,
144
+ useEventEmitter,
145
+ } from '@usesidekick/react';
200
146
  ```
201
147
 
202
- Override modules can then inject into these points:
148
+ ## SidekickPanel
149
+
150
+ A built-in admin panel for managing overrides at runtime. Supports AI-powered generation, toggling, and deletion.
203
151
 
204
152
  ```tsx
205
- sdk.ui.inject('header-actions', ({ user }) => (
206
- <button>Hello, {user.name}!</button>
207
- ));
153
+ import { SidekickPanel } from '@usesidekick/react';
154
+
155
+ <SidekickPanel
156
+ apiEndpoint="/api/sidekick/generate"
157
+ toggleEndpoint="/api/sidekick/toggle"
158
+ deleteEndpoint="/api/sidekick/delete"
159
+ />
208
160
  ```
209
161
 
210
- ## Technical Details
162
+ | Prop | Type | Description |
163
+ |------|------|-------------|
164
+ | `apiEndpoint` | `string?` | Endpoint for AI override generation |
165
+ | `toggleEndpoint` | `string?` | Endpoint for enable/disable |
166
+ | `deleteEndpoint` | `string?` | Endpoint for deletion |
167
+ | `onGenerate` | `function?` | Custom generate handler (overrides `apiEndpoint`) |
168
+ | `onToggle` | `function?` | Custom toggle handler (overrides `toggleEndpoint`) |
169
+ | `onDelete` | `function?` | Custom delete handler (overrides `deleteEndpoint`) |
170
+
171
+ ## Server (`@usesidekick/react/server`)
172
+
173
+ Server-side utilities for building the Sidekick API backend. Handles override CRUD, AI generation, and validation.
211
174
 
212
- ### How It Works
175
+ ### Setup with Drizzle + Neon
213
176
 
214
- The SDK overrides `React.createElement` to intercept every component render. When a component is rendered, it checks:
177
+ ```ts
178
+ // app/api/sidekick/[...action]/route.ts
179
+ import { createSidekickHandler, createDrizzleStorage } from '@usesidekick/react/server';
180
+ import { db } from '@/lib/db';
181
+ import { overrides } from '@/lib/db/schema';
182
+
183
+ const handler = createSidekickHandler({
184
+ storage: createDrizzleStorage(db, overrides),
185
+ });
186
+
187
+ export const GET = handler.GET;
188
+ export const POST = handler.POST;
189
+ ```
215
190
 
216
- 1. Is there a replacement registered for this component name? If so, use it.
217
- 2. Is there a wrapper registered? If so, wrap the original component.
191
+ This single catch-all route handles four actions (dispatched by the last URL path segment):
192
+
193
+ | Method | Path | Action |
194
+ |--------|------|--------|
195
+ | GET | `/api/sidekick/overrides` | List all overrides |
196
+ | POST | `/api/sidekick/generate` | AI-generate an override |
197
+ | POST | `/api/sidekick/toggle` | Enable/disable an override |
198
+ | POST | `/api/sidekick/delete` | Delete an override |
199
+
200
+ ### Server Exports
201
+
202
+ ```ts
203
+ import {
204
+ // Handler factory
205
+ createSidekickHandler,
206
+
207
+ // Storage
208
+ createDrizzleStorage, // Drizzle ORM adapter
209
+ sidekickOverrides, // pgTable definition (for your schema)
210
+
211
+ // AI generation (for custom integrations)
212
+ buildSystemPrompt,
213
+ buildUserPrompt,
214
+ callAI,
215
+ parseAIResponse,
216
+ validateCode,
217
+ formatDesignSystem,
218
+ } from '@usesidekick/react/server';
219
+ ```
220
+
221
+ ### Custom Storage Adapter
222
+
223
+ Implement `SidekickStorage` to use any database:
224
+
225
+ ```ts
226
+ import type { SidekickStorage } from '@usesidekick/react/server';
227
+
228
+ const myStorage: SidekickStorage = {
229
+ listOverrides: async () => { /* ... */ },
230
+ getOverride: async (id) => { /* ... */ },
231
+ createOverride: async (data) => { /* ... */ },
232
+ updateOverride: async (id, data) => { /* ... */ },
233
+ deleteOverride: async (id) => { /* ... */ },
234
+ };
235
+
236
+ const handler = createSidekickHandler({ storage: myStorage });
237
+ ```
238
+
239
+ ## How It Works
240
+
241
+ The SDK overrides `React.createElement` via a custom JSX runtime to intercept every component render. When a component renders, it checks:
242
+
243
+ 1. Is there a **replacement** registered for this component? Use it.
244
+ 2. Is there a **wrapper** registered? Wrap the original.
218
245
  3. Otherwise, render normally.
219
246
 
220
- This approach requires **zero changes** to existing components - just wrap your app with `SidekickProvider`.
247
+ This requires **zero changes** to existing components.
248
+
249
+ ### Component Name Resolution
250
+
251
+ Names are resolved in order: `displayName` > `function.name` > inner component name (for memo/forwardRef).
252
+
253
+ | Environment | Name Source | Reliable? |
254
+ |-------------|-------------|-----------|
255
+ | Development | function name | Yes |
256
+ | Production + build plugin | auto-added displayName | Yes |
257
+ | Production without plugin | minified name | No |
221
258
 
222
259
  ### Performance
223
260
 
224
- - One Map lookup per createElement call
225
- - Negligible overhead (<1% in benchmarks)
226
- - No impact when no overrides are registered
261
+ - One `Map.get()` per `createElement` call
262
+ - No overhead when no overrides are registered
227
263
 
228
264
  ### Compatibility
229
265
 
230
- - Functional components
231
- - Class components
232
- - React.memo wrapped components
233
- - React.forwardRef wrapped components
234
- - Suspense boundaries
235
- - Portals
236
- - **Not supported**: Server Components (Next.js RSC) - client components only
266
+ Works with: functional components, class components, `React.memo`, `React.forwardRef`, Suspense, Portals.
267
+
268
+ **Not supported:** React Server Components (client-side only).
269
+
270
+ ## Package Exports
271
+
272
+ | Import Path | Description |
273
+ |-------------|-------------|
274
+ | `@usesidekick/react` | Provider, hooks, panel, override API, types |
275
+ | `@usesidekick/react/jsx-runtime` | Custom JSX runtime (configured via `jsxImportSource`) |
276
+ | `@usesidekick/react/jsx-dev-runtime` | Development JSX runtime |
277
+ | `@usesidekick/react/server` | Server handler, storage adapters, AI generation |
237
278
 
238
- ### Limitations
279
+ ## Peer Dependencies
239
280
 
240
- - Anonymous components (`export default () => ...`) cannot be targeted by name
241
- - Server Components cannot be intercepted (they don't run on client)
242
- - React-specific (other frameworks need separate adapters)
281
+ | Package | Version | Required? |
282
+ |---------|---------|-----------|
283
+ | `react` | ^18.2.0 | Yes |
284
+ | `drizzle-orm` | >=0.29.0 | Only if using `createDrizzleStorage` |
285
+ | `next` | >=14.0.0 | Only if using `createSidekickHandler` |
243
286
 
244
287
  ## License
245
288
 
@@ -222,6 +222,8 @@ type DrizzleDb = {
222
222
  };
223
223
  declare function createDrizzleStorage(db: DrizzleDb, overridesTable: typeof sidekickOverrides): SidekickStorage;
224
224
 
225
+ declare function createJsonFileStorage(filePath?: string): SidekickStorage;
226
+
225
227
  declare function formatDesignSystem(schema: Record<string, unknown>): string;
226
228
  declare function buildSystemPrompt(schema: Record<string, unknown>): string;
227
229
  declare function buildUserPrompt(request: string, previousErrors?: string[], existingCode?: string): string;
@@ -232,4 +234,4 @@ declare function validateCode(code: string): {
232
234
  errors: string[];
233
235
  };
234
236
 
235
- export { type GeneratedOverride, type NewOverrideRecord, type NewSidekickOverride, type OverrideManifest, type OverrideRecord, type SidekickHandlerOptions, type SidekickOverride, type SidekickStorage, buildSystemPrompt, buildUserPrompt, callAI, createDrizzleStorage, createSidekickHandler, formatDesignSystem, parseAIResponse, sidekickOverrides, validateCode };
237
+ export { type GeneratedOverride, type NewOverrideRecord, type NewSidekickOverride, type OverrideManifest, type OverrideRecord, type SidekickHandlerOptions, type SidekickOverride, type SidekickStorage, buildSystemPrompt, buildUserPrompt, callAI, createDrizzleStorage, createJsonFileStorage, createSidekickHandler, formatDesignSystem, parseAIResponse, sidekickOverrides, validateCode };
@@ -222,6 +222,8 @@ type DrizzleDb = {
222
222
  };
223
223
  declare function createDrizzleStorage(db: DrizzleDb, overridesTable: typeof sidekickOverrides): SidekickStorage;
224
224
 
225
+ declare function createJsonFileStorage(filePath?: string): SidekickStorage;
226
+
225
227
  declare function formatDesignSystem(schema: Record<string, unknown>): string;
226
228
  declare function buildSystemPrompt(schema: Record<string, unknown>): string;
227
229
  declare function buildUserPrompt(request: string, previousErrors?: string[], existingCode?: string): string;
@@ -232,4 +234,4 @@ declare function validateCode(code: string): {
232
234
  errors: string[];
233
235
  };
234
236
 
235
- export { type GeneratedOverride, type NewOverrideRecord, type NewSidekickOverride, type OverrideManifest, type OverrideRecord, type SidekickHandlerOptions, type SidekickOverride, type SidekickStorage, buildSystemPrompt, buildUserPrompt, callAI, createDrizzleStorage, createSidekickHandler, formatDesignSystem, parseAIResponse, sidekickOverrides, validateCode };
237
+ export { type GeneratedOverride, type NewOverrideRecord, type NewSidekickOverride, type OverrideManifest, type OverrideRecord, type SidekickHandlerOptions, type SidekickOverride, type SidekickStorage, buildSystemPrompt, buildUserPrompt, callAI, createDrizzleStorage, createJsonFileStorage, createSidekickHandler, formatDesignSystem, parseAIResponse, sidekickOverrides, validateCode };
@@ -34,6 +34,7 @@ __export(server_exports, {
34
34
  buildUserPrompt: () => buildUserPrompt,
35
35
  callAI: () => callAI,
36
36
  createDrizzleStorage: () => createDrizzleStorage,
37
+ createJsonFileStorage: () => createJsonFileStorage,
37
38
  createSidekickHandler: () => createSidekickHandler,
38
39
  formatDesignSystem: () => formatDesignSystem,
39
40
  parseAIResponse: () => parseAIResponse,
@@ -613,6 +614,81 @@ function createDrizzleStorage(db, overridesTable) {
613
614
  };
614
615
  }
615
616
 
617
+ // src/server/json-storage.ts
618
+ var fs2 = __toESM(require("fs/promises"));
619
+ var path2 = __toESM(require("path"));
620
+ function deserializeDates(record) {
621
+ return {
622
+ ...record,
623
+ createdAt: new Date(record.createdAt),
624
+ updatedAt: new Date(record.updatedAt)
625
+ };
626
+ }
627
+ async function readData(filePath) {
628
+ try {
629
+ const raw = await fs2.readFile(filePath, "utf-8");
630
+ const parsed = JSON.parse(raw);
631
+ return {
632
+ overrides: (parsed.overrides || []).map(deserializeDates)
633
+ };
634
+ } catch (err) {
635
+ if (err.code === "ENOENT") {
636
+ const empty = { overrides: [] };
637
+ await fs2.mkdir(path2.dirname(filePath), { recursive: true });
638
+ await fs2.writeFile(filePath, JSON.stringify(empty, null, 2) + "\n");
639
+ return empty;
640
+ }
641
+ throw err;
642
+ }
643
+ }
644
+ async function writeData(filePath, data) {
645
+ const tmpPath = filePath + ".tmp";
646
+ await fs2.mkdir(path2.dirname(filePath), { recursive: true });
647
+ await fs2.writeFile(tmpPath, JSON.stringify(data, null, 2) + "\n");
648
+ await fs2.rename(tmpPath, filePath);
649
+ }
650
+ function createJsonFileStorage(filePath) {
651
+ const resolvedPath = filePath || path2.join(process.cwd(), ".sidekick", "overrides.json");
652
+ return {
653
+ async listOverrides() {
654
+ const data = await readData(resolvedPath);
655
+ return data.overrides;
656
+ },
657
+ async getOverride(id) {
658
+ const data = await readData(resolvedPath);
659
+ return data.overrides.find((o) => o.id === id) ?? null;
660
+ },
661
+ async createOverride(record) {
662
+ const data = await readData(resolvedPath);
663
+ data.overrides.push({
664
+ id: record.id,
665
+ name: record.name,
666
+ description: record.description,
667
+ version: record.version,
668
+ primitives: record.primitives,
669
+ code: record.code,
670
+ enabled: record.enabled ?? true,
671
+ createdAt: record.createdAt ?? /* @__PURE__ */ new Date(),
672
+ updatedAt: record.updatedAt ?? /* @__PURE__ */ new Date()
673
+ });
674
+ await writeData(resolvedPath, data);
675
+ },
676
+ async updateOverride(id, updates) {
677
+ const data = await readData(resolvedPath);
678
+ const idx = data.overrides.findIndex((o) => o.id === id);
679
+ if (idx === -1) return;
680
+ const { id: _id, createdAt: _ca, ...rest } = updates;
681
+ data.overrides[idx] = { ...data.overrides[idx], ...rest, updatedAt: /* @__PURE__ */ new Date() };
682
+ await writeData(resolvedPath, data);
683
+ },
684
+ async deleteOverride(id) {
685
+ const data = await readData(resolvedPath);
686
+ data.overrides = data.overrides.filter((o) => o.id !== id);
687
+ await writeData(resolvedPath, data);
688
+ }
689
+ };
690
+ }
691
+
616
692
  // src/server/drizzle-schema.ts
617
693
  var import_pg_core = require("drizzle-orm/pg-core");
618
694
  var sidekickOverrides = (0, import_pg_core.pgTable)("overrides", {
@@ -633,6 +709,7 @@ var sidekickOverrides = (0, import_pg_core.pgTable)("overrides", {
633
709
  buildUserPrompt,
634
710
  callAI,
635
711
  createDrizzleStorage,
712
+ createJsonFileStorage,
636
713
  createSidekickHandler,
637
714
  formatDesignSystem,
638
715
  parseAIResponse,