@pyric/ui 0.1.0-alpha.11 → 0.1.0-alpha.13
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,58 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { inferType } from '../types.js';
|
|
3
|
+
import { FieldRenderer } from './FieldRenderer.js';
|
|
4
|
+
import type { FieldEditorRegistry } from '../fieldEditors/types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One row of the read-only value tree. A scalar renders `key: value` inline; a
|
|
8
|
+
* map/array renders a disclosure chevron + key that toggles its nested tree
|
|
9
|
+
* (collapsible, default expanded). Shared by the map + array displays so both
|
|
10
|
+
* lists and objects get the same chevron behavior.
|
|
11
|
+
*/
|
|
12
|
+
export function TreeEntry({
|
|
13
|
+
label,
|
|
14
|
+
value,
|
|
15
|
+
path,
|
|
16
|
+
fieldEditors,
|
|
17
|
+
kind,
|
|
18
|
+
}: {
|
|
19
|
+
label: string;
|
|
20
|
+
value: unknown;
|
|
21
|
+
path: string;
|
|
22
|
+
fieldEditors: FieldEditorRegistry;
|
|
23
|
+
kind: 'key' | 'index';
|
|
24
|
+
}) {
|
|
25
|
+
const type = inferType(value);
|
|
26
|
+
const nested = type === 'map' || type === 'array';
|
|
27
|
+
const [expanded, setExpanded] = useState(true);
|
|
28
|
+
const labelAttr = kind === 'key' ? { 'data-pyric-tree-key': '' } : { 'data-pyric-tree-index': '' };
|
|
29
|
+
// Stable targeting hook for the entry (map key / array index).
|
|
30
|
+
const entryAttr = kind === 'key' ? { 'data-field-name': label } : { 'data-field-index': label };
|
|
31
|
+
|
|
32
|
+
if (!nested) {
|
|
33
|
+
return (
|
|
34
|
+
<li data-pyric-tree-entry {...entryAttr}>
|
|
35
|
+
<span data-pyric-tree-chevron data-pyric-tree-chevron-empty aria-hidden="true" />
|
|
36
|
+
<span {...labelAttr}>{label}</span>
|
|
37
|
+
<FieldRenderer value={value} path={path} fieldEditors={fieldEditors} />
|
|
38
|
+
</li>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<li data-pyric-tree-entry data-pyric-tree-nested {...entryAttr}>
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
data-pyric-tree-toggle
|
|
47
|
+
aria-expanded={expanded}
|
|
48
|
+
onClick={() => setExpanded((e) => !e)}
|
|
49
|
+
>
|
|
50
|
+
<span data-pyric-tree-chevron aria-hidden="true">
|
|
51
|
+
›
|
|
52
|
+
</span>
|
|
53
|
+
<span {...labelAttr}>{label}</span>
|
|
54
|
+
</button>
|
|
55
|
+
{expanded ? <FieldRenderer value={value} path={path} fieldEditors={fieldEditors} /> : null}
|
|
56
|
+
</li>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import type { DocumentReference } from 'pyric/firestore';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cross-cutting display context threaded into individual field
|
|
6
|
+
* editors. Today it carries:
|
|
7
|
+
*
|
|
8
|
+
* - `onReferenceClick` — fires when the reference Display
|
|
9
|
+
* component is activated. Consumers wire navigation here.
|
|
10
|
+
*
|
|
11
|
+
* Read by the per-type Display components via `useDisplayContext`.
|
|
12
|
+
* Set by `<DocumentPreview>`'s wrapper. Editors that don't need it
|
|
13
|
+
* (string, number, …) simply ignore the context.
|
|
14
|
+
*/
|
|
15
|
+
export interface DisplayContextValue {
|
|
16
|
+
onReferenceClick?: (ref: DocumentReference) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const Ctx = createContext<DisplayContextValue>({});
|
|
20
|
+
|
|
21
|
+
export const DisplayContextProvider = Ctx.Provider;
|
|
22
|
+
|
|
23
|
+
export function useDisplayContext(): DisplayContextValue {
|
|
24
|
+
return useContext(Ctx);
|
|
25
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { TreeEntry } from '../components/TreeEntry.js';
|
|
2
|
+
import type { FieldEditorContract, FieldDisplayProps } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read-only array renderer as an indented tree: each element is `index value`
|
|
6
|
+
* inline for scalars, or a collapsible disclosure (chevron + index) whose
|
|
7
|
+
* nested tree indents below for maps/arrays (so arrays-of-maps render through
|
|
8
|
+
* the same recursion). {@link TreeEntry} owns the collapse state + chevron.
|
|
9
|
+
*/
|
|
10
|
+
function ArrayDisplay({ value, path, fieldEditors }: FieldDisplayProps<unknown[]>) {
|
|
11
|
+
return (
|
|
12
|
+
<ul data-pyric-field-type="array" data-pyric-tree data-pyric-field-path={path}>
|
|
13
|
+
{value.map((item, i) => (
|
|
14
|
+
<TreeEntry
|
|
15
|
+
key={i}
|
|
16
|
+
label={String(i)}
|
|
17
|
+
value={item}
|
|
18
|
+
path={path ? `${path}[${i}]` : `[${i}]`}
|
|
19
|
+
fieldEditors={fieldEditors ?? {}}
|
|
20
|
+
kind="index"
|
|
21
|
+
/>
|
|
22
|
+
))}
|
|
23
|
+
</ul>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const arrayEditor: FieldEditorContract<unknown[]> = {
|
|
28
|
+
type: 'array',
|
|
29
|
+
Display: ArrayDisplay,
|
|
30
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { FieldEditorContract, FieldDisplayProps, FieldEditProps } from './types.js';
|
|
2
|
+
|
|
3
|
+
function BooleanDisplay({ value, path }: FieldDisplayProps<boolean>) {
|
|
4
|
+
return (
|
|
5
|
+
<span
|
|
6
|
+
data-pyric-field-type="boolean"
|
|
7
|
+
data-pyric-field-path={path}
|
|
8
|
+
data-value={value ? 'true' : 'false'}
|
|
9
|
+
>
|
|
10
|
+
{value ? 'true' : 'false'}
|
|
11
|
+
</span>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function BooleanEdit({ value, onChange, path }: FieldEditProps<boolean>) {
|
|
16
|
+
return (
|
|
17
|
+
<span
|
|
18
|
+
data-pyric-field-type="boolean"
|
|
19
|
+
data-pyric-field-path={path}
|
|
20
|
+
data-value={value ? 'true' : 'false'}
|
|
21
|
+
>
|
|
22
|
+
<select
|
|
23
|
+
value={value ? 'true' : 'false'}
|
|
24
|
+
onChange={(e) => onChange(e.target.value === 'true')}
|
|
25
|
+
aria-label="Boolean value"
|
|
26
|
+
data-pyric-boolean-select
|
|
27
|
+
>
|
|
28
|
+
<option value="true">true</option>
|
|
29
|
+
<option value="false">false</option>
|
|
30
|
+
</select>
|
|
31
|
+
</span>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const booleanEditor: FieldEditorContract<boolean> = {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
Display: BooleanDisplay,
|
|
38
|
+
Edit: BooleanEdit,
|
|
39
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Bytes } from 'pyric/firestore';
|
|
2
|
+
import type { FieldEditorContract, FieldDisplayProps, FieldEditProps } from './types.js';
|
|
3
|
+
|
|
4
|
+
function BytesDisplay({ value, path }: FieldDisplayProps<Bytes>) {
|
|
5
|
+
const b64 = value.toBase64();
|
|
6
|
+
return (
|
|
7
|
+
<code
|
|
8
|
+
data-pyric-field-type="bytes"
|
|
9
|
+
data-pyric-field-path={path}
|
|
10
|
+
data-byte-length={String(value.toUint8Array().byteLength)}
|
|
11
|
+
>
|
|
12
|
+
{b64}
|
|
13
|
+
</code>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Base64 textarea. Anything that's not legal base64 produces a
|
|
19
|
+
* thrown error when passed to `Bytes.fromBase64String`; we swallow
|
|
20
|
+
* it and leave the old value in place. The validator surfaces the
|
|
21
|
+
* stale state by not flagging it (the underlying Bytes is still
|
|
22
|
+
* valid), so consumers wanting stricter feedback need to swap the
|
|
23
|
+
* editor.
|
|
24
|
+
*/
|
|
25
|
+
function BytesEdit({ value, onChange, error, path }: FieldEditProps<Bytes>) {
|
|
26
|
+
return (
|
|
27
|
+
<label
|
|
28
|
+
data-pyric-field-type="bytes"
|
|
29
|
+
data-pyric-field-path={path}
|
|
30
|
+
data-pyric-error={error ? '' : undefined}
|
|
31
|
+
>
|
|
32
|
+
<textarea
|
|
33
|
+
value={value.toBase64()}
|
|
34
|
+
onChange={(e) => {
|
|
35
|
+
try {
|
|
36
|
+
onChange(Bytes.fromBase64String(e.target.value));
|
|
37
|
+
} catch {
|
|
38
|
+
// Invalid base64 — keep the previous value. The user
|
|
39
|
+
// sees the bad text in the textarea until they fix it.
|
|
40
|
+
}
|
|
41
|
+
}}
|
|
42
|
+
aria-invalid={error ? 'true' : undefined}
|
|
43
|
+
/>
|
|
44
|
+
{error ? <span data-pyric-error-message>{error}</span> : null}
|
|
45
|
+
</label>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const bytesEditor: FieldEditorContract<Bytes> = {
|
|
50
|
+
type: 'bytes',
|
|
51
|
+
Display: BytesDisplay,
|
|
52
|
+
Edit: BytesEdit,
|
|
53
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { GeoPoint } from 'pyric/firestore';
|
|
2
|
+
import type { FieldEditorContract, FieldDisplayProps, FieldEditProps } from './types.js';
|
|
3
|
+
|
|
4
|
+
function GeoPointDisplay({ value, path }: FieldDisplayProps<GeoPoint>) {
|
|
5
|
+
return (
|
|
6
|
+
<span
|
|
7
|
+
data-pyric-field-type="geopoint"
|
|
8
|
+
data-pyric-field-path={path}
|
|
9
|
+
data-lat={String(value.latitude)}
|
|
10
|
+
data-lng={String(value.longitude)}
|
|
11
|
+
>
|
|
12
|
+
{value.latitude}, {value.longitude}
|
|
13
|
+
</span>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function GeoPointEdit({ value, onChange, error, path }: FieldEditProps<GeoPoint>) {
|
|
18
|
+
return (
|
|
19
|
+
<span
|
|
20
|
+
data-pyric-field-type="geopoint"
|
|
21
|
+
data-pyric-field-path={path}
|
|
22
|
+
data-pyric-error={error ? '' : undefined}
|
|
23
|
+
>
|
|
24
|
+
<label>
|
|
25
|
+
<input
|
|
26
|
+
type="number"
|
|
27
|
+
step="any"
|
|
28
|
+
min={-90}
|
|
29
|
+
max={90}
|
|
30
|
+
value={value.latitude}
|
|
31
|
+
onChange={(e) => {
|
|
32
|
+
const lat = parseFloat(e.target.value);
|
|
33
|
+
try {
|
|
34
|
+
onChange(new GeoPoint(lat, value.longitude));
|
|
35
|
+
} catch {
|
|
36
|
+
// GeoPoint constructor throws on out-of-range. Leave
|
|
37
|
+
// the previous value in place; the validator will
|
|
38
|
+
// surface the issue on next render.
|
|
39
|
+
}
|
|
40
|
+
}}
|
|
41
|
+
aria-label="Latitude"
|
|
42
|
+
/>
|
|
43
|
+
</label>
|
|
44
|
+
<label>
|
|
45
|
+
<input
|
|
46
|
+
type="number"
|
|
47
|
+
step="any"
|
|
48
|
+
min={-180}
|
|
49
|
+
max={180}
|
|
50
|
+
value={value.longitude}
|
|
51
|
+
onChange={(e) => {
|
|
52
|
+
const lng = parseFloat(e.target.value);
|
|
53
|
+
try {
|
|
54
|
+
onChange(new GeoPoint(value.latitude, lng));
|
|
55
|
+
} catch {
|
|
56
|
+
// see above
|
|
57
|
+
}
|
|
58
|
+
}}
|
|
59
|
+
aria-label="Longitude"
|
|
60
|
+
/>
|
|
61
|
+
</label>
|
|
62
|
+
{error ? <span data-pyric-error-message>{error}</span> : null}
|
|
63
|
+
</span>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const geopointEditor: FieldEditorContract<GeoPoint> = {
|
|
68
|
+
type: 'geopoint',
|
|
69
|
+
Display: GeoPointDisplay,
|
|
70
|
+
Edit: GeoPointEdit,
|
|
71
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { TreeEntry } from '../components/TreeEntry.js';
|
|
2
|
+
import type { FieldEditorContract, FieldDisplayProps } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read-only map renderer as an indented tree (not a key/value grid): each entry
|
|
6
|
+
* is `key: value` inline for scalars, or a collapsible disclosure (chevron +
|
|
7
|
+
* key) whose nested tree indents below for maps/arrays. {@link TreeEntry} owns
|
|
8
|
+
* the per-node collapse state + chevron.
|
|
9
|
+
*/
|
|
10
|
+
function MapDisplay({ value, path, fieldEditors }: FieldDisplayProps<Record<string, unknown>>) {
|
|
11
|
+
// Lexicographic key order so fields render in a stable sequence regardless of
|
|
12
|
+
// JS object enumeration order (matches firebase-tools-ui).
|
|
13
|
+
const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
|
|
14
|
+
return (
|
|
15
|
+
<ul data-pyric-field-type="map" data-pyric-tree data-pyric-field-path={path}>
|
|
16
|
+
{entries.map(([key, childValue]) => (
|
|
17
|
+
<TreeEntry
|
|
18
|
+
key={key}
|
|
19
|
+
label={key}
|
|
20
|
+
value={childValue}
|
|
21
|
+
path={path ? `${path}.${key}` : key}
|
|
22
|
+
fieldEditors={fieldEditors ?? {}}
|
|
23
|
+
kind="key"
|
|
24
|
+
/>
|
|
25
|
+
))}
|
|
26
|
+
</ul>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const mapEditor: FieldEditorContract<Record<string, unknown>> = {
|
|
31
|
+
type: 'map',
|
|
32
|
+
Display: MapDisplay,
|
|
33
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FieldEditorContract, FieldDisplayProps, FieldEditProps } from './types.js';
|
|
2
|
+
|
|
3
|
+
function NullDisplay({ path }: FieldDisplayProps<null>) {
|
|
4
|
+
return (
|
|
5
|
+
<span data-pyric-field-type="null" data-pyric-field-path={path}>
|
|
6
|
+
null
|
|
7
|
+
</span>
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// `null` has no editable surface — the user changes the type if they
|
|
12
|
+
// want a value. The Edit component renders a placeholder so the
|
|
13
|
+
// editor still emits a slot at the right path. Consumers style it
|
|
14
|
+
// however they like (e.g. ghost text).
|
|
15
|
+
function NullEdit({ path }: FieldEditProps<null>) {
|
|
16
|
+
return (
|
|
17
|
+
<span data-pyric-field-type="null" data-pyric-field-path={path}>
|
|
18
|
+
null
|
|
19
|
+
</span>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const nullEditor: FieldEditorContract<null> = {
|
|
24
|
+
type: 'null',
|
|
25
|
+
Display: NullDisplay,
|
|
26
|
+
Edit: NullEdit,
|
|
27
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { FieldEditorContract, FieldDisplayProps, FieldEditProps } from './types.js';
|
|
2
|
+
|
|
3
|
+
function NumberDisplay({ value, path }: FieldDisplayProps<number>) {
|
|
4
|
+
return (
|
|
5
|
+
<span data-pyric-field-type="number" data-pyric-field-path={path}>
|
|
6
|
+
{String(value)}
|
|
7
|
+
</span>
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function NumberEdit({ value, onChange, error, path }: FieldEditProps<number>) {
|
|
12
|
+
return (
|
|
13
|
+
<label
|
|
14
|
+
data-pyric-field-type="number"
|
|
15
|
+
data-pyric-field-path={path}
|
|
16
|
+
data-pyric-error={error ? '' : undefined}
|
|
17
|
+
>
|
|
18
|
+
<input
|
|
19
|
+
type="number"
|
|
20
|
+
// Render NaN as empty so the input doesn't show literally
|
|
21
|
+
// "NaN" — the underlying value still carries it until the
|
|
22
|
+
// user types something valid.
|
|
23
|
+
value={Number.isFinite(value) ? value : ''}
|
|
24
|
+
onChange={(e) => {
|
|
25
|
+
const raw = e.target.value;
|
|
26
|
+
// Empty input commits NaN so the reducer's validator
|
|
27
|
+
// flags it; this keeps the editor's UI in sync with the
|
|
28
|
+
// underlying value rather than swallowing the change.
|
|
29
|
+
const parsed = raw === '' ? Number.NaN : parseFloat(raw);
|
|
30
|
+
onChange(parsed);
|
|
31
|
+
}}
|
|
32
|
+
aria-invalid={error ? 'true' : undefined}
|
|
33
|
+
/>
|
|
34
|
+
{error ? <span data-pyric-error-message>{error}</span> : null}
|
|
35
|
+
</label>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const numberEditor: FieldEditorContract<number> = {
|
|
40
|
+
type: 'number',
|
|
41
|
+
Display: NumberDisplay,
|
|
42
|
+
Edit: NumberEdit,
|
|
43
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { DocumentReference } from 'pyric/firestore';
|
|
2
|
+
import type { FieldEditorContract, FieldDisplayProps, FieldEditProps } from './types.js';
|
|
3
|
+
import { useDisplayContext } from '../components/context.js';
|
|
4
|
+
|
|
5
|
+
function ReferenceDisplay({ value, path }: FieldDisplayProps<DocumentReference>) {
|
|
6
|
+
// Clickable when a consumer has wired `onReferenceClick` via the
|
|
7
|
+
// `<DocumentPreview onReferenceClick={…}>` prop. Otherwise renders
|
|
8
|
+
// as plain text — staying headless and avoiding fake interactivity.
|
|
9
|
+
const { onReferenceClick } = useDisplayContext();
|
|
10
|
+
if (onReferenceClick) {
|
|
11
|
+
return (
|
|
12
|
+
<button
|
|
13
|
+
type="button"
|
|
14
|
+
onClick={() => onReferenceClick(value)}
|
|
15
|
+
data-pyric-field-type="reference"
|
|
16
|
+
data-pyric-field-path={path}
|
|
17
|
+
data-target-path={value.path}
|
|
18
|
+
data-pyric-clickable=""
|
|
19
|
+
>
|
|
20
|
+
{value.path}
|
|
21
|
+
</button>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return (
|
|
25
|
+
<span
|
|
26
|
+
data-pyric-field-type="reference"
|
|
27
|
+
data-pyric-field-path={path}
|
|
28
|
+
data-target-path={value.path}
|
|
29
|
+
>
|
|
30
|
+
{value.path}
|
|
31
|
+
</span>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Text-input reference editor. M3 only — the M5 `<ReferencePicker>`
|
|
37
|
+
* replaces this with a popover that browses the data plane. The
|
|
38
|
+
* editor commits a "ref-shaped" stand-in object (matching the
|
|
39
|
+
* structural check in `inferType` + `validateLeaf`) so the value
|
|
40
|
+
* flows through the reducer without needing a live Firestore handle
|
|
41
|
+
* at this layer.
|
|
42
|
+
*/
|
|
43
|
+
function ReferenceEdit({ value, onChange, error, path }: FieldEditProps<DocumentReference>) {
|
|
44
|
+
return (
|
|
45
|
+
<label
|
|
46
|
+
data-pyric-field-type="reference"
|
|
47
|
+
data-pyric-field-path={path}
|
|
48
|
+
data-pyric-error={error ? '' : undefined}
|
|
49
|
+
>
|
|
50
|
+
<input
|
|
51
|
+
type="text"
|
|
52
|
+
value={value.path}
|
|
53
|
+
placeholder="users/alice"
|
|
54
|
+
onChange={(e) => {
|
|
55
|
+
const nextPath = e.target.value;
|
|
56
|
+
const segments = nextPath.split('/').filter(Boolean);
|
|
57
|
+
const id = segments[segments.length - 1] ?? '';
|
|
58
|
+
// Build a ref-shaped stand-in. Real refs are constructed
|
|
59
|
+
// by `<ReferencePicker>` (M5) once it can talk to a
|
|
60
|
+
// Firestore handle.
|
|
61
|
+
// Preserve the existing firestore handle if the current
|
|
62
|
+
// value already carries one (so it can be wired back when
|
|
63
|
+
// the M5 picker arrives). Cast through `unknown` because
|
|
64
|
+
// the pyric/firestore type union doesn't expose
|
|
65
|
+
// `.firestore` uniformly across chainable + modular.
|
|
66
|
+
const existingFirestore =
|
|
67
|
+
(value as unknown as { firestore?: unknown }).firestore ?? {};
|
|
68
|
+
const stand = {
|
|
69
|
+
path: nextPath,
|
|
70
|
+
id,
|
|
71
|
+
firestore: existingFirestore,
|
|
72
|
+
type: 'document',
|
|
73
|
+
} as unknown as DocumentReference;
|
|
74
|
+
onChange(stand);
|
|
75
|
+
}}
|
|
76
|
+
aria-invalid={error ? 'true' : undefined}
|
|
77
|
+
/>
|
|
78
|
+
{error ? <span data-pyric-error-message>{error}</span> : null}
|
|
79
|
+
</label>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const referenceEditor: FieldEditorContract<DocumentReference> = {
|
|
84
|
+
type: 'reference',
|
|
85
|
+
Display: ReferenceDisplay,
|
|
86
|
+
Edit: ReferenceEdit,
|
|
87
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { stringEditor } from './string.js';
|
|
2
|
+
import { numberEditor } from './number.js';
|
|
3
|
+
import { booleanEditor } from './boolean.js';
|
|
4
|
+
import { nullEditor } from './null.js';
|
|
5
|
+
import { timestampEditor } from './timestamp.js';
|
|
6
|
+
import { geopointEditor } from './geopoint.js';
|
|
7
|
+
import { referenceEditor } from './reference.js';
|
|
8
|
+
import { bytesEditor } from './bytes.js';
|
|
9
|
+
import { mapEditor } from './map.js';
|
|
10
|
+
import { arrayEditor } from './array.js';
|
|
11
|
+
import { vectorEditor } from './vector.js';
|
|
12
|
+
import type { FieldEditorRegistry } from './types.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default registry. Covers every {@link FieldType}. Consumers extend
|
|
16
|
+
* or override by passing their own (partial) registry into
|
|
17
|
+
* `<DocumentPreview>` / `<FieldRenderer>` — the components merge
|
|
18
|
+
* overrides into these defaults so a consumer can swap just one
|
|
19
|
+
* editor without re-declaring the rest.
|
|
20
|
+
*/
|
|
21
|
+
export const defaultFieldEditors: FieldEditorRegistry = {
|
|
22
|
+
string: stringEditor,
|
|
23
|
+
number: numberEditor,
|
|
24
|
+
boolean: booleanEditor,
|
|
25
|
+
null: nullEditor,
|
|
26
|
+
timestamp: timestampEditor,
|
|
27
|
+
geopoint: geopointEditor,
|
|
28
|
+
reference: referenceEditor,
|
|
29
|
+
bytes: bytesEditor,
|
|
30
|
+
map: mapEditor,
|
|
31
|
+
array: arrayEditor,
|
|
32
|
+
vector: vectorEditor,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Merge a consumer-supplied registry over the defaults. `undefined`
|
|
37
|
+
* input returns the defaults as-is. Used internally by
|
|
38
|
+
* `<DocumentPreview>` so consumers don't have to spread manually.
|
|
39
|
+
*/
|
|
40
|
+
export function mergeFieldEditors(
|
|
41
|
+
override: FieldEditorRegistry | undefined,
|
|
42
|
+
): FieldEditorRegistry {
|
|
43
|
+
if (!override) return defaultFieldEditors;
|
|
44
|
+
return { ...defaultFieldEditors, ...override };
|
|
45
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { FieldEditorContract, FieldDisplayProps, FieldEditProps } from './types.js';
|
|
2
|
+
|
|
3
|
+
function StringDisplay({ value, path }: FieldDisplayProps<string>) {
|
|
4
|
+
return (
|
|
5
|
+
<span data-pyric-field-type="string" data-pyric-field-path={path}>
|
|
6
|
+
{value}
|
|
7
|
+
</span>
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function StringEdit({ value, onChange, error, path }: FieldEditProps<string>) {
|
|
12
|
+
return (
|
|
13
|
+
<label
|
|
14
|
+
data-pyric-field-type="string"
|
|
15
|
+
data-pyric-field-path={path}
|
|
16
|
+
data-pyric-error={error ? '' : undefined}
|
|
17
|
+
>
|
|
18
|
+
<input
|
|
19
|
+
type="text"
|
|
20
|
+
value={value}
|
|
21
|
+
onChange={(e) => onChange(e.target.value)}
|
|
22
|
+
aria-invalid={error ? 'true' : undefined}
|
|
23
|
+
/>
|
|
24
|
+
{error ? <span data-pyric-error-message>{error}</span> : null}
|
|
25
|
+
</label>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const stringEditor: FieldEditorContract<string> = {
|
|
30
|
+
type: 'string',
|
|
31
|
+
Display: StringDisplay,
|
|
32
|
+
Edit: StringEdit,
|
|
33
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Timestamp } from 'pyric/firestore';
|
|
2
|
+
import type { FieldEditorContract, FieldDisplayProps, FieldEditProps } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Coerce a Timestamp value to a Date. Real instances use `.toDate()`; a value
|
|
6
|
+
* that crossed a worker / postMessage boundary arrives as a plain
|
|
7
|
+
* `{ seconds, nanoseconds }` (or `{ _seconds, _nanoseconds }`) and is rebuilt
|
|
8
|
+
* here, so the display + editor work in both in-process and served modes.
|
|
9
|
+
*/
|
|
10
|
+
function coerceDate(value: unknown): Date {
|
|
11
|
+
if (value instanceof Timestamp) return value.toDate();
|
|
12
|
+
const o = value as Record<string, number> | null;
|
|
13
|
+
const seconds = o ? (o.seconds ?? o._seconds) : undefined;
|
|
14
|
+
const nanoseconds = o ? (o.nanoseconds ?? o._nanoseconds ?? 0) : 0;
|
|
15
|
+
if (typeof seconds === 'number') return new Date(seconds * 1000 + Math.floor(nanoseconds / 1e6));
|
|
16
|
+
return new Date(NaN);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function TimestampDisplay({ value, path }: FieldDisplayProps<Timestamp>) {
|
|
20
|
+
const date = coerceDate(value);
|
|
21
|
+
const iso = Number.isNaN(date.getTime()) ? '' : date.toISOString();
|
|
22
|
+
return (
|
|
23
|
+
<time
|
|
24
|
+
dateTime={iso}
|
|
25
|
+
data-pyric-field-type="timestamp"
|
|
26
|
+
data-pyric-field-path={path}
|
|
27
|
+
>
|
|
28
|
+
{iso}
|
|
29
|
+
</time>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Native `<input type="datetime-local">` for editing. The element
|
|
35
|
+
* speaks local time (no zone offset); we convert to/from a UTC
|
|
36
|
+
* `Timestamp` at the boundary so the underlying value stays
|
|
37
|
+
* timezone-correct.
|
|
38
|
+
*/
|
|
39
|
+
function TimestampEdit({ value, onChange, error, path }: FieldEditProps<Timestamp>) {
|
|
40
|
+
// Convert the value to the `YYYY-MM-DDTHH:MM` shape <input
|
|
41
|
+
// datetime-local> wants. Seconds and millis aren't part of the
|
|
42
|
+
// native input precision; round-trips through this editor lose
|
|
43
|
+
// sub-minute resolution intentionally — consumers needing higher
|
|
44
|
+
// fidelity should swap the editor out via the registry.
|
|
45
|
+
const dt = coerceDate(value);
|
|
46
|
+
const local = new Date(dt.getTime() - dt.getTimezoneOffset() * 60_000);
|
|
47
|
+
const inputValue = Number.isNaN(local.getTime()) ? '' : local.toISOString().slice(0, 16);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<label
|
|
51
|
+
data-pyric-field-type="timestamp"
|
|
52
|
+
data-pyric-field-path={path}
|
|
53
|
+
data-pyric-error={error ? '' : undefined}
|
|
54
|
+
>
|
|
55
|
+
<input
|
|
56
|
+
type="datetime-local"
|
|
57
|
+
value={inputValue}
|
|
58
|
+
onChange={(e) => {
|
|
59
|
+
const next = e.target.value;
|
|
60
|
+
if (!next) return;
|
|
61
|
+
const localDate = new Date(next);
|
|
62
|
+
onChange(Timestamp.fromDate(localDate));
|
|
63
|
+
}}
|
|
64
|
+
aria-invalid={error ? 'true' : undefined}
|
|
65
|
+
/>
|
|
66
|
+
{error ? <span data-pyric-error-message>{error}</span> : null}
|
|
67
|
+
</label>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const timestampEditor: FieldEditorContract<Timestamp> = {
|
|
72
|
+
type: 'timestamp',
|
|
73
|
+
Display: TimestampDisplay,
|
|
74
|
+
Edit: TimestampEdit,
|
|
75
|
+
};
|