@rebasepro/agent-skills 0.9.1-canary.ff338b5 → 0.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/agent-skills",
3
- "version": "0.9.1-canary.ff338b5",
3
+ "version": "0.10.0",
4
4
  "private": false,
5
5
  "description": "Bundled AI agent skills for Rebase",
6
6
  "type": "module",
@@ -219,11 +219,48 @@ Twitter uses OAuth 2.0 with PKCE. The client must send `codeVerifier` alongside
219
219
 
220
220
  ### OAuth Account Linking
221
221
 
222
- When an OAuth user signs in:
223
- 1. If an identity record exists for `(providerId, provider)` → log in that user.
224
- 2. If no identity exists but a user with the same email exists **link** the provider to the existing account.
222
+ When an OAuth user signs in via `POST /api/auth/{provider}`:
223
+
224
+ 1. If an identity record exists for `(provider, providerId)` log in that user. The email is not consulted.
225
+ 2. If no identity exists but a user with the same email exists:
226
+ - **The provider asserted `emailVerified: true`** → **link** the provider to the existing account and log in as that user. One account, two sign-in methods.
227
+ - **The provider did NOT verify the email** → reject with `403 EMAIL_NOT_VERIFIED`. Nothing is created or modified.
225
228
  3. If neither exists → create a new user, link the identity, assign `defaultRole`.
226
229
 
230
+ > **IMPORTANT FOR AGENTS:** A second account is **never** silently created for
231
+ > an email that already exists. If asked "does signing in with Google create a
232
+ > duplicate user?", the answer is no — it either links (verified) or errors
233
+ > (unverified). This is **not configurable**; there is deliberately no option to
234
+ > auto-link on unverified emails, because that would let anyone who can make a
235
+ > provider emit an address they don't own take over the matching account.
236
+ > Google always asserts `email_verified` for real Google accounts, so linking
237
+ > is the normal path for Google sign-in.
238
+
239
+ ### Linking a Provider to a Signed-In Account
240
+
241
+ `POST /api/auth/link/{provider}` attaches a provider identity to the **already
242
+ authenticated** account (requires `Authorization: Bearer <token>`). The body is
243
+ the same payload the provider's sign-in route takes, e.g. `{ idToken }`.
244
+
245
+ This is the escape hatch from an `EMAIL_NOT_VERIFIED` rejection, and the way to
246
+ attach a provider whose email differs from the account's.
247
+
248
+ Unlike sign-in, linking here does **not** require a verified email and does not
249
+ require the emails to match — on sign-in the provider's email is the only
250
+ evidence tying the identity to an account, whereas here the caller has already
251
+ proven ownership by holding a valid session.
252
+
253
+ - `409 IDENTITY_ALREADY_LINKED` if that provider identity belongs to another user.
254
+ - Idempotent (`alreadyLinked: true`) if already linked to the caller.
255
+
256
+ ### Adding a Password to a Provider-Only Account
257
+
258
+ A user who signed up via OAuth has no `passwordHash`:
259
+
260
+ - `POST /auth/register` with the same email → `409 EMAIL_EXISTS`.
261
+ - `POST /auth/change-password` → `400 INVALID_ACCOUNT` (no existing password to verify).
262
+ - **`forgot-password` → `reset-password` is the supported path.** It re-proves ownership of the address by email, after which the account has both sign-in methods.
263
+
227
264
  ### Custom OAuth Provider
228
265
 
229
266
  You can register any OAuth provider by implementing the `OAuthProvider<T>` interface:
