Skip to content

Fix sidebar plugin links#54

Merged
Its4Nik merged 8 commits intodevfrom
fix-sidebar-plugin-links
Jan 8, 2026
Merged

Fix sidebar plugin links#54
Its4Nik merged 8 commits intodevfrom
fix-sidebar-plugin-links

Conversation

@Its4Nik
Copy link
Copy Markdown
Owner

@Its4Nik Its4Nik commented Jan 8, 2026

Summary by Sourcery

Add a searchable modal and improved navigation for main and plugin routes, enhance modal and card styling with glass effects, and refactor toast utilities.

New Features:

  • Introduce a global SearchLinkModal for searching pins, plugin routes, and sidebar links with keyboard shortcut support.
  • Add tabbed switching between main routes and plugin routes in the sidebar navigation.
  • Support glass-style variants for cards and transparent modals with shared animation variants.

Bug Fixes:

  • Fix plugin sidebar items so they render and pin correctly alongside main navigation routes.

Enhancements:

  • Improve sidebar item styling, spacing, and nested child rendering for better readability.
  • Make the navbar logo act as a home link and add animated list transitions for sidebar content.
  • Expose and reuse toast and modal components/utilities across the app and adjust toast button styling.
  • Allow inputs and buttons to support autofocus and extra-small sizing for better UX.
  • Increase hover tooltip z-index to prevent it being hidden behind other elements.

Its4Nik and others added 6 commits January 7, 2026 20:25
…toast

- Refactors the `toast` utility function into `lib/toast.tsx`, separating it from the `Toast` component definition.
- Updates the default dismiss button in the `Toast` component to use an 'X' icon from `lucide-react` and apply `sm` size styling.
- Introduces animated toggling between 'Main routes' and 'Plugin routes' in the Sidebar using `framer-motion`'s `AnimatePresence`.
- Adds a new `xs` button size to the `@dockstat/ui` package, utilized by the new sidebar route selection buttons.
- Simplifies the `isPinned` check for sidebar items within plugin routes.
The DockStat logo in the application's Navbar component now functions as a clickable link, directing users to the root path ("/"). This change improves user experience by providing a quick way to return to the home screen from any page.

Additionally, this commit updates the import path for the 'toast' utility in `apps/dockstat/src/pages/extensions/plugins.tsx` from `@/components/toast` to `@/lib/toast`, reflecting a refactor in the project's utility structure.
Introduces a global search modal, accessible via CMD/CTRL+K, allowing users to quickly navigate across DockStat. This feature aggregates all available links, including pinned items, plugin-provided routes, and standard sidebar pages, for efficient access.

This commit also includes significant UI component enhancements and structural improvements:

- **Global Search (`SearchLinkModal`):**
  - Aggregates links from pins, plugins, and sidebar paths.
  - Provides fuzzy filtering and animated result display.
  - Implements keyboard shortcuts (CMD/CTRL+K to open, Escape to close).
- **Card Component:**
  - Adds a new `glass` prop to enable a frosted glass background effect (`backdrop-blur-lg`) across all card variants.
- **Modal Component:**
  - Introduces a `transparent` prop to render modals with a frosted glass backdrop.
  - Extracts modal animation variants into a dedicated `variants.ts` file for better modularity.
  - Enhances `AnimatePresence` and event handling for smoother modal interactions.
- **Input Component:**
  - Adds an `autoFocus` prop for programmatic focus, utilized in the new search modal.
- **HoverBubble Component:**
  - Increases its z-index (`z-100`) to ensure it always appears above other elements.
- **Sidebar & Navbar:**
  - Refines styling for active navigation links and the pin button for improved visual clarity and interactivity.
  - Adds subtle dotted dividers between top-level sidebar items for better structure.
  - Implements an Escape key handler to easily close the sidebar.
  - Integrates the new `SearchLinkModal` into the Navbar.
- **Extension Paths:**
  - Expands the sidebar's "Extensions" section with new default paths: "Explore themes" and "Stacks".
- **Code Organization:**
  - Reorders import statements in several files for consistency (likely due to automated formatting).
Sorted imports alphabetically across several UI components including Modal, Navbar, SearchLinks, and SidebarItem to improve code consistency.
Removed the unused 'type Variants' import from Modal.tsx as it was no longer directly required, reducing unnecessary dependencies.
@Its4Nik Its4Nik self-assigned this Jan 8, 2026
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Jan 8, 2026

Reviewer's Guide

Implements a searchable, animated navigation experience by adding a global link search modal, refactoring modal animations, enhancing sidebar/plugin navigation presentation, and slightly adjusting shared UI components (Card, Button, Input, HoverBubble, Toast) and toast usage paths.

Class diagram for updated navigation and modal components

classDiagram
  direction LR

  class SearchLinkModal {
    +pins: PathItem[]
    +pluginLinks: PluginLink[]
    +sidebarLinks: PathItem[]
    -modalOpen: boolean
    -searchQuery: string
    -filteredResults: SearchResult[]
    -allResults: SearchResult[]
    +SearchLinkModal(props)
    -handleResultClick(result: SearchResult) void
  }

  class SearchResult {
    +id: string
    +type: string
    +path: string
    +title: string
    +pluginName: string
    +metaTitle: string
  }

  class PathItem {
    +slug: string
    +path: string
    +isPinned: boolean
    +children: PathItem[]
  }

  class PluginLink {
    +pluginName: string
    +paths: PluginPath[]
  }

  class PluginPath {
    +fullPath: string
    +metaTitle: string
  }

  class Modal {
    +open: boolean
    +onClose: function
    +title: ReactNode
    +footer: ReactNode
    +children: ReactNode
    +bodyClasses: string
    +size: ModalSize
    +transparent: boolean
  }

  class ModalSize {
    <<enum>>
    +sm
    +md
    +lg
    +xl
    +full
  }

  class Card {
    +variant: CardVariant
    +size: CardSize
    +hoverable: boolean
    +glass: boolean
    +onClick: function
  }

  class CardVariant {
    <<enum>>
    +default
    +outlined
    +elevated
    +flat
    +dark
    +error
    +success
  }

  class CardSize {
    <<enum>>
    +xs
    +sm
    +md
    +lg
  }

  class Input {
    +label: string
    +placeholder: string
    +value: string
    +variant: string
    +error: boolean
    +success: boolean
    +autoFocus: boolean
    +onChange(value: string) void
  }

  class Button {
    +variant: ButtonVariant
    +size: ButtonSize
    +noFocusRing: boolean
  }

  class ButtonVariant {
    <<enum>>
    +primary
    +secondary
    +outline
    +ghost
    +danger
  }

  class ButtonSize {
    <<enum>>
    +xs
    +sm
    +md
    +lg
  }

  class Toast {
    +id: string | number
    +title: ReactNode
    +description: ReactNode
    +variant: string
    +button: ToastButton
  }

  class ToastButton {
    +label: ReactNode
    +onClick: function
  }

  class ToastLib {
    +toast(toast: ToastWithoutId) void
  }

  class ToastWithoutId {
    +title: ReactNode
    +description: ReactNode
    +variant: string
    +button: ToastButton
  }

  class SidebarItem {
    +item: PathItem
    +depth: number
    +isLoading: boolean
    +handleTogglePin(item: PathItem) void
  }

  class Navbar {
    +navLinks: PathItem[]
    +pluginLinks: PluginLink[]
  }

  %% Relationships
  Navbar --> SearchLinkModal : uses
  Navbar --> SidebarItem : renders
  SearchLinkModal --> Modal : uses
  SearchLinkModal --> Input : uses
  SearchLinkModal --> Card : uses
  SearchLinkModal --> PathItem : aggregates
  SearchLinkModal --> PluginLink : aggregates
  Modal --> Card : wraps_content
  Card --> CardVariant : configures
  Card --> CardSize : configures
  Button --> ButtonVariant : configures
  Button --> ButtonSize : configures
  ToastLib --> Toast : renders
  ToastLib --> ToastWithoutId : accepts
  Toast --> ToastButton : has
  SidebarItem --> PathItem : renders
Loading

File-Level Changes

Change Details Files
Add animated toggle between main and plugin routes in the sidebar and improve sidebar item behavior.
  • Introduce list and item animation variants for sidebar navigation sections and wrap nav lists in AnimatePresence and motion.nav/motion.div for transitions.
  • Replace always-visible main+plugin lists with a tab-like toggle (Main routes vs Plugin routes) governed by local state, including conditional rendering when no plugins exist.
  • Refine plugin pinning logic using pins.some path checks and keep loading state wiring to existing mutation functions.
  • Adjust Sidebar to handle Escape key when open to close via onClose callback.
packages/ui/src/components/Navbar/Sidebar.tsx
Refactor Modal to support glass/transparent styling and centralize animation variants.
  • Extract backdrop and modal animation variants into a dedicated variants module and import them into Modal.
  • Add a transparent prop to Modal that switches between standard and glass-specific animation variants.
  • Adjust AnimatePresence usage and motion wrappers to add keys, tweak classes (margins, rounded) and stop click propagation on content container.
  • Update Card usage within Modal to support glass mode via the new glass prop.
packages/ui/src/components/Modal/Modal.tsx
packages/ui/src/components/Modal/variants.ts
packages/ui/src/components/Card/Card.tsx
Create a global link search modal with keyboard shortcut support, searching across pinned, plugin, and sidebar routes.
  • Introduce SearchLinkModal component that builds a de-duplicated, prioritized set of SearchResult entries from pins, plugin links, and sidebar paths (including child routes).
  • Implement query-based filtering with useEffect and useMemo, updating a list of animated result cards with icons and badges for type, plugin name, and index.
  • Wire up keyboard shortcuts (Ctrl/Cmd+K to open and Escape to close) and navigate() on result click, closing and clearing search state after navigation.
  • Render the SearchLinkModal from Navbar, passing navLinks as pins and pluginLinks from props.
packages/ui/src/components/Navbar/SearchLinks.tsx
packages/ui/src/components/Navbar/Navbar.tsx
packages/ui/src/components/Navbar/consts.ts
Polish sidebar item visuals, structure, and depth handling.
  • Adjust SidebarItem layout to use different text colors, padding calculation based on depth, and updated pin button styling with filled background and hover behavior.
  • Guard child rendering with an explicit children length check and wrap the child list in its own container div to improve structure.
  • Introduce a dotted Divider rendered conditionally based on depth (currently rendered when depth is 0 via showDiv flag inversion).
packages/ui/src/components/Navbar/SidebarItem.tsx
Make toast component reusable, move toast factory to a shared lib module, and tweak toast UI.
  • Export ToastProps and Toast from the toast component file and change the default dismiss button label to an icon-only X with small button size.
  • Create a new toast helper in apps/dockstat/src/lib/toast.tsx that wraps sonnerToast.custom and uses the shared Toast component.
  • Update imports in layout.tsx and plugins page to use the new lib/toast helper instead of the local components/toast export.
apps/dockstat/src/components/toast.tsx
apps/dockstat/src/lib/toast.tsx
apps/dockstat/src/layout.tsx
apps/dockstat/src/pages/extensions/plugins.tsx
Enhance shared UI primitives (Card, Button, Input, HoverBubble, Navbar animations) for new UX patterns.
  • Extend Card with a glass boolean prop, applying alternate "glass" variant class maps that use translucent backgrounds and backdrop blur, used by Modal for transparent styling.
  • Add an xs size option to Button and corresponding size classes, used for small toggle buttons and toast dismiss buttons.
  • Add autoFocus support to Input, passing it through to the underlying input (with an a11y lint ignore comment) to enable focus in SearchLinkModal.
  • Increase HoverBubble z-index to avoid overlap issues with new overlays.
  • Add animatedBlur variants for navbar to support blurred backdrops (not yet wired in this diff).
