@xnetjs/react 0.12.0 → 1.0.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.
@@ -2478,7 +2478,9 @@ function useCan(nodeId) {
2478
2478
  try {
2479
2479
  const [read, write, del, share] = await Promise.all([
2480
2480
  store.auth.can({ action: "read", nodeId }),
2481
- store.auth.can({ action: "write", nodeId }),
2481
+ // 'update' is the precise verb for mutating an existing node; it
2482
+ // falls back to the schema's write expression (0304).
2483
+ store.auth.can({ action: "update", nodeId }),
2482
2484
  store.auth.can({ action: "delete", nodeId }),
2483
2485
  store.auth.can({ action: "share", nodeId })
2484
2486
  ]);
@@ -2553,7 +2555,9 @@ function useCanEdit(nodeId) {
2553
2555
  try {
2554
2556
  const [read, write] = await Promise.all([
2555
2557
  store.auth.can({ action: "read", nodeId }),
2556
- store.auth.can({ action: "write", nodeId })
2558
+ // 'update' is the precise verb for mutating an existing node; it
2559
+ // falls back to the schema's write expression (0304).
2560
+ store.auth.can({ action: "update", nodeId })
2557
2561
  ]);
2558
2562
  if (requestRef.current !== requestId) return;
2559
2563
  const roleSet = /* @__PURE__ */ new Set([...read.roles, ...write.roles]);
@@ -73,7 +73,7 @@ import {
73
73
  useUndo,
74
74
  useVerification,
75
75
  useVisibleComments
76
- } from "./chunk-SEYIZDHG.js";
76
+ } from "./chunk-MEIYFD6R.js";
77
77
  import {
78
78
  flattenNode,
79
79
  flattenNodes,
package/dist/index.d.ts CHANGED
@@ -457,6 +457,45 @@ declare function createSavedViewVisualCanvasProjectionRequest(input: {
457
457
  }): SavedViewVisualCanvasProjectionRequest;
458
458
  declare function SavedViewResultTable({ query, columns, expandedRowId, onToggleRow, loadingLabel, emptyLabel, formatValue }: SavedViewResultTableProps): JSX.Element;
459
459
 
460
+ /**
461
+ * Minimal Awareness surface (compatible with y-protocols/awareness).
462
+ * Duck-typed so tests and non-Yjs transports can provide their own.
463
+ */
464
+ interface PresenceAwareness {
465
+ clientID: number;
466
+ getLocalState(): Record<string, unknown> | null;
467
+ setLocalState(state: Record<string, unknown> | null): void;
468
+ getStates(): Map<number, Record<string, unknown>>;
469
+ on(event: 'change', handler: () => void): void;
470
+ off(event: 'change', handler: () => void): void;
471
+ }
472
+ interface PresencePeer<T> {
473
+ /** Awareness client id (one per connected tab/device). */
474
+ clientId: number;
475
+ /** That peer's presence state. */
476
+ state: T;
477
+ }
478
+ interface UsePresenceOptions {
479
+ /**
480
+ * Minimum ms between broadcasts (default 33 ≈ 30fps). The hub rate-limits
481
+ * at 100 messages/sec/connection and closes the socket on repeated
482
+ * breaches — do not go below ~16ms.
483
+ */
484
+ throttleMs?: number;
485
+ }
486
+ interface UsePresenceResult<T> {
487
+ /** Remote peers only (local client excluded). Evicted on disconnect. */
488
+ peers: Array<PresencePeer<T>>;
489
+ /**
490
+ * Merge a partial patch into local presence. Patches within one throttle
491
+ * window coalesce into a single broadcast (leading + trailing edge).
492
+ */
493
+ setState: (patch: Partial<T>) => void;
494
+ /** Local awareness client id, or null before awareness attaches. */
495
+ clientId: number | null;
496
+ }
497
+ declare function usePresence<T extends Record<string, unknown>>(awareness: PresenceAwareness | null | undefined, initialState: T, options?: UsePresenceOptions): UsePresenceResult<T>;
498
+
460
499
  /**
461
500
  * useCanvasTaskSync - Reconcile canvas checklist items with Task nodes.
462
501
  *
@@ -630,6 +669,31 @@ interface UseGlobalUndoResult {
630
669
  }
631
670
  declare function useGlobalUndo(): UseGlobalUndoResult;
632
671
 
672
+ /**
673
+ * useCanCreate - Check whether the current user may create a node of a schema
674
+ * (exploration 0304).
675
+ *
676
+ * The check runs against a *draft* node built from `schemaId` + `properties`,
677
+ * so container relations in the draft (e.g. `space`, `channel`) resolve
678
+ * membership roles — pass the relation the composer is about to write into:
679
+ *
680
+ * ```tsx
681
+ * const { canCreate } = useCanCreate(CHAT_MESSAGE_SCHEMA_IRI, { channel: channelId })
682
+ * <SendButton disabled={!canCreate} />
683
+ * ```
684
+ *
685
+ * Schemas that declare no `create` expression fall back to their `write`
686
+ * policy, which includes the creator role in every preset — those schemas
687
+ * resolve to `canCreate: true`.
688
+ */
689
+
690
+ type UseCanCreateResult = {
691
+ canCreate: boolean;
692
+ loading: boolean;
693
+ error: Error | null;
694
+ };
695
+ declare function useCanCreate(schemaId: SchemaIRI, properties?: Record<string, unknown>): UseCanCreateResult;
696
+
633
697
  /**
634
698
  * useBilling — reactive billing state + checkout, the same shape as `useIdentity`.
635
699
  *
@@ -693,4 +757,4 @@ declare function flattenTaskTree(items: TaskTreeItem[], depth?: number): Rendera
693
757
  declare function formatTaskDueDate(timestamp: number | undefined): string | null;
694
758
  declare function isTaskOverdue(timestamp: number | undefined, completed: boolean): boolean;
695
759
 
696
- export { type AddRowOptions, type CanvasTaskInput, type CheckoutOptions, FlatNode, type GridFieldModel, type GridOptionModel, type GridRowModel, type GridViewModel, QueryFilter, type RenderableTaskRow, type SavedViewCanvasProjectionNode, type SavedViewDateBrushSelection, type SavedViewDateBucketFieldSummary, type SavedViewDateBucketInterval, type SavedViewDateBucketSummary, type SavedViewFacetSelection, type SavedViewFacetSummary, type SavedViewFacetValueSummary, type SavedViewFeedEnrichmentAdapter, type SavedViewFeedEnrichmentEntry, type SavedViewInspectorItem, type SavedViewInspectorItemKind, type SavedViewLensDraft, type SavedViewPresentationMode, type SavedViewPrivacyChip, type SavedViewPrivacyChipTone, type SavedViewPrivacySummary, type SavedViewQueryOverride, type SavedViewQueryResult, SavedViewResultTable, type SavedViewResultTableProps, type SavedViewRowInspectorModel, SavedViewRunner, type SavedViewRunnerProps, type SavedViewSchemaRegistry, type SavedViewSortDirection, type SavedViewVisualCanvasProjectionRequest, SavedViewVisualFeed, type SavedViewVisualLayoutId, type SavedViewVisualLayoutOption, type SavedViewVisualPreviewCreator, type SavedViewVisualPreviewKind, type SavedViewVisualPreviewModel, type SavedViewVisualPreviewPrivacy, type SavedViewVisualPreviewRelationship, type SavedViewVisualTimelineBucket, type SavedViewVisualWorkspaceLayout, TaskProjectionInput, TaskTreeItem, type UseBillingResult, type UseCanvasTaskSyncOptions, type UseCanvasTaskSyncResult, type UseEffectiveSchemaResult, type UseGlobalUndoResult, type UseGridDatabaseOptions, type UseGridDatabaseResult, type UseSavedViewOptions, type UseSavedViewResult, UseTaskProjectionSyncResult, createSavedViewCanvasProjectionNodes, createSavedViewLensDraft, createSavedViewVisualCanvasProjectionRequest, createSavedViewVisualPreviewFingerprint, deriveCachedSavedViewVisualPreviews, deriveSavedViewColumns, deriveSavedViewDateBucketSummaries, deriveSavedViewFacetSummaries, deriveSavedViewPrivacyChips, deriveSavedViewRowInspector, deriveSavedViewTimelineBuckets, deriveSavedViewVisualPreview, deriveSavedViewVisualPreviews, filterSavedViewRowsByDateBrush, filterSavedViewRowsByFacets, flattenTaskTree, formatSavedViewCellValue, formatTaskDueDate, getSavedViewSensitiveResultWarning, hasSavedViewVisualPreviewSensitiveData, isSavedViewVisualPreviewEmbeddable, isTaskOverdue, mergeSavedViewFeedEnrichment, savedViewVisualPreviewIsSelfActor, useBilling, useCanvasTaskSync, useEffectiveSchema, useGlobalUndo, useGridDatabase, useSavedView };
760
+ export { type AddRowOptions, type CanvasTaskInput, type CheckoutOptions, FlatNode, type GridFieldModel, type GridOptionModel, type GridRowModel, type GridViewModel, type PresenceAwareness, type PresencePeer, QueryFilter, type RenderableTaskRow, type SavedViewCanvasProjectionNode, type SavedViewDateBrushSelection, type SavedViewDateBucketFieldSummary, type SavedViewDateBucketInterval, type SavedViewDateBucketSummary, type SavedViewFacetSelection, type SavedViewFacetSummary, type SavedViewFacetValueSummary, type SavedViewFeedEnrichmentAdapter, type SavedViewFeedEnrichmentEntry, type SavedViewInspectorItem, type SavedViewInspectorItemKind, type SavedViewLensDraft, type SavedViewPresentationMode, type SavedViewPrivacyChip, type SavedViewPrivacyChipTone, type SavedViewPrivacySummary, type SavedViewQueryOverride, type SavedViewQueryResult, SavedViewResultTable, type SavedViewResultTableProps, type SavedViewRowInspectorModel, SavedViewRunner, type SavedViewRunnerProps, type SavedViewSchemaRegistry, type SavedViewSortDirection, type SavedViewVisualCanvasProjectionRequest, SavedViewVisualFeed, type SavedViewVisualLayoutId, type SavedViewVisualLayoutOption, type SavedViewVisualPreviewCreator, type SavedViewVisualPreviewKind, type SavedViewVisualPreviewModel, type SavedViewVisualPreviewPrivacy, type SavedViewVisualPreviewRelationship, type SavedViewVisualTimelineBucket, type SavedViewVisualWorkspaceLayout, TaskProjectionInput, TaskTreeItem, type UseBillingResult, type UseCanCreateResult, type UseCanvasTaskSyncOptions, type UseCanvasTaskSyncResult, type UseEffectiveSchemaResult, type UseGlobalUndoResult, type UseGridDatabaseOptions, type UseGridDatabaseResult, type UsePresenceOptions, type UsePresenceResult, type UseSavedViewOptions, type UseSavedViewResult, UseTaskProjectionSyncResult, createSavedViewCanvasProjectionNodes, createSavedViewLensDraft, createSavedViewVisualCanvasProjectionRequest, createSavedViewVisualPreviewFingerprint, deriveCachedSavedViewVisualPreviews, deriveSavedViewColumns, deriveSavedViewDateBucketSummaries, deriveSavedViewFacetSummaries, deriveSavedViewPrivacyChips, deriveSavedViewRowInspector, deriveSavedViewTimelineBuckets, deriveSavedViewVisualPreview, deriveSavedViewVisualPreviews, filterSavedViewRowsByDateBrush, filterSavedViewRowsByFacets, flattenTaskTree, formatSavedViewCellValue, formatTaskDueDate, getSavedViewSensitiveResultWarning, hasSavedViewVisualPreviewSensitiveData, isSavedViewVisualPreviewEmbeddable, isTaskOverdue, mergeSavedViewFeedEnrichment, savedViewVisualPreviewIsSelfActor, useBilling, useCanCreate, useCanvasTaskSync, useEffectiveSchema, useGlobalUndo, useGridDatabase, usePresence, useSavedView };
package/dist/index.js CHANGED
@@ -94,7 +94,7 @@ import {
94
94
  useUndo,
95
95
  useVerification,
96
96
  useVisibleComments
97
- } from "./chunk-SEYIZDHG.js";
97
+ } from "./chunk-MEIYFD6R.js";
98
98
  import {
99
99
  EMPTY_PAGE_INFO,
100
100
  computeFallbackPageInfo,
@@ -4205,6 +4205,92 @@ function formatSavedViewInspectorValue(key, value) {
4205
4205
  return formatSavedViewCellValue(key, value);
4206
4206
  }
4207
4207
 
4208
+ // src/hooks/usePresence.ts
4209
+ import { useCallback as useCallback2, useEffect as useEffect5, useRef as useRef3, useState as useState5 } from "react";
4210
+ function readPeers(awareness, ownKeys) {
4211
+ const peers = [];
4212
+ awareness.getStates().forEach((state, clientId) => {
4213
+ if (clientId === awareness.clientID || !state) return;
4214
+ for (const key of ownKeys) {
4215
+ if (key in state) {
4216
+ peers.push({ clientId, state });
4217
+ return;
4218
+ }
4219
+ }
4220
+ });
4221
+ return peers;
4222
+ }
4223
+ function usePresence(awareness, initialState, options = {}) {
4224
+ const { throttleMs = 33 } = options;
4225
+ const [peers, setPeers] = useState5([]);
4226
+ const [clientId, setClientId] = useState5(null);
4227
+ const ownKeysRef = useRef3(new Set(Object.keys(initialState)));
4228
+ const initialStateRef = useRef3(initialState);
4229
+ initialStateRef.current = initialState;
4230
+ const awarenessRef = useRef3(null);
4231
+ const pendingRef = useRef3({});
4232
+ const lastBroadcastRef = useRef3(0);
4233
+ const timerRef = useRef3(null);
4234
+ const flush = useCallback2(() => {
4235
+ const aw = awarenessRef.current;
4236
+ if (!aw) return;
4237
+ lastBroadcastRef.current = Date.now();
4238
+ const patch = pendingRef.current;
4239
+ pendingRef.current = {};
4240
+ aw.setLocalState({ ...aw.getLocalState() ?? {}, ...patch });
4241
+ }, []);
4242
+ const setState = useCallback2(
4243
+ (patch) => {
4244
+ for (const key of Object.keys(patch)) ownKeysRef.current.add(key);
4245
+ pendingRef.current = { ...pendingRef.current, ...patch };
4246
+ const elapsed = Date.now() - lastBroadcastRef.current;
4247
+ if (elapsed >= throttleMs) {
4248
+ flush();
4249
+ } else if (timerRef.current === null) {
4250
+ timerRef.current = setTimeout(() => {
4251
+ timerRef.current = null;
4252
+ flush();
4253
+ }, throttleMs - elapsed);
4254
+ }
4255
+ },
4256
+ [flush, throttleMs]
4257
+ );
4258
+ useEffect5(() => {
4259
+ if (!awareness) {
4260
+ awarenessRef.current = null;
4261
+ setClientId(null);
4262
+ setPeers([]);
4263
+ return;
4264
+ }
4265
+ awarenessRef.current = awareness;
4266
+ setClientId(awareness.clientID);
4267
+ const ownKeys = ownKeysRef.current;
4268
+ awareness.setLocalState({
4269
+ ...awareness.getLocalState() ?? {},
4270
+ ...initialStateRef.current
4271
+ });
4272
+ lastBroadcastRef.current = Date.now();
4273
+ const handleChange = () => {
4274
+ setPeers(readPeers(awareness, ownKeysRef.current));
4275
+ };
4276
+ handleChange();
4277
+ awareness.on("change", handleChange);
4278
+ return () => {
4279
+ awareness.off("change", handleChange);
4280
+ if (timerRef.current !== null) {
4281
+ clearTimeout(timerRef.current);
4282
+ timerRef.current = null;
4283
+ }
4284
+ pendingRef.current = {};
4285
+ const remaining = { ...awareness.getLocalState() ?? {} };
4286
+ for (const key of ownKeys) delete remaining[key];
4287
+ awareness.setLocalState(remaining);
4288
+ if (awarenessRef.current === awareness) awarenessRef.current = null;
4289
+ };
4290
+ }, [awareness]);
4291
+ return { peers, setState, clientId };
4292
+ }
4293
+
4208
4294
  // src/hooks/useCanvasTaskSync.ts
4209
4295
  function useCanvasTaskSync({
4210
4296
  canvasId,
@@ -4237,7 +4323,7 @@ import {
4237
4323
  convertCellValue,
4238
4324
  FormulaService
4239
4325
  } from "@xnetjs/data";
4240
- import { useCallback as useCallback2, useEffect as useEffect5, useMemo as useMemo5, useRef as useRef3, useState as useState5 } from "react";
4326
+ import { useCallback as useCallback3, useEffect as useEffect6, useMemo as useMemo5, useRef as useRef4, useState as useState6 } from "react";
4241
4327
  function toFieldModel(node) {
4242
4328
  return {
4243
4329
  id: node.id,
@@ -4358,8 +4444,8 @@ function useGridDatabase(databaseId, options = {}) {
4358
4444
  return effective.map((e) => e.field);
4359
4445
  }, [fields, activeView]);
4360
4446
  const { store } = useNodeStore();
4361
- const [rollupValues, setRollupValues] = useState5(/* @__PURE__ */ new Map());
4362
- useEffect5(() => {
4447
+ const [rollupValues, setRollupValues] = useState6(/* @__PURE__ */ new Map());
4448
+ useEffect6(() => {
4363
4449
  const rollupFields = fields.filter((f) => f.type === "rollup");
4364
4450
  if (rollupFields.length === 0 || !store) {
4365
4451
  setRollupValues((prev) => prev.size > 0 ? /* @__PURE__ */ new Map() : prev);
@@ -4459,11 +4545,11 @@ function useGridDatabase(databaseId, options = {}) {
4459
4545
  return sortRows(filtered, columns, activeView.sorts);
4460
4546
  }, [rowNodes, activeView, fields, databaseId, rollupValues]);
4461
4547
  const loading = dbStatus === "loading" || fieldStatus === "loading" || viewStatus === "loading" || rowStatus === "loading";
4462
- const rowTailRef = useRef3(void 0);
4463
- const fieldTailRef = useRef3(void 0);
4464
- const viewTailRef = useRef3(void 0);
4465
- const optionTailRef = useRef3(/* @__PURE__ */ new Map());
4466
- useEffect5(() => {
4548
+ const rowTailRef = useRef4(void 0);
4549
+ const fieldTailRef = useRef4(void 0);
4550
+ const viewTailRef = useRef4(void 0);
4551
+ const optionTailRef = useRef4(/* @__PURE__ */ new Map());
4552
+ useEffect6(() => {
4467
4553
  const dataTail = rows.reduce(
4468
4554
  (max, r) => !max || compareSortKeys(r.sortKey, max) > 0 ? r.sortKey : max,
4469
4555
  void 0
@@ -4472,19 +4558,19 @@ function useGridDatabase(databaseId, options = {}) {
4472
4558
  rowTailRef.current = dataTail;
4473
4559
  }
4474
4560
  }, [rows]);
4475
- useEffect5(() => {
4561
+ useEffect6(() => {
4476
4562
  const dataTail = fields[fields.length - 1]?.sortKey;
4477
4563
  if (dataTail && (!fieldTailRef.current || compareSortKeys(dataTail, fieldTailRef.current) > 0)) {
4478
4564
  fieldTailRef.current = dataTail;
4479
4565
  }
4480
4566
  }, [fields]);
4481
- useEffect5(() => {
4567
+ useEffect6(() => {
4482
4568
  const dataTail = views[views.length - 1]?.sortKey;
4483
4569
  if (dataTail && (!viewTailRef.current || compareSortKeys(dataTail, viewTailRef.current) > 0)) {
4484
4570
  viewTailRef.current = dataTail;
4485
4571
  }
4486
4572
  }, [views]);
4487
- useEffect5(() => {
4573
+ useEffect6(() => {
4488
4574
  for (const [fieldId, list] of optionsByField) {
4489
4575
  const dataTail = list[list.length - 1]?.sortKey;
4490
4576
  const current = optionTailRef.current.get(fieldId);
@@ -4493,7 +4579,7 @@ function useGridDatabase(databaseId, options = {}) {
4493
4579
  }
4494
4580
  }
4495
4581
  }, [optionsByField]);
4496
- const nextAppendKey = useCallback2((tailRef) => {
4582
+ const nextAppendKey = useCallback3((tailRef) => {
4497
4583
  const key = generateSortKey(tailRef.current, void 0);
4498
4584
  tailRef.current = key;
4499
4585
  return key;
@@ -4511,19 +4597,19 @@ function useGridDatabase(databaseId, options = {}) {
4511
4597
  localDID: did ?? null,
4512
4598
  options: { localOnly: true }
4513
4599
  });
4514
- const updateRowProps = useCallback2(
4600
+ const updateRowProps = useCallback3(
4515
4601
  async (rowId, properties) => {
4516
4602
  await mutate.update(DatabaseRowSchema, rowId, properties);
4517
4603
  },
4518
4604
  [mutate]
4519
4605
  );
4520
- const updateCell = useCallback2(
4606
+ const updateCell = useCallback3(
4521
4607
  async (rowId, fieldId, value) => {
4522
4608
  await updateRowProps(rowId, { [cellKey(fieldId)]: value });
4523
4609
  },
4524
4610
  [updateRowProps]
4525
4611
  );
4526
- const clearCells = useCallback2(
4612
+ const clearCells = useCallback3(
4527
4613
  async (cells) => {
4528
4614
  const byRow = /* @__PURE__ */ new Map();
4529
4615
  for (const { rowId, fieldId } of cells) {
@@ -4535,7 +4621,7 @@ function useGridDatabase(databaseId, options = {}) {
4535
4621
  },
4536
4622
  [updateRowProps]
4537
4623
  );
4538
- const addRow = useCallback2(
4624
+ const addRow = useCallback3(
4539
4625
  async (afterRowId, cells, opts) => {
4540
4626
  let sortKey;
4541
4627
  if (afterRowId) {
@@ -4570,13 +4656,13 @@ function useGridDatabase(databaseId, options = {}) {
4570
4656
  },
4571
4657
  [mutate, databaseId, rows, nextAppendKey]
4572
4658
  );
4573
- const deleteRows = useCallback2(
4659
+ const deleteRows = useCallback3(
4574
4660
  async (rowIds) => {
4575
4661
  await Promise.all(rowIds.map((id) => mutate.remove(id)));
4576
4662
  },
4577
4663
  [mutate]
4578
4664
  );
4579
- const moveRowToIndex = useCallback2(
4665
+ const moveRowToIndex = useCallback3(
4580
4666
  async (rowId, targetIndex) => {
4581
4667
  if (activeView && activeView.sorts.length > 0) return;
4582
4668
  const ordered = rows.map((r) => ({ id: r.id, key: r.sortKey }));
@@ -4585,7 +4671,7 @@ function useGridDatabase(databaseId, options = {}) {
4585
4671
  },
4586
4672
  [rows, activeView, updateRowProps]
4587
4673
  );
4588
- const createOption = useCallback2(
4674
+ const createOption = useCallback3(
4589
4675
  async (fieldId, name) => {
4590
4676
  const existing = optionsByField.get(fieldId) ?? [];
4591
4677
  const match = existing.find((o) => o.name.toLowerCase() === name.toLowerCase());
@@ -4604,7 +4690,7 @@ function useGridDatabase(databaseId, options = {}) {
4604
4690
  },
4605
4691
  [mutate, databaseId, optionsByField]
4606
4692
  );
4607
- const addField = useCallback2(
4693
+ const addField = useCallback3(
4608
4694
  async (name, type, config, opts) => {
4609
4695
  const node = await mutate.create(DatabaseFieldSchema, {
4610
4696
  database: databaseId,
@@ -4619,13 +4705,13 @@ function useGridDatabase(databaseId, options = {}) {
4619
4705
  },
4620
4706
  [mutate, databaseId, nextAppendKey]
4621
4707
  );
4622
- const renameField = useCallback2(
4708
+ const renameField = useCallback3(
4623
4709
  async (fieldId, name) => {
4624
4710
  await mutate.update(DatabaseFieldSchema, fieldId, { name });
4625
4711
  },
4626
4712
  [mutate]
4627
4713
  );
4628
- const updateFieldConfig = useCallback2(
4714
+ const updateFieldConfig = useCallback3(
4629
4715
  async (fieldId, config) => {
4630
4716
  await mutate.update(DatabaseFieldSchema, fieldId, {
4631
4717
  config
@@ -4633,7 +4719,7 @@ function useGridDatabase(databaseId, options = {}) {
4633
4719
  },
4634
4720
  [mutate]
4635
4721
  );
4636
- const changeFieldType = useCallback2(
4722
+ const changeFieldType = useCallback3(
4637
4723
  async (fieldId, type) => {
4638
4724
  const field = fields.find((f) => f.id === fieldId);
4639
4725
  const sourceType = field?.type ?? "text";
@@ -4670,7 +4756,7 @@ function useGridDatabase(databaseId, options = {}) {
4670
4756
  },
4671
4757
  [mutate, fields, rows, createOption, updateRowProps]
4672
4758
  );
4673
- const removeField = useCallback2(
4759
+ const removeField = useCallback3(
4674
4760
  async (fieldId) => {
4675
4761
  const orphanedOptions = optionsByField.get(fieldId) ?? [];
4676
4762
  await Promise.all(orphanedOptions.map((o) => mutate.remove(o.id)));
@@ -4678,7 +4764,7 @@ function useGridDatabase(databaseId, options = {}) {
4678
4764
  },
4679
4765
  [mutate, optionsByField]
4680
4766
  );
4681
- const moveFieldToIndex = useCallback2(
4767
+ const moveFieldToIndex = useCallback3(
4682
4768
  async (fieldId, targetIndex) => {
4683
4769
  if (!activeView) return;
4684
4770
  const ordered = visibleFields.map((f) => ({
@@ -4692,7 +4778,7 @@ function useGridDatabase(databaseId, options = {}) {
4692
4778
  },
4693
4779
  [mutate, activeView, visibleFields, fields]
4694
4780
  );
4695
- const resizeField = useCallback2(
4781
+ const resizeField = useCallback3(
4696
4782
  async (fieldId, width) => {
4697
4783
  if (!activeView) return;
4698
4784
  await mutate.update(DatabaseViewSchema, activeView.id, {
@@ -4701,7 +4787,7 @@ function useGridDatabase(databaseId, options = {}) {
4701
4787
  },
4702
4788
  [mutate, activeView]
4703
4789
  );
4704
- const setFieldHiddenInView = useCallback2(
4790
+ const setFieldHiddenInView = useCallback3(
4705
4791
  async (fieldId, hidden) => {
4706
4792
  if (!activeView) return;
4707
4793
  const set = new Set(activeView.hiddenFields);
@@ -4711,7 +4797,7 @@ function useGridDatabase(databaseId, options = {}) {
4711
4797
  },
4712
4798
  [mutate, activeView]
4713
4799
  );
4714
- const toggleSort = useCallback2(
4800
+ const toggleSort = useCallback3(
4715
4801
  async (fieldId) => {
4716
4802
  if (!activeView) return;
4717
4803
  const current = activeView.sorts.find((s) => s.columnId === fieldId);
@@ -4727,28 +4813,28 @@ function useGridDatabase(databaseId, options = {}) {
4727
4813
  },
4728
4814
  [mutate, activeView]
4729
4815
  );
4730
- const setFilters = useCallback2(
4816
+ const setFilters = useCallback3(
4731
4817
  async (filters) => {
4732
4818
  if (!activeView) return;
4733
4819
  await mutate.update(DatabaseViewSchema, activeView.id, { filters });
4734
4820
  },
4735
4821
  [mutate, activeView]
4736
4822
  );
4737
- const setGroupBy = useCallback2(
4823
+ const setGroupBy = useCallback3(
4738
4824
  async (fieldId) => {
4739
4825
  if (!activeView) return;
4740
4826
  await mutate.update(DatabaseViewSchema, activeView.id, { groupBy: fieldId ?? void 0 });
4741
4827
  },
4742
4828
  [mutate, activeView]
4743
4829
  );
4744
- const setRowHeight = useCallback2(
4830
+ const setRowHeight = useCallback3(
4745
4831
  async (rowHeight) => {
4746
4832
  if (!activeView) return;
4747
4833
  await mutate.update(DatabaseViewSchema, activeView.id, { rowHeight });
4748
4834
  },
4749
4835
  [mutate, activeView]
4750
4836
  );
4751
- const setColumnSummary = useCallback2(
4837
+ const setColumnSummary = useCallback3(
4752
4838
  async (fieldId, fn) => {
4753
4839
  if (!activeView) return;
4754
4840
  const next = { ...activeView.columnSummaries };
@@ -4758,28 +4844,28 @@ function useGridDatabase(databaseId, options = {}) {
4758
4844
  },
4759
4845
  [mutate, activeView]
4760
4846
  );
4761
- const setFormConfig = useCallback2(
4847
+ const setFormConfig = useCallback3(
4762
4848
  async (config) => {
4763
4849
  if (!activeView) return;
4764
4850
  await mutate.update(DatabaseViewSchema, activeView.id, { formConfig: config });
4765
4851
  },
4766
4852
  [mutate, activeView]
4767
4853
  );
4768
- const setFormRules = useCallback2(
4854
+ const setFormRules = useCallback3(
4769
4855
  async (rules) => {
4770
4856
  if (!activeView) return;
4771
4857
  await mutate.update(DatabaseViewSchema, activeView.id, { formRules: rules });
4772
4858
  },
4773
4859
  [mutate, activeView]
4774
4860
  );
4775
- const setFormAccepting = useCallback2(
4861
+ const setFormAccepting = useCallback3(
4776
4862
  async (accepting) => {
4777
4863
  if (!activeView) return;
4778
4864
  await mutate.update(DatabaseViewSchema, activeView.id, { formAccepting: accepting });
4779
4865
  },
4780
4866
  [mutate, activeView]
4781
4867
  );
4782
- const addView = useCallback2(
4868
+ const addView = useCallback3(
4783
4869
  async (name, type) => {
4784
4870
  const node = await mutate.create(DatabaseViewSchema, {
4785
4871
  database: databaseId,
@@ -4791,13 +4877,13 @@ function useGridDatabase(databaseId, options = {}) {
4791
4877
  },
4792
4878
  [mutate, databaseId, nextAppendKey]
4793
4879
  );
4794
- const renameView = useCallback2(
4880
+ const renameView = useCallback3(
4795
4881
  async (id, name) => {
4796
4882
  await mutate.update(DatabaseViewSchema, id, { name });
4797
4883
  },
4798
4884
  [mutate]
4799
4885
  );
4800
- const removeView = useCallback2(
4886
+ const removeView = useCallback3(
4801
4887
  async (id) => {
4802
4888
  await mutate.remove(id);
4803
4889
  },
@@ -4844,7 +4930,7 @@ function useGridDatabase(databaseId, options = {}) {
4844
4930
  }
4845
4931
 
4846
4932
  // src/hooks/useGlobalUndo.ts
4847
- import { useCallback as useCallback3, useContext, useEffect as useEffect6, useReducer } from "react";
4933
+ import { useCallback as useCallback4, useContext, useEffect as useEffect7, useReducer } from "react";
4848
4934
  function readUndoContext(ctx) {
4849
4935
  if (!ctx) return { undoManager: null, nodeStore: null };
4850
4936
  return { undoManager: ctx.undoManager, nodeStore: ctx.nodeStore };
@@ -4853,17 +4939,17 @@ function useGlobalUndo() {
4853
4939
  const ctx = useContext(XNetContext);
4854
4940
  const { undoManager, nodeStore } = readUndoContext(ctx);
4855
4941
  const [, bump] = useReducer((n) => n + 1, 0);
4856
- useEffect6(() => {
4942
+ useEffect7(() => {
4857
4943
  if (!nodeStore) return;
4858
4944
  return nodeStore.subscribe(() => bump());
4859
4945
  }, [nodeStore]);
4860
- const undo = useCallback3(async () => {
4946
+ const undo = useCallback4(async () => {
4861
4947
  if (!undoManager) return false;
4862
4948
  const ok = await undoManager.undoLatest();
4863
4949
  bump();
4864
4950
  return ok;
4865
4951
  }, [undoManager]);
4866
- const redo = useCallback3(async () => {
4952
+ const redo = useCallback4(async () => {
4867
4953
  if (!undoManager) return false;
4868
4954
  const ok = await undoManager.redoLatest();
4869
4955
  bump();
@@ -4877,6 +4963,57 @@ function useGlobalUndo() {
4877
4963
  };
4878
4964
  }
4879
4965
 
4966
+ // src/hooks/useCanCreate.ts
4967
+ import { useEffect as useEffect8, useMemo as useMemo6, useRef as useRef5, useState as useState7 } from "react";
4968
+ var INITIAL_STATE = {
4969
+ canCreate: false,
4970
+ loading: true,
4971
+ error: null
4972
+ };
4973
+ function useCanCreate(schemaId, properties) {
4974
+ const { store, isReady } = useNodeStore();
4975
+ const [state, setState] = useState7(INITIAL_STATE);
4976
+ const requestRef = useRef5(0);
4977
+ const propertiesKey = useMemo6(() => JSON.stringify(properties ?? {}), [properties]);
4978
+ useEffect8(() => {
4979
+ if (!store || !isReady) {
4980
+ setState((prev) => ({ ...prev, loading: true }));
4981
+ return;
4982
+ }
4983
+ if (!store.auth) {
4984
+ setState({
4985
+ ...INITIAL_STATE,
4986
+ loading: false,
4987
+ error: new Error("Authorization API is not configured on this NodeStore")
4988
+ });
4989
+ return;
4990
+ }
4991
+ const run = async () => {
4992
+ const requestId = ++requestRef.current;
4993
+ setState((prev) => ({ ...prev, loading: true, error: null }));
4994
+ try {
4995
+ const decision = await store.auth.can({
4996
+ action: "create",
4997
+ // A synthetic id: the draft node must not collide with a stored one.
4998
+ nodeId: `draft:${schemaId}`,
4999
+ node: {
5000
+ schemaId,
5001
+ properties: JSON.parse(propertiesKey)
5002
+ }
5003
+ });
5004
+ if (requestRef.current !== requestId) return;
5005
+ setState({ canCreate: decision.allowed, loading: false, error: null });
5006
+ } catch (error) {
5007
+ if (requestRef.current !== requestId) return;
5008
+ const normalized = error instanceof Error ? error : new Error(String(error));
5009
+ setState((prev) => ({ ...prev, loading: false, error: normalized }));
5010
+ }
5011
+ };
5012
+ void run();
5013
+ }, [isReady, propertiesKey, schemaId, store]);
5014
+ return state;
5015
+ }
5016
+
4880
5017
  // src/index.ts
4881
5018
  import { WebSocketSyncProvider } from "@xnetjs/runtime";
4882
5019
  import {
@@ -4907,7 +5044,7 @@ import {
4907
5044
  } from "@xnetjs/runtime";
4908
5045
 
4909
5046
  // src/hooks/useBilling.ts
4910
- import { useCallback as useCallback4, useContext as useContext2, useEffect as useEffect7, useMemo as useMemo6, useState as useState6 } from "react";
5047
+ import { useCallback as useCallback5, useContext as useContext2, useEffect as useEffect9, useMemo as useMemo7, useState as useState8 } from "react";
4911
5048
  var toHttpUrl = (hubUrl) => {
4912
5049
  try {
4913
5050
  const url = new URL(hubUrl);
@@ -4944,20 +5081,20 @@ function deriveBilling(state) {
4944
5081
  }
4945
5082
  function useBilling() {
4946
5083
  const context = useContext2(XNetContext);
4947
- const [state, setState] = useState6(null);
4948
- const [loading, setLoading] = useState6(
5084
+ const [state, setState] = useState8(null);
5085
+ const [loading, setLoading] = useState8(
4949
5086
  Boolean(context?.hubUrl || context?.billing?.apiBase)
4950
5087
  );
4951
- const [error, setError] = useState6(null);
4952
- const apiBase = useMemo6(() => {
5088
+ const [error, setError] = useState8(null);
5089
+ const apiBase = useMemo7(() => {
4953
5090
  if (context?.billing?.apiBase) return context.billing.apiBase.replace(/\/$/, "");
4954
5091
  return context?.hubUrl ? toHttpUrl(context.hubUrl) : null;
4955
5092
  }, [context?.billing?.apiBase, context?.hubUrl]);
4956
- const authHeaders = useCallback4(async () => {
5093
+ const authHeaders = useCallback5(async () => {
4957
5094
  const token = context?.getHubAuthToken ? await context.getHubAuthToken() : "";
4958
5095
  return token ? { Authorization: `Bearer ${token}` } : {};
4959
5096
  }, [context]);
4960
- const reload = useCallback4(async () => {
5097
+ const reload = useCallback5(async () => {
4961
5098
  if (!apiBase) {
4962
5099
  setState(null);
4963
5100
  setLoading(false);
@@ -4975,10 +5112,10 @@ function useBilling() {
4975
5112
  setLoading(false);
4976
5113
  }
4977
5114
  }, [apiBase, authHeaders]);
4978
- useEffect7(() => {
5115
+ useEffect9(() => {
4979
5116
  void reload();
4980
5117
  }, [reload]);
4981
- const redirectVia = useCallback4(
5118
+ const redirectVia = useCallback5(
4982
5119
  async (path, body) => {
4983
5120
  if (!apiBase) throw new Error("Hub URL not configured");
4984
5121
  const res = await fetch(`${apiBase}${path}`, {
@@ -4992,11 +5129,11 @@ function useBilling() {
4992
5129
  },
4993
5130
  [apiBase, authHeaders]
4994
5131
  );
4995
- const openCheckout = useCallback4(
5132
+ const openCheckout = useCallback5(
4996
5133
  (priceRef, options = {}) => redirectVia("/billing/checkout", buildCheckoutBody(priceRef, options)),
4997
5134
  [redirectVia]
4998
5135
  );
4999
- const openPortal = useCallback4(
5136
+ const openPortal = useCallback5(
5000
5137
  (returnUrl) => redirectVia("/billing/portal", returnUrl ? { returnUrl } : {}),
5001
5138
  [redirectVia]
5002
5139
  );
@@ -5120,6 +5257,7 @@ export {
5120
5257
  useBilling,
5121
5258
  useBlame,
5122
5259
  useCan,
5260
+ useCanCreate,
5123
5261
  useCanEdit,
5124
5262
  useCanvasTaskSync,
5125
5263
  useCell,
@@ -5164,6 +5302,7 @@ export {
5164
5302
  usePluginRegistryOptional,
5165
5303
  usePlugins,
5166
5304
  usePolicyFilteredReactionCounters,
5305
+ usePresence,
5167
5306
  useQuery,
5168
5307
  useRelatedRows,
5169
5308
  useRemoteSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xnetjs/react",
3
- "version": "0.12.0",
3
+ "version": "1.0.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -46,15 +46,15 @@
46
46
  "y-protocols": "^1.0.6",
47
47
  "yjs": "^13.6.24",
48
48
  "@xnetjs/billing": "0.0.2",
49
- "@xnetjs/core": "0.12.0",
50
- "@xnetjs/crypto": "0.12.0",
51
- "@xnetjs/data": "0.12.0",
52
- "@xnetjs/data-bridge": "0.12.0",
53
- "@xnetjs/history": "0.12.0",
54
- "@xnetjs/identity": "0.12.0",
55
- "@xnetjs/plugins": "0.12.0",
56
- "@xnetjs/runtime": "0.3.2",
57
- "@xnetjs/sync": "0.12.0"
49
+ "@xnetjs/core": "1.0.0",
50
+ "@xnetjs/crypto": "1.0.0",
51
+ "@xnetjs/data": "1.0.0",
52
+ "@xnetjs/data-bridge": "1.0.0",
53
+ "@xnetjs/history": "1.0.0",
54
+ "@xnetjs/identity": "1.0.0",
55
+ "@xnetjs/plugins": "1.0.0",
56
+ "@xnetjs/runtime": "0.4.0",
57
+ "@xnetjs/sync": "1.0.0"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "react": "^18.0.0 || ^19.0.0"