feat: add sidebar enhancement components (Tier 7)

New components:
- KeyboardShortcutsPanel: Collapsible panel for displaying keyboard shortcuts
  with support for categories and compact mode
- SidebarSection: Grouped navigation section with optional collapse behavior

New types:
- KeyboardShortcut: Interface for shortcut definitions (keys, label, category)

These components enable building feature-rich sidebars like Memoro's while
keeping the shared-ui Sidebar flexible and data-driven.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-25 00:42:46 +01:00
parent 5045d70bf7
commit cacbd61fe4
6 changed files with 306 additions and 4 deletions

View file

@ -21,12 +21,12 @@ export { SkeletonBox, SkeletonText } from './molecules';
export { EmptyState } from './molecules';
// Layout
export { ModalFooter, DataCard, PageHeader } from './molecules';
export { ModalFooter, DataCard, PageHeader, KeyboardShortcutsPanel } from './molecules';
// Organisms
export { Modal, ConfirmationModal, FormModal, AppSlider } from './organisms';
export type { AppItem } from './organisms';
// Navigation
export { NavLink, Navbar, Sidebar } from './navigation';
export type { NavItem, NavbarProps, SidebarProps, NavLinkProps } from './navigation';
export { NavLink, Navbar, Sidebar, SidebarSection } from './navigation';
export type { NavItem, NavbarProps, SidebarProps, NavLinkProps, KeyboardShortcut } from './navigation';

View file

