@pyric/ui 0.1.0-alpha.11 → 0.1.0-alpha.12

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 (152) hide show
  1. package/package.json +5 -4
  2. package/src/agents/ContextWindowUsage.tsx +514 -0
  3. package/src/agents/EmptyState.tsx +27 -0
  4. package/src/agents/Fold.tsx +64 -0
  5. package/src/agents/Modal.tsx +65 -0
  6. package/src/agents/PulsingDot.tsx +28 -0
  7. package/src/agents/inbrowser-agent-usage.d.ts +141 -0
  8. package/src/agents/index.ts +28 -0
  9. package/src/auth/authApi.ts +72 -0
  10. package/src/auth/claims.ts +63 -0
  11. package/src/auth/components/AuthProviderToggles.tsx +147 -0
  12. package/src/auth/components/AuthSignInHelper.tsx +197 -0
  13. package/src/auth/components/AuthUserForm.tsx +328 -0
  14. package/src/auth/components/AuthUserList.tsx +219 -0
  15. package/src/auth/components/ClaimsField.tsx +50 -0
  16. package/src/auth/components/confirmActions.tsx +114 -0
  17. package/src/auth/controller.ts +173 -0
  18. package/src/auth/hooks/index.ts +36 -0
  19. package/src/auth/hooks/useAuthFlowHelper.ts +55 -0
  20. package/src/auth/hooks/useAuthProviderConfig.ts +139 -0
  21. package/src/auth/hooks/useAuthUserEditor.ts +76 -0
  22. package/src/auth/hooks/useAuthUsers.ts +154 -0
  23. package/src/auth/index.ts +42 -0
  24. package/src/auth/providers.ts +28 -0
  25. package/src/auth/reducers/userEditor.ts +186 -0
  26. package/src/events/components/ActivityActionItems.tsx +136 -0
  27. package/src/events/components/ActivityGrid.tsx +197 -0
  28. package/src/events/components/ActivityGridRow.tsx +65 -0
  29. package/src/events/components/ProposedChangeDiff.tsx +175 -0
  30. package/src/events/components/format.ts +21 -0
  31. package/src/events/components/index.ts +17 -0
  32. package/src/events/digest.ts +630 -0
  33. package/src/events/hooks/index.ts +9 -0
  34. package/src/events/hooks/useActivityDigest.ts +42 -0
  35. package/src/events/hooks/useActivityStream.ts +86 -0
  36. package/src/events/index.ts +39 -0
  37. package/src/events/types.ts +152 -0
  38. package/src/firestore/components/CollectionList.tsx +78 -0
  39. package/src/firestore/components/DeleteWithConfirm.tsx +88 -0
  40. package/src/firestore/components/DocumentEditor.tsx +350 -0
  41. package/src/firestore/components/DocumentList.tsx +217 -0
  42. package/src/firestore/components/DocumentPreview.tsx +265 -0
  43. package/src/firestore/components/FieldRenderer.tsx +25 -0
  44. package/src/firestore/components/QueryBuilder.tsx +181 -0
  45. package/src/firestore/components/ReferencePicker.tsx +212 -0
  46. package/src/firestore/components/TreeEntry.tsx +58 -0
  47. package/src/firestore/components/context.ts +25 -0
  48. package/src/firestore/fieldEditors/array.tsx +30 -0
  49. package/src/firestore/fieldEditors/boolean.tsx +39 -0
  50. package/src/firestore/fieldEditors/bytes.tsx +53 -0
  51. package/src/firestore/fieldEditors/geopoint.tsx +71 -0
  52. package/src/firestore/fieldEditors/map.tsx +33 -0
  53. package/src/firestore/fieldEditors/null.tsx +27 -0
  54. package/src/firestore/fieldEditors/number.tsx +43 -0
  55. package/src/firestore/fieldEditors/reference.tsx +87 -0
  56. package/src/firestore/fieldEditors/registry.ts +45 -0
  57. package/src/firestore/fieldEditors/string.tsx +33 -0
  58. package/src/firestore/fieldEditors/timestamp.tsx +75 -0
  59. package/src/firestore/fieldEditors/types.ts +68 -0
  60. package/src/firestore/fieldEditors/vector.tsx +142 -0
  61. package/src/firestore/firestoreApi.ts +86 -0
  62. package/src/firestore/hooks/coerceError.ts +34 -0
  63. package/src/firestore/hooks/index.ts +46 -0
  64. package/src/firestore/hooks/useCollectionList.ts +102 -0
  65. package/src/firestore/hooks/useDocumentEditor.ts +161 -0
  66. package/src/firestore/hooks/useDocumentList.ts +242 -0
  67. package/src/firestore/hooks/useDocumentSubcollections.ts +86 -0
  68. package/src/firestore/hooks/useFirestoreCollection.ts +51 -0
  69. package/src/firestore/hooks/useFirestoreDoc.ts +55 -0
  70. package/src/firestore/hooks/useQueryBuilder.ts +188 -0
  71. package/src/firestore/hooks/useRecursiveDelete.ts +77 -0
  72. package/src/firestore/hooks/useReferencePicker.ts +228 -0
  73. package/src/firestore/import/parseImport.ts +137 -0
  74. package/src/firestore/index.ts +87 -0
  75. package/src/firestore/reducers/defaults.ts +38 -0
  76. package/src/firestore/reducers/documentEditor.ts +239 -0
  77. package/src/firestore/reducers/tree.ts +140 -0
  78. package/src/firestore/reducers/types.ts +92 -0
  79. package/src/firestore/reducers/validation.ts +129 -0
  80. package/src/firestore/types.ts +231 -0
  81. package/src/firestore/validation/ids.ts +45 -0
  82. package/src/firestore/valueEquality.ts +43 -0
  83. package/src/index.ts +10 -0
  84. package/src/primitives/Badge.tsx +40 -0
  85. package/src/primitives/ConfirmDialog.tsx +137 -0
  86. package/src/primitives/CopyButton.tsx +62 -0
  87. package/src/primitives/JsonView.tsx +151 -0
  88. package/src/primitives/SegmentedControl.tsx +72 -0
  89. package/src/primitives/Toast.tsx +161 -0
  90. package/src/primitives/VirtualList.tsx +104 -0
  91. package/src/primitives/hooks/useContainerSize.ts +53 -0
  92. package/src/primitives/hooks/useUpdateHighlights.ts +109 -0
  93. package/src/primitives/index.ts +39 -0
  94. package/src/primitives/useConfirm.tsx +113 -0
  95. package/src/rtdb/components/RtdbPathBar.tsx +135 -0
  96. package/src/rtdb/components/RtdbTree.tsx +409 -0
  97. package/src/rtdb/editor.ts +79 -0
  98. package/src/rtdb/hooks/useRtdbTree.ts +137 -0
  99. package/src/rtdb/index.ts +66 -0
  100. package/src/rtdb/pathInput.ts +47 -0
  101. package/src/rtdb/reducers/tree.ts +191 -0
  102. package/src/rtdb/rtdbApi.ts +23 -0
  103. package/src/rtdb/values.ts +188 -0
  104. package/src/rules/components/DenialInspector.tsx +227 -0
  105. package/src/rules/components/format.ts +171 -0
  106. package/src/rules/components/index.ts +12 -0
  107. package/src/rules/components/scope.ts +75 -0
  108. package/src/rules/hooks/index.ts +5 -0
  109. package/src/rules/hooks/useDenialTrace.ts +100 -0
  110. package/src/rules/index.ts +24 -0
  111. package/src/rules/types.ts +91 -0
  112. package/src/storage/collisionRename.ts +114 -0
  113. package/src/storage/components/DeleteSelectionWithConfirm.tsx +193 -0
  114. package/src/storage/components/ObjectBrowser.tsx +219 -0
  115. package/src/storage/components/ObjectInspector.tsx +169 -0
  116. package/src/storage/components/PathBreadcrumb.tsx +84 -0
  117. package/src/storage/components/UploadDropzone.tsx +182 -0
  118. package/src/storage/folderPlaceholder.ts +40 -0
  119. package/src/storage/hooks/index.ts +59 -0
  120. package/src/storage/hooks/useMetadataEditor.ts +329 -0
  121. package/src/storage/hooks/useObjectUpload.ts +262 -0
  122. package/src/storage/hooks/usePathState.ts +94 -0
  123. package/src/storage/hooks/useStorageDelete.ts +195 -0
  124. package/src/storage/hooks/useStorageList.ts +261 -0
  125. package/src/storage/hooks/useStorageObject.ts +162 -0
  126. package/src/storage/hooks/useStorageRulesGate.ts +270 -0
  127. package/src/storage/hooks/useStorageSelection.ts +90 -0
  128. package/src/storage/index.ts +59 -0
  129. package/src/storage/pendingPrefixes.ts +125 -0
  130. package/src/storage/previews.tsx +120 -0
  131. package/src/storage/storageApi.ts +54 -0
  132. package/src/traffic/components/RuleHeatmap.tsx +97 -0
  133. package/src/traffic/components/TrafficDetail.tsx +160 -0
  134. package/src/traffic/components/TrafficGroupRow.tsx +91 -0
  135. package/src/traffic/components/TrafficLineChart.tsx +139 -0
  136. package/src/traffic/components/TrafficLog.tsx +175 -0
  137. package/src/traffic/components/TrafficMetricCards.tsx +77 -0
  138. package/src/traffic/components/TrafficRow.tsx +69 -0
  139. package/src/traffic/components/TrafficStats.tsx +73 -0
  140. package/src/traffic/components/TrafficTimeline.tsx +289 -0
  141. package/src/traffic/components/format.ts +22 -0
  142. package/src/traffic/components/index.ts +22 -0
  143. package/src/traffic/hooks/index.ts +60 -0
  144. package/src/traffic/hooks/useRuleHeatmap.ts +92 -0
  145. package/src/traffic/hooks/useTrafficBuckets.ts +146 -0
  146. package/src/traffic/hooks/useTrafficFilter.ts +74 -0
  147. package/src/traffic/hooks/useTrafficGroups.ts +126 -0
  148. package/src/traffic/hooks/useTrafficMetrics.ts +250 -0
  149. package/src/traffic/hooks/useTrafficMonitor.ts +111 -0
  150. package/src/traffic/hooks/useTrafficStats.ts +77 -0
  151. package/src/traffic/index.ts +13 -0
  152. package/src/traffic/types.ts +85 -0
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Inline value-editor logic (pure): the type select + text input pair used by
3
+ * the tree's click-to-edit and add-child rows. Four author-facing types cover
4
+ * every RTDB value: string / number / boolean scalars, and `json` for
5
+ * objects/arrays/null (or any hand-written literal).
6
+ */
7
+
8
+ export type RtdbEditorType = 'string' | 'number' | 'boolean' | 'json';
9
+
10
+ export const RTDB_EDITOR_TYPES: readonly RtdbEditorType[] = [
11
+ 'string',
12
+ 'number',
13
+ 'boolean',
14
+ 'json',
15
+ ];
16
+
17
+ /** The editor type a value opens under when clicked. */
18
+ export function inferRtdbEditorType(value: unknown): RtdbEditorType {
19
+ switch (typeof value) {
20
+ case 'string':
21
+ return 'string';
22
+ case 'number':
23
+ return 'number';
24
+ case 'boolean':
25
+ return 'boolean';
26
+ default:
27
+ return 'json';
28
+ }
29
+ }
30
+
31
+ /** Seed the editor's text field from the current value under a type. */
32
+ export function formatRtdbEditorValue(value: unknown, type: RtdbEditorType): string {
33
+ if (type === 'json') return JSON.stringify(value ?? null);
34
+ if (value === null || value === undefined) return '';
35
+ return String(value);
36
+ }
37
+
38
+ /** Why a typed child key is unusable, or `null` when it's fine. RTDB forbids
39
+ * `. $ # [ ] /` in keys (a `/` would silently create a nested path). */
40
+ export function rtdbKeyInputError(key: string): string | null {
41
+ if (key.trim().length === 0) return 'Enter a key.';
42
+ const bad = key.match(/[.$#[\]/]/);
43
+ if (bad) return `Keys can't contain "${bad[0]}".`;
44
+ return null;
45
+ }
46
+
47
+ export type RtdbEditorResult =
48
+ | { ok: true; value: unknown }
49
+ | { ok: false; error: string };
50
+
51
+ /** Coerce the editor's text under the selected type, or explain why not. */
52
+ export function coerceRtdbEditorValue(type: RtdbEditorType, text: string): RtdbEditorResult {
53
+ switch (type) {
54
+ case 'string':
55
+ return { ok: true, value: text };
56
+ case 'number': {
57
+ const trimmed = text.trim();
58
+ if (trimmed.length === 0) return { ok: false, error: 'Enter a number.' };
59
+ const n = Number(trimmed);
60
+ if (Number.isNaN(n)) return { ok: false, error: `"${text}" is not a number.` };
61
+ return { ok: true, value: n };
62
+ }
63
+ case 'boolean': {
64
+ const t = text.trim().toLowerCase();
65
+ if (t === 'true') return { ok: true, value: true };
66
+ if (t === 'false') return { ok: true, value: false };
67
+ return { ok: false, error: 'Enter true or false.' };
68
+ }
69
+ case 'json': {
70
+ const t = text.trim();
71
+ if (t.length === 0) return { ok: true, value: null };
72
+ try {
73
+ return { ok: true, value: JSON.parse(t) };
74
+ } catch (e) {
75
+ return { ok: false, error: e instanceof Error ? e.message : 'Invalid JSON.' };
76
+ }
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,137 @@
1
+ import { useCallback, useEffect, useMemo, useReducer } from 'react';
2
+ import type { RtdbApi } from '../rtdbApi.js';
3
+ import {
4
+ normalizeRtdbPath,
5
+ rtdbUpdateEntries,
6
+ rtdbValuesEqual,
7
+ } from '../values.js';
8
+ import {
9
+ useUpdateHighlights,
10
+ type UpdateHighlight,
11
+ } from '../../primitives/hooks/useUpdateHighlights.js';
12
+ import {
13
+ initialRtdbTree,
14
+ isRtdbPathExpanded,
15
+ rtdbTreeReducer,
16
+ rtdbTreeValueAt,
17
+ rtdbVisibleChildren,
18
+ type RtdbTreeState,
19
+ type RtdbVisibleChildren,
20
+ } from '../reducers/tree.js';
21
+
22
+ /** Console-style child paging (Firebase console shows 50, then "show more"). */
23
+ export const RTDB_DEFAULT_PAGE_SIZE = 50;
24
+
25
+ export interface UseRtdbTreeOptions {
26
+ /** Children rendered per level before "show more". Default 50. */
27
+ pageSize?: number;
28
+ }
29
+
30
+ /**
31
+ * Everything a tree view needs: the reducer state plus path-addressed
32
+ * selectors and the expansion/paging commands. All paths are ABSOLUTE
33
+ * database paths.
34
+ */
35
+ export interface RtdbTreeController {
36
+ state: RtdbTreeState;
37
+ pageSize: number;
38
+ valueAt(path: string): unknown;
39
+ isExpanded(path: string): boolean;
40
+ childrenAt(path: string): RtdbVisibleChildren;
41
+ toggle(path: string): void;
42
+ showMore(path: string): void;
43
+ updateAt(path: string): UpdateHighlight | undefined;
44
+ }
45
+
46
+ /**
47
+ * Live tree state for the RTDB viewer: subscribes to the value at the view
48
+ * root `path` (realtime — every write re-delivers the subtree) and runs the
49
+ * expansion/paging reducer over it. See `reducers/tree.ts` for the
50
+ * one-subscription-per-view-root loading strategy and why expansion is a
51
+ * pure render toggle rather than a fetch.
52
+ */
53
+ export function useRtdbTree(
54
+ api: RtdbApi,
55
+ path: string,
56
+ options: UseRtdbTreeOptions = {},
57
+ ): RtdbTreeController {
58
+ const pageSize = options.pageSize ?? RTDB_DEFAULT_PAGE_SIZE;
59
+ const normalized = normalizeRtdbPath(path);
60
+ const [state, dispatch] = useReducer(rtdbTreeReducer, normalized, initialRtdbTree);
61
+ const updateEntries = useMemo(
62
+ () => rtdbUpdateEntries(state.value, state.path),
63
+ [state.path, state.value],
64
+ );
65
+ const updates = useUpdateHighlights({
66
+ scope: state.path,
67
+ entries: updateEntries,
68
+ equals: rtdbValuesEqual,
69
+ ready: state.status === 'live',
70
+ });
71
+
72
+ // Keep the reducer's view root in lockstep with the prop BEFORE the
73
+ // subscription effect below re-runs, so a snapshot for the new root never
74
+ // lands on the old root's state.
75
+ if (state.path !== normalized) {
76
+ dispatch({ type: 'navigate', path: normalized });
77
+ }
78
+
79
+ useEffect(() => {
80
+ let cancelled = false;
81
+ const unsubscribe = api.subscribeValue(
82
+ normalized,
83
+ (value) => {
84
+ if (!cancelled) dispatch({ type: 'value', path: normalized, value });
85
+ },
86
+ (err) => {
87
+ if (!cancelled) {
88
+ dispatch({
89
+ type: 'error',
90
+ path: normalized,
91
+ message: err instanceof Error ? err.message : String(err),
92
+ });
93
+ }
94
+ },
95
+ );
96
+ return () => {
97
+ cancelled = true;
98
+ unsubscribe();
99
+ };
100
+ }, [api, normalized]);
101
+
102
+ const valueAt = useCallback(
103
+ (p: string) => rtdbTreeValueAt(state, p),
104
+ [state],
105
+ );
106
+ const isExpanded = useCallback(
107
+ (p: string) => isRtdbPathExpanded(state, p),
108
+ [state],
109
+ );
110
+ const childrenAt = useCallback(
111
+ (p: string) => rtdbVisibleChildren(state, p, pageSize),
112
+ [state, pageSize],
113
+ );
114
+ const toggle = useCallback((p: string) => dispatch({ type: 'toggle', path: p }), []);
115
+ const showMore = useCallback(
116
+ (p: string) => dispatch({ type: 'show-more', path: p, pageSize }),
117
+ [pageSize],
118
+ );
119
+ const updateAt = useCallback(
120
+ (p: string) => updates.get(normalizeRtdbPath(p)),
121
+ [updates],
122
+ );
123
+
124
+ return useMemo(
125
+ () => ({
126
+ state,
127
+ pageSize,
128
+ valueAt,
129
+ isExpanded,
130
+ childrenAt,
131
+ toggle,
132
+ showMore,
133
+ updateAt,
134
+ }),
135
+ [state, pageSize, valueAt, isExpanded, childrenAt, toggle, showMore, updateAt],
136
+ );
137
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * `@pyric/ui/rtdb` — the headless RTDB data viewer, in the Firebase console /
3
+ * firebase-tools-ui form: an editable path bar (crumbs + direct path entry)
4
+ * over an expandable tree with inline add/edit/delete affordances. Follows
5
+ * this package's firestore/auth/storage split: pure logic + hooks +
6
+ * unstyled components on `data-*` styling contracts; the consumer (Studio)
7
+ * brings the CSS and the backend bundle ({@link RtdbApi}).
8
+ */
9
+
10
+ // Pure path + value helpers.
11
+ export {
12
+ normalizeRtdbPath,
13
+ rtdbPathSegments,
14
+ joinRtdbPath,
15
+ parentRtdbPath,
16
+ relativeRtdbPath,
17
+ isRtdbObjectValue,
18
+ hasRtdbChildren,
19
+ rtdbChildEntries,
20
+ rtdbValueAt,
21
+ formatRtdbJson,
22
+ parseRtdbJson,
23
+ rtdbValueKind,
24
+ previewRtdbValue,
25
+ normalizeRtdbSnapshotValue,
26
+ formatRtdbValueLabel,
27
+ } from './values.js';
28
+
29
+ // Path-bar input parsing + crumb derivation.
30
+ export { parseRtdbPathInput, rtdbCrumbs, type RtdbCrumb } from './pathInput.js';
31
+
32
+ // Inline value-editor logic.
33
+ export {
34
+ RTDB_EDITOR_TYPES,
35
+ inferRtdbEditorType,
36
+ formatRtdbEditorValue,
37
+ coerceRtdbEditorValue,
38
+ rtdbKeyInputError,
39
+ type RtdbEditorType,
40
+ type RtdbEditorResult,
41
+ } from './editor.js';
42
+
43
+ // Backend seam.
44
+ export type { RtdbApi } from './rtdbApi.js';
45
+
46
+ // Tree state (pure reducer + selectors).
47
+ export {
48
+ initialRtdbTree,
49
+ rtdbTreeReducer,
50
+ rtdbTreeValueAt,
51
+ isRtdbPathExpanded,
52
+ rtdbVisibleChildren,
53
+ type RtdbTreeState,
54
+ type RtdbTreeAction,
55
+ type RtdbVisibleChildren,
56
+ } from './reducers/tree.js';
57
+
58
+ // Hook + components.
59
+ export {
60
+ useRtdbTree,
61
+ RTDB_DEFAULT_PAGE_SIZE,
62
+ type UseRtdbTreeOptions,
63
+ type RtdbTreeController,
64
+ } from './hooks/useRtdbTree.js';
65
+ export { RtdbPathBar, type RtdbPathBarProps } from './components/RtdbPathBar.js';
66
+ export { RtdbTree, type RtdbTreeProps } from './components/RtdbTree.js';
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Editable path-bar logic (pure): parse what a user TYPES OR PASTES into the
3
+ * path input, and derive the clickable crumb list for display mode. The
4
+ * interaction form follows the Firebase console / firebase-tools-ui database
5
+ * viewer's editable breadcrumb bar (clean-room adaptation — behavior only).
6
+ */
7
+
8
+ import { normalizeRtdbPath, rtdbPathSegments } from './values.js';
9
+
10
+ /**
11
+ * Normalize raw path-input text to a database path. Tolerant of what people
12
+ * paste into a database URL bar:
13
+ * - a plain path, with or without the leading slash (`rooms/r1`, `/rooms`)
14
+ * - a full URL (`https://x.firebaseio.com/rooms/r1` → `/rooms/r1`)
15
+ * - repeated/trailing slashes, surrounding whitespace
16
+ * - a `?query` / `#hash` tail (dropped)
17
+ * Empty input is the root.
18
+ */
19
+ export function parseRtdbPathInput(raw: string): string {
20
+ let text = raw.trim();
21
+ if (text.length === 0) return '/';
22
+ // A pasted absolute URL: strip `scheme://host[:port]`.
23
+ const url = /^[a-z][a-z0-9+.-]*:\/\/[^/?#]+/i.exec(text);
24
+ if (url) text = text.slice(url[0].length);
25
+ // Drop a query/hash tail (URL bars carry them; database paths do not).
26
+ text = text.replace(/[?#].*$/, '');
27
+ return normalizeRtdbPath(text);
28
+ }
29
+
30
+ export interface RtdbCrumb {
31
+ /** The path segment to display. */
32
+ label: string;
33
+ /** Absolute database path this crumb navigates to. */
34
+ path: string;
35
+ }
36
+
37
+ /**
38
+ * The non-root crumbs for a path, in order. `'/'` yields `[]` — the root crumb
39
+ * is the caller's (it carries the database/instance label, not a segment).
40
+ */
41
+ export function rtdbCrumbs(path: string): RtdbCrumb[] {
42
+ const segments = rtdbPathSegments(path);
43
+ return segments.map((label, i) => ({
44
+ label,
45
+ path: `/${segments.slice(0, i + 1).join('/')}`,
46
+ }));
47
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * RTDB viewer tree state (pure reducer). The interaction form is the Firebase
3
+ * console / firebase-tools-ui data viewer: one view root (set by the path
4
+ * bar), expandable/collapsible descendants, and per-level paging for wide
5
+ * child lists.
6
+ *
7
+ * DATA-LOADING STRATEGY (documented decision): the worker protocol exposes
8
+ * `rtdb.get` / value subscriptions per PATH but no shallow or depth-limited
9
+ * reads — a read at a path always ships that path's WHOLE subtree over the
10
+ * MessagePort. A per-expand fetch would therefore re-ship data the view-root
11
+ * read already delivered. So the viewer holds ONE live value subscription at
12
+ * the view root (realtime by construction) and makes RENDERING lazy instead:
13
+ * nodes below the root start collapsed and mount nothing until expanded, and
14
+ * an expanded level renders at most `pageSize` children until "show more"
15
+ * (console-style paging at 50, chosen over virtualization for its simplicity —
16
+ * it bounds the mounted DOM the same way). Cost is bounded by navigating the
17
+ * view root deeper, which is the path bar's job.
18
+ */
19
+
20
+ import {
21
+ hasRtdbChildren,
22
+ normalizeRtdbPath,
23
+ normalizeRtdbSnapshotValue,
24
+ relativeRtdbPath,
25
+ rtdbChildEntries,
26
+ rtdbValueAt,
27
+ } from '../values.js';
28
+
29
+ export interface RtdbTreeState {
30
+ /** The view root — the absolute database path the path bar points at. */
31
+ path: string;
32
+ /** `loading` until the first snapshot after a navigate; then `live`. */
33
+ status: 'loading' | 'live' | 'error';
34
+ /** The subtree value AT `path` (plain JSON; `null` = nothing there). */
35
+ value: unknown;
36
+ error: string | null;
37
+ /** Expanded descendant paths (absolute). The view root itself is always
38
+ * expanded and is never in this set. */
39
+ expanded: Record<string, true>;
40
+ /** Per-path shown-children override (absolute path → count). Absent means
41
+ * one page. */
42
+ pages: Record<string, number>;
43
+ }
44
+
45
+ export type RtdbTreeAction =
46
+ /** Path bar navigation: reset to a new view root. */
47
+ | { type: 'navigate'; path: string }
48
+ /** A value snapshot arrived. `path` is the subscription's view root — a
49
+ * snapshot from a superseded subscription (its path no longer the state's)
50
+ * is ignored. */
51
+ | { type: 'value'; path: string; value: unknown }
52
+ /** The view-root subscription failed (same `path` guard as `value`). */
53
+ | { type: 'error'; path: string; message: string }
54
+ | { type: 'toggle'; path: string }
55
+ | { type: 'expand'; path: string }
56
+ | { type: 'collapse'; path: string }
57
+ /** Reveal one more page of children at `path`. */
58
+ | { type: 'show-more'; path: string; pageSize: number };
59
+
60
+ export function initialRtdbTree(path: string): RtdbTreeState {
61
+ return {
62
+ path: normalizeRtdbPath(path),
63
+ status: 'loading',
64
+ value: null,
65
+ error: null,
66
+ expanded: {},
67
+ pages: {},
68
+ };
69
+ }
70
+
71
+ /** The subtree value at an ABSOLUTE database path, resolved against the
72
+ * loaded view-root value. `null` outside the view root. */
73
+ export function rtdbTreeValueAt(state: RtdbTreeState, path: string): unknown {
74
+ const rel = relativeRtdbPath(state.path, path);
75
+ return rel === null ? null : rtdbValueAt(state.value, rel);
76
+ }
77
+
78
+ /** Is this absolute path expanded? The view root always is. */
79
+ export function isRtdbPathExpanded(state: RtdbTreeState, path: string): boolean {
80
+ const normalized = normalizeRtdbPath(path);
81
+ return normalized === state.path || state.expanded[normalized] === true;
82
+ }
83
+
84
+ export interface RtdbVisibleChildren {
85
+ /** The children to render, in key order, capped at the shown count. */
86
+ entries: Array<[string, unknown]>;
87
+ /** Total direct children at this path. */
88
+ total: number;
89
+ /** How many more "show more" would reveal (0 = all shown). */
90
+ hiddenCount: number;
91
+ }
92
+
93
+ /** The page-capped child list at an absolute path (console pages at 50). */
94
+ export function rtdbVisibleChildren(
95
+ state: RtdbTreeState,
96
+ path: string,
97
+ pageSize: number,
98
+ ): RtdbVisibleChildren {
99
+ const entries = rtdbChildEntries(rtdbTreeValueAt(state, path));
100
+ const shown = Math.min(
101
+ entries.length,
102
+ state.pages[normalizeRtdbPath(path)] ?? pageSize,
103
+ );
104
+ return {
105
+ entries: entries.slice(0, shown),
106
+ total: entries.length,
107
+ hiddenCount: entries.length - shown,
108
+ };
109
+ }
110
+
111
+ /** Keep only entries whose path still resolves to a node WITH children under
112
+ * the new value — a removed or scalar-ified node can't stay expanded/paged. */
113
+ function pruneByValue<V>(
114
+ record: Record<string, V>,
115
+ state: Pick<RtdbTreeState, 'path' | 'value'>,
116
+ ): Record<string, V> {
117
+ const next: Record<string, V> = {};
118
+ let changed = false;
119
+ for (const [path, v] of Object.entries(record)) {
120
+ const rel = relativeRtdbPath(state.path, path);
121
+ if (rel !== null && hasRtdbChildren(rtdbValueAt(state.value, rel))) {
122
+ next[path] = v;
123
+ } else {
124
+ changed = true;
125
+ }
126
+ }
127
+ return changed ? next : record;
128
+ }
129
+
130
+ export function rtdbTreeReducer(
131
+ state: RtdbTreeState,
132
+ action: RtdbTreeAction,
133
+ ): RtdbTreeState {
134
+ switch (action.type) {
135
+ case 'navigate': {
136
+ const path = normalizeRtdbPath(action.path);
137
+ // Re-navigating to the current root keeps the loaded value + expansion.
138
+ if (path === state.path) return state;
139
+ return initialRtdbTree(path);
140
+ }
141
+ case 'value': {
142
+ if (normalizeRtdbPath(action.path) !== state.path) return state;
143
+ const next: RtdbTreeState = {
144
+ ...state,
145
+ status: 'live',
146
+ // RTDB semantics at the ingestion seam: an empty object IS null (an
147
+ // empty database reads back as `{}`), so the tree never holds a
148
+ // childless object that would render as a scalar leaf.
149
+ value: normalizeRtdbSnapshotValue(action.value),
150
+ error: null,
151
+ };
152
+ // UPDATE-MERGE: a live snapshot keeps expansion/paging for surviving
153
+ // paths and prunes state for nodes that vanished or became scalars.
154
+ next.expanded = pruneByValue(state.expanded, next);
155
+ next.pages = pruneByValue(state.pages, next);
156
+ return next;
157
+ }
158
+ case 'error':
159
+ if (normalizeRtdbPath(action.path) !== state.path) return state;
160
+ return { ...state, status: 'error', error: action.message };
161
+ case 'expand': {
162
+ const path = normalizeRtdbPath(action.path);
163
+ if (path === state.path || state.expanded[path]) return state;
164
+ // LAZY RENDER: expanding only flips the flag — children mount from the
165
+ // already-loaded subtree (see the data-loading strategy note above).
166
+ return { ...state, expanded: { ...state.expanded, [path]: true } };
167
+ }
168
+ case 'collapse': {
169
+ const path = normalizeRtdbPath(action.path);
170
+ if (!state.expanded[path]) return state;
171
+ const expanded = { ...state.expanded };
172
+ delete expanded[path];
173
+ return { ...state, expanded };
174
+ }
175
+ case 'toggle': {
176
+ const path = normalizeRtdbPath(action.path);
177
+ return rtdbTreeReducer(state, {
178
+ type: state.expanded[path] ? 'collapse' : 'expand',
179
+ path,
180
+ });
181
+ }
182
+ case 'show-more': {
183
+ const path = normalizeRtdbPath(action.path);
184
+ const shown = state.pages[path] ?? action.pageSize;
185
+ return {
186
+ ...state,
187
+ pages: { ...state.pages, [path]: shown + action.pageSize },
188
+ };
189
+ }
190
+ }
191
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The RTDB backend seam the viewer drives — an EXPLICIT bundle, not a context
3
+ * default. Unlike Firestore/Auth/Storage there is no in-process handle typed
4
+ * into `@pyric/ui`, so the consumer constructs the bundle and passes it to the
5
+ * hook/components directly (Studio wires it to the SharedWorker client's
6
+ * admin-lens ops; data views are always admin — PRINCIPLES M3).
7
+ */
8
+ export interface RtdbApi {
9
+ /** Replace the value at `path` (`null` deletes, RTDB semantics). */
10
+ set(path: string, value: unknown): Promise<void>;
11
+ /** Delete the subtree at `path`. */
12
+ remove(path: string): Promise<void>;
13
+ /**
14
+ * Live value subscription at `path`: `next` fires with the subtree's plain
15
+ * JSON value (`null` when absent) on subscribe and after every change.
16
+ * Returns the unsubscribe.
17
+ */
18
+ subscribeValue(
19
+ path: string,
20
+ next: (value: unknown) => void,
21
+ error?: (err: unknown) => void,
22
+ ): () => void;
23
+ }