From c8f3361816cbe4f47cc1a2eb905ce42b11b00fe7 Mon Sep 17 00:00:00 2001 From: Schalli Date: Thu, 2 Jul 2026 08:57:37 +0200 Subject: [PATCH] fix(dashboard): noCompactor + note edit/preview toggle + widget border - dashboard-grid: add noCompactor to prevent auto-compaction on drag - note-widget: edit/preview toggle button (pencil icon), isEditing state, hideToolbar in preview mode - widget-wrapper: border-primary/20 accent border - dashboard-store: console.error on widget add/remove/layout-save failures Co-Authored-By: Claude Sonnet 4.6 --- .../components/dashboard/dashboard-grid.tsx | 3 +- .../dashboard/widgets/note-widget.tsx | 64 +++++++++---------- .../dashboard/widgets/widget-wrapper.tsx | 2 +- apps/web/src/lib/stores/dashboard-store.ts | 12 ++-- 4 files changed, 41 insertions(+), 40 deletions(-) diff --git a/apps/web/src/components/dashboard/dashboard-grid.tsx b/apps/web/src/components/dashboard/dashboard-grid.tsx index 5e15c76..585e1bc 100644 --- a/apps/web/src/components/dashboard/dashboard-grid.tsx +++ b/apps/web/src/components/dashboard/dashboard-grid.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useRef, useState } from 'react'; -import { Responsive } from 'react-grid-layout'; +import { Responsive, noCompactor } from 'react-grid-layout'; import type { ResponsiveLayouts } from 'react-grid-layout'; import 'react-grid-layout/css/styles.css'; import 'react-resizable/css/styles.css'; @@ -97,6 +97,7 @@ export function DashboardGrid({ resizeConfig={{ enabled: isEditMode, }} + compactor={noCompactor} onLayoutChange={(_currentLayout, allLayouts) => onLayoutChange(allLayouts)} > {widgets.map((widget) => { diff --git a/apps/web/src/components/dashboard/widgets/note-widget.tsx b/apps/web/src/components/dashboard/widgets/note-widget.tsx index 6680ab7..0d0b17e 100644 --- a/apps/web/src/components/dashboard/widgets/note-widget.tsx +++ b/apps/web/src/components/dashboard/widgets/note-widget.tsx @@ -9,10 +9,6 @@ import { updateWidgetConfig } from '@/lib/dashboard-api'; const DEBOUNCE_MS = 1500; -/** - * Compact toolbar commands (D-16). - * Bold, Italic, Strikethrough, divider, Unordered List, Checkbox, divider, Link, Code - */ const NOTE_COMMANDS = [ commands.bold, commands.italic, @@ -25,12 +21,6 @@ const NOTE_COMMANDS = [ commands.code, ]; -/** - * Notes widget (DASH-06, D-16/D-17/D-18). - * Markdown editor with compact toolbar and debounced autosave. - * Uses AbortController to cancel in-flight requests before issuing new ones (Pitfall 7). - * rehype-sanitize is enabled for XSS prevention (T-05-05). - */ export function NoteWidget({ instanceId, config }: WidgetProps) { const t = useTranslations('widgets'); const [title, setTitle] = useState( @@ -39,12 +29,12 @@ export function NoteWidget({ instanceId, config }: WidgetProps) { const [content, setContent] = useState( (config.content as string) || '', ); + const [isEditing, setIsEditing] = useState(false); const [saveError, setSaveError] = useState(false); const timerRef = useRef | undefined>(undefined); const abortRef = useRef(undefined); - // Cleanup on unmount useEffect(() => { return () => { clearTimeout(timerRef.current); @@ -54,21 +44,13 @@ export function NoteWidget({ instanceId, config }: WidgetProps) { const save = useCallback( async (newContent: string, newTitle: string) => { - // Abort any in-flight request (Pitfall 7) abortRef.current?.abort(); abortRef.current = new AbortController(); - try { - await updateWidgetConfig(instanceId, { - content: newContent, - title: newTitle, - }, abortRef.current.signal); + await updateWidgetConfig(instanceId, { content: newContent, title: newTitle }, abortRef.current.signal); setSaveError(false); } catch (err: unknown) { - // AbortError is expected — swallow it - if (err instanceof DOMException && err.name === 'AbortError') { - return; - } + if (err instanceof DOMException && err.name === 'AbortError') return; setSaveError(true); } }, @@ -78,9 +60,7 @@ export function NoteWidget({ instanceId, config }: WidgetProps) { const scheduleSave = useCallback( (newContent: string, newTitle: string) => { clearTimeout(timerRef.current); - timerRef.current = setTimeout(() => { - save(newContent, newTitle); - }, DEBOUNCE_MS); + timerRef.current = setTimeout(() => save(newContent, newTitle), DEBOUNCE_MS); }, [save], ); @@ -105,32 +85,52 @@ export function NoteWidget({ instanceId, config }: WidgetProps) { return (
- {/* Editable title (D-17, Body 14px weight 600) */} -
+ {/* Header: title + pencil toggle */} +
- {/* Autosave error indicator (small red dot top-right) */} {saveError && ( )} +
- {/* Markdown editor (D-16, D-18) */} + {/* Content: preview or editor */}
()((set, get) => ({ isDirty: true, }; }); - } catch { - set({ error: 'Failed to add widget' }); + } catch (err) { + console.error('Failed to add widget:', err); } }, @@ -100,8 +100,8 @@ export const useDashboardStore = create()((set, get) => ({ isDirty: true, }; }); - } catch { - set({ error: 'Failed to remove widget' }); + } catch (err) { + console.error('Failed to remove widget:', err); } }, @@ -129,8 +129,8 @@ export const useDashboardStore = create()((set, get) => ({ try { await api.saveLayout(get().layouts); set({ isDirty: false }); - } catch { - set({ error: 'Failed to save layout' }); + } catch (err) { + console.error('Failed to save layout:', err); } }, }));