packages/ui/src/components/Card/Card.tsx
packages/ui/src/components/Button/Button.tsx
packages/ui/src/components/Forms/Input.tsx
packages/ui/src/components/HoverBubble/HoverBubble.tsx
packages/ui/src/components/Navbar/animations.ts

Possibly linked issues

  • #UI/UX: Update Sidebar: The PR substantially updates sidebar links and plugin route navigation, directly addressing the sidebar/UI refactor requested.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • In SearchLinkModal, the setTimeout block that queries #search-input will never find an element because the Input has no id prop; either pass id="search-input" into the Input or remove this manual focusing logic and rely solely on the autoFocus prop.
  • The showDiv flag in SidebarItem is inverted (a Divider is rendered only when depth === 0) and named in a confusing way; consider simplifying it to an explicit condition like if (depth === 0) <Divider ... /> to make the intent clearer and avoid the extra mutable variable.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `SearchLinkModal`, the `setTimeout` block that queries `#search-input` will never find an element because the `Input` has no `id` prop; either pass `id="search-input"` into the `Input` or remove this manual focusing logic and rely solely on the `autoFocus` prop.
- The `showDiv` flag in `SidebarItem` is inverted (a `Divider` is rendered only when `depth === 0`) and named in a confusing way; consider simplifying it to an explicit condition like `if (depth === 0) <Divider ... />` to make the intent clearer and avoid the extra mutable variable.

## Individual Comments

### Comment 1
<location> `apps/dockstat/src/components/toast.tsx:63-72` </location>
<code_context>
               }}
             >
-              {button?.label || "Dismiss"}
+              {button?.label || <X size={15} />}
             </Button>
           </div>
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Icon-only fallback for the toast button could use an accessible label.

When `button.label` is missing, the `X` icon renders without any accessible name. Please add an `aria-label` (e.g., `aria-label="Dismiss"`) to the `Button` in the icon-only case, or include a visually hidden text label so screen readers and keyboard users can identify the control.

```suggestion
            <Button
              size="sm"
              variant={cardVariant === "error" ? "danger" : "secondary"}
              aria-label={button?.label ? undefined : "Dismiss"}
              onClick={() => {
                button?.onClick()
                sonnerToast.dismiss(id)
              }}
            >
              {button?.label || <X size={15} aria-hidden="true" />}
            </Button>
```
</issue_to_address>

### Comment 2
<location> `packages/ui/src/components/Navbar/Sidebar.tsx:150` </location>
<code_context>
                 </div>
-              ))}
+
+                <AnimatePresence mode="wait" initial={false}>
+                  {showPluginRoutes ? (
+                    <motion.nav
</code_context>

<issue_to_address>
**issue (complexity):** Consider extracting the repeated animated nav/item wrappers and pin-lookup logic into reusable helpers to keep Sidebar focused and easier to read.

You can keep all the behavior while reducing complexity by extracting the duplicated animated nav + pin logic into small helpers.

### 1. Extract common animated nav wrapper

Both branches use the same `motion.nav` config and mostly differ in how items are rendered. You can pull that out into a tiny, focused component:

```tsx
// SidebarAnimatedNav.tsx
import { motion } from "framer-motion"

const listVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: { staggerChildren: 0.05 },
  },
}

const itemVariants = {
  hidden: { opacity: 0, x: -10 },
  visible: { opacity: 1, x: 0 },
}

type SidebarAnimatedNavProps = {
  children: React.ReactNode
}

export function SidebarAnimatedNav({ children }: SidebarAnimatedNavProps) {
  return (
    <motion.nav
      variants={listVariants}
      initial="hidden"
      animate="visible"
      exit="hidden"
      className="flex flex-1 flex-col gap-1 mt-4"
    >
      {children}
    </motion.nav>
  )
}

export function SidebarAnimatedItem({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <motion.div variants={itemVariants} transition={{ duration: 0.2 }}>
      {children}
    </motion.div>
  )
}
```

Then your JSX flattens nicely:

```tsx
<AnimatePresence mode="wait" initial={false}>
  {showPluginRoutes ? (
    <SidebarAnimatedNav key="plugins">
      {pluginLinks.map((plugin) => (
        <div key={plugin.pluginName}>
          <Divider label={plugin.pluginName} className="mb-2" />
          <div className="flex flex-1 flex-col gap-1">
            {plugin.paths.map((path) => (
              <SidebarAnimatedItem key={path.fullPath}>
                <SidebarItem
                  handleTogglePin={() =>
                    handleTogglePin({
                      path: path.fullPath,
                      slug: path.metaTitle,
                      isPinned: isPinned(path.fullPath),
                    })
                  }
                  isLoading={mutationFn.pin.isPending || mutationFn.unpin.isPending}
                  item={{
                    path: path.fullPath,
                    slug: path.metaTitle,
                    isPinned: isPinned(path.fullPath),
                  }}
                />
              </SidebarAnimatedItem>
            ))}
          </div>
        </div>
      ))}
    </SidebarAnimatedNav>
  ) : (
    <SidebarAnimatedNav key="default">
      {pathsWithPinStatus?.map((p) => (
        <SidebarAnimatedItem key={p.slug}>
          <SidebarItem
            item={p}
            handleTogglePin={handleTogglePin}
            isLoading={mutationFn.pin.isPending || mutationFn.unpin.isPending}
          />
        </SidebarAnimatedItem>
      ))}
    </SidebarAnimatedNav>
  )}
</AnimatePresence>
```

This removes all the repeated `motion.nav` / `variants` / `initial` / `animate` / `exit` boilerplate from `Sidebar`.

### 2. Centralize pin lookup for plugin routes

You compute `isPinned` for plugin paths twice with `pins.some(...)`. A tiny helper (or memoized set) keeps the JSX cleaner and avoids duplication:

```tsx
const pinnedPaths = useMemo(
  () => new Set(pins.map((p) => p.path)),
  [pins],
)

const isPinned = (path: string) => pinnedPaths.has(path)
```

Then use it in the plugin branch:

```tsx
<SidebarItem
  handleTogglePin={() =>
    handleTogglePin({
      path: path.fullPath,
      slug: path.metaTitle,
      isPinned: isPinned(path.fullPath),
    })
  }
  isLoading={mutationFn.pin.isPending || mutationFn.unpin.isPending}
  item={{
    path: path.fullPath,
    slug: path.metaTitle,
    isPinned: isPinned(path.fullPath),
  }}
/>
```

These two changes preserve all behavior (toggling views, animations, pinning) while making `Sidebar` read more like high‑level orchestration instead of wiring details.
</issue_to_address>

### Comment 3
<location> `packages/ui/src/components/Navbar/SidebarItem.tsx:19` </location>
<code_context>

 export const SidebarItem = ({ item, depth = 0, handleTogglePin, isLoading }: SidebarItemProps) => {
   const onToggle = () => handleTogglePin(item)
+  let showDiv = true
</code_context>

<issue_to_address>
**issue (complexity):** Consider removing the unnecessary `showDiv` flag and simplifying the children rendering condition to make the SidebarItem component easier to read and maintain.

You can keep all the visual changes while simplifying the logic and reducing nesting a bit.

**1. Replace `showDiv` with a direct condition**

`showDiv` is only derived from `depth === 0`, so it adds mutable state without benefit and makes the divider condition harder to read.

Current:

```ts
export const SidebarItem = ({ item, depth = 0, handleTogglePin, isLoading }: SidebarItemProps) => {
  const onToggle = () => handleTogglePin(item)
  let showDiv = true

  if (depth === 0) {
    showDiv = false
  }

  return (
    <div className="flex flex-col gap-1">
      {/* ... */}
      {!showDiv && <Divider variant="dotted" />}
    </div>
  )
}
```

Suggested:

```tsx
export const SidebarItem = ({ item, depth = 0, handleTogglePin, isLoading }: SidebarItemProps) => {
  const onToggle = () => handleTogglePin(item)

  return (
    <div className="flex flex-col gap-1">
      {/* ... */}
      {depth === 0 && <Divider variant="dotted" />}
    </div>
  )
}
```

Same behavior, less cognitive load (no extra flag, no negation).

---

**2. Simplify the children rendering condition**

You can avoid the double condition and keep recursion clearer by leaning on optional chaining and `length`:

Current:

```tsx
{item.children && item.children.length > 0 && (
  <div className="flex flex-col gap-1">
    {item.children.map((child) => (
      <SidebarItem
        key={child.slug}
        item={child}
        depth={depth + 1}
        handleTogglePin={handleTogglePin}
        isLoading={isLoading}
      />
    ))}
  </div>
)}
```

Suggested (same wrapper, simpler condition):

```tsx
{item.children?.length ? (
  <div className="flex flex-col gap-1">
    {item.children.map((child) => (
      <SidebarItem
        key={child.slug}
        item={child}
        depth={depth + 1}
        handleTogglePin={handleTogglePin}
        isLoading={isLoading}
      />
    ))}
  </div>
) : null}
```

If you want to reduce visual nesting further, you can extract the wrapper into a tiny component without changing behavior:

```tsx
const SidebarChildren = ({ children }: { children: PathItem[] }) => (
  <div className="flex flex-col gap-1">
    {children.map((child) => (
      <SidebarItem
        key={child.slug}
        item={child}
        depth={depth + 1}
        handleTogglePin={handleTogglePin}
        isLoading={isLoading}
      />
    ))}
  </div>
)

// usage
{item.children?.length ? <SidebarChildren children={item.children} /> : null}
```

This keeps the recursive structure of `SidebarItem` leaner and easier to scan.
</issue_to_address>

### Comment 4
<location> `packages/ui/src/components/Navbar/SearchLinks.tsx:29` </location>
<code_context>
+
+  const [modalOpen, setModalOpen] = useState<boolean>(false)
+  const [searchQuery, setSearchQuery] = useState<string>("")
+  const [filteredResults, setFilteredResults] = useState<SearchResult[]>([])
+
+  interface SearchResult {
</code_context>

<issue_to_address>
**issue (complexity):** Consider simplifying this component by deriving filtered results with useMemo, hoisting static animation configs, and flattening AnimatePresence usage to reduce mental overhead while preserving behavior.

You can significantly cut cognitive load without changing behavior by factoring out some derived state and static config.

**1. Avoid derived state/effect for `filteredResults`**

`filteredResults` is purely derived from `searchQuery` + `allResults`, so you don’t need extra state + effect:

```tsx
// before
const [filteredResults, setFilteredResults] = useState<SearchResult[]>([])

useEffect(() => {
  if (!searchQuery.trim()) {
    setFilteredResults(allResults)
    return
  }

  const query = searchQuery.toLowerCase().trim()
  const filtered = allResults.filter(
    (result) =>
      result.title.toLowerCase().includes(query) ||
      result.path.toLowerCase().includes(query) ||
      (result?.pluginName || "").toLowerCase().includes(query),
  )

  setFilteredResults(filtered)
}, [searchQuery, allResults])
```

You can replace this with a memo and remove the effect + setter:

```tsx
const filteredResults = useMemo(() => {
  const query = searchQuery.toLowerCase().trim()
  if (!query) return allResults

  return allResults.filter(
    (result) =>
      result.title.toLowerCase().includes(query) ||
      result.path.toLowerCase().includes(query) ||
      (result.pluginName ?? "").toLowerCase().includes(query),
  )
}, [searchQuery, allResults])
```

That keeps behavior identical but simplifies the mental model.

---

**2. Move static animation variants out of the component**

`containerVariants` and `itemVariants` never use props/state, so they can live at module scope. That makes the component body shorter and avoids re-creating them on every render:

```tsx
// outside of SearchLinkModal
const containerVariants: Variants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: { staggerChildren: 0.03, delayChildren: 0.05 },
  },
}

const itemVariants: Variants = {
  hidden: {
    opacity: 0,
    scale: 0.8,
    y: 20,
    filter: "blur(4px)",
  },
  visible: {
    opacity: 1,
    scale: 1,
    y: 0,
    filter: "blur(0px)",
    transition: { type: "spring", stiffness: 300, damping: 24 },
  },
  exit: {
    opacity: 0,
    scale: 0.8,
    y: -10,
    filter: "blur(4px)",
    transition: { duration: 0.2, ease: "easeOut" },
  },
  hover: {
    scale: 1.02,
    transition: { type: "spring", stiffness: 400, damping: 25 },
  },
  tap: { scale: 0.98 },
}

// inside SearchLinkModal: just use containerVariants/itemVariants
```

---

**3. Consolidate `AnimatePresence` usage**

The nested `AnimatePresence` is hard to follow. You can keep the same UX with a single presence wrapper around the items:

```tsx
<div className="max-h-96 overflow-y-auto p-2">
  <AnimatePresence mode="popLayout">
    {filteredResults.length > 0 ? (
      <motion.div
        variants={containerVariants}
        initial="hidden"
        animate="visible"
        className="flex flex-wrap gap-2"
      >
        {filteredResults.map((result, index) => (
          <motion.div
            key={result.id}
            layout
            layoutId={result.id}
            variants={itemVariants}
            initial="hidden"
            animate="visible"
            exit="exit"
            whileHover="hover"
            whileTap="tap"
            className="flex-1"
          >
            {/* Card content */}
          </motion.div>
        ))}
      </motion.div>
    ) : searchQuery ? (
      /* no-results state */
    ) : (
      /* idle state */
    )}
  </AnimatePresence>
</div>
```

This keeps animations but reduces the JSX nesting and makes the flow easier to scan.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread apps/dockstat/src/components/toast.tsx
Comment thread packages/ui/src/components/Sidebar/Sidebar.tsx
Comment thread packages/ui/src/components/Navbar/SidebarItem.tsx
Comment thread packages/ui/src/components/Navbar/SearchLinks.tsx Outdated
Introduce a new global link lookup modal (`LinkLookup`) accessible via `Ctrl/Cmd + K` for quick navigation. This component provides fuzzy searching across pinned links, plugin routes, and default sidebar pages, featuring `framer-motion` animations for a smooth user experience. It replaces the previous `SearchLinkModal`.

The `Sidebar` component has been refactored and moved to its own dedicated directory (`components/Sidebar`). This refactor includes:
- Moving `SidebarItem` and `SidebarAnimatedNav` to separate files for better modularity.
- Integrating `framer-motion` for animated transitions within the sidebar navigation.
- Enhancing overall organization and maintainability of navigation-related components.
@Its4Nik
Copy link
Copy Markdown
Owner Author

Its4Nik commented Jan 8, 2026

@sourcery-ai resolve

@Its4Nik Its4Nik merged commit cc3e69b into dev Jan 8, 2026
2 checks passed
@Its4Nik Its4Nik deleted the fix-sidebar-plugin-links branch January 8, 2026 04:35
@Its4Nik Its4Nik mentioned this pull request Jan 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant