| name | ui-design |
| description | Design patterns and component guidelines for the Nomendex UI. Use when building dialogs, layouts, or fixing visual/layout issues. |
UI Design Patterns
Reference documentation for consistent UI patterns in Nomendex.
Jumbo Dialogs
Jumbo dialogs (size="jumbo") are full-viewport dialogs (90vw x 90vh) for complex content like search interfaces, file browsers, or multi-pane layouts.
Structure Requirements
The jumbo dialog uses display: flex with flex-direction: column. The CommandDialogProvider automatically wraps content in a flex-growing container when size="jumbo":
<DialogContent size="jumbo">
<DialogHeader className="shrink-0">
<DialogTitle>Title</DialogTitle>
<DialogDescription>Description</DialogDescription>
</DialogHeader>
{}
{dialogState.content}
</DialogContent>
Key CSS Classes
shrink-0 - Prevents header from shrinking
flex-1 - Allows content to grow and fill space
min-h-0 - Critical for flex children to allow shrinking below content size (enables overflow)
overflow-y-auto - For scrollable sections
Why min-h-0 Matters
In flexbox, children have min-height: auto by default, which means they won't shrink below their content size. This breaks overflow scrolling. Adding min-h-0 allows the element to shrink, enabling overflow-y-auto to work.
Reference Implementation: Search Notes Dialog
Located at src/features/notes/search-notes-dialog.tsx, this demonstrates the full pattern.
Opening via CommandDialogProvider
openDialog({
title: "Search Notes",
description: "Search for text across all your notes",
content: <SearchNotesDialog />,
size: "jumbo",
});
Two-Column Layout Pattern
<div className="flex flex-col h-full">
{}
<div
className="shrink-0 px-4 py-3 border-b"
style={{ borderColor: styles.borderDefault }}
>
<Input placeholder="Search notes..." />
</div>
{}
<div className="flex-1 flex min-h-0">
{}
<div
className="w-1/2 overflow-y-auto border-r"
style={{ borderColor: styles.borderDefault }}
>
{}
</div>
{}
<div
className="w-1/2 overflow-y-auto"
style={{ backgroundColor: styles.surfacePrimary }}
>
{}
</div>
</div>
</div>
Theme-Aware Highlights
Use theme system colors for search highlights:
import { useTheme } from "@/hooks/useTheme";
const { currentTheme } = useTheme();
const { styles } = currentTheme;
<mark
style={{
backgroundColor: styles.semanticPrimary,
color: styles.semanticPrimaryForeground,
borderRadius: "2px",
padding: "0 2px",
}}
>
{matchedText}
</mark>
<div
style={{
backgroundColor: isMatchLine ? styles.surfaceAccent : "transparent",
}}
>
{lineContent}
</div>
Available Theme Colors
From useTheme().currentTheme.styles:
Surfaces (backgrounds):
surfacePrimary - Main background
surfaceSecondary - Secondary/elevated background
surfaceTertiary - Hover states, selected items
surfaceAccent - Accent/highlight backgrounds
surfaceMuted - Muted backgrounds
Content (text):
contentPrimary - Main text
contentSecondary - Secondary text
contentTertiary - Muted/disabled text
contentAccent - Accent text
Borders:
borderDefault - Standard borders
borderAccent - Accent borders
Semantic (actions/status):
semanticPrimary / semanticPrimaryForeground - Primary actions, highlights
semanticDestructive / semanticDestructiveForeground - Delete, danger
semanticSuccess / semanticSuccessForeground - Success states
Keyboard Navigation
Implement arrow key navigation for lists:
const [selectedIndex, setSelectedIndex] = React.useState(0);
React.useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "ArrowDown") {
e.preventDefault();
setSelectedIndex(prev => (prev + 1) % results.length);
} else if (e.key === "ArrowUp") {
e.preventDefault();
setSelectedIndex(prev => (prev - 1 + results.length) % results.length);
} else if (e.key === "Enter") {
e.preventDefault();
} else if (e.key === "Escape") {
e.preventDefault();
closeDialog();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [results, selectedIndex]);
Auto-scroll Selected Item Into View
const resultsContainerRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (resultsContainerRef.current && results.length > 0) {
const selectedElement = resultsContainerRef.current.querySelector(
`[data-index="${selectedIndex}"]`
);
if (selectedElement) {
selectedElement.scrollIntoView({ block: "nearest", behavior: "smooth" });
}
}
}, [selectedIndex, results.length]);
<div ref={resultsContainerRef}>
{results.map((result, index) => (
<div key={result.id} data-index={index}>
{/* ... */}
</div>
))}
</div>
Scroll-to-Line on Note Open
When opening a note from search results, scroll to the first match:
const openNote = (fileName: string, scrollToLine?: number) => {
addNewTab({
pluginMeta: notesPluginSerial,
view: "editor",
props: { noteFileName: fileName, scrollToLine }
});
};
const contentMatches = result.matches.filter(m => m.line > 0);
const firstMatchLine = contentMatches.length > 0 ? contentMatches[0].line : undefined;
openNote(result.fileName, firstMatchLine);
The NotesView component accepts scrollToLine prop and scrolls the ProseMirror editor to that line on initial load.