Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…tion (Q3 3.2) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…t (ROADMAP Q3 3.3) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…s (Q3 3.4) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…r integration (Q3 3.1) Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
- Add useNavOrder hook for localStorage-persisted navigation ordering - Add HTML5 native drag-and-drop to NavigationItemRenderer with visual drop indicator - Create PerformanceDashboard component with Web Vitals, memory, render metrics - Dashboard integrates with usePerformance and usePerformanceBudget hooks - Toggle via Ctrl+Shift+P or floating button, dev-mode only Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…rovider WebSocket, ROADMAP updates Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…torage JSON sanitization, fetch URL validation Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR implements several remaining Q2–Q3 2026 roadmap items across the runtime hooks, console app UX/perf tooling, and collaboration capabilities, with accompanying ROADMAP updates.
Changes:
- Added new React runtime hooks:
useViewTransition,usePerformanceBudget, anduseETagCache(plus tests). - Introduced
@object-ui/collaborationpackage with realtime subscription, presence, conflict resolution, and UI components. - Enhanced console UX/perf: route-based lazy loading, dev-only Performance Dashboard, and sidebar drag-and-drop reordering; plus View Transitions support in
ViewSwitcherand WebSocket support in designerCollaborationProvider.
Reviewed changes
Copilot reviewed 25 out of 26 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds workspace importer for packages/collaboration and updates lock snapshots. |
| packages/react/src/hooks/useViewTransition.ts | New hook wrapping View Transitions API with reduced-motion and fallback behavior. |
| packages/react/src/hooks/usePerformanceBudget.ts | New hook for performance budget enforcement and violation tracking. |
| packages/react/src/hooks/useETagCache.ts | New ETag-aware fetch hook with LRU/TTL and optional SW registration. |
| packages/react/src/hooks/index.ts | Re-exports the new hooks from @object-ui/react. |
| packages/react/src/hooks/tests/* | Adds tests for new runtime hooks. |
| packages/plugin-list/src/ViewSwitcher.tsx | Adds animated view switching using the View Transitions API when available. |
| packages/plugin-designer/src/CollaborationProvider.tsx | Adds WebSocket transport/presence wiring and version counting fields. |
| packages/collaboration/* | New collaboration package (hooks/components + tests + build config). |
| apps/console/src/components/PerformanceDashboard.tsx | Adds dev-only floating performance dashboard UI. |
| apps/console/src/components/AppSidebar.tsx | Adds localStorage-persisted HTML5 DnD nav reordering. |
| apps/console/src/App.tsx | Adds route-based code splitting via React.lazy + Suspense. |
| ROADMAP.md | Marks roadmap items complete and updates compliance percentages. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| sendPresenceRef.current(updated); | ||
| return updated; | ||
| }); | ||
| }, awayTimeout - idleTimeout); |
There was a problem hiding this comment.
awayTimeout - idleTimeout can be negative if a caller configures awayTimeout smaller than idleTimeout, which results in an immediate timeout and flips users to away unexpectedly. Consider clamping the delay (e.g. Math.max(0, awayTimeout - idleTimeout)) or enforcing awayTimeout >= idleTimeout.
| }, awayTimeout - idleTimeout); | |
| }, Math.max(0, awayTimeout - idleTimeout)); |
| const result: Record<string, string[]> = {}; | ||
| for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) { |
There was a problem hiding this comment.
The localStorage “shape validation” still assigns arbitrary keys from parsed JSON onto a plain object (result[k] = ...). If k is __proto__ / constructor / prototype, this can trigger prototype pollution. Consider building result with Object.create(null) and/or explicitly rejecting those keys before assignment.
| const result: Record<string, string[]> = {}; | |
| for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) { | |
| const result = Object.create(null) as Record<string, string[]>; | |
| for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) { | |
| // Avoid prototype pollution from malicious localStorage keys | |
| if (k === '__proto__' || k === 'constructor' || k === 'prototype') { | |
| continue; | |
| } |
| // 304 Not Modified — serve from cache | ||
| if (res.status === 304 && cached) { | ||
| touchAccessOrder(url); | ||
| return { data: cached.data, fromCache: true, etag: cached.etag }; |
There was a problem hiding this comment.
On 304 Not Modified, the cached entry is returned but its timestamp is not refreshed. With TTL enabled, a frequently-accessed resource can still expire and stop sending conditional headers even though the server confirmed it is unchanged. Consider updating the cache entry timestamp (and persisting if needed) when serving a 304.
| // 304 Not Modified — serve from cache | |
| if (res.status === 304 && cached) { | |
| touchAccessOrder(url); | |
| return { data: cached.data, fromCache: true, etag: cached.etag }; | |
| // 304 Not Modified — refresh cache metadata and serve from cache | |
| if (res.status === 304 && cached) { | |
| const refreshedEntry: CacheEntry<T> = { | |
| ...cached, | |
| timestamp: Date.now(), | |
| }; | |
| setEntry(url, refreshedEntry); | |
| touchAccessOrder(url); | |
| return { data: refreshedEntry.data, fromCache: true, etag: refreshedEntry.etag }; |
| const [remoteUsers, setRemoteUsers] = useState<Map<string, PresenceUser>>(new Map()); | ||
| const [currentUser, setCurrentUser] = useState<PresenceUser>(() => ({ | ||
| userId: user.id, |
There was a problem hiding this comment.
remoteUsers / setRemoteUsers are never updated anywhere in this hook, so users will always be an empty array. This makes the hook unable to "manage a set of remote users" as described. Consider either (a) exposing an API to apply remote presence updates (e.g. updateRemoteUser/removeRemoteUser), or (b) wiring this hook to createPresenceUpdater internally.
| currentUserId: user?.id, | ||
| connectionState, | ||
| versionCount: versionRef.current, | ||
| }), | ||
| [users, isConnected, sendOperation, user?.id], | ||
| [users, isConnected, sendOperation, user?.id, connectionState], |
There was a problem hiding this comment.
versionCount is derived from versionRef.current, but updating a ref does not trigger a re-render. As a result, consumers of the context will not see versionCount change when sendOperation is called. Consider tracking the version in state (or deriving it from operations) and including that state in the context value dependencies.
| <SidebarMenuItem | ||
| draggable | ||
| onDragStart={(e: React.DragEvent) => { | ||
| e.dataTransfer.effectAllowed = 'move'; | ||
| dnd?.onDragStart(groupKey, item.id); |
There was a problem hiding this comment.
Sidebar reordering is currently mouse/touch drag-and-drop only. This is not keyboard-accessible, and draggable elements can be hard to use with assistive tech. Consider adding a keyboard alternative (e.g. "Move up/down" actions) so reordering is possible without a pointer.
| const styles = { | ||
| thread: { | ||
| fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', | ||
| fontSize: '14px', | ||
| lineHeight: '1.5', |
There was a problem hiding this comment.
This component hard-codes many light-theme colors via inline styles (e.g. #fff, #e2e8f0, #475569). In apps that support theming (including dark mode), this will likely render incorrectly and be difficult to customize. Consider switching to Tailwind classes using theme tokens/CSS variables (or exposing className hooks for key parts).
| // Connect on mount, disconnect on unmount | ||
| useEffect(() => { | ||
| mountedRef.current = true; | ||
| if (url) { | ||
| connect(); |
There was a problem hiding this comment.
The mount effect suppresses react-hooks/exhaustive-deps and doesn’t depend on connect(), even though connect() is parameterized by other options (autoReconnect/backoff limits). If those options change, the hook may keep reconnecting with stale settings. Consider depending on connect (and making it stable) rather than disabling the lint rule.
| const data = (await res.json()) as T; | ||
| const etag = res.headers.get('etag') ?? undefined; | ||
| const lastModified = res.headers.get('last-modified') ?? undefined; | ||
|
|
||
| if (etag || lastModified) { |
There was a problem hiding this comment.
res.json() is called unconditionally. For non-2xx responses (and for some valid responses like 204), this can throw and/or cause error payloads to be treated like cached data. Consider checking res.ok (and possibly content-type) before parsing/caching, and surfacing a structured error for non-OK responses.
| const wsUrl = authToken ? `${url}?token=${encodeURIComponent(authToken)}&channel=${encodeURIComponent(channel)}` : `${url}?channel=${encodeURIComponent(channel)}`; | ||
|
|
There was a problem hiding this comment.
wsUrl is built via string concatenation with ?token=...&channel=.... If url already contains query params or a hash, this will produce an invalid URL. Consider constructing this with new URL(url) and searchParams.set(...) to ensure correct encoding and merging.
Implements all remaining code-deliverable items from the 2026 roadmap (Q2 2.1, Q3 3.1–3.4), bringing spec compliance from 96% → 98%.
New package:
@object-ui/collaborationuseRealtimeSubscription— WebSocket subscriptions with auto-reconnect, exponential backoff, message bufferingusePresence— cursor/selection tracking, idle/away detection via activity listeners, throttled updatesuseConflictResolution— version history, conflict queue, resolution strategies (local/remote/merge), version diffingCommentThread— threaded comments with@mentionsuggestions, replies, resolve/reopenLiveCursors/PresenceAvatars— remote cursor overlay and avatar stack componentsCollaborationProviderin plugin-designer with actual WebSocket transport, presence sync, version countingPerformance optimization (Q3 3.3)
usePerformanceBudget— LCP/FCP/render-time/memory budget enforcement with violation tracking and dev-mode console warningsApp.tsx— 13 routes lazy-loaded viaReact.lazy+Suspense(main bundle reduced ~100KB)PerformanceDashboard— dev-mode floating panel (Ctrl+Shift+P) showing Web Vitals, memory usage, budget violationsView transitions (Q3 3.4)
useViewTransition— wraps browser View Transitions API with CSS class fallback, respectsprefers-reduced-motionViewSwitchergainsanimatedprop that callsdocument.startViewTransition()on view type changesDnD sidebar reordering (Q2 2.1)
localStorageper appETag caching + Service Worker (Q3 3.1)
useETagCache—If-None-Match/If-Modified-Sinceaware fetch, LRU in-memory cache with optional localStorage persistence, SW registrationSecurity hardening
ws:/wss:only) inuseRealtimeSubscriptionandCollaborationProviderlocalStorageJSON shape validation in sidebar DnD to prevent prototype pollutionhttp:/https:only) inuseETagCacheTests
35 new tests across 6 files. All 42 builds pass, 3181/3185 tests pass (2 pre-existing failures, 2 skipped).
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.