@pyric/ui 0.1.0-alpha.10 → 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,328 @@
1
+ import { Fragment } from 'react';
2
+ import type { FormEvent, ReactNode } from 'react';
3
+ import { FEDERATED_PROVIDER_IDS } from 'pyric/auth';
4
+ import type { AuthUserRecord, CreateUserRequest, UpdateUserRequest } from 'pyric/auth';
5
+ import { useAuthUserEditor } from '../hooks/useAuthUserEditor.js';
6
+ import { providerLabel } from '../providers.js';
7
+ import { ClaimsField } from './ClaimsField.js';
8
+
9
+ /** Create-mode provider multi-select: one checkbox per federated provider
10
+ * id the sandbox supports (the canonical `FEDERATED_PROVIDER_IDS` set from
11
+ * `pyric/auth` — derived from the shipped provider classes, not a UI-side
12
+ * copy). A user can link one or several. Headless: style
13
+ * `[data-pyric-provider-checklist]` / `[data-pyric-provider-option]`. */
14
+ function ProviderChecklist({
15
+ selected,
16
+ onChange,
17
+ }: {
18
+ selected: string[];
19
+ onChange: (ids: string[]) => void;
20
+ }) {
21
+ return (
22
+ <div data-pyric-provider-checklist>
23
+ {FEDERATED_PROVIDER_IDS.map((id) => (
24
+ <label key={id} data-pyric-provider-option={id}>
25
+ <input
26
+ type="checkbox"
27
+ checked={selected.includes(id)}
28
+ onChange={(e) =>
29
+ onChange(
30
+ e.target.checked ? [...selected, id] : selected.filter((p) => p !== id),
31
+ )
32
+ }
33
+ />
34
+ <span data-pyric-provider-option-label>{providerLabel(id)}</span>
35
+ <span data-pyric-provider-option-id>{id}</span>
36
+ </label>
37
+ ))}
38
+ </div>
39
+ );
40
+ }
41
+
42
+ /** What `onSubmit` receives — discriminated on the form's mode. */
43
+ export type AuthUserFormSubmit =
44
+ | { mode: 'create'; request: CreateUserRequest }
45
+ | { mode: 'edit'; uid: string; request: UpdateUserRequest };
46
+
47
+ /** Field names the {@link AuthUserFormProps.renderField} slot receives,
48
+ * in render order. Claims is NOT a slot field — it stays the standalone
49
+ * `<ClaimsField>` (override it by composing `useAuthUserEditor`). */
50
+ export type AuthUserFormFieldName =
51
+ | 'email'
52
+ | 'password'
53
+ | 'display-name'
54
+ | 'phone-number'
55
+ | 'photo-url'
56
+ | 'providers'
57
+ | 'email-verified'
58
+ | 'disabled';
59
+
60
+ /** What the {@link AuthUserFormProps.renderField} slot receives per field. */
61
+ export interface AuthUserFormField {
62
+ name: AuthUserFormFieldName;
63
+ /** The visible label text the default rendering uses. */
64
+ label: string;
65
+ /** The wired, controlled input element (carries `data-pyric-field`).
66
+ * Place it anywhere; state/validation stay connected. */
67
+ input: ReactNode;
68
+ /** Current validation message for this field, or null. */
69
+ error: string | null;
70
+ /** `'text' | 'checkbox' | 'group'` — lets one slot impl branch on
71
+ * layout (`group` = a multi-control block, e.g. the provider
72
+ * checklist; create mode only). */
73
+ kind: 'text' | 'checkbox' | 'group';
74
+ /** The default rendering (label wrapper + label text + input + error).
75
+ * Call it to keep the stock layout for fields you don't customize. */
76
+ defaultRender: () => ReactNode;
77
+ }
78
+
79
+ export interface AuthUserFormProps {
80
+ /** Existing record → edit mode (delta payloads); omit → create mode. */
81
+ initial?: AuthUserRecord;
82
+ /** Receives the validated payload. Wire `create` to
83
+ * `useAuthUsers().createUser` and `edit` to `updateUser`. */
84
+ onSubmit: (submit: AuthUserFormSubmit) => void;
85
+ onCancel?: () => void;
86
+ submitLabel?: string;
87
+ cancelLabel?: string;
88
+ /** Extra content rendered before the action row (e.g. an error from a
89
+ * failed `createUser` call). */
90
+ children?: ReactNode;
91
+ className?: string;
92
+ /** Per-field layout override. Called for each field (see
93
+ * {@link AuthUserFormFieldName} order); return your own markup around
94
+ * `field.input`, or `field.defaultRender()` to keep the stock label
95
+ * wrapper for that field. Omit the prop for the default layout. */
96
+ renderField?: (field: AuthUserFormField) => ReactNode;
97
+ }
98
+
99
+ /**
100
+ * Headless add/edit-user form over `useAuthUserEditor` — the emulator
101
+ * UI's user dialog fields (email, password, display name, phone, photo
102
+ * URL, verified/disabled toggles, custom claims) with its validation
103
+ * messages. Zero CSS; structure addressable via `data-pyric-*`:
104
+ *
105
+ * - root `form[data-pyric-ui="auth-user-form"]` with `data-pyric-mode`,
106
+ * `data-pyric-is-dirty`, `data-pyric-is-valid` state attrs
107
+ * - every field (text inputs AND checkboxes) is wrapped in a
108
+ * `label[data-pyric-field-label="<name>"]` carrying a visible
109
+ * `span[data-pyric-label-text]` — labeled grid layouts are pure CSS
110
+ * (`display: grid` on the wrappers); label-less designs hide
111
+ * `[data-pyric-label-text]` and lean on the placeholders
112
+ * - inputs `[data-pyric-field="email" | "password" | "display-name" |
113
+ * "phone-number" | "photo-url" | "email-verified" | "disabled"]`
114
+ * - CREATE mode only: a "Sign-in providers" group
115
+ * (`fieldset[data-pyric-field-label="providers"]` wrapping
116
+ * `[data-pyric-provider-checklist]`) — one checkbox per federated
117
+ * provider the sandbox supports (`FEDERATED_PROVIDER_IDS` from
118
+ * `pyric/auth`; multiple selectable, entries land on
119
+ * `CreateUserRequest.providerUserInfo`)
120
+ * - claims via the standalone `<ClaimsField>`
121
+ * - per-field messages `[data-pyric-field-error="email" | "password"]`
122
+ * render INSIDE the field's label wrapper, after the input
123
+ * - `button[data-pyric-cancel]` / `button[data-pyric-submit]` (submit is
124
+ * disabled while invalid, or pristine in edit mode)
125
+ *
126
+ * Submit emits payloads only — no sandbox calls — so the same form works
127
+ * for create and edit and the consumer owns error handling.
128
+ */
129
+ export function AuthUserForm({
130
+ initial,
131
+ onSubmit,
132
+ onCancel,
133
+ submitLabel = 'Save',
134
+ cancelLabel = 'Cancel',
135
+ children,
136
+ className,
137
+ renderField,
138
+ }: AuthUserFormProps) {
139
+ const editor = useAuthUserEditor({ initial });
140
+ const mode = initial ? 'edit' : 'create';
141
+ const submittable = editor.isValid && (mode === 'create' || editor.isDirty);
142
+
143
+ /** Build one field descriptor: the wired input + the stock label-wrapper
144
+ * rendering. `renderField` consumers place `input` themselves. */
145
+ const field = (
146
+ name: AuthUserFormFieldName,
147
+ label: string,
148
+ kind: 'text' | 'checkbox' | 'group',
149
+ input: ReactNode,
150
+ error: string | null = null,
151
+ ): AuthUserFormField => ({
152
+ name,
153
+ label,
154
+ input,
155
+ error,
156
+ kind,
157
+ defaultRender: () =>
158
+ kind === 'checkbox' ? (
159
+ <label data-pyric-field-label={name} key={name}>
160
+ {input}
161
+ <span data-pyric-label-text>{label}</span>
162
+ </label>
163
+ ) : kind === 'group' ? (
164
+ <fieldset data-pyric-field-label={name} key={name}>
165
+ <legend data-pyric-label-text>{label}</legend>
166
+ {input}
167
+ </fieldset>
168
+ ) : (
169
+ <label data-pyric-field-label={name} key={name}>
170
+ <span data-pyric-label-text>{label}</span>
171
+ {input}
172
+ {error != null && (
173
+ <p role="alert" data-pyric-field-error={name}>
174
+ {error}
175
+ </p>
176
+ )}
177
+ </label>
178
+ ),
179
+ });
180
+
181
+ const fields: AuthUserFormField[] = [
182
+ field(
183
+ 'email',
184
+ 'Email',
185
+ 'text',
186
+ <input
187
+ type="email"
188
+ data-pyric-field="email"
189
+ placeholder="email@example.com"
190
+ value={editor.fields.email}
191
+ onChange={(e) => editor.setField('email', e.target.value)}
192
+ />,
193
+ editor.errors.email ?? null,
194
+ ),
195
+ field(
196
+ 'password',
197
+ 'Password',
198
+ 'text',
199
+ <input
200
+ type="password"
201
+ data-pyric-field="password"
202
+ placeholder={mode === 'edit' ? 'New password (unchanged if empty)' : 'Password'}
203
+ value={editor.fields.password}
204
+ onChange={(e) => editor.setField('password', e.target.value)}
205
+ />,
206
+ editor.errors.password ?? null,
207
+ ),
208
+ field(
209
+ 'display-name',
210
+ 'Display name',
211
+ 'text',
212
+ <input
213
+ type="text"
214
+ data-pyric-field="display-name"
215
+ placeholder="Display name (optional)"
216
+ value={editor.fields.displayName}
217
+ onChange={(e) => editor.setField('displayName', e.target.value)}
218
+ />,
219
+ ),
220
+ field(
221
+ 'phone-number',
222
+ 'Phone number',
223
+ 'text',
224
+ <input
225
+ type="tel"
226
+ data-pyric-field="phone-number"
227
+ placeholder="+1 555 555 0100"
228
+ value={editor.fields.phoneNumber}
229
+ onChange={(e) => editor.setField('phoneNumber', e.target.value)}
230
+ />,
231
+ ),
232
+ field(
233
+ 'photo-url',
234
+ 'Photo URL',
235
+ 'text',
236
+ <input
237
+ type="url"
238
+ data-pyric-field="photo-url"
239
+ placeholder="https://example.com/avatar.png"
240
+ value={editor.fields.photoUrl}
241
+ onChange={(e) => editor.setField('photoUrl', e.target.value)}
242
+ />,
243
+ ),
244
+ // Provider selection is CREATE-only: in edit mode linked providers are
245
+ // managed by the consumer's own affordance over `updateUser` (Studio's
246
+ // per-user ProvidersEditor), not the delta form.
247
+ ...(mode === 'create'
248
+ ? [
249
+ field(
250
+ 'providers',
251
+ 'Sign-in providers',
252
+ 'group',
253
+ <ProviderChecklist
254
+ selected={editor.fields.providerIds}
255
+ onChange={(ids) => editor.setField('providerIds', ids)}
256
+ />,
257
+ ),
258
+ ]
259
+ : []),
260
+ field(
261
+ 'email-verified',
262
+ 'Verified email',
263
+ 'checkbox',
264
+ <input
265
+ type="checkbox"
266
+ data-pyric-field="email-verified"
267
+ checked={editor.fields.emailVerified}
268
+ onChange={(e) => editor.setField('emailVerified', e.target.checked)}
269
+ />,
270
+ ),
271
+ field(
272
+ 'disabled',
273
+ 'Disabled (sign-in attempts are rejected)',
274
+ 'checkbox',
275
+ <input
276
+ type="checkbox"
277
+ data-pyric-field="disabled"
278
+ checked={editor.fields.disabled}
279
+ onChange={(e) => editor.setField('disabled', e.target.checked)}
280
+ />,
281
+ ),
282
+ ];
283
+
284
+ const submit = (e: FormEvent) => {
285
+ e.preventDefault();
286
+ if (!submittable) return;
287
+ if (mode === 'edit') {
288
+ onSubmit({ mode, uid: initial!.uid, request: editor.toUpdateRequest() });
289
+ } else {
290
+ onSubmit({ mode, request: editor.toCreateRequest() });
291
+ }
292
+ };
293
+
294
+ return (
295
+ <form
296
+ className={className}
297
+ data-pyric-ui="auth-user-form"
298
+ data-pyric-mode={mode}
299
+ data-pyric-is-dirty={editor.isDirty ? '' : undefined}
300
+ data-pyric-is-valid={editor.isValid ? '' : undefined}
301
+ onSubmit={submit}
302
+ >
303
+ {fields.map((f) =>
304
+ renderField ? (
305
+ <Fragment key={f.name}>{renderField(f)}</Fragment>
306
+ ) : (
307
+ f.defaultRender()
308
+ ),
309
+ )}
310
+ <ClaimsField
311
+ value={editor.fields.claimsText}
312
+ onChange={(text) => editor.setField('claimsText', text)}
313
+ error={editor.errors.claims}
314
+ />
315
+ {children}
316
+ <footer data-pyric-form-actions>
317
+ {onCancel ? (
318
+ <button type="button" data-pyric-cancel onClick={onCancel}>
319
+ {cancelLabel}
320
+ </button>
321
+ ) : null}
322
+ <button type="submit" data-pyric-submit disabled={!submittable}>
323
+ {submitLabel}
324
+ </button>
325
+ </footer>
326
+ </form>
327
+ );
328
+ }
@@ -0,0 +1,219 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { AuthUserRecord } from 'pyric/auth';
3
+ import { VirtualList } from '../../primitives/VirtualList.js';
4
+ import { providerLabel } from '../providers.js';
5
+
6
+ export interface AuthUserListProps {
7
+ /** Rows to render — usually `useAuthUsers().users`. */
8
+ users: AuthUserRecord[];
9
+ isLoading?: boolean;
10
+ error?: Error;
11
+ /** The active filter text. Distinguishes the "no users yet" zero state
12
+ * (empty filter) from "no results" (non-empty). */
13
+ filter?: string;
14
+ /** Fired when the identifier cell is clicked. When omitted, the
15
+ * identifier renders as plain text. */
16
+ onSelect?: (user: AuthUserRecord) => void;
17
+ /** Identifier-cell override. Default: email, else phone, else
18
+ * `anonymous`, else the uid. */
19
+ renderIdentifier?: (user: AuthUserRecord) => ReactNode;
20
+ /** Providers-cell override. Default: one `<span data-pyric-provider-id>`
21
+ * per linked provider with its text label (`anonymous` for anonymous
22
+ * users) — hook icons off the attribute. */
23
+ renderProviders?: (user: AuthUserRecord) => ReactNode;
24
+ /** Per-row selection control. Rendered in the leading cell so bulk
25
+ * selection stays visually separate from trailing row actions. */
26
+ renderSelection?: (user: AuthUserRecord) => ReactNode;
27
+ /** Optional content for the leading selection column header (for example,
28
+ * a select-all checkbox). Only rendered with `renderSelection`. */
29
+ renderSelectionHeader?: ReactNode;
30
+ /** Per-row action slot (edit / disable / delete menu). Rendered in a
31
+ * trailing cell; column header is added when this is provided. */
32
+ renderActions?: (user: AuthUserRecord) => ReactNode;
33
+ /** Optional content for the trailing actions column header (for example,
34
+ * a select-all checkbox). Only rendered with `renderActions`. */
35
+ renderActionsHeader?: ReactNode;
36
+ /** Timestamp formatter for Created. Default: locale date, em dash for null. */
37
+ formatCreatedAt?: (iso: string | null) => ReactNode;
38
+ /** Timestamp formatter for Signed In. Kept separate because a missing login
39
+ * means "never", while a missing/invalid creation time is malformed data. */
40
+ formatLastLoginAt?: (iso: string | null) => ReactNode;
41
+ /** Zero state when the project has no users at all. */
42
+ emptyState?: ReactNode;
43
+ /** Zero state when the filter matches nothing. */
44
+ noResultsState?: ReactNode;
45
+ className?: string;
46
+ /** Above this row count, rows render through `<VirtualList>`.
47
+ * Default 100. `Infinity` disables. */
48
+ virtualizeThreshold?: number;
49
+ /** Estimated row height when virtualizing. Default 44. */
50
+ rowHeight?: number | ((index: number) => number);
51
+ /** Scroll-container height when virtualized. Default `'60vh'`. */
52
+ virtualizedHeight?: number | string;
53
+ }
54
+
55
+ function defaultIdentifier(user: AuthUserRecord): ReactNode {
56
+ return user.email ?? user.phoneNumber ?? (user.isAnonymous ? 'anonymous' : user.uid);
57
+ }
58
+
59
+ function defaultProviders(user: AuthUserRecord): ReactNode {
60
+ if (user.isAnonymous && user.providerUserInfo.length === 0) {
61
+ return <span data-pyric-provider-id="anonymous">{providerLabel('anonymous')}</span>;
62
+ }
63
+ return user.providerUserInfo.map((p) => (
64
+ <span key={p.providerId} data-pyric-provider-id={p.providerId}>
65
+ {providerLabel(p.providerId)}
66
+ </span>
67
+ ));
68
+ }
69
+
70
+ function defaultFormatDate(iso: string | null): ReactNode {
71
+ if (!iso) return '—';
72
+ const d = new Date(iso);
73
+ return Number.isNaN(d.getTime()) ? '—' : d.toLocaleDateString();
74
+ }
75
+
76
+ /**
77
+ * Headless users table — the emulator UI's columns (Identifier,
78
+ * Provider, Created, Signed In, User UID, actions) over the
79
+ * `data-pyric-*` styling contract, with role-based table semantics so
80
+ * rows can virtualize (a real `<table>` can't wrap a scroll container).
81
+ *
82
+ * The hook (`useAuthUsers`) owns data + filter state; this component
83
+ * just renders. Disabled accounts carry `data-pyric-user-disabled` for
84
+ * dimmed styling.
85
+ */
86
+ export function AuthUserList({
87
+ users,
88
+ isLoading,
89
+ error,
90
+ filter,
91
+ onSelect,
92
+ renderIdentifier,
93
+ renderProviders,
94
+ renderSelection,
95
+ renderSelectionHeader,
96
+ renderActions,
97
+ renderActionsHeader,
98
+ formatCreatedAt = defaultFormatDate,
99
+ formatLastLoginAt = defaultFormatDate,
100
+ emptyState,
101
+ noResultsState,
102
+ className,
103
+ virtualizeThreshold = 100,
104
+ rowHeight = 44,
105
+ virtualizedHeight = '60vh',
106
+ }: AuthUserListProps) {
107
+ if (error) {
108
+ return (
109
+ <div className={className} data-pyric-ui="auth-user-list" data-pyric-error="" role="alert">
110
+ {error.message}
111
+ </div>
112
+ );
113
+ }
114
+ if (isLoading && users.length === 0) {
115
+ return <div className={className} data-pyric-ui="auth-user-list" data-pyric-loading="" />;
116
+ }
117
+ if (users.length === 0) {
118
+ const filtered = Boolean(filter?.trim());
119
+ return (
120
+ <div
121
+ className={className}
122
+ data-pyric-ui="auth-user-list"
123
+ data-pyric-empty=""
124
+ data-pyric-no-results={filtered ? '' : undefined}
125
+ >
126
+ {filtered
127
+ ? (noResultsState ?? 'No results')
128
+ : (emptyState ?? 'No users for this project yet')}
129
+ </div>
130
+ );
131
+ }
132
+
133
+ const virtualized = users.length > virtualizeThreshold;
134
+
135
+ const row = (user: AuthUserRecord) => (
136
+ <div
137
+ key={user.uid}
138
+ role="row"
139
+ data-pyric-user-entry
140
+ data-pyric-user-uid={user.uid}
141
+ data-pyric-user-disabled={user.disabled ? '' : undefined}
142
+ >
143
+ {renderSelection ? (
144
+ <span role="cell" data-pyric-user-cell="selection">
145
+ {renderSelection(user)}
146
+ </span>
147
+ ) : null}
148
+ <span role="cell" data-pyric-user-cell="identifier">
149
+ {onSelect ? (
150
+ <button type="button" data-pyric-user-select onClick={() => onSelect(user)}>
151
+ {renderIdentifier ? renderIdentifier(user) : defaultIdentifier(user)}
152
+ </button>
153
+ ) : (
154
+ (renderIdentifier ?? defaultIdentifier)(user)
155
+ )}
156
+ </span>
157
+ <span role="cell" data-pyric-user-cell="providers">
158
+ {(renderProviders ?? defaultProviders)(user)}
159
+ </span>
160
+ <span role="cell" data-pyric-user-cell="created">
161
+ {formatCreatedAt(user.createdAt)}
162
+ </span>
163
+ <span role="cell" data-pyric-user-cell="signed-in">
164
+ {formatLastLoginAt(user.lastLoginAt)}
165
+ </span>
166
+ <span role="cell" data-pyric-user-cell="uid">
167
+ {user.uid}
168
+ </span>
169
+ {renderActions ? (
170
+ <span role="cell" data-pyric-user-cell="actions">
171
+ {renderActions(user)}
172
+ </span>
173
+ ) : null}
174
+ </div>
175
+ );
176
+
177
+ return (
178
+ <div
179
+ className={className}
180
+ role="table"
181
+ aria-label="Authentication users"
182
+ data-pyric-ui="auth-user-list"
183
+ data-pyric-virtualized={virtualized ? '' : undefined}
184
+ data-pyric-selection={renderSelection ? '' : undefined}
185
+ data-pyric-actions={renderActions ? '' : undefined}
186
+ >
187
+ <div role="row" data-pyric-user-header>
188
+ {renderSelection ? (
189
+ <span role="columnheader" data-pyric-user-cell="selection" aria-label="Selection">
190
+ {renderSelectionHeader}
191
+ </span>
192
+ ) : null}
193
+ <span role="columnheader" data-pyric-user-cell="identifier">Identifier</span>
194
+ <span role="columnheader" data-pyric-user-cell="providers">Provider</span>
195
+ <span role="columnheader" data-pyric-user-cell="created">Created</span>
196
+ <span role="columnheader" data-pyric-user-cell="signed-in">Signed In</span>
197
+ <span role="columnheader" data-pyric-user-cell="uid">User UID</span>
198
+ {renderActions ? (
199
+ <span role="columnheader" data-pyric-user-cell="actions" aria-label="Actions">
200
+ {renderActionsHeader}
201
+ </span>
202
+ ) : null}
203
+ </div>
204
+ {virtualized ? (
205
+ <VirtualList
206
+ items={users}
207
+ estimateSize={rowHeight}
208
+ height={virtualizedHeight}
209
+ getItemKey={(user) => user.uid}
210
+ renderItem={row}
211
+ />
212
+ ) : (
213
+ <div role="rowgroup" data-pyric-user-rows>
214
+ {users.map(row)}
215
+ </div>
216
+ )}
217
+ </div>
218
+ );
219
+ }
@@ -0,0 +1,50 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export interface ClaimsFieldProps {
4
+ /** Raw claims JSON text. */
5
+ value: string;
6
+ onChange: (text: string) => void;
7
+ /** Validation message (from `validateSerializedClaims` /
8
+ * `useAuthUserEditor().errors.claims`). Renders a `role="alert"`
9
+ * paragraph and marks the textarea invalid. */
10
+ error?: string;
11
+ /** Helper text under the field (rules-usage hint by default). */
12
+ hint?: ReactNode;
13
+ placeholder?: string;
14
+ className?: string;
15
+ }
16
+
17
+ /**
18
+ * Headless custom-claims textarea — the emulator UI's
19
+ * `customAttributes` control. Standalone so custom forms can reuse the
20
+ * exact field (the playground's sign-in helper and the user form both
21
+ * render one); validation itself lives in `validateSerializedClaims`.
22
+ */
23
+ export function ClaimsField({
24
+ value,
25
+ onChange,
26
+ error,
27
+ hint,
28
+ placeholder = '{"role":"admin"}',
29
+ className,
30
+ }: ClaimsFieldProps) {
31
+ return (
32
+ <div className={className} data-pyric-ui="claims-field">
33
+ <textarea
34
+ data-pyric-field="claims"
35
+ data-pyric-claims-invalid={error != null ? '' : undefined}
36
+ aria-label="Custom claims (optional)"
37
+ aria-invalid={error != null || undefined}
38
+ placeholder={placeholder}
39
+ value={value}
40
+ onChange={(e) => onChange(e.target.value)}
41
+ />
42
+ {error != null && (
43
+ <p role="alert" data-pyric-claims-error>
44
+ {error}
45
+ </p>
46
+ )}
47
+ {hint != null && <p data-pyric-claims-hint>{hint}</p>}
48
+ </div>
49
+ );
50
+ }