@rebasepro/agent-skills 0.7.0 → 0.9.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.
@@ -848,6 +848,97 @@ Key props: `data`, `columns` (array of `VirtualTableColumn`), `cellRenderer`, `o
848
848
 
849
849
  See `VirtualTableProps` and `VirtualTableColumn` types for full API.
850
850
 
851
+ #### Generic Editable Tables & Cell Selection
852
+
853
+ `@rebasepro/ui` includes a generic, high-performance selection engine and primitive cell inputs that allow you to construct keyboard-navigable editable grid tables without any Rebase database dependencies.
854
+
855
+ **Selection Hooks & Context:**
856
+ * `createVirtualTableSelectionStore()` — Instantiates a selection store tracking active selection by `rowId` and `columnKey`. It uses `useSyncExternalStore` so that selection changes only trigger re-renders on the active cell (preventing whole-table lag).
857
+ * `VirtualTableSelectionProvider` — React context provider to pass down the selection store.
858
+ * `useVirtualTableSelection()` — Hook to access the selection store and triggers inside custom cells.
859
+ * `useVirtualTableCellSelected(store, columnKey, rowId)` — Returns `boolean` indicating if this cell is currently focused.
860
+
861
+ **Generic Cell Inputs:**
862
+ * `VirtualTableInput` — Auto-growing, debounced multiline/single text input.
863
+ * `VirtualTableNumberInput` — Constrained numeric input.
864
+ * `VirtualTableSwitch` — Small boolean switch toggle.
865
+ * `VirtualTableDateField` — Decoupled Date/DateTime picker (accepts `locale`, `timezone`, `mode`).
866
+ * `VirtualTableSelect` — Option-based single/multi dropdown list using chips. Takes `options: { value, label, color? }[]`.
867
+
868
+ **Example Custom Editable Table:**
869
+ ```tsx
870
+ import React, { useMemo, useState, useCallback } from "react";
871
+ import {
872
+ VirtualTable,
873
+ createVirtualTableSelectionStore,
874
+ VirtualTableSelectionProvider,
875
+ useVirtualTableCellSelected,
876
+ useVirtualTableSelection,
877
+ VirtualTableInput
878
+ } from "@rebasepro/ui";
879
+
880
+ function EditableCell({ rowId, columnKey, value, onUpdate }) {
881
+ const { selectionStore } = useVirtualTableSelection();
882
+ const selected = useVirtualTableCellSelected(selectionStore, columnKey, rowId);
883
+
884
+ return (
885
+ <div
886
+ className="w-full h-full flex items-center px-2 cursor-text"
887
+ onClick={(e) => selectionStore.select({
888
+ rowId,
889
+ columnKey,
890
+ cellRect: e.currentTarget.getBoundingClientRect(),
891
+ width: e.currentTarget.offsetWidth,
892
+ height: e.currentTarget.offsetHeight
893
+ })}
894
+ >
895
+ <VirtualTableInput
896
+ value={value}
897
+ updateValue={onUpdate}
898
+ focused={selected}
899
+ disabled={false}
900
+ multiline={false}
901
+ />
902
+ </div>
903
+ );
904
+ }
905
+
906
+ export function MyEditableGrid() {
907
+ const [data, setData] = useState([
908
+ { id: "row-1", name: "Project Alpha" },
909
+ { id: "row-2", name: "Project Beta" }
910
+ ]);
911
+
912
+ const selectionStore = useMemo(() => createVirtualTableSelectionStore(), []);
913
+
914
+ const updateCell = useCallback((rowId: string, value: string) => {
915
+ setData(prev => prev.map(row => row.id === rowId ? { ...row, name: value } : row));
916
+ }, []);
917
+
918
+ const columns = [
919
+ { key: "name", title: "Name", width: 300 }
920
+ ];
921
+
922
+ return (
923
+ <VirtualTableSelectionProvider store={selectionStore}>
924
+ <VirtualTable
925
+ data={data}
926
+ columns={columns}
927
+ rowHeight={52}
928
+ cellRenderer={({ rowData, column }) => (
929
+ <EditableCell
930
+ rowId={rowData.id}
931
+ columnKey={column.key}
932
+ value={rowData.name}
933
+ onUpdate={(val) => updateCell(rowData.id, val || "")}
934
+ />
935
+ )}
936
+ />
937
+ </VirtualTableSelectionProvider>
938
+ );
939
+ }
940
+ ```
941
+
851
942
  ### ToggleButtonGroup
852
943
 
853
944
  Single-select button group for toggling between options.
@@ -5,7 +5,7 @@ description: Guide for sending outbound HTTP webhooks on entity changes in a Reb
5
5
 
6
6
  # Rebase Webhooks
7
7
 
8
- > **IMPORTANT FOR AGENTS**: The `WebhookDispatcher` is a **standalone service class** exported from `@rebasepro/server-core`. It is **not** auto-wired into the backend init pipeline — you must instantiate it yourself and call `onEntityChange()` from your application code (e.g. entity callbacks, custom functions, or cron jobs). Do NOT look for a `webhooks` key in `RebaseBackendConfig`.
8
+ > **IMPORTANT FOR AGENTS**: The `WebhookDispatcher` is a **standalone service class** exported from `@rebasepro/server-core`. It is **not** auto-wired into the backend init pipeline — you must instantiate it yourself and call `onEntityChange()` from your application code (e.g. collection callbacks, custom functions, or cron jobs). Do NOT look for a `webhooks` key in `RebaseBackendConfig`.
9
9
 
10
10
  > **IMPORTANT FOR AGENTS**: Webhooks are **outbound** HTTP POST requests sent by your Rebase backend to external URLs. They are NOT inbound endpoints. To receive webhooks FROM external services, use Rebase custom functions instead (see `rebase-custom-functions` skill).
11
11
 
@@ -71,7 +71,7 @@ dispatcher.setWebhooks([
71
71
 
72
72
  ### Dispatch on Entity Changes
73
73
 
74
- Call `onEntityChange()` whenever an entity is created, updated, or deleted. The dispatcher checks all registered webhooks and fires matching ones.
74
+ Call `onEntityChange()` whenever a entity is created, updated, or deleted. The dispatcher checks all registered webhooks and fires matching ones.
75
75
 
76
76
  ```typescript
