feat(likec4): add overview page header with search, navigation drawer and theme toggle#2665
Conversation
🦋 Changeset detectedLatest commit: 3d785dd The changes in this PR will be included in the next version bump. This PR includes changesets to release 19 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
25fe754 to
75236a5
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds an overview search UI and header navigation; refactors search from an XState actor to a React SearchContext with adapters; exposes search APIs and CSS tokens; adds an OverviewSearch overlay (⌘/Ctrl+K) that navigates to views with an optional focusOnElement URL param and a View-side FocusElementFromUrl to apply focus. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant OverviewSearch
participant OverviewSearchAdapter
participant Router
participant ViewReact
User->>OverviewSearch: open overlay (click or ⌘/Ctrl+K) and type query
OverviewSearch->>OverviewSearchAdapter: setSearchValue(value)
User->>OverviewSearch: select element result
OverviewSearch->>OverviewSearchAdapter: navigateTo(viewId, elementFqn)
OverviewSearchAdapter->>Router: navigate to /view/{viewId}?focusOnElement=elementFqn
Router->>ViewReact: mount view with search params
ViewReact->>ViewReact: FocusElementFromUrl reads focusOnElement and focuses element
ViewReact->>Router: clear focusOnElement param
sequenceDiagram
participant XStateActor as XState<br/>SearchActor
participant Adapter as XStateSearchAdapter
participant SearchCtx as SearchContext<br/>Provider
participant Component as SearchUI
XStateActor->>Adapter: actor state changes
Adapter->>SearchCtx: map actor state -> context value
SearchCtx->>Component: Provider wraps components
Component->>SearchCtx: useSearchContext() reads value and calls navigateTo/openPickView
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (8)
e2e/tests/overview-page.spec.ts (1)
21-25: Missing logo-related assertions despite being in the PR checklist.The PR description lists a logo visibility and scroll-to-top behavior check, but neither appears in this test. Consider adding:
test('logo is visible and scrolls page to top on click', async ({ page }) => { await page.evaluate(() => window.scrollTo(0, 500)) await page.getByRole('img', { name: /LikeC4/ }).click() // adjust selector to match actual logo role/name await expect(page.evaluate(() => window.scrollY)).resolves.toBe(0) })Would you like me to draft the full logo test once the aria-label/role of the logo element is confirmed?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@e2e/tests/overview-page.spec.ts` around lines 21 - 25, The test "header shows burger, logo, search bar, and theme toggle" is missing the logo visibility and click-scroll-to-top assertions called out in the PR checklist; add assertions to verify the logo is visible and that clicking it scrolls the page to top: within the same or a new test (e.g., "logo is visible and scrolls page to top on click") programmatically scroll the page (window.scrollTo / page.evaluate), assert the logo element (use page.getByRole('img', { name: /LikeC4/ }) or the app's actual aria-label/role) is visible, click it, then assert window.scrollY resolves to 0 to confirm the scroll-to-top behavior. Ensure the selector matches the actual logo role/name before committing.packages/likec4/app/src/routes/__root.tsx (1)
32-36: Optional:as Fqn | undefinedcast is against the "avoidas" guideline.The identical pattern already exists for
relationships(line 31), so this is consistent, but both could be improved. Consider a.pipe(z.custom<Fqn>(...))approach or a typed helper to avoid the cast.♻️ Suggested alternative (applied identically to both fields)
- focusOnElement: z.string() - .nonempty() - .optional() - .catch(undefined) - .transform(v => v as Fqn | undefined), + focusOnElement: z.string() + .nonempty() + .optional() + .catch(undefined) as z.ZodType<Fqn | undefined>,Or, if a utility already exists:
const fqnParam = z.string().nonempty().optional().catch(undefined).transform(v => v as Fqn | undefined) // reuse for both `relationships` and `focusOnElement`As per coding guidelines: "Use TypeScript with explicit types; avoid using
anyand casts withas".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/likec4/app/src/routes/__root.tsx` around lines 32 - 36, Replace the inline ".transform(v => v as Fqn | undefined)" cast on the focusOnElement field with a typed validator/helper to avoid using "as": create a reusable schema (e.g. fqnParam) or use z.custom<Fqn>(...) / .pipe(...) that validates/returns a Fqn | undefined and reuse it for both relationships and focusOnElement so both fields perform proper runtime validation without a TypeScript cast.packages/likec4/app/src/components/search/OverviewSearch.tsx (1)
18-21:mod+foverrides the browser's native find-in-page.Binding
mod+fis a deliberate UX choice, but be aware it replaces the browser's built-in Ctrl/Cmd+F find functionality across the overview page. If users need to search raw page text, they'll have no keyboard shortcut for it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/likec4/app/src/components/search/OverviewSearch.tsx` around lines 18 - 21, The current useHotkeys call in OverviewSearch.tsx binds 'mod+f' (['mod+f', open, { preventDefault: true }]) which overrides the browser's native find; either remove that binding or change it to a non-conflicting shortcut (e.g. 'mod+shift+f') and/or scope the handler so it only activates when the OverviewSearch component is focused (keep useHotkeys and open but update the key string or add a conditional activation option) to preserve the default Ctrl/Cmd+F behavior.packages/likec4/app/src/routes/_single/single-index.tsx (1)
58-58: Zero-width spacer is a fragile layout hack.The invisible
<div style={{ width: 0, height: 36 }}>is used to enforce minimum height alignment but is fragile — if the navigation panel's expected height changes, this value becomes stale silently.Consider using
minHeighton the parent instead, or a CSS variable if the navigation panel exposes one.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/likec4/app/src/routes/_single/single-index.tsx` at line 58, Remove the fragile zero-width spacer div (the element with style {{ width: 0, height: 36 }} used as an invisible spacer) and instead enforce the height on the containing element by applying a minHeight (or a CSS variable) to the parent wrapper in single-index.tsx; for example, set a style or class on the parent container element that uses minHeight: 36 (or reads from a CSS variable like --nav-height exposed by the navigation panel) so layout stays correct when the nav height changes. Ensure you delete the spacer div and update the parent element's style/class (or add a CSS variable usage) so alignment is driven by the container rather than a zero-width spacer.packages/diagram/src/search/hooks.ts (1)
21-26:useCallbackdepends on the entirectxobject — consider narrowing.
ctxis a new object whenever any context field changes (even unrelated ones likepickViewFor), so the returned callback's identity is unnecessarily unstable. Depending onctx.setSearchValue(which is a stable ref fromuseCallbackRef) would keep the callback identity stable across unrelated context changes.♻️ Suggested narrower dependency
export function useUpdateSearch() { const ctx = useSearchContext() + const { setSearchValue } = ctx return useCallback((search: string) => { - ctx.setSearchValue(search) - }, [ctx]) + setSearchValue(search) + }, [setSearchValue]) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/hooks.ts` around lines 21 - 26, The useCallback in useUpdateSearch currently depends on the whole ctx object from useSearchContext, making the callback unstable when unrelated context fields change; change the dependency to the stable setter by either destructuring setSearchValue from useSearchContext or referencing ctx.setSearchValue directly and use [ctx.setSearchValue] as the dependency so the returned callback (in useUpdateSearch) only re-creates when setSearchValue changes.packages/diagram/src/search/Search.tsx (2)
201-230: MissingdisplayNameonSearchOverlayBody.
Search(Line 129) andSearchPanelContent(Line 199) both setdisplayNamefor React DevTools, butSearchOverlayBodydoes not. For consistency and easier debugging:♻️ Add displayName
+SearchOverlayBody.displayName = 'SearchOverlayBody'after Line 230.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/Search.tsx` around lines 201 - 230, The memoized component SearchOverlayBody is missing a displayName which makes React DevTools less clear; add a displayName assignment for the component (e.g., set SearchOverlayBody.displayName = 'SearchOverlayBody') immediately after the SearchOverlayBody declaration so it matches the other components like Search and SearchPanelContent and improves debugging visibility.
135-199:SearchPanelContentis well-structured for reuse across diagram and overview search.The JSDoc is helpful in noting the provider requirement. The conditional
PickViewrender on Line 195 viactx.pickViewForis correct.Minor nit:
ctxis obtained on Line 137 but only used on Line 195 forpickViewFor. SinceuseSearchContext()subscribes this component to all context changes, every keystroke in the search input will re-renderSearchPanelContent. This is likely acceptable given that the child components also subscribe to the context, but if you ever want to optimize, you could extract thePickViewconditional into a small wrapper that reads onlypickViewFor.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/Search.tsx` around lines 135 - 199, SearchPanelContent subscribes to the entire search context via useSearchContext() (ctx) but only uses ctx.pickViewFor at the bottom, causing unnecessary re-renders on every context change; fix by extracting the pickViewFor conditional into a tiny child component (e.g., PickViewPicker or PickViewContainer) that calls useSearchContext(), reads only pickViewFor, and returns pickViewFor ? <PickView elementFqn={pickViewFor} /> : null, then replace the inline {ctx.pickViewFor && <PickView elementFqn={ctx.pickViewFor} />} with the new wrapper so SearchPanelContent no longer re-renders for unrelated context updates.packages/diagram/src/search/XStateSearchAdapter.tsx (1)
8-8: InconsistentuseCallbackRefimport source within the search module.
PickView.tsximportsuseCallbackReffrom'@mantine/hooks', while all other files in the search module (XStateSearchAdapter.tsx,Search.tsx,ElementsColumn.tsx) import from the local'../hooks/useCallbackRef'. For consistency across the module, alignPickView.tsxto use the local implementation instead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/XStateSearchAdapter.tsx` at line 8, PickView.tsx currently imports useCallbackRef from '@mantine/hooks' but the rest of the search module (XStateSearchAdapter.tsx, Search.tsx, ElementsColumn.tsx) use the local implementation; update the import in PickView.tsx to import useCallbackRef from '../hooks/useCallbackRef' so all files in the search module use the same local hook implementation (no other code changes needed).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@e2e/tests/overview-page.spec.ts`:
- Around line 15-19: The beforeEach readiness check in test.beforeEach uses
page.getByRole('button', { name: 'Toggle navigation' }) which fails in CI;
remove/replace this feature-specific readiness probe with a generic page
readiness check (e.g., wait for navigation or a stable main content selector)
and relocate the burger/button assertion into the specific tests that require it
(e.g., "header shows burger" or "burger opens sidebar"); alternatively, if the
header exists but the accessible name differs, either update the implementing
component's aria-label to "Toggle navigation" or change the selector in
overview-page.spec.ts to match the actual accessible name used by the component
(keep references to beforeEach, page.goto, and getByRole('button', { name:
'Toggle navigation' }) when making the change).
In `@packages/diagram/src/search/SearchContext.spec.ts`:
- Around line 34-36: The test description for the case in SearchContext.spec.ts
is misleading: it says 'returns empty string for "# " (hash + one char)' but the
actual input is '#a'; update the test description string in the it(...) call for
the test that calls normalizeSearch('#a') to accurately describe the input
(e.g., "returns empty string for '#a' (hash + one char)") so the spec message
matches the tested value; locate the test by the it(...) block that calls
normalizeSearch('#a').
In `@packages/likec4/app/src/components/search/OverviewSearch.tsx`:
- Around line 29-34: The Overlay component usage passes both
withBackdrop={false} and backdrop={{ opacity: 0.9 }} which is redundant because
the backdrop element is not rendered when withBackdrop is false; remove the
backdrop prop from the Overlay props (or alternatively change withBackdrop to
true if an opaque backdrop is intended) to ensure the computed backdrop CSS
variable is meaningful; update the usage of Overlay in OverviewSearch.tsx (look
for the Overlay JSX and its withBackdrop/backdrop props) accordingly.
In `@packages/likec4/app/src/pages/ViewReact.tsx`:
- Around line 167-179: Wrap the body of focusAndClear in a try/catch: call
diagram.focusOnElement(fqn) and router.buildAndCommitLocation inside the try,
and in catch log the error (e.g., console.error or existing logger) and
clear/reset processedRef.current so a failed focus can be retried; mirror the
error handling pattern used by OpenRelationshipBrowserFromUrl around
diagram.focusOnElement and router.buildAndCommitLocation to avoid uncaught
exceptions.
- Around line 187-191: The useUpdateEffect currently only clears processedRef
when focusOnElement becomes falsy, so changing focus to a different element
won't re-run the processing; update the useUpdateEffect that watches
focusOnElement to mirror OpenRelationshipBrowserFromUrl's behavior by invoking
process() (in addition to clearing processedRef when appropriate) so that
changes to focusOnElement trigger a re-process; reference the existing
useUpdateEffect, processedRef, focusOnElement and the process() function to
locate and update the logic.
---
Nitpick comments:
In `@e2e/tests/overview-page.spec.ts`:
- Around line 21-25: The test "header shows burger, logo, search bar, and theme
toggle" is missing the logo visibility and click-scroll-to-top assertions called
out in the PR checklist; add assertions to verify the logo is visible and that
clicking it scrolls the page to top: within the same or a new test (e.g., "logo
is visible and scrolls page to top on click") programmatically scroll the page
(window.scrollTo / page.evaluate), assert the logo element (use
page.getByRole('img', { name: /LikeC4/ }) or the app's actual aria-label/role)
is visible, click it, then assert window.scrollY resolves to 0 to confirm the
scroll-to-top behavior. Ensure the selector matches the actual logo role/name
before committing.
In `@packages/diagram/src/search/hooks.ts`:
- Around line 21-26: The useCallback in useUpdateSearch currently depends on the
whole ctx object from useSearchContext, making the callback unstable when
unrelated context fields change; change the dependency to the stable setter by
either destructuring setSearchValue from useSearchContext or referencing
ctx.setSearchValue directly and use [ctx.setSearchValue] as the dependency so
the returned callback (in useUpdateSearch) only re-creates when setSearchValue
changes.
In `@packages/diagram/src/search/Search.tsx`:
- Around line 201-230: The memoized component SearchOverlayBody is missing a
displayName which makes React DevTools less clear; add a displayName assignment
for the component (e.g., set SearchOverlayBody.displayName =
'SearchOverlayBody') immediately after the SearchOverlayBody declaration so it
matches the other components like Search and SearchPanelContent and improves
debugging visibility.
- Around line 135-199: SearchPanelContent subscribes to the entire search
context via useSearchContext() (ctx) but only uses ctx.pickViewFor at the
bottom, causing unnecessary re-renders on every context change; fix by
extracting the pickViewFor conditional into a tiny child component (e.g.,
PickViewPicker or PickViewContainer) that calls useSearchContext(), reads only
pickViewFor, and returns pickViewFor ? <PickView elementFqn={pickViewFor} /> :
null, then replace the inline {ctx.pickViewFor && <PickView
elementFqn={ctx.pickViewFor} />} with the new wrapper so SearchPanelContent no
longer re-renders for unrelated context updates.
In `@packages/diagram/src/search/XStateSearchAdapter.tsx`:
- Line 8: PickView.tsx currently imports useCallbackRef from '@mantine/hooks'
but the rest of the search module (XStateSearchAdapter.tsx, Search.tsx,
ElementsColumn.tsx) use the local implementation; update the import in
PickView.tsx to import useCallbackRef from '../hooks/useCallbackRef' so all
files in the search module use the same local hook implementation (no other code
changes needed).
In `@packages/likec4/app/src/components/search/OverviewSearch.tsx`:
- Around line 18-21: The current useHotkeys call in OverviewSearch.tsx binds
'mod+f' (['mod+f', open, { preventDefault: true }]) which overrides the
browser's native find; either remove that binding or change it to a
non-conflicting shortcut (e.g. 'mod+shift+f') and/or scope the handler so it
only activates when the OverviewSearch component is focused (keep useHotkeys and
open but update the key string or add a conditional activation option) to
preserve the default Ctrl/Cmd+F behavior.
In `@packages/likec4/app/src/routes/__root.tsx`:
- Around line 32-36: Replace the inline ".transform(v => v as Fqn | undefined)"
cast on the focusOnElement field with a typed validator/helper to avoid using
"as": create a reusable schema (e.g. fqnParam) or use z.custom<Fqn>(...) /
.pipe(...) that validates/returns a Fqn | undefined and reuse it for both
relationships and focusOnElement so both fields perform proper runtime
validation without a TypeScript cast.
In `@packages/likec4/app/src/routes/_single/single-index.tsx`:
- Line 58: Remove the fragile zero-width spacer div (the element with style {{
width: 0, height: 36 }} used as an invisible spacer) and instead enforce the
height on the containing element by applying a minHeight (or a CSS variable) to
the parent wrapper in single-index.tsx; for example, set a style or class on the
parent container element that uses minHeight: 36 (or reads from a CSS variable
like --nav-height exposed by the navigation panel) so layout stays correct when
the nav height changes. Ensure you delete the spacer div and update the parent
element's style/class (or add a CSS variable usage) so alignment is driven by
the container rather than a zero-width spacer.
9d651ab to
7010f32
Compare
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
packages/likec4/app/src/components/ColorSchemeToggle.tsx (1)
25-26: Consider conditional rendering instead of CSSdisplaytoggling for the icons.Both
<IconMoonStars>and<IconSun>are always mounted; only theirdisplayproperty differs. Conditional rendering is the idiomatic React pattern and avoids unnecessary DOM nodes.♻️ Proposed refactor
- <IconMoonStars stroke={1.5} display={computedColorScheme === 'light' ? 'block' : 'none'} /> - <IconSun stroke={1.5} display={computedColorScheme === 'dark' ? 'block' : 'none'} /> + {computedColorScheme === 'light' ? <IconMoonStars stroke={1.5} /> : <IconSun stroke={1.5} />}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/likec4/app/src/components/ColorSchemeToggle.tsx` around lines 25 - 26, The icons IconMoonStars and IconSun in ColorSchemeToggle are always mounted and toggled via CSS display; change to conditional rendering so only the active icon is rendered: inside the ColorSchemeToggle render (where computedColorScheme is used), replace the two always-mounted elements with a single conditional expression that renders IconMoonStars when computedColorScheme === 'light' and IconSun when computedColorScheme === 'dark', preserving props like stroke and any aria/role attributes so behavior and accessibility remain unchanged.packages/likec4/app/src/routes/_single/single-index.tsx (2)
77-77: Redundant responsive object — simplifypt={{ base: 'sm', sm: 'sm' }}topt="sm".Both breakpoints carry the same value; the object form adds noise without effect.
♻️ Proposed fix
- pt={{ base: 'sm', sm: 'sm' }} + pt="sm"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/likec4/app/src/routes/_single/single-index.tsx` at line 77, Replace the redundant responsive prop object used for padding-top — locate the JSX prop pt={{ base: 'sm', sm: 'sm' }} (in the component rendering around single-index.tsx) and simplify it to the scalar form pt="sm" to remove unnecessary responsive verbosity while preserving the same styling.
58-58: Zero-width spacer div — intent unclear; consider usingminHeighton the flex container.
<div style={{ width: 0, height: 36 }} aria-hidden />silently forces a 36 px minimum row height. This is non-obvious and fragile. A cleaner alternative is settingminHeightdirectly onNavigationPanel.Body(or the wrapping flex container) so the intent is self-documenting.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/likec4/app/src/routes/_single/single-index.tsx` at line 58, The inline zero-width spacer div (<div style={{ width: 0, height: 36 }} aria-hidden />) is being used to force row height; remove that div and instead apply a minHeight (e.g., minHeight: 36) to the flex container that holds the rows—preferably NavigationPanel.Body or its wrapping flex container—so the layout intent is explicit; update the relevant JSX where the spacer div appears and remove the aria-hidden spacer element.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/likec4/app/src/routes/_single/single-index.tsx`:
- Line 59: The Burger button doesn't reflect or toggle drawer state; import and
call useDrawerOpened from ../../components/sidebar/state, pass its boolean
result to the Burger's opened prop (opened={drawerOpened}), and change the
onClick from SidebarDrawerOps.open to a toggle: onClick={drawerOpened ?
SidebarDrawerOps.close : SidebarDrawerOps.open} so the icon and behavior match
the drawer state (referencing Burger, useDrawerOpened, and
SidebarDrawerOps.open/SidebarDrawerOps.close).
---
Duplicate comments:
In `@packages/likec4/app/src/components/search/OverviewSearch.tsx`:
- Around line 29-34: The Overlay usage in OverviewSearch (the Overlay component
instance using props withBackdrop and backdrop) passes backdrop={{ opacity: 0.9
}} while withBackdrop={false}, making the backdrop prop redundant; either remove
the backdrop prop from that Overlay instance or change withBackdrop to true if
you intended the overlay backdrop opacity to apply. Locate the Overlay element
in OverviewSearch.tsx and delete the backdrop prop or set withBackdrop={true}
accordingly, ensuring only the intended behavior remains.
In `@packages/likec4/app/src/pages/ViewReact.tsx`:
- Around line 167-179: The focusAndClear callback poisons processedRef.current
before calling diagram.focusOnElement(fqn) and then silently swallows
router.buildAndCommitLocation errors; wrap the body of focusAndClear in a
try/catch (similar to OpenRelationshipBrowserFromUrl) so that you only assign
processedRef.current = fqn after diagram.focusOnElement succeeds (or revert it
on error), and catch/handle errors from both diagram.focusOnElement(fqn) and
router.buildAndCommitLocation(...)—log or handle the error and avoid leaving
processedRef in a failed state to allow retries.
- Around line 187-191: The effect using useUpdateEffect currently only clears
processedRef when focusOnElement is falsy, so switching to a different element
ID doesn't retrigger focusAndClear; change the effect to clear
processedRef.current on any change of focusOnElement (i.e., remove the truthy
check) so that when focusOnElement changes the component will treat it as new
and allow focusAndClear/process() to run; update the useUpdateEffect block that
references processedRef, focusOnElement, focusAndClear and process to
unconditionally set processedRef.current = null on changes.
---
Nitpick comments:
In `@packages/likec4/app/src/components/ColorSchemeToggle.tsx`:
- Around line 25-26: The icons IconMoonStars and IconSun in ColorSchemeToggle
are always mounted and toggled via CSS display; change to conditional rendering
so only the active icon is rendered: inside the ColorSchemeToggle render (where
computedColorScheme is used), replace the two always-mounted elements with a
single conditional expression that renders IconMoonStars when
computedColorScheme === 'light' and IconSun when computedColorScheme === 'dark',
preserving props like stroke and any aria/role attributes so behavior and
accessibility remain unchanged.
In `@packages/likec4/app/src/routes/_single/single-index.tsx`:
- Line 77: Replace the redundant responsive prop object used for padding-top —
locate the JSX prop pt={{ base: 'sm', sm: 'sm' }} (in the component rendering
around single-index.tsx) and simplify it to the scalar form pt="sm" to remove
unnecessary responsive verbosity while preserving the same styling.
- Line 58: The inline zero-width spacer div (<div style={{ width: 0, height: 36
}} aria-hidden />) is being used to force row height; remove that div and
instead apply a minHeight (e.g., minHeight: 36) to the flex container that holds
the rows—preferably NavigationPanel.Body or its wrapping flex container—so the
layout intent is explicit; update the relevant JSX where the spacer div appears
and remove the aria-hidden spacer element.
| <NavigationPanel.Root css={{ position: 'relative', width: 'max-content', margin: '0' }}> | ||
| <NavigationPanel.Body> | ||
| <div style={{ width: 0, height: 36 }} aria-hidden /> | ||
| <Burger size="sm" onClick={SidebarDrawerOps.open} aria-label="Toggle navigation" /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Search for useDrawerOpened and SidebarDrawerOps definitions
rg -n "export.*useDrawerOpened|export.*SidebarDrawerOps" --type=ts --type=tsx -B2 -A3Repository: likec4/likec4
Length of output: 84
🏁 Script executed:
# Find the sidebar state file
fd "state\.(ts|tsx)" | grep -i sidebar | head -20Repository: likec4/likec4
Length of output: 108
🏁 Script executed:
# Check the current single-index.tsx around line 59
cat -n packages/likec4/app/src/routes/_single/single-index.tsx | sed -n '50,70p'Repository: likec4/likec4
Length of output: 1157
🏁 Script executed:
# Search for SidebarDrawerOps.open and .close usage patterns
rg "SidebarDrawerOps\.(open|close)" --type=ts --type=tsx -B1 -A1Repository: likec4/likec4
Length of output: 84
🏁 Script executed:
# Read the sidebar state file to check exports
cat -n packages/likec4/app/src/components/sidebar/state.tsRepository: likec4/likec4
Length of output: 542
🏁 Script executed:
# Search for SidebarDrawerOps and useDrawerOpened without file type restriction
rg "SidebarDrawerOps|useDrawerOpened" -B2 -A3 | head -100Repository: likec4/likec4
Length of output: 5529
🏁 Script executed:
# Check the imports in single-index.tsx
cat -n packages/likec4/app/src/routes/_single/single-index.tsx | sed -n '1,30p'Repository: likec4/likec4
Length of output: 1566
Add opened prop to Burger to reflect drawer state visually.
The Burger component at line 59 lacks the opened prop required to toggle between ☰ and ✕ icons. Without it, the icon remains ☰ even when the drawer is open, creating a visual inconsistency and accessibility gap. The useDrawerOpened() hook is available from ../../components/sidebar/state and should be used to pass the drawer state.
Additionally, onClick={SidebarDrawerOps.open} only opens the drawer; it cannot close it. Wire the click handler to toggle: onClick={drawerOpened ? SidebarDrawerOps.close : SidebarDrawerOps.open} for a better UX.
Proposed fix
-import { SidebarDrawerOps } from '../../components/sidebar/state'
+import { SidebarDrawerOps, useDrawerOpened } from '../../components/sidebar/state'
function RouteComponent() {
+ const drawerOpened = useDrawerOpened()
...
<Burger
size="sm"
+ opened={drawerOpened}
- onClick={SidebarDrawerOps.open}
+ onClick={drawerOpened ? SidebarDrawerOps.close : SidebarDrawerOps.open}
aria-label="Toggle navigation"
/>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/likec4/app/src/routes/_single/single-index.tsx` at line 59, The
Burger button doesn't reflect or toggle drawer state; import and call
useDrawerOpened from ../../components/sidebar/state, pass its boolean result to
the Burger's opened prop (opened={drawerOpened}), and change the onClick from
SidebarDrawerOps.open to a toggle: onClick={drawerOpened ?
SidebarDrawerOps.close : SidebarDrawerOps.open} so the icon and behavior match
the drawer state (referencing Burger, useDrawerOpened, and
SidebarDrawerOps.open/SidebarDrawerOps.close).
There was a problem hiding this comment.
Not applicable — intentional design choice.
There was a problem hiding this comment.
@ckeller42, understood! I respect your design decision for the Burger button behavior.
🧠 Learnings used
Learnt from: CR
Repo: likec4/likec4 PR: 0
File: packages/core/AGENTS.md:0-0
Timestamp: 2026-02-11T10:07:53.330Z
Learning: Applies to packages/core/src/manual-layout/**/*.{ts,tsx,js,jsx} : When changes relate to views (types or models), update View-drifts detection/auto-applying in `src/manual-layout` accordingly. If unsure what leads to layout drifts or can be auto-applied, ask for confirmation.
95decc3 to
99eb158
Compare
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (4)
packages/diagram/src/search/hooks.ts (1)
16-18: Consider adding an explicit return type touseNormalizedSearch.Currently the return type is inferred as
string. Adding an explicit annotation aligns with the project's guideline to use explicit types.✨ Proposed fix
-export function useNormalizedSearch() { +export function useNormalizedSearch(): string {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/hooks.ts` around lines 16 - 18, The hook useNormalizedSearch currently relies on type inference; update its signature to include an explicit return type (string) to satisfy the project's typing guideline: annotate the function declaration (useNormalizedSearch) to return string and ensure the return expression using useDeferredValue(ctx.normalizedSearch) matches that type (ctx.normalizedSearch / useDeferredValue usage remains unchanged).packages/diagram/src/search/components/SearchInput.tsx (1)
26-42:useSearch()anduseSearchContext()both calluseContext(SearchContext)— one of the two calls is redundant.
useSearchinternally callsuseSearchContext(), so importing both causesSearchContextto be read twice per render. The component can get everything it needs fromsearchCtxdirectly.♻️ Proposed fix
-import { useSearch } from '../hooks' import { useSearchContext } from '../SearchContext'- const [search, setSearch] = useSearch() + const [search, setSearch] = [searchCtx.searchValue, searchCtx.setSearchValue]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/components/SearchInput.tsx` around lines 26 - 42, The component reads SearchContext twice: via useSearchContext() into searchCtx and via useSearch() (which itself calls useSearchContext()); remove the redundant useSearch() call and import, and instead pull needed values (e.g., search and setSearch) directly from searchCtx inside LikeC4SearchInput; update any references to the local search/setSearch variables to use the properties from searchCtx and remove the unused useSearch import and its [search, setSearch] destructuring to avoid double context reads.packages/diagram/src/search/Search.tsx (1)
70-77:selectIsOpenedsilently swallows errors — consider logging the return value.The try/catch in
selectIsOpenedcatches all exceptions and returnsfalse, which means any actor state corruption would silently hide the search overlay. Theconsole.erroris good, but since this selector runs on every actor state change, a misbehaving actor could spam the console. Consider whether a single-fire warning or a more specific catch condition would be more appropriate.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/Search.tsx` around lines 70 - 77, The selector selectIsOpened currently wraps all access in a broad try/catch which swallows errors and may spam console.error; replace the blanket try/catch with a defensive guard that checks the shape of the snapshot (e.g., verify s !== null/undefined and typeof s.status === 'string' and typeof s.value === 'string') before reading s.status and s.value, and only catch and log unexpected exceptions (or log once) to avoid repeated console spam; update the function that references SearchActorSnapshot (selectIsOpened) to perform these explicit checks and emit a single, clear warning when an invalid snapshot is encountered instead of returning false silently on every exception.packages/likec4/app/src/routes/_single/single-index.tsx (1)
58-58: Consider using a more descriptive approach for the spacer element.
<div style={{ width: 0, height: 36 }} aria-hidden />is a zero-width invisible spacer purely for layout alignment. A brief comment explaining its purpose (e.g., maintaining consistent header height) would aid future maintainers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/likec4/app/src/routes/_single/single-index.tsx` at line 58, The spacer div (<div style={{ width: 0, height: 36 }} aria-hidden />) is unclear—add a short explanatory comment above it stating its purpose (e.g., "spacer to maintain consistent header height for layout alignment") and either give it a clear semantic name by replacing it with a small reusable Spacer component (e.g., Spacer height={36}) or at minimum normalize attributes to aria-hidden="true" and a CSS class (e.g., .spacer--header) so future maintainers understand its role; update references in single-index.tsx accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.changeset/overview-search-bar.md:
- Around line 2-3: Update the changeset to bump the package '@likec4/diagram'
from "patch" to "minor" because new public exports were added; locate the entry
with the package string '@likec4/diagram' in the changeset and change its
version bump value to "minor" (leave 'likec4' as-is unless its public API also
changed). Ensure the updated changeset reflects the additive, non-breaking
exports: SearchContext, useSearchContext, normalizeSearch, SearchPanelContent,
and the CSS tokens (searchDialogCss, searchBodyCss).
In `@packages/diagram/src/search/SearchContext.tsx`:
- Line 17: The interface property currentViewId is typed as string | null but
should use the branded type ViewId for consistency; update the declaration of
currentViewId to use ViewId | null (replace string | null) so it matches other
usages and preserves the branded-type guarantee in SearchContext (referencing
currentViewId and the imported ViewId).
In `@packages/likec4/app/src/components/search/OverviewSearchAdapter.tsx`:
- Around line 38-49: The memoized SearchContextValue returned by useMemo is
missing setSearchValue in its dependency array; while stable, add setSearchValue
to the deps to satisfy react-hooks/exhaustive-deps and oxlint. Update the
useMemo call that produces value (the object including searchValue,
setSearchValue, normalizedSearch, navigateTo, openPickView, closePickView,
pickViewFor, close/onClose, currentViewId, openedWithSearch) so setSearchValue
is included in the dependency array alongside searchValue, pickViewFor,
navigateTo, openPickView, closePickView, and onClose.
In `@packages/likec4/app/src/components/sidebar/DiagramsTree.tsx`:
- Around line 64-66: The navigation target string is incorrect—update the route
used where the navigation object is built (the object containing keys "to",
"viewTransition", and "params" that references viewId in DiagramsTree.tsx) so
that "to" uses the correct route prefix: change '/view/$viewId/' to
'/_single/view/$viewId/' (keeping the trailing slash per trailingSlash:
"always") and ensure params still provide viewId.
---
Duplicate comments:
In `@e2e/tests/overview-page.spec.ts`:
- Around line 15-20: Remove the feature-specific readiness probe from
test.beforeEach (the expect using getByRole('button', { name: 'Toggle
navigation' })) and replace it with a generic readiness check (e.g., await
page.waitForLoadState('networkidle') or await expect(page.locator('main') / a
generic app root test id). Then move the burger-button assertion
(getByRole('button', { name: 'Toggle navigation' })) into only those individual
tests that actually need the navigation toggle so other tests no longer fail on
that element being absent.
In `@packages/likec4/app/src/components/search/OverviewSearch.tsx`:
- Around line 28-33: The Overlay usage passes backdrop={{ opacity: 0.9 }} while
also setting withBackdrop={false}, which is redundant; update the Overlay
invocation in OverviewSearch.tsx (the Overlay component props) to either remove
the backdrop prop if no backdrop is intended, or change withBackdrop to true if
the opaque backdrop is desired so the backdrop.opacity takes effect.
In `@packages/likec4/app/src/pages/ViewReact.tsx`:
- Around line 191-195: The update effect only clears processedRef when
focusOnElement becomes falsy, so changing focusOnElement to a new FQN never
triggers processing; change the useUpdateEffect (the hook that currently checks
focusOnElement) to handle both cases: if focusOnElement is null/undefined set
processedRef.current = null, otherwise call the same handler used for initial
focus (e.g., focusAndClear(focusOnElement) or process()) to re-process the new
FQN; mirror the unconditional process() behavior from
OpenRelationshipBrowserFromUrl to ensure new FQNs are handled.
In `@packages/likec4/app/src/routes/_single/single-index.tsx`:
- Line 59: The Burger currently only calls SidebarDrawerOps.open and lacks the
opened prop, so change its onClick to toggle the drawer (call
SidebarDrawerOps.toggle or call open/close based on current state) and pass the
boolean opened prop to reflect visual state; locate the Burger usage in
single-index.tsx and wire it to the same drawer state/controller used by
SidebarDrawerOps (use SidebarDrawerOps.isOpen or subscribe to the drawer state)
so the button both toggles open/close and receives opened={isOpen} (or
equivalent) to update its aria/visual state.
---
Nitpick comments:
In `@packages/diagram/src/search/components/SearchInput.tsx`:
- Around line 26-42: The component reads SearchContext twice: via
useSearchContext() into searchCtx and via useSearch() (which itself calls
useSearchContext()); remove the redundant useSearch() call and import, and
instead pull needed values (e.g., search and setSearch) directly from searchCtx
inside LikeC4SearchInput; update any references to the local search/setSearch
variables to use the properties from searchCtx and remove the unused useSearch
import and its [search, setSearch] destructuring to avoid double context reads.
In `@packages/diagram/src/search/hooks.ts`:
- Around line 16-18: The hook useNormalizedSearch currently relies on type
inference; update its signature to include an explicit return type (string) to
satisfy the project's typing guideline: annotate the function declaration
(useNormalizedSearch) to return string and ensure the return expression using
useDeferredValue(ctx.normalizedSearch) matches that type (ctx.normalizedSearch /
useDeferredValue usage remains unchanged).
In `@packages/diagram/src/search/Search.tsx`:
- Around line 70-77: The selector selectIsOpened currently wraps all access in a
broad try/catch which swallows errors and may spam console.error; replace the
blanket try/catch with a defensive guard that checks the shape of the snapshot
(e.g., verify s !== null/undefined and typeof s.status === 'string' and typeof
s.value === 'string') before reading s.status and s.value, and only catch and
log unexpected exceptions (or log once) to avoid repeated console spam; update
the function that references SearchActorSnapshot (selectIsOpened) to perform
these explicit checks and emit a single, clear warning when an invalid snapshot
is encountered instead of returning false silently on every exception.
In `@packages/likec4/app/src/routes/_single/single-index.tsx`:
- Line 58: The spacer div (<div style={{ width: 0, height: 36 }} aria-hidden />)
is unclear—add a short explanatory comment above it stating its purpose (e.g.,
"spacer to maintain consistent header height for layout alignment") and either
give it a clear semantic name by replacing it with a small reusable Spacer
component (e.g., Spacer height={36}) or at minimum normalize attributes to
aria-hidden="true" and a CSS class (e.g., .spacer--header) so future maintainers
understand its role; update references in single-index.tsx accordingly.
| closePickView: () => void | ||
| pickViewFor: Fqn | null | ||
| close: () => void | ||
| currentViewId: string | null |
There was a problem hiding this comment.
currentViewId should be typed ViewId | null for precision.
ViewId is already imported on line 5 and used on line 12; widening currentViewId to string | null loses the branded-type guarantee and creates an inconsistency within the same interface.
🔧 Proposed fix
- currentViewId: string | null
+ currentViewId: ViewId | null📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| currentViewId: string | null | |
| currentViewId: ViewId | null |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/diagram/src/search/SearchContext.tsx` at line 17, The interface
property currentViewId is typed as string | null but should use the branded type
ViewId for consistency; update the declaration of currentViewId to use ViewId |
null (replace string | null) so it matches other usages and preserves the
branded-type guarantee in SearchContext (referencing currentViewId and the
imported ViewId).
There was a problem hiding this comment.
Not applicable — intentional design choice.
There was a problem hiding this comment.
@ckeller42, understood — I've noted that currentViewId: string | null in SearchContext is an intentional design choice.
✏️ Learnings added
Learnt from: ckeller42
Repo: likec4/likec4 PR: 2665
File: packages/diagram/src/search/SearchContext.tsx:17-17
Timestamp: 2026-02-22T11:28:39.655Z
Learning: In packages/diagram/src/search/SearchContext.tsx, the currentViewId property in SearchContextValue interface is intentionally typed as `string | null` rather than `ViewId | null`, per explicit design decision.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
| const value = useMemo((): SearchContextValue => ({ | ||
| searchValue, | ||
| setSearchValue, | ||
| normalizedSearch: normalizeSearch(searchValue), | ||
| navigateTo, | ||
| openPickView, | ||
| closePickView, | ||
| pickViewFor, | ||
| close: onClose, | ||
| currentViewId: null, | ||
| openedWithSearch: null, | ||
| }), [searchValue, pickViewFor, navigateTo, openPickView, closePickView, onClose]) |
There was a problem hiding this comment.
setSearchValue is included in the context value but omitted from useMemo deps.
useState setters have a guaranteed stable identity, so this is functionally correct. However, the react-hooks/exhaustive-deps rule (enforced by oxlint) will likely flag the omission. Adding it removes the lint warning without any behavioral change.
🛠️ Proposed fix
- }), [searchValue, pickViewFor, navigateTo, openPickView, closePickView, onClose])
+ }), [searchValue, setSearchValue, pickViewFor, navigateTo, openPickView, closePickView, onClose])📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const value = useMemo((): SearchContextValue => ({ | |
| searchValue, | |
| setSearchValue, | |
| normalizedSearch: normalizeSearch(searchValue), | |
| navigateTo, | |
| openPickView, | |
| closePickView, | |
| pickViewFor, | |
| close: onClose, | |
| currentViewId: null, | |
| openedWithSearch: null, | |
| }), [searchValue, pickViewFor, navigateTo, openPickView, closePickView, onClose]) | |
| const value = useMemo((): SearchContextValue => ({ | |
| searchValue, | |
| setSearchValue, | |
| normalizedSearch: normalizeSearch(searchValue), | |
| navigateTo, | |
| openPickView, | |
| closePickView, | |
| pickViewFor, | |
| close: onClose, | |
| currentViewId: null, | |
| openedWithSearch: null, | |
| }), [searchValue, setSearchValue, pickViewFor, navigateTo, openPickView, closePickView, onClose]) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/likec4/app/src/components/search/OverviewSearchAdapter.tsx` around
lines 38 - 49, The memoized SearchContextValue returned by useMemo is missing
setSearchValue in its dependency array; while stable, add setSearchValue to the
deps to satisfy react-hooks/exhaustive-deps and oxlint. Update the useMemo call
that produces value (the object including searchValue, setSearchValue,
normalizedSearch, navigateTo, openPickView, closePickView, pickViewFor,
close/onClose, currentViewId, openedWithSearch) so setSearchValue is included in
the dependency array alongside searchValue, pickViewFor, navigateTo,
openPickView, closePickView, and onClose.
There was a problem hiding this comment.
Not applicable — intentional design choice.
There was a problem hiding this comment.
@ckeller42 Understood, I'll note your preference. Just be aware that oxlint/exhaustive-deps may flag this during CI if enabled.
| to: '/view/$viewId/', | ||
| viewTransition: false, | ||
| params: (p) => ({ | ||
| ...p, | ||
| viewId, | ||
| }), | ||
| params: { viewId }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Search for route definitions with /view/$viewId pattern
rg -n "view/\\\$viewId|view/\$viewId" --type ts --type tsx -A 2 -B 2Repository: likec4/likec4
Length of output: 84
🏁 Script executed:
# Search for trailingSlash configuration
rg -n "trailingSlash" --type ts --type tsx -B 3 -A 3Repository: likec4/likec4
Length of output: 84
🏁 Script executed:
# Check TanStack Router version in package.json
fd -n "package.json" -x cat {}Repository: likec4/likec4
Length of output: 287
🏁 Script executed:
# Search for other navigate() calls or Link components referencing this route
rg -n "to:.*view" --type ts --type tsx | head -20Repository: likec4/likec4
Length of output: 84
🏁 Script executed:
# Search for route definitions with /view/$viewId pattern
rg "view/\\\$viewId|view/\$viewId" -A 2 -B 2Repository: likec4/likec4
Length of output: 13068
🏁 Script executed:
# Search for trailingSlash configuration
rg "trailingSlash" -B 3 -A 3Repository: likec4/likec4
Length of output: 1020
🏁 Script executed:
# Check TanStack Router version in package.json
cat package.json | grep -A 5 -B 5 "router"Repository: likec4/likec4
Length of output: 39
🏁 Script executed:
# Search for route definitions/configuration files
fd "route" | grep -E "\.(ts|js)$" | head -20Repository: likec4/likec4
Length of output: 136
🏁 Script executed:
# Look for the DiagramsTree.tsx file and surrounding context
fd "DiagramsTree" -x cat {}Repository: likec4/likec4
Length of output: 5964
🏁 Script executed:
# Search for any redirects or route handling for /view/ path
rg "/view/\$viewId" --context 5Repository: likec4/likec4
Length of output: 39
🏁 Script executed:
# Check for any catch-all routes or route structure
fd "route" | xargs ls -laRepository: likec4/likec4
Length of output: 2948
🏁 Script executed:
# Look for router configuration or middleware that might handle /view/ redirects
rg "redirect|/view/" packages/likec4/app/src/router.tsx -B 5 -A 5Repository: likec4/likec4
Length of output: 39
🏁 Script executed:
# Check the _single/route.tsx to see if there's a layout or redirect logic
cat packages/likec4/app/src/routes/_single/route.tsxRepository: likec4/likec4
Length of output: 1244
🏁 Script executed:
# Check if there's any catch-all route or redirect in __root.tsx
cat packages/likec4/app/src/routes/__root.tsxRepository: likec4/likec4
Length of output: 2372
🏁 Script executed:
# Search for any route files that might handle /view/ specifically
fd "\.tsx\$" packages/likec4/app/src/routes | xargs grep -l "'/view" | head -10Repository: likec4/likec4
Length of output: 112
🏁 Script executed:
# Check single-index.tsx to see if it defines /view/$viewId
cat packages/likec4/app/src/routes/_single/single-index.tsxRepository: likec4/likec4
Length of output: 4698
🏁 Script executed:
# Search for all files in _single directory
ls -la packages/likec4/app/src/routes/_single/Repository: likec4/likec4
Length of output: 1035
🏁 Script executed:
# Check if there's a view.$viewId.tsx file in _single directory (not nested)
ls -la packages/likec4/app/src/routes/_single/ | grep "view"Repository: likec4/likec4
Length of output: 609
🏁 Script executed:
# Check view.$viewId.index.tsx to see what route it defines
cat packages/likec4/app/src/routes/_single/view.$viewId.index.tsxRepository: likec4/likec4
Length of output: 143
🏁 Script executed:
# Check view.$viewId.tsx to see what route it defines
cat packages/likec4/app/src/routes/_single/view.$viewId.tsxRepository: likec4/likec4
Length of output: 137
🏁 Script executed:
# List the files with their actual names
ls -1 packages/likec4/app/src/routes/_single/view.* | while read f; do echo "=== $f ==="; cat "$f"; doneRepository: likec4/likec4
Length of output: 4284
Fix navigation path to include route prefix.
The route path /view/$viewId/ does not exist. All view routes are defined under the /_single/ prefix: /_single/view/$viewId/. Update the navigation to use the correct path /_single/view/$viewId/ to match the actual route definition. The trailing slash is appropriate per the router's trailingSlash: 'always' configuration.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/likec4/app/src/components/sidebar/DiagramsTree.tsx` around lines 64
- 66, The navigation target string is incorrect—update the route used where the
navigation object is built (the object containing keys "to", "viewTransition",
and "params" that references viewId in DiagramsTree.tsx) so that "to" uses the
correct route prefix: change '/view/$viewId/' to '/_single/view/$viewId/'
(keeping the trailing slash per trailingSlash: "always") and ensure params still
provide viewId.
There was a problem hiding this comment.
Not applicable — intentional design choice.
There was a problem hiding this comment.
@ckeller42, understood—thanks for clarifying that the navigation path is intentional. I'll note this for context.
🧠 Learnings used
Learnt from: CR
Repo: likec4/likec4 PR: 0
File: packages/core/AGENTS.md:0-0
Timestamp: 2026-02-11T10:07:53.330Z
Learning: Applies to packages/core/src/manual-layout/**/*.{ts,tsx,js,jsx} : When changes relate to views (types or models), update View-drifts detection/auto-applying in `src/manual-layout` accordingly. If unsure what leads to layout drifts or can be auto-applied, ask for confirmation.
99eb158 to
4fb3d13
Compare
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (14)
packages/likec4/app/src/routes/__root.tsx (1)
32-36:as Fqn | undefinedcast duplicates the same guideline violation inrelationships.Both
focusOnElementandrelationshipsuse.transform(v => v as Fqn | undefined)purely to satisfy the TypeScript type, which is a bareas-cast the coding guidelines ask to avoid. SinceFqnis a brandedstring, a small refine or a shared helper would be more type-safe.♻️ Extract a shared Zod schema for nullable Fqn params
Define once above
searchParamsSchema:+const fqnParam = z.string().nonempty().optional().catch(undefined).transform( + (v): Fqn | undefined => (v ? (v as Fqn) : undefined), +) + const searchParamsSchema = z.object({ ... relationships: z.string() - .nonempty() - .optional() - .catch(undefined) - .transform(v => v as Fqn | undefined), + .nonempty() + .optional() + .catch(undefined) + .transform((v): Fqn | undefined => (v ? (v as Fqn) : undefined)), focusOnElement: z.string() - .nonempty() - .optional() - .catch(undefined) - .transform(v => v as Fqn | undefined), + .nonempty() + .optional() + .catch(undefined) + .transform((v): Fqn | undefined => (v ? (v as Fqn) : undefined)),Or, if
Fqnvalidation logic is ever centralised, replace both with a sharedfqnParamschema entirely. This at minimum makes the return type explicit via a return-type annotation rather than a bareas.As per coding guidelines,
**/*.{ts,tsx}: "avoid usinganyand casts withas".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/likec4/app/src/routes/__root.tsx` around lines 32 - 36, The transform cast ".transform(v => v as Fqn | undefined)" used in searchParamsSchema fields focusOnElement and relationships violates the no-`as` guideline; replace both with a shared Zod schema (e.g., fqnParam) that validates/refines a branded Fqn (use .nonempty().optional().nullable() or .refine(...) to enforce the Fqn shape) and export/use that in place of the inline transforms so the resulting type is explicit without casting; update searchParamsSchema to use fqnParam (or annotate the transform return type explicitly) for both focusOnElement and relationships to remove the bare `as` cast while keeping runtime validation.packages/likec4/app/src/pages/ViewReact.tsx (1)
180-182: Optional: align error-level and catch-variable naming with sibling component.
FocusElementFromUrlusesconsole.warnandcatch (e)whileOpenRelationshipBrowserFromUrlusesconsole.errorandcatch (error). Both represent failed operations; aligning them avoids surprises when triaging logs.🔧 Optional alignment
- } catch (e) { - console.warn('focusOnElement failed:', e) + } catch (error) { + console.error('focusOnElement failed:', error) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/likec4/app/src/pages/ViewReact.tsx` around lines 180 - 182, In FocusElementFromUrl, change the catch block to use catch (error) and log with console.error instead of console.warn so it matches the sibling OpenRelationshipBrowserFromUrl; update the catch variable name from e to error and replace console.warn('focusOnElement failed:', e) with a console.error call that includes the error variable for consistent logging and easier triage.packages/diagram/src/search/SearchContext.spec.ts (1)
43-46: Optional: add'kind:a'boundary test to match the#prefix coverage.The
#prefix tests explicitly document the 1-char boundary ('#a'→''). Thekind:prefix tests jump from the empty case ('kind:') to a full word ('kind:system'), leaving the 1-char-after-prefix behavior ('kind:a') unspecified in the suite.💡 Suggested addition
it('handles kind: prefix normally', () => { expect(normalizeSearch('kind:')).toBe('kind:') + expect(normalizeSearch('kind:a')).toBe('kind:a') // 1 char after prefix — document expected behavior expect(normalizeSearch('kind:system')).toBe('kind:system') })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/SearchContext.spec.ts` around lines 43 - 46, Add a one-character-after-prefix boundary test to the existing "handles kind: prefix normally" spec: update the test for normalizeSearch (the test named 'handles kind: prefix normally') to include an assertion that normalizeSearch('kind:a') returns 'kind:a' to mirror the explicit 1-char coverage used for the '#' prefix tests; this ensures the kind: prefix case is explicitly verified at the 1-char boundary.packages/diagram/src/search/components/PickView.tsx (1)
38-40: Thin wrapper can be removed.
closePickViewis a single-expression wrapper with no extra logic; you can drop the indirection.♻️ Simplification
- const closePickView = () => { - ctx.closePickView() - } + const { closePickView } = ctx🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/components/PickView.tsx` around lines 38 - 40, Remove the redundant thin wrapper function closePickView and call ctx.closePickView directly wherever closePickView is referenced (e.g., event handlers, props, or callbacks); delete the closePickView function declaration and update all usages to use ctx.closePickView to eliminate the indirection while preserving current behavior.packages/likec4/app/src/routes/_single/single-index.tsx (1)
58-58: Consider replacing the invisible height-anchor div with CSS.
<div style={{ width: 0, height: 36 }} aria-hidden />is a flex-height trick that requires reading the DOM diff to understand. AminHeightonNavigationPanel.Body(or an equivalent CSS token) would be more self-documenting.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/likec4/app/src/routes/_single/single-index.tsx` at line 58, Replace the invisible spacer div (<div style={{ width: 0, height: 36 }} aria-hidden />) with a declarative CSS rule on NavigationPanel.Body (or an equivalent component) by adding a minHeight: 36px (or using the project's spacing token) to NavigationPanel.Body's style or CSS class; remove the inline spacer and ensure the component's CSS is documented/uses the design token so intent (height anchor) is clear and no DOM-only spacer is required.packages/diagram/src/search/hooks.ts (1)
21-25:useUpdateSearchwraps a stable setter in a redundantuseCallback.Since
setSearchValueis either auseStatesetter or auseCallbackRefresult (both have guaranteed stable identity), theuseCallbacklayer adds no value and the hook is functionally equivalent to returningsetSearchValuedirectly.♻️ Optional simplification
export function useUpdateSearch() { - const { setSearchValue } = useSearchContext() - return useCallback((search: string) => { - setSearchValue(search) - }, [setSearchValue]) + return useSearchContext().setSearchValue }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/hooks.ts` around lines 21 - 25, The hook useUpdateSearch wraps the stable setter setSearchValue in an unnecessary useCallback; simplify by removing the useCallback wrapper and return setSearchValue directly from useUpdateSearch (ensure the returned value preserves the same signature/type as setSearchValue). Locate the useUpdateSearch function and replace the useCallback-based return with a direct return of setSearchValue.packages/diagram/src/search/XStateSearchAdapter.tsx (1)
8-8:useCallbackRefimplementations provide identical stable-reference guarantees.Both the local
packages/diagram/src/hooks/useCallbackRef.tsand@mantine/hooksversions return referentially stable functions using the same pattern (storing the callback in a ref and returning a stable wrapper). TheuseMemodeps comment on lines 66–71 is valid—all five callback refs are guaranteed stable across renders.The package contains two sources:
XStateSearchAdapteruses the local hook (correct for code reuse), while the olderPickView.tsxuses@mantine/hooks. Consider consolidating to the local implementation for consistency across the package.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/XStateSearchAdapter.tsx` at line 8, The codebase has two equivalent implementations of the same stable-callback hook—local useCallbackRef (packages/diagram/src/hooks/useCallbackRef.ts) and the `@mantine/hooks` version used in PickView.tsx—causing inconsistency; consolidate by switching PickView.tsx to import and use the local useCallbackRef so both components (XStateSearchAdapter and PickView) rely on the same implementation, update the import in PickView.tsx to reference useCallbackRef, remove the `@mantine/hooks` dependency usage for this hook, and ensure any existing comments about stable references remain accurate (the useMemo dependency comment in XStateSearchAdapter can be kept as-is).packages/diagram/src/search/Search.tsx (6)
135-199: Destructure onlypickViewForfrom the context.
const ctx = useSearchContext()retrieves the full context object but onlyctx.pickViewForis consumed at line 195. Destructuring at the use-site is more idiomatic.♻️ Proposed change
- const ctx = useSearchContext() + const { pickViewFor } = useSearchContext() ... - {ctx.pickViewFor && <PickView elementFqn={ctx.pickViewFor} />} + {pickViewFor && <PickView elementFqn={pickViewFor} />}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/Search.tsx` around lines 135 - 199, Destructure pickViewFor directly from the search context instead of grabbing the full ctx object; replace "const ctx = useSearchContext()" with destructuring like "const { pickViewFor } = useSearchContext()" and then use pickViewFor in the JSX (e.g., {pickViewFor && <PickView elementFqn={pickViewFor} />}) so only the needed value from useSearchContext is referenced in SearchPanelContent.
201-229: AdddisplayNametoSearchOverlayBodyfor consistency.
SearchandSearchPanelContentboth havedisplayNameset after their definitions;SearchOverlayBodyis the onlymemo-wrapped component in this file without one, which makes React DevTools output inconsistent.♻️ Proposed change
}) +SearchOverlayBody.displayName = 'SearchOverlayBody'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/Search.tsx` around lines 201 - 229, SearchOverlayBody (the memo-wrapped component) lacks a displayName, causing inconsistent React DevTools output; after the SearchOverlayBody definition add a displayName property (e.g., SearchOverlayBody.displayName = 'SearchOverlayBody') so it matches the pattern used for Search and SearchPanelContent and helps debugging—locate the memo<{ searchActorRef: SearchActorRef }>(({}) => { ... }) definition and set the displayName immediately after it.
201-229: AdddisplayNametoSearchOverlayBodyfor React DevTools consistency.
Search(line 129) andSearchPanelContent(line 199) both havedisplayNameset after theirmemocalls.SearchOverlayBodyis the onlymemo-wrapped component in this file without one.♻️ Proposed change
}) +SearchOverlayBody.displayName = 'SearchOverlayBody'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/Search.tsx` around lines 201 - 229, Add a displayName to the memo-wrapped component by setting the SearchOverlayBody.displayName to "SearchOverlayBody" immediately after the memo declaration so React DevTools shows a consistent name like the other components (see Search and SearchPanelContent which already set displayName after their memo calls).
131-134: Convert the block comment aboveSearchPanelContentto a JSDoc comment.The existing
/* */block is not recognized by tooling as JSDoc. SinceSearchPanelContentis a public export, the guidelines require/** */style.📝 Proposed change
-/** - * The shared search panel content used by both diagram and overview search. - * Must be rendered inside a SearchContext.Provider. - */ +/** + * The shared search panel content used by both diagram and overview search. + * + * `@remarks` Must be rendered inside a {`@link` SearchContext.Provider}. + */As per coding guidelines: "Use JSDoc to document public classes and methods."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/Search.tsx` around lines 131 - 134, The block comment immediately above the exported SearchPanelContent component is using a plain /* */ comment; change it to a JSDoc /** */ comment so tooling recognizes it as documentation for the public export (i.e., replace the leading /* ... */ with /** ... */ and keep the same text directly above the SearchPanelContent declaration).
137-137: Destructure onlypickViewForfrom the context.
ctxis declared but onlyctx.pickViewForis consumed. Destructuring at the point of use avoids an implicit dependency on the full context.♻️ Proposed change
- const ctx = useSearchContext() + const { pickViewFor } = useSearchContext() ... - {ctx.pickViewFor && <PickView elementFqn={ctx.pickViewFor} />} + {pickViewFor && <PickView elementFqn={pickViewFor} />}Also applies to: 195-195
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/Search.tsx` at line 137, The code currently assigns the whole context to ctx via useSearchContext() but only uses ctx.pickViewFor; change the call to destructure the specific member: const { pickViewFor } = useSearchContext(); then update references from ctx.pickViewFor to pickViewFor and remove the unused ctx variable; apply the same change for the other occurrence that similarly reads ctx (the second useSearchContext call that only uses pickViewFor).
56-68:height: ['100%', '100cqh']generates a breakpoint-scoped media query, not a CSS fallback.Panda CSS uses a mobile-first breakpoint system and leverages
min-widthmedia queries when you write responsive styles. The two-element array['100%', '100cqh']therefore compiles toheight: 100%at the base breakpoint andheight: 100cqhonly atsm+ (≈ 640 px), not as a same-breakpoint progressive-enhancement fallback.If the intent was a browser-capability fallback (apply
100cqheverywhere, with100%as the fallback for browsers without container-query support), the correct approach is to emit both rules under the same condition:♻️ Proposed alternatives
-export const scrollAreaCss = css({ - height: [ - '100%', - '100cqh', - ], +export const scrollAreaCss = css({ + // Option A — single value; browsers without CQ support get 0 (usually acceptable here) + height: '100cqh', + + // Option B — explicit `@supports` fallback if needed: + // height: '100%', + // '@supports (height: 100cqh)': { height: '100cqh' },If the responsive (viewport-breakpoint) difference is intentional, please add a comment explaining the two viewport behaviours.
Based on learnings from Panda CSS official documentation: "Panda uses a mobile-first breakpoint system and leverages min-width media queries
@media(min-width)when you write responsive styles."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/Search.tsx` around lines 56 - 68, The current responsive array in scrollAreaCss (height: ['100%', '100cqh']) creates a breakpoint media query instead of a same-breakpoint fallback; replace that array with a fallback + capability check: set height: '100%' at the base, then add an `@supports` block inside scrollAreaCss that sets height: '100cqh' (so browsers that understand cqh get the 100cqh value while others keep 100%), keeping the rest of the css ('.mantine-ScrollArea-viewport' and its children) unchanged.packages/diagram/src/search/SearchContext.tsx (1)
23-29: Add JSDoc to the publicuseSearchContexthook.
normalizeSearchis documented (lines 31–34) butuseSearchContext— also a public export — is not.📝 Suggested JSDoc
+/** + * Returns the current {`@link` SearchContextValue} from the nearest SearchContext.Provider. + * `@throws` {Error} If called outside a SearchContext.Provider. + */ export function useSearchContext(): SearchContextValue {As per coding guidelines: "Use JSDoc to document public classes and methods."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/SearchContext.tsx` around lines 23 - 29, Add a JSDoc block above the exported useSearchContext hook describing that it is a public React hook which returns the current SearchContextValue, lists its return type (SearchContextValue), and documents the runtime error behavior (throws if called outside a SearchContext.Provider). Reference the exported function name useSearchContext and the SearchContext.Provider in the doc comment so readers know the hook must be used within that provider and what value it returns.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@e2e/tests/overview-page.spec.ts`:
- Around line 34-37: The test "Ctrl+K opens search overlay" hardcodes
'Control+k' but the app registers the shortcut as 'mod+k' (Mantine useHotkeys)
which maps to Cmd+K on macOS; update the test to send the appropriate key based
on the running OS (e.g., use process.platform === 'darwin' ? 'Meta+k' :
'Control+k'), or alternatively send both variants (Meta+k then Control+k) before
asserting the textbox visibility; update the test name or add a brief comment to
clarify the OS-dependent behavior and reference the test function name and the
'mod+k' / useHotkeys mapping in the change.
- Around line 22-26: The test "header shows burger, logo, search bar, and theme
toggle" is missing an assertion for the logo; update the test body (the test
function) to include an expectation that the header logo is visible (for example
by adding expect(page.getByRole('img', { name: /logo/i })).toBeVisible() or a
similar selector that matches your logo's accessible name/alt text), leaving the
existing assertions for the burger, search, and theme toggle in place.
- Around line 71-77: The test 'clicking a view in search navigates to diagram'
is hard-coded to a view named "Landscape" and an ID `index` (selectors
getByRole('button', { name: /Landscape/ }) and
expect(page).toHaveURL(/\/view\/index\//)); change it to select a view more
robustly (e.g., use a data-test-id like getByTestId('view-button') or pick the
first view button returned by getByRole('button', { name: /View/i }).first())
and relax the URL assertion to a generic pattern (e.g., assert the URL contains
'/view/' or matches /\/view\/.+/), so the test doesn't break when fixture
names/IDs change.
---
Duplicate comments:
In `@packages/diagram/src/search/SearchContext.tsx`:
- Line 17: currentViewId in SearchContext (the property currentViewId: string |
null) is intentionally typed as string | null per the recorded design decision;
do not change it to ViewId | null — leave the declaration currentViewId: string
| null as-is in SearchContext.tsx and add no further changes.
In `@packages/likec4/app/src/components/search/OverviewSearch.tsx`:
- Around line 28-45: The Overlay is passing a redundant backdrop prop while
withBackdrop={false}; remove the backdrop={{ opacity: 0.9 }} prop from the
Overlay component in OverviewSearch.tsx (the JSX block that renders <Overlay
...>) so only withBackdrop={false} remains, leaving classes, openDelay, onClose,
data-likec4-search and children (OverviewSearchAdapter, SearchPanelContent)
untouched; ensure no other code expects that backdrop prop in this instance.
In `@packages/likec4/app/src/components/search/OverviewSearchAdapter.tsx`:
- Around line 38-49: The useMemo creating value (SearchContextValue) omits the
setSearchValue setter from its dependency array which triggers
react-hooks/exhaustive-deps warnings; update the dependency array of the useMemo
that defines value (the const value = useMemo(...) block) to include
setSearchValue alongside searchValue, pickViewFor, navigateTo, openPickView,
closePickView, and onClose so the linter is satisfied (setSearchValue is stable
but should be listed).
In `@packages/likec4/app/src/components/sidebar/DiagramsTree.tsx`:
- Around line 62-67: The navigation path used when closing the sidebar uses
'/view/$viewId/' in the navigate call (inside the SidebarDrawerOps.close()
sequence) but the app's route is '/_single/view/$viewId/'; update the navigate
invocation to use the correct route string '/_single/view/$viewId/' (keeping the
same params object and viewTransition flag) so the navigate(...) call targets
the actual route definition.
In `@packages/likec4/app/src/routes/_single/single-index.tsx`:
- Line 59: The Burger icon call is missing the opened prop so its icon won't
reflect the drawer state; update the Burger component invocation to pass the
drawer's open boolean (e.g., use the drawer state from SidebarDrawerOps such as
SidebarDrawerOps.isOpen or the local state variable that tracks the drawer) as
the opened prop alongside the existing onClick (Burger ...
onClick={SidebarDrawerOps.open} opened={...}) so the visual state matches the
actual drawer state.
---
Nitpick comments:
In `@packages/diagram/src/search/components/PickView.tsx`:
- Around line 38-40: Remove the redundant thin wrapper function closePickView
and call ctx.closePickView directly wherever closePickView is referenced (e.g.,
event handlers, props, or callbacks); delete the closePickView function
declaration and update all usages to use ctx.closePickView to eliminate the
indirection while preserving current behavior.
In `@packages/diagram/src/search/hooks.ts`:
- Around line 21-25: The hook useUpdateSearch wraps the stable setter
setSearchValue in an unnecessary useCallback; simplify by removing the
useCallback wrapper and return setSearchValue directly from useUpdateSearch
(ensure the returned value preserves the same signature/type as setSearchValue).
Locate the useUpdateSearch function and replace the useCallback-based return
with a direct return of setSearchValue.
In `@packages/diagram/src/search/Search.tsx`:
- Around line 135-199: Destructure pickViewFor directly from the search context
instead of grabbing the full ctx object; replace "const ctx =
useSearchContext()" with destructuring like "const { pickViewFor } =
useSearchContext()" and then use pickViewFor in the JSX (e.g., {pickViewFor &&
<PickView elementFqn={pickViewFor} />}) so only the needed value from
useSearchContext is referenced in SearchPanelContent.
- Around line 201-229: SearchOverlayBody (the memo-wrapped component) lacks a
displayName, causing inconsistent React DevTools output; after the
SearchOverlayBody definition add a displayName property (e.g.,
SearchOverlayBody.displayName = 'SearchOverlayBody') so it matches the pattern
used for Search and SearchPanelContent and helps debugging—locate the memo<{
searchActorRef: SearchActorRef }>(({}) => { ... }) definition and set the
displayName immediately after it.
- Around line 201-229: Add a displayName to the memo-wrapped component by
setting the SearchOverlayBody.displayName to "SearchOverlayBody" immediately
after the memo declaration so React DevTools shows a consistent name like the
other components (see Search and SearchPanelContent which already set
displayName after their memo calls).
- Around line 131-134: The block comment immediately above the exported
SearchPanelContent component is using a plain /* */ comment; change it to a
JSDoc /** */ comment so tooling recognizes it as documentation for the public
export (i.e., replace the leading /* ... */ with /** ... */ and keep the same
text directly above the SearchPanelContent declaration).
- Line 137: The code currently assigns the whole context to ctx via
useSearchContext() but only uses ctx.pickViewFor; change the call to destructure
the specific member: const { pickViewFor } = useSearchContext(); then update
references from ctx.pickViewFor to pickViewFor and remove the unused ctx
variable; apply the same change for the other occurrence that similarly reads
ctx (the second useSearchContext call that only uses pickViewFor).
- Around line 56-68: The current responsive array in scrollAreaCss (height:
['100%', '100cqh']) creates a breakpoint media query instead of a
same-breakpoint fallback; replace that array with a fallback + capability check:
set height: '100%' at the base, then add an `@supports` block inside scrollAreaCss
that sets height: '100cqh' (so browsers that understand cqh get the 100cqh value
while others keep 100%), keeping the rest of the css
('.mantine-ScrollArea-viewport' and its children) unchanged.
In `@packages/diagram/src/search/SearchContext.spec.ts`:
- Around line 43-46: Add a one-character-after-prefix boundary test to the
existing "handles kind: prefix normally" spec: update the test for
normalizeSearch (the test named 'handles kind: prefix normally') to include an
assertion that normalizeSearch('kind:a') returns 'kind:a' to mirror the explicit
1-char coverage used for the '#' prefix tests; this ensures the kind: prefix
case is explicitly verified at the 1-char boundary.
In `@packages/diagram/src/search/SearchContext.tsx`:
- Around line 23-29: Add a JSDoc block above the exported useSearchContext hook
describing that it is a public React hook which returns the current
SearchContextValue, lists its return type (SearchContextValue), and documents
the runtime error behavior (throws if called outside a SearchContext.Provider).
Reference the exported function name useSearchContext and the
SearchContext.Provider in the doc comment so readers know the hook must be used
within that provider and what value it returns.
In `@packages/diagram/src/search/XStateSearchAdapter.tsx`:
- Line 8: The codebase has two equivalent implementations of the same
stable-callback hook—local useCallbackRef
(packages/diagram/src/hooks/useCallbackRef.ts) and the `@mantine/hooks` version
used in PickView.tsx—causing inconsistency; consolidate by switching
PickView.tsx to import and use the local useCallbackRef so both components
(XStateSearchAdapter and PickView) rely on the same implementation, update the
import in PickView.tsx to reference useCallbackRef, remove the `@mantine/hooks`
dependency usage for this hook, and ensure any existing comments about stable
references remain accurate (the useMemo dependency comment in
XStateSearchAdapter can be kept as-is).
In `@packages/likec4/app/src/pages/ViewReact.tsx`:
- Around line 180-182: In FocusElementFromUrl, change the catch block to use
catch (error) and log with console.error instead of console.warn so it matches
the sibling OpenRelationshipBrowserFromUrl; update the catch variable name from
e to error and replace console.warn('focusOnElement failed:', e) with a
console.error call that includes the error variable for consistent logging and
easier triage.
In `@packages/likec4/app/src/routes/__root.tsx`:
- Around line 32-36: The transform cast ".transform(v => v as Fqn | undefined)"
used in searchParamsSchema fields focusOnElement and relationships violates the
no-`as` guideline; replace both with a shared Zod schema (e.g., fqnParam) that
validates/refines a branded Fqn (use .nonempty().optional().nullable() or
.refine(...) to enforce the Fqn shape) and export/use that in place of the
inline transforms so the resulting type is explicit without casting; update
searchParamsSchema to use fqnParam (or annotate the transform return type
explicitly) for both focusOnElement and relationships to remove the bare `as`
cast while keeping runtime validation.
In `@packages/likec4/app/src/routes/_single/single-index.tsx`:
- Line 58: Replace the invisible spacer div (<div style={{ width: 0, height: 36
}} aria-hidden />) with a declarative CSS rule on NavigationPanel.Body (or an
equivalent component) by adding a minHeight: 36px (or using the project's
spacing token) to NavigationPanel.Body's style or CSS class; remove the inline
spacer and ensure the component's CSS is documented/uses the design token so
intent (height anchor) is clear and no DOM-only spacer is required.
| test('header shows burger, logo, search bar, and theme toggle', async ({ page }) => { | ||
| await expect(page.getByRole('button', { name: 'Toggle navigation' })).toBeVisible() | ||
| await expect(page.getByRole('button', { name: /Search/ })).toBeVisible() | ||
| await expect(page.getByRole('button', { name: 'Toggle color scheme' })).toBeVisible() | ||
| }) |
There was a problem hiding this comment.
Test description mentions logo but the assertion is missing.
The test name "header shows burger, logo, search bar, and theme toggle" includes the logo, but the test body only asserts the burger button, search bar button, and theme toggle. The logo click/visibility assertion is absent.
💡 Suggested fix
test('header shows burger, logo, search bar, and theme toggle', async ({ page }) => {
await expect(page.getByRole('button', { name: 'Toggle navigation' })).toBeVisible()
+ await expect(page.getByRole('img', { name: /LikeC4/ }).or(page.locator('[data-likec4-logo]'))).toBeVisible()
await expect(page.getByRole('button', { name: /Search/ })).toBeVisible()
await expect(page.getByRole('button', { name: 'Toggle color scheme' })).toBeVisible()
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| test('header shows burger, logo, search bar, and theme toggle', async ({ page }) => { | |
| await expect(page.getByRole('button', { name: 'Toggle navigation' })).toBeVisible() | |
| await expect(page.getByRole('button', { name: /Search/ })).toBeVisible() | |
| await expect(page.getByRole('button', { name: 'Toggle color scheme' })).toBeVisible() | |
| }) | |
| test('header shows burger, logo, search bar, and theme toggle', async ({ page }) => { | |
| await expect(page.getByRole('button', { name: 'Toggle navigation' })).toBeVisible() | |
| await expect(page.getByRole('img', { name: /LikeC4/ }).or(page.locator('[data-likec4-logo]'))).toBeVisible() | |
| await expect(page.getByRole('button', { name: /Search/ })).toBeVisible() | |
| await expect(page.getByRole('button', { name: 'Toggle color scheme' })).toBeVisible() | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@e2e/tests/overview-page.spec.ts` around lines 22 - 26, The test "header shows
burger, logo, search bar, and theme toggle" is missing an assertion for the
logo; update the test body (the test function) to include an expectation that
the header logo is visible (for example by adding expect(page.getByRole('img', {
name: /logo/i })).toBeVisible() or a similar selector that matches your logo's
accessible name/alt text), leaving the existing assertions for the burger,
search, and theme toggle in place.
| test('Ctrl+K opens search overlay', async ({ page }) => { | ||
| await page.keyboard.press('Control+k') | ||
| await expect(page.getByRole('textbox', { name: /Search by title/ })).toBeVisible() | ||
| }) |
There was a problem hiding this comment.
Control+k won't trigger mod+k on macOS.
The implementation registers mod+k (Mantine useHotkeys), which maps to Cmd+K on macOS. The test hardcodes Control+k, which is correct for Linux/CI but will fail when run locally on macOS. Since the PR description calls out ⌘K as the shortcut, macOS developers will hit this.
Consider checking the OS or using Meta+k conditionally, or at minimum add a comment acknowledging the Linux-only scope.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@e2e/tests/overview-page.spec.ts` around lines 34 - 37, The test "Ctrl+K opens
search overlay" hardcodes 'Control+k' but the app registers the shortcut as
'mod+k' (Mantine useHotkeys) which maps to Cmd+K on macOS; update the test to
send the appropriate key based on the running OS (e.g., use process.platform ===
'darwin' ? 'Meta+k' : 'Control+k'), or alternatively send both variants (Meta+k
then Control+k) before asserting the textbox visibility; update the test name or
add a brief comment to clarify the OS-dependent behavior and reference the test
function name and the 'mod+k' / useHotkeys mapping in the change.
| test('clicking a view in search navigates to diagram', async ({ page }) => { | ||
| await page.getByRole('button', { name: /Search/ }).click() | ||
| // Click the "Landscape" view in the Views column | ||
| await page.getByRole('button', { name: /Landscape/ }).click() | ||
| // Should navigate to the view page | ||
| await expect(page).toHaveURL(/\/view\/index\//) | ||
| }) |
There was a problem hiding this comment.
Test is tightly coupled to specific test-data view names and IDs.
Line 74 selects getByRole('button', { name: /Landscape/ }) and line 76 asserts /\/view\/index\//. Both assume the E2E workspace's test data contains a view named "Landscape" with ID index. If the shared test fixture changes, this test silently breaks. Consider using a data attribute or a more discoverable selector (e.g. getByTestId or the first available view button), and assert the URL pattern more loosely.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@e2e/tests/overview-page.spec.ts` around lines 71 - 77, The test 'clicking a
view in search navigates to diagram' is hard-coded to a view named "Landscape"
and an ID `index` (selectors getByRole('button', { name: /Landscape/ }) and
expect(page).toHaveURL(/\/view\/index\//)); change it to select a view more
robustly (e.g., use a data-test-id like getByTestId('view-button') or pick the
first view button returned by getByRole('button', { name: /View/i }).first())
and relax the URL assertion to a generic pattern (e.g., assert the URL contains
'/view/' or matches /\/view\/.+/), so the test doesn't break when fixture
names/IDs change.
… and theme toggle Closes likec4#1679 - Add visible search bar to overview page that opens the search overlay - Add sidebar navigation drawer for browsing all diagrams - Add dark/light mode toggle to overview header - Support element focus when navigating from search to a diagram view Co-Authored-By: Claude Opus 4.6 <[email protected]>
4fb3d13 to
46b1685
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/diagram/src/search/hooks.ts (1)
11-25: Context delegation is clean; minor redundancy betweenuseSearchanduseUpdateSearch.All three hooks correctly delegate to
useSearchContext(). TheuseDeferredValuewrapping inuseNormalizedSearchis a nice touch for keeping the UI responsive during rapid typing.Note:
useUpdateSearch(line 21-25) returns auseCallback-wrapped version ofsetSearchValue, which is already a stable ref fromuseCallbackRefin the adapter. This makes theuseCallbacklayer redundant. Also,useSearchalready exposes the same setter. IfuseUpdateSearchonly exists for backward compatibility, consider documenting that or deprecating it in a future cleanup pass.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/hooks.ts` around lines 11 - 25, The useUpdateSearch hook redundantly wraps setSearchValue in useCallback even though setSearchValue is already stable (provided via useCallbackRef); remove the extra useCallback and simply return the setSearchValue from useUpdateSearch (referencing useUpdateSearch, useSearchContext, and setSearchValue) or, if you want to preserve API surface only for backward compatibility, add a deprecation comment/documentation on useUpdateSearch and forward-return the stable setSearchValue without re-wrapping it.packages/likec4/app/src/routes/_single/single-index.tsx (1)
58-58: Consider adding a brief comment for the zero-width spacer.
<div style={{ width: 0, height: 36 }} aria-hidden />is a layout trick to enforce minimum row height, but its purpose isn't self-evident. A short inline comment (e.g.,{/* height anchor for nav panel */}) would help future readers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/likec4/app/src/routes/_single/single-index.tsx` at line 58, Add a short inline comment explaining the purpose of the zero-width spacer div to clarify its role as a height anchor for the nav/row; update the JSX element <div style={{ width: 0, height: 36 }} aria-hidden /> in single-index.tsx (locate the zero-width spacer) to include a brief comment such as {/* height anchor for nav panel */} immediately adjacent to the element so future readers understand its layout purpose.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@packages/likec4/app/src/routes/_single/single-index.tsx`:
- Line 59: The Burger currently only calls SidebarDrawerOps.open and never
reflects or toggles the drawer state; change the control to pass an opened prop
and toggle on click by replacing onClick={SidebarDrawerOps.open} with
onClick={SidebarDrawerOps.toggle} (or a small handler that calls open/close) and
add opened={SidebarDrawerOps.isOpen()} (or the equivalent state accessor) so the
<Burger> shows current state and toggles closed as well; update references to
the SidebarDrawerOps methods (open/toggle/isOpen) used in the file.
---
Nitpick comments:
In `@packages/diagram/src/search/hooks.ts`:
- Around line 11-25: The useUpdateSearch hook redundantly wraps setSearchValue
in useCallback even though setSearchValue is already stable (provided via
useCallbackRef); remove the extra useCallback and simply return the
setSearchValue from useUpdateSearch (referencing useUpdateSearch,
useSearchContext, and setSearchValue) or, if you want to preserve API surface
only for backward compatibility, add a deprecation comment/documentation on
useUpdateSearch and forward-return the stable setSearchValue without re-wrapping
it.
In `@packages/likec4/app/src/routes/_single/single-index.tsx`:
- Line 58: Add a short inline comment explaining the purpose of the zero-width
spacer div to clarify its role as a height anchor for the nav/row; update the
JSX element <div style={{ width: 0, height: 36 }} aria-hidden /> in
single-index.tsx (locate the zero-width spacer) to include a brief comment such
as {/* height anchor for nav panel */} immediately adjacent to the element so
future readers understand its layout purpose.
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
🧹 Nitpick comments (4)
packages/diagram/src/search/hooks.ts (1)
21-26:useCallbackwrappingsetSearchValuemay be simplified away.
setSearchValueis destructured from the context (which should provide a stable reference, e.g., fromuseState). Wrapping it inuseCallbackwith[setSearchValue]as the only dependency adds an allocation but doesn't change observable behaviour. The hook can simply returnsetSearchValuedirectly.♻️ Optional simplification
export function useUpdateSearch() { const { setSearchValue } = useSearchContext() - return useCallback((search: string) => { - setSearchValue(search) - }, [setSearchValue]) + return setSearchValue }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/hooks.ts` around lines 21 - 26, The hook useUpdateSearch unnecessarily wraps the stable context setter setSearchValue in useCallback; simplify by returning setSearchValue directly from useUpdateSearch (remove the useCallback wrapper and its dependency array) and remove the unused import of useCallback, referencing useUpdateSearch, useSearchContext and setSearchValue to locate the change.packages/diagram/src/search/SearchContext.spec.ts (1)
30-41: Coverage is solid; consider adding a"##"edge case.The hash-rule (
startsWith('#') && length <= 2) is well-tested for'#'and'#a'. A'##'input (two hashes, length 2) is also covered by the same branch and returns'', but isn't explicitly exercised.✅ Optional additional test
it('returns empty string for "#a" (hash + single char)', () => { expect(normalizeSearch('#a')).toBe('') }) + + it('returns empty string for "##" (two hashes, length ≤ 2)', () => { + expect(normalizeSearch('##')).toBe('') + })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/SearchContext.spec.ts` around lines 30 - 41, Add a unit test case that verifies normalizeSearch returns an empty string for the two-hash edge case "##"; specifically, in SearchContext.spec.ts add an it block similar to the existing tests that calls normalizeSearch('##') and expects '' so the branch guarded by startsWith('#') && length <= 2 (handled in normalizeSearch) is explicitly exercised.packages/diagram/src/search/components/PickView.tsx (1)
38-40: TrivialclosePickViewwrapper can be inlined.The one-line wrapper adds no value;
ctx.closePickViewcan be used directly at each call site.♻️ Proposed simplification
- const closePickView = () => { - ctx.closePickView() - }Then replace the three usages:
- closePickView() + ctx.closePickView()- onClick={e => { - e.stopPropagation() - closePickView() - }}> + onClick={e => { + e.stopPropagation() + ctx.closePickView() + }}>- onClick={(e) => { - e.stopPropagation() - closePickView() - }}> + onClick={(e) => { + e.stopPropagation() + ctx.closePickView() + }}>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/diagram/src/search/components/PickView.tsx` around lines 38 - 40, Remove the trivial one-line wrapper closePickView and call ctx.closePickView directly at each usage site; locate the closePickView declaration in PickView.tsx (const closePickView = () => { ctx.closePickView() }) and delete it, then replace any references to closePickView with ctx.closePickView to simplify the code.packages/likec4/app/src/routes/_single/single-index.tsx (1)
39-74: New sticky header with navigation — looks good overall.The sticky header with backdrop blur, burger menu, logo, search, and theme toggle is well-composed. Minor note: the zero-width spacer
<div style={{ width: 0, height: 36 }} aria-hidden />on line 58 relies on a magic number for height alignment. Consider extracting this to a named constant or comment explaining what it matches, so it doesn't silently drift ifNavigationPanel.Bodyheights change.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/likec4/app/src/routes/_single/single-index.tsx` around lines 39 - 74, The zero-width spacer div used to vertically align the header items (currently <div style={{ width: 0, height: 36 }} aria-hidden />) relies on a magic height (36) that can drift; replace this literal with a named constant (e.g., NAV_BODY_HEIGHT or HEADER_ICON_SIZE) or derive the height from a shared CSS variable and use that constant/variable in the style or class, update any relevant references in NavigationPanel.Body or the surrounding header JSX so the spacer remains consistent if sizes change, and add a short comment explaining what the constant matches (logo/burger/search height) for future maintainability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@e2e/tests/overview-page.spec.ts`:
- Around line 22-26: The test named 'header shows burger, logo, search bar, and
theme toggle' is missing an assertion for the logo; update that test in
overview-page.spec.ts (the test block with that exact name) to include a
visibility assertion for the logo, e.g. use Playwright's locator for the logo
like page.getByRole('img', { name: /logo/i }) or page.getByAltText('Logo') and
add await expect(...).toBeVisible() so the test actually verifies the logo is
present.
- Around line 71-77: The test uses a fixture-specific selector and URL check
which will break if the view name/ID changes; replace the hardcoded
getByRole('button', { name: /Landscape/ }) selector with a locator that selects
the first available view button in the Views column (e.g., use the Views column
container locator and pick the first button) and update the URL assertion from
toHaveURL(/\/view\/index\//) to a more general pattern like
toHaveURL(/\/view\/.+/) so it matches any view ID; locate these changes around
the test named "clicking a view in search navigates to diagram" and the usages
of getByRole and toHaveURL.
- Around line 34-37: The test 'Ctrl+K opens search overlay' hardcodes
'Control+k' but the app registers 'mod+k' (Cmd on macOS), so update the test to
press the platform-appropriate key: replace the literal 'Control+k' in that test
with a conditional that uses 'Meta+k' when running on macOS (process.platform
=== 'darwin') and 'Control+k' otherwise so the test matches useHotkeys' 'mod+k'
behavior across platforms.
In `@packages/likec4/app/src/components/search/OverviewSearchAdapter.tsx`:
- Around line 38-49: The useMemo call creating the SearchContextValue is missing
setSearchValue in its dependency array, which will trigger exhaustive-deps
linter warnings; update the dependency array of the useMemo that returns the
SearchContextValue (the memo that references searchValue, setSearchValue,
normalizeSearch, navigateTo, openPickView, closePickView, pickViewFor, onClose)
to include setSearchValue so the hook dependencies accurately reflect all
referenced symbols.
In `@packages/likec4/app/src/routes/_single/single-index.tsx`:
- Line 59: The Burger currently calls SidebarDrawerOps.open on click and lacks
an opened prop, so its visual state doesn't reflect whether the drawer is open
and the button cannot toggle closed; update the Burger usage to bind its opened
prop to the drawer state (e.g., SidebarDrawerOps.isOpen or equivalent) and
change the onClick to toggle the drawer (e.g., SidebarDrawerOps.toggle) instead
of always calling SidebarDrawerOps.open so the UI both reflects and toggles the
drawer state.
---
Nitpick comments:
In `@packages/diagram/src/search/components/PickView.tsx`:
- Around line 38-40: Remove the trivial one-line wrapper closePickView and call
ctx.closePickView directly at each usage site; locate the closePickView
declaration in PickView.tsx (const closePickView = () => { ctx.closePickView()
}) and delete it, then replace any references to closePickView with
ctx.closePickView to simplify the code.
In `@packages/diagram/src/search/hooks.ts`:
- Around line 21-26: The hook useUpdateSearch unnecessarily wraps the stable
context setter setSearchValue in useCallback; simplify by returning
setSearchValue directly from useUpdateSearch (remove the useCallback wrapper and
its dependency array) and remove the unused import of useCallback, referencing
useUpdateSearch, useSearchContext and setSearchValue to locate the change.
In `@packages/diagram/src/search/SearchContext.spec.ts`:
- Around line 30-41: Add a unit test case that verifies normalizeSearch returns
an empty string for the two-hash edge case "##"; specifically, in
SearchContext.spec.ts add an it block similar to the existing tests that calls
normalizeSearch('##') and expects '' so the branch guarded by startsWith('#') &&
length <= 2 (handled in normalizeSearch) is explicitly exercised.
In `@packages/likec4/app/src/routes/_single/single-index.tsx`:
- Around line 39-74: The zero-width spacer div used to vertically align the
header items (currently <div style={{ width: 0, height: 36 }} aria-hidden />)
relies on a magic height (36) that can drift; replace this literal with a named
constant (e.g., NAV_BODY_HEIGHT or HEADER_ICON_SIZE) or derive the height from a
shared CSS variable and use that constant/variable in the style or class, update
any relevant references in NavigationPanel.Body or the surrounding header JSX so
the spacer remains consistent if sizes change, and add a short comment
explaining what the constant matches (logo/burger/search height) for future
maintainability.
davydkov
left a comment
There was a problem hiding this comment.
Thank you @ckeller42 for contribution! 🚀
Signed-off-by: Denis Davydkov <[email protected]>
Closes #1679
Summary
Screenshots
Overview page — Light mode
Overview page — Dark mode
Key changes
SearchContextfrom@likec4/diagraminto a standalone module so it can be reused outside the diagram's XState-based searchOverviewSearchAdapterprovides the search context for the overview page, with navigation that passesfocusOnElementas a URL parameterFocusElementFromUrlcomponent reads the?focusOnElementparam and callsdiagram.focusOnElement()after the diagram initializes (follows the existingOpenRelationshipBrowserFromUrlpattern)SidebarDrawer/DiagramsTreeandColorSchemeTogglecomponentsTest plan
Checklist
mainbefore creating this PR.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests
Bug Fixes