Conversation
…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).
…t into fix-sidebar-plugin-links
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.
Contributor
Reviewer's GuideImplements 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 componentsclassDiagram
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
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Contributor
There was a problem hiding this comment.
Hey - I've found 4 issues, and left some high level feedback:
- In
SearchLinkModal, thesetTimeoutblock that queries#search-inputwill never find an element because theInputhas noidprop; either passid="search-input"into theInputor remove this manual focusing logic and rely solely on theautoFocusprop. - The
showDivflag inSidebarItemis inverted (aDivideris rendered only whendepth === 0) and named in a confusing way; consider simplifying it to an explicit condition likeif (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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
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.
Owner
Author
|
@sourcery-ai resolve |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
Bug Fixes:
Enhancements: