constellation-analyzer/src/stores/panelStore.ts
Jan-Henrik Bruhn d98acf963b feat: implement label system and redesign filtering with positive filters
Implements a comprehensive label system and completely redesigns all
filtering (labels, actor types, relation types) to use intuitive positive
filtering where empty selection shows all items.

Label System Features:
- Create, edit, delete labels with names, colors, and scope (actors/relations/both)
- Inline editing with click-to-edit UI for quick modifications
- Quick-add label forms in config modals
- Autocomplete label selector with inline label creation
- Label badges rendered on nodes and edges (no overflow limits)
- Full undo/redo support for label operations
- Label validation and cleanup when labels are deleted
- Labels stored per-document in workspace system

Filter System Redesign:
- Changed from negative to positive filtering for all filter types
- Empty selection = show all items (intuitive default)
- Selected items = show only those items (positive filter)
- Consistent behavior across actor types, relation types, and labels
- Clear visual feedback with selection counts and helper text
- Auto-zoom viewport adjustment works for all filter types including labels

Label Cleanup & Validation:
- When label deleted, automatically removed from all nodes/edges across all timeline states
- Label references validated during node/edge updates
- Unknown label IDs filtered out to maintain data integrity

UI Improvements:
- All labels rendered without overflow limits (removed +N more indicators)
- Filter checkboxes start unchecked (select to filter, not hide)
- Helper text explains current filter state
- Selection counts displayed in filter section headers

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 10:40:00 +02:00

174 lines
4.3 KiB
TypeScript

import { create } from 'zustand';
import { persist } from 'zustand/middleware';
/**
* Panel Store - Manages state of collapsible side panels
*
* Features:
* - Left panel (tools) visibility and width
* - Right panel (properties) visibility and width
* - Panel state persistence to localStorage
* - Collapsed section state within panels
*/
interface PanelState {
// Left Panel
leftPanelVisible: boolean;
leftPanelWidth: number;
leftPanelCollapsed: boolean;
leftPanelSections: {
history: boolean;
addActors: boolean;
relations: boolean;
labels: boolean;
layout: boolean;
view: boolean;
search: boolean;
};
// Right Panel
rightPanelVisible: boolean;
rightPanelWidth: number;
rightPanelCollapsed: boolean;
// Bottom Panel (Timeline)
bottomPanelVisible: boolean;
bottomPanelHeight: number;
bottomPanelCollapsed: boolean;
// Actions
toggleLeftPanel: () => void;
toggleRightPanel: () => void;
setLeftPanelWidth: (width: number) => void;
setRightPanelWidth: (width: number) => void;
setBottomPanelHeight: (height: number) => void;
toggleLeftPanelSection: (section: keyof PanelState['leftPanelSections']) => void;
collapseLeftPanel: () => void;
expandLeftPanel: () => void;
collapseRightPanel: () => void;
expandRightPanel: () => void;
collapseBottomPanel: () => void;
expandBottomPanel: () => void;
}
const DEFAULT_LEFT_WIDTH = 280;
const DEFAULT_RIGHT_WIDTH = 320;
const DEFAULT_BOTTOM_HEIGHT = 200;
const MIN_LEFT_WIDTH = 240;
const MAX_LEFT_WIDTH = 400;
const MIN_RIGHT_WIDTH = 280;
const MAX_RIGHT_WIDTH = 500;
const MIN_BOTTOM_HEIGHT = 150;
const MAX_BOTTOM_HEIGHT = 500;
const COLLAPSED_LEFT_WIDTH = 40;
const COLLAPSED_BOTTOM_HEIGHT = 48;
export const usePanelStore = create<PanelState>()(
persist(
(set) => ({
// Initial state
leftPanelVisible: true,
leftPanelWidth: DEFAULT_LEFT_WIDTH,
leftPanelCollapsed: false,
leftPanelSections: {
history: true,
addActors: true,
relations: true,
labels: false,
layout: false,
view: false,
search: false,
},
rightPanelVisible: true,
rightPanelWidth: DEFAULT_RIGHT_WIDTH,
rightPanelCollapsed: false,
bottomPanelVisible: true, // Timeline panel is always visible (can be collapsed but not hidden)
bottomPanelHeight: DEFAULT_BOTTOM_HEIGHT,
bottomPanelCollapsed: false,
// Actions
toggleLeftPanel: () =>
set((state) => ({
leftPanelVisible: !state.leftPanelVisible,
})),
toggleRightPanel: () =>
set((state) => ({
rightPanelVisible: !state.rightPanelVisible,
})),
setLeftPanelWidth: (width: number) =>
set(() => ({
leftPanelWidth: Math.max(MIN_LEFT_WIDTH, Math.min(MAX_LEFT_WIDTH, width)),
})),
setRightPanelWidth: (width: number) =>
set(() => ({
rightPanelWidth: Math.max(MIN_RIGHT_WIDTH, Math.min(MAX_RIGHT_WIDTH, width)),
})),
setBottomPanelHeight: (height: number) =>
set(() => ({
bottomPanelHeight: Math.max(MIN_BOTTOM_HEIGHT, Math.min(MAX_BOTTOM_HEIGHT, height)),
})),
toggleLeftPanelSection: (section) =>
set((state) => ({
leftPanelSections: {
...state.leftPanelSections,
[section]: !state.leftPanelSections[section],
},
})),
collapseLeftPanel: () =>
set(() => ({
leftPanelCollapsed: true,
})),
expandLeftPanel: () =>
set(() => ({
leftPanelCollapsed: false,
})),
collapseRightPanel: () =>
set(() => ({
rightPanelCollapsed: true,
})),
expandRightPanel: () =>
set(() => ({
rightPanelCollapsed: false,
})),
collapseBottomPanel: () =>
set(() => ({
bottomPanelCollapsed: true,
})),
expandBottomPanel: () =>
set(() => ({
bottomPanelCollapsed: false,
})),
}),
{
name: 'constellation-panel-state',
}
)
);
// Export constants for use in components
export const PANEL_CONSTANTS = {
DEFAULT_LEFT_WIDTH,
DEFAULT_RIGHT_WIDTH,
DEFAULT_BOTTOM_HEIGHT,
MIN_LEFT_WIDTH,
MAX_LEFT_WIDTH,
MIN_RIGHT_WIDTH,
MAX_RIGHT_WIDTH,
MIN_BOTTOM_HEIGHT,
MAX_BOTTOM_HEIGHT,
COLLAPSED_LEFT_WIDTH,
COLLAPSED_BOTTOM_HEIGHT,
};