@@ -659,6 +696,7 @@ All auth endpoints are mounted under `/api/auth`. Admin endpoints are under `/ap
659
696
  | `PATCH` | `/auth/me` | Required | Update profile. Body: `{ displayName?, photoURL? }`. |
660
697
  | `POST` | `/auth/change-password` | Required | Change password. Body: `{ oldPassword, newPassword }`. Invalidates all sessions. |
661
698
  | `POST` | `/auth/send-verification` | Required | Send email verification link. Requires email service. |
699
+ | `POST` | `/auth/link/{provider}` | Required | Link an OAuth provider to the current account. Body: the provider's sign-in payload (e.g. `{ idToken }`). `409 IDENTITY_ALREADY_LINKED` if it belongs to another user. |
662
700
  | `GET` | `/auth/sessions` | Required | List active sessions (refresh tokens). |
663
701
  | `DELETE` | `/auth/sessions` | Required | Revoke all sessions (remote logout). |
664
702
  | `DELETE` | `/auth/sessions/:id` | Required | Revoke a specific session. |
@@ -95,7 +95,7 @@ const { db, pool, connectionString } = createPostgresDatabaseConnection(
95
95
 
96
96
  **Signature:**
97
97
 
98
- ```typescript
98
+ ```typescript no-verify
99
99
  function createPostgresDatabaseConnection(
100
100
  connectionString: string,
101
101
  schema?: Record<string, unknown>,
@@ -120,7 +120,7 @@ const readResources = createReadReplicaConnection(
120
120
 
121
121
  **Signature:**
122
122
 
123
- ```typescript
123
+ ```typescript no-verify
124
124
  function createReadReplicaConnection(
125
125
  connectionString: string,
126
126
  schema?: Record<string, unknown>,
@@ -145,7 +145,7 @@ const directResources = createDirectDatabaseConnection(
145
145
 
146
146
  **Signature:**
147
147
 
148
- ```typescript
148
+ ```typescript no-verify
149
149
  function createDirectDatabaseConnection(
150
150
  connectionString: string,
151
151
  schema?: Record<string, unknown>,
@@ -387,7 +387,8 @@ export const env = loadEnv();
387
387
 
388
388
  ```typescript
389
389
  import dotenv from "dotenv";
390
- import { loadEnv, z } from "@rebasepro/server";
390
+ import { loadEnv } from "@rebasepro/server";
391
+ import { z } from "zod";
391
392
 
392
393
  dotenv.config({ path: "../../.env" });
393
394
 
