@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.
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,139 @@
1
+ import { useState } from 'react';
2
+ import type { MetricPoint, MetricSeries } from '../hooks/useTrafficMetrics.js';
3
+
4
+ export interface TrafficLineChartProps {
5
+ points: readonly MetricPoint[];
6
+ series: readonly MetricSeries[];
7
+ /** Series keys currently drawn. Omit to draw every series. Pair with
8
+ * {@link TrafficMetricCards}' `visible`/`onToggle` so the legend cards
9
+ * and the chart's lines toggle in lockstep. */
10
+ visible?: ReadonlySet<string>;
11
+ /** Omit all-zero series from the plot and tooltip while retaining their
12
+ * explicit total in the accompanying metric strip. */
13
+ omitZeroSeries?: boolean;
14
+ formatValue?: (n: number) => string;
15
+ formatTime?: (t: number) => string;
16
+ emptyState?: React.ReactNode;
17
+ className?: string;
18
+ }
19
+
20
+ const defaultFormatValue = (n: number) => String(n);
21
+ const defaultFormatTime = (t: number) => new Date(t).toLocaleTimeString();
22
+
23
+ /** Hand-rolled SVG line chart, no charting dependency — plain lines over
24
+ * a 0..100 viewBox so CSS drives the actual size (intrinsic layout, no
25
+ * fixed pixel chart). Each series draws only when `visible` includes
26
+ * its key (or `visible` is omitted). The y-scale is the max value
27
+ * across the currently VISIBLE series only, so toggling a tall series
28
+ * off rescales the rest up — matching the Console reference.
29
+ *
30
+ * Interaction: hovering (or focusing, via the invisible per-bucket hit
31
+ * targets) shows a tooltip with the bucket's time range and each visible
32
+ * series' value at that bucket — `[data-pyric-chart-tooltip]`, positioned
33
+ * via `--pyric-hover-x`.
34
+ *
35
+ * Styling hooks: `[data-pyric-ui="traffic-line-chart"]`,
36
+ * `[data-pyric-chart-svg]`, `[data-pyric-chart-line]` (with
37
+ * `data-pyric-series-key`, `data-pyric-series-index` — the same index a
38
+ * `TrafficMetricCards` card carries, so line + card colors line up),
39
+ * `[data-pyric-chart-hit]` (with `data-pyric-point-index`),
40
+ * `[data-pyric-chart-tooltip]`, `[data-pyric-tooltip-row]`.
41
+ */
42
+ export function TrafficLineChart({
43
+ points,
44
+ series,
45
+ visible,
46
+ omitZeroSeries = false,
47
+ formatValue = defaultFormatValue,
48
+ formatTime = defaultFormatTime,
49
+ emptyState,
50
+ className,
51
+ }: TrafficLineChartProps) {
52
+ const [hovered, setHovered] = useState<number | null>(null);
53
+
54
+ if (points.length === 0) {
55
+ return (
56
+ <div className={className} data-pyric-ui="traffic-line-chart" data-pyric-empty="">
57
+ {emptyState}
58
+ </div>
59
+ );
60
+ }
61
+
62
+ const visibleSeries = series.filter(
63
+ (s) => (!visible || visible.has(s.key)) && (!omitZeroSeries || s.total > 0),
64
+ );
65
+ let maxValue = 0;
66
+ for (const s of visibleSeries) for (const v of s.values) if (v > maxValue) maxValue = v;
67
+ const scale = maxValue === 0 ? 1 : maxValue;
68
+
69
+ const n = points.length;
70
+ const xAt = (i: number) => (n === 1 ? 50 : (i / (n - 1)) * 100);
71
+ const yAt = (v: number) => 100 - (v / scale) * 100;
72
+
73
+ const hoveredPoint = hovered != null ? points[hovered] : null;
74
+
75
+ return (
76
+ <div className={className} data-pyric-ui="traffic-line-chart">
77
+ <div data-pyric-chart-svg="">
78
+ <svg viewBox="0 0 100 100" preserveAspectRatio="none" role="img" aria-label="metrics over time">
79
+ {series.map((s) => {
80
+ const seriesIndex = series.indexOf(s);
81
+ const isVisible =
82
+ (!visible || visible.has(s.key)) && (!omitZeroSeries || s.total > 0);
83
+ if (!isVisible) return null;
84
+ const d = s.values.map((v, i) => `${i === 0 ? 'M' : 'L'}${xAt(i)},${yAt(v)}`).join(' ');
85
+ return (
86
+ <path
87
+ key={s.key}
88
+ data-pyric-chart-line=""
89
+ data-pyric-series-key={s.key}
90
+ data-pyric-series-index={seriesIndex}
91
+ d={d}
92
+ fill="none"
93
+ vectorEffect="non-scaling-stroke"
94
+ />
95
+ );
96
+ })}
97
+ </svg>
98
+
99
+ <div data-pyric-chart-hits="">
100
+ {points.map((point, i) => (
101
+ <button
102
+ key={point.index}
103
+ type="button"
104
+ data-pyric-chart-hit=""
105
+ data-pyric-point-index={i}
106
+ onMouseEnter={() => setHovered(i)}
107
+ onFocus={() => setHovered(i)}
108
+ onMouseLeave={() => setHovered((cur) => (cur === i ? null : cur))}
109
+ onBlur={() => setHovered((cur) => (cur === i ? null : cur))}
110
+ aria-label={`${formatTime(point.start)} to ${formatTime(point.end)}`}
111
+ />
112
+ ))}
113
+ </div>
114
+
115
+ {hoveredPoint && (
116
+ <div
117
+ data-pyric-chart-tooltip=""
118
+ style={{ '--pyric-hover-x': xAt(hoveredPoint.index) } as React.CSSProperties}
119
+ >
120
+ <div data-pyric-tooltip-time="">
121
+ {formatTime(hoveredPoint.start)} – {formatTime(hoveredPoint.end)}
122
+ </div>
123
+ {visibleSeries.map((s) => (
124
+ <div
125
+ key={s.key}
126
+ data-pyric-tooltip-row=""
127
+ data-pyric-series-key={s.key}
128
+ data-pyric-series-index={series.indexOf(s)}
129
+ >
130
+ <span data-pyric-tooltip-label="">{s.label}</span>
131
+ <span data-pyric-tooltip-value="">{formatValue(s.values[hoveredPoint.index])}</span>
132
+ </div>
133
+ ))}
134
+ </div>
135
+ )}
136
+ </div>
137
+ </div>
138
+ );
139
+ }
@@ -0,0 +1,175 @@
1
+ import type { ReactNode } from 'react';
2
+ import { VirtualList } from '../../primitives/VirtualList.js';
3
+ import type { TrafficEvent } from '../types.js';
4
+ import type { TrafficLogItem } from '../hooks/useTrafficGroups.js';
5
+ import { TrafficRow } from './TrafficRow.js';
6
+ import { TrafficGroupRow } from './TrafficGroupRow.js';
7
+
8
+ export interface TrafficLogProps {
9
+ /** Events to render, in display order. The hook layer decides the
10
+ * order — `<TrafficLog>` renders the array as given. */
11
+ events: TrafficEvent[];
12
+ /**
13
+ * Grouped items from `useTrafficGroups`. When provided, the log
14
+ * renders singles + collapsible group rows and does NOT
15
+ * virtualize — grouping is itself the volume reducer (a 250-event
16
+ * listener storm becomes one group row). `events` is ignored.
17
+ */
18
+ items?: TrafficLogItem[];
19
+ /** The selected event's id, marked with `data-pyric-selected`. */
20
+ selectedId?: string;
21
+ onSelect?: (event: TrafficEvent) => void;
22
+ /** Passed through to each `<TrafficRow>`. */
23
+ renderClassification?: (event: TrafficEvent) => ReactNode;
24
+ /** Passed through to each `<TrafficRow>`. */
25
+ formatTime?: (at: number) => string;
26
+ /**
27
+ * Full escape hatch — render a row yourself instead of the default
28
+ * `<TrafficRow>`. Receives the event and its selected state.
29
+ * Applies to the flat (`events`) path only.
30
+ */
31
+ renderRow?: (event: TrafficEvent, selected: boolean) => ReactNode;
32
+ emptyState?: ReactNode;
33
+ className?: string;
34
+ /**
35
+ * Above this row count, the list virtualizes via `<VirtualList>`.
36
+ * Default 100 — a `load-test`-shaped session can emit 100k+ events,
37
+ * so virtualization is load-bearing, not polish.
38
+ */
39
+ virtualizeThreshold?: number;
40
+ /** Estimated row height when virtualizing. Default 28. */
41
+ rowHeight?: number | ((index: number) => number);
42
+ /** Pixel height the virtualized scroll container fills. Default
43
+ * `'60vh'`. */
44
+ virtualizedHeight?: number | string;
45
+ }
46
+
47
+ /**
48
+ * Headless traffic log — a Chrome DevTools Network-panel-style event
49
+ * stream. Below `virtualizeThreshold` it renders a plain `<ul>`;
50
+ * above it, TanStack-Virtual via `<VirtualList>`.
51
+ *
52
+ * Styling hooks: `[data-pyric-ui="traffic-log"]`,
53
+ * `[data-pyric-traffic-entry]` (the list item wrapper), plus the
54
+ * `<TrafficRow>` hooks.
55
+ */
56
+ export function TrafficLog({
57
+ events,
58
+ items,
59
+ selectedId,
60
+ onSelect,
61
+ renderClassification,
62
+ formatTime,
63
+ renderRow,
64
+ emptyState,
65
+ className,
66
+ virtualizeThreshold = 100,
67
+ rowHeight = 28,
68
+ virtualizedHeight = '60vh',
69
+ }: TrafficLogProps) {
70
+ // Grouped mode — singles + collapsible group rows, no virtualization.
71
+ if (items !== undefined) {
72
+ if (items.length === 0) {
73
+ return (
74
+ <div
75
+ className={className}
76
+ data-pyric-ui="traffic-log"
77
+ data-pyric-empty=""
78
+ >
79
+ {emptyState}
80
+ </div>
81
+ );
82
+ }
83
+ return (
84
+ <div className={className} data-pyric-ui="traffic-log" data-pyric-grouped="">
85
+ <ul data-pyric-traffic-log-items="">
86
+ {items.map((item) =>
87
+ item.type === 'group' ? (
88
+ <li key={item.key} data-pyric-traffic-group-entry="">
89
+ <TrafficGroupRow
90
+ group={item}
91
+ onSelect={onSelect}
92
+ selectedId={selectedId}
93
+ renderClassification={renderClassification}
94
+ formatTime={formatTime}
95
+ />
96
+ </li>
97
+ ) : (
98
+ <li
99
+ key={item.event.id}
100
+ data-pyric-traffic-entry=""
101
+ data-pyric-traffic-id={item.event.id}
102
+ >
103
+ <TrafficRow
104
+ event={item.event}
105
+ selected={item.event.id === selectedId}
106
+ onSelect={onSelect}
107
+ renderClassification={renderClassification}
108
+ formatTime={formatTime}
109
+ />
110
+ </li>
111
+ ),
112
+ )}
113
+ </ul>
114
+ </div>
115
+ );
116
+ }
117
+
118
+ if (events.length === 0) {
119
+ return (
120
+ <div className={className} data-pyric-ui="traffic-log" data-pyric-empty="">
121
+ {emptyState}
122
+ </div>
123
+ );
124
+ }
125
+
126
+ const row = (event: TrafficEvent): ReactNode => {
127
+ const selected = event.id === selectedId;
128
+ if (renderRow) return renderRow(event, selected);
129
+ return (
130
+ <TrafficRow
131
+ event={event}
132
+ selected={selected}
133
+ onSelect={onSelect}
134
+ renderClassification={renderClassification}
135
+ formatTime={formatTime}
136
+ />
137
+ );
138
+ };
139
+
140
+ const virtualized = events.length > virtualizeThreshold;
141
+
142
+ return (
143
+ <div
144
+ className={className}
145
+ data-pyric-ui="traffic-log"
146
+ data-pyric-virtualized={virtualized ? '' : undefined}
147
+ >
148
+ {virtualized ? (
149
+ <VirtualList
150
+ items={events}
151
+ estimateSize={rowHeight}
152
+ height={virtualizedHeight}
153
+ getItemKey={(event) => event.id}
154
+ renderItem={(event) => (
155
+ <div data-pyric-traffic-entry="" data-pyric-traffic-id={event.id}>
156
+ {row(event)}
157
+ </div>
158
+ )}
159
+ />
160
+ ) : (
161
+ <ul data-pyric-traffic-log-items="">
162
+ {events.map((event) => (
163
+ <li
164
+ key={event.id}
165
+ data-pyric-traffic-entry=""
166
+ data-pyric-traffic-id={event.id}
167
+ >
168
+ {row(event)}
169
+ </li>
170
+ ))}
171
+ </ul>
172
+ )}
173
+ </div>
174
+ );
175
+ }
@@ -0,0 +1,77 @@
1
+ import type { MetricSeries } from '../hooks/useTrafficMetrics.js';
2
+
3
+ export interface TrafficMetricCardsProps {
4
+ series: readonly MetricSeries[];
5
+ /** When supplied (with `onToggle`), each card gets a checkbox and
6
+ * doubles as the chart's legend — Console reference semantics
7
+ * ("toggle series visibility"). Omit for a read-only totals strip. */
8
+ visible?: ReadonlySet<string>;
9
+ onToggle?: (key: string) => void;
10
+ formatValue?: (n: number) => string;
11
+ className?: string;
12
+ }
13
+
14
+ const defaultFormatValue = (n: number) => String(n);
15
+
16
+ /**
17
+ * The period-total strip: one card per series, each showing the series'
18
+ * total for the current window. Doubles as BOTH the chart's legend row
19
+ * (pass `visible` + `onToggle` to get the checkbox-toggle semantics) AND
20
+ * the standalone fallback presentation when a chart isn't warranted —
21
+ * this component never depends on {@link TrafficLineChart}.
22
+ *
23
+ * Styling hooks: `[data-pyric-ui="traffic-metric-cards"]`,
24
+ * `[data-pyric-metric-card]` (with `data-pyric-metric-key`,
25
+ * `data-pyric-series-index` — the color channel a chart's lines also
26
+ * key off of, so a card and its line always match), `[data-pyric-metric-hidden]`
27
+ * when toggled off.
28
+ */
29
+ export function TrafficMetricCards({
30
+ series,
31
+ visible,
32
+ onToggle,
33
+ formatValue = defaultFormatValue,
34
+ className,
35
+ }: TrafficMetricCardsProps) {
36
+ const toggleable = visible !== undefined && onToggle !== undefined;
37
+ return (
38
+ <div className={className} data-pyric-ui="traffic-metric-cards" role={toggleable ? 'group' : undefined}>
39
+ {series.map((s, index) => {
40
+ const isVisible = visible ? visible.has(s.key) : true;
41
+ const card = (
42
+ <>
43
+ <span data-pyric-metric-swatch="" aria-hidden="true" />
44
+ <span data-pyric-metric-label="">{s.label}</span>
45
+ <span data-pyric-metric-value="">{formatValue(s.total)}</span>
46
+ </>
47
+ );
48
+ return toggleable ? (
49
+ <label
50
+ key={s.key}
51
+ data-pyric-metric-card=""
52
+ data-pyric-metric-key={s.key}
53
+ data-pyric-series-index={index}
54
+ data-pyric-metric-hidden={isVisible ? undefined : ''}
55
+ >
56
+ <input
57
+ type="checkbox"
58
+ checked={isVisible}
59
+ onChange={() => onToggle!(s.key)}
60
+ data-pyric-metric-checkbox=""
61
+ />
62
+ {card}
63
+ </label>
64
+ ) : (
65
+ <span
66
+ key={s.key}
67
+ data-pyric-metric-card=""
68
+ data-pyric-metric-key={s.key}
69
+ data-pyric-series-index={index}
70
+ >
71
+ {card}
72
+ </span>
73
+ );
74
+ })}
75
+ </div>
76
+ );
77
+ }
@@ -0,0 +1,69 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Badge } from '../../primitives/Badge.js';
3
+ import type { TrafficEvent } from '../types.js';
4
+ import { defaultFormatTime } from './format.js';
5
+
6
+ export interface TrafficRowProps {
7
+ event: TrafficEvent;
8
+ /** Marks the row as the active selection (`data-pyric-selected`). */
9
+ selected?: boolean;
10
+ onSelect?: (event: TrafficEvent) => void;
11
+ /**
12
+ * Render-prop slot for a consumer-specific classification badge —
13
+ * e.g. the playground's `expected`/`ambiguous`/`unexpected`
14
+ * verdict, which is app-source analysis the library doesn't own.
15
+ * Returns `null` to render nothing.
16
+ */
17
+ renderClassification?: (event: TrafficEvent) => ReactNode;
18
+ /** Override the timestamp rendering. Default is `HH:MM:SS`. */
19
+ formatTime?: (at: number) => string;
20
+ className?: string;
21
+ }
22
+
23
+ /**
24
+ * One headless traffic row — timestamp, method badge, path, and an
25
+ * optional consumer classification slot. The raw result is exposed as
26
+ * `data-pyric-result` on the row (for tinting/filtering); rendering a
27
+ * verdict/outcome label is the consumer's job via
28
+ * `renderClassification` — the row itself draws no result chip, so a
29
+ * consumer verdict never collides with a built-in one. Latency is
30
+ * intentionally absent: this is a local simulator, so `evalMs` lives
31
+ * in `<TrafficDetail>` only.
32
+ *
33
+ * Styling hooks:
34
+ * - `[data-pyric-traffic-row]` — the row button, with
35
+ * `data-pyric-result`, `data-pyric-origin`, `data-pyric-method`
36
+ * - `[data-pyric-traffic-row][data-pyric-selected]` — active row
37
+ * - `[data-pyric-traffic-time]` / `[data-pyric-traffic-path]`
38
+ * - `[data-pyric-traffic-service]` — the service label (firestore / rtdb /
39
+ * storage / auth), rendered even when unknown (empty) so fixed-width
40
+ * styling keeps the columns aligned
41
+ * - method renders as `<Badge>` (`data-pyric-badge-kind`)
42
+ */
43
+ export function TrafficRow({
44
+ event,
45
+ selected,
46
+ onSelect,
47
+ renderClassification,
48
+ formatTime = defaultFormatTime,
49
+ className,
50
+ }: TrafficRowProps) {
51
+ return (
52
+ <button
53
+ type="button"
54
+ onClick={() => onSelect?.(event)}
55
+ className={className}
56
+ data-pyric-traffic-row=""
57
+ data-pyric-result={event.result}
58
+ data-pyric-origin={event.origin}
59
+ data-pyric-method={event.method}
60
+ data-pyric-selected={selected ? '' : undefined}
61
+ >
62
+ <span data-pyric-traffic-time="">{formatTime(event.at)}</span>
63
+ <span data-pyric-traffic-service={event.service ?? ''}>{event.service ?? ''}</span>
64
+ <Badge kind={event.method}>{event.method}</Badge>
65
+ <span data-pyric-traffic-path="">{event.path || '/'}</span>
66
+ {renderClassification ? renderClassification(event) : null}
67
+ </button>
68
+ );
69
+ }
@@ -0,0 +1,73 @@
1
+ import type { TrafficStatsSummary } from '../hooks/useTrafficStats.js';
2
+
3
+ export interface TrafficStatsProps {
4
+ stats: TrafficStatsSummary;
5
+ className?: string;
6
+ }
7
+
8
+ function BucketList({
9
+ label,
10
+ buckets,
11
+ }: {
12
+ label: string;
13
+ buckets: TrafficStatsSummary['byMethod'];
14
+ }) {
15
+ return (
16
+ <div data-pyric-stat-group="" data-pyric-stat-group-label={label}>
17
+ <h4 data-pyric-stat-group-heading="">{label}</h4>
18
+ <ul data-pyric-stat-buckets="">
19
+ {buckets.map((b) => (
20
+ <li key={b.key} data-pyric-stat-bucket="" data-pyric-stat-key={b.key}>
21
+ <span data-pyric-stat-bucket-label="">{b.key}</span>
22
+ <span data-pyric-stat-bucket-count="">{b.count}</span>
23
+ </li>
24
+ ))}
25
+ </ul>
26
+ </div>
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Headless aggregation panel. Renders the totals, the deny rate, and
32
+ * count breakdowns by method / origin / path. The deny rate is
33
+ * exposed both as text and as a `--pyric-deny-rate` CSS custom
34
+ * property on the root for a proportional meter.
35
+ *
36
+ * Styling hooks: `[data-pyric-ui="traffic-stats"]`,
37
+ * `[data-pyric-stat]` (with `data-pyric-stat-key`),
38
+ * `[data-pyric-stat-group]` (with `data-pyric-stat-group-label`),
39
+ * `[data-pyric-stat-bucket]`.
40
+ */
41
+ export function TrafficStats({ stats, className }: TrafficStatsProps) {
42
+ return (
43
+ <div
44
+ className={className}
45
+ data-pyric-ui="traffic-stats"
46
+ style={{ '--pyric-deny-rate': stats.denyRate } as React.CSSProperties}
47
+ >
48
+ <div data-pyric-stat-totals="">
49
+ <span data-pyric-stat="" data-pyric-stat-key="total">
50
+ <span data-pyric-stat-label="">total</span>
51
+ <span data-pyric-stat-value="">{stats.total}</span>
52
+ </span>
53
+ <span data-pyric-stat="" data-pyric-stat-key="allows">
54
+ <span data-pyric-stat-label="">allowed</span>
55
+ <span data-pyric-stat-value="">{stats.allows}</span>
56
+ </span>
57
+ <span data-pyric-stat="" data-pyric-stat-key="denies">
58
+ <span data-pyric-stat-label="">denied</span>
59
+ <span data-pyric-stat-value="">{stats.denies}</span>
60
+ </span>
61
+ <span data-pyric-stat="" data-pyric-stat-key="deny-rate">
62
+ <span data-pyric-stat-label="">deny rate</span>
63
+ <span data-pyric-stat-value="">
64
+ {(stats.denyRate * 100).toFixed(0)}%
65
+ </span>
66
+ </span>
67
+ </div>
68
+ <BucketList label="method" buckets={stats.byMethod} />
69
+ <BucketList label="origin" buckets={stats.byOrigin} />
70
+ <BucketList label="path" buckets={stats.byPath} />
71
+ </div>
72
+ );
73
+ }