@marktiderman/genesis-core 1.5.1 → 1.7.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.
Files changed (55) hide show
  1. package/README.md +112 -0
  2. package/dist/chunk-ADD2RIGA.js +108 -0
  3. package/dist/chunk-ADD2RIGA.js.map +1 -0
  4. package/dist/chunk-KXVHDK3Q.js +71 -0
  5. package/dist/chunk-KXVHDK3Q.js.map +1 -0
  6. package/dist/{chunk-F3BE7NGM.js → chunk-OBUBP4BY.js} +83 -47
  7. package/dist/chunk-OBUBP4BY.js.map +1 -0
  8. package/dist/{chunk-6Q477CHB.js → chunk-QFR6YJ3R.js} +7 -4
  9. package/dist/chunk-QFR6YJ3R.js.map +1 -0
  10. package/dist/chunk-V3GDAELU.js +3 -0
  11. package/dist/{chunk-HT43MPKW.js.map → chunk-V3GDAELU.js.map} +1 -1
  12. package/dist/{chunk-3JSFTLDV.js → chunk-Y3SCYMNN.js} +6 -6
  13. package/dist/{chunk-3JSFTLDV.js.map → chunk-Y3SCYMNN.js.map} +1 -1
  14. package/dist/{chunk-E5VYFAQO.js → chunk-Y475Z7BM.js} +202 -11
  15. package/dist/chunk-Y475Z7BM.js.map +1 -0
  16. package/dist/{chunk-ZK5C3OOT.js → chunk-ZKMAU6NN.js} +12 -3
  17. package/dist/chunk-ZKMAU6NN.js.map +1 -0
  18. package/dist/hooks/index.d.ts +4 -2
  19. package/dist/hooks/index.js +9 -7
  20. package/dist/hooks/mutation-mode.d.ts +2 -2
  21. package/dist/hooks/mutation-mode.js +1 -1
  22. package/dist/hooks/related-keys.d.ts +2 -2
  23. package/dist/hooks/use-capabilities.d.ts +1 -1
  24. package/dist/hooks/use-infinite-resource.d.ts +50 -0
  25. package/dist/hooks/use-infinite-resource.js +6 -0
  26. package/dist/hooks/use-infinite-resource.js.map +1 -0
  27. package/dist/hooks/use-live.d.ts +27 -0
  28. package/dist/hooks/use-live.js +5 -0
  29. package/dist/hooks/use-live.js.map +1 -0
  30. package/dist/hooks/use-one.d.ts +9 -2
  31. package/dist/hooks/use-one.js +2 -1
  32. package/dist/hooks/use-resource-form.d.ts +2 -2
  33. package/dist/hooks/use-resource-form.js +6 -5
  34. package/dist/hooks/use-resource.d.ts +14 -2
  35. package/dist/hooks/use-resource.js +4 -3
  36. package/dist/index.d.ts +6 -4
  37. package/dist/index.js +10 -8
  38. package/dist/navigation.d.ts +108 -0
  39. package/dist/navigation.js +101 -0
  40. package/dist/navigation.js.map +1 -0
  41. package/dist/provider/index.d.ts +54 -6
  42. package/dist/provider/index.js +1 -1
  43. package/dist/{read-outcome-kgNhfuST.d.ts → read-outcome-C_9HbXvb.d.ts} +1 -1
  44. package/dist/schema/index.d.ts +4 -4
  45. package/dist/slots.d.ts +60 -0
  46. package/dist/slots.js +12 -0
  47. package/dist/slots.js.map +1 -0
  48. package/dist/{types-5cuflvha.d.ts → types-C7FJtFXV.d.ts} +12 -0
  49. package/dist/{types-DDOmG9hj.d.ts → types-CS9UpxQS.d.ts} +62 -4
  50. package/package.json +9 -1
  51. package/dist/chunk-6Q477CHB.js.map +0 -1
  52. package/dist/chunk-E5VYFAQO.js.map +0 -1
  53. package/dist/chunk-F3BE7NGM.js.map +0 -1
  54. package/dist/chunk-HT43MPKW.js +0 -3
  55. package/dist/chunk-ZK5C3OOT.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"use-infinite-resource.js"}
@@ -0,0 +1,27 @@
1
+ import { QueryKey } from '@tanstack/react-query';
2
+ import { o as SubscribeParams, l as ResourceChangeEvent } from '../types-CS9UpxQS.js';
3
+ import '../types-C7FJtFXV.js';
4
+ import '../form-types.js';
5
+
6
+ /** How long a burst of change events is gathered into one refetch. */
7
+ declare const DEFAULT_LIVE_DEBOUNCE_MS = 250;
8
+ /** `live` as an object, to tune it. `live: true` takes the defaults. */
9
+ interface LiveOptions {
10
+ /**
11
+ * Events arriving within this many milliseconds of each other cause one
12
+ * refetch, not one each. Default {@link DEFAULT_LIVE_DEBOUNCE_MS}. A
13
+ * steady stream still refetches at least every `4 × debounceMs`.
14
+ */
15
+ debounceMs?: number;
16
+ }
17
+ /**
18
+ * Subscribes while mounted and `enabled`, and invalidates `queryKey` (a
19
+ * prefix) once per burst of events that `accept` lets through. While the
20
+ * page is hidden the queries are only marked stale; they refetch when it is
21
+ * shown again (TanStack Query's `refetchOnWindowFocus`) or next used.
22
+ *
23
+ * @internal Used by `useResource` and `useOne`'s `live` option.
24
+ */
25
+ declare function useLiveInvalidation(resource: string, live: boolean | LiveOptions | undefined, enabled: boolean, params: SubscribeParams, queryKey: QueryKey, accept?: (event: ResourceChangeEvent) => boolean): void;
26
+
27
+ export { DEFAULT_LIVE_DEBOUNCE_MS, type LiveOptions, useLiveInvalidation };
@@ -0,0 +1,5 @@
1
+ export { DEFAULT_LIVE_DEBOUNCE_MS, useLiveInvalidation } from '../chunk-KXVHDK3Q.js';
2
+ import '../chunk-GGSXSI6Z.js';
3
+ import '../chunk-ZYJUEJ4Z.js';
4
+ //# sourceMappingURL=use-live.js.map
5
+ //# sourceMappingURL=use-live.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"use-live.js"}
@@ -1,6 +1,7 @@
1
1
  import { UseQueryResult } from '@tanstack/react-query';