@ -0,0 +1,181 @@
<script lang="ts">
/**
* KeyboardShortcutsPanel - Collapsible panel showing keyboard shortcuts
*
* Used in sidebars to show available keyboard shortcuts.
* Supports grouping shortcuts by category.
*
* @example Basic usage
* ```svelte
* <KeyboardShortcutsPanel
* shortcuts={[
* { keys: ['Ctrl', 'S'], label: 'Save' },
* { keys: ['Ctrl', 'Z'], label: 'Undo' }
* ]}
* />
* ```
*
* @example With categories
* ```svelte
* <KeyboardShortcutsPanel
* shortcuts={[
* { keys: ['Ctrl', 'W'], label: 'Close Tab', category: 'Navigation' },
* { keys: ['Ctrl', '1'], label: 'Go to Record', category: 'Quick Access' }
* ]}
* title="Keyboard Shortcuts"
* />
* ```
*/
import { Text } from '../atoms';
export interface KeyboardShortcut {
/** Key combination (e.g., ['Ctrl', 'S'] or ['Cmd', 'Shift', 'P']) */
keys: string[];
/** Description of what the shortcut does */
label: string;
/** Category for grouping (optional) */
category?: string;
}
interface Props {
/** List of keyboard shortcuts */
shortcuts: KeyboardShortcut[];
/** Panel title */
title?: string;
/** Whether panel is initially expanded */
expanded?: boolean;
/** Whether panel is collapsible */
collapsible?: boolean;
/** Whether to show in compact mode (for minimized sidebar) */
compact?: boolean;
/** Additional CSS classes */
class?: string;
}
let {
shortcuts,
title = 'Shortcuts',
expanded = $bindable(false),
collapsible = true,
compact = false,
class: className = ''
}: Props = $props();
// Group shortcuts by category
const groupedShortcuts = $derived(() => {
const groups: Record<string, KeyboardShortcut[]> = {};
for (const shortcut of shortcuts) {
const category = shortcut.category || 'General';
if (!groups[category]) {
groups[category] = [];
}
groups[category].push(shortcut);
}
return groups;
});
function toggleExpanded() {
if (collapsible) {
expanded = !expanded;
}
}
</script>
{#if !compact}
<div class="keyboard-shortcuts-panel {className}">
<!-- Header -->
<button
type="button"
class="w-full flex items-center justify-between px-3 py-2 text-left hover:bg-menu-hover rounded-lg transition-colors"
onclick={toggleExpanded}
disabled={!collapsible}
>
<div class="flex items-center gap-2">
<svg
class="w-4 h-4 text-theme-secondary"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707"
/>
</svg>
<Text variant="small" weight="medium">{title}</Text>
</div>
{#if collapsible}
<svg
class="w-4 h-4 text-theme-secondary transition-transform {expanded ? 'rotate-180' : ''}"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
{/if}
</button>
<!-- Content -->
{#if expanded || !collapsible}
<div class="mt-2 space-y-3 px-3 pb-3">
{#each Object.entries(groupedShortcuts()) as [category, categoryShortcuts]}
<div class="shortcut-group">
{#if Object.keys(groupedShortcuts()).length > 1}
<Text variant="small" class="text-theme-tertiary mb-1.5 block">
{category}
</Text>
{/if}
<div class="space-y-1.5">
{#each categoryShortcuts as shortcut}
<div class="flex items-center justify-between gap-2">
<Text variant="small" class="text-theme-secondary truncate">
{shortcut.label}
</Text>
<div class="flex items-center gap-1 flex-shrink-0">
{#each shortcut.keys as key, i}
<kbd
class="px-1.5 py-0.5 text-xs font-mono bg-surface-secondary rounded border border-theme"
>
{key}
</kbd>
{#if i < shortcut.keys.length - 1}
<span class="text-theme-tertiary text-xs">+</span>
{/if}
{/each}
</div>
</div>
{/each}
</div>
</div>
{/each}
</div>
{/if}
</div>
{:else}
<!-- Compact mode: just show icon with tooltip -->
<button
type="button"
class="w-full flex items-center justify-center p-2 hover:bg-menu-hover rounded-lg transition-colors group relative"
title={title}
>
<svg
class="w-5 h-5 text-theme-secondary"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707"
/>
</svg>
</button>
{/if}

View file

@ -24,3 +24,4 @@ export { EmptyState } from './feedback';
export { default as ModalFooter } from './ModalFooter.svelte';
export { default as DataCard } from './DataCard.svelte';
export { default as PageHeader } from './PageHeader.svelte';
export { default as KeyboardShortcutsPanel } from './KeyboardShortcutsPanel.svelte';

View file

@ -0,0 +1,110 @@
<script lang="ts">
/**
* SidebarSection - Grouped navigation section within a sidebar
*
* Provides a labeled section for organizing related navigation items.
* Supports collapsible behavior and minimized sidebar mode.
*
* @example Basic usage
* ```svelte
* <SidebarSection title="Main" items={mainNavItems} {currentPath} />
* ```
*
* @example Collapsible section
* ```svelte
* <SidebarSection
* title="Settings"
* items={settingsItems}
* collapsible
* expanded={false}
* />
* ```
*/
import type { NavItem } from './types';
import NavLink from './NavLink.svelte';
import { Text } from '../atoms';
interface Props {
/** Section title (hidden when minimized) */
title?: string;
/** Navigation items in this section */
items: NavItem[];
/** Current path for active state */
currentPath?: string;
/** Whether sidebar is minimized */
minimized?: boolean;
/** Whether section can be collapsed */
collapsible?: boolean;
/** Whether section is expanded (when collapsible) */
expanded?: boolean;
/** Divider above section */
divider?: boolean;
/** Additional CSS classes */
class?: string;
}
let {
title,
items,
currentPath = '',
minimized = false,
collapsible = false,
expanded = $bindable(true),
divider = false,
class: className = ''
}: Props = $props();
function isActive(item: NavItem): boolean {
if (item.active !== undefined) return item.active;
return currentPath === item.href || currentPath.startsWith(item.href + '/');
}
function toggleExpanded() {
if (collapsible) {
expanded = !expanded;
}
}
</script>
<div class="sidebar-section {className}">
{#if divider && !minimized}
<div class="border-t border-theme my-2 mx-3"></div>
{/if}
{#if title && !minimized}
{#if collapsible}
<button
type="button"
class="w-full flex items-center justify-between px-3 py-1.5 text-left hover:bg-menu-hover rounded transition-colors"
onclick={toggleExpanded}
>
<Text variant="small" class="text-theme-tertiary uppercase tracking-wider">
{title}
</Text>
<svg
class="w-3 h-3 text-theme-tertiary transition-transform {expanded ? '' : '-rotate-90'}"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
{:else}
<div class="px-3 py-1.5">
<Text variant="small" class="text-theme-tertiary uppercase tracking-wider">
{title}
</Text>
</div>
{/if}
{/if}
{#if expanded || !collapsible}
<nav class="space-y-0.5 {title && !minimized ? 'mt-1' : ''}">
{#each items as item}
<NavLink {item} variant="sidebar" {minimized} active={isActive(item)} />
{/each}
</nav>
{/if}
</div>

View file

@ -1,4 +1,5 @@
export { default as NavLink } from './NavLink.svelte';
export { default as Navbar } from './Navbar.svelte';
export { default as Sidebar } from './Sidebar.svelte';
export type { NavItem, NavbarProps, SidebarProps, NavLinkProps } from './types';
export { default as SidebarSection } from './SidebarSection.svelte';
export type { NavItem, NavbarProps, SidebarProps, NavLinkProps, KeyboardShortcut } from './types';

View file

@ -1,5 +1,14 @@
import type { Snippet } from 'svelte';
export interface KeyboardShortcut {
/** Key combination (e.g., ['Ctrl', 'S'] or ['Cmd', 'Shift', 'P']) */
keys: string[];
/** Description of what the shortcut does */
label: string;
/** Category for grouping (optional) */
category?: string;
}
export interface NavItem {
/** Display label for the navigation item */
label: string;