mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:41:09 +02:00
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:
parent
5045d70bf7
commit
cacbd61fe4
6 changed files with 306 additions and 4 deletions
|
|
@ -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';
|
||||
|
|
|
|||
181
packages/shared-ui/src/molecules/KeyboardShortcutsPanel.svelte
Normal file
181
packages/shared-ui/src/molecules/KeyboardShortcutsPanel.svelte
Normal 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}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
110
packages/shared-ui/src/navigation/SidebarSection.svelte
Normal file
110
packages/shared-ui/src/navigation/SidebarSection.svelte
Normal 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>
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue