proto-plugin 0.1.0

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 (209) hide show
  1. package/AGENTS.md +350 -0
  2. package/README.md +279 -0
  3. package/components.json +18 -0
  4. package/package.json +73 -0
  5. package/scripts/download-mobbin-reference-images.mjs +112 -0
  6. package/scripts/lib/host-config.example.sh +48 -0
  7. package/scripts/lib/host-config.sh +25 -0
  8. package/scripts/lib/resolve-host-root.mjs +18 -0
  9. package/scripts/lib/source-path.sh +49 -0
  10. package/scripts/link-source-db.sh +128 -0
  11. package/scripts/link-source.sh +40 -0
  12. package/scripts/publish-package.sh +147 -0
  13. package/scripts/share-prototype.sh +279 -0
  14. package/scripts/sync-from-source.sh +74 -0
  15. package/scripts/verify-prototype-component-ids.mjs +151 -0
  16. package/scripts/verify-prototype-preview-states.mjs +135 -0
  17. package/src/app.ts +4 -0
  18. package/src/components/platform-ui/code-block.tsx +21 -0
  19. package/src/components/platform-ui/controls-panel-options.tsx +71 -0
  20. package/src/components/platform-ui/controls-panel-select.tsx +438 -0
  21. package/src/components/platform-ui/empty-state.tsx +15 -0
  22. package/src/components/platform-ui/icon-button.tsx +18 -0
  23. package/src/components/platform-ui/panel-select.tsx +123 -0
  24. package/src/components/platform-ui/panel.tsx +97 -0
  25. package/src/components/platform-ui/review-dropdown-menu.tsx +35 -0
  26. package/src/components/platform-ui/sidebar.tsx +293 -0
  27. package/src/components/platform-ui/toolbar-icon-button.tsx +34 -0
  28. package/src/components/platform-ui/toolbar.tsx +37 -0
  29. package/src/components/prototype-gallery-client.tsx +57 -0
  30. package/src/components/prototype-provider.tsx +20 -0
  31. package/src/components/prototypes/mini-pill-label.tsx +37 -0
  32. package/src/components/prototypes/prototype-change-log-panel.tsx +207 -0
  33. package/src/components/prototypes/prototype-comment-provider.tsx +61 -0
  34. package/src/components/prototypes/prototype-comment-toolbar.tsx +65 -0
  35. package/src/components/prototypes/prototype-control.tsx +31 -0
  36. package/src/components/prototypes/prototype-controls.tsx +553 -0
  37. package/src/components/prototypes/prototype-create-exploration-modal.tsx +117 -0
  38. package/src/components/prototypes/prototype-create-state-modal.tsx +117 -0
  39. package/src/components/prototypes/prototype-default-state-map-page.tsx +66 -0
  40. package/src/components/prototypes/prototype-design-brief-modal.tsx +128 -0
  41. package/src/components/prototypes/prototype-design-brief-panel.tsx +525 -0
  42. package/src/components/prototypes/prototype-design-context-panel.tsx +65 -0
  43. package/src/components/prototypes/prototype-floating-pill.module.scss +1341 -0
  44. package/src/components/prototypes/prototype-mobbin-gallery.tsx +330 -0
  45. package/src/components/prototypes/prototype-reference-docs.tsx +222 -0
  46. package/src/components/prototypes/prototype-review-chrome.module.scss +630 -0
  47. package/src/components/prototypes/prototype-review-chrome.tsx +1241 -0
  48. package/src/components/prototypes/prototype-review-dropdown-menu-item.tsx +1 -0
  49. package/src/components/prototypes/prototype-review-dropdown-menu-separator.tsx +1 -0
  50. package/src/components/prototypes/prototype-review-panel-menu-item.tsx +1 -0
  51. package/src/components/prototypes/prototype-review-panel-menu-separator.tsx +1 -0
  52. package/src/components/prototypes/prototype-review-panel-select.tsx +4 -0
  53. package/src/components/prototypes/prototype-review-sidebar.tsx +167 -0
  54. package/src/components/prototypes/prototype-share-link-restore.tsx +75 -0
  55. package/src/components/prototypes/prototype-share-mode.tsx +206 -0
  56. package/src/components/prototypes/prototype-spec-panel-content.tsx +538 -0
  57. package/src/components/prototypes/prototype-state-canvas-overlay.module.scss +336 -0
  58. package/src/components/prototypes/prototype-state-canvas-overlay.tsx +1023 -0
  59. package/src/components/prototypes/prototype-state-canvas.tsx +38 -0
  60. package/src/components/prototypes/prototype-state-screenshot-preview.tsx +47 -0
  61. package/src/components/prototypes/prototype-target.tsx +120 -0
  62. package/src/components/prototypes/prototype-tweak-highlight.tsx +132 -0
  63. package/src/components/prototypes/prototype-tweaks.tsx +48 -0
  64. package/src/components/prototypes/prototype-variant-explorer.tsx +347 -0
  65. package/src/components/prototypes/prototype-variant-rationale-panel.tsx +49 -0
  66. package/src/components/prototypes/prototype-variant-tabs.tsx +207 -0
  67. package/src/components/prototypes/prototype-vercel-preview-modal.tsx +120 -0
  68. package/src/components/prototypes/state-map-annotation-tooltip.module.scss +89 -0
  69. package/src/components/prototypes/state-map-annotation-tooltip.tsx +137 -0
  70. package/src/components/prototypes/state-map-wireframe-highlight.module.scss +25 -0
  71. package/src/components/prototypes/state-map-wireframe-highlight.tsx +82 -0
  72. package/src/components/shell/prototype-gallery-shell.tsx +79 -0
  73. package/src/components/shell/prototype-screenshot-capture.tsx +45 -0
  74. package/src/components/shell/prototype-shell.module.scss +194 -0
  75. package/src/components/shell/prototype-shell.tsx +160 -0
  76. package/src/components/shell/prototype-state-screenshot-capture.tsx +68 -0
  77. package/src/components/ui/button.tsx +58 -0
  78. package/src/components/ui/dialog.module.scss +81 -0
  79. package/src/components/ui/dialog.tsx +127 -0
  80. package/src/components/ui/dropdown-menu.tsx +200 -0
  81. package/src/components/ui/input.tsx +22 -0
  82. package/src/components/ui/label.tsx +26 -0
  83. package/src/components/ui/option-select.tsx +81 -0
  84. package/src/components/ui/scroll-area.tsx +48 -0
  85. package/src/components/ui/separator.tsx +31 -0
  86. package/src/components/ui/slider.tsx +28 -0
  87. package/src/components/ui/switch.tsx +29 -0
  88. package/src/components/ui/tabs.tsx +55 -0
  89. package/src/components/ui/toggle-group.tsx +80 -0
  90. package/src/components/ui/tooltip.tsx +32 -0
  91. package/src/config/create-prototype-gallery-page.tsx +65 -0
  92. package/src/config/create-prototype-host-app.tsx +121 -0
  93. package/src/config/create-prototype-page.tsx +41 -0
  94. package/src/config/create-prototype-site-layout.tsx +28 -0
  95. package/src/config/define-prototype-config.ts +7 -0
  96. package/src/config/prototype-icon.tsx +25 -0
  97. package/src/config/prototype-site-layout-client.tsx +35 -0
  98. package/src/config/with-prototype.ts +20 -0
  99. package/src/index.ts +303 -0
  100. package/src/lib/pr-split/build-pr-agent-prompt.ts +68 -0
  101. package/src/lib/pr-split/build-pr-split-prototype-url.ts +12 -0
  102. package/src/lib/pr-split/pr-split-highlight.ts +6 -0
  103. package/src/lib/pr-split/pr-split-types.ts +58 -0
  104. package/src/lib/pr-split/resolve-source-preview-url.ts +45 -0
  105. package/src/lib/pr-split/use-pr-vercel-previews.ts +69 -0
  106. package/src/lib/prototype-comments/core/annotation-channel.ts +41 -0
  107. package/src/lib/prototype-comments/core/annotation-status.ts +60 -0
  108. package/src/lib/prototype-comments/core/annotation-target.ts +567 -0
  109. package/src/lib/prototype-comments/core/capture-theme.ts +41 -0
  110. package/src/lib/prototype-comments/core/capture.ts +11 -0
  111. package/src/lib/prototype-comments/core/comment-capture-blocked.ts +47 -0
  112. package/src/lib/prototype-comments/core/comment-threads.ts +52 -0
  113. package/src/lib/prototype-comments/core/element-identification.ts +615 -0
  114. package/src/lib/prototype-comments/core/export.ts +35 -0
  115. package/src/lib/prototype-comments/core/freeze-animations.ts +266 -0
  116. package/src/lib/prototype-comments/core/identify-element.ts +76 -0
  117. package/src/lib/prototype-comments/core/normalize-viewport.ts +35 -0
  118. package/src/lib/prototype-comments/core/react-detection.ts +704 -0
  119. package/src/lib/prototype-comments/core/restore.ts +7 -0
  120. package/src/lib/prototype-comments/core/screenshot.ts +428 -0
  121. package/src/lib/prototype-comments/core/source-location.ts +906 -0
  122. package/src/lib/prototype-comments/core/storage.ts +80 -0
  123. package/src/lib/prototype-comments/core/types.ts +79 -0
  124. package/src/lib/prototype-comments/core/validation.ts +17 -0
  125. package/src/lib/prototype-comments/hooks/useLayoutShift.ts +61 -0
  126. package/src/lib/prototype-comments/index.ts +58 -0
  127. package/src/lib/prototype-comments/react/CommentCaptureToolbar.tsx +1398 -0
  128. package/src/lib/prototype-comments/react/CommentProvider.tsx +298 -0
  129. package/src/lib/prototype-comments/scss.d.ts +10 -0
  130. package/src/lib/prototype-comments/ui/CommentAnnotationOverlay.tsx +253 -0
  131. package/src/lib/prototype-comments/ui/CommentDetailPage.tsx +56 -0
  132. package/src/lib/prototype-comments/ui/CommentHeaderCount.tsx +28 -0
  133. package/src/lib/prototype-comments/ui/CommentThread.tsx +331 -0
  134. package/src/lib/prototype-comments/ui/CommentsGrid.tsx +780 -0
  135. package/src/lib/prototype-comments/ui/CommentsListPage.tsx +25 -0
  136. package/src/lib/prototype-comments/ui/CommentsSidebar.tsx +77 -0
  137. package/src/lib/prototype-comments/ui/annotation-marker/index.tsx +200 -0
  138. package/src/lib/prototype-comments/ui/annotation-marker/styles.module.scss +194 -0
  139. package/src/lib/prototype-comments/ui/annotation-popup/index.tsx +335 -0
  140. package/src/lib/prototype-comments/ui/annotation-popup/styles.module.scss +469 -0
  141. package/src/lib/prototype-comments/ui/capture-styles.module.scss +2225 -0
  142. package/src/lib/prototype-comments/ui/comment-highlight.ts +48 -0
  143. package/src/lib/prototype-comments/ui/comment-thread.module.scss +203 -0
  144. package/src/lib/prototype-comments/ui/comments-grid-card.module.scss +253 -0
  145. package/src/lib/prototype-comments/ui/comments-sidebar.module.scss +123 -0
  146. package/src/lib/prototype-comments/ui/icon-transitions.module.scss +51 -0
  147. package/src/lib/prototype-comments/ui/icons.tsx +1086 -0
  148. package/src/lib/prototype-comments/ui/is-comment-detail-path.ts +7 -0
  149. package/src/lib/prototypes/capture-prototype-viewport.ts +32 -0
  150. package/src/lib/prototypes/changelog-meta-context.tsx +162 -0
  151. package/src/lib/prototypes/changelog-meta-storage.ts +62 -0
  152. package/src/lib/prototypes/changelog-meta.ts +95 -0
  153. package/src/lib/prototypes/create-prototype-registry.ts +74 -0
  154. package/src/lib/prototypes/create-state-screenshot-thumbnail.ts +46 -0
  155. package/src/lib/prototypes/design-exploration-default-storage.ts +68 -0
  156. package/src/lib/prototypes/design-exploration-default.ts +51 -0
  157. package/src/lib/prototypes/design-exploration-types.ts +203 -0
  158. package/src/lib/prototypes/prototype-comment-registry.tsx +145 -0
  159. package/src/lib/prototypes/prototype-comment-review-bridge.tsx +40 -0
  160. package/src/lib/prototypes/prototype-comment-review-state.ts +57 -0
  161. package/src/lib/prototypes/prototype-component-registry.ts +40 -0
  162. package/src/lib/prototypes/prototype-config-types.ts +34 -0
  163. package/src/lib/prototypes/prototype-preview-state-registry.ts +443 -0
  164. package/src/lib/prototypes/prototype-preview-state-types.ts +89 -0
  165. package/src/lib/prototypes/prototype-review-context.tsx +905 -0
  166. package/src/lib/prototypes/prototype-review-url.ts +134 -0
  167. package/src/lib/prototypes/prototype-share-link.ts +175 -0
  168. package/src/lib/prototypes/prototype-state-canvas-constants.ts +118 -0
  169. package/src/lib/prototypes/prototype-state-canvas-types.ts +159 -0
  170. package/src/lib/prototypes/prototype-state-screenshot.ts +39 -0
  171. package/src/lib/prototypes/prototype-tweak-registry.ts +96 -0
  172. package/src/lib/prototypes/prototype-tweak-types.ts +31 -0
  173. package/src/lib/prototypes/reference-docs.ts +137 -0
  174. package/src/lib/prototypes/screenshot-manifest.ts +33 -0
  175. package/src/lib/prototypes/screenshot-src.ts +7 -0
  176. package/src/lib/prototypes/share-command.ts +8 -0
  177. package/src/lib/prototypes/state-map-annotation.ts +70 -0
  178. package/src/lib/prototypes/use-design-exploration-default.ts +114 -0
  179. package/src/lib/prototypes/use-persisted-local-state.ts +140 -0
  180. package/src/lib/prototypes/use-prototype-comments.ts +63 -0
  181. package/src/lib/prototypes/use-prototype-mobile-viewport-frame.ts +46 -0
  182. package/src/lib/prototypes/use-prototype-state-screenshot-src.ts +71 -0
  183. package/src/lib/prototypes/use-prototype-tool-theme.tsx +115 -0
  184. package/src/lib/prototypes/use-prototype-viewport-frame.ts +34 -0
  185. package/src/lib/prototypes/use-register-prototype-reference-docs.ts +16 -0
  186. package/src/lib/prototypes/use-snap-corner-position.ts +417 -0
  187. package/src/lib/prototypes/use-toolbar-hover-panel.ts +98 -0
  188. package/src/lib/prototypes/variant-set-lucide-icon.tsx +55 -0
  189. package/src/lib/tool-portal.ts +33 -0
  190. package/src/lib/use-copy-to-clipboard.ts +81 -0
  191. package/src/lib/utils.ts +6 -0
  192. package/src/lib/vercel-preview/parse-vercel-github-comment.ts +81 -0
  193. package/src/server/create-host-api-route.ts +100 -0
  194. package/src/server/create-prototype-api-routes.ts +428 -0
  195. package/src/server/gallery/create-gallery-api.ts +56 -0
  196. package/src/server/gallery/folders-route.ts +156 -0
  197. package/src/server/gallery/gallery-env.ts +39 -0
  198. package/src/server/gallery/gallery-route.ts +150 -0
  199. package/src/server/gallery/gallery-types.ts +15 -0
  200. package/src/server/gallery/image-route.ts +59 -0
  201. package/src/server/gallery/item-route.ts +92 -0
  202. package/src/server/pr-vercel-preview-route.ts +162 -0
  203. package/src/server/redis/client.ts +55 -0
  204. package/src/server/redis/prototype-changelog-meta.ts +31 -0
  205. package/src/server/redis/prototype-comments.ts +92 -0
  206. package/src/server/redis/prototype-design-exploration-default.ts +32 -0
  207. package/src/server.ts +51 -0
  208. package/src/styles/globals.css +624 -0
  209. package/tsconfig.json +19 -0
@@ -0,0 +1,704 @@
1
+ // =============================================================================
2
+ // React Component Name Detection
3
+ // Uses React DevTools techniques to extract component names from fiber nodes
4
+ // =============================================================================
5
+
6
+ /**
7
+ * React Fiber node type (minimal subset we care about)
8
+ * Based on React internal structure
9
+ */
10
+ interface ReactFiber {
11
+ tag: number;
12
+ type: ComponentType | string | null;
13
+ elementType: ComponentType | null;
14
+ return: ReactFiber | null;
15
+ }
16
+
17
+ interface ComponentType {
18
+ name?: string;
19
+ displayName?: string;
20
+ render?: { name?: string; displayName?: string };
21
+ type?: ComponentType;
22
+ _context?: { displayName?: string };
23
+ _status?: number;
24
+ _result?: ComponentType;
25
+ $$typeof?: symbol;
26
+ }
27
+
28
+ /**
29
+ * Fiber tags from React source (stable across versions)
30
+ * https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactWorkTags.js
31
+ */
32
+ const FiberTags = {
33
+ FunctionComponent: 0,
34
+ ClassComponent: 1,
35
+ IndeterminateComponent: 2,
36
+ HostRoot: 3,
37
+ HostPortal: 4,
38
+ HostComponent: 5, // DOM elements like <div>
39
+ HostText: 6,
40
+ Fragment: 7,
41
+ Mode: 8,
42
+ ContextConsumer: 9,
43
+ ContextProvider: 10,
44
+ ForwardRef: 11,
45
+ Profiler: 12,
46
+ SuspenseComponent: 13,
47
+ MemoComponent: 14,
48
+ SimpleMemoComponent: 15,
49
+ LazyComponent: 16,
50
+ // React 18/19 additions
51
+ IncompleteClassComponent: 17,
52
+ DehydratedFragment: 18,
53
+ SuspenseListComponent: 19,
54
+ // Note: 20 is unused/reserved
55
+ ScopeComponent: 21,
56
+ OffscreenComponent: 22,
57
+ LegacyHiddenComponent: 23,
58
+ CacheComponent: 24,
59
+ TracingMarkerComponent: 25,
60
+ HostHoistable: 26,
61
+ HostSingleton: 27,
62
+ IncompleteFunctionComponent: 28,
63
+ Throw: 29,
64
+ ViewTransitionComponent: 30,
65
+ ActivityComponent: 31,
66
+ } as const;
67
+
68
+ // =============================================================================
69
+ // Default Filter Configuration
70
+ // =============================================================================
71
+
72
+ /**
73
+ * Default exact names to always skip (React internals)
74
+ */
75
+ export const DEFAULT_SKIP_EXACT = new Set([
76
+ "Component",
77
+ "PureComponent",
78
+ "Fragment",
79
+ "Suspense",
80
+ "Profiler",
81
+ "StrictMode",
82
+ "Routes",
83
+ "Route",
84
+ "Outlet",
85
+ // Framework internals - exact matches
86
+ "Root",
87
+ "ErrorBoundaryHandler",
88
+ "HotReload",
89
+ "Hot",
90
+ ]);
91
+
92
+ /**
93
+ * Default patterns for framework internals
94
+ * Note: Patterns are designed to be specific to avoid false positives
95
+ * (e.g., ServerStatus, ClientProfile should NOT be filtered)
96
+ */
97
+ export const DEFAULT_SKIP_PATTERNS: RegExp[] = [
98
+ /Boundary$/, // ErrorBoundary, RedirectBoundary
99
+ /BoundaryHandler$/, // ErrorBoundaryHandler
100
+ /Provider$/, // ThemeProvider, Context.Provider
101
+ /Consumer$/, // Context.Consumer
102
+ /^(Inner|Outer)/, // InnerLayoutRouter
103
+ /Router$/, // AppRouter, BrowserRouter
104
+ /^Client(Page|Segment|Root)/, // ClientPageRoot, ClientSegmentRoot
105
+ /^Segment(ViewNode|Node)$/, // Next.js App Router internals
106
+ /^LayoutSegment/, // Next.js layout segment wrappers
107
+ /^Server(Root|Component|Render)/, // ServerRoot (not ServerStatus)
108
+ /^RSC/, // RSCComponent
109
+ /Context$/, // LayoutRouterContext
110
+ /^Hot(Reload)?$/, // HotReload (exact match to avoid false positives)
111
+ /^(Dev|React)(Overlay|Tools|Root)/, // DevTools, ReactDevOverlay
112
+ /Overlay$/, // ReactDevOverlay, ErrorOverlay
113
+ /Handler$/, // ScrollAndFocusHandler, ErrorBoundaryHandler
114
+ /^With[A-Z]/, // withRouter, WithAuth (HOCs)
115
+ /Wrapper$/, // Generic wrappers
116
+ /^Root$/, // Generic Root component
117
+ ];
118
+
119
+ /**
120
+ * Patterns that indicate likely user-defined components
121
+ * Used as fallback in 'smart' mode
122
+ */
123
+ const DEFAULT_USER_PATTERNS: RegExp[] = [
124
+ /Page$/, // HomePage, InstallPage
125
+ /View$/, // ListView, DetailView
126
+ /Screen$/, // HomeScreen
127
+ /Section$/, // HeroSection
128
+ /Card$/, // ProductCard
129
+ /List$/, // UserList
130
+ /Item$/, // ListItem, MenuItem
131
+ /Form$/, // LoginForm
132
+ /Modal$/, // ConfirmModal
133
+ /Dialog$/, // AlertDialog
134
+ /Button$/, // SubmitButton (but not all buttons)
135
+ /Nav$/, // SideNav, TopNav
136
+ /Header$/, // PageHeader
137
+ /Footer$/, // PageFooter
138
+ /Layout$/, // MainLayout (careful - could be framework)
139
+ /Panel$/, // SidePanel
140
+ /Tab$/, // SettingsTab
141
+ /Menu$/, // DropdownMenu
142
+ ];
143
+
144
+ // =============================================================================
145
+ // Configuration Types
146
+ // =============================================================================
147
+
148
+ export type ReactDetectionMode = "all" | "filtered" | "smart";
149
+
150
+ export interface ReactDetectionConfig {
151
+ /**
152
+ * How many component names to collect
153
+ * @default 3
154
+ */
155
+ maxComponents?: number;
156
+
157
+ /**
158
+ * Maximum fiber depth to traverse
159
+ * @default 25
160
+ */
161
+ maxDepth?: number;
162
+
163
+ /**
164
+ * Detection mode:
165
+ * - 'smart': Only show components that correlate with DOM classes (strictest, most relevant)
166
+ * - 'filtered': Skip known framework internals (default)
167
+ * - 'all': Show all components (no filtering)
168
+ * @default 'filtered'
169
+ */
170
+ mode?: ReactDetectionMode;
171
+
172
+ /**
173
+ * Additional exact names to skip (merged with defaults in 'filtered' mode)
174
+ */
175
+ skipExact?: Set<string> | string[];
176
+
177
+ /**
178
+ * Additional patterns to skip (merged with defaults in 'filtered' mode)
179
+ */
180
+ skipPatterns?: RegExp[];
181
+
182
+ /**
183
+ * Patterns for user components (used as fallback in 'smart' mode)
184
+ */
185
+ userPatterns?: RegExp[];
186
+
187
+ /**
188
+ * Custom filter function for full control
189
+ * Return true to INCLUDE the component, false to skip
190
+ */
191
+ filter?: (name: string, depth: number) => boolean;
192
+ }
193
+
194
+ /**
195
+ * Resolved configuration with all defaults applied
196
+ */
197
+ interface ResolvedConfig {
198
+ maxComponents: number;
199
+ maxDepth: number;
200
+ mode: ReactDetectionMode;
201
+ skipExact: Set<string>;
202
+ skipPatterns: RegExp[];
203
+ userPatterns: RegExp[];
204
+ filter?: (name: string, depth: number) => boolean;
205
+ }
206
+
207
+ function resolveConfig(config?: ReactDetectionConfig): ResolvedConfig {
208
+ const mode = config?.mode ?? "filtered";
209
+
210
+ // Convert skipExact to Set if array
211
+ let skipExact = DEFAULT_SKIP_EXACT;
212
+ if (config?.skipExact) {
213
+ const additional =
214
+ config.skipExact instanceof Set
215
+ ? config.skipExact
216
+ : new Set(config.skipExact);
217
+ skipExact = new Set([...DEFAULT_SKIP_EXACT, ...additional]);
218
+ }
219
+
220
+ return {
221
+ maxComponents: config?.maxComponents ?? 6,
222
+ maxDepth: config?.maxDepth ?? 30,
223
+ mode,
224
+ skipExact,
225
+ skipPatterns: config?.skipPatterns
226
+ ? [...DEFAULT_SKIP_PATTERNS, ...config.skipPatterns]
227
+ : DEFAULT_SKIP_PATTERNS,
228
+ userPatterns: config?.userPatterns ?? DEFAULT_USER_PATTERNS,
229
+ filter: config?.filter,
230
+ };
231
+ }
232
+
233
+ // =============================================================================
234
+ // Filter Logic
235
+ // =============================================================================
236
+
237
+ /**
238
+ * Normalize a component name to match CSS class conventions
239
+ * SideNav -> side-nav, LinkComponent -> link-component
240
+ */
241
+ function normalizeComponentName(name: string): string {
242
+ return name
243
+ .replace(/([a-z])([A-Z])/g, "$1-$2")
244
+ .replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
245
+ .toLowerCase();
246
+ }
247
+
248
+ /**
249
+ * Collect CSS classes from an element and its ancestors
250
+ */
251
+ function getAncestorClasses(element: HTMLElement, maxDepth = 10): Set<string> {
252
+ const classes = new Set<string>();
253
+ let current: HTMLElement | null = element;
254
+ let depth = 0;
255
+
256
+ while (current && depth < maxDepth) {
257
+ if (current.className && typeof current.className === "string") {
258
+ current.className.split(/\s+/).forEach((cls) => {
259
+ if (cls.length > 1) {
260
+ // Normalize: remove CSS module hashes, convert to lowercase
261
+ const normalized = cls
262
+ .replace(/[_][a-zA-Z0-9]{5,}.*$/, "")
263
+ .toLowerCase();
264
+ if (normalized.length > 1) {
265
+ classes.add(normalized);
266
+ }
267
+ }
268
+ });
269
+ }
270
+ current = current.parentElement;
271
+ depth++;
272
+ }
273
+
274
+ return classes;
275
+ }
276
+
277
+ /**
278
+ * Check if a component name correlates with any DOM class
279
+ */
280
+ function componentCorrelatesWithDOM(
281
+ componentName: string,
282
+ domClasses: Set<string>,
283
+ ): boolean {
284
+ const normalized = normalizeComponentName(componentName);
285
+
286
+ for (const cls of domClasses) {
287
+ // Exact match: SideNav -> side-nav
288
+ if (cls === normalized) return true;
289
+
290
+ // Contains match: LinkComponent -> nav-link contains "link"
291
+ // Split both by hyphens and check for word overlaps
292
+ const componentWords = normalized.split("-").filter((w) => w.length > 2);
293
+ const classWords = cls.split("-").filter((w) => w.length > 2);
294
+
295
+ for (const cWord of componentWords) {
296
+ for (const dWord of classWords) {
297
+ if (cWord === dWord || cWord.includes(dWord) || dWord.includes(cWord)) {
298
+ return true;
299
+ }
300
+ }
301
+ }
302
+ }
303
+
304
+ return false;
305
+ }
306
+
307
+ function shouldIncludeComponent(
308
+ name: string,
309
+ depth: number,
310
+ config: ResolvedConfig,
311
+ domClasses?: Set<string>,
312
+ ): boolean {
313
+ // Custom filter takes precedence
314
+ if (config.filter) {
315
+ return config.filter(name, depth);
316
+ }
317
+
318
+ switch (config.mode) {
319
+ case "all":
320
+ // "all" mode shows everything - no filtering at all
321
+ return true;
322
+
323
+ case "filtered":
324
+ // "filtered" mode skips framework internals
325
+ if (config.skipExact.has(name)) {
326
+ return false;
327
+ }
328
+ if (config.skipPatterns.some((p) => p.test(name))) {
329
+ return false;
330
+ }
331
+ return true;
332
+
333
+ case "smart":
334
+ // "smart" mode: first apply framework filters, then require DOM correlation
335
+ if (config.skipExact.has(name)) {
336
+ return false;
337
+ }
338
+ if (config.skipPatterns.some((p) => p.test(name))) {
339
+ return false;
340
+ }
341
+ // Must correlate with DOM classes OR match user patterns
342
+ if (domClasses && componentCorrelatesWithDOM(name, domClasses)) {
343
+ return true;
344
+ }
345
+ if (config.userPatterns.some((p) => p.test(name))) {
346
+ return true;
347
+ }
348
+ // Skip components that don't correlate - this mode is intentionally strict
349
+ return false;
350
+
351
+ default:
352
+ return true;
353
+ }
354
+ }
355
+
356
+ // =============================================================================
357
+ // React Detection
358
+ // =============================================================================
359
+
360
+ let reactDetectionCache: boolean | null = null;
361
+
362
+ // Only cache for 'all' mode - filtered modes should NOT cache because:
363
+ // 1. Filter results depend on config that may change between calls
364
+ // 2. Cached results from before filter changes would return stale/unfiltered data
365
+ // 3. The cache lookup happens BEFORE filtering, so old cached data bypasses filters
366
+ // Using WeakMap allows garbage collection when elements are removed from DOM.
367
+ const componentCacheAll = new WeakMap<HTMLElement, ReactComponentInfo>();
368
+
369
+ /**
370
+ * Checks if React is present on the page
371
+ */
372
+ /**
373
+ * Check if an element has React fiber keys
374
+ */
375
+ function hasReactFiber(element: Element): boolean {
376
+ return Object.keys(element).some(
377
+ (key) =>
378
+ key.startsWith("__reactFiber$") ||
379
+ key.startsWith("__reactInternalInstance$") ||
380
+ key.startsWith("__reactProps$"),
381
+ );
382
+ }
383
+
384
+ /**
385
+ * Checks if React is present on the page.
386
+ * Scans common React root containers since React typically mounts
387
+ * to #root, #app, #__next, etc. rather than document.body directly.
388
+ */
389
+ export function isReactPage(): boolean {
390
+ if (reactDetectionCache !== null) {
391
+ return reactDetectionCache;
392
+ }
393
+
394
+ if (typeof document === "undefined") {
395
+ return false;
396
+ }
397
+
398
+ // Check body first (some apps mount directly to body)
399
+ if (document.body && hasReactFiber(document.body)) {
400
+ reactDetectionCache = true;
401
+ return true;
402
+ }
403
+
404
+ // Check common React root containers
405
+ const commonRoots = ["#root", "#app", "#__next", "[data-reactroot]"];
406
+ for (const selector of commonRoots) {
407
+ const el = document.querySelector(selector);
408
+ if (el && hasReactFiber(el)) {
409
+ reactDetectionCache = true;
410
+ return true;
411
+ }
412
+ }
413
+
414
+ // Scan immediate children of body as fallback
415
+ if (document.body) {
416
+ for (const child of document.body.children) {
417
+ if (hasReactFiber(child)) {
418
+ reactDetectionCache = true;
419
+ return true;
420
+ }
421
+ }
422
+ }
423
+
424
+ reactDetectionCache = false;
425
+ return false;
426
+ }
427
+
428
+ // Wrapper object to allow cache clearing (WeakMap has no clear() method)
429
+ let componentCacheAllRef = { map: componentCacheAll };
430
+
431
+ /**
432
+ * Clears the React detection cache
433
+ * Note: Only 'all' mode uses caching; filtered modes don't cache to avoid stale filter results
434
+ */
435
+ export function clearReactDetectionCache(): void {
436
+ reactDetectionCache = null;
437
+ componentCacheAllRef.map = new WeakMap<HTMLElement, ReactComponentInfo>();
438
+ }
439
+
440
+ function getReactFiberKey(element: HTMLElement): string | null {
441
+ const keys = Object.keys(element);
442
+ return (
443
+ keys.find(
444
+ (key) =>
445
+ key.startsWith("__reactFiber$") ||
446
+ key.startsWith("__reactInternalInstance$"),
447
+ ) || null
448
+ );
449
+ }
450
+
451
+ function getFiberFromElement(element: HTMLElement): ReactFiber | null {
452
+ const key = getReactFiberKey(element);
453
+ if (!key) return null;
454
+ return (element as unknown as Record<string, unknown>)[
455
+ key
456
+ ] as ReactFiber | null;
457
+ }
458
+
459
+ function getComponentNameFromType(type: ComponentType | null): string | null {
460
+ if (!type) return null;
461
+ if (type.displayName) return type.displayName;
462
+ if (type.name) return type.name;
463
+ return null;
464
+ }
465
+
466
+ function getComponentNameFromFiber(fiber: ReactFiber): string | null {
467
+ const { tag, type, elementType } = fiber;
468
+
469
+ // Skip DOM elements and host types
470
+ if (
471
+ tag === FiberTags.HostComponent ||
472
+ tag === FiberTags.HostText ||
473
+ tag === FiberTags.HostHoistable ||
474
+ tag === FiberTags.HostSingleton
475
+ ) {
476
+ return null;
477
+ }
478
+
479
+ // Skip Fragment, Mode, Profiler, and related internal types
480
+ if (
481
+ tag === FiberTags.Fragment ||
482
+ tag === FiberTags.Mode ||
483
+ tag === FiberTags.Profiler ||
484
+ tag === FiberTags.DehydratedFragment
485
+ ) {
486
+ return null;
487
+ }
488
+
489
+ // Skip React internal infrastructure types (these are internal implementation details)
490
+ if (
491
+ tag === FiberTags.HostRoot ||
492
+ tag === FiberTags.HostPortal ||
493
+ tag === FiberTags.ScopeComponent ||
494
+ tag === FiberTags.OffscreenComponent ||
495
+ tag === FiberTags.LegacyHiddenComponent ||
496
+ tag === FiberTags.CacheComponent ||
497
+ tag === FiberTags.TracingMarkerComponent ||
498
+ tag === FiberTags.Throw ||
499
+ tag === FiberTags.ViewTransitionComponent ||
500
+ tag === FiberTags.ActivityComponent
501
+ ) {
502
+ return null;
503
+ }
504
+
505
+ // Handle ForwardRef
506
+ if (tag === FiberTags.ForwardRef) {
507
+ const elType = elementType as ComponentType | null;
508
+ if (elType?.render) {
509
+ const innerName = getComponentNameFromType(elType.render);
510
+ if (innerName) return innerName;
511
+ }
512
+ if (elType?.displayName) return elType.displayName;
513
+ return getComponentNameFromType(type as ComponentType);
514
+ }
515
+
516
+ // Handle Memo
517
+ if (
518
+ tag === FiberTags.MemoComponent ||
519
+ tag === FiberTags.SimpleMemoComponent
520
+ ) {
521
+ const elType = elementType as ComponentType | null;
522
+ if (elType?.type) {
523
+ const innerName = getComponentNameFromType(elType.type);
524
+ if (innerName) return innerName;
525
+ }
526
+ if (elType?.displayName) return elType.displayName;
527
+ return getComponentNameFromType(type as ComponentType);
528
+ }
529
+
530
+ // Handle Context Provider
531
+ if (tag === FiberTags.ContextProvider) {
532
+ const elType = type as ComponentType | null;
533
+ if (elType?._context?.displayName) {
534
+ return `${elType._context.displayName}.Provider`;
535
+ }
536
+ return null;
537
+ }
538
+
539
+ // Handle Context Consumer
540
+ if (tag === FiberTags.ContextConsumer) {
541
+ const elType = type as ComponentType | null;
542
+ if (elType?.displayName) {
543
+ return `${elType.displayName}.Consumer`;
544
+ }
545
+ return null;
546
+ }
547
+
548
+ // Handle Lazy
549
+ if (tag === FiberTags.LazyComponent) {
550
+ const elType = elementType as ComponentType | null;
551
+ if (elType?._status === 1 && elType._result) {
552
+ return getComponentNameFromType(elType._result);
553
+ }
554
+ return null;
555
+ }
556
+
557
+ // Handle Suspense and SuspenseList
558
+ if (
559
+ tag === FiberTags.SuspenseComponent ||
560
+ tag === FiberTags.SuspenseListComponent
561
+ ) {
562
+ return null;
563
+ }
564
+
565
+ // Handle incomplete components (error states during rendering)
566
+ if (
567
+ tag === FiberTags.IncompleteClassComponent ||
568
+ tag === FiberTags.IncompleteFunctionComponent
569
+ ) {
570
+ // These are components that errored during rendering
571
+ // Try to get the name anyway for debugging purposes
572
+ return getComponentNameFromType(type as ComponentType);
573
+ }
574
+
575
+ // Function and Class components
576
+ if (
577
+ tag === FiberTags.FunctionComponent ||
578
+ tag === FiberTags.ClassComponent ||
579
+ tag === FiberTags.IndeterminateComponent
580
+ ) {
581
+ return getComponentNameFromType(type as ComponentType);
582
+ }
583
+
584
+ return null;
585
+ }
586
+
587
+ // =============================================================================
588
+ // Public API
589
+ // =============================================================================
590
+
591
+ /**
592
+ * Result from React component detection
593
+ */
594
+ export interface ReactComponentInfo {
595
+ /** Full component path like "<App> <Layout> <Button>" */
596
+ path: string | null;
597
+ /** Array of component names from innermost to outermost */
598
+ components: string[];
599
+ }
600
+
601
+ /**
602
+ * Check if a name looks like minified/production code (single letter or very short)
603
+ */
604
+ function isMinifiedName(name: string): boolean {
605
+ // Single letter or two letters that look like minified (e.g., "e", "t", "Zt")
606
+ if (name.length <= 2) return true;
607
+ // All lowercase short names are likely minified
608
+ if (name.length <= 3 && name === name.toLowerCase()) return true;
609
+ return false;
610
+ }
611
+
612
+ /**
613
+ * Walks up the fiber tree to collect React component names
614
+ *
615
+ * @param element - The DOM element to start from
616
+ * @param config - Optional configuration
617
+ * @returns ReactComponentInfo with component path and array
618
+ */
619
+ export function getReactComponentName(
620
+ element: HTMLElement,
621
+ config?: ReactDetectionConfig,
622
+ ): ReactComponentInfo {
623
+ const resolved = resolveConfig(config);
624
+
625
+ // Only use cache for 'all' mode - filtered modes must NOT cache because:
626
+ // - Cache lookup happens BEFORE filtering logic runs
627
+ // - Cached results from before filter updates would bypass new filters
628
+ // - This was causing "Root", "ErrorBoundaryHandler" to leak through
629
+ const useCache = resolved.mode === "all";
630
+
631
+ if (useCache) {
632
+ const cached = componentCacheAllRef.map.get(element);
633
+ if (cached !== undefined) {
634
+ return cached;
635
+ }
636
+ }
637
+
638
+ if (!isReactPage()) {
639
+ const result: ReactComponentInfo = { path: null, components: [] };
640
+ if (useCache) {
641
+ componentCacheAllRef.map.set(element, result);
642
+ }
643
+ return result;
644
+ }
645
+
646
+ // Collect DOM classes for smart mode
647
+ const domClasses =
648
+ resolved.mode === "smart" ? getAncestorClasses(element) : undefined;
649
+
650
+ const components: string[] = [];
651
+
652
+ try {
653
+ let fiber = getFiberFromElement(element);
654
+ let depth = 0;
655
+
656
+ while (
657
+ fiber &&
658
+ depth < resolved.maxDepth &&
659
+ components.length < resolved.maxComponents
660
+ ) {
661
+ const name = getComponentNameFromFiber(fiber);
662
+
663
+ // Skip minified names and apply filter
664
+ if (
665
+ name &&
666
+ !isMinifiedName(name) &&
667
+ shouldIncludeComponent(name, depth, resolved, domClasses)
668
+ ) {
669
+ components.push(name);
670
+ }
671
+
672
+ fiber = fiber.return;
673
+ depth++;
674
+ }
675
+ } catch {
676
+ // Fiber structure may be corrupted or inaccessible - return empty result
677
+ const result: ReactComponentInfo = { path: null, components: [] };
678
+ if (useCache) {
679
+ componentCacheAllRef.map.set(element, result);
680
+ }
681
+ return result;
682
+ }
683
+
684
+ if (components.length === 0) {
685
+ const result: ReactComponentInfo = { path: null, components: [] };
686
+ if (useCache) {
687
+ componentCacheAllRef.map.set(element, result);
688
+ }
689
+ return result;
690
+ }
691
+
692
+ // Build path from outermost to innermost: <App> <Layout> <Button>
693
+ const path = components
694
+ .slice()
695
+ .reverse()
696
+ .map((c) => `<${c}>`)
697
+ .join(" ");
698
+
699
+ const result: ReactComponentInfo = { path, components };
700
+ if (useCache) {
701
+ componentCacheAllRef.map.set(element, result);
702
+ }
703
+ return result;
704
+ }
@@ -0,0 +1,7 @@
1
+ export function commentToLiveState<TState>(
2
+ comment: TState & { capturedAt: string; route: string },
3
+ ): TState {
4
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
5
+ const { capturedAt: _capturedAt, route: _route, ...live } = comment as Record<string, unknown>;
6
+ return structuredClone(live) as TState;
7
+ }