77
77
  const results = await dispatcher.onEntityChange(
@@ -355,13 +355,13 @@ If the receiver does not respond within 10 seconds, the request is aborted and t
355
355
 
356
356
  ## Integration Patterns
357
357
 
358
- ### With Entity Callbacks
358
+ ### With Collection Callbacks
359
359
 
360
- The most common pattern is to wire the dispatcher into Rebase entity callbacks so webhooks fire automatically on CRUD operations:
360
+ The most common pattern is to wire the dispatcher into Rebase collection callbacks so webhooks fire automatically on CRUD operations:
361
361
 
362
362
  ```typescript
363
363
  // backend/collections/orders.ts
364
- import type { EntityCollection, EntityCallbacks } from "@rebasepro/types";
364
+ import type { CollectionConfig, CollectionCallbacks } from "@rebasepro/types";
365
365
  import { WebhookDispatcher } from "@rebasepro/server-core/services/webhook-service";
366
366
 
367
367
  const dispatcher = new WebhookDispatcher();
@@ -376,35 +376,28 @@ dispatcher.setWebhooks([
376
376
  },
377
377
  ]);
378
378
 
379
- const callbacks: EntityCallbacks = {
380
- afterCreate: async ({ entity, collection }) => {
379
+ const callbacks: CollectionCallbacks = {
380
+ afterSave: async ({ id, values, previousValues, status, collection }) => {
381
+ const event = status === "new" ? "INSERT" : "UPDATE";
381
382
  await dispatcher.onEntityChange(
382
383
  collection.path,
383
- "INSERT",
384
- entity.id,
385
- entity
386
- );
387
- },
388
- afterUpdate: async ({ entity, previousEntity, collection }) => {
389
- await dispatcher.onEntityChange(
390
- collection.path,
391
- "UPDATE",
392
- entity.id,
393
- entity,
394
- previousEntity
384
+ event,
385
+ String(id),
386
+ values,
387
+ status === "new" ? undefined : previousValues
395
388
  );
396
389
  },
397
- afterDelete: async ({ entityId, collection }) => {
390
+ afterDelete: async ({ id, collection }) => {
398
391
  await dispatcher.onEntityChange(
399
392
  collection.path,
400
393
  "DELETE",
401
- entityId,
394
+ String(id),
402
395
  null
403
396
  );
404
397
  },
405
398
  };
406
399
 
407
- const ordersCollection: EntityCollection = {
400
+ const ordersCollection: CollectionConfig = {
408
401
  name: "Orders",
409
402
  path: "orders",
410
403
  callbacks,
@@ -519,9 +512,11 @@ Then import it from any callback or function:
519
512
  ```typescript
520
513
  import { dispatcher } from "../lib/webhooks";
521
514
 
522
- // In an entity callback:
523
- afterCreate: async ({ entity, collection }) => {
524
- await dispatcher.onEntityChange(collection.path, "INSERT", entity.id, entity);
515
+ // In a collection callback:
516
+ afterSave: async ({ id, values, status, collection }) => {
517
+ if (status === "new") {
518
+ await dispatcher.onEntityChange(collection.path, "INSERT", String(id), values);
519
+ }
525
520
  },
526
521
  ```
527
522
 
@@ -535,7 +530,7 @@ Registers the list of webhooks to watch. Filters out any with `enabled: false`.
535
530
  |-----------|------|-------------|
536
531
  | `webhooks` | `WebhookConfig[]` | Array of webhook configurations. |
537
532
 
538
- ### `onEntityChange(table, event, entityId, entity, previousEntity?): Promise<WebhookDeliveryResult[]>`
533
+ ### `onEntityChange(table, event, id, entity, previousEntity?): Promise<WebhookDeliveryResult[]>`
539
534
 
540
535
  Checks all registered webhooks for matching `table` + `event`, and dispatches to each match.
541
536
 
@@ -543,7 +538,7 @@ Checks all registered webhooks for matching `table` + `event`, and dispatches to
543
538
  |-----------|------|-------------|
544
539
  | `table` | `string` | The database table name (e.g. `"orders"`). |
545
540
  | `event` | `"INSERT" \| "UPDATE" \| "DELETE"` | The type of entity change. |
546
- | `entityId` | `string` | The unique ID of the changed entity. |
541
+ | `id` | `string` | The unique ID of the changed entity. |
547
542
  | `entity` | `Record<string, unknown> \| null` | The current entity data. May be `null` for deletes. |
548
543
  | `previousEntity` | `Record<string, unknown> \| null` | *(Optional)* The previous entity state. Only relevant for `UPDATE` events — included as `old_record` in the payload. |
549
544
 
@@ -557,7 +552,7 @@ The dispatcher supports three event types, passed as the `event` parameter to `o
557
552
  |-------|-------------|--------------------|-----------------------|
558
553
  | `INSERT` | A new entity was created | The new entity | `undefined` |
559
554
  | `UPDATE` | An existing entity was modified | The updated entity | The entity before update |
560
- | `DELETE` | An entity was removed | The deleted entity (or `null`) | `undefined` |
555
+ | `DELETE` | a entity was removed | The deleted entity (or `null`) | `undefined` |
561
556
 
562
557
  ## Error Handling
563
558
 
@@ -592,9 +587,9 @@ for (const result of results) {
592
587
  If you don't want webhook delivery to block your API response, use fire-and-forget:
593
588
 
594
589
  ```typescript
595
- afterCreate: async ({ entity, collection }) => {
590
+ afterSave: async ({ id, values, collection }) => {
596
591
  // Fire-and-forget — don't await
597
- dispatcher.onEntityChange(collection.path, "INSERT", entity.id, entity)
592
+ dispatcher.onEntityChange(collection.path, "INSERT", String(id), values)
598
593
  .catch(err => console.error("Webhook dispatch error:", err));
599
594
  },
600
595
  ```