@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.
- package/package.json +5 -4
- package/src/agents/ContextWindowUsage.tsx +514 -0
- package/src/agents/EmptyState.tsx +27 -0
- package/src/agents/Fold.tsx +64 -0
- package/src/agents/Modal.tsx +65 -0
- package/src/agents/PulsingDot.tsx +28 -0
- package/src/agents/inbrowser-agent-usage.d.ts +141 -0
- package/src/agents/index.ts +28 -0
- package/src/auth/authApi.ts +72 -0
- package/src/auth/claims.ts +63 -0
- package/src/auth/components/AuthProviderToggles.tsx +147 -0
- package/src/auth/components/AuthSignInHelper.tsx +197 -0
- package/src/auth/components/AuthUserForm.tsx +328 -0
- package/src/auth/components/AuthUserList.tsx +219 -0
- package/src/auth/components/ClaimsField.tsx +50 -0
- package/src/auth/components/confirmActions.tsx +114 -0
- package/src/auth/controller.ts +173 -0
- package/src/auth/hooks/index.ts +36 -0
- package/src/auth/hooks/useAuthFlowHelper.ts +55 -0
- package/src/auth/hooks/useAuthProviderConfig.ts +139 -0
- package/src/auth/hooks/useAuthUserEditor.ts +76 -0
- package/src/auth/hooks/useAuthUsers.ts +154 -0
- package/src/auth/index.ts +42 -0
- package/src/auth/providers.ts +28 -0
- package/src/auth/reducers/userEditor.ts +186 -0
- package/src/events/components/ActivityActionItems.tsx +136 -0
- package/src/events/components/ActivityGrid.tsx +197 -0
- package/src/events/components/ActivityGridRow.tsx +65 -0
- package/src/events/components/ProposedChangeDiff.tsx +175 -0
- package/src/events/components/format.ts +21 -0
- package/src/events/components/index.ts +17 -0
- package/src/events/digest.ts +630 -0
- package/src/events/hooks/index.ts +9 -0
- package/src/events/hooks/useActivityDigest.ts +42 -0
- package/src/events/hooks/useActivityStream.ts +86 -0
- package/src/events/index.ts +39 -0
- package/src/events/types.ts +152 -0
- package/src/firestore/components/CollectionList.tsx +78 -0
- package/src/firestore/components/DeleteWithConfirm.tsx +88 -0
- package/src/firestore/components/DocumentEditor.tsx +350 -0
- package/src/firestore/components/DocumentList.tsx +217 -0
- package/src/firestore/components/DocumentPreview.tsx +265 -0
- package/src/firestore/components/FieldRenderer.tsx +25 -0
- package/src/firestore/components/QueryBuilder.tsx +181 -0
- package/src/firestore/components/ReferencePicker.tsx +212 -0
- package/src/firestore/components/TreeEntry.tsx +58 -0
- package/src/firestore/components/context.ts +25 -0
- package/src/firestore/fieldEditors/array.tsx +30 -0
- package/src/firestore/fieldEditors/boolean.tsx +39 -0
- package/src/firestore/fieldEditors/bytes.tsx +53 -0
- package/src/firestore/fieldEditors/geopoint.tsx +71 -0
- package/src/firestore/fieldEditors/map.tsx +33 -0
- package/src/firestore/fieldEditors/null.tsx +27 -0
- package/src/firestore/fieldEditors/number.tsx +43 -0
- package/src/firestore/fieldEditors/reference.tsx +87 -0
- package/src/firestore/fieldEditors/registry.ts +45 -0
- package/src/firestore/fieldEditors/string.tsx +33 -0
- package/src/firestore/fieldEditors/timestamp.tsx +75 -0
- package/src/firestore/fieldEditors/types.ts +68 -0
- package/src/firestore/fieldEditors/vector.tsx +142 -0
- package/src/firestore/firestoreApi.ts +86 -0
- package/src/firestore/hooks/coerceError.ts +34 -0
- package/src/firestore/hooks/index.ts +46 -0
- package/src/firestore/hooks/useCollectionList.ts +102 -0
- package/src/firestore/hooks/useDocumentEditor.ts +161 -0
- package/src/firestore/hooks/useDocumentList.ts +242 -0
- package/src/firestore/hooks/useDocumentSubcollections.ts +86 -0
- package/src/firestore/hooks/useFirestoreCollection.ts +51 -0
- package/src/firestore/hooks/useFirestoreDoc.ts +55 -0
- package/src/firestore/hooks/useQueryBuilder.ts +188 -0
- package/src/firestore/hooks/useRecursiveDelete.ts +77 -0
- package/src/firestore/hooks/useReferencePicker.ts +228 -0
- package/src/firestore/import/parseImport.ts +137 -0
- package/src/firestore/index.ts +87 -0
- package/src/firestore/reducers/defaults.ts +38 -0
- package/src/firestore/reducers/documentEditor.ts +239 -0
- package/src/firestore/reducers/tree.ts +140 -0
- package/src/firestore/reducers/types.ts +92 -0
- package/src/firestore/reducers/validation.ts +129 -0
- package/src/firestore/types.ts +231 -0
- package/src/firestore/validation/ids.ts +45 -0
- package/src/firestore/valueEquality.ts +43 -0
- package/src/index.ts +10 -0
- package/src/primitives/Badge.tsx +40 -0
- package/src/primitives/ConfirmDialog.tsx +137 -0
- package/src/primitives/CopyButton.tsx +62 -0
- package/src/primitives/JsonView.tsx +151 -0
- package/src/primitives/SegmentedControl.tsx +72 -0
- package/src/primitives/Toast.tsx +161 -0
- package/src/primitives/VirtualList.tsx +104 -0
- package/src/primitives/hooks/useContainerSize.ts +53 -0
- package/src/primitives/hooks/useUpdateHighlights.ts +109 -0
- package/src/primitives/index.ts +39 -0
- package/src/primitives/useConfirm.tsx +113 -0
- package/src/rtdb/components/RtdbPathBar.tsx +135 -0
- package/src/rtdb/components/RtdbTree.tsx +409 -0
- package/src/rtdb/editor.ts +79 -0
- package/src/rtdb/hooks/useRtdbTree.ts +137 -0
- package/src/rtdb/index.ts +66 -0
- package/src/rtdb/pathInput.ts +47 -0
- package/src/rtdb/reducers/tree.ts +191 -0
- package/src/rtdb/rtdbApi.ts +23 -0
- package/src/rtdb/values.ts +188 -0
- package/src/rules/components/DenialInspector.tsx +227 -0
- package/src/rules/components/format.ts +171 -0
- package/src/rules/components/index.ts +12 -0
- package/src/rules/components/scope.ts +75 -0
- package/src/rules/hooks/index.ts +5 -0
- package/src/rules/hooks/useDenialTrace.ts +100 -0
- package/src/rules/index.ts +24 -0
- package/src/rules/types.ts +91 -0
- package/src/storage/collisionRename.ts +114 -0
- package/src/storage/components/DeleteSelectionWithConfirm.tsx +193 -0
- package/src/storage/components/ObjectBrowser.tsx +219 -0
- package/src/storage/components/ObjectInspector.tsx +169 -0
- package/src/storage/components/PathBreadcrumb.tsx +84 -0
- package/src/storage/components/UploadDropzone.tsx +182 -0
- package/src/storage/folderPlaceholder.ts +40 -0
- package/src/storage/hooks/index.ts +59 -0
- package/src/storage/hooks/useMetadataEditor.ts +329 -0
- package/src/storage/hooks/useObjectUpload.ts +262 -0
- package/src/storage/hooks/usePathState.ts +94 -0
- package/src/storage/hooks/useStorageDelete.ts +195 -0
- package/src/storage/hooks/useStorageList.ts +261 -0
- package/src/storage/hooks/useStorageObject.ts +162 -0
- package/src/storage/hooks/useStorageRulesGate.ts +270 -0
- package/src/storage/hooks/useStorageSelection.ts +90 -0
- package/src/storage/index.ts +59 -0
- package/src/storage/pendingPrefixes.ts +125 -0
- package/src/storage/previews.tsx +120 -0
- package/src/storage/storageApi.ts +54 -0
- package/src/traffic/components/RuleHeatmap.tsx +97 -0
- package/src/traffic/components/TrafficDetail.tsx +160 -0
- package/src/traffic/components/TrafficGroupRow.tsx +91 -0
- package/src/traffic/components/TrafficLineChart.tsx +139 -0
- package/src/traffic/components/TrafficLog.tsx +175 -0
- package/src/traffic/components/TrafficMetricCards.tsx +77 -0
- package/src/traffic/components/TrafficRow.tsx +69 -0
- package/src/traffic/components/TrafficStats.tsx +73 -0
- package/src/traffic/components/TrafficTimeline.tsx +289 -0
- package/src/traffic/components/format.ts +22 -0
- package/src/traffic/components/index.ts +22 -0
- package/src/traffic/hooks/index.ts +60 -0
- package/src/traffic/hooks/useRuleHeatmap.ts +92 -0
- package/src/traffic/hooks/useTrafficBuckets.ts +146 -0
- package/src/traffic/hooks/useTrafficFilter.ts +74 -0
- package/src/traffic/hooks/useTrafficGroups.ts +126 -0
- package/src/traffic/hooks/useTrafficMetrics.ts +250 -0
- package/src/traffic/hooks/useTrafficMonitor.ts +111 -0
- package/src/traffic/hooks/useTrafficStats.ts +77 -0
- package/src/traffic/index.ts +13 -0
- package/src/traffic/types.ts +85 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { AnyActivityEvent } from '../types.js';
|
|
3
|
+
import {
|
|
4
|
+
computeActivityDigest,
|
|
5
|
+
type ActivityBandWithGroups,
|
|
6
|
+
type ActivityDigest,
|
|
7
|
+
type ActivityDigestOptions,
|
|
8
|
+
type ActivityRow,
|
|
9
|
+
} from '../digest.js';
|
|
10
|
+
import { ActivityGridRow } from './ActivityGridRow.js';
|
|
11
|
+
|
|
12
|
+
export interface ActivityGridProps {
|
|
13
|
+
/**
|
|
14
|
+
* The unified event stream — `sandbox.history()` and/or a live buffer
|
|
15
|
+
* (see `useActivityStream`). The grid folds it into the banded digest
|
|
16
|
+
* internally. Mutually exclusive with `digest`.
|
|
17
|
+
*/
|
|
18
|
+
events?: readonly AnyActivityEvent[];
|
|
19
|
+
/**
|
|
20
|
+
* A precomputed digest from `useActivityDigest` / `computeActivity
|
|
21
|
+
* Digest`. Pass this when the host already memoizes the fold (so the
|
|
22
|
+
* grid doesn't recompute). Mutually exclusive with `events`.
|
|
23
|
+
*/
|
|
24
|
+
digest?: ActivityDigest;
|
|
25
|
+
/** Grouping / ordering options, forwarded to the reducer when the grid
|
|
26
|
+
* folds `events` itself. Ignored when `digest` is supplied. */
|
|
27
|
+
options?: ActivityDigestOptions & { now?: number };
|
|
28
|
+
/** The selected row id (`data-pyric-selected`). */
|
|
29
|
+
selectedId?: string;
|
|
30
|
+
onSelect?: (row: ActivityRow) => void;
|
|
31
|
+
/** Clamp rows shown per band; the rest collapse into a "N more" stub.
|
|
32
|
+
* Independent of the reducer's `rowsPerBand` (which trims the data) —
|
|
33
|
+
* this is a pure display clamp that keeps the true count visible.
|
|
34
|
+
* Default: show all rows. */
|
|
35
|
+
maxRowsPerBand?: number;
|
|
36
|
+
/** Render the per-band overflow stub. Default renders a
|
|
37
|
+
* `[data-pyric-band-more]` element reading "N more {label}". */
|
|
38
|
+
renderBandMore?: (band: ActivityBandWithGroups, hidden: number) => ReactNode;
|
|
39
|
+
/** Override the `when` column rendering. */
|
|
40
|
+
formatWhen?: (at: number, now: number) => string;
|
|
41
|
+
/** "Now" anchor for `formatWhen`. */
|
|
42
|
+
now?: number;
|
|
43
|
+
/** Shown when the digest has no rows. */
|
|
44
|
+
emptyState?: ReactNode;
|
|
45
|
+
className?: string;
|
|
46
|
+
/** Render a leading column-header row (`target change for lens when`).
|
|
47
|
+
* Default true — matches the mock's `.colhead`. */
|
|
48
|
+
showColumnHeader?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const COLUMN_HEADERS = ['target', 'change', 'for', 'as', 'when'] as const;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Headless activity grid over the unified `SandboxEvent` stream. Folds
|
|
55
|
+
* events into category bands (Denied / Added / Updated / Removed /
|
|
56
|
+
* Signed in / …), each band a `target · change · for · lens · when`
|
|
57
|
+
* column grid grouped under a `label · count · attribution` header.
|
|
58
|
+
* Denials lead (lead-with-consequence) and are flagged first-class.
|
|
59
|
+
*
|
|
60
|
+
* Ships ZERO styling — the host (Pyric Studio) applies the rigid column
|
|
61
|
+
* grid + band typography via the `data-pyric-*` contract:
|
|
62
|
+
* - `[data-pyric-ui="activity-grid"]` — the root.
|
|
63
|
+
* - `[data-pyric-band]` — a band header, with `data-pyric-band-key`,
|
|
64
|
+
* `data-pyric-band-count`, `data-pyric-band-denied` (on the denied
|
|
65
|
+
* band). Children: `[data-pyric-band-label]`, `[data-pyric-band-n]`,
|
|
66
|
+
* `[data-pyric-band-attr]` (omitted when attribution is mixed).
|
|
67
|
+
* - `[data-pyric-band-rows]` — the row container; with grouping,
|
|
68
|
+
* `[data-pyric-band-subgroup]` (+ `data-pyric-subgroup-key`) wraps
|
|
69
|
+
* each pivot bucket.
|
|
70
|
+
* - `[data-pyric-band-more]` — the "N more" overflow stub.
|
|
71
|
+
* - the `<ActivityGridRow>` contract for each row.
|
|
72
|
+
*/
|
|
73
|
+
export function ActivityGrid({
|
|
74
|
+
events,
|
|
75
|
+
digest,
|
|
76
|
+
options,
|
|
77
|
+
selectedId,
|
|
78
|
+
onSelect,
|
|
79
|
+
maxRowsPerBand,
|
|
80
|
+
renderBandMore,
|
|
81
|
+
formatWhen,
|
|
82
|
+
now,
|
|
83
|
+
emptyState,
|
|
84
|
+
className,
|
|
85
|
+
showColumnHeader = true,
|
|
86
|
+
}: ActivityGridProps): ReactNode {
|
|
87
|
+
const model: ActivityDigest =
|
|
88
|
+
digest ?? computeActivityDigest(events ?? [], options ?? {});
|
|
89
|
+
|
|
90
|
+
if (model.total === 0) {
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
className={className}
|
|
94
|
+
data-pyric-ui="activity-grid"
|
|
95
|
+
data-pyric-empty=""
|
|
96
|
+
>
|
|
97
|
+
{emptyState}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const renderRow = (row: ActivityRow): ReactNode => (
|
|
103
|
+
<ActivityGridRow
|
|
104
|
+
key={row.id}
|
|
105
|
+
row={row}
|
|
106
|
+
selected={row.id === selectedId}
|
|
107
|
+
onSelect={onSelect}
|
|
108
|
+
formatWhen={formatWhen}
|
|
109
|
+
now={now}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const renderBandBody = (band: ActivityBandWithGroups): ReactNode => {
|
|
114
|
+
const shown =
|
|
115
|
+
maxRowsPerBand !== undefined
|
|
116
|
+
? band.rows.slice(0, maxRowsPerBand)
|
|
117
|
+
: band.rows;
|
|
118
|
+
const hidden = band.count - shown.length;
|
|
119
|
+
|
|
120
|
+
// Grouped: render pivot buckets, preserving the band's row order
|
|
121
|
+
// within each. We re-bucket the *shown* rows so the display clamp
|
|
122
|
+
// and the pivot compose.
|
|
123
|
+
let body: ReactNode;
|
|
124
|
+
if (band.subgroups) {
|
|
125
|
+
const shownIds = new Set(shown.map((r) => r.id));
|
|
126
|
+
const buckets = band.subgroups
|
|
127
|
+
.map((g) => ({
|
|
128
|
+
...g,
|
|
129
|
+
rows: g.rows.filter((r) => shownIds.has(r.id)),
|
|
130
|
+
}))
|
|
131
|
+
.filter((g) => g.rows.length > 0);
|
|
132
|
+
body = buckets.map((g) => (
|
|
133
|
+
<div
|
|
134
|
+
key={g.key}
|
|
135
|
+
data-pyric-band-subgroup=""
|
|
136
|
+
data-pyric-subgroup-key={g.key}
|
|
137
|
+
data-pyric-subgroup-count={g.count}
|
|
138
|
+
>
|
|
139
|
+
{g.rows.map(renderRow)}
|
|
140
|
+
</div>
|
|
141
|
+
));
|
|
142
|
+
} else {
|
|
143
|
+
body = shown.map(renderRow);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div data-pyric-band-rows="">
|
|
148
|
+
{body}
|
|
149
|
+
{hidden > 0
|
|
150
|
+
? renderBandMore
|
|
151
|
+
? renderBandMore(band, hidden)
|
|
152
|
+
: (
|
|
153
|
+
<div data-pyric-band-more="">
|
|
154
|
+
{hidden} more {band.label.toLowerCase()}
|
|
155
|
+
</div>
|
|
156
|
+
)
|
|
157
|
+
: null}
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<div className={className} data-pyric-ui="activity-grid">
|
|
164
|
+
{showColumnHeader ? (
|
|
165
|
+
<div data-pyric-event-colhead="">
|
|
166
|
+
{COLUMN_HEADERS.map((h) => (
|
|
167
|
+
<span key={h} data-pyric-event-col={h}>
|
|
168
|
+
{h}
|
|
169
|
+
</span>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
) : null}
|
|
173
|
+
|
|
174
|
+
{model.bands.map((band) => (
|
|
175
|
+
<section
|
|
176
|
+
key={band.key}
|
|
177
|
+
data-pyric-band-section=""
|
|
178
|
+
data-pyric-band-key={band.key}
|
|
179
|
+
>
|
|
180
|
+
<header
|
|
181
|
+
data-pyric-band=""
|
|
182
|
+
data-pyric-band-key={band.key}
|
|
183
|
+
data-pyric-band-count={band.count}
|
|
184
|
+
data-pyric-band-denied={band.key === 'denied' ? '' : undefined}
|
|
185
|
+
>
|
|
186
|
+
<span data-pyric-band-label="">{band.label}</span>
|
|
187
|
+
<span data-pyric-band-n="">{band.count}</span>
|
|
188
|
+
{band.attribution ? (
|
|
189
|
+
<span data-pyric-band-attr="">{band.attribution}</span>
|
|
190
|
+
) : null}
|
|
191
|
+
</header>
|
|
192
|
+
{renderBandBody(band)}
|
|
193
|
+
</section>
|
|
194
|
+
))}
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ActivityRow } from '../digest.js';
|
|
3
|
+
import { defaultFormatWhen } from './format.js';
|
|
4
|
+
|
|
5
|
+
export interface ActivityGridRowProps {
|
|
6
|
+
row: ActivityRow;
|
|
7
|
+
/** Marks the row as the active selection (`data-pyric-selected`). */
|
|
8
|
+
selected?: boolean;
|
|
9
|
+
onSelect?: (row: ActivityRow) => void;
|
|
10
|
+
/** Override the `when` rendering. Receives `(at, now)`; default is the
|
|
11
|
+
* session-relative duration. */
|
|
12
|
+
formatWhen?: (at: number, now: number) => string;
|
|
13
|
+
/** The "now" anchor passed to `formatWhen` (and `row.when` is used as
|
|
14
|
+
* a fallback when omitted). */
|
|
15
|
+
now?: number;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* One activity grid row — the `target · change · for · lens · when`
|
|
21
|
+
* column contract from `c-result.html`, plus a trailing drill affordance
|
|
22
|
+
* column. Headless: every cell is a `data-pyric-event-*` span the host
|
|
23
|
+
* styles into the rigid column grid.
|
|
24
|
+
*
|
|
25
|
+
* Styling / data contract:
|
|
26
|
+
* - `[data-pyric-event-row]` — the row, with `data-pyric-event-band`,
|
|
27
|
+
* `data-pyric-event-service`, `data-pyric-event-lens`,
|
|
28
|
+
* `data-pyric-event-denied` (present only on denials), and
|
|
29
|
+
* `data-pyric-selected` when active.
|
|
30
|
+
* - `[data-pyric-event-target]` / `-change` / `-for` / `-lens` / `-when`
|
|
31
|
+
* — the five columns, in order.
|
|
32
|
+
*/
|
|
33
|
+
export function ActivityGridRow({
|
|
34
|
+
row,
|
|
35
|
+
selected,
|
|
36
|
+
onSelect,
|
|
37
|
+
formatWhen,
|
|
38
|
+
now,
|
|
39
|
+
className,
|
|
40
|
+
}: ActivityGridRowProps): ReactNode {
|
|
41
|
+
const when = formatWhen
|
|
42
|
+
? formatWhen(row.at, now ?? Date.now())
|
|
43
|
+
: row.when || defaultFormatWhen(row.at, now ?? Date.now());
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
onClick={() => onSelect?.(row)}
|
|
49
|
+
className={className}
|
|
50
|
+
data-pyric-event-row=""
|
|
51
|
+
data-pyric-event-band={row.band}
|
|
52
|
+
data-pyric-event-service={row.service}
|
|
53
|
+
data-pyric-event-lens={row.authLens.mode}
|
|
54
|
+
data-pyric-event-actor={row.actor.kind}
|
|
55
|
+
data-pyric-event-denied={row.denied ? '' : undefined}
|
|
56
|
+
data-pyric-selected={selected ? '' : undefined}
|
|
57
|
+
>
|
|
58
|
+
<span data-pyric-event-target="">{row.target}</span>
|
|
59
|
+
<span data-pyric-event-change="">{row.change}</span>
|
|
60
|
+
<span data-pyric-event-for="">{row.for}</span>
|
|
61
|
+
<span data-pyric-event-lens="">{row.lens}</span>
|
|
62
|
+
<span data-pyric-event-when="">{when}</span>
|
|
63
|
+
</button>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { useMemo, type ReactNode } from 'react';
|
|
2
|
+
import { truncateVectorsForDisplay } from '../../firestore/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One field-level change in a staged proposal. A UI-level diff row: the host
|
|
6
|
+
* adapts its backend diff (e.g. the sandbox's `Divergence[]`) into these so the
|
|
7
|
+
* component stays decoupled from any backend type.
|
|
8
|
+
*/
|
|
9
|
+
export interface FieldChange {
|
|
10
|
+
/** Full document path, e.g. `notes/abc123`. */
|
|
11
|
+
docPath: string;
|
|
12
|
+
/** The field that changed. */
|
|
13
|
+
field: string;
|
|
14
|
+
before: unknown;
|
|
15
|
+
after: unknown;
|
|
16
|
+
/** `added` = new field, `removed` = cleared, `changed` = value differs. */
|
|
17
|
+
kind: 'added' | 'changed' | 'removed';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* An auth user a staged proposal creates (a sign-in account, distinct from
|
|
22
|
+
* Firestore documents). The host adapts its backend request (e.g. a
|
|
23
|
+
* `CreateUserRequest`) into this UI-level shape.
|
|
24
|
+
*/
|
|
25
|
+
export interface CreatedAuthUser {
|
|
26
|
+
uid: string;
|
|
27
|
+
email?: string;
|
|
28
|
+
displayName?: string;
|
|
29
|
+
provider?: string;
|
|
30
|
+
emailVerified?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ProposedChangeDiffProps {
|
|
34
|
+
changes: FieldChange[];
|
|
35
|
+
/** Auth users the proposal creates, shown as a leading "auth users" group. */
|
|
36
|
+
authUsers?: readonly CreatedAuthUser[];
|
|
37
|
+
className?: string;
|
|
38
|
+
/** Rendered when there are no changes. Defaults to nothing. */
|
|
39
|
+
emptyState?: ReactNode;
|
|
40
|
+
/** Format a value for display. Default: JSON-ish, empty for `undefined`. */
|
|
41
|
+
formatValue?: (value: unknown) => ReactNode;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface DocGroup {
|
|
45
|
+
docPath: string;
|
|
46
|
+
docId: string;
|
|
47
|
+
fields: FieldChange[];
|
|
48
|
+
}
|
|
49
|
+
interface CollectionGroup {
|
|
50
|
+
collection: string;
|
|
51
|
+
docs: DocGroup[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function groupChanges(changes: FieldChange[]): CollectionGroup[] {
|
|
55
|
+
const byCollection = new Map<string, Map<string, FieldChange[]>>();
|
|
56
|
+
for (const change of changes) {
|
|
57
|
+
const slash = change.docPath.lastIndexOf('/');
|
|
58
|
+
const collection = slash > 0 ? change.docPath.slice(0, slash) : change.docPath;
|
|
59
|
+
let docs = byCollection.get(collection);
|
|
60
|
+
if (!docs) {
|
|
61
|
+
docs = new Map();
|
|
62
|
+
byCollection.set(collection, docs);
|
|
63
|
+
}
|
|
64
|
+
let fields = docs.get(change.docPath);
|
|
65
|
+
if (!fields) {
|
|
66
|
+
fields = [];
|
|
67
|
+
docs.set(change.docPath, fields);
|
|
68
|
+
}
|
|
69
|
+
fields.push(change);
|
|
70
|
+
}
|
|
71
|
+
return [...byCollection.entries()].map(([collection, docs]) => ({
|
|
72
|
+
collection,
|
|
73
|
+
docs: [...docs.entries()].map(([docPath, fields]) => ({
|
|
74
|
+
docPath,
|
|
75
|
+
docId: docPath.slice(docPath.lastIndexOf('/') + 1),
|
|
76
|
+
fields,
|
|
77
|
+
})),
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A one-line account summary: "alice@x.dev · Alice · verified". */
|
|
82
|
+
function formatAuthUser(u: CreatedAuthUser): string {
|
|
83
|
+
const parts = [u.email, u.displayName, u.provider, u.emailVerified ? 'verified' : null].filter(
|
|
84
|
+
Boolean,
|
|
85
|
+
);
|
|
86
|
+
return parts.length ? (parts as string[]).join(' · ') : 'new account';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function defaultFormatValue(value: unknown): string {
|
|
90
|
+
if (value === undefined) return '';
|
|
91
|
+
if (typeof value === 'string') return JSON.stringify(value);
|
|
92
|
+
try {
|
|
93
|
+
// Truncate vectors so a real embedding never dumps its full array here.
|
|
94
|
+
return JSON.stringify(truncateVectorsForDisplay(value));
|
|
95
|
+
} catch {
|
|
96
|
+
return String(value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Headless renderer for a staged change: the documents a proposal touches, with
|
|
102
|
+
* per-field before/after. Grouped by collection. Ships zero styling; the host
|
|
103
|
+
* styles the `data-pyric-*` contract (`proposed-change-diff` /
|
|
104
|
+
* `data-pyric-change-*`). The c-review diff grid is this, styled.
|
|
105
|
+
*/
|
|
106
|
+
export function ProposedChangeDiff({
|
|
107
|
+
changes,
|
|
108
|
+
authUsers = [],
|
|
109
|
+
className,
|
|
110
|
+
emptyState = null,
|
|
111
|
+
formatValue = defaultFormatValue,
|
|
112
|
+
}: ProposedChangeDiffProps) {
|
|
113
|
+
const groups = useMemo(() => groupChanges(changes), [changes]);
|
|
114
|
+
if (groups.length === 0 && authUsers.length === 0) return <>{emptyState}</>;
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className={className} data-pyric-ui="proposed-change-diff">
|
|
118
|
+
{authUsers.length > 0 ? (
|
|
119
|
+
<section data-pyric-change-group data-pyric-change-authgroup data-pyric-change-collection="auth users">
|
|
120
|
+
<header data-pyric-change-grouphead>
|
|
121
|
+
<span data-pyric-change-collname>auth users</span>
|
|
122
|
+
<span data-pyric-change-count>{authUsers.length}</span>
|
|
123
|
+
</header>
|
|
124
|
+
<ul data-pyric-change-docs>
|
|
125
|
+
{authUsers.map((user) => (
|
|
126
|
+
<li key={user.uid} data-pyric-change-doc data-pyric-change-authuser>
|
|
127
|
+
<span data-pyric-change-docid>{user.uid}</span>
|
|
128
|
+
<span data-pyric-change-authmeta>{formatAuthUser(user)}</span>
|
|
129
|
+
</li>
|
|
130
|
+
))}
|
|
131
|
+
</ul>
|
|
132
|
+
</section>
|
|
133
|
+
) : null}
|
|
134
|
+
{groups.map((group) => (
|
|
135
|
+
<section
|
|
136
|
+
key={group.collection}
|
|
137
|
+
data-pyric-change-group
|
|
138
|
+
data-pyric-change-collection={group.collection}
|
|
139
|
+
>
|
|
140
|
+
<header data-pyric-change-grouphead>
|
|
141
|
+
<span data-pyric-change-collname>{group.collection}</span>
|
|
142
|
+
<span data-pyric-change-count>{group.docs.length}</span>
|
|
143
|
+
</header>
|
|
144
|
+
<ul data-pyric-change-docs>
|
|
145
|
+
{group.docs.map((doc) => (
|
|
146
|
+
<li key={doc.docPath} data-pyric-change-doc>
|
|
147
|
+
<span data-pyric-change-docid>{doc.docId}</span>
|
|
148
|
+
<ul data-pyric-change-fields>
|
|
149
|
+
{doc.fields.map((field) => (
|
|
150
|
+
<li
|
|
151
|
+
key={field.field}
|
|
152
|
+
data-pyric-change-field
|
|
153
|
+
data-pyric-change-kind={field.kind}
|
|
154
|
+
>
|
|
155
|
+
<span data-pyric-change-key>{field.field}</span>
|
|
156
|
+
{field.kind !== 'added' ? (
|
|
157
|
+
<span data-pyric-change-before>{formatValue(field.before)}</span>
|
|
158
|
+
) : null}
|
|
159
|
+
<span data-pyric-change-arrow aria-hidden>
|
|
160
|
+
{'→'}
|
|
161
|
+
</span>
|
|
162
|
+
{field.kind !== 'removed' ? (
|
|
163
|
+
<span data-pyric-change-after>{formatValue(field.after)}</span>
|
|
164
|
+
) : null}
|
|
165
|
+
</li>
|
|
166
|
+
))}
|
|
167
|
+
</ul>
|
|
168
|
+
</li>
|
|
169
|
+
))}
|
|
170
|
+
</ul>
|
|
171
|
+
</section>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default `when`-column formatter: a session-relative duration
|
|
3
|
+
* (`now` / `12s` / `3m` / `1h`), matching the mock's `c-when` strings.
|
|
4
|
+
* The grid is session-scoped (see design-ideation "FRAME CORRECTION")
|
|
5
|
+
* so anchors are relative, never absolute calendar dates.
|
|
6
|
+
*
|
|
7
|
+
* Override via `<ActivityGrid formatWhen={...} />` when the host has a
|
|
8
|
+
* better clock anchor (e.g. a ticking "session now").
|
|
9
|
+
*/
|
|
10
|
+
export function defaultFormatWhen(at: number, now: number = Date.now()): string {
|
|
11
|
+
const deltaMs = Math.max(0, now - at);
|
|
12
|
+
const s = Math.floor(deltaMs / 1000);
|
|
13
|
+
if (s < 1) return 'now';
|
|
14
|
+
if (s < 60) return `${s}s`;
|
|
15
|
+
const m = Math.floor(s / 60);
|
|
16
|
+
if (m < 60) return `${m}m`;
|
|
17
|
+
const h = Math.floor(m / 60);
|
|
18
|
+
if (h < 24) return `${h}h`;
|
|
19
|
+
const d = Math.floor(h / 24);
|
|
20
|
+
return `${d}d`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { ActivityGrid, type ActivityGridProps } from './ActivityGrid.js';
|
|
2
|
+
export {
|
|
3
|
+
ActivityGridRow,
|
|
4
|
+
type ActivityGridRowProps,
|
|
5
|
+
} from './ActivityGridRow.js';
|
|
6
|
+
export {
|
|
7
|
+
ActivityActionItems,
|
|
8
|
+
type ActivityActionItemsProps,
|
|
9
|
+
type ActivityActionItem,
|
|
10
|
+
} from './ActivityActionItems.js';
|
|
11
|
+
export {
|
|
12
|
+
ProposedChangeDiff,
|
|
13
|
+
type ProposedChangeDiffProps,
|
|
14
|
+
type FieldChange,
|
|
15
|
+
type CreatedAuthUser,
|
|
16
|
+
} from './ProposedChangeDiff.js';
|
|
17
|
+
export { defaultFormatWhen } from './format.js';
|