@@ -495,7 +495,7 @@ validation: {
495
495
 
496
496
  Maps store nested objects as `JSONB` in PostgreSQL. They can define their own inner properties schema.
497
497
 
498
- ```typescript
498
+ ```typescript no-verify
499
499
  address: {
500
500
  name: "Address",
501
501
  type: "map",
@@ -663,7 +663,7 @@ Every property supports a `validation` object with these common options:
663
663
 
664
664
  Use the `enum` property on `string` or `number` types to define picklist options:
665
665
 
666
- ```typescript
666
+ ```typescript no-verify
667
667
  status: {
668
668
  name: "Status",
669
669
  type: "string",
@@ -687,7 +687,7 @@ Each `EnumValueConfig` entry supports:
687
687
 
688
688
  `EnumValues` can also be a `Record<string, string | EnumValueConfig>` for simpler definitions:
689
689
 
690
- ```typescript
690
+ ```typescript no-verify
691
691
  enum: {
692
692
  draft: "Draft",
693
693
  published: { label: "Published", color: "green" }
@@ -348,7 +348,8 @@ Use the `extend` option to add your own typed env variables on top of the base R
348
348
 
349
349
  ```typescript
350
350
  import dotenv from "dotenv";
351
- import { loadEnv, z } from "@rebasepro/server";
351
+ import { loadEnv } from "@rebasepro/server";
352
+ import { z } from "zod";
352
353
 
353
354
  dotenv.config({ path: "../../.env" });
354
355
 
@@ -202,7 +202,7 @@ The typography scale is compact and uses semibold weights for headings — desig
202
202
 
203
203
  ### Typography Anti-Patterns
204
204
 
205
- ```tsx
205
+ ```tsx no-verify
206
206
  // ❌ NEVER: Gradient text on headings
207
207
  <Typography className="bg-gradient-to-r from-violet-600 to-cyan-500 bg-clip-text text-transparent">
208
208
 
@@ -270,7 +270,7 @@ The `Card` component from `@rebasepro/ui` automatically applies `cardMixin` and,
270
270
  // cardClickableMixin = "hover:bg-surface-50 dark:hover:bg-surface-800 cursor-pointer transition-colors duration-150"
271
271
  ```
272
272
 
273
- ```tsx
273
+ ```tsx no-verify
274
274
  // ✅ CORRECT: Using Card component
275
275
  <Card className="p-4">
276
276
  <Typography variant="subtitle1">Title</Typography>
@@ -407,7 +407,7 @@ The `Button` component accepts these variants and colors. Buttons use `rounded-l
407
407
 
408
408
  ### Button Anti-Patterns
409
409
 
410
- ```tsx
410
+ ```tsx no-verify
411
411
  // ❌ NEVER: Casting variant to bypass types
412
412
  <Button variant={"standard" as any}>
413
413
 
@@ -432,7 +432,7 @@ The `Button` component accepts these variants and colors. Buttons use `rounded-l
432
432
 
433
433
  ### Use the `Alert` Component
434
434
 
435
- ```tsx
435
+ ```tsx no-verify
436
436
  // ✅ CORRECT
437
437
  <Alert color="success" size="small">Lead captured successfully!</Alert>
438
438
  <Alert color="error">Could not load dashboard statistics.</Alert>
@@ -508,7 +508,7 @@ When displaying numeric KPIs, use plain muted icons (matching NavigationCard)
508
508
 
509
509
  ### Metric Card Anti-Patterns
510
510
 
511
- ```tsx
511
+ ```tsx no-verify
512
512
  // ❌ NEVER: Gradient icon backgrounds
513
513
  <div className="p-4 bg-gradient-to-br from-blue-500 to-indigo-600 text-white rounded-2xl shadow-lg shadow-blue-500/20">
514
514
 
@@ -555,7 +555,7 @@ For larger content sections (like a table, form, or embedded panel), use `Paper`
555
555
 
556
556
  ### Panel Anti-Patterns
557
557
 
558
- ```tsx
558
+ ```tsx no-verify
559
559
  // ❌ WRONG: Custom panel styling
560
560
  <div className="p-6 rounded-2xl border bg-white dark:bg-surface-900/50 backdrop-blur-md shadow-sm">
561
561
 
@@ -217,7 +217,7 @@ import {
217
217
 
218
218
  #### getPasswordResetTemplate
219
219
 
220
- ```typescript
220
+ ```typescript no-verify
221
221
  function getPasswordResetTemplate(
222
222
  resetUrl: string,
223
223
  user: { email: string; displayName?: string | null },
@@ -229,7 +229,7 @@ The template includes a "Reset Password" button linking to `resetUrl`, a plain-t
229
229
 
230
230
  #### getEmailVerificationTemplate
231
231
 
232
- ```typescript
232
+ ```typescript no-verify
233
233
  function getEmailVerificationTemplate(
234
234
  verifyUrl: string,
235
235
  user: { email: string; displayName?: string | null },
@@ -241,7 +241,7 @@ The template includes a "Verify Email Address" button linking to `verifyUrl`.
241
241
 
242
242
  #### getUserInvitationTemplate
243
243
 
244
- ```typescript
244
+ ```typescript no-verify
245
245
  function getUserInvitationTemplate(
246
246
  setPasswordUrl: string,
247
247
  user: { email: string; displayName?: string | null },
@@ -253,7 +253,7 @@ Sent when an admin creates a user via `POST /api/admin/users` without specifying
253
253
 
254
254
  #### getWelcomeEmailTemplate
255
255
 
256
- ```typescript
256
+ ```typescript no-verify
257
257
  function getWelcomeEmailTemplate(
258
258
  user: { email: string; displayName?: string | null },
259
259
  appName?: string, // default: "Rebase"
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: rebase-realtime
3
- description: Guide for the Rebase WebSocket realtime engine. Use this skill when the user needs real-time data synchronization, broadcast channels, presence tracking, table change broadcasts, or live updates in their application.
3
+ description: Guide for the Rebase WebSocket realtime engine. Use this skill when the user needs real-time data synchronization, broadcast channels, presence tracking, channel message history and reconnect catch-up (collaborative/op-based editing), table change broadcasts, or live updates in their application.
4
4
  ---
5
5
 
6
6
  # Rebase Realtime Engine
7
7
 
8
- Rebase includes a built-in WebSocket-based realtime engine that broadcasts database changes, supports broadcast channels for arbitrary client-to-client messaging, and provides presence tracking for online status.
8
+ Rebase includes a built-in WebSocket-based realtime engine that broadcasts database changes, supports broadcast channels for arbitrary client-to-client messaging (optionally with replayable message history), and provides presence tracking for online status.
9
9
 
10
10
  > **IMPORTANT FOR AGENTS:** The WebSocket protocol uses specific message types that differ from typical pub/sub patterns. Read the **Message Types** section carefully before generating any WebSocket code. Do NOT invent message type names — use only the types documented here.
11
11
 
@@ -68,8 +68,8 @@ Pass `FindParams` as the first argument to filter the subscription:
68
68
  ```typescript
69
69
  const unsubscribe = client.data.products.listen(
70
70
  {
71
- where: { status: "published" },
72
- orderBy: "created_at:desc",
71
+ where: { status: ["==", "published"] },
72
+ orderBy: ["created_at", "desc"],
73
73
  limit: 50,
74
74
  },
75
75
  (response) => {
@@ -120,7 +120,7 @@ const unsubscribe = client.data.products.listenById(
120
120
 
121
121
  ### `listenById()` Signature
122
122
 
123
- ```typescript
123
+ ```typescript no-verify
124
124
  listenById(
125
125
  id: string | number,
126
126
  onUpdate: (entity: Entity<M> | undefined) => void,
@@ -148,7 +148,7 @@ In React, use `useEffect` cleanup:
148
148
  ```tsx
149
149
  useEffect(() => {
150
150
  const unsubscribe = client.data.products.listen(
151
- { where: { active: true } },
151
+ { where: { active: ["==", true] } },
152
152
  (response) => setProducts(response.data)
153
153
  );
154
154
  return () => unsubscribe();
@@ -342,6 +342,7 @@ ws.onmessage = (event) => {
342
342
  | `presence_track` | `{ channel, state }` | Track presence with custom state (auto-joins channel) |
343
343
  | `presence_untrack` | `{ channel }` | Stop tracking presence |
344
344
  | `presence_state` | `{ channel }` | Request full presence entity |
345
+ | `channel_history` | `{ channel, sinceSeq?, limit? }` | Request retained messages after `sinceSeq` (retained channels only) |
345
346
  | `FETCH_COLLECTION` | `FetchCollectionProps` | One-shot collection fetch (request/response) |
346
347
  | `FETCH_ENTITY` | `FetchEntityProps` | One-shot entity fetch |
347
348
  | `SAVE_ENTITY` | `SaveEntityProps` | Create or update a entity |
@@ -358,9 +359,10 @@ ws.onmessage = (event) => {
358
359
  | `collection_update` | `{ subscriptionId, entities }` | Full collection data (authoritative refetch) |
359
360
  | `entity_update` | `{ subscriptionId, entity }` | Single entity data (entity or `null` if deleted) |
360
361
  | `collection_entity_patch` | `{ subscriptionId, entityId, entity }` | Instant single-entity patch for a collection subscription |
361
- | `broadcast` | `{ channel, event, payload }` | Broadcast from another channel member |
362
+ | `broadcast` | `{ channel, event, payload, seq? }` | Broadcast from another channel member. `seq` is present only on retained channels |
362
363
  | `presence_state` | `{ channel, presences }` | Full presence entity |
363
364
  | `presence_diff` | `{ channel, joins, leaves }` | Incremental presence update |
365
+ | `channel_history` | `{ channel, messages, retained, latestSeq? }` | Retained messages a client missed. `retained: false` means the channel keeps no history |
364
366
  | `ERROR` | `{ requestId?, payload: { error: { message, code } } }` | General error |
365
367
  | `FETCH_COLLECTION_SUCCESS` | `{ requestId, payload: { entities } }` | Response to FETCH_COLLECTION |
366
368
  | `FETCH_ENTITY_SUCCESS` | `{ requestId, payload: { entity } }` | Response to FETCH_ENTITY |
@@ -449,6 +451,63 @@ ws.send(JSON.stringify({
449
451
 
450
452
  When a client disconnects, they are automatically removed from all channels.
451
453
 
454
+ ## Channel History and Catch-Up
455
+
456
+ By default a broadcast reaches currently-connected members and is then gone — correct for cursors and notifications, and it costs nothing. A channel can instead be configured to **retain** its messages, so a reconnecting client catches up on what it missed rather than resyncing. This is what makes channels usable for operation streams (collaborative editing).
457
+
458
+ > **IMPORTANT FOR AGENTS:** Retention is configured on the **server** and cannot be requested by a client. `{ history: true }` on the client only asks the SDK to *use* history for a channel the server already retains. If the user reports "history isn't working", check the server config first — the symptom of a missing rule is `retained: false`, not an error.
459
+
460
+ ### Server config (required)
461
+
462
+ ```typescript
463
+ createPostgresAdapter({
464
+ connection: db,
465
+ schema: { tables, enums, relations },
466
+ realtime: {
467
+ channels: [
468
+ // Most specific first — first match wins.
469
+ { match: "doc:*", limit: 500, ttl: "24h" }
470
+ ]
471
+ }
472
+ })
473
+ ```
474
+
475
+ `match` is an exact channel name or a trailing-`*` prefix — **not** a general glob (`"doc:*:live"` matches nothing). A rule needs at least one of `limit` or `ttl`, or it is ignored and logged; unbounded retention is refused rather than honoured.
476
+
477
+ `ttl` accepts `"30s"`, `"15m"`, `"24h"`, `"7d"`, or a millisecond number. An unparseable value is ignored with a warning rather than guessed at.
478
+
479
+ ### Client usage
480
+
481
+ ```typescript
482
+ const channel = client.realtime.channel("doc:42", { history: true });
483
+
484
+ // Replayed messages arrive through the SAME handler, in order.
485
+ channel.onBroadcast("op", (payload) => applyOperation(payload));
486
+ await channel.join();
487
+ ```
488
+
489
+ The SDK requests catch-up on `join()` and after every reconnect, resuming from the highest `seq` it has delivered. Explicit fetch:
490
+
491
+ ```typescript
492
+ const { messages, retained, latestSeq } = await channel.history({ sinceSeq, limit });
493
+ console.log(channel.sequence); // highest seq delivered so far
494
+ ```
495
+
496
+ ### Behaviour worth knowing
497
+
498
+ - **`seq` is per-channel, dense and increasing.** It appears on `BroadcastEvent.seq` only for retained channels; ephemeral broadcasts have no `seq`.
499
+ - **`replayed: true`** marks a message delivered by catch-up rather than live. Handlers usually ignore it.
500
+ - **Replays overlap, and duplicates are dropped by the SDK.** Anything at or below the last delivered sequence is discarded, so handlers never see a message twice.
501
+ - **Your own messages are not filtered out of a replay.** A reconnect assigns a new client id, so filtering by sender would fail in exactly the case catch-up exists for. Operations must be idempotent.
502
+ - **A message that cannot be persisted is not delivered**, and the sender gets a `CHANNEL_HISTORY_WRITE_FAILED` error. Delivering it would leave a gap no replay could repair.
503
+ - **Live messages are held back during catch-up** so ordering holds, bounded by a 10s timeout.
504
+
505
+ ### Storage
506
+
507
+ Two tables in the `rebase` schema, created on startup only when a rule exists: `rebase.channel_messages` (keyed `(channel, seq)`) and `rebase.channel_cursors`. Pruning removes messages only — cursors are kept forever, because restarting a sequence would change what a client's saved resume point means.
508
+
509
+ > **Security:** history has the same access model as the channel. Anyone who may join may replay what was said before they arrived. Enabling retention on a publicly-joinable channel makes its past readable to any visitor.
510
+
452
511
  ## Presence Tracking
453
512
 
454
513
  Presence tracks which users are currently online in a channel, with custom state per user.
@@ -150,7 +150,7 @@ await rebase.data.posts.delete('post-123');
150
150
 
151
151
  // Count
152
152
  const total = await rebase.data.posts.count({
153
- where: { status: 'eq.published' },
153
+ where: { status: ['==', 'published'] },
154
154
  });
155
155
  ```
156
156
 
@@ -177,31 +177,39 @@ Pass a params object directly to `.find()`:
177
177
  const result = await rebase.data.posts.find({
178
178
  limit: 20,
179
179
  offset: 0,
180
- orderBy: 'createdAt:desc',
180
+ orderBy: ['createdAt', 'desc'],
181
181
  searchString: 'search term',
182
182
  include: ['tags', 'author'], // or ['*'] for all relations
183
183
  where: {
184
- status: 'eq.published',
185
- price: 'gte.100',
186
- category: 'in.(electronics,books)',
184
+ status: ['==', 'published'],
185
+ price: ['>=', 100],
186
+ category: ['in', ['electronics', 'books']],
187
187
  },
188
188
  });
189
189
  ```
190
190
 
191
- ### Filter Operators (String Syntax)
191
+ ### Filter Operators (Tuple Syntax)
192
+
193
+ Every filter is a `[operator, value]` tuple. The bare-string and
194
+ `'eq.published'` wire formats are **not** part of the public API: they are
195
+ `WireFilterValues`, marked `@internal`, and `serializeFilter` passes such a
196
+ string straight through to PostgREST unchanged — so an operator-less value
197
+ silently produces a malformed query rather than an error.
192
198
 
193
199
  | Operator | Description | Example |
194
200
  |----------|-------------|---------|
195
- | `eq` | Equal | `'eq.published'` |
196
- | `neq` | Not equal | `'neq.draft'` |
197
- | `gt` | Greater than | `'gt.100'` |
198
- | `gte` | Greater than or equal | `'gte.100'` |
199
- | `lt` | Less than | `'lt.50'` |
200
- | `lte` | Less than or equal | `'lte.50'` |
201
- | `in` | In list | `'in.(electronics,books)'` |
202
- | `nin` | Not in list | `'nin.(archived,deleted)'` |
203
- | `cs` | Array contains | `'cs.tag1'` |
204
- | `csa` | Array contains any | `'csa.(tag1,tag2)'` |
201
+ | `==` | Equal | `['==', 'published']` |
202
+ | `!=` | Not equal | `['!=', 'draft']` |
203
+ | `>` | Greater than | `['>', 100]` |
204
+ | `>=` | Greater than or equal | `['>=', 100]` |
205
+ | `<` | Less than | `['<', 50]` |
206
+ | `<=` | Less than or equal | `['<=', 50]` |
207
+ | `in` | In list | `['in', ['electronics', 'books']]` |
208
+ | `not-in` | Not in list | `['not-in', ['archived', 'deleted']]` |
209
+ | `array-contains` | Array contains | `['array-contains', 'tag1']` |
210
+ | `array-contains-any` | Array contains any | `['array-contains-any', ['tag1', 'tag2']]` |
211
+ | `like` / `ilike` | Pattern match (SQL wildcards) | `['ilike', '%john%']` |
212
+ | `is-null` / `is-not-null` | Null checks (value ignored) | `['is-null', null]` |
205
213
 
206
214
  ### Approach 2: Fluent QueryBuilder
207
215
 
@@ -352,7 +360,7 @@ unsubscribe();
352
360
  ```typescript
353
361
  // Via CollectionClient
354
362
  const unsubscribe = rebase.data.posts.listen(
355
- { where: { status: 'published' }, limit: 50 },
363
+ { where: { status: ['==', 'published'] }, limit: 50 },
356
364
  (response) => {
357
365
  // response: FindResponse with updated data
358
366
  console.log('Updated posts:', response.data);
@@ -316,7 +316,7 @@ For full documentation on collection callbacks, see the `rebase-collections` ski
316
316
  Rebase follows a **fail-closed** security model throughout the stack:
317
317
 
318
318
  1. **Scoped driver or nothing** — The REST API's `getScopedDriver()` throws if no scoped driver is available. It **never** falls back to the unscoped driver:
319
- ```typescript
319
+ ```typescript no-verify
320
320
  private getScopedDriver(c): DataDriver {
321
321
  const driver = c.get("driver") as DataDriver | undefined;
322
322
  if (!driver) throw ApiError.internal("Scoped driver not available");
@@ -332,7 +332,7 @@ STORAGE_GCS_BUCKET=my-bucket
332
332
  ```
333
333
 
334
334
  ❌ **Anti-pattern (never do this in app/backend code):**
335
- ```typescript
335
+ ```typescript no-verify
336
336
  import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; // ❌
337
337
  const s3 = new S3Client({ region, endpoint }); // ❌ hardcodes S3
338
338
  await s3.send(new PutObjectCommand({ Bucket, Key, Body })); // ❌ bypasses rebase.storage
@@ -31,7 +31,7 @@ The package exports five modules:
31
31
  export * from "./components"; // All UI components
32
32
  export * from "./styles"; // Tailwind style mixin strings
33
33
  export * from "./util"; // cls(), debounce(), chip_colors, keyToIconComponent
34
- export * from "./icons"; // Icon component, iconSize, lucideIcons map, icon_keys, cool_icon_keys
34
+ export * from "./icons"; // Icon component, iconSize, lucideIcons map, iconKeys, cool_icon_keys
35
35
  export * from "./hooks"; // React hooks
36
36
  ```
37
37
 
@@ -1149,13 +1149,13 @@ const IconComponent = lucideIcons["Database"]; // LucideIcon component
1149
1149
  <IconComponent size={24} />
1150
1150
  ```
1151
1151
 
1152
- ### `icon_keys` and `cool_icon_keys`
1152
+ ### `iconKeys` and `cool_icon_keys`
1153
1153
 
1154
- - **`icon_keys`** — Array of ~1900+ all available Lucide icon name strings.
1154
+ - **`iconKeys`** — Array of ~1900+ all available Lucide icon name strings.
1155
1155
  - **`cool_icon_keys`** — Curated subset of ~50 visually distinctive icon names for icon pickers.
1156
1156
 
1157
1157
  ```tsx
1158
- import { icon_keys, coolIconKeys, lucideIcons } from "@rebasepro/ui";
1158
+ import { iconKeys, coolIconKeys, lucideIcons } from "@rebasepro/ui";
1159
1159
  ```
1160
1160
 
1161
1161
  ### `keyToIconComponent(key: string): string`
@@ -1265,7 +1265,7 @@ debouncedSearch.clear(); // Cancel pending call
1265
1265
  Deterministic 32-bit hash of a string. Returns a positive integer.
1266
1266
 
1267
1267
  ```tsx
1268
- import { hashString } from "@rebasepro/ui";
1268
+ import { hashString } from "@rebasepro/utils";
1269
1269
  hashString("my-entity-id"); // e.g. 1234567
1270
1270
  ```
1271
1271
 
@@ -1280,7 +1280,7 @@ import {
1280
1280
  ```
1281
1281
 
1282
1282
  **`ChipColorScheme`** type:
1283
- ```ts
1283
+ ```ts no-verify
1284
1284
  {
1285
1285
  color: string; // Light mode background hex
1286
1286
  text: string; // Light mode text hex