2
- import { B as BaseRecord, I as Identifier } from '../types-DDOmG9hj.js';
3
- import '../types-5cuflvha.js';
2
+ import { B as BaseRecord, I as Identifier } from '../types-CS9UpxQS.js';
3
+ import { LiveOptions } from './use-live.js';
4
+ import '../types-C7FJtFXV.js';
4
5
  import '../form-types.js';
5
6
 
6
7
  interface UseOneOptions {
@@ -8,6 +9,12 @@ interface UseOneOptions {
8
9
  select?: string;
9
10
  /** Defaults to `!!id` — won't fetch when id is falsy. */
10
11
  enabled?: boolean;
12
+ /**
13
+ * Keep the record current while mounted: refetch it when the provider
14
+ * reports a change to it (`DataProvider.subscribe`). See
15
+ * `UseResourceOptions.live`. Off by default.
16
+ */
17
+ live?: boolean | LiveOptions;
11
18
  }
12
19
  /**
13
20
  * Fetch a single record by id, powered by TanStack Query.
@@ -1,4 +1,5 @@
1
- export { useOne } from '../chunk-ZK5C3OOT.js';
1
+ export { useOne } from '../chunk-ZKMAU6NN.js';
2
+ import '../chunk-KXVHDK3Q.js';
2
3
  import '../chunk-GGSXSI6Z.js';
3
4
  import '../chunk-ZYJUEJ4Z.js';
4
5
  //# sourceMappingURL=use-one.js.map
@@ -1,8 +1,8 @@
1
1
  import { UseFormReturn } from 'react-hook-form';
2
2
  import { ResourceFormFieldDef } from '../form-types.js';
3
- import { j as InvalidationKey } from '../types-DDOmG9hj.js';
3
+ import { j as InvalidationKey } from '../types-CS9UpxQS.js';
4
4
  import { MutationModeOptions } from './mutation-mode.js';
5
- import '../types-5cuflvha.js';
5
+ import '../types-C7FJtFXV.js';
6
6
  import '@tanstack/react-query';
7
7
 
8
8
  interface UseResourceFormOptions extends MutationModeOptions {
@@ -1,12 +1,13 @@
1
- export { buildArrayItemDefaults, useResourceForm } from '../chunk-3JSFTLDV.js';
2
- import '../chunk-XQO4OH2N.js';
1
+ export { buildArrayItemDefaults, useResourceForm } from '../chunk-Y3SCYMNN.js';
3
2
  import '../chunk-5NHR6ZXS.js';
3
+ import '../chunk-ZKMAU6NN.js';
4
+ import '../chunk-KXVHDK3Q.js';
5
+ import '../chunk-XQO4OH2N.js';
4
6
  import '../chunk-RYEXWQFD.js';
5
- import '../chunk-F3BE7NGM.js';
6
- import '../chunk-7NUL32W7.js';
7
+ import '../chunk-OBUBP4BY.js';
7
8
  import '../chunk-BQOJX67X.js';
8
- import '../chunk-ZK5C3OOT.js';
9
9
  import '../chunk-GGSXSI6Z.js';
10
10
  import '../chunk-ZYJUEJ4Z.js';
11
+ import '../chunk-7NUL32W7.js';
11
12
  //# sourceMappingURL=use-resource-form.js.map
12
13
  //# sourceMappingURL=use-resource-form.js.map
@@ -1,7 +1,8 @@
1
1
  import { UseQueryResult, UseMutationResult } from '@tanstack/react-query';
2
- import { m as SortParam, F as FilterParam, j as InvalidationKey, k as ListResult, L as ListParams, I as Identifier, B as BaseRecord } from '../types-DDOmG9hj.js';
2
+ import { n as SortParam, F as FilterParam, j as InvalidationKey, k as ListResult, L as ListParams, I as Identifier, B as BaseRecord } from '../types-CS9UpxQS.js';
3
3
  import { MutationModeOptions } from './mutation-mode.js';
4
- import '../types-5cuflvha.js';
4
+ import { LiveOptions } from './use-live.js';
5
+ import '../types-C7FJtFXV.js';
5
6
  import '../form-types.js';
6
7
 
7
8
  interface UseResourceOptions<T> extends MutationModeOptions {
@@ -40,6 +41,17 @@ interface UseResourceOptions<T> extends MutationModeOptions {
40
41
  * rather than flashing a skeleton. Default false.
41
42
  */
42
43
  keepPreviousData?: boolean;
44
+ /**
45
+ * Keep the list current while mounted: subscribe to the provider's change
46
+ * events (`DataProvider.subscribe`) and refetch this resource's cached
47
+ * reads (lists and records) when it reports one, once per burst of
48
+ * events. Events are hints: rows always come from the provider's normal
49
+ * read. Pending optimistic and undoable writes stay applied over the
50
+ * refetch. Needs a provider that serves `subscribe` for the resource
51
+ * (`capabilities(resource).realtime`); otherwise it does nothing. Off by
52
+ * default.
53
+ */
54
+ live?: boolean | LiveOptions;
43
55
  /**
44
56
  * After the write commits, with the provider's row. In `optimistic` and
45
57
  * `undoable` modes that is after `create` / `update` / `remove` have
@@ -1,8 +1,9 @@
1
- export { useResource } from '../chunk-6Q477CHB.js';
2
- import '../chunk-F3BE7NGM.js';
3
- import '../chunk-7NUL32W7.js';
1
+ export { useResource } from '../chunk-QFR6YJ3R.js';
2
+ import '../chunk-KXVHDK3Q.js';
3
+ import '../chunk-OBUBP4BY.js';
4
4
  import '../chunk-BQOJX67X.js';
5
5
  import '../chunk-GGSXSI6Z.js';
6
6
  import '../chunk-ZYJUEJ4Z.js';
7
+ import '../chunk-7NUL32W7.js';
7
8
  //# sourceMappingURL=use-resource.js.map
8
9
  //# sourceMappingURL=use-resource.js.map
package/dist/index.d.ts CHANGED
@@ -1,17 +1,19 @@
1
- export { A as ArrayFilter, B as BaseRecord, a as BulkChange, b as BulkResult, C as CreateParams, c as CreateResult, D as DataProvider, d as DeleteParams, e as DeleteResult, F as FilterParam, G as GetManyParams, f as GetManyResult, g as GetOneParams, h as GetOneResult, H as HistoryParams, I as Identifier, i as ImportParams, j as InvalidationKey, L as ListParams, k as ListResult, N as NullFilter, P as PaginationParam, R as RangeFilter, l as RowFailure, S as ScalarFilter, m as SortParam, U as UpdateParams, n as UpdateResult, W as WriteEvent } from './types-DDOmG9hj.js';
2
- export { D as DataReadError, a as DataReadErrorOptions, b as DataWriteError, c as DataWriteErrorOptions, R as ReadOutcome, W as WriteOutcome, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from './read-outcome-kgNhfuST.js';
3
- export { DataProviderRoot, DataProviderRootProps, GenesisProvider, GenesisProviderProps, MockProviderOptions, PostgrestFieldErrors, SupabaseClient, SupabaseClientInput, SupabaseProviderOptions, SupabaseResourceConfig, SupabaseWriteContext, createMockProvider, createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, useDataProvider, useStorage, writeOutcomeFromCode } from './provider/index.js';
1
+ export { A as ArrayFilter, B as BaseRecord, a as BulkChange, b as BulkResult, C as CreateParams, c as CreateResult, D as DataProvider, d as DeleteParams, e as DeleteResult, F as FilterParam, G as GetManyParams, f as GetManyResult, g as GetOneParams, h as GetOneResult, H as HistoryParams, I as Identifier, i as ImportParams, j as InvalidationKey, L as ListParams, k as ListResult, N as NullFilter, P as PaginationParam, R as RangeFilter, l as ResourceChangeEvent, m as RowFailure, S as ScalarFilter, n as SortParam, o as SubscribeParams, U as UpdateParams, p as UpdateResult, W as WriteEvent } from './types-CS9UpxQS.js';
2
+ export { D as DataReadError, a as DataReadErrorOptions, b as DataWriteError, c as DataWriteErrorOptions, R as ReadOutcome, W as WriteOutcome, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from './read-outcome-C_9HbXvb.js';
3
+ export { DataProviderRoot, DataProviderRootProps, GenesisProvider, GenesisProviderProps, MockProviderOptions, PostgrestFieldErrors, SupabaseClient, SupabaseClientInput, SupabaseProviderOptions, SupabaseRealtimeChannel, SupabaseRealtimePayload, SupabaseResourceConfig, SupabaseWriteContext, createMockProvider, createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, useDataProvider, useStorage, writeOutcomeFromCode } from './provider/index.js';
4
4
  export { S as StorageAdapter } from './types-DKcxHXxq.js';
5
5
  export { createLocalStorageAdapter, createMemoryStorageAdapter, createNoopStorageAdapter } from './storage/index.js';
6
6
  export { UseResourceOptions, UseResourceReturn, useResource } from './hooks/use-resource.js';
7
7
  export { UseOneOptions, useOne } from './hooks/use-one.js';
8
+ export { InfinitePageParam, UseInfiniteResourceOptions, UseInfiniteResourceReturn, useInfiniteResource } from './hooks/use-infinite-resource.js';
8
9
  export { useCapabilities } from './hooks/use-capabilities.js';
10
+ export { DEFAULT_LIVE_DEBOUNCE_MS, LiveOptions } from './hooks/use-live.js';
9
11
  export { UseResourceFormOptions, UseResourceFormReturn, buildArrayItemDefaults, useResourceForm } from './hooks/use-resource-form.js';
10
12
  export { UseWizardReturn, WizardStep, useWizard } from './hooks/use-wizard.js';
11
13
  export { DEFAULT_UNDO_DELAY_MS, MutationMode, MutationModeOptions, UndoableWrite, pendingWriteCount } from './hooks/mutation-mode.js';
12
14
  export { DEFAULT_UNSAVED_CHANGES_MESSAGE, UseUnsavedChangesGuardOptions, UseUnsavedChangesGuardReturn, useUnsavedChangesGuard } from './hooks/use-unsaved-changes-guard.js';
13
15
  export { FieldType, ResourceFormFieldDef, SelectOption, FieldType as WidgetHint } from './form-types.js';
14
- export { A as AuditEntry, D as DataRow, F as FieldPermissions, a as FieldReference, b as FieldSchema, c as FilterOperator, P as ProviderCapabilities, R as RecordComment, d as ResourceAccess, e as ResourceContract, f as ResourceRelation, g as ResourceSchema, h as ResourceSummary, i as RowAction, S as StorageType, V as VersionField } from './types-5cuflvha.js';
16
+ export { A as AuditEntry, D as DataRow, F as FieldPermissions, a as FieldReference, b as FieldSchema, c as FilterOperator, P as ProviderCapabilities, R as RecordComment, d as ResourceAccess, e as ResourceContract, f as ResourceRelation, g as ResourceSchema, h as ResourceSummary, i as RowAction, S as StorageType, V as VersionField } from './types-C7FJtFXV.js';
15
17
  export { decodeRecordKey, encodeRecordKey, keyFields } from './schema/index.js';
16
18
  export { titleCase } from './utils.js';
17
19
  import 'react';
package/dist/index.js CHANGED
@@ -1,20 +1,22 @@
1
- export { createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, writeOutcomeFromCode } from './chunk-E5VYFAQO.js';
1
+ export { createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, writeOutcomeFromCode } from './chunk-Y475Z7BM.js';
2
2
  export { decodeRecordKey, encodeRecordKey, keyFields } from './chunk-KV2HW4G4.js';
3
3
  export { DataReadError, isDataReadError, readFailureMessage } from './chunk-HUNGCTRA.js';
4
4
  import './chunk-XCX2TRBL.js';
5
5
  import './chunk-65SXNHWE.js';
6
- import './chunk-HT43MPKW.js';
7
- export { buildArrayItemDefaults, useResourceForm } from './chunk-3JSFTLDV.js';
8
- export { useResource } from './chunk-6Q477CHB.js';
6
+ import './chunk-V3GDAELU.js';
7
+ export { useInfiniteResource } from './chunk-ADD2RIGA.js';
8
+ export { buildArrayItemDefaults, useResourceForm } from './chunk-Y3SCYMNN.js';
9
+ export { DataWriteError, isDataWriteError } from './chunk-5NHR6ZXS.js';
10
+ export { useOne } from './chunk-ZKMAU6NN.js';
11
+ export { useResource } from './chunk-QFR6YJ3R.js';
12
+ export { DEFAULT_LIVE_DEBOUNCE_MS } from './chunk-KXVHDK3Q.js';
9
13
  export { DEFAULT_UNSAVED_CHANGES_MESSAGE, useUnsavedChangesGuard } from './chunk-XQO4OH2N.js';
10
14
  export { useWizard } from './chunk-7KOLYF27.js';
11
- export { DataWriteError, isDataWriteError } from './chunk-5NHR6ZXS.js';
12
15
  export { titleCase } from './chunk-RYEXWQFD.js';
13
- export { DEFAULT_UNDO_DELAY_MS, pendingWriteCount } from './chunk-F3BE7NGM.js';
14
- import './chunk-7NUL32W7.js';
16
+ export { DEFAULT_UNDO_DELAY_MS, pendingWriteCount } from './chunk-OBUBP4BY.js';
15
17
  export { useCapabilities } from './chunk-BQOJX67X.js';
16
- export { useOne } from './chunk-ZK5C3OOT.js';
17
18
  export { DataProviderRoot, GenesisProvider, createMockProvider, useDataProvider, useStorage } from './chunk-GGSXSI6Z.js';
18
19
  export { createLocalStorageAdapter, createMemoryStorageAdapter, createNoopStorageAdapter } from './chunk-ZYJUEJ4Z.js';
20
+ import './chunk-7NUL32W7.js';
19
21
  //# sourceMappingURL=index.js.map
20
22
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Router-free, DOM-free navigation: the logical navigation tree and the
3
+ * active-path matcher, shared by the web app shell and native shells.
4
+ * `resolveNavigation` never reads browser history or `window`.
5
+ */
6
+ /**
7
+ * Options for {@link isPathActive}.
8
+ *
9
+ * @stability Beta
10
+ */
11
+ interface IsPathActiveOptions {
12
+ /** Require an exact path match instead of a prefix match (like `NavLink end`). */
13
+ end?: boolean;
14
+ }
15
+ /**
16
+ * Default active-path matcher, mirroring React Router's `NavLink` (prefix
17
+ * match, with `/` treated as exact). Consumers can override via an `isActive`
18
+ * prop where components accept one.
19
+ *
20
+ * `activePath` may carry a query and hash (`pathname + search + hash`). An
21
+ * item whose `to` has neither compares against the path part only, exactly as
22
+ * before. An item that opts in by putting a query or hash in its `to`
23
+ * (`/admin?tab=users`, `/settings#profile`) is active only when the path
24
+ * matches exactly AND every query param it names has the same value in
25
+ * `activePath` (extra params such as `&page=2` are ignored) AND, if it names a
26
+ * hash, the hash is equal.
27
+ *
28
+ * @stability Beta
29
+ */
30
+ declare function isPathActive(to: string, activePath?: string, options?: IsPathActiveOptions): boolean;
31
+ /**
32
+ * One node of a logical navigation hierarchy. `path` is a route pattern whose
33
+ * segments are either static (`clients`) or named params (`$clientId`). A
34
+ * node's place in the tree is its LOGICAL parent, which need not share its URL
35
+ * prefix — `/clients/$clientId` can sit under `/dashboard`.
36
+ *
37
+ * @stability Beta
38
+ */
39
+ interface NavigationNode<T> {
40
+ id: string;
41
+ path: string;
42
+ label: string;
43
+ /** Contextual nav items to show while this node is current. */
44
+ items: T[];
45
+ children?: NavigationNode<T>[];
46
+ /**
47
+ * Ask the app shell to open its desktop sidebar collapsed (icon rail) while
48
+ * this node, or a descendant that doesn't set its own value, is current.
49
+ * The nearest node in the resolved trail that sets it wins, so
50
+ * `defaultCollapsed: false` on a child re-opens it under a collapsed
51
+ * parent. Only a default: once the user has toggled the sidebar, their
52
+ * choice wins everywhere. `AppShell` reads it; `resolveNavigation` copies
53
+ * it through untouched.
54
+ *
55
+ * @stability Beta
56
+ */
57
+ defaultCollapsed?: boolean;
58
+ /**
59
+ * How the app shell shows this node's `items` (its child pages, e.g. a
60
+ * form's Questions / Logic / Responses / Settings). `"tabs"` (across the
61
+ * top of the content) or `"sidebar"` (nested under the active item) turn
62
+ * shell sub-navigation on, which also gives phones a back item plus the
63
+ * children in the bottom bar. `"section"` is the legacy rendering (the
64
+ * shell's default) and `"off"` hides them. The nearest node in the trail
65
+ * that sets it wins over the shell's `options.subNav`.
66
+ *
67
+ * @stability Beta
68
+ */
69
+ subNav?: "section" | "tabs" | "sidebar" | "off";
70
+ }
71
+ /**
72
+ * Result of {@link resolveNavigation}. `current`, `parent` and every `trail`
73
+ * entry are shallow copies of their tree nodes with `path` interpolated from
74
+ * `params` (a param the URL did not supply stays as `$name`).
75
+ *
76
+ * @stability Beta
77
+ */
78
+ interface ResolvedNavigation<T> {
79
+ /** The best-matching node — the root when nothing else matches. */
80
+ current: NavigationNode<T>;
81
+ /** `current`'s logical parent, for an "up" / back-to-parent link. */
82
+ parent?: NavigationNode<T>;
83
+ /** `current.items`. */
84
+ items: T[];
85
+ /** Root → `current`, inclusive. */
86
+ trail: NavigationNode<T>[];
87
+ /** Params captured by `current.path`, URL-decoded. */
88
+ params: Record<string, string>;
89
+ }
90
+ /**
91
+ * Resolve where `pathname` sits in a logical navigation tree. Pure: it never
92
+ * reads browser history or `window`.
93
+ *
94
+ * A node matches when its pattern matches a leading run of WHOLE path
95
+ * segments (`/my` matches `/my/tasks`, never `/my-other`). Among matches the
96
+ * winner is decided by positional specificity: at the first position where
97
+ * one pattern is static and the other a `$param`, the static one wins (so
98
+ * `/clients/new` beats `/clients/$clientId` whatever the sibling order, and
99
+ * `/my` beats `/$space/tasks` for `/my/tasks`). Otherwise the pattern matching
100
+ * more segments wins, then the deeper tree node.
101
+ * Non-matching nodes are still searched, so a match keeps its logical
102
+ * ancestors even when their URLs differ.
103
+ *
104
+ * @stability Beta
105
+ */
106
+ declare function resolveNavigation<T>(tree: NavigationNode<T>, pathname: string): ResolvedNavigation<T>;
107
+
108
+ export { type IsPathActiveOptions, type NavigationNode, type ResolvedNavigation, isPathActive, resolveNavigation };
@@ -0,0 +1,101 @@
1
+ // src/navigation.ts
2
+ function splitHref(href) {
3
+ const hashAt = href.indexOf("#");
4
+ const hash = hashAt === -1 ? "" : href.slice(hashAt + 1);
5
+ const beforeHash = hashAt === -1 ? href : href.slice(0, hashAt);
6
+ const searchAt = beforeHash.indexOf("?");
7
+ const rawPath = searchAt === -1 ? beforeHash : beforeHash.slice(0, searchAt);
8
+ return {
9
+ path: rawPath.length > 1 ? rawPath.replace(/\/+$/, "") || "/" : rawPath,
10
+ search: searchAt === -1 ? "" : beforeHash.slice(searchAt + 1),
11
+ hash
12
+ };
13
+ }
14
+ function isPathActive(to, activePath, options) {
15
+ if (activePath == null) return false;
16
+ if (to === activePath) return true;
17
+ const target = splitHref(to);
18
+ const current = splitHref(activePath);
19
+ if (!target.search && !target.hash) {
20
+ if (target.path === current.path) return true;
21
+ if (options?.end) return false;
22
+ return target.path !== "/" && current.path.startsWith(target.path + "/");
23
+ }
24
+ if (target.path !== current.path) return false;
25
+ if (target.hash && target.hash !== current.hash) return false;
26
+ if (target.search) {
27
+ const want = new URLSearchParams(target.search);
28
+ const have = new URLSearchParams(current.search);
29
+ for (const key of new Set(want.keys())) {
30
+ const wanted = want.getAll(key);
31
+ const had = have.getAll(key);
32
+ if (!wanted.every((value) => had.includes(value))) return false;
33
+ }
34
+ }
35
+ return true;
36
+ }
37
+ function pathSegments(path) {
38
+ return splitHref(path).path.split("/").filter(Boolean);
39
+ }
40
+ function decodeSegment(segment) {
41
+ try {
42
+ return decodeURIComponent(segment);
43
+ } catch {
44
+ return segment;
45
+ }
46
+ }
47
+ function resolveNavigation(tree, pathname) {
48
+ const segments = pathSegments(pathname).map(decodeSegment);
49
+ const matches = (pattern) => pattern.length <= segments.length && pattern.every(
50
+ (part, i) => part.startsWith("$") || decodeSegment(part) === segments[i]
51
+ );
52
+ const compare = (a, b) => {
53
+ const shared = Math.min(a.pattern.length, b.pattern.length);
54
+ for (let i = 0; i < shared; i++) {
55
+ const aParam = a.pattern[i].startsWith("$");
56
+ const bParam = b.pattern[i].startsWith("$");
57
+ if (aParam !== bParam) return aParam ? -1 : 1;
58
+ }
59
+ if (a.pattern.length !== b.pattern.length) {
60
+ return a.pattern.length - b.pattern.length;
61
+ }
62
+ return a.trail.length - b.trail.length;
63
+ };
64
+ let best;
65
+ const visit = (node, ancestors) => {
66
+ const trail2 = [...ancestors, node];
67
+ const pattern = pathSegments(node.path);
68
+ if (matches(pattern)) {
69
+ const candidate = { trail: trail2, pattern };
70
+ if (!best || compare(candidate, best) > 0) best = candidate;
71
+ }
72
+ for (const child of node.children ?? []) visit(child, trail2);
73
+ };
74
+ visit(tree, []);
75
+ const winner = best ?? { trail: [tree], pattern: [] };
76
+ const params = {};
77
+ winner.pattern.forEach((part, i) => {
78
+ if (part.startsWith("$")) params[part.slice(1)] = segments[i];
79
+ });
80
+ const interpolate = (node) => ({
81
+ ...node,
82
+ path: node.path.split("/").map((part) => {
83
+ if (!part.startsWith("$")) return part;
84
+ const value = params[part.slice(1)];
85
+ return value == null ? part : encodeURIComponent(value);
86
+ }).join("/")
87
+ });
88
+ const trail = winner.trail.map(interpolate);
89
+ const current = trail[trail.length - 1];
90
+ return {
91
+ current,
92
+ parent: trail.length > 1 ? trail[trail.length - 2] : void 0,
93
+ items: current.items,
94
+ trail,
95
+ params
96
+ };
97
+ }
98
+
99
+ export { isPathActive, resolveNavigation };
100
+ //# sourceMappingURL=navigation.js.map
101
+ //# sourceMappingURL=navigation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/navigation.ts"],"names":["trail"],"mappings":"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","file":"navigation.js","sourcesContent":["/**\n * Router-free, DOM-free navigation: the logical navigation tree and the\n * active-path matcher, shared by the web app shell and native shells.\n * `resolveNavigation` never reads browser history or `window`.\n */\n\n/**\n * Split a href into its path (trailing slash dropped, except for `/`), query\n * (no `?`) and hash (no `#`).\n */\nfunction splitHref(href: string): { path: string; search: string; hash: string } {\n const hashAt = href.indexOf(\"#\");\n const hash = hashAt === -1 ? \"\" : href.slice(hashAt + 1);\n const beforeHash = hashAt === -1 ? href : href.slice(0, hashAt);\n const searchAt = beforeHash.indexOf(\"?\");\n const rawPath = searchAt === -1 ? beforeHash : beforeHash.slice(0, searchAt);\n return {\n path: rawPath.length > 1 ? rawPath.replace(/\\/+$/, \"\") || \"/\" : rawPath,\n search: searchAt === -1 ? \"\" : beforeHash.slice(searchAt + 1),\n hash,\n };\n}\n\n/**\n * Options for {@link isPathActive}.\n *\n * @stability Beta\n */\nexport interface IsPathActiveOptions {\n /** Require an exact path match instead of a prefix match (like `NavLink end`). */\n end?: boolean;\n}\n\n/**\n * Default active-path matcher, mirroring React Router's `NavLink` (prefix\n * match, with `/` treated as exact). Consumers can override via an `isActive`\n * prop where components accept one.\n *\n * `activePath` may carry a query and hash (`pathname + search + hash`). An\n * item whose `to` has neither compares against the path part only, exactly as\n * before. An item that opts in by putting a query or hash in its `to`\n * (`/admin?tab=users`, `/settings#profile`) is active only when the path\n * matches exactly AND every query param it names has the same value in\n * `activePath` (extra params such as `&page=2` are ignored) AND, if it names a\n * hash, the hash is equal.\n *\n * @stability Beta\n */\nexport function isPathActive(\n to: string,\n activePath?: string,\n options?: IsPathActiveOptions,\n): boolean {\n if (activePath == null) return false;\n if (to === activePath) return true;\n const target = splitHref(to);\n const current = splitHref(activePath);\n\n if (!target.search && !target.hash) {\n if (target.path === current.path) return true;\n if (options?.end) return false;\n return target.path !== \"/\" && current.path.startsWith(target.path + \"/\");\n }\n\n if (target.path !== current.path) return false;\n if (target.hash && target.hash !== current.hash) return false;\n if (target.search) {\n const want = new URLSearchParams(target.search);\n const have = new URLSearchParams(current.search);\n for (const key of new Set(want.keys())) {\n const wanted = want.getAll(key);\n const had = have.getAll(key);\n if (!wanted.every((value) => had.includes(value))) return false;\n }\n }\n return true;\n}\n\n// ---------------------------------------------------------------------------\n// Hierarchical navigation resolver\n// ---------------------------------------------------------------------------\n\n/**\n * One node of a logical navigation hierarchy. `path` is a route pattern whose\n * segments are either static (`clients`) or named params (`$clientId`). A\n * node's place in the tree is its LOGICAL parent, which need not share its URL\n * prefix — `/clients/$clientId` can sit under `/dashboard`.\n *\n * @stability Beta\n */\nexport interface NavigationNode<T> {\n id: string;\n path: string;\n label: string;\n /** Contextual nav items to show while this node is current. */\n items: T[];\n children?: NavigationNode<T>[];\n /**\n * Ask the app shell to open its desktop sidebar collapsed (icon rail) while\n * this node, or a descendant that doesn't set its own value, is current.\n * The nearest node in the resolved trail that sets it wins, so\n * `defaultCollapsed: false` on a child re-opens it under a collapsed\n * parent. Only a default: once the user has toggled the sidebar, their\n * choice wins everywhere. `AppShell` reads it; `resolveNavigation` copies\n * it through untouched.\n *\n * @stability Beta\n */\n defaultCollapsed?: boolean;\n /**\n * How the app shell shows this node's `items` (its child pages, e.g. a\n * form's Questions / Logic / Responses / Settings). `\"tabs\"` (across the\n * top of the content) or `\"sidebar\"` (nested under the active item) turn\n * shell sub-navigation on, which also gives phones a back item plus the\n * children in the bottom bar. `\"section\"` is the legacy rendering (the\n * shell's default) and `\"off\"` hides them. The nearest node in the trail\n * that sets it wins over the shell's `options.subNav`.\n *\n * @stability Beta\n */\n subNav?: \"section\" | \"tabs\" | \"sidebar\" | \"off\";\n}\n\n/**\n * Result of {@link resolveNavigation}. `current`, `parent` and every `trail`\n * entry are shallow copies of their tree nodes with `path` interpolated from\n * `params` (a param the URL did not supply stays as `$name`).\n *\n * @stability Beta\n */\nexport interface ResolvedNavigation<T> {\n /** The best-matching node — the root when nothing else matches. */\n current: NavigationNode<T>;\n /** `current`'s logical parent, for an \"up\" / back-to-parent link. */\n parent?: NavigationNode<T>;\n /** `current.items`. */\n items: T[];\n /** Root → `current`, inclusive. */\n trail: NavigationNode<T>[];\n /** Params captured by `current.path`, URL-decoded. */\n params: Record<string, string>;\n}\n\nfunction pathSegments(path: string): string[] {\n return splitHref(path).path.split(\"/\").filter(Boolean);\n}\n\nfunction decodeSegment(segment: string): string {\n try {\n return decodeURIComponent(segment);\n } catch {\n return segment;\n }\n}\n\n/**\n * Resolve where `pathname` sits in a logical navigation tree. Pure: it never\n * reads browser history or `window`.\n *\n * A node matches when its pattern matches a leading run of WHOLE path\n * segments (`/my` matches `/my/tasks`, never `/my-other`). Among matches the\n * winner is decided by positional specificity: at the first position where\n * one pattern is static and the other a `$param`, the static one wins (so\n * `/clients/new` beats `/clients/$clientId` whatever the sibling order, and\n * `/my` beats `/$space/tasks` for `/my/tasks`). Otherwise the pattern matching\n * more segments wins, then the deeper tree node.\n * Non-matching nodes are still searched, so a match keeps its logical\n * ancestors even when their URLs differ.\n *\n * @stability Beta\n */\nexport function resolveNavigation<T>(\n tree: NavigationNode<T>,\n pathname: string,\n): ResolvedNavigation<T> {\n // Compare decoded forms on both sides, so `/caf%C3%A9` matches a `/café`\n // node (and an encoded pattern matches a decoded pathname).\n const segments = pathSegments(pathname).map(decodeSegment);\n\n type Match = { trail: NavigationNode<T>[]; pattern: string[] };\n const matches = (pattern: string[]) =>\n pattern.length <= segments.length &&\n pattern.every(\n (part, i) => part.startsWith(\"$\") || decodeSegment(part) === segments[i],\n );\n\n // > 0 when `a` should win over `b`.\n // Positional specificity, as TanStack Router and React Router rank routes:\n // walking left to right, the first position where one pattern is static and\n // the other a `$param` decides. Only then does the longer match win.\n const compare = (a: Match, b: Match): number => {\n const shared = Math.min(a.pattern.length, b.pattern.length);\n for (let i = 0; i < shared; i++) {\n const aParam = a.pattern[i]!.startsWith(\"$\");\n const bParam = b.pattern[i]!.startsWith(\"$\");\n if (aParam !== bParam) return aParam ? -1 : 1;\n }\n if (a.pattern.length !== b.pattern.length) {\n return a.pattern.length - b.pattern.length;\n }\n return a.trail.length - b.trail.length;\n };\n\n let best: Match | undefined;\n const visit = (node: NavigationNode<T>, ancestors: NavigationNode<T>[]) => {\n const trail = [...ancestors, node];\n const pattern = pathSegments(node.path);\n if (matches(pattern)) {\n const candidate = { trail, pattern };\n if (!best || compare(candidate, best) > 0) best = candidate;\n }\n for (const child of node.children ?? []) visit(child, trail);\n };\n visit(tree, []);\n\n const winner: Match = best ?? { trail: [tree], pattern: [] };\n const params: Record<string, string> = {};\n winner.pattern.forEach((part, i) => {\n if (part.startsWith(\"$\")) params[part.slice(1)] = segments[i]!;\n });\n\n const interpolate = (node: NavigationNode<T>): NavigationNode<T> => ({\n ...node,\n path: node.path\n .split(\"/\")\n .map((part) => {\n if (!part.startsWith(\"$\")) return part;\n const value = params[part.slice(1)];\n return value == null ? part : encodeURIComponent(value);\n })\n .join(\"/\"),\n });\n\n const trail = winner.trail.map(interpolate);\n const current = trail[trail.length - 1]!;\n return {\n current,\n parent: trail.length > 1 ? trail[trail.length - 2] : undefined,\n items: current.items,\n trail,\n params,\n };\n}\n"]}
@@ -1,12 +1,12 @@
1
- import { D as DataProvider, I as Identifier, B as BaseRecord, j as InvalidationKey, W as WriteEvent } from '../types-DDOmG9hj.js';
2
- export { A as ArrayFilter, a as BulkChange, b as BulkResult, C as CreateParams, c as CreateResult, d as DeleteParams, e as DeleteResult, F as FilterParam, G as GetManyParams, f as GetManyResult, g as GetOneParams, h as GetOneResult, H as HistoryParams, i as ImportParams, L as ListParams, k as ListResult, N as NullFilter, P as PaginationParam, R as RangeFilter, l as RowFailure, S as ScalarFilter, m as SortParam, U as UpdateParams, n as UpdateResult } from '../types-DDOmG9hj.js';
3
- import { D as DataReadError, R as ReadOutcome, b as DataWriteError, W as WriteOutcome } from '../read-outcome-kgNhfuST.js';
4
- export { a as DataReadErrorOptions, c as DataWriteErrorOptions, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from '../read-outcome-kgNhfuST.js';
1
+ import { D as DataProvider, I as Identifier, B as BaseRecord, j as InvalidationKey, W as WriteEvent } from '../types-CS9UpxQS.js';
2
+ export { A as ArrayFilter, a as BulkChange, b as BulkResult, C as CreateParams, c as CreateResult, d as DeleteParams, e as DeleteResult, F as FilterParam, G as GetManyParams, f as GetManyResult, g as GetOneParams, h as GetOneResult, H as HistoryParams, i as ImportParams, L as ListParams, k as ListResult, N as NullFilter, P as PaginationParam, R as RangeFilter, l as ResourceChangeEvent, m as RowFailure, S as ScalarFilter, n as SortParam, o as SubscribeParams, U as UpdateParams, p as UpdateResult } from '../types-CS9UpxQS.js';
3
+ import { D as DataReadError, R as ReadOutcome, b as DataWriteError, W as WriteOutcome } from '../read-outcome-C_9HbXvb.js';
4
+ export { a as DataReadErrorOptions, c as DataWriteErrorOptions, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from '../read-outcome-C_9HbXvb.js';
5
5
  import * as react from 'react';
6
6
  import { ReactNode } from 'react';
7
7
  import { QueryClient } from '@tanstack/react-query';
8
8
  import { S as StorageAdapter } from '../types-DKcxHXxq.js';
9
- import '../types-5cuflvha.js';
9
+ import '../types-C7FJtFXV.js';
10
10
  import '../form-types.js';
11
11
 
12
12
  interface MockProviderOptions {
@@ -37,6 +37,7 @@ interface SupabaseQueryBuilder {
37
37
  or(filters: string): SupabaseQueryBuilder;
38
38
  order(column: string, options?: {
39
39
  ascending?: boolean;
40
+ nullsFirst?: boolean;
40
41
  }): SupabaseQueryBuilder;
41
42
  range(from: number, to: number): SupabaseQueryBuilder;
42
43
  single(): SupabaseQueryBuilder;
@@ -55,6 +56,35 @@ interface SupabaseQueryBuilder {
55
56
  */
56
57
  interface SupabaseClient {
57
58
  from(table: string): SupabaseQueryBuilder;
59
+ /** Supabase Realtime; needed only for `subscribe` (see `SupabaseProviderOptions.realtime`). */
60
+ channel?(name: string): SupabaseRealtimeChannel;
61
+ /** Releases a channel made by `channel()`. */
62
+ removeChannel?(channel: SupabaseRealtimeChannel): unknown;
63
+ }
64
+ /**
65
+ * One `postgres_changes` message, as Supabase Realtime sends it. Only the
66
+ * fields the provider reads; a real payload has more.
67
+ */
68
+ interface SupabaseRealtimePayload {
69
+ eventType: "INSERT" | "UPDATE" | "DELETE";
70
+ /** The row after an insert or update (empty for a delete). */
71
+ new?: Record<string, unknown>;
72
+ /** For a delete or update: the old row's key (all of it with `REPLICA IDENTITY FULL`). */
73
+ old?: Record<string, unknown>;
74
+ }
75
+ /**
76
+ * The part of a Supabase `RealtimeChannel` the provider uses. A real
77
+ * `@supabase/supabase-js` channel satisfies it.
78
+ */
79
+ interface SupabaseRealtimeChannel {
80
+ on(type: "postgres_changes", filter: {
81
+ event: "*" | "INSERT" | "UPDATE" | "DELETE";
82
+ schema: string;
83
+ table: string;
84
+ filter?: string;
85
+ }, callback: (payload: SupabaseRealtimePayload) => void): SupabaseRealtimeChannel;
86
+ subscribe(callback?: (status: string, error?: unknown) => void): SupabaseRealtimeChannel;
87
+ unsubscribe?(): unknown;
58
88
  }
59
89
  /**
60
90
  * What `createSupabaseProvider` actually requires from its `client` argument
@@ -72,6 +102,9 @@ interface SupabaseClient {
72
102
  */
73
103
  interface SupabaseClientInput {
74
104
  from(table: string): unknown;
105
+ /** Supabase Realtime, for `subscribe`. Any real client's `channel` fits. */
106
+ channel?(name: string): unknown;
107
+ removeChannel?(channel: unknown): unknown;
75
108
  }
76
109
  /** What a write hook is told about the write it is part of. */
77
110
  interface SupabaseWriteContext {
@@ -138,6 +171,14 @@ interface SupabaseResourceConfig {
138
171
  * `useResource` and `useResourceForm` invalidate after their writes.
139
172
  */
140
173
  invalidates?: InvalidationKey[] | ((event: WriteEvent) => InvalidationKey[]);
174
+ /**
175
+ * Serve `subscribe` for this resource through Supabase Realtime
176
+ * (`postgres_changes`), so `live` lists and records refresh when the table
177
+ * changes. The table must be in the `supabase_realtime` publication, with
178
+ * RLS on. `false` turns it off even when `SupabaseProviderOptions.realtime`
179
+ * covers the resource.
180
+ */
181
+ realtime?: boolean;
141
182
  }
142
183
  /** Options for `createSupabaseProvider`. All optional; none changes reads. */
143
184
  interface SupabaseProviderOptions {
@@ -175,6 +216,13 @@ interface SupabaseProviderOptions {
175
216
  resource: string;
176
217
  operation: "getList" | "getOne" | "getMany";
177
218
  }) => void | Promise<void>;
219
+ /**
220
+ * Which resources `subscribe` serves through Supabase Realtime: `true` for
221
+ * all, a list of names, or a test. Off by default; a resource's own
222
+ * `resources[name].realtime` wins. Needs a client with `channel()` (any
223
+ * `@supabase/supabase-js` client).
224
+ */
225
+ realtime?: boolean | readonly string[] | ((resource: string) => boolean);
178
226
  }
179
227
  /**
180
228
  * The Supabase data provider. Reads and writes go through the client you
@@ -310,4 +358,4 @@ type GenesisProviderProps = {
310
358
  */
311
359
  declare function GenesisProvider({ children, provider, mock, storage, queryClient: queryClientProp, }: GenesisProviderProps): react.JSX.Element;
312
360
 
313
- export { BaseRecord, DataProvider, DataProviderRoot, type DataProviderRootProps, DataReadError, DataWriteError, GenesisProvider, type GenesisProviderProps, Identifier, InvalidationKey, type MockProviderOptions, type PostgrestFieldErrors, ReadOutcome, type SupabaseClient, type SupabaseClientInput, type SupabaseProviderOptions, type SupabaseResourceConfig, type SupabaseWriteContext, WriteEvent, WriteOutcome, createMockProvider, createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, useDataProvider, useStorage, writeOutcomeFromCode };
361
+ export { BaseRecord, DataProvider, DataProviderRoot, type DataProviderRootProps, DataReadError, DataWriteError, GenesisProvider, type GenesisProviderProps, Identifier, InvalidationKey, type MockProviderOptions, type PostgrestFieldErrors, ReadOutcome, type SupabaseClient, type SupabaseClientInput, type SupabaseProviderOptions, type SupabaseRealtimeChannel, type SupabaseRealtimePayload, type SupabaseResourceConfig, type SupabaseWriteContext, WriteEvent, WriteOutcome, createMockProvider, createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, useDataProvider, useStorage, writeOutcomeFromCode };
@@ -1,4 +1,4 @@
1
- export { createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, writeOutcomeFromCode } from '../chunk-E5VYFAQO.js';
1
+ export { createSupabaseProvider, fieldErrorsFromPostgrest, readOutcomeFromCode, toDataWriteError, writeOutcomeFromCode } from '../chunk-Y475Z7BM.js';
2
2
  export { DataReadError, isDataReadError, readFailureMessage } from '../chunk-HUNGCTRA.js';
3
3
  export { DataWriteError, isDataWriteError } from '../chunk-5NHR6ZXS.js';
4
4
  import '../chunk-RYEXWQFD.js';
@@ -1,4 +1,4 @@
1
- import { D as DataRow } from './types-5cuflvha.js';
1
+ import { D as DataRow } from './types-C7FJtFXV.js';
2
2
 
3
3
  /**
4
4
  * - `conflict`: someone else changed the row since it was read. `current`
@@ -1,7 +1,7 @@
1
- import { e as ResourceContract, D as DataRow } from '../types-5cuflvha.js';
2
- export { A as AuditEntry, F as FieldPermissions, a as FieldReference, b as FieldSchema, c as FilterOperator, P as ProviderCapabilities, R as RecordComment, d as ResourceAccess, f as ResourceRelation, g as ResourceSchema, h as ResourceSummary, i as RowAction, S as StorageType, V as VersionField } from '../types-5cuflvha.js';
3
- import { I as Identifier } from '../types-DDOmG9hj.js';
4
- export { D as DataReadError, a as DataReadErrorOptions, b as DataWriteError, c as DataWriteErrorOptions, R as ReadOutcome, W as WriteOutcome, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from '../read-outcome-kgNhfuST.js';
1
+ import { e as ResourceContract, D as DataRow } from '../types-C7FJtFXV.js';
2
+ export { A as AuditEntry, F as FieldPermissions, a as FieldReference, b as FieldSchema, c as FilterOperator, P as ProviderCapabilities, R as RecordComment, d as ResourceAccess, f as ResourceRelation, g as ResourceSchema, h as ResourceSummary, i as RowAction, S as StorageType, V as VersionField } from '../types-C7FJtFXV.js';
3
+ import { I as Identifier } from '../types-CS9UpxQS.js';
4
+ export { D as DataReadError, a as DataReadErrorOptions, b as DataWriteError, c as DataWriteErrorOptions, R as ReadOutcome, W as WriteOutcome, i as isDataReadError, d as isDataWriteError, r as readFailureMessage } from '../read-outcome-C_9HbXvb.js';
5
5
  export { FieldType as WidgetHint } from '../form-types.js';
6
6
 
7
7
  /** The key of a contract as a list, whichever form `key` was declared in. */