From 405a3df66bcd7ac77c1187db918b606c093c7df4 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Thu, 13 Nov 2025 23:35:10 -0600 Subject: [PATCH 1/4] Fix coaching relationship sync and stale data when navigating between sessions When navigating between sessions with different relationships, the page displayed wrong coachee names while showing correct notes. Fixed by: - Updated auto-sync to trigger when relationship ID differs (not just when empty) - Added shouldSyncRelationship helper for readability - Call refresh() after setting new relationship ID to fetch correct data - Added comprehensive test coverage for all sync scenarios Fixes #228 --- .../coaching-session-page.test.tsx | 296 ++++++++++++++++-- src/app/coaching-sessions/[id]/page.tsx | 41 ++- 2 files changed, 311 insertions(+), 26 deletions(-) diff --git a/__tests__/app/coaching-sessions/coaching-session-page.test.tsx b/__tests__/app/coaching-sessions/coaching-session-page.test.tsx index 5d35c8a5..0da523f0 100644 --- a/__tests__/app/coaching-sessions/coaching-session-page.test.tsx +++ b/__tests__/app/coaching-sessions/coaching-session-page.test.tsx @@ -3,6 +3,8 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { useRouter, useParams, useSearchParams } from 'next/navigation' import CoachingSessionsPage from '@/app/coaching-sessions/[id]/page' import { TestProviders } from '@/test-utils/providers' +import { useCurrentCoachingSession } from '@/lib/hooks/use-current-coaching-session' +import { useCurrentCoachingRelationship } from '@/lib/hooks/use-current-coaching-relationship' // Mock Next.js navigation hooks vi.mock('next/navigation', () => ({ @@ -12,24 +14,8 @@ vi.mock('next/navigation', () => ({ })) // Mock the coaching session hooks -vi.mock('@/lib/hooks/use-current-coaching-session', () => ({ - useCurrentCoachingSession: vi.fn(() => ({ - currentCoachingSessionId: 'session-123', - currentCoachingSession: { - id: 'session-123', - title: 'Test Session', - coaching_relationship_id: 'rel-123' - }, - isError: false, - })) -})) - -vi.mock('@/lib/hooks/use-current-coaching-relationship', () => ({ - useCurrentCoachingRelationship: vi.fn(() => ({ - currentCoachingRelationshipId: 'rel-123', - setCurrentCoachingRelationshipId: vi.fn(), - })) -})) +vi.mock('@/lib/hooks/use-current-coaching-session') +vi.mock('@/lib/hooks/use-current-coaching-relationship') // Mock auth store vi.mock('@/lib/providers/auth-store-provider', () => ({ @@ -94,6 +80,30 @@ describe('CoachingSessionsPage URL Parameter Persistence', () => { vi.clearAllMocks() ;(useRouter as any).mockReturnValue(mockRouter) ;(useParams as any).mockReturnValue(mockParams) + + // Set default mocks for relationship hooks + vi.mocked(useCurrentCoachingSession).mockReturnValue({ + currentCoachingSessionId: 'session-123', + currentCoachingSession: { + id: 'session-123', + title: 'Test Session', + coaching_relationship_id: 'rel-123' + } as any, + isError: false, + isLoading: false, + refresh: vi.fn(), + }) + + vi.mocked(useCurrentCoachingRelationship).mockReturnValue({ + currentCoachingRelationshipId: 'rel-123', + setCurrentCoachingRelationshipId: vi.fn(), + currentCoachingRelationship: null, + isLoading: false, + isError: false, + currentOrganizationId: 'org-123', + resetCoachingRelationshipState: vi.fn(), + refresh: vi.fn(), + }) }) /** @@ -217,4 +227,254 @@ describe('CoachingSessionsPage URL Parameter Persistence', () => { ) }) }) +}) + +/** + * Test Suite: Relationship Auto-Sync Behavior + * + * Purpose: Validates that the coaching relationship ID is correctly synced from the current + * session data to the store in various navigation scenarios, fixing Bug #228 while preserving + * the fix for Issue #79 (new tab support). + */ +describe('CoachingSessionsPage - Relationship Auto-Sync', () => { + const mockRouter = { + push: vi.fn(), + replace: vi.fn(), + } + + const mockParams = { + id: 'session-123' + } + + beforeEach(() => { + vi.clearAllMocks() + ;(useRouter as any).mockReturnValue(mockRouter) + ;(useParams as any).mockReturnValue(mockParams) + ;(useSearchParams as any).mockReturnValue(new URLSearchParams()) + }) + + /** + * Test: First Load with Empty Store (Issue #79) + * + * Scenario: User opens a session URL in a new tab/window with empty sessionStorage + * Expected: Relationship ID should be synced from session data to store AND refresh called + * This ensures Issue #79 (new tab support) continues to work + */ + it('should sync relationship ID on first load with empty store', () => { + const mockSetRelationshipId = vi.fn() + const mockRefresh = vi.fn() + + // Session has relationship ID, but store is empty (new tab scenario) + vi.mocked(useCurrentCoachingSession).mockReturnValue({ + currentCoachingSessionId: 'session-123', + currentCoachingSession: { + id: 'session-123', + coaching_relationship_id: 'rel-123' + } as any, + isError: false, + isLoading: false, + refresh: vi.fn(), + }) + + vi.mocked(useCurrentCoachingRelationship).mockReturnValue({ + currentCoachingRelationshipId: null, // Empty store + setCurrentCoachingRelationshipId: mockSetRelationshipId, + currentCoachingRelationship: null, + isLoading: false, + isError: false, + currentOrganizationId: 'org-123', + resetCoachingRelationshipState: vi.fn(), + refresh: mockRefresh, + }) + + render( + + + + ) + + // Should call setCurrentCoachingRelationshipId with the session's relationship ID + expect(mockSetRelationshipId).toHaveBeenCalledWith('rel-123') + // Should call refresh to fetch the relationship data + expect(mockRefresh).toHaveBeenCalled() + }) + + /** + * Test: Switching Between Sessions with Different Relationships (Bug #228) + * + * Scenario: User navigates from Session A (rel-1) to Session B (rel-2) + * Expected: Relationship ID should update from rel-1 to rel-2 AND refresh called + * This is the primary fix for Bug #228 + */ + it('should update relationship ID when switching to session with different relationship', () => { + const mockSetRelationshipId = vi.fn() + const mockRefresh = vi.fn() + + // Session has relationship ID 'rel-456', but store has stale 'rel-123' + vi.mocked(useCurrentCoachingSession).mockReturnValue({ + currentCoachingSessionId: 'session-456', + currentCoachingSession: { + id: 'session-456', + coaching_relationship_id: 'rel-456' // Different relationship + } as any, + isError: false, + isLoading: false, + refresh: vi.fn(), + }) + + vi.mocked(useCurrentCoachingRelationship).mockReturnValue({ + currentCoachingRelationshipId: 'rel-123', // Stale relationship from previous session + setCurrentCoachingRelationshipId: mockSetRelationshipId, + currentCoachingRelationship: null, + isLoading: false, + isError: false, + currentOrganizationId: 'org-123', + resetCoachingRelationshipState: vi.fn(), + refresh: mockRefresh, + }) + + render( + + + + ) + + // Should call setCurrentCoachingRelationshipId to update to the new relationship + expect(mockSetRelationshipId).toHaveBeenCalledWith('rel-456') + // Should call refresh to fetch the new relationship data (fixes stale cache bug) + expect(mockRefresh).toHaveBeenCalled() + }) + + /** + * Test: Same Relationship, Different Session + * + * Scenario: User navigates from Session A to Session B, both in the same relationship + * Expected: setCurrentCoachingRelationshipId should NOT be called (optimization) + * This ensures we don't trigger unnecessary updates + */ + it('should not update relationship ID when switching to session with same relationship', () => { + const mockSetRelationshipId = vi.fn() + + // Session and store both have the same relationship ID + vi.mocked(useCurrentCoachingSession).mockReturnValue({ + currentCoachingSessionId: 'session-456', + currentCoachingSession: { + id: 'session-456', + coaching_relationship_id: 'rel-123' // Same relationship + } as any, + isError: false, + isLoading: false, + refresh: vi.fn(), + }) + + vi.mocked(useCurrentCoachingRelationship).mockReturnValue({ + currentCoachingRelationshipId: 'rel-123', // Same relationship already in store + setCurrentCoachingRelationshipId: mockSetRelationshipId, + currentCoachingRelationship: null, + isLoading: false, + isError: false, + currentOrganizationId: 'org-123', + resetCoachingRelationshipState: vi.fn(), + refresh: vi.fn(), + }) + + render( + + + + ) + + // Should NOT call setCurrentCoachingRelationshipId since they match + expect(mockSetRelationshipId).not.toHaveBeenCalled() + }) + + /** + * Test: Session Without Relationship ID + * + * Scenario: Session data is loaded but doesn't have a coaching_relationship_id + * Expected: setCurrentCoachingRelationshipId should NOT be called + * This handles edge cases where session data might be incomplete + */ + it('should not update relationship ID when session has no relationship', () => { + const mockSetRelationshipId = vi.fn() + + // Session without relationship ID + vi.mocked(useCurrentCoachingSession).mockReturnValue({ + currentCoachingSessionId: 'session-123', + currentCoachingSession: { + id: 'session-123', + // No coaching_relationship_id + } as any, + isError: false, + isLoading: false, + refresh: vi.fn(), + }) + + vi.mocked(useCurrentCoachingRelationship).mockReturnValue({ + currentCoachingRelationshipId: 'rel-123', + setCurrentCoachingRelationshipId: mockSetRelationshipId, + currentCoachingRelationship: null, + isLoading: false, + isError: false, + currentOrganizationId: 'org-123', + resetCoachingRelationshipState: vi.fn(), + refresh: vi.fn(), + }) + + render( + + + + ) + + // Should NOT call setCurrentCoachingRelationshipId + expect(mockSetRelationshipId).not.toHaveBeenCalled() + }) + + /** + * Test: Direct URL Access with Stale Store + * + * Scenario: User manually types a session URL while store has a different relationship + * Expected: Relationship ID should update to match the session from the URL AND refresh called + * This ensures URL is always the source of truth + */ + it('should handle direct URL access with stale relationship ID in store', () => { + const mockSetRelationshipId = vi.fn() + const mockRefresh = vi.fn() + + // User types URL for session-789 which belongs to rel-789 + // But store has stale rel-123 from previous browsing + vi.mocked(useCurrentCoachingSession).mockReturnValue({ + currentCoachingSessionId: 'session-789', + currentCoachingSession: { + id: 'session-789', + coaching_relationship_id: 'rel-789' + } as any, + isError: false, + isLoading: false, + refresh: vi.fn(), + }) + + vi.mocked(useCurrentCoachingRelationship).mockReturnValue({ + currentCoachingRelationshipId: 'rel-123', // Stale from previous session + setCurrentCoachingRelationshipId: mockSetRelationshipId, + currentCoachingRelationship: null, + isLoading: false, + isError: false, + currentOrganizationId: 'org-123', + resetCoachingRelationshipState: vi.fn(), + refresh: mockRefresh, + }) + + render( + + + + ) + + // Should update to match the URL-based session + expect(mockSetRelationshipId).toHaveBeenCalledWith('rel-789') + // Should call refresh to fetch the new relationship data + expect(mockRefresh).toHaveBeenCalled() + }) }) \ No newline at end of file diff --git a/src/app/coaching-sessions/[id]/page.tsx b/src/app/coaching-sessions/[id]/page.tsx index 1ed5bc9e..c0f1a174 100644 --- a/src/app/coaching-sessions/[id]/page.tsx +++ b/src/app/coaching-sessions/[id]/page.tsx @@ -37,25 +37,50 @@ export default function CoachingSessionsPage() { const { currentCoachingSession, currentCoachingSessionId, isError } = useCurrentCoachingSession(); // Get current coaching relationship state and data - const { currentCoachingRelationshipId, setCurrentCoachingRelationshipId } = + const { currentCoachingRelationshipId, setCurrentCoachingRelationshipId, refresh } = useCurrentCoachingRelationship(); + /** + * Helper function to determine if relationship ID should be synced from session data. + * + * The URL is the source of truth for the current session. We sync the relationship ID + * from the session data in two cases: + * 1. Store is empty (e.g., new tab/window) - fixes Issue #79 + * 2. Store has a different relationship (e.g., navigating between sessions) - fixes Bug #228 + * + * @param sessionRelationshipId - The relationship ID from the current session + * @param currentRelationshipId - The relationship ID currently in the store + * @returns true if we should sync the relationship ID + */ + const shouldSyncRelationship = ( + sessionRelationshipId: string | undefined, + currentRelationshipId: string | null + ): boolean => { + if (!sessionRelationshipId) return false; + // Always sync when empty (new tab) or when different (switching sessions) + return !currentRelationshipId || sessionRelationshipId !== currentRelationshipId; + }; - // Auto-sync relationship ID when session data loads (if not already set) + // Auto-sync relationship ID when session data loads + // This ensures the relationship selector always matches the current session useEffect(() => { - if ( - currentCoachingSession?.coaching_relationship_id && - !currentCoachingRelationshipId - ) { + if (shouldSyncRelationship( + currentCoachingSession?.coaching_relationship_id, + currentCoachingRelationshipId + )) { setCurrentCoachingRelationshipId( currentCoachingSession.coaching_relationship_id ); + + // Force immediate fetch of new relationship data to prevent showing stale cached data + // This ensures the coaching session title shows the correct coach/coachee names + refresh(); } - // setCurrentCoachingRelationshipId is stable and doesn't need to be in deps - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ currentCoachingSession?.coaching_relationship_id, currentCoachingRelationshipId, + setCurrentCoachingRelationshipId, + refresh, ]); // Check for 403 Forbidden error AFTER all hooks are called From b75657de479bb544f93c7504eec12c8921ea9140 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Thu, 13 Nov 2025 23:39:37 -0600 Subject: [PATCH 2/4] Fix TypeScript null check in auto-sync logic --- src/app/coaching-sessions/[id]/page.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/coaching-sessions/[id]/page.tsx b/src/app/coaching-sessions/[id]/page.tsx index c0f1a174..f8c0a356 100644 --- a/src/app/coaching-sessions/[id]/page.tsx +++ b/src/app/coaching-sessions/[id]/page.tsx @@ -64,10 +64,13 @@ export default function CoachingSessionsPage() { // Auto-sync relationship ID when session data loads // This ensures the relationship selector always matches the current session useEffect(() => { - if (shouldSyncRelationship( - currentCoachingSession?.coaching_relationship_id, - currentCoachingRelationshipId - )) { + if ( + currentCoachingSession?.coaching_relationship_id && + shouldSyncRelationship( + currentCoachingSession.coaching_relationship_id, + currentCoachingRelationshipId + ) + ) { setCurrentCoachingRelationshipId( currentCoachingSession.coaching_relationship_id ); From 17dee9c4712c3a1882cfd121c0017704da4f8467 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Fri, 14 Nov 2025 00:05:58 -0600 Subject: [PATCH 3/4] Improve TypeScript type safety and code organization - Extract shouldSyncRelationship to separate module with unit tests - Add explicit return type interfaces for hooks - Create type-safe test factory for CoachingSession (removes 'as any') - Add const assertions to test mock parameters for immutability - Rename coaching_relationship.ts to coaching-relationship.ts for consistency - Update all imports to use hyphenated naming convention - Fix type signatures to allow null coaching relationship IDs - Add 9 comprehensive unit tests for shouldSyncRelationship helper These changes improve type safety, maintainability, and follow TypeScript best practices without changing functionality. --- .../coaching-session-page.test.tsx | 36 +- .../relationship-sync.test.ts | 50 ++ .../factories/coaching-session.factory.ts | 28 + docs/api-spec-todays-sessions-endpoint.md | 666 ++++++++++++++++++ docs/bug-analysis-link-popover-188.md | 401 +++++++++++ docs/prd-improved-coaching-notes-editor.md | 526 ++++++++++++++ docs/ui-mockups-toolbar-design.md | 604 ++++++++++++++++ src/app/coaching-sessions/[id]/page.tsx | 24 +- .../[id]/relationship-sync.ts | 36 + src/components/ui/members/member-card.tsx | 2 +- .../ui/members/member-container.tsx | 2 +- src/components/ui/members/member-list.tsx | 2 +- src/lib/api/coaching-relationships.ts | 2 +- .../use-auto-select-single-relationship.ts | 6 +- .../use-current-coaching-relationship.ts | 28 +- src/lib/hooks/use-current-coaching-session.ts | 24 +- src/lib/relationships/relationship-utils.ts | 2 +- src/lib/sessions/session-utils.ts | 2 +- src/lib/utils/user-roles.ts | 2 +- ...lationship.ts => coaching-relationship.ts} | 0 src/types/coaching-session.ts | 2 +- src/types/session-title.ts | 2 +- 22 files changed, 2390 insertions(+), 57 deletions(-) create mode 100644 __tests__/app/coaching-sessions/relationship-sync.test.ts create mode 100644 __tests__/factories/coaching-session.factory.ts create mode 100644 docs/api-spec-todays-sessions-endpoint.md create mode 100644 docs/bug-analysis-link-popover-188.md create mode 100644 docs/prd-improved-coaching-notes-editor.md create mode 100644 docs/ui-mockups-toolbar-design.md create mode 100644 src/app/coaching-sessions/[id]/relationship-sync.ts rename src/types/{coaching_relationship.ts => coaching-relationship.ts} (100%) diff --git a/__tests__/app/coaching-sessions/coaching-session-page.test.tsx b/__tests__/app/coaching-sessions/coaching-session-page.test.tsx index 0da523f0..ffd9514f 100644 --- a/__tests__/app/coaching-sessions/coaching-session-page.test.tsx +++ b/__tests__/app/coaching-sessions/coaching-session-page.test.tsx @@ -5,6 +5,7 @@ import CoachingSessionsPage from '@/app/coaching-sessions/[id]/page' import { TestProviders } from '@/test-utils/providers' import { useCurrentCoachingSession } from '@/lib/hooks/use-current-coaching-session' import { useCurrentCoachingRelationship } from '@/lib/hooks/use-current-coaching-relationship' +import { createMockCoachingSession } from '../../factories/coaching-session.factory' // Mock Next.js navigation hooks vi.mock('next/navigation', () => ({ @@ -70,11 +71,11 @@ describe('CoachingSessionsPage URL Parameter Persistence', () => { const mockRouter = { push: vi.fn(), replace: vi.fn(), - } + } as const const mockParams = { id: 'session-123' - } + } as const beforeEach(() => { vi.clearAllMocks() @@ -84,11 +85,10 @@ describe('CoachingSessionsPage URL Parameter Persistence', () => { // Set default mocks for relationship hooks vi.mocked(useCurrentCoachingSession).mockReturnValue({ currentCoachingSessionId: 'session-123', - currentCoachingSession: { + currentCoachingSession: createMockCoachingSession({ id: 'session-123', - title: 'Test Session', coaching_relationship_id: 'rel-123' - } as any, + }), isError: false, isLoading: false, refresh: vi.fn(), @@ -240,11 +240,11 @@ describe('CoachingSessionsPage - Relationship Auto-Sync', () => { const mockRouter = { push: vi.fn(), replace: vi.fn(), - } + } as const const mockParams = { id: 'session-123' - } + } as const beforeEach(() => { vi.clearAllMocks() @@ -267,10 +267,10 @@ describe('CoachingSessionsPage - Relationship Auto-Sync', () => { // Session has relationship ID, but store is empty (new tab scenario) vi.mocked(useCurrentCoachingSession).mockReturnValue({ currentCoachingSessionId: 'session-123', - currentCoachingSession: { + currentCoachingSession: createMockCoachingSession({ id: 'session-123', coaching_relationship_id: 'rel-123' - } as any, + }), isError: false, isLoading: false, refresh: vi.fn(), @@ -313,10 +313,10 @@ describe('CoachingSessionsPage - Relationship Auto-Sync', () => { // Session has relationship ID 'rel-456', but store has stale 'rel-123' vi.mocked(useCurrentCoachingSession).mockReturnValue({ currentCoachingSessionId: 'session-456', - currentCoachingSession: { + currentCoachingSession: createMockCoachingSession({ id: 'session-456', coaching_relationship_id: 'rel-456' // Different relationship - } as any, + }), isError: false, isLoading: false, refresh: vi.fn(), @@ -358,10 +358,10 @@ describe('CoachingSessionsPage - Relationship Auto-Sync', () => { // Session and store both have the same relationship ID vi.mocked(useCurrentCoachingSession).mockReturnValue({ currentCoachingSessionId: 'session-456', - currentCoachingSession: { + currentCoachingSession: createMockCoachingSession({ id: 'session-456', coaching_relationship_id: 'rel-123' // Same relationship - } as any, + }), isError: false, isLoading: false, refresh: vi.fn(), @@ -401,10 +401,10 @@ describe('CoachingSessionsPage - Relationship Auto-Sync', () => { // Session without relationship ID vi.mocked(useCurrentCoachingSession).mockReturnValue({ currentCoachingSessionId: 'session-123', - currentCoachingSession: { + currentCoachingSession: createMockCoachingSession({ id: 'session-123', - // No coaching_relationship_id - } as any, + coaching_relationship_id: undefined as any // No coaching_relationship_id + }), isError: false, isLoading: false, refresh: vi.fn(), @@ -446,10 +446,10 @@ describe('CoachingSessionsPage - Relationship Auto-Sync', () => { // But store has stale rel-123 from previous browsing vi.mocked(useCurrentCoachingSession).mockReturnValue({ currentCoachingSessionId: 'session-789', - currentCoachingSession: { + currentCoachingSession: createMockCoachingSession({ id: 'session-789', coaching_relationship_id: 'rel-789' - } as any, + }), isError: false, isLoading: false, refresh: vi.fn(), diff --git a/__tests__/app/coaching-sessions/relationship-sync.test.ts b/__tests__/app/coaching-sessions/relationship-sync.test.ts new file mode 100644 index 00000000..7e97e9e7 --- /dev/null +++ b/__tests__/app/coaching-sessions/relationship-sync.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from 'vitest' +import { shouldSyncRelationship } from '@/app/coaching-sessions/[id]/relationship-sync' + +describe('shouldSyncRelationship', () => { + describe('when session has no relationship ID', () => { + it('returns false with null store', () => { + expect(shouldSyncRelationship(undefined, null)).toBe(false) + }) + + it('returns false with populated store', () => { + expect(shouldSyncRelationship(undefined, 'rel-123')).toBe(false) + }) + }) + + describe('when store is empty', () => { + it('returns true (Issue #79: new tab scenario)', () => { + expect(shouldSyncRelationship('rel-123', null)).toBe(true) + }) + }) + + describe('when relationship IDs differ', () => { + it('returns true (Bug #228: switching between sessions)', () => { + expect(shouldSyncRelationship('rel-456', 'rel-123')).toBe(true) + }) + + it('returns true for any different ID', () => { + expect(shouldSyncRelationship('rel-999', 'rel-000')).toBe(true) + }) + }) + + describe('when relationship IDs match', () => { + it('returns false (optimization: no sync needed)', () => { + expect(shouldSyncRelationship('rel-123', 'rel-123')).toBe(false) + }) + + it('returns false for any matching ID', () => { + expect(shouldSyncRelationship('rel-xyz', 'rel-xyz')).toBe(false) + }) + }) + + describe('edge cases', () => { + it('handles empty string as session ID', () => { + expect(shouldSyncRelationship('', 'rel-123')).toBe(false) + }) + + it('handles empty string in both params', () => { + expect(shouldSyncRelationship('', '')).toBe(false) + }) + }) +}) diff --git a/__tests__/factories/coaching-session.factory.ts b/__tests__/factories/coaching-session.factory.ts new file mode 100644 index 00000000..86fae755 --- /dev/null +++ b/__tests__/factories/coaching-session.factory.ts @@ -0,0 +1,28 @@ +import type { CoachingSession } from '@/types/coaching-session' + +/** + * Creates a mock CoachingSession for testing purposes. + * + * @param overrides - Partial CoachingSession to override default values + * @returns A complete CoachingSession object with sensible defaults + * + * @example + * const session = createMockCoachingSession({ + * id: 'session-456', + * coaching_relationship_id: 'rel-456' + * }) + */ +export function createMockCoachingSession( + overrides?: Partial +): CoachingSession { + const now = new Date().toISOString() + + return { + id: 'session-123', + coaching_relationship_id: 'rel-123', + date: now, + created_at: now, + updated_at: now, + ...overrides, + } +} diff --git a/docs/api-spec-todays-sessions-endpoint.md b/docs/api-spec-todays-sessions-endpoint.md new file mode 100644 index 00000000..8a79a5c0 --- /dev/null +++ b/docs/api-spec-todays-sessions-endpoint.md @@ -0,0 +1,666 @@ +# API Specification: Today's Sessions Endpoint + +**Version:** 1.0 +**Date:** 2025-10-23 +**Author:** System Architecture Team +**Status:** Proposed + +## Overview + +This document specifies a new backend API endpoint designed to efficiently fetch all of a user's coaching sessions scheduled for "today" across all their coaching relationships, including associated overarching goals, relationship details, and organization information. + +### Problem Statement + +The current frontend implementation requires multiple API calls to display today's sessions: +1. Fetch user's organizations +2. For each organization, fetch coaching relationships +3. For each relationship, fetch sessions in date range +4. For each session, fetch overarching goal + +This results in an N+1 query problem and dozens of API round trips, causing: +- Performance degradation +- Complex client-side state management +- Infinite loop vulnerabilities from unstable array references +- Poor user experience with loading states + +### Solution + +A single, optimized backend endpoint that returns all necessary data in one request. + +--- + +## Endpoint Definition + +### HTTP Method and Path + +``` +GET /api/v1/users/{userId}/todays-sessions +``` + +### Path Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `userId` | UUID | Yes | The ID of the user requesting their sessions | + +### Query Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `timezone` | String | No | User's profile timezone or UTC | IANA timezone identifier (e.g., "America/Los_Angeles", "Europe/London") | +| `include_past` | Boolean | No | `false` | Whether to include sessions that have already ended today | + +### Request Headers + +``` +Authorization: Bearer {jwt_token} +Content-Type: application/json +``` + +### Authentication & Authorization + +- **Authentication:** Required. User must be authenticated via JWT. +- **Authorization:** User can only access their own sessions (`userId` in path must match authenticated user's ID). +- **Error Response:** `403 Forbidden` if user attempts to access another user's sessions. + +--- + +## Response Format + +### Success Response (200 OK) + +```json +{ + "user_id": "550e8400-e29b-41d4-a716-446655440000", + "date": "2025-10-23", + "timezone": "America/Los_Angeles", + "sessions": [ + { + "id": "650e8400-e29b-41d4-a716-446655440001", + "date": "2025-10-23T14:00:00Z", + "coaching_relationship_id": "750e8400-e29b-41d4-a716-446655440002", + "overarching_goal": { + "id": "850e8400-e29b-41d4-a716-446655440003", + "title": "Improve leadership communication skills", + "description": "Develop clearer communication strategies for team management" + }, + "relationship": { + "id": "750e8400-e29b-41d4-a716-446655440002", + "coach_id": "950e8400-e29b-41d4-a716-446655440004", + "coach_first_name": "Sarah", + "coach_last_name": "Johnson", + "coachee_id": "550e8400-e29b-41d4-a716-446655440000", + "coachee_first_name": "John", + "coachee_last_name": "Smith", + "organization_id": "a50e8400-e29b-41d4-a716-446655440005" + }, + "organization": { + "id": "a50e8400-e29b-41d4-a716-446655440005", + "name": "Refactor Group" + } + }, + { + "id": "650e8400-e29b-41d4-a716-446655440006", + "date": "2025-10-23T16:30:00Z", + "coaching_relationship_id": "750e8400-e29b-41d4-a716-446655440007", + "overarching_goal": null, + "relationship": { + "id": "750e8400-e29b-41d4-a716-446655440007", + "coach_id": "550e8400-e29b-41d4-a716-446655440000", + "coach_first_name": "John", + "coach_last_name": "Smith", + "coachee_id": "950e8400-e29b-41d4-a716-446655440008", + "coachee_first_name": "Emily", + "coachee_last_name": "Davis", + "organization_id": "a50e8400-e29b-41d4-a716-446655440009" + }, + "organization": { + "id": "a50e8400-e29b-41d4-a716-446655440009", + "name": "Tech Innovations Inc" + } + } + ], + "total_count": 2 +} +``` + +### Response Fields + +#### Root Level + +| Field | Type | Description | +|-------|------|-------------| +| `user_id` | UUID | The user ID for whom sessions were fetched | +| `date` | String (ISO 8601 Date) | The date for which sessions are returned (in user's timezone) | +| `timezone` | String | The timezone used to determine "today" | +| `sessions` | Array | Array of session objects (see below) | +| `total_count` | Integer | Total number of sessions returned | + +#### Session Object + +| Field | Type | Nullable | Description | +|-------|------|----------|-------------| +| `id` | UUID | No | Session unique identifier | +| `date` | String (ISO 8601 DateTime) | No | Session date/time in UTC | +| `coaching_relationship_id` | UUID | No | ID of the associated coaching relationship | +| `overarching_goal` | Object | Yes | Associated overarching goal (null if none) | +| `relationship` | Object | No | Coaching relationship details | +| `organization` | Object | No | Organization details | + +#### Overarching Goal Object (nullable) + +| Field | Type | Description | +|-------|------|-------------| +| `id` | UUID | Goal unique identifier | +| `title` | String | Goal title/summary | +| `description` | String | Detailed goal description | + +#### Relationship Object + +| Field | Type | Description | +|-------|------|-------------| +| `id` | UUID | Relationship unique identifier | +| `coach_id` | UUID | Coach user ID | +| `coach_first_name` | String | Coach first name | +| `coach_last_name` | String | Coach last name | +| `coachee_id` | UUID | Coachee user ID | +| `coachee_first_name` | String | Coachee first name | +| `coachee_last_name` | String | Coachee last name | +| `organization_id` | UUID | Organization ID | + +#### Organization Object + +| Field | Type | Description | +|-------|------|-------------| +| `id` | UUID | Organization unique identifier | +| `name` | String | Organization name | + +### Error Responses + +#### 401 Unauthorized +```json +{ + "error": "Unauthorized", + "message": "Authentication required" +} +``` + +#### 403 Forbidden +```json +{ + "error": "Forbidden", + "message": "You do not have permission to access sessions for this user" +} +``` + +#### 400 Bad Request +```json +{ + "error": "Bad Request", + "message": "Invalid timezone identifier", + "details": { + "field": "timezone", + "provided": "Invalid/Timezone", + "valid_example": "America/Los_Angeles" + } +} +``` + +#### 404 Not Found +```json +{ + "error": "Not Found", + "message": "User not found" +} +``` + +#### 500 Internal Server Error +```json +{ + "error": "Internal Server Error", + "message": "An unexpected error occurred" +} +``` + +--- + +## Business Logic + +### "Today" Definition + +The endpoint determines "today" based on the following logic: + +1. **Timezone Resolution:** + - Use `timezone` query parameter if provided + - Else, use user's profile timezone setting + - Else, default to UTC + +2. **Date Range Calculation:** + ``` + startOfDay = today at 00:00:00 in user's timezone, converted to UTC + endOfDay = today at 23:59:59.999 in user's timezone, converted to UTC + ``` + +3. **Session Filtering:** + - Include sessions where `session.date >= startOfDay AND session.date <= endOfDay` + - Apply timezone conversion to ensure accurate filtering + +### Relationship Inclusion + +Include sessions from all coaching relationships where: +- User is either coach OR coachee +- Relationship is active (not archived/deleted) +- Session falls within today's date range + +### Overarching Goal Association + +- Each session can have 0 or 1 associated overarching goal +- If no goal exists, `overarching_goal` field is `null` +- If multiple goals exist (edge case), return the most recently created goal + +### Sorting + +Sessions are sorted chronologically by `date` (ascending) - earliest session first. + +--- + +## Implementation Considerations + +### Database Query Optimization + +Recommended SQL approach (pseudo-code): + +```sql +-- Single query with LEFT JOINs to avoid N+1 queries +SELECT + cs.id, + cs.date, + cs.coaching_relationship_id, + og.id as goal_id, + og.title as goal_title, + og.description as goal_description, + cr.id as relationship_id, + cr.coach_id, + coach.first_name as coach_first_name, + coach.last_name as coach_last_name, + cr.coachee_id, + coachee.first_name as coachee_first_name, + coachee.last_name as coachee_last_name, + cr.organization_id, + org.name as organization_name +FROM coaching_sessions cs +INNER JOIN coaching_relationships cr + ON cs.coaching_relationship_id = cr.id +INNER JOIN users coach ON cr.coach_id = coach.id +INNER JOIN users coachee ON cr.coachee_id = coachee.id +INNER JOIN organizations org ON cr.organization_id = org.id +LEFT JOIN overarching_goals og + ON cs.id = og.coaching_session_id +WHERE + (cr.coach_id = :userId OR cr.coachee_id = :userId) + AND cs.date >= :startOfDay + AND cs.date <= :endOfDay + AND cr.deleted_at IS NULL +ORDER BY cs.date ASC +``` + +### Caching Strategy + +Consider caching this endpoint response with: +- **Cache Key:** `user:{userId}:todays-sessions:{date}:{timezone}` +- **TTL:** 5-15 minutes (sessions don't change frequently) +- **Invalidation:** Clear cache when: + - User creates/updates/deletes a session + - User creates/updates/deletes an overarching goal + - Date changes (midnight in user's timezone) + +### Performance Targets + +- **Response Time:** < 200ms (p95) +- **Database Query:** Single query with JOINs (no N+1) +- **Typical Payload Size:** < 10KB (assuming ~5 sessions per user per day) + +### Index Recommendations + +```sql +-- Composite index for efficient date range + user filtering +CREATE INDEX idx_sessions_date_relationship + ON coaching_sessions(date, coaching_relationship_id); + +-- Index for relationship user lookups +CREATE INDEX idx_relationships_users + ON coaching_relationships(coach_id, coachee_id) + WHERE deleted_at IS NULL; + +-- Index for goal lookups +CREATE INDEX idx_goals_session + ON overarching_goals(coaching_session_id); +``` + +--- + +## Example Usage + +### Request Example 1: Basic Request + +```bash +curl -X GET \ + 'https://api.refactorcoaching.com/api/v1/users/550e8400-e29b-41d4-a716-446655440000/todays-sessions' \ + -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \ + -H 'Content-Type: application/json' +``` + +### Request Example 2: With Timezone + +```bash +curl -X GET \ + 'https://api.refactorcoaching.com/api/v1/users/550e8400-e29b-41d4-a716-446655440000/todays-sessions?timezone=America/New_York' \ + -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \ + -H 'Content-Type: application/json' +``` + +### Request Example 3: Include Past Sessions + +```bash +curl -X GET \ + 'https://api.refactorcoaching.com/api/v1/users/550e8400-e29b-41d4-a716-446655440000/todays-sessions?include_past=true' \ + -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \ + -H 'Content-Type: application/json' +``` + +--- + +## Frontend Integration + +### Proposed Hook Implementation + +Once this endpoint is available, the frontend can be simplified to: + +```typescript +// src/lib/api/todays-sessions.ts +export interface TodaysSessionsResponse { + user_id: string; + date: string; + timezone: string; + sessions: Array<{ + id: string; + date: string; + coaching_relationship_id: string; + overarching_goal: { + id: string; + title: string; + description: string; + } | null; + relationship: { + id: string; + coach_id: string; + coach_first_name: string; + coach_last_name: string; + coachee_id: string; + coachee_first_name: string; + coachee_last_name: string; + organization_id: string; + }; + organization: { + id: string; + name: string; + }; + }>; + total_count: number; +} + +export const TodaysSessionsApi = { + get: async (userId: string, timezone?: string): Promise => { + const params = new URLSearchParams(); + if (timezone) params.append('timezone', timezone); + + const response = await fetch( + `${API_BASE_URL}/users/${userId}/todays-sessions?${params}`, + { + headers: { + 'Authorization': `Bearer ${getToken()}`, + 'Content-Type': 'application/json', + }, + } + ); + + if (!response.ok) { + throw new Error(`Failed to fetch today's sessions: ${response.statusText}`); + } + + return response.json(); + }, +}; + +// Hook +export function useTodaysSessions() { + const { userSession } = useAuthStore(); + const userId = userSession?.id; + const timezone = userSession?.timezone || getBrowserTimezone(); + + const { data, error, isLoading, mutate } = useSWR( + userId ? ['todays-sessions', userId, timezone] : null, + () => TodaysSessionsApi.get(userId!, timezone), + { + revalidateOnFocus: false, + revalidateOnReconnect: true, + dedupingInterval: 300000, // 5 minutes + } + ); + + return { + sessions: data?.sessions || [], + isLoading, + error, + refresh: mutate, + }; +} +``` + +### Benefits of New Hook + +1. **Single API Call:** Replaces dozens of calls with one +2. **No Client-Side Joins:** All data pre-joined by backend +3. **No N+1 Queries:** Goals included in initial response +4. **Stable References:** Single array from single source +5. **Simpler State:** No complex Maps/Sets for tracking loaded data +6. **Timezone Accuracy:** Backend handles timezone logic correctly + +--- + +## Migration Path + +### Phase 1: Backend Implementation +1. Implement new endpoint with comprehensive tests +2. Deploy to staging environment +3. Validate performance and correctness + +### Phase 2: Frontend Preparation +1. Create new `useTodaysSessions` hook using new endpoint +2. Keep old implementation as fallback +3. Feature flag to toggle between old/new implementation + +### Phase 3: Gradual Rollout +1. Enable for internal users (beta testing) +2. Monitor performance metrics and error rates +3. Gradually increase rollout percentage +4. Full rollout once validated + +### Phase 4: Cleanup +1. Remove old implementation after 30 days +2. Delete obsolete hooks and utilities +3. Update documentation + +--- + +## Testing Requirements + +### Unit Tests + +- Timezone edge cases (date boundaries, DST transitions) +- Null handling for optional fields (overarching goals) +- User authorization (coach vs coachee access) +- Date range filtering accuracy + +### Integration Tests + +- Multiple organizations and relationships +- Sessions spanning midnight in user's timezone +- Cache invalidation scenarios +- Performance under load (100+ concurrent requests) + +### Test Cases + +1. **User with no sessions today** → Returns empty array +2. **User in PST timezone at 11:30 PM PST** → Only includes sessions before midnight PST +3. **User as both coach and coachee** → Includes sessions from both roles +4. **Session with no overarching goal** → `overarching_goal` is null +5. **Invalid timezone parameter** → Returns 400 Bad Request +6. **Unauthorized access** → Returns 403 Forbidden +7. **User from different timezone** → Correctly filters based on their timezone + +--- + +## Security Considerations + +1. **Authorization:** Enforce user can only access their own sessions +2. **Data Exposure:** Only expose necessary fields (no sensitive internal data) +3. **Rate Limiting:** Apply standard API rate limits (suggested: 100 requests/minute per user) +4. **Input Validation:** Validate timezone parameter against IANA timezone database +5. **SQL Injection:** Use parameterized queries for all database access + +--- + +## Monitoring & Observability + +### Metrics to Track + +- Request latency (p50, p95, p99) +- Error rate by status code +- Cache hit/miss ratio +- Database query execution time +- Number of sessions returned per request + +### Alerts + +- Error rate > 1% for 5 minutes → Page on-call engineer +- p95 latency > 500ms for 5 minutes → Warning notification +- Database query time > 1 second → Warning notification + +### Logging + +Log the following for each request: +- User ID +- Timezone used +- Number of sessions returned +- Query execution time +- Any errors encountered + +--- + +## Future Enhancements + +Potential future improvements to consider: + +1. **Date Range Parameter:** Allow fetching sessions for arbitrary date ranges + ``` + GET /users/{userId}/sessions?from_date={date}&to_date={date} + ``` + +2. **Pagination:** For users with many sessions per day + ``` + GET /users/{userId}/todays-sessions?page=1&per_page=10 + ``` + +3. **Filtering:** Filter by organization, relationship, or goal status + ``` + GET /users/{userId}/todays-sessions?organization_id={id} + ``` + +4. **Sorting Options:** Allow custom sorting (by date, organization, etc.) + ``` + GET /users/{userId}/todays-sessions?sort_by=organization&sort_order=asc + ``` + +5. **Field Selection:** Allow clients to request specific fields only + ``` + GET /users/{userId}/todays-sessions?fields=id,date,overarching_goal.title + ``` + +--- + +## Questions for Backend Team + +1. **Current Session Model:** Does the current `coaching_sessions` table include soft deletes? Should deleted sessions be excluded? + +2. **Overarching Goal Cardinality:** Is it always 1:1 with sessions, or can multiple goals exist per session? + +3. **Performance Baseline:** What is the current average response time for the existing session list endpoint? + +4. **Caching Infrastructure:** Is Redis/Memcached available for response caching? + +5. **Rate Limiting:** What are the current API rate limits, and should this endpoint have special limits? + +6. **Database Load:** What is the expected query load (requests per second) during peak usage? + +--- + +## Appendix: Alternative Approaches Considered + +### Alternative 1: Batch Endpoint for Goals Only + +**Approach:** Keep existing session fetching, add batch endpoint for goals +``` +GET /overarching-goals/batch?session_ids=id1,id2,id3 +``` + +**Pros:** +- Smaller change to existing architecture +- Easier to implement incrementally + +**Cons:** +- Still requires multiple API calls +- Doesn't solve the multi-relationship infinite loop issue +- Client still needs complex state management + +**Verdict:** Rejected in favor of single comprehensive endpoint + +### Alternative 2: GraphQL + +**Approach:** Implement GraphQL API allowing clients to request exact data needed +```graphql +query { + user(id: "123") { + todaysSessions { + id + date + overarchingGoal { title } + relationship { + coach { firstName lastName } + organization { name } + } + } + } +} +``` + +**Pros:** +- Extremely flexible +- Clients control data shape +- Industry standard for complex data fetching + +**Cons:** +- Requires new GraphQL infrastructure +- Higher implementation complexity +- Learning curve for team + +**Verdict:** Good long-term strategy, but REST endpoint is faster to implement + +--- + +## Revision History + +| Version | Date | Author | Changes | +|---------|------|--------|---------| +| 1.0 | 2025-10-23 | System Architecture Team | Initial specification | + diff --git a/docs/bug-analysis-link-popover-188.md b/docs/bug-analysis-link-popover-188.md new file mode 100644 index 00000000..6daac49d --- /dev/null +++ b/docs/bug-analysis-link-popover-188.md @@ -0,0 +1,401 @@ +# Bug Analysis: Link Popover Issue #188 + +**Issue**: [GitHub Issue #188](https://github.com/refactor-group/refactor-platform-fe/issues/188) +**Version**: 1.0.0-beta2 +**Priority**: High (UX Impact) + +## Problem Summary + +The URL link popover exhibits three distinct issues: + +1. **Duplicate Popover Display**: Popover sometimes appears twice - once in the correct location and once in the upper-left corner +2. **Poor Link Selection UX**: Clicking existing links doesn't show popover above/below the link as expected +3. **Navigation Difficulties**: Cursor behavior doesn't match standard rich text editors (popover should only appear when cursor is directly on link text) + +## Root Cause Analysis + +### Issue 1: Duplicate Popover Rendering + +**Root Cause**: Portal positioning calculation issue in `popover.tsx:300-344` + +The `PopoverContent` component has complex logic for determining the portal container and calculating positions: + +```typescript +// popover.tsx:301-323 +const portalContainer = React.useMemo(() => { + if (!portal) return null; + if (container) return container; + + // Find closest positioned ancestor (not static) + const referenceElement = context.refs.reference.current; + if (!referenceElement) return document.body; + + if ('parentElement' in referenceElement) { + let element = referenceElement.parentElement; + while (element) { + const position = window.getComputedStyle(element).position; + if (position !== 'static') { + return element; + } + element = element.parentElement; + } + } + + return document.body; +}, [portal, container, context.refs.reference]); +``` + +**Problem**: +- The algorithm searches for a positioned ancestor, but when it finds one, it calculates relative positions (lines 326-344) +- However, FloatingUI already handles positioning through its middleware +- This creates a conflict where FloatingUI positions the element, then the component adjusts it again relative to the container +- Result: One popover at FloatingUI's calculated position, another at the adjusted position + +**Evidence**: +- `containerRef` is passed from `SimpleToolbar` (line 85 of simple-toolbar.tsx) to `LinkPopover` (line 209 of coaching-notes.tsx) +- The ref points to `.coaching-notes-editor` div +- The popover component tries to be "smart" about positioning within this container +- But the dual positioning logic creates the duplicate appearance + +### Issue 2: Popover Position When Editing Links + +**Root Cause**: Single positioning strategy in `link-popover.tsx:243-353` + +The `LinkPopover` component uses a fixed positioning strategy: + +```typescript +// link-popover.tsx:334-352 + + + + + + + + + +``` + +**Problem**: +- Popover is hardcoded to `side="bottom"` and `align="start"` +- This works for the toolbar button trigger +- But doesn't adapt when the user is editing an existing link in the text +- When a link is active in the editor, the popover should position relative to the link text, not the toolbar button + +**Missing Logic**: No detection of whether user is: +1. Creating a new link (position under toolbar button ✓) +2. Editing an existing link (should position near the link text ✗) + +### Issue 3: Auto-Open Behavior + +**Root Cause**: Aggressive auto-open logic in `link-popover.tsx:45-77` + +```typescript +// link-popover.tsx:49-59 +React.useEffect(() => { + if (!editor) return; + const { href } = editor.getAttributes("link"); + + if (editor.isActive("link") && url === null) { + setUrl(href || ""); + onLinkActive?.(); // Triggers popover open + } +}, [editor, onLinkActive, url]); + +// link-popover.tsx:61-77 +React.useEffect(() => { + if (!editor) return; + + const updateLinkState = () => { + const { href } = editor.getAttributes("link"); + setUrl(href || ""); + + if (editor.isActive("link") && url !== null) { + onLinkActive?.(); // Triggers popover open + } + }; + + editor.on("selectionUpdate", updateLinkState); + return () => editor.off("selectionUpdate", updateLinkState); +}, [editor, onLinkActive, url]); +``` + +**Problem**: +- Popover opens on **every** `selectionUpdate` event when cursor is on a link +- This happens even when user is just navigating through the document +- Standard rich text editor behavior: Only show popover when user **clicks** the link with intent to edit + +**Expected Behavior**: +- User navigating with arrow keys across a link → No popover +- User clicking on a link → Show popover for editing +- Alternative: Show link preview tooltip (non-interactive) during navigation, full popover only on explicit interaction + +## Impact Assessment + +| Issue | Severity | User Impact | Frequency | +|-------|----------|-------------|-----------| +| Duplicate Popover | High | Confusing UI, looks broken | Occasional | +| Wrong Position on Edit | Medium | Annoying but usable | Every link edit | +| Aggressive Auto-Open | High | Prevents navigation | Every cursor movement on links | + +## Proposed Solution + +### Phase 1: Fix Duplicate Popover (Critical) + +**Change**: Simplify portal positioning logic in `popover.tsx` + +**Current approach**: Find positioned ancestor + calculate relative position +**New approach**: Trust FloatingUI's positioning, only use container for boundary clipping + +```typescript +// popover.tsx - Simplified container logic +const portalContainer = React.useMemo(() => { + if (!portal) return null; + // Always use document.body for portal + // Use boundary prop for constraining, not container positioning + return document.body; +}, [portal]); + +// Remove the complex position adjustment (lines 326-344) +const enhancedStyle = React.useMemo(() => { + return { + position: context.strategy, + top: context.y ?? 0, + left: context.x ?? 0, + ...style, + }; +}, [context, style]); +``` + +**Rationale**: +- FloatingUI is designed to handle positioning with boundaries +- The `boundary` prop in `link-popover.tsx:318-322` should constrain the popover +- No need for custom position recalculation + +### Phase 2: Adaptive Popover Positioning (High Priority) + +**Change**: Detect context and position appropriately in `link-popover.tsx` + +```typescript +// New utility function +function getLinkPositionInEditor(editor: Editor): { x: number; y: number } | null { + if (!editor.isActive("link")) return null; + + const { state } = editor; + const { from, to } = state.selection; + const start = editor.view.coordsAtPos(from); + const end = editor.view.coordsAtPos(to); + + return { + x: start.left, + y: start.bottom + }; +} + +// In LinkPopover component +const linkPosition = React.useMemo(() => { + if (!editor || !editor.isActive("link")) return null; + return getLinkPositionInEditor(editor); +}, [editor, editor?.state.selection]); + +const popoverPositioning = React.useMemo(() => { + if (linkPosition) { + // Editing existing link - position near the link + return { + side: "bottom" as const, + align: "start" as const, + sideOffset: 4, + }; + } else { + // Creating new link - position under toolbar button + return { + side: "bottom" as const, + align: "start" as const, + sideOffset: 8, + }; + } +}, [linkPosition]); + +// Apply to PopoverContent + +``` + +**Alternative Approach**: Create two separate popovers: +- One triggered by toolbar button (current implementation) +- One triggered by link click in editor (new floating popover near link) + +This is cleaner separation of concerns and matches standard rich text editor patterns. + +### Phase 3: Improve Auto-Open Behavior (High Priority) + +**Change**: Only auto-open on explicit user action, not cursor movement + +```typescript +// Option A: Remove auto-open entirely +// Set autoOpenOnLinkActive={false} in simple-toolbar.tsx:85 + + +// Option B: Smarter detection - only open on click, not selection change +const useLinkHandler = (props: LinkHandlerProps) => { + const { editor, onSetLink, onLinkActive } = props; + const [url, setUrl] = React.useState(null); + const lastInteractionType = React.useRef<'click' | 'selection' | null>(null); + + React.useEffect(() => { + if (!editor) return; + + const handleTransaction = ({ transaction }: { transaction: any }) => { + // Detect if this was a click vs keyboard navigation + const wasMouse = transaction.getMeta('pointer'); + if (wasMouse && editor.isActive("link")) { + const { href } = editor.getAttributes("link"); + setUrl(href || ""); + onLinkActive?.(); + } + }; + + editor.on("transaction", handleTransaction); + return () => editor.off("transaction", handleTransaction); + }, [editor, onLinkActive]); + + // Remove the aggressive selectionUpdate listener (lines 61-77) + + // ... rest of implementation +}; + +// Option C: Add click handler to links in the editor +// In coaching-notes.tsx, enhance createLinkClickHandler: +const createLinkClickHandler = (onLinkClick?: (href: string) => void) => + (_view: unknown, event: Event) => { + const target = event.target as HTMLElement; + const mouseEvent = event as MouseEvent; + + if (isClickOnLink(target, mouseEvent)) { + if (mouseEvent.shiftKey) { + // Existing behavior - open in new tab + event.preventDefault(); + openLinkInNewTab(target); + return true; + } else { + // New behavior - trigger link edit popover + event.preventDefault(); + const href = getLinkHref(target); + onLinkClick?.(href); + return true; + } + } + return false; + }; +``` + +**Recommendation**: Combination of Option A + Option C +- Disable aggressive auto-open on selection changes +- Add explicit click handler for links in editor content +- Keep toolbar button as primary entry point for creating new links + +## Implementation Plan + +### Step 1: Fix Duplicate Popover (30 min) +**File**: `src/components/ui/tiptap-ui-primitive/popover/popover.tsx` + +- Simplify `portalContainer` logic (always use document.body) +- Remove complex position recalculation in `enhancedStyle` +- Test: Verify popover appears only once in correct location + +### Step 2: Disable Aggressive Auto-Open (15 min) +**File**: `src/components/ui/coaching-sessions/coaching-notes/simple-toolbar.tsx` + +- Add `autoOpenOnLinkActive={false}` prop to `LinkPopover` +- Test: Verify cursor can move across links without opening popover + +### Step 3: Add Click-to-Edit Behavior (45 min) +**Files**: +- `src/components/ui/coaching-sessions/coaching-notes.tsx` +- `src/components/ui/tiptap-ui/link-popover/link-popover.tsx` + +- Enhance `createLinkClickHandler` to detect normal clicks on links +- Add state management for "edit mode" in LinkPopover +- Position popover near clicked link (using TipTap's `coordsAtPos`) +- Test: Click on link → popover appears near link, not toolbar + +### Step 4: Improve Positioning Strategy (1 hour) +**File**: `src/components/ui/tiptap-ui/link-popover/link-popover.tsx` + +- Detect if editing existing link vs creating new link +- Use different positioning strategy for each case +- Consider implementing separate popovers for better separation +- Test: Both creation and editing workflows feel natural + +### Step 5: Polish & Edge Cases (30 min) +- Test rapid clicking, keyboard navigation +- Test on different screen sizes +- Test with floating toolbar visible/hidden +- Ensure accessibility (keyboard-only workflow) + +**Total Estimate**: 3 hours + +## Testing Checklist + +- [ ] Popover appears only once (no duplicate) +- [ ] Creating new link: Popover under toolbar button +- [ ] Editing existing link: Popover near link text +- [ ] Cursor navigation across links: No popover +- [ ] Click on link: Popover opens for editing +- [ ] Shift+click on link: Opens in new tab (existing) +- [ ] Keyboard-only workflow: Can create and edit links +- [ ] Floating toolbar: Link popover works when toolbar is floating +- [ ] Boundary constraints: Popover stays within editor container +- [ ] Rapid interactions: No visual glitches + +## Alternative Approaches Considered + +### Approach 1: Use TipTap's Built-in Link Extension +**Pros**: Battle-tested, maintained by TipTap team +**Cons**: Less customization, may not match design system +**Decision**: Rejected - Current implementation is nearly there, just needs fixes + +### Approach 2: Implement Bubble Menu for Links +**Pros**: Standard pattern in rich text editors, appears directly near content +**Cons**: Requires refactoring, loses toolbar button UX +**Decision**: Consider for v2 - Good long-term direction + +### Approach 3: Separate Toolbar Button and Inline Editor +**Pros**: Clear separation, easier to maintain +**Cons**: More components, more state management +**Decision**: Recommend for Step 4 if time permits + +## References + +- [TipTap Link Extension Docs](https://tiptap.dev/api/marks/link) +- [TipTap Bubble Menu](https://tiptap.dev/api/extensions/bubble-menu) +- [FloatingUI Positioning](https://floating-ui.com/docs/tutorial) +- [React Component Patterns for Rich Text](https://github.com/ianstormtaylor/slate/blob/main/site/examples/hovering-toolbar.tsx) + +## Success Criteria + +1. **No duplicate popovers**: Single popover instance, always correctly positioned +2. **Intuitive navigation**: Users can move cursor across links without popover appearing +3. **Clear editing workflow**: + - Toolbar button → Popover under button + - Click on link → Popover near link +4. **Performance**: No lag or visual glitches during interaction +5. **Accessibility**: Full keyboard navigation support maintained + +--- + +**Analysis Date**: 2025-10-15 +**Analyzed By**: Claude Code +**Status**: Ready for Implementation diff --git a/docs/prd-improved-coaching-notes-editor.md b/docs/prd-improved-coaching-notes-editor.md new file mode 100644 index 00000000..1513b497 --- /dev/null +++ b/docs/prd-improved-coaching-notes-editor.md @@ -0,0 +1,526 @@ +# Product Requirements Document +## Improved Coaching Notes TipTap Editor + +**Version:** 1.0 +**Date:** January 2025 +**Status:** Draft for Review +**Author:** Engineering Team + +--- + +## 1. Executive Summary + +### 1.1 Purpose +This PRD outlines the requirements for enhancing the TipTap-based rich text editor used for coaching notes within the Refactor Coaching Platform. The improvements focus on user experience, collaboration features, and professional formatting capabilities to better serve the needs of coaches and coachees during their sessions. + +### 1.2 Current State +The existing editor provides basic rich text editing with real-time collaboration via Y.js/Hocuspocus. While functional, it lacks advanced features that would enhance productivity and note organization during coaching sessions. + +### 1.3 Vision +Transform the coaching notes editor into a best-in-class collaborative writing tool that rivals modern document editors while maintaining focus on coaching-specific workflows and content structures. + +--- + +## 2. Problem Statement + +### 2.1 User Pain Points +1. **Limited Formatting Options**: Current toolbar lacks essential formatting tools (tables, task lists, quotes) +2. **No Template System**: Users start from blank slate each session +3. **Poor Mobile Experience**: Toolbar not optimized for touch interfaces +4. **Limited Keyboard Shortcuts**: Only basic shortcuts supported +5. **No AI Assistance**: Missing smart features like auto-summarization or action item extraction +6. **Weak Search/Navigation**: No find/replace or document outline +7. **Limited Export Options**: Cannot export notes in different formats + +### 2.2 Business Impact +- Reduced session productivity due to manual formatting +- Inconsistent note structure across sessions +- Difficulty in extracting actionable insights +- Poor accessibility for users with disabilities + +--- + +## 3. Goals & Success Metrics + +### 3.1 Primary Goals +1. **Enhance Productivity**: Reduce time spent on formatting by 40% +2. **Improve Collaboration**: Increase real-time collaboration usage by 60% +3. **Standardize Structure**: 80% of sessions use templates +4. **Mobile Optimization**: Achieve 90% feature parity on mobile + +### 3.2 Success Metrics +| Metric | Current | Target | Timeline | +|--------|---------|--------|----------| +| Editor Load Time | 2.5s | <1s | Q1 2025 | +| User Satisfaction (NPS) | 65 | 85 | Q2 2025 | +| Mobile Usage | 15% | 35% | Q2 2025 | +| Template Adoption | 0% | 80% | Q1 2025 | +| Accessibility Score (WCAG) | AA | AAA | Q1 2025 | + +--- + +## 4. User Personas & Use Cases + +### 4.1 Primary Personas + +#### Coach (Sarah) +- **Needs**: Quick note-taking, action item tracking, session summaries +- **Pain Points**: Repetitive formatting, manual template creation +- **Goals**: Focus on coaching, not documentation + +#### Coachee (Alex) +- **Needs**: Clear action items, searchable notes, progress tracking +- **Pain Points**: Finding past discussions, tracking commitments +- **Goals**: Clear understanding of next steps + +#### Manager (David) +- **Needs**: Progress reports, aggregate insights, team patterns +- **Pain Points**: Manual report generation, inconsistent formats +- **Goals**: Data-driven coaching outcomes + +### 4.2 Core Use Cases + +1. **Session Preparation** + - Load previous session notes + - Apply session template + - Review action items + +2. **During Session** + - Real-time collaborative editing + - Quick formatting via shortcuts + - Action item creation + - Link resources + +3. **Post-Session** + - Generate summary + - Export notes + - Create follow-up tasks + - Share with stakeholders + +--- + +## 5. Feature Requirements + +### 5.1 Enhanced Toolbar & Formatting + +#### Must Have (P0) +- [ ] **Extended Formatting** + - Tables with row/column management + - Block quotes for emphasis + - Task lists with checkboxes + - Inline code formatting + - Horizontal rules + - Text alignment (left, center, right, justify) + +- [ ] **Improved Link Management** + - Link preview on hover + - Auto-link detection + - Internal document linking (#headings) + - Link validation indicator + +- [ ] **Enhanced Lists** + - Nested list support (3+ levels) + - List style options (bullets, numbers, letters) + - Indent/outdent controls + - Convert between list types + +#### Should Have (P1) +- [ ] **Advanced Formatting** + - Callout/alert boxes (info, warning, success) + - Collapsible sections + - Footnotes + - Subscript/superscript + - Text color/background color + +- [ ] **Media Support** + - Image upload/embed + - Video embed (YouTube, Loom) + - File attachments + - Drawing/diagram tool integration + +#### Nice to Have (P2) +- [ ] Math equation support (LaTeX) +- [ ] Mermaid diagram support +- [ ] Code syntax for 20+ languages +- [ ] Custom emoji picker + +### 5.2 Template System + +#### Must Have (P0) +- [ ] **Pre-built Templates** + - Initial Session Template + - Regular Check-in Template + - Goal Review Template + - Retrospective Template + - Action Planning Template + +- [ ] **Template Management** + - Create custom templates + - Save as template from existing notes + - Template gallery/library + - Organization-level templates + - Personal templates + +- [ ] **Smart Placeholders** + - Date/time stamps + - Participant names + - Previous action items + - Goals reference + +#### Should Have (P1) +- [ ] Template versioning +- [ ] Template sharing between coaches +- [ ] Conditional template sections +- [ ] Template analytics (usage tracking) + +### 5.3 Collaboration Enhancements + +#### Must Have (P0) +- [ ] **Presence Indicators** + - Active user avatars + - Cursor colors per user + - User typing indicators + - Selection highlighting + +- [ ] **Comments & Annotations** + - Inline comments + - Threaded discussions + - Comment resolution + - @mentions with notifications + +#### Should Have (P1) +- [ ] **Version History** + - Auto-save with timestamps + - Version comparison/diff view + - Restore previous versions + - Change attribution + +- [ ] **Collaboration Controls** + - Read-only mode + - Suggestion mode + - Section locking + - Permission levels + +### 5.4 AI-Powered Features + +#### Must Have (P0) +- [ ] **Smart Summarization** + - Auto-generate session summary + - Key points extraction + - Action items detection + - Decision highlights + +#### Should Have (P1) +- [ ] **Content Assistance** + - Grammar/spell check + - Tone suggestions + - Sentence completion + - Professional rephrasing + +- [ ] **Insights Generation** + - Pattern recognition across sessions + - Progress tracking + - Goal alignment analysis + - Coaching effectiveness metrics + +#### Nice to Have (P2) +- [ ] Meeting transcription integration +- [ ] Sentiment analysis +- [ ] Auto-tagging/categorization +- [ ] Predictive text based on context + +### 5.5 Navigation & Search + +#### Must Have (P0) +- [ ] **Document Outline** + - Auto-generated TOC from headings + - Click to navigate + - Collapsible outline panel + - Current position indicator + +- [ ] **Find & Replace** + - Case-sensitive option + - Whole word matching + - Regular expression support + - Replace all functionality + +#### Should Have (P1) +- [ ] **Cross-Session Search** + - Search across all notes + - Filter by date/participant + - Search in comments + - Saved searches + +- [ ] **Quick Navigation** + - Go to line/section + - Bookmark positions + - Recent edits jump + - Split view mode + +### 5.6 Mobile Optimization + +#### Must Have (P0) +- [ ] **Responsive Toolbar** + - Collapsible toolbar + - Touch-optimized buttons + - Swipe gestures for formatting + - Context menu on selection + +- [ ] **Mobile-First Features** + - Voice-to-text input + - Simplified formatting palette + - Thumb-friendly controls + - Offline mode with sync + +#### Should Have (P1) +- [ ] Handwriting recognition +- [ ] Photo capture for whiteboard +- [ ] Mobile-specific templates +- [ ] Gesture-based undo/redo + +### 5.7 Accessibility & Internationalization + +#### Must Have (P0) +- [ ] **WCAG AAA Compliance** + - Full keyboard navigation + - Screen reader support + - High contrast mode + - Focus indicators + - ARIA labels + +- [ ] **Internationalization** + - RTL language support + - Unicode support + - Locale-specific formatting + - Translation-ready UI + +#### Should Have (P1) +- [ ] Voice commands +- [ ] Customizable shortcuts +- [ ] Dyslexia-friendly fonts +- [ ] Reading mode + +### 5.8 Import/Export + +#### Must Have (P0) +- [ ] **Export Formats** + - PDF with formatting + - Markdown + - Plain text + - HTML + - DOCX + +- [ ] **Import Support** + - Markdown files + - Plain text + - DOCX (basic) + - Copy/paste with formatting + +#### Should Have (P1) +- [ ] Notion export +- [ ] Google Docs sync +- [ ] Email integration +- [ ] Print preview/settings + +--- + +## 6. Technical Architecture + +### 6.1 Component Structure +```typescript +CoachingNotesEditor/ +├── EditorCore/ +│ ├── TipTapProvider +│ ├── CollaborationProvider +│ └── EditorContent +├── Toolbar/ +│ ├── FormattingTools/ +│ ├── InsertMenu/ +│ ├── AITools/ +│ └── ViewControls/ +├── Sidebar/ +│ ├── DocumentOutline/ +│ ├── Comments/ +│ ├── VersionHistory/ +│ └── Templates/ +├── StatusBar/ +│ ├── WordCount +│ ├── SaveStatus +│ └── CollaborationStatus +└── Dialogs/ + ├── LinkDialog/ + ├── ImageDialog/ + ├── TableDialog/ + └── TemplateDialog/ +``` + +### 6.2 Extension Architecture +```typescript +// Core Extensions (existing) +- Document, Paragraph, Text, Bold, Italic, etc. + +// New Extensions +- TaskList & TaskItem +- Table, TableRow, TableCell, TableHeader +- Mention +- Comment +- Template +- SmartSummary +- DocumentOutline +- FindReplace +``` + +### 6.3 Performance Requirements +- Initial render: <500ms +- Typing latency: <50ms +- Collaboration sync: <200ms +- Auto-save interval: 5 seconds +- Bundle size increase: <100KB gzipped + +### 6.4 Data Model Updates +```typescript +interface CoachingNote { + id: string; + sessionId: string; + content: JSONContent; // TipTap format + template?: TemplateReference; + metadata: { + wordCount: number; + lastEditedBy: string; + lastEditedAt: Date; + version: number; + }; + comments: Comment[]; + actionItems: ActionItem[]; + summary?: AISummary; +} +``` + +--- + +## 7. Design Specifications + +### 7.1 Visual Design Principles +- **Clean & Focused**: Minimal UI that doesn't distract from content +- **Contextual Controls**: Show relevant tools based on selection +- **Consistent Spacing**: 8px grid system +- **Accessible Colors**: WCAG AAA contrast ratios + +### 7.2 Responsive Breakpoints +- Mobile: 320px - 768px +- Tablet: 768px - 1024px +- Desktop: 1024px+ + +### 7.3 Interactive States +- Default, Hover, Active, Focus, Disabled +- Loading states for async operations +- Error states with recovery actions + +--- + +## 8. Implementation Phases + +### Phase 1: Foundation (Week 1-3) +- Enhanced toolbar with new formatting options +- Template system implementation +- Mobile-responsive toolbar +- Basic keyboard shortcuts + +### Phase 2: Collaboration (Week 4-5) +- Comments system +- Enhanced presence indicators +- Version history +- Mention functionality + +### Phase 3: Intelligence (Week 6-7) +- AI summarization +- Smart action item extraction +- Content suggestions +- Pattern recognition + +### Phase 4: Polish (Week 8) +- Performance optimization +- Accessibility audit & fixes +- Cross-browser testing +- Documentation + +--- + +## 9. Testing Strategy + +### 9.1 Test Coverage Requirements +- Unit tests: 90% coverage +- Integration tests: Key workflows +- E2E tests: Critical paths +- Performance tests: Load time, typing latency +- Accessibility tests: WCAG AAA compliance + +### 9.2 Browser Support +- Chrome 90+ +- Firefox 88+ +- Safari 14+ +- Edge 90+ +- Mobile Safari (iOS 14+) +- Chrome Mobile (Android 10+) + +--- + +## 10. Risks & Mitigation + +| Risk | Impact | Probability | Mitigation | +|------|--------|-------------|------------| +| Bundle size increase | High | Medium | Code splitting, lazy loading | +| Collaboration conflicts | High | Low | Operational transformation, CRDT | +| AI feature latency | Medium | Medium | Caching, background processing | +| Mobile performance | High | Medium | Progressive enhancement | +| Template complexity | Low | High | Phased rollout, user training | + +--- + +## 11. Open Questions + +1. **AI Provider**: OpenAI, Anthropic, or custom model? +2. **File Storage**: Where to store uploaded images/files? +3. **Template Marketplace**: Allow public template sharing? +4. **Offline Support**: Full offline capability or read-only? +5. **Integration Priorities**: Which third-party tools to integrate first? + +--- + +## 12. Success Criteria + +The project will be considered successful when: +1. ✅ 90% of users adopt at least one template +2. ✅ Mobile usage increases to 35% +3. ✅ Average session note quality score improves by 40% +4. ✅ Zero critical accessibility issues +5. ✅ Page load time under 1 second +6. ✅ User satisfaction (NPS) reaches 85 + +--- + +## Appendices + +### A. Competitive Analysis +- Notion: Excellent templates, blocks system +- Google Docs: Superior collaboration, comments +- Obsidian: Great linking, markdown support +- Roam Research: Powerful references, daily notes + +### B. User Research Data +- Survey results from 50 coaches +- Session recordings analysis +- Feature request prioritization + +### C. Technical Dependencies +- TipTap 2.x ecosystem +- Y.js for CRDT +- Hocuspocus for collaboration server +- AI API integration requirements + +--- + +**Document Control:** +- Review Cycle: Bi-weekly +- Stakeholders: Product, Engineering, Design, Customer Success +- Next Review: February 1, 2025 \ No newline at end of file diff --git a/docs/ui-mockups-toolbar-design.md b/docs/ui-mockups-toolbar-design.md new file mode 100644 index 00000000..5cc6b656 --- /dev/null +++ b/docs/ui-mockups-toolbar-design.md @@ -0,0 +1,604 @@ +# UI Design Mockups: Enhanced Coaching Notes Toolbar +## Based on TipTap SimpleEditor Template + +--- + +## 1. Design Overview + +### 1.1 Design Principles +- **Progressive Disclosure**: Show common tools first, advanced features on-demand +- **Context-Aware**: Toolbar adapts based on selection and content type +- **Mobile-First**: Touch-optimized with responsive breakpoints +- **Coaching-Focused**: Quick access to session-specific tools + +### 1.2 Toolbar Architecture +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Primary Toolbar (Always Visible) │ +├─────────────────────────────────────────────────────────────────┤ +│ Secondary Toolbar (Contextual) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. Desktop Toolbar Design + +### 2.1 Primary Toolbar - Default State +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ ┌──────────┬───────────────┬──────────────┬────────────┬─────────┬────────┐ │ +│ │ Template │ Text Format │ Lists │ Insert │ Coach │ More │ │ +│ │ ▼ │ B I U S ─ 🎨 │ • 1. ☑ │ 🔗 📷 📊 │ 💡 ✅ │ ⋯ │ │ +│ └──────────┴───────────────┴──────────────┴────────────┴─────────┴────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────────────────┐ │ +│ │ Editor Content Area │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 Detailed Toolbar Groups + +#### Group 1: Template Selector (Coaching-Specific) +``` +┌─────────────────┐ +│ 📝 Template ▼ │ ← Click to expand +└─────────────────┘ + ↓ +┌──────────────────────────┐ +│ 🎯 Initial Session │ +│ 📅 Regular Check-in │ +│ 🎖️ Goal Review │ +│ 🔄 Retrospective │ +│ 📋 Action Planning │ +│ ───────────────────── │ +│ ➕ Create Custom... │ +│ 📁 Manage Templates... │ +└──────────────────────────┘ +``` + +#### Group 2: Text Formatting +``` +┌─────────────────────────────────┐ +│ B │ I │ U │ S │ H │ 🎨│ ← Individual toggle buttons +└─────────────────────────────────┘ + Bold Italic Under Strike High Color + +Color Picker Dropdown: +┌──────────────────┐ +│ Text Color │ +│ ● ● ● ● ● ● ● │ +│ ● ● ● ● ● ● ● │ +│ Background │ +│ ○ ○ ○ ○ ○ ○ ○ │ +└──────────────────┘ +``` + +#### Group 3: Paragraph Formatting +``` +┌──────────────────────────────┐ +│ ¶ Normal ▼ │ ≡ │ ≡ │ ≡ │ ≡ │ +└──────────────────────────────┘ + ↓ Left Center Right Justify +┌─────────────┐ +│ ¶ Paragraph │ +│ H1 Title │ +│ H2 Heading │ +│ H3 Subhead │ +│ " Quote │ +│ 💡 Callout │ +└─────────────┘ +``` + +#### Group 4: Lists & Tasks +``` +┌────────────────────┐ +│ • │ 1. │ ☑ │ │ ← List types +└────────────────────┘ +Bullet Ordered Tasks + +Task List Special Features: +- Auto-converts "- [ ]" to checkbox +- Keyboard shortcut: Cmd+Shift+X +- Supports nested tasks +``` + +#### Group 5: Insert Menu +``` +┌─────────────────────┐ +│ 🔗 │ 📷 │ 📊 │ ← Insert elements +└─────────────────────┘ + Link Image Table + +Extended Insert (via dropdown): +┌────────────────────┐ +│ 🔗 Link │ +│ 📷 Image │ +│ 📊 Table │ +│ ───────────── │ +│ 📹 Video │ +│ 💻 Code Block │ +│ ➖ Divider │ +│ 📝 Note Box │ +│ ⚠️ Alert Box │ +└────────────────────┘ +``` + +#### Group 6: Coaching Tools (Unique Features) +``` +┌──────────────────────────┐ +│ 💡 │ ✅ │ 👥 │ 📌 │ +└──────────────────────────┘ +Action Summary Mention Bookmark + +Action Item Quick Add: +┌─────────────────────────────┐ +│ ✅ Create Action Item │ +│ ┌─────────────────────────┐ │ +│ │ Description... │ │ +│ └─────────────────────────┘ │ +│ Owner: [Select...] Due: []│ +│ [Create] [Cancel] │ +└─────────────────────────────┘ +``` + +### 2.3 Contextual/Floating Toolbar +``` +When text is selected: +┌─────────────────────────────────────┐ +│ B I U │ 🔗 │ 💬 │ 🎨 │ ⋯ │ +└─────────────────────────────────────┘ + ↑ + Appears above selection + +When hovering over link: +┌──────────────────────────────┐ +│ 🔗 example.com │ +│ [Edit] [Unlink] [Open] │ +└──────────────────────────────┘ +``` + +--- + +## 3. Mobile Toolbar Design + +### 3.1 Mobile Primary Toolbar (Collapsed) +``` +┌─────────────────────────────────┐ +│ ┌───┬─────────────────────┬───┐ │ +│ │ ≡ │ Coaching Notes │ ⋯ │ │ +│ └───┴─────────────────────┴───┘ │ +│ ┌─────────────────────────────┐ │ +│ │ B I U │ • │ 🔗 │ 💡 │ ▶ │ │ +│ └─────────────────────────────┘ │ +└─────────────────────────────────┘ + Menu Essential Tools More +``` + +### 3.2 Mobile Expanded Toolbar (Swipe Up) +``` +┌─────────────────────────────────┐ +│ ┌─────────────────────────────┐ │ +│ │ Text Formatting │ │ +│ │ B I U S H 🎨 │ │ +│ └─────────────────────────────┘ │ +│ ┌─────────────────────────────┐ │ +│ │ Lists │ │ +│ │ • 1. ☑ │ │ +│ └─────────────────────────────┘ │ +│ ┌─────────────────────────────┐ │ +│ │ Insert │ │ +│ │ 🔗 📷 📊 💻 │ │ +│ └─────────────────────────────┘ │ +│ ┌─────────────────────────────┐ │ +│ │ Coaching Tools │ │ +│ │ 💡 ✅ 👥 📌 │ │ +│ └─────────────────────────────┘ │ +└─────────────────────────────────┘ +``` + +### 3.3 Mobile Context Menu (Long Press) +``` +On text selection: +┌─────────────────┐ +│ Cut │ +│ Copy │ +│ Paste │ +│ ─────────── │ +│ Bold │ +│ Italic │ +│ Link... │ +│ Comment... │ +│ ─────────── │ +│ Create Action │ +└─────────────────┘ +``` + +--- + +## 4. Advanced Features UI + +### 4.1 AI Assistant Panel +``` +┌──────────────────────────────────────────────┐ +│ 🤖 AI Assistant [×] │ +├──────────────────────────────────────────────┤ +│ Quick Actions: │ +│ ┌──────────────┬──────────────┐ │ +│ │ Summarize │ Extract │ │ +│ │ Session │ Actions │ │ +│ └──────────────┴──────────────┘ │ +│ ┌──────────────┬──────────────┐ │ +│ │ Improve │ Generate │ │ +│ │ Writing │ Follow-up │ │ +│ └──────────────┴──────────────┘ │ +│ │ +│ Custom Prompt: │ +│ ┌────────────────────────────────┐ │ +│ │ Ask AI anything... │ │ +│ └────────────────────────────────┘ │ +│ [Send] │ +└──────────────────────────────────────────────┘ +``` + +### 4.2 Template Builder Interface +``` +┌──────────────────────────────────────────────────────┐ +│ Template Builder [×] │ +├──────────────────────────────────────────────────────┤ +│ Template Name: [____________________] │ +│ │ +│ Sections: │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ 1. Meeting Objectives [⚙️] │ │ +│ │ Placeholder: {{session.goals}} │ │ +│ └──────────────────────────────────────────────┘ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ 2. Discussion Points [⚙️] │ │ +│ │ • {{auto.previous_actions}} │ │ +│ └──────────────────────────────────────────────┘ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ 3. Action Items [⚙️] │ │ +│ │ ☑ Task list here... │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ +│ [+ Add Section] │ +│ │ +│ Available Variables: │ +│ {{date}} {{coach.name}} {{coachee.name}} │ +│ {{session.number}} {{goals.current}} │ +│ │ +│ [Save Template] [Preview] [Cancel] │ +└──────────────────────────────────────────────────────┘ +``` + +### 4.3 Collaboration Sidebar +``` +┌─────────────────────┐ +│ Collaborators 2 │ +├─────────────────────┤ +│ 👤 Sarah Chen │ +│ 🟢 Active now │ +│ Cursor: Line 42 │ +│ │ +│ 👤 Alex Kumar │ +│ 🟡 Idle (2 min) │ +│ Last: Line 18 │ +├─────────────────────┤ +│ Comments 3 │ +├─────────────────────┤ +│ 💬 Sarah: "Let's │ +│ focus on the Q2 │ +│ goals here" │ +│ └─ Reply... │ +│ │ +│ 💬 Alex: "Added │ +│ action items" │ +│ └─ ✓ Resolved │ +└─────────────────────┘ +``` + +--- + +## 5. Responsive Behavior + +### 5.1 Breakpoint Behaviors +``` +Desktop (>1024px): +- Full toolbar with all groups visible +- Floating toolbar on selection +- Sidebars for outline/comments + +Tablet (768-1024px): +- Grouped toolbar with dropdowns +- Essential tools visible, rest in "More" +- Collapsible sidebars + +Mobile (<768px): +- Minimal toolbar with expandable drawer +- Context menu on long-press +- Full-screen editing mode option +``` + +### 5.2 Touch Gestures +``` +Swipe Right → Undo +Swipe Left → Redo +Swipe Up → Show toolbar +Swipe Down → Hide toolbar +Pinch → Zoom text +Long Press → Context menu +Double Tap → Select word +Triple Tap → Select paragraph +``` + +--- + +## 6. Keyboard Shortcuts Overlay + +### 6.1 Shortcut Helper (? or Cmd+/) +``` +┌──────────────────────────────────────────────┐ +│ Keyboard Shortcuts [×] │ +├──────────────────────────────────────────────┤ +│ Text Formatting │ +│ Cmd+B Bold │ +│ Cmd+I Italic │ +│ Cmd+U Underline │ +│ Cmd+Shift+S Strikethrough │ +│ Cmd+Shift+H Highlight │ +│ │ +│ Coaching Features │ +│ Cmd+Shift+A Create Action Item │ +│ Cmd+Shift+M Insert @Mention │ +│ Cmd+E Generate Summary │ +│ Cmd+T Apply Template │ +│ │ +│ Navigation │ +│ Cmd+F Find in Document │ +│ Cmd+G Go to Next │ +│ Cmd+[ Decrease Indent │ +│ Cmd+] Increase Indent │ +└──────────────────────────────────────────────┘ +``` + +--- + +## 7. Status Bar Design + +### 7.1 Desktop Status Bar +``` +┌────────────────────────────────────────────────────────────┐ +│ 📝 1,247 words │ 5 min read │ 💾 Saved │ 👥 2 active │ ⚡ │ +└────────────────────────────────────────────────────────────┘ + Word count Read time Save status Collaborators AI +``` + +### 7.2 Mobile Status Bar (Minimal) +``` +┌──────────────────────────┐ +│ 1.2k words │ 💾 │ 👥 2 │ +└──────────────────────────┘ +``` + +--- + +## 8. Theme Variations + +### 8.1 Light Theme +```css +/* Toolbar */ +--toolbar-bg: #FFFFFF; +--toolbar-border: #E5E7EB; +--button-bg: transparent; +--button-hover: #F3F4F6; +--button-active: #3B82F6; +--text-primary: #111827; +--text-secondary: #6B7280; + +/* Editor */ +--editor-bg: #FFFFFF; +--editor-text: #111827; +--selection-bg: #DBEAFE; +``` + +### 8.2 Dark Theme +```css +/* Toolbar */ +--toolbar-bg: #1F2937; +--toolbar-border: #374151; +--button-bg: transparent; +--button-hover: #374151; +--button-active: #3B82F6; +--text-primary: #F9FAFB; +--text-secondary: #9CA3AF; + +/* Editor */ +--editor-bg: #111827; +--editor-text: #F9FAFB; +--selection-bg: #1E3A8A; +``` + +### 8.3 High Contrast (Accessibility) +```css +/* Maximum contrast for visibility */ +--toolbar-bg: #000000; +--toolbar-border: #FFFFFF; +--button-bg: #000000; +--button-hover: #FFFFFF; +--button-active: #FFFF00; +--text-primary: #FFFFFF; +--text-secondary: #FFFF00; +``` + +--- + +## 9. Animation & Micro-interactions + +### 9.1 Toolbar Animations +``` +Button Press: +- Scale: 0.95 +- Duration: 150ms +- Easing: ease-out + +Dropdown Open: +- Slide down: 200ms +- Fade in: 150ms +- Easing: cubic-bezier(0.4, 0, 0.2, 1) + +Tool Switch: +- Morph transition: 200ms +- Color fade: 150ms +``` + +### 9.2 Feedback Animations +``` +Action Item Created: +✅ → Scale from 0 → Bounce → Settle (400ms) + +Auto-save Indicator: +💾 → Pulse (2s interval) → Checkmark → Fade + +Collaboration Cursor: +- Smooth position: 16ms updates +- Name tag fade: 200ms +- Color pulse on typing +``` + +--- + +## 10. Implementation Components + +### 10.1 Component Structure (React/TypeScript) +```typescript +// Main Toolbar Component + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### 10.2 Responsive Container +```typescript +const ResponsiveToolbar = () => { + const isMobile = useMediaQuery('(max-width: 768px)'); + const isTablet = useMediaQuery('(max-width: 1024px)'); + + if (isMobile) { + return ; + } + + if (isTablet) { + return ; + } + + return ; +}; +``` + +--- + +## 11. Accessibility Features + +### 11.1 ARIA Labels +```html + + +
+ +
+``` + +### 11.2 Keyboard Navigation +- Tab: Navigate between toolbar groups +- Arrow keys: Navigate within groups +- Enter/Space: Activate button +- Escape: Close dropdowns/dialogs +- F10: Focus toolbar + +--- + +## 12. Performance Optimizations + +### 12.1 Lazy Loading +```typescript +// Load advanced features on-demand +const AIAssistant = lazy(() => import('./AIAssistant')); +const TemplateBuilder = lazy(() => import('./TemplateBuilder')); +const TableEditor = lazy(() => import('./TableEditor')); +``` + +### 12.2 Virtualization +- Virtual scrolling for long documents +- Render only visible toolbar buttons +- Defer non-critical UI updates + +--- + +## Summary + +This comprehensive toolbar design leverages the TipTap SimpleEditor template while adding coaching-specific features: + +**Key Enhancements:** +1. **Template System** - Quick access to coaching templates +2. **Coaching Tools** - Action items, summaries, mentions +3. **Mobile-First** - Touch-optimized with gestures +4. **AI Integration** - Smart assistance panel +5. **Collaboration** - Real-time presence and comments +6. **Accessibility** - WCAG AAA compliant +7. **Performance** - Lazy loading and virtualization + +The design maintains the simplicity of the SimpleEditor while providing powerful features for professional coaching sessions. The progressive disclosure pattern ensures new users aren't overwhelmed while power users have quick access to advanced features. \ No newline at end of file diff --git a/src/app/coaching-sessions/[id]/page.tsx b/src/app/coaching-sessions/[id]/page.tsx index f8c0a356..6a700bc7 100644 --- a/src/app/coaching-sessions/[id]/page.tsx +++ b/src/app/coaching-sessions/[id]/page.tsx @@ -19,6 +19,7 @@ import ShareSessionLink from "@/components/ui/share-session-link"; import { toast } from "sonner"; import { ForbiddenError } from "@/components/ui/errors/forbidden-error"; import { EntityApiError } from "@/types/general"; +import { shouldSyncRelationship } from "./relationship-sync"; export default function CoachingSessionsPage() { const router = useRouter(); @@ -40,27 +41,6 @@ export default function CoachingSessionsPage() { const { currentCoachingRelationshipId, setCurrentCoachingRelationshipId, refresh } = useCurrentCoachingRelationship(); - /** - * Helper function to determine if relationship ID should be synced from session data. - * - * The URL is the source of truth for the current session. We sync the relationship ID - * from the session data in two cases: - * 1. Store is empty (e.g., new tab/window) - fixes Issue #79 - * 2. Store has a different relationship (e.g., navigating between sessions) - fixes Bug #228 - * - * @param sessionRelationshipId - The relationship ID from the current session - * @param currentRelationshipId - The relationship ID currently in the store - * @returns true if we should sync the relationship ID - */ - const shouldSyncRelationship = ( - sessionRelationshipId: string | undefined, - currentRelationshipId: string | null - ): boolean => { - if (!sessionRelationshipId) return false; - // Always sync when empty (new tab) or when different (switching sessions) - return !currentRelationshipId || sessionRelationshipId !== currentRelationshipId; - }; - // Auto-sync relationship ID when session data loads // This ensures the relationship selector always matches the current session useEffect(() => { @@ -137,7 +117,7 @@ export default function CoachingSessionsPage() { onError={handleShareError} /> diff --git a/src/app/coaching-sessions/[id]/relationship-sync.ts b/src/app/coaching-sessions/[id]/relationship-sync.ts new file mode 100644 index 00000000..2a297392 --- /dev/null +++ b/src/app/coaching-sessions/[id]/relationship-sync.ts @@ -0,0 +1,36 @@ +/** + * Determines if coaching relationship ID should be synced from session data. + * + * The URL is the source of truth for the current session. We sync the relationship ID + * from the session data in two cases: + * 1. Store is empty (e.g., new tab/window) - fixes Issue #79 + * 2. Store has a different relationship (e.g., navigating between sessions) - fixes Bug #228 + * + * @param sessionRelationshipId - The relationship ID from the current session + * @param currentRelationshipId - The relationship ID currently in the store + * @returns true if we should sync the relationship ID + * + * @example + * // Store is empty (new tab) + * shouldSyncRelationship('rel-123', null) // returns true + * + * @example + * // Store has different relationship (switching sessions) + * shouldSyncRelationship('rel-456', 'rel-123') // returns true + * + * @example + * // Store matches session (same relationship) + * shouldSyncRelationship('rel-123', 'rel-123') // returns false + * + * @example + * // Session has no relationship (incomplete data) + * shouldSyncRelationship(undefined, 'rel-123') // returns false + */ +export function shouldSyncRelationship( + sessionRelationshipId: string | undefined, + currentRelationshipId: string | null +): boolean { + if (!sessionRelationshipId) return false + // Always sync when empty (new tab) or when different (switching sessions) + return !currentRelationshipId || sessionRelationshipId !== currentRelationshipId +} diff --git a/src/components/ui/members/member-card.tsx b/src/components/ui/members/member-card.tsx index f5168fc4..d150d1af 100644 --- a/src/components/ui/members/member-card.tsx +++ b/src/components/ui/members/member-card.tsx @@ -27,7 +27,7 @@ import { SelectContent, SelectItem, } from "@/components/ui/select"; -import { CoachingRelationshipWithUserNames } from "@/types/coaching_relationship"; +import { CoachingRelationshipWithUserNames } from "@/types/coaching-relationship"; import { AuthStore } from "@/lib/stores/auth-store"; import { Id } from "@/types/general"; import { User, isAdminOrSuperAdmin, UserRoleState } from "@/types/user"; diff --git a/src/components/ui/members/member-container.tsx b/src/components/ui/members/member-container.tsx index 61778581..e7875f0c 100644 --- a/src/components/ui/members/member-container.tsx +++ b/src/components/ui/members/member-container.tsx @@ -1,7 +1,7 @@ import { MemberList } from "./member-list"; import { AddMemberButton } from "./add-member-button"; import { User, isAdminOrSuperAdmin, sortUsersAlphabetically } from "@/types/user"; -import { CoachingRelationshipWithUserNames, isUserCoach } from "@/types/coaching_relationship"; +import { CoachingRelationshipWithUserNames, isUserCoach } from "@/types/coaching-relationship"; import { UserSession } from "@/types/user-session"; import { useAuthStore } from "@/lib/providers/auth-store-provider"; import { useCurrentUserRole } from "@/lib/hooks/use-current-user-role"; diff --git a/src/components/ui/members/member-list.tsx b/src/components/ui/members/member-list.tsx index 6a3c5b71..2157c3f6 100644 --- a/src/components/ui/members/member-list.tsx +++ b/src/components/ui/members/member-list.tsx @@ -1,7 +1,7 @@ import { Card, CardContent } from "@/components/ui/card"; import { User, UserRoleState } from "@/types/user"; import { MemberCard } from "./member-card"; -import { CoachingRelationshipWithUserNames } from "@/types/coaching_relationship"; +import { CoachingRelationshipWithUserNames } from "@/types/coaching-relationship"; import { Id } from "@/types/general"; interface MemberListProps { diff --git a/src/lib/api/coaching-relationships.ts b/src/lib/api/coaching-relationships.ts index d6873071..f00e6849 100644 --- a/src/lib/api/coaching-relationships.ts +++ b/src/lib/api/coaching-relationships.ts @@ -6,7 +6,7 @@ import { NewCoachingRelationship, CoachingRelationshipWithUserNames, defaultCoachingRelationshipWithUserNames, -} from "@/types/coaching_relationship"; +} from "@/types/coaching-relationship"; import { EntityApi } from "./entity-api"; const ORGANIZATIONS_BASEURL: string = `${siteConfig.env.backendServiceURL}/organizations`; diff --git a/src/lib/hooks/use-auto-select-single-relationship.ts b/src/lib/hooks/use-auto-select-single-relationship.ts index 6b65e0d4..20cd191e 100644 --- a/src/lib/hooks/use-auto-select-single-relationship.ts +++ b/src/lib/hooks/use-auto-select-single-relationship.ts @@ -2,7 +2,7 @@ import { useEffect } from "react"; import { Id } from "@/types/general"; -import { CoachingRelationshipWithUserNames } from "@/types/coaching_relationship"; +import { CoachingRelationshipWithUserNames } from "@/types/coaching-relationship"; /** * Custom hook that automatically selects a coaching relationship when: @@ -12,14 +12,14 @@ import { CoachingRelationshipWithUserNames } from "@/types/coaching_relationship * * @param relationships Array of available coaching relationships * @param isLoading Whether relationships are currently being loaded - * @param currentId Currently selected coaching relationship ID + * @param currentId Currently selected coaching relationship ID (can be null) * @param setCurrentId Function to set the current coaching relationship ID * @param onSelect Optional callback fired when auto-selection occurs */ export const useAutoSelectSingleRelationship = ( relationships: CoachingRelationshipWithUserNames[] | undefined, isLoading: boolean, - currentId: Id, + currentId: Id | null, setCurrentId: (id: Id) => void, onSelect?: (relationshipId: Id) => void ) => { diff --git a/src/lib/hooks/use-current-coaching-relationship.ts b/src/lib/hooks/use-current-coaching-relationship.ts index 50aaa92d..52462625 100644 --- a/src/lib/hooks/use-current-coaching-relationship.ts +++ b/src/lib/hooks/use-current-coaching-relationship.ts @@ -3,15 +3,39 @@ import { useCoachingRelationship } from "@/lib/api/coaching-relationships"; import { useCoachingRelationshipStateStore } from "@/lib/providers/coaching-relationship-state-store-provider"; import { useCurrentOrganization } from "./use-current-organization"; +import type { CoachingRelationshipWithUserNames } from "@/types/coaching-relationship"; +import type { EntityApiError } from "@/types/general"; + +/** + * Return type for the useCurrentCoachingRelationship hook. + */ +export interface UseCurrentCoachingRelationshipReturn { + /** Current coaching relationship ID from state store */ + currentCoachingRelationshipId: string | null; + /** Full coaching relationship data from SWR (null if no IDs set) */ + currentCoachingRelationship: CoachingRelationshipWithUserNames | null; + /** Loading state from SWR */ + isLoading: boolean; + /** Error state from SWR */ + isError: EntityApiError | false; + /** Current organization ID (needed for relationship operations) */ + currentOrganizationId: string | null; + /** Function to set the current coaching relationship ID in store */ + setCurrentCoachingRelationshipId: (id: string) => void; + /** Function to reset the coaching relationship state */ + resetCoachingRelationshipState: () => void; + /** Function to refresh/revalidate the relationship data */ + refresh: () => Promise; +} /** * Hook that provides current coaching relationship state and data. * Tracks the active coaching relationship ID and fetches the full relationship * data using SWR when both organization ID and relationship ID are available. - * + * * @returns Object containing current relationship ID, full relationship data, loading state, and error state */ -export const useCurrentCoachingRelationship = () => { +export const useCurrentCoachingRelationship = (): UseCurrentCoachingRelationshipReturn => { const { currentCoachingRelationshipId, setCurrentCoachingRelationshipId, resetCoachingRelationshipState } = useCoachingRelationshipStateStore((state) => state); diff --git a/src/lib/hooks/use-current-coaching-session.ts b/src/lib/hooks/use-current-coaching-session.ts index b1daee9f..7b363e58 100644 --- a/src/lib/hooks/use-current-coaching-session.ts +++ b/src/lib/hooks/use-current-coaching-session.ts @@ -3,16 +3,34 @@ import { useParams } from "next/navigation"; import { useCoachingSession } from "@/lib/api/coaching-sessions"; import { Id } from "@/types/general"; +import type { CoachingSession } from "@/types/coaching-session"; +import type { EntityApiError } from "@/types/general"; + +/** + * Return type for the useCurrentCoachingSession hook. + */ +export interface UseCurrentCoachingSessionReturn { + /** Current coaching session ID from URL path parameter */ + currentCoachingSessionId: string | null; + /** Full coaching session data from SWR (null if no ID in URL) */ + currentCoachingSession: CoachingSession | null; + /** Loading state from SWR */ + isLoading: boolean; + /** Error state from SWR */ + isError: EntityApiError | false; + /** Function to refresh/revalidate the session data */ + refresh: () => Promise; +} /** * Hook that gets the current coaching session ID from URL path parameters * and fetches the full coaching session data using SWR. - * + * * URL structure: /coaching-sessions/[id] - * + * * @returns Object containing current session ID, full session data, loading state, and error state */ -export const useCurrentCoachingSession = () => { +export const useCurrentCoachingSession = (): UseCurrentCoachingSessionReturn => { const params = useParams(); // Extract coaching session ID from URL path params (/coaching-sessions/123) diff --git a/src/lib/relationships/relationship-utils.ts b/src/lib/relationships/relationship-utils.ts index ca35a4b5..8f21ac73 100644 --- a/src/lib/relationships/relationship-utils.ts +++ b/src/lib/relationships/relationship-utils.ts @@ -1,4 +1,4 @@ -import { CoachingRelationshipWithUserNames } from "@/types/coaching_relationship"; +import { CoachingRelationshipWithUserNames } from "@/types/coaching-relationship"; import { User } from "@/types/user"; import { RelationshipRole } from "@/types/relationship-role"; diff --git a/src/lib/sessions/session-utils.ts b/src/lib/sessions/session-utils.ts index 54d01d0a..1d9a5f96 100644 --- a/src/lib/sessions/session-utils.ts +++ b/src/lib/sessions/session-utils.ts @@ -1,6 +1,6 @@ import { DateTime } from "ts-luxon"; import { CoachingSession } from "@/types/coaching-session"; -import { CoachingRelationshipWithUserNames } from "@/types/coaching_relationship"; +import { CoachingRelationshipWithUserNames } from "@/types/coaching-relationship"; import { User } from "@/types/user"; import { SessionUrgency, diff --git a/src/lib/utils/user-roles.ts b/src/lib/utils/user-roles.ts index 0f2e6670..89e660c8 100644 --- a/src/lib/utils/user-roles.ts +++ b/src/lib/utils/user-roles.ts @@ -1,5 +1,5 @@ import { User, Role } from "@/types/user"; -import { CoachingRelationshipWithUserNames, isUserCoach, isUserCoachee } from "@/types/coaching_relationship"; +import { CoachingRelationshipWithUserNames, isUserCoach, isUserCoachee } from "@/types/coaching-relationship"; import { RelationshipRole } from "@/types/relationship-role"; import { Id } from "@/types/general"; diff --git a/src/types/coaching_relationship.ts b/src/types/coaching-relationship.ts similarity index 100% rename from src/types/coaching_relationship.ts rename to src/types/coaching-relationship.ts diff --git a/src/types/coaching-session.ts b/src/types/coaching-session.ts index aa061aac..8800dafa 100644 --- a/src/types/coaching-session.ts +++ b/src/types/coaching-session.ts @@ -1,7 +1,7 @@ import { DateTime } from "ts-luxon"; import { Id } from "@/types/general"; import { SortOrder } from "@/types/sorting"; -import { CoachingRelationship } from "@/types/coaching_relationship"; +import { CoachingRelationship } from "@/types/coaching-relationship"; import { User } from "@/types/user"; import { Organization } from "@/types/organization"; import { OverarchingGoal } from "@/types/overarching-goal"; diff --git a/src/types/session-title.ts b/src/types/session-title.ts index 09e4f695..ec365526 100644 --- a/src/types/session-title.ts +++ b/src/types/session-title.ts @@ -1,5 +1,5 @@ import { DateTime } from "ts-luxon"; -import { CoachingRelationshipWithUserNames } from "./coaching_relationship"; +import { CoachingRelationshipWithUserNames } from "./coaching-relationship"; import { CoachingSession } from "./coaching-session"; import { siteConfig } from "@/site.config"; import { getDateTimeFromString } from "./general"; From a159bce297c858b1aa0226de3dbde37310dffe87 Mon Sep 17 00:00:00 2001 From: Jim Hodapp Date: Fri, 14 Nov 2025 09:31:12 -0600 Subject: [PATCH 4/4] Remove unnecessary documentation files from branch These documentation files were committed by mistake and should not be part of this PR. --- docs/api-spec-todays-sessions-endpoint.md | 666 --------------------- docs/bug-analysis-link-popover-188.md | 401 ------------- docs/prd-improved-coaching-notes-editor.md | 526 ---------------- docs/ui-mockups-toolbar-design.md | 604 ------------------- 4 files changed, 2197 deletions(-) delete mode 100644 docs/api-spec-todays-sessions-endpoint.md delete mode 100644 docs/bug-analysis-link-popover-188.md delete mode 100644 docs/prd-improved-coaching-notes-editor.md delete mode 100644 docs/ui-mockups-toolbar-design.md diff --git a/docs/api-spec-todays-sessions-endpoint.md b/docs/api-spec-todays-sessions-endpoint.md deleted file mode 100644 index 8a79a5c0..00000000 --- a/docs/api-spec-todays-sessions-endpoint.md +++ /dev/null @@ -1,666 +0,0 @@ -# API Specification: Today's Sessions Endpoint - -**Version:** 1.0 -**Date:** 2025-10-23 -**Author:** System Architecture Team -**Status:** Proposed - -## Overview - -This document specifies a new backend API endpoint designed to efficiently fetch all of a user's coaching sessions scheduled for "today" across all their coaching relationships, including associated overarching goals, relationship details, and organization information. - -### Problem Statement - -The current frontend implementation requires multiple API calls to display today's sessions: -1. Fetch user's organizations -2. For each organization, fetch coaching relationships -3. For each relationship, fetch sessions in date range -4. For each session, fetch overarching goal - -This results in an N+1 query problem and dozens of API round trips, causing: -- Performance degradation -- Complex client-side state management -- Infinite loop vulnerabilities from unstable array references -- Poor user experience with loading states - -### Solution - -A single, optimized backend endpoint that returns all necessary data in one request. - ---- - -## Endpoint Definition - -### HTTP Method and Path - -``` -GET /api/v1/users/{userId}/todays-sessions -``` - -### Path Parameters - -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `userId` | UUID | Yes | The ID of the user requesting their sessions | - -### Query Parameters - -| Parameter | Type | Required | Default | Description | -|-----------|------|----------|---------|-------------| -| `timezone` | String | No | User's profile timezone or UTC | IANA timezone identifier (e.g., "America/Los_Angeles", "Europe/London") | -| `include_past` | Boolean | No | `false` | Whether to include sessions that have already ended today | - -### Request Headers - -``` -Authorization: Bearer {jwt_token} -Content-Type: application/json -``` - -### Authentication & Authorization - -- **Authentication:** Required. User must be authenticated via JWT. -- **Authorization:** User can only access their own sessions (`userId` in path must match authenticated user's ID). -- **Error Response:** `403 Forbidden` if user attempts to access another user's sessions. - ---- - -## Response Format - -### Success Response (200 OK) - -```json -{ - "user_id": "550e8400-e29b-41d4-a716-446655440000", - "date": "2025-10-23", - "timezone": "America/Los_Angeles", - "sessions": [ - { - "id": "650e8400-e29b-41d4-a716-446655440001", - "date": "2025-10-23T14:00:00Z", - "coaching_relationship_id": "750e8400-e29b-41d4-a716-446655440002", - "overarching_goal": { - "id": "850e8400-e29b-41d4-a716-446655440003", - "title": "Improve leadership communication skills", - "description": "Develop clearer communication strategies for team management" - }, - "relationship": { - "id": "750e8400-e29b-41d4-a716-446655440002", - "coach_id": "950e8400-e29b-41d4-a716-446655440004", - "coach_first_name": "Sarah", - "coach_last_name": "Johnson", - "coachee_id": "550e8400-e29b-41d4-a716-446655440000", - "coachee_first_name": "John", - "coachee_last_name": "Smith", - "organization_id": "a50e8400-e29b-41d4-a716-446655440005" - }, - "organization": { - "id": "a50e8400-e29b-41d4-a716-446655440005", - "name": "Refactor Group" - } - }, - { - "id": "650e8400-e29b-41d4-a716-446655440006", - "date": "2025-10-23T16:30:00Z", - "coaching_relationship_id": "750e8400-e29b-41d4-a716-446655440007", - "overarching_goal": null, - "relationship": { - "id": "750e8400-e29b-41d4-a716-446655440007", - "coach_id": "550e8400-e29b-41d4-a716-446655440000", - "coach_first_name": "John", - "coach_last_name": "Smith", - "coachee_id": "950e8400-e29b-41d4-a716-446655440008", - "coachee_first_name": "Emily", - "coachee_last_name": "Davis", - "organization_id": "a50e8400-e29b-41d4-a716-446655440009" - }, - "organization": { - "id": "a50e8400-e29b-41d4-a716-446655440009", - "name": "Tech Innovations Inc" - } - } - ], - "total_count": 2 -} -``` - -### Response Fields - -#### Root Level - -| Field | Type | Description | -|-------|------|-------------| -| `user_id` | UUID | The user ID for whom sessions were fetched | -| `date` | String (ISO 8601 Date) | The date for which sessions are returned (in user's timezone) | -| `timezone` | String | The timezone used to determine "today" | -| `sessions` | Array | Array of session objects (see below) | -| `total_count` | Integer | Total number of sessions returned | - -#### Session Object - -| Field | Type | Nullable | Description | -|-------|------|----------|-------------| -| `id` | UUID | No | Session unique identifier | -| `date` | String (ISO 8601 DateTime) | No | Session date/time in UTC | -| `coaching_relationship_id` | UUID | No | ID of the associated coaching relationship | -| `overarching_goal` | Object | Yes | Associated overarching goal (null if none) | -| `relationship` | Object | No | Coaching relationship details | -| `organization` | Object | No | Organization details | - -#### Overarching Goal Object (nullable) - -| Field | Type | Description | -|-------|------|-------------| -| `id` | UUID | Goal unique identifier | -| `title` | String | Goal title/summary | -| `description` | String | Detailed goal description | - -#### Relationship Object - -| Field | Type | Description | -|-------|------|-------------| -| `id` | UUID | Relationship unique identifier | -| `coach_id` | UUID | Coach user ID | -| `coach_first_name` | String | Coach first name | -| `coach_last_name` | String | Coach last name | -| `coachee_id` | UUID | Coachee user ID | -| `coachee_first_name` | String | Coachee first name | -| `coachee_last_name` | String | Coachee last name | -| `organization_id` | UUID | Organization ID | - -#### Organization Object - -| Field | Type | Description | -|-------|------|-------------| -| `id` | UUID | Organization unique identifier | -| `name` | String | Organization name | - -### Error Responses - -#### 401 Unauthorized -```json -{ - "error": "Unauthorized", - "message": "Authentication required" -} -``` - -#### 403 Forbidden -```json -{ - "error": "Forbidden", - "message": "You do not have permission to access sessions for this user" -} -``` - -#### 400 Bad Request -```json -{ - "error": "Bad Request", - "message": "Invalid timezone identifier", - "details": { - "field": "timezone", - "provided": "Invalid/Timezone", - "valid_example": "America/Los_Angeles" - } -} -``` - -#### 404 Not Found -```json -{ - "error": "Not Found", - "message": "User not found" -} -``` - -#### 500 Internal Server Error -```json -{ - "error": "Internal Server Error", - "message": "An unexpected error occurred" -} -``` - ---- - -## Business Logic - -### "Today" Definition - -The endpoint determines "today" based on the following logic: - -1. **Timezone Resolution:** - - Use `timezone` query parameter if provided - - Else, use user's profile timezone setting - - Else, default to UTC - -2. **Date Range Calculation:** - ``` - startOfDay = today at 00:00:00 in user's timezone, converted to UTC - endOfDay = today at 23:59:59.999 in user's timezone, converted to UTC - ``` - -3. **Session Filtering:** - - Include sessions where `session.date >= startOfDay AND session.date <= endOfDay` - - Apply timezone conversion to ensure accurate filtering - -### Relationship Inclusion - -Include sessions from all coaching relationships where: -- User is either coach OR coachee -- Relationship is active (not archived/deleted) -- Session falls within today's date range - -### Overarching Goal Association - -- Each session can have 0 or 1 associated overarching goal -- If no goal exists, `overarching_goal` field is `null` -- If multiple goals exist (edge case), return the most recently created goal - -### Sorting - -Sessions are sorted chronologically by `date` (ascending) - earliest session first. - ---- - -## Implementation Considerations - -### Database Query Optimization - -Recommended SQL approach (pseudo-code): - -```sql --- Single query with LEFT JOINs to avoid N+1 queries -SELECT - cs.id, - cs.date, - cs.coaching_relationship_id, - og.id as goal_id, - og.title as goal_title, - og.description as goal_description, - cr.id as relationship_id, - cr.coach_id, - coach.first_name as coach_first_name, - coach.last_name as coach_last_name, - cr.coachee_id, - coachee.first_name as coachee_first_name, - coachee.last_name as coachee_last_name, - cr.organization_id, - org.name as organization_name -FROM coaching_sessions cs -INNER JOIN coaching_relationships cr - ON cs.coaching_relationship_id = cr.id -INNER JOIN users coach ON cr.coach_id = coach.id -INNER JOIN users coachee ON cr.coachee_id = coachee.id -INNER JOIN organizations org ON cr.organization_id = org.id -LEFT JOIN overarching_goals og - ON cs.id = og.coaching_session_id -WHERE - (cr.coach_id = :userId OR cr.coachee_id = :userId) - AND cs.date >= :startOfDay - AND cs.date <= :endOfDay - AND cr.deleted_at IS NULL -ORDER BY cs.date ASC -``` - -### Caching Strategy - -Consider caching this endpoint response with: -- **Cache Key:** `user:{userId}:todays-sessions:{date}:{timezone}` -- **TTL:** 5-15 minutes (sessions don't change frequently) -- **Invalidation:** Clear cache when: - - User creates/updates/deletes a session - - User creates/updates/deletes an overarching goal - - Date changes (midnight in user's timezone) - -### Performance Targets - -- **Response Time:** < 200ms (p95) -- **Database Query:** Single query with JOINs (no N+1) -- **Typical Payload Size:** < 10KB (assuming ~5 sessions per user per day) - -### Index Recommendations - -```sql --- Composite index for efficient date range + user filtering -CREATE INDEX idx_sessions_date_relationship - ON coaching_sessions(date, coaching_relationship_id); - --- Index for relationship user lookups -CREATE INDEX idx_relationships_users - ON coaching_relationships(coach_id, coachee_id) - WHERE deleted_at IS NULL; - --- Index for goal lookups -CREATE INDEX idx_goals_session - ON overarching_goals(coaching_session_id); -``` - ---- - -## Example Usage - -### Request Example 1: Basic Request - -```bash -curl -X GET \ - 'https://api.refactorcoaching.com/api/v1/users/550e8400-e29b-41d4-a716-446655440000/todays-sessions' \ - -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \ - -H 'Content-Type: application/json' -``` - -### Request Example 2: With Timezone - -```bash -curl -X GET \ - 'https://api.refactorcoaching.com/api/v1/users/550e8400-e29b-41d4-a716-446655440000/todays-sessions?timezone=America/New_York' \ - -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \ - -H 'Content-Type: application/json' -``` - -### Request Example 3: Include Past Sessions - -```bash -curl -X GET \ - 'https://api.refactorcoaching.com/api/v1/users/550e8400-e29b-41d4-a716-446655440000/todays-sessions?include_past=true' \ - -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' \ - -H 'Content-Type: application/json' -``` - ---- - -## Frontend Integration - -### Proposed Hook Implementation - -Once this endpoint is available, the frontend can be simplified to: - -```typescript -// src/lib/api/todays-sessions.ts -export interface TodaysSessionsResponse { - user_id: string; - date: string; - timezone: string; - sessions: Array<{ - id: string; - date: string; - coaching_relationship_id: string; - overarching_goal: { - id: string; - title: string; - description: string; - } | null; - relationship: { - id: string; - coach_id: string; - coach_first_name: string; - coach_last_name: string; - coachee_id: string; - coachee_first_name: string; - coachee_last_name: string; - organization_id: string; - }; - organization: { - id: string; - name: string; - }; - }>; - total_count: number; -} - -export const TodaysSessionsApi = { - get: async (userId: string, timezone?: string): Promise => { - const params = new URLSearchParams(); - if (timezone) params.append('timezone', timezone); - - const response = await fetch( - `${API_BASE_URL}/users/${userId}/todays-sessions?${params}`, - { - headers: { - 'Authorization': `Bearer ${getToken()}`, - 'Content-Type': 'application/json', - }, - } - ); - - if (!response.ok) { - throw new Error(`Failed to fetch today's sessions: ${response.statusText}`); - } - - return response.json(); - }, -}; - -// Hook -export function useTodaysSessions() { - const { userSession } = useAuthStore(); - const userId = userSession?.id; - const timezone = userSession?.timezone || getBrowserTimezone(); - - const { data, error, isLoading, mutate } = useSWR( - userId ? ['todays-sessions', userId, timezone] : null, - () => TodaysSessionsApi.get(userId!, timezone), - { - revalidateOnFocus: false, - revalidateOnReconnect: true, - dedupingInterval: 300000, // 5 minutes - } - ); - - return { - sessions: data?.sessions || [], - isLoading, - error, - refresh: mutate, - }; -} -``` - -### Benefits of New Hook - -1. **Single API Call:** Replaces dozens of calls with one -2. **No Client-Side Joins:** All data pre-joined by backend -3. **No N+1 Queries:** Goals included in initial response -4. **Stable References:** Single array from single source -5. **Simpler State:** No complex Maps/Sets for tracking loaded data -6. **Timezone Accuracy:** Backend handles timezone logic correctly - ---- - -## Migration Path - -### Phase 1: Backend Implementation -1. Implement new endpoint with comprehensive tests -2. Deploy to staging environment -3. Validate performance and correctness - -### Phase 2: Frontend Preparation -1. Create new `useTodaysSessions` hook using new endpoint -2. Keep old implementation as fallback -3. Feature flag to toggle between old/new implementation - -### Phase 3: Gradual Rollout -1. Enable for internal users (beta testing) -2. Monitor performance metrics and error rates -3. Gradually increase rollout percentage -4. Full rollout once validated - -### Phase 4: Cleanup -1. Remove old implementation after 30 days -2. Delete obsolete hooks and utilities -3. Update documentation - ---- - -## Testing Requirements - -### Unit Tests - -- Timezone edge cases (date boundaries, DST transitions) -- Null handling for optional fields (overarching goals) -- User authorization (coach vs coachee access) -- Date range filtering accuracy - -### Integration Tests - -- Multiple organizations and relationships -- Sessions spanning midnight in user's timezone -- Cache invalidation scenarios -- Performance under load (100+ concurrent requests) - -### Test Cases - -1. **User with no sessions today** → Returns empty array -2. **User in PST timezone at 11:30 PM PST** → Only includes sessions before midnight PST -3. **User as both coach and coachee** → Includes sessions from both roles -4. **Session with no overarching goal** → `overarching_goal` is null -5. **Invalid timezone parameter** → Returns 400 Bad Request -6. **Unauthorized access** → Returns 403 Forbidden -7. **User from different timezone** → Correctly filters based on their timezone - ---- - -## Security Considerations - -1. **Authorization:** Enforce user can only access their own sessions -2. **Data Exposure:** Only expose necessary fields (no sensitive internal data) -3. **Rate Limiting:** Apply standard API rate limits (suggested: 100 requests/minute per user) -4. **Input Validation:** Validate timezone parameter against IANA timezone database -5. **SQL Injection:** Use parameterized queries for all database access - ---- - -## Monitoring & Observability - -### Metrics to Track - -- Request latency (p50, p95, p99) -- Error rate by status code -- Cache hit/miss ratio -- Database query execution time -- Number of sessions returned per request - -### Alerts - -- Error rate > 1% for 5 minutes → Page on-call engineer -- p95 latency > 500ms for 5 minutes → Warning notification -- Database query time > 1 second → Warning notification - -### Logging - -Log the following for each request: -- User ID -- Timezone used -- Number of sessions returned -- Query execution time -- Any errors encountered - ---- - -## Future Enhancements - -Potential future improvements to consider: - -1. **Date Range Parameter:** Allow fetching sessions for arbitrary date ranges - ``` - GET /users/{userId}/sessions?from_date={date}&to_date={date} - ``` - -2. **Pagination:** For users with many sessions per day - ``` - GET /users/{userId}/todays-sessions?page=1&per_page=10 - ``` - -3. **Filtering:** Filter by organization, relationship, or goal status - ``` - GET /users/{userId}/todays-sessions?organization_id={id} - ``` - -4. **Sorting Options:** Allow custom sorting (by date, organization, etc.) - ``` - GET /users/{userId}/todays-sessions?sort_by=organization&sort_order=asc - ``` - -5. **Field Selection:** Allow clients to request specific fields only - ``` - GET /users/{userId}/todays-sessions?fields=id,date,overarching_goal.title - ``` - ---- - -## Questions for Backend Team - -1. **Current Session Model:** Does the current `coaching_sessions` table include soft deletes? Should deleted sessions be excluded? - -2. **Overarching Goal Cardinality:** Is it always 1:1 with sessions, or can multiple goals exist per session? - -3. **Performance Baseline:** What is the current average response time for the existing session list endpoint? - -4. **Caching Infrastructure:** Is Redis/Memcached available for response caching? - -5. **Rate Limiting:** What are the current API rate limits, and should this endpoint have special limits? - -6. **Database Load:** What is the expected query load (requests per second) during peak usage? - ---- - -## Appendix: Alternative Approaches Considered - -### Alternative 1: Batch Endpoint for Goals Only - -**Approach:** Keep existing session fetching, add batch endpoint for goals -``` -GET /overarching-goals/batch?session_ids=id1,id2,id3 -``` - -**Pros:** -- Smaller change to existing architecture -- Easier to implement incrementally - -**Cons:** -- Still requires multiple API calls -- Doesn't solve the multi-relationship infinite loop issue -- Client still needs complex state management - -**Verdict:** Rejected in favor of single comprehensive endpoint - -### Alternative 2: GraphQL - -**Approach:** Implement GraphQL API allowing clients to request exact data needed -```graphql -query { - user(id: "123") { - todaysSessions { - id - date - overarchingGoal { title } - relationship { - coach { firstName lastName } - organization { name } - } - } - } -} -``` - -**Pros:** -- Extremely flexible -- Clients control data shape -- Industry standard for complex data fetching - -**Cons:** -- Requires new GraphQL infrastructure -- Higher implementation complexity -- Learning curve for team - -**Verdict:** Good long-term strategy, but REST endpoint is faster to implement - ---- - -## Revision History - -| Version | Date | Author | Changes | -|---------|------|--------|---------| -| 1.0 | 2025-10-23 | System Architecture Team | Initial specification | - diff --git a/docs/bug-analysis-link-popover-188.md b/docs/bug-analysis-link-popover-188.md deleted file mode 100644 index 6daac49d..00000000 --- a/docs/bug-analysis-link-popover-188.md +++ /dev/null @@ -1,401 +0,0 @@ -# Bug Analysis: Link Popover Issue #188 - -**Issue**: [GitHub Issue #188](https://github.com/refactor-group/refactor-platform-fe/issues/188) -**Version**: 1.0.0-beta2 -**Priority**: High (UX Impact) - -## Problem Summary - -The URL link popover exhibits three distinct issues: - -1. **Duplicate Popover Display**: Popover sometimes appears twice - once in the correct location and once in the upper-left corner -2. **Poor Link Selection UX**: Clicking existing links doesn't show popover above/below the link as expected -3. **Navigation Difficulties**: Cursor behavior doesn't match standard rich text editors (popover should only appear when cursor is directly on link text) - -## Root Cause Analysis - -### Issue 1: Duplicate Popover Rendering - -**Root Cause**: Portal positioning calculation issue in `popover.tsx:300-344` - -The `PopoverContent` component has complex logic for determining the portal container and calculating positions: - -```typescript -// popover.tsx:301-323 -const portalContainer = React.useMemo(() => { - if (!portal) return null; - if (container) return container; - - // Find closest positioned ancestor (not static) - const referenceElement = context.refs.reference.current; - if (!referenceElement) return document.body; - - if ('parentElement' in referenceElement) { - let element = referenceElement.parentElement; - while (element) { - const position = window.getComputedStyle(element).position; - if (position !== 'static') { - return element; - } - element = element.parentElement; - } - } - - return document.body; -}, [portal, container, context.refs.reference]); -``` - -**Problem**: -- The algorithm searches for a positioned ancestor, but when it finds one, it calculates relative positions (lines 326-344) -- However, FloatingUI already handles positioning through its middleware -- This creates a conflict where FloatingUI positions the element, then the component adjusts it again relative to the container -- Result: One popover at FloatingUI's calculated position, another at the adjusted position - -**Evidence**: -- `containerRef` is passed from `SimpleToolbar` (line 85 of simple-toolbar.tsx) to `LinkPopover` (line 209 of coaching-notes.tsx) -- The ref points to `.coaching-notes-editor` div -- The popover component tries to be "smart" about positioning within this container -- But the dual positioning logic creates the duplicate appearance - -### Issue 2: Popover Position When Editing Links - -**Root Cause**: Single positioning strategy in `link-popover.tsx:243-353` - -The `LinkPopover` component uses a fixed positioning strategy: - -```typescript -// link-popover.tsx:334-352 - - - - - - - - - -``` - -**Problem**: -- Popover is hardcoded to `side="bottom"` and `align="start"` -- This works for the toolbar button trigger -- But doesn't adapt when the user is editing an existing link in the text -- When a link is active in the editor, the popover should position relative to the link text, not the toolbar button - -**Missing Logic**: No detection of whether user is: -1. Creating a new link (position under toolbar button ✓) -2. Editing an existing link (should position near the link text ✗) - -### Issue 3: Auto-Open Behavior - -**Root Cause**: Aggressive auto-open logic in `link-popover.tsx:45-77` - -```typescript -// link-popover.tsx:49-59 -React.useEffect(() => { - if (!editor) return; - const { href } = editor.getAttributes("link"); - - if (editor.isActive("link") && url === null) { - setUrl(href || ""); - onLinkActive?.(); // Triggers popover open - } -}, [editor, onLinkActive, url]); - -// link-popover.tsx:61-77 -React.useEffect(() => { - if (!editor) return; - - const updateLinkState = () => { - const { href } = editor.getAttributes("link"); - setUrl(href || ""); - - if (editor.isActive("link") && url !== null) { - onLinkActive?.(); // Triggers popover open - } - }; - - editor.on("selectionUpdate", updateLinkState); - return () => editor.off("selectionUpdate", updateLinkState); -}, [editor, onLinkActive, url]); -``` - -**Problem**: -- Popover opens on **every** `selectionUpdate` event when cursor is on a link -- This happens even when user is just navigating through the document -- Standard rich text editor behavior: Only show popover when user **clicks** the link with intent to edit - -**Expected Behavior**: -- User navigating with arrow keys across a link → No popover -- User clicking on a link → Show popover for editing -- Alternative: Show link preview tooltip (non-interactive) during navigation, full popover only on explicit interaction - -## Impact Assessment - -| Issue | Severity | User Impact | Frequency | -|-------|----------|-------------|-----------| -| Duplicate Popover | High | Confusing UI, looks broken | Occasional | -| Wrong Position on Edit | Medium | Annoying but usable | Every link edit | -| Aggressive Auto-Open | High | Prevents navigation | Every cursor movement on links | - -## Proposed Solution - -### Phase 1: Fix Duplicate Popover (Critical) - -**Change**: Simplify portal positioning logic in `popover.tsx` - -**Current approach**: Find positioned ancestor + calculate relative position -**New approach**: Trust FloatingUI's positioning, only use container for boundary clipping - -```typescript -// popover.tsx - Simplified container logic -const portalContainer = React.useMemo(() => { - if (!portal) return null; - // Always use document.body for portal - // Use boundary prop for constraining, not container positioning - return document.body; -}, [portal]); - -// Remove the complex position adjustment (lines 326-344) -const enhancedStyle = React.useMemo(() => { - return { - position: context.strategy, - top: context.y ?? 0, - left: context.x ?? 0, - ...style, - }; -}, [context, style]); -``` - -**Rationale**: -- FloatingUI is designed to handle positioning with boundaries -- The `boundary` prop in `link-popover.tsx:318-322` should constrain the popover -- No need for custom position recalculation - -### Phase 2: Adaptive Popover Positioning (High Priority) - -**Change**: Detect context and position appropriately in `link-popover.tsx` - -```typescript -// New utility function -function getLinkPositionInEditor(editor: Editor): { x: number; y: number } | null { - if (!editor.isActive("link")) return null; - - const { state } = editor; - const { from, to } = state.selection; - const start = editor.view.coordsAtPos(from); - const end = editor.view.coordsAtPos(to); - - return { - x: start.left, - y: start.bottom - }; -} - -// In LinkPopover component -const linkPosition = React.useMemo(() => { - if (!editor || !editor.isActive("link")) return null; - return getLinkPositionInEditor(editor); -}, [editor, editor?.state.selection]); - -const popoverPositioning = React.useMemo(() => { - if (linkPosition) { - // Editing existing link - position near the link - return { - side: "bottom" as const, - align: "start" as const, - sideOffset: 4, - }; - } else { - // Creating new link - position under toolbar button - return { - side: "bottom" as const, - align: "start" as const, - sideOffset: 8, - }; - } -}, [linkPosition]); - -// Apply to PopoverContent - -``` - -**Alternative Approach**: Create two separate popovers: -- One triggered by toolbar button (current implementation) -- One triggered by link click in editor (new floating popover near link) - -This is cleaner separation of concerns and matches standard rich text editor patterns. - -### Phase 3: Improve Auto-Open Behavior (High Priority) - -**Change**: Only auto-open on explicit user action, not cursor movement - -```typescript -// Option A: Remove auto-open entirely -// Set autoOpenOnLinkActive={false} in simple-toolbar.tsx:85 - - -// Option B: Smarter detection - only open on click, not selection change -const useLinkHandler = (props: LinkHandlerProps) => { - const { editor, onSetLink, onLinkActive } = props; - const [url, setUrl] = React.useState(null); - const lastInteractionType = React.useRef<'click' | 'selection' | null>(null); - - React.useEffect(() => { - if (!editor) return; - - const handleTransaction = ({ transaction }: { transaction: any }) => { - // Detect if this was a click vs keyboard navigation - const wasMouse = transaction.getMeta('pointer'); - if (wasMouse && editor.isActive("link")) { - const { href } = editor.getAttributes("link"); - setUrl(href || ""); - onLinkActive?.(); - } - }; - - editor.on("transaction", handleTransaction); - return () => editor.off("transaction", handleTransaction); - }, [editor, onLinkActive]); - - // Remove the aggressive selectionUpdate listener (lines 61-77) - - // ... rest of implementation -}; - -// Option C: Add click handler to links in the editor -// In coaching-notes.tsx, enhance createLinkClickHandler: -const createLinkClickHandler = (onLinkClick?: (href: string) => void) => - (_view: unknown, event: Event) => { - const target = event.target as HTMLElement; - const mouseEvent = event as MouseEvent; - - if (isClickOnLink(target, mouseEvent)) { - if (mouseEvent.shiftKey) { - // Existing behavior - open in new tab - event.preventDefault(); - openLinkInNewTab(target); - return true; - } else { - // New behavior - trigger link edit popover - event.preventDefault(); - const href = getLinkHref(target); - onLinkClick?.(href); - return true; - } - } - return false; - }; -``` - -**Recommendation**: Combination of Option A + Option C -- Disable aggressive auto-open on selection changes -- Add explicit click handler for links in editor content -- Keep toolbar button as primary entry point for creating new links - -## Implementation Plan - -### Step 1: Fix Duplicate Popover (30 min) -**File**: `src/components/ui/tiptap-ui-primitive/popover/popover.tsx` - -- Simplify `portalContainer` logic (always use document.body) -- Remove complex position recalculation in `enhancedStyle` -- Test: Verify popover appears only once in correct location - -### Step 2: Disable Aggressive Auto-Open (15 min) -**File**: `src/components/ui/coaching-sessions/coaching-notes/simple-toolbar.tsx` - -- Add `autoOpenOnLinkActive={false}` prop to `LinkPopover` -- Test: Verify cursor can move across links without opening popover - -### Step 3: Add Click-to-Edit Behavior (45 min) -**Files**: -- `src/components/ui/coaching-sessions/coaching-notes.tsx` -- `src/components/ui/tiptap-ui/link-popover/link-popover.tsx` - -- Enhance `createLinkClickHandler` to detect normal clicks on links -- Add state management for "edit mode" in LinkPopover -- Position popover near clicked link (using TipTap's `coordsAtPos`) -- Test: Click on link → popover appears near link, not toolbar - -### Step 4: Improve Positioning Strategy (1 hour) -**File**: `src/components/ui/tiptap-ui/link-popover/link-popover.tsx` - -- Detect if editing existing link vs creating new link -- Use different positioning strategy for each case -- Consider implementing separate popovers for better separation -- Test: Both creation and editing workflows feel natural - -### Step 5: Polish & Edge Cases (30 min) -- Test rapid clicking, keyboard navigation -- Test on different screen sizes -- Test with floating toolbar visible/hidden -- Ensure accessibility (keyboard-only workflow) - -**Total Estimate**: 3 hours - -## Testing Checklist - -- [ ] Popover appears only once (no duplicate) -- [ ] Creating new link: Popover under toolbar button -- [ ] Editing existing link: Popover near link text -- [ ] Cursor navigation across links: No popover -- [ ] Click on link: Popover opens for editing -- [ ] Shift+click on link: Opens in new tab (existing) -- [ ] Keyboard-only workflow: Can create and edit links -- [ ] Floating toolbar: Link popover works when toolbar is floating -- [ ] Boundary constraints: Popover stays within editor container -- [ ] Rapid interactions: No visual glitches - -## Alternative Approaches Considered - -### Approach 1: Use TipTap's Built-in Link Extension -**Pros**: Battle-tested, maintained by TipTap team -**Cons**: Less customization, may not match design system -**Decision**: Rejected - Current implementation is nearly there, just needs fixes - -### Approach 2: Implement Bubble Menu for Links -**Pros**: Standard pattern in rich text editors, appears directly near content -**Cons**: Requires refactoring, loses toolbar button UX -**Decision**: Consider for v2 - Good long-term direction - -### Approach 3: Separate Toolbar Button and Inline Editor -**Pros**: Clear separation, easier to maintain -**Cons**: More components, more state management -**Decision**: Recommend for Step 4 if time permits - -## References - -- [TipTap Link Extension Docs](https://tiptap.dev/api/marks/link) -- [TipTap Bubble Menu](https://tiptap.dev/api/extensions/bubble-menu) -- [FloatingUI Positioning](https://floating-ui.com/docs/tutorial) -- [React Component Patterns for Rich Text](https://github.com/ianstormtaylor/slate/blob/main/site/examples/hovering-toolbar.tsx) - -## Success Criteria - -1. **No duplicate popovers**: Single popover instance, always correctly positioned -2. **Intuitive navigation**: Users can move cursor across links without popover appearing -3. **Clear editing workflow**: - - Toolbar button → Popover under button - - Click on link → Popover near link -4. **Performance**: No lag or visual glitches during interaction -5. **Accessibility**: Full keyboard navigation support maintained - ---- - -**Analysis Date**: 2025-10-15 -**Analyzed By**: Claude Code -**Status**: Ready for Implementation diff --git a/docs/prd-improved-coaching-notes-editor.md b/docs/prd-improved-coaching-notes-editor.md deleted file mode 100644 index 1513b497..00000000 --- a/docs/prd-improved-coaching-notes-editor.md +++ /dev/null @@ -1,526 +0,0 @@ -# Product Requirements Document -## Improved Coaching Notes TipTap Editor - -**Version:** 1.0 -**Date:** January 2025 -**Status:** Draft for Review -**Author:** Engineering Team - ---- - -## 1. Executive Summary - -### 1.1 Purpose -This PRD outlines the requirements for enhancing the TipTap-based rich text editor used for coaching notes within the Refactor Coaching Platform. The improvements focus on user experience, collaboration features, and professional formatting capabilities to better serve the needs of coaches and coachees during their sessions. - -### 1.2 Current State -The existing editor provides basic rich text editing with real-time collaboration via Y.js/Hocuspocus. While functional, it lacks advanced features that would enhance productivity and note organization during coaching sessions. - -### 1.3 Vision -Transform the coaching notes editor into a best-in-class collaborative writing tool that rivals modern document editors while maintaining focus on coaching-specific workflows and content structures. - ---- - -## 2. Problem Statement - -### 2.1 User Pain Points -1. **Limited Formatting Options**: Current toolbar lacks essential formatting tools (tables, task lists, quotes) -2. **No Template System**: Users start from blank slate each session -3. **Poor Mobile Experience**: Toolbar not optimized for touch interfaces -4. **Limited Keyboard Shortcuts**: Only basic shortcuts supported -5. **No AI Assistance**: Missing smart features like auto-summarization or action item extraction -6. **Weak Search/Navigation**: No find/replace or document outline -7. **Limited Export Options**: Cannot export notes in different formats - -### 2.2 Business Impact -- Reduced session productivity due to manual formatting -- Inconsistent note structure across sessions -- Difficulty in extracting actionable insights -- Poor accessibility for users with disabilities - ---- - -## 3. Goals & Success Metrics - -### 3.1 Primary Goals -1. **Enhance Productivity**: Reduce time spent on formatting by 40% -2. **Improve Collaboration**: Increase real-time collaboration usage by 60% -3. **Standardize Structure**: 80% of sessions use templates -4. **Mobile Optimization**: Achieve 90% feature parity on mobile - -### 3.2 Success Metrics -| Metric | Current | Target | Timeline | -|--------|---------|--------|----------| -| Editor Load Time | 2.5s | <1s | Q1 2025 | -| User Satisfaction (NPS) | 65 | 85 | Q2 2025 | -| Mobile Usage | 15% | 35% | Q2 2025 | -| Template Adoption | 0% | 80% | Q1 2025 | -| Accessibility Score (WCAG) | AA | AAA | Q1 2025 | - ---- - -## 4. User Personas & Use Cases - -### 4.1 Primary Personas - -#### Coach (Sarah) -- **Needs**: Quick note-taking, action item tracking, session summaries -- **Pain Points**: Repetitive formatting, manual template creation -- **Goals**: Focus on coaching, not documentation - -#### Coachee (Alex) -- **Needs**: Clear action items, searchable notes, progress tracking -- **Pain Points**: Finding past discussions, tracking commitments -- **Goals**: Clear understanding of next steps - -#### Manager (David) -- **Needs**: Progress reports, aggregate insights, team patterns -- **Pain Points**: Manual report generation, inconsistent formats -- **Goals**: Data-driven coaching outcomes - -### 4.2 Core Use Cases - -1. **Session Preparation** - - Load previous session notes - - Apply session template - - Review action items - -2. **During Session** - - Real-time collaborative editing - - Quick formatting via shortcuts - - Action item creation - - Link resources - -3. **Post-Session** - - Generate summary - - Export notes - - Create follow-up tasks - - Share with stakeholders - ---- - -## 5. Feature Requirements - -### 5.1 Enhanced Toolbar & Formatting - -#### Must Have (P0) -- [ ] **Extended Formatting** - - Tables with row/column management - - Block quotes for emphasis - - Task lists with checkboxes - - Inline code formatting - - Horizontal rules - - Text alignment (left, center, right, justify) - -- [ ] **Improved Link Management** - - Link preview on hover - - Auto-link detection - - Internal document linking (#headings) - - Link validation indicator - -- [ ] **Enhanced Lists** - - Nested list support (3+ levels) - - List style options (bullets, numbers, letters) - - Indent/outdent controls - - Convert between list types - -#### Should Have (P1) -- [ ] **Advanced Formatting** - - Callout/alert boxes (info, warning, success) - - Collapsible sections - - Footnotes - - Subscript/superscript - - Text color/background color - -- [ ] **Media Support** - - Image upload/embed - - Video embed (YouTube, Loom) - - File attachments - - Drawing/diagram tool integration - -#### Nice to Have (P2) -- [ ] Math equation support (LaTeX) -- [ ] Mermaid diagram support -- [ ] Code syntax for 20+ languages -- [ ] Custom emoji picker - -### 5.2 Template System - -#### Must Have (P0) -- [ ] **Pre-built Templates** - - Initial Session Template - - Regular Check-in Template - - Goal Review Template - - Retrospective Template - - Action Planning Template - -- [ ] **Template Management** - - Create custom templates - - Save as template from existing notes - - Template gallery/library - - Organization-level templates - - Personal templates - -- [ ] **Smart Placeholders** - - Date/time stamps - - Participant names - - Previous action items - - Goals reference - -#### Should Have (P1) -- [ ] Template versioning -- [ ] Template sharing between coaches -- [ ] Conditional template sections -- [ ] Template analytics (usage tracking) - -### 5.3 Collaboration Enhancements - -#### Must Have (P0) -- [ ] **Presence Indicators** - - Active user avatars - - Cursor colors per user - - User typing indicators - - Selection highlighting - -- [ ] **Comments & Annotations** - - Inline comments - - Threaded discussions - - Comment resolution - - @mentions with notifications - -#### Should Have (P1) -- [ ] **Version History** - - Auto-save with timestamps - - Version comparison/diff view - - Restore previous versions - - Change attribution - -- [ ] **Collaboration Controls** - - Read-only mode - - Suggestion mode - - Section locking - - Permission levels - -### 5.4 AI-Powered Features - -#### Must Have (P0) -- [ ] **Smart Summarization** - - Auto-generate session summary - - Key points extraction - - Action items detection - - Decision highlights - -#### Should Have (P1) -- [ ] **Content Assistance** - - Grammar/spell check - - Tone suggestions - - Sentence completion - - Professional rephrasing - -- [ ] **Insights Generation** - - Pattern recognition across sessions - - Progress tracking - - Goal alignment analysis - - Coaching effectiveness metrics - -#### Nice to Have (P2) -- [ ] Meeting transcription integration -- [ ] Sentiment analysis -- [ ] Auto-tagging/categorization -- [ ] Predictive text based on context - -### 5.5 Navigation & Search - -#### Must Have (P0) -- [ ] **Document Outline** - - Auto-generated TOC from headings - - Click to navigate - - Collapsible outline panel - - Current position indicator - -- [ ] **Find & Replace** - - Case-sensitive option - - Whole word matching - - Regular expression support - - Replace all functionality - -#### Should Have (P1) -- [ ] **Cross-Session Search** - - Search across all notes - - Filter by date/participant - - Search in comments - - Saved searches - -- [ ] **Quick Navigation** - - Go to line/section - - Bookmark positions - - Recent edits jump - - Split view mode - -### 5.6 Mobile Optimization - -#### Must Have (P0) -- [ ] **Responsive Toolbar** - - Collapsible toolbar - - Touch-optimized buttons - - Swipe gestures for formatting - - Context menu on selection - -- [ ] **Mobile-First Features** - - Voice-to-text input - - Simplified formatting palette - - Thumb-friendly controls - - Offline mode with sync - -#### Should Have (P1) -- [ ] Handwriting recognition -- [ ] Photo capture for whiteboard -- [ ] Mobile-specific templates -- [ ] Gesture-based undo/redo - -### 5.7 Accessibility & Internationalization - -#### Must Have (P0) -- [ ] **WCAG AAA Compliance** - - Full keyboard navigation - - Screen reader support - - High contrast mode - - Focus indicators - - ARIA labels - -- [ ] **Internationalization** - - RTL language support - - Unicode support - - Locale-specific formatting - - Translation-ready UI - -#### Should Have (P1) -- [ ] Voice commands -- [ ] Customizable shortcuts -- [ ] Dyslexia-friendly fonts -- [ ] Reading mode - -### 5.8 Import/Export - -#### Must Have (P0) -- [ ] **Export Formats** - - PDF with formatting - - Markdown - - Plain text - - HTML - - DOCX - -- [ ] **Import Support** - - Markdown files - - Plain text - - DOCX (basic) - - Copy/paste with formatting - -#### Should Have (P1) -- [ ] Notion export -- [ ] Google Docs sync -- [ ] Email integration -- [ ] Print preview/settings - ---- - -## 6. Technical Architecture - -### 6.1 Component Structure -```typescript -CoachingNotesEditor/ -├── EditorCore/ -│ ├── TipTapProvider -│ ├── CollaborationProvider -│ └── EditorContent -├── Toolbar/ -│ ├── FormattingTools/ -│ ├── InsertMenu/ -│ ├── AITools/ -│ └── ViewControls/ -├── Sidebar/ -│ ├── DocumentOutline/ -│ ├── Comments/ -│ ├── VersionHistory/ -│ └── Templates/ -├── StatusBar/ -│ ├── WordCount -│ ├── SaveStatus -│ └── CollaborationStatus -└── Dialogs/ - ├── LinkDialog/ - ├── ImageDialog/ - ├── TableDialog/ - └── TemplateDialog/ -``` - -### 6.2 Extension Architecture -```typescript -// Core Extensions (existing) -- Document, Paragraph, Text, Bold, Italic, etc. - -// New Extensions -- TaskList & TaskItem -- Table, TableRow, TableCell, TableHeader -- Mention -- Comment -- Template -- SmartSummary -- DocumentOutline -- FindReplace -``` - -### 6.3 Performance Requirements -- Initial render: <500ms -- Typing latency: <50ms -- Collaboration sync: <200ms -- Auto-save interval: 5 seconds -- Bundle size increase: <100KB gzipped - -### 6.4 Data Model Updates -```typescript -interface CoachingNote { - id: string; - sessionId: string; - content: JSONContent; // TipTap format - template?: TemplateReference; - metadata: { - wordCount: number; - lastEditedBy: string; - lastEditedAt: Date; - version: number; - }; - comments: Comment[]; - actionItems: ActionItem[]; - summary?: AISummary; -} -``` - ---- - -## 7. Design Specifications - -### 7.1 Visual Design Principles -- **Clean & Focused**: Minimal UI that doesn't distract from content -- **Contextual Controls**: Show relevant tools based on selection -- **Consistent Spacing**: 8px grid system -- **Accessible Colors**: WCAG AAA contrast ratios - -### 7.2 Responsive Breakpoints -- Mobile: 320px - 768px -- Tablet: 768px - 1024px -- Desktop: 1024px+ - -### 7.3 Interactive States -- Default, Hover, Active, Focus, Disabled -- Loading states for async operations -- Error states with recovery actions - ---- - -## 8. Implementation Phases - -### Phase 1: Foundation (Week 1-3) -- Enhanced toolbar with new formatting options -- Template system implementation -- Mobile-responsive toolbar -- Basic keyboard shortcuts - -### Phase 2: Collaboration (Week 4-5) -- Comments system -- Enhanced presence indicators -- Version history -- Mention functionality - -### Phase 3: Intelligence (Week 6-7) -- AI summarization -- Smart action item extraction -- Content suggestions -- Pattern recognition - -### Phase 4: Polish (Week 8) -- Performance optimization -- Accessibility audit & fixes -- Cross-browser testing -- Documentation - ---- - -## 9. Testing Strategy - -### 9.1 Test Coverage Requirements -- Unit tests: 90% coverage -- Integration tests: Key workflows -- E2E tests: Critical paths -- Performance tests: Load time, typing latency -- Accessibility tests: WCAG AAA compliance - -### 9.2 Browser Support -- Chrome 90+ -- Firefox 88+ -- Safari 14+ -- Edge 90+ -- Mobile Safari (iOS 14+) -- Chrome Mobile (Android 10+) - ---- - -## 10. Risks & Mitigation - -| Risk | Impact | Probability | Mitigation | -|------|--------|-------------|------------| -| Bundle size increase | High | Medium | Code splitting, lazy loading | -| Collaboration conflicts | High | Low | Operational transformation, CRDT | -| AI feature latency | Medium | Medium | Caching, background processing | -| Mobile performance | High | Medium | Progressive enhancement | -| Template complexity | Low | High | Phased rollout, user training | - ---- - -## 11. Open Questions - -1. **AI Provider**: OpenAI, Anthropic, or custom model? -2. **File Storage**: Where to store uploaded images/files? -3. **Template Marketplace**: Allow public template sharing? -4. **Offline Support**: Full offline capability or read-only? -5. **Integration Priorities**: Which third-party tools to integrate first? - ---- - -## 12. Success Criteria - -The project will be considered successful when: -1. ✅ 90% of users adopt at least one template -2. ✅ Mobile usage increases to 35% -3. ✅ Average session note quality score improves by 40% -4. ✅ Zero critical accessibility issues -5. ✅ Page load time under 1 second -6. ✅ User satisfaction (NPS) reaches 85 - ---- - -## Appendices - -### A. Competitive Analysis -- Notion: Excellent templates, blocks system -- Google Docs: Superior collaboration, comments -- Obsidian: Great linking, markdown support -- Roam Research: Powerful references, daily notes - -### B. User Research Data -- Survey results from 50 coaches -- Session recordings analysis -- Feature request prioritization - -### C. Technical Dependencies -- TipTap 2.x ecosystem -- Y.js for CRDT -- Hocuspocus for collaboration server -- AI API integration requirements - ---- - -**Document Control:** -- Review Cycle: Bi-weekly -- Stakeholders: Product, Engineering, Design, Customer Success -- Next Review: February 1, 2025 \ No newline at end of file diff --git a/docs/ui-mockups-toolbar-design.md b/docs/ui-mockups-toolbar-design.md deleted file mode 100644 index 5cc6b656..00000000 --- a/docs/ui-mockups-toolbar-design.md +++ /dev/null @@ -1,604 +0,0 @@ -# UI Design Mockups: Enhanced Coaching Notes Toolbar -## Based on TipTap SimpleEditor Template - ---- - -## 1. Design Overview - -### 1.1 Design Principles -- **Progressive Disclosure**: Show common tools first, advanced features on-demand -- **Context-Aware**: Toolbar adapts based on selection and content type -- **Mobile-First**: Touch-optimized with responsive breakpoints -- **Coaching-Focused**: Quick access to session-specific tools - -### 1.2 Toolbar Architecture -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Primary Toolbar (Always Visible) │ -├─────────────────────────────────────────────────────────────────┤ -│ Secondary Toolbar (Contextual) │ -└─────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 2. Desktop Toolbar Design - -### 2.1 Primary Toolbar - Default State -``` -┌──────────────────────────────────────────────────────────────────────────────┐ -│ ┌──────────┬───────────────┬──────────────┬────────────┬─────────┬────────┐ │ -│ │ Template │ Text Format │ Lists │ Insert │ Coach │ More │ │ -│ │ ▼ │ B I U S ─ 🎨 │ • 1. ☑ │ 🔗 📷 📊 │ 💡 ✅ │ ⋯ │ │ -│ └──────────┴───────────────┴──────────────┴────────────┴─────────┴────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────────────────┐ │ -│ │ Editor Content Area │ │ -│ │ │ │ -│ └──────────────────────────────────────────────────────────────────────┘ │ -└──────────────────────────────────────────────────────────────────────────────┘ -``` - -### 2.2 Detailed Toolbar Groups - -#### Group 1: Template Selector (Coaching-Specific) -``` -┌─────────────────┐ -│ 📝 Template ▼ │ ← Click to expand -└─────────────────┘ - ↓ -┌──────────────────────────┐ -│ 🎯 Initial Session │ -│ 📅 Regular Check-in │ -│ 🎖️ Goal Review │ -│ 🔄 Retrospective │ -│ 📋 Action Planning │ -│ ───────────────────── │ -│ ➕ Create Custom... │ -│ 📁 Manage Templates... │ -└──────────────────────────┘ -``` - -#### Group 2: Text Formatting -``` -┌─────────────────────────────────┐ -│ B │ I │ U │ S │ H │ 🎨│ ← Individual toggle buttons -└─────────────────────────────────┘ - Bold Italic Under Strike High Color - -Color Picker Dropdown: -┌──────────────────┐ -│ Text Color │ -│ ● ● ● ● ● ● ● │ -│ ● ● ● ● ● ● ● │ -│ Background │ -│ ○ ○ ○ ○ ○ ○ ○ │ -└──────────────────┘ -``` - -#### Group 3: Paragraph Formatting -``` -┌──────────────────────────────┐ -│ ¶ Normal ▼ │ ≡ │ ≡ │ ≡ │ ≡ │ -└──────────────────────────────┘ - ↓ Left Center Right Justify -┌─────────────┐ -│ ¶ Paragraph │ -│ H1 Title │ -│ H2 Heading │ -│ H3 Subhead │ -│ " Quote │ -│ 💡 Callout │ -└─────────────┘ -``` - -#### Group 4: Lists & Tasks -``` -┌────────────────────┐ -│ • │ 1. │ ☑ │ │ ← List types -└────────────────────┘ -Bullet Ordered Tasks - -Task List Special Features: -- Auto-converts "- [ ]" to checkbox -- Keyboard shortcut: Cmd+Shift+X -- Supports nested tasks -``` - -#### Group 5: Insert Menu -``` -┌─────────────────────┐ -│ 🔗 │ 📷 │ 📊 │ ← Insert elements -└─────────────────────┘ - Link Image Table - -Extended Insert (via dropdown): -┌────────────────────┐ -│ 🔗 Link │ -│ 📷 Image │ -│ 📊 Table │ -│ ───────────── │ -│ 📹 Video │ -│ 💻 Code Block │ -│ ➖ Divider │ -│ 📝 Note Box │ -│ ⚠️ Alert Box │ -└────────────────────┘ -``` - -#### Group 6: Coaching Tools (Unique Features) -``` -┌──────────────────────────┐ -│ 💡 │ ✅ │ 👥 │ 📌 │ -└──────────────────────────┘ -Action Summary Mention Bookmark - -Action Item Quick Add: -┌─────────────────────────────┐ -│ ✅ Create Action Item │ -│ ┌─────────────────────────┐ │ -│ │ Description... │ │ -│ └─────────────────────────┘ │ -│ Owner: [Select...] Due: []│ -│ [Create] [Cancel] │ -└─────────────────────────────┘ -``` - -### 2.3 Contextual/Floating Toolbar -``` -When text is selected: -┌─────────────────────────────────────┐ -│ B I U │ 🔗 │ 💬 │ 🎨 │ ⋯ │ -└─────────────────────────────────────┘ - ↑ - Appears above selection - -When hovering over link: -┌──────────────────────────────┐ -│ 🔗 example.com │ -│ [Edit] [Unlink] [Open] │ -└──────────────────────────────┘ -``` - ---- - -## 3. Mobile Toolbar Design - -### 3.1 Mobile Primary Toolbar (Collapsed) -``` -┌─────────────────────────────────┐ -│ ┌───┬─────────────────────┬───┐ │ -│ │ ≡ │ Coaching Notes │ ⋯ │ │ -│ └───┴─────────────────────┴───┘ │ -│ ┌─────────────────────────────┐ │ -│ │ B I U │ • │ 🔗 │ 💡 │ ▶ │ │ -│ └─────────────────────────────┘ │ -└─────────────────────────────────┘ - Menu Essential Tools More -``` - -### 3.2 Mobile Expanded Toolbar (Swipe Up) -``` -┌─────────────────────────────────┐ -│ ┌─────────────────────────────┐ │ -│ │ Text Formatting │ │ -│ │ B I U S H 🎨 │ │ -│ └─────────────────────────────┘ │ -│ ┌─────────────────────────────┐ │ -│ │ Lists │ │ -│ │ • 1. ☑ │ │ -│ └─────────────────────────────┘ │ -│ ┌─────────────────────────────┐ │ -│ │ Insert │ │ -│ │ 🔗 📷 📊 💻 │ │ -│ └─────────────────────────────┘ │ -│ ┌─────────────────────────────┐ │ -│ │ Coaching Tools │ │ -│ │ 💡 ✅ 👥 📌 │ │ -│ └─────────────────────────────┘ │ -└─────────────────────────────────┘ -``` - -### 3.3 Mobile Context Menu (Long Press) -``` -On text selection: -┌─────────────────┐ -│ Cut │ -│ Copy │ -│ Paste │ -│ ─────────── │ -│ Bold │ -│ Italic │ -│ Link... │ -│ Comment... │ -│ ─────────── │ -│ Create Action │ -└─────────────────┘ -``` - ---- - -## 4. Advanced Features UI - -### 4.1 AI Assistant Panel -``` -┌──────────────────────────────────────────────┐ -│ 🤖 AI Assistant [×] │ -├──────────────────────────────────────────────┤ -│ Quick Actions: │ -│ ┌──────────────┬──────────────┐ │ -│ │ Summarize │ Extract │ │ -│ │ Session │ Actions │ │ -│ └──────────────┴──────────────┘ │ -│ ┌──────────────┬──────────────┐ │ -│ │ Improve │ Generate │ │ -│ │ Writing │ Follow-up │ │ -│ └──────────────┴──────────────┘ │ -│ │ -│ Custom Prompt: │ -│ ┌────────────────────────────────┐ │ -│ │ Ask AI anything... │ │ -│ └────────────────────────────────┘ │ -│ [Send] │ -└──────────────────────────────────────────────┘ -``` - -### 4.2 Template Builder Interface -``` -┌──────────────────────────────────────────────────────┐ -│ Template Builder [×] │ -├──────────────────────────────────────────────────────┤ -│ Template Name: [____________________] │ -│ │ -│ Sections: │ -│ ┌──────────────────────────────────────────────┐ │ -│ │ 1. Meeting Objectives [⚙️] │ │ -│ │ Placeholder: {{session.goals}} │ │ -│ └──────────────────────────────────────────────┘ │ -│ ┌──────────────────────────────────────────────┐ │ -│ │ 2. Discussion Points [⚙️] │ │ -│ │ • {{auto.previous_actions}} │ │ -│ └──────────────────────────────────────────────┘ │ -│ ┌──────────────────────────────────────────────┐ │ -│ │ 3. Action Items [⚙️] │ │ -│ │ ☑ Task list here... │ │ -│ └──────────────────────────────────────────────┘ │ -│ │ -│ [+ Add Section] │ -│ │ -│ Available Variables: │ -│ {{date}} {{coach.name}} {{coachee.name}} │ -│ {{session.number}} {{goals.current}} │ -│ │ -│ [Save Template] [Preview] [Cancel] │ -└──────────────────────────────────────────────────────┘ -``` - -### 4.3 Collaboration Sidebar -``` -┌─────────────────────┐ -│ Collaborators 2 │ -├─────────────────────┤ -│ 👤 Sarah Chen │ -│ 🟢 Active now │ -│ Cursor: Line 42 │ -│ │ -│ 👤 Alex Kumar │ -│ 🟡 Idle (2 min) │ -│ Last: Line 18 │ -├─────────────────────┤ -│ Comments 3 │ -├─────────────────────┤ -│ 💬 Sarah: "Let's │ -│ focus on the Q2 │ -│ goals here" │ -│ └─ Reply... │ -│ │ -│ 💬 Alex: "Added │ -│ action items" │ -│ └─ ✓ Resolved │ -└─────────────────────┘ -``` - ---- - -## 5. Responsive Behavior - -### 5.1 Breakpoint Behaviors -``` -Desktop (>1024px): -- Full toolbar with all groups visible -- Floating toolbar on selection -- Sidebars for outline/comments - -Tablet (768-1024px): -- Grouped toolbar with dropdowns -- Essential tools visible, rest in "More" -- Collapsible sidebars - -Mobile (<768px): -- Minimal toolbar with expandable drawer -- Context menu on long-press -- Full-screen editing mode option -``` - -### 5.2 Touch Gestures -``` -Swipe Right → Undo -Swipe Left → Redo -Swipe Up → Show toolbar -Swipe Down → Hide toolbar -Pinch → Zoom text -Long Press → Context menu -Double Tap → Select word -Triple Tap → Select paragraph -``` - ---- - -## 6. Keyboard Shortcuts Overlay - -### 6.1 Shortcut Helper (? or Cmd+/) -``` -┌──────────────────────────────────────────────┐ -│ Keyboard Shortcuts [×] │ -├──────────────────────────────────────────────┤ -│ Text Formatting │ -│ Cmd+B Bold │ -│ Cmd+I Italic │ -│ Cmd+U Underline │ -│ Cmd+Shift+S Strikethrough │ -│ Cmd+Shift+H Highlight │ -│ │ -│ Coaching Features │ -│ Cmd+Shift+A Create Action Item │ -│ Cmd+Shift+M Insert @Mention │ -│ Cmd+E Generate Summary │ -│ Cmd+T Apply Template │ -│ │ -│ Navigation │ -│ Cmd+F Find in Document │ -│ Cmd+G Go to Next │ -│ Cmd+[ Decrease Indent │ -│ Cmd+] Increase Indent │ -└──────────────────────────────────────────────┘ -``` - ---- - -## 7. Status Bar Design - -### 7.1 Desktop Status Bar -``` -┌────────────────────────────────────────────────────────────┐ -│ 📝 1,247 words │ 5 min read │ 💾 Saved │ 👥 2 active │ ⚡ │ -└────────────────────────────────────────────────────────────┘ - Word count Read time Save status Collaborators AI -``` - -### 7.2 Mobile Status Bar (Minimal) -``` -┌──────────────────────────┐ -│ 1.2k words │ 💾 │ 👥 2 │ -└──────────────────────────┘ -``` - ---- - -## 8. Theme Variations - -### 8.1 Light Theme -```css -/* Toolbar */ ---toolbar-bg: #FFFFFF; ---toolbar-border: #E5E7EB; ---button-bg: transparent; ---button-hover: #F3F4F6; ---button-active: #3B82F6; ---text-primary: #111827; ---text-secondary: #6B7280; - -/* Editor */ ---editor-bg: #FFFFFF; ---editor-text: #111827; ---selection-bg: #DBEAFE; -``` - -### 8.2 Dark Theme -```css -/* Toolbar */ ---toolbar-bg: #1F2937; ---toolbar-border: #374151; ---button-bg: transparent; ---button-hover: #374151; ---button-active: #3B82F6; ---text-primary: #F9FAFB; ---text-secondary: #9CA3AF; - -/* Editor */ ---editor-bg: #111827; ---editor-text: #F9FAFB; ---selection-bg: #1E3A8A; -``` - -### 8.3 High Contrast (Accessibility) -```css -/* Maximum contrast for visibility */ ---toolbar-bg: #000000; ---toolbar-border: #FFFFFF; ---button-bg: #000000; ---button-hover: #FFFFFF; ---button-active: #FFFF00; ---text-primary: #FFFFFF; ---text-secondary: #FFFF00; -``` - ---- - -## 9. Animation & Micro-interactions - -### 9.1 Toolbar Animations -``` -Button Press: -- Scale: 0.95 -- Duration: 150ms -- Easing: ease-out - -Dropdown Open: -- Slide down: 200ms -- Fade in: 150ms -- Easing: cubic-bezier(0.4, 0, 0.2, 1) - -Tool Switch: -- Morph transition: 200ms -- Color fade: 150ms -``` - -### 9.2 Feedback Animations -``` -Action Item Created: -✅ → Scale from 0 → Bounce → Settle (400ms) - -Auto-save Indicator: -💾 → Pulse (2s interval) → Checkmark → Fade - -Collaboration Cursor: -- Smooth position: 16ms updates -- Name tag fade: 200ms -- Color pulse on typing -``` - ---- - -## 10. Implementation Components - -### 10.1 Component Structure (React/TypeScript) -```typescript -// Main Toolbar Component - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -### 10.2 Responsive Container -```typescript -const ResponsiveToolbar = () => { - const isMobile = useMediaQuery('(max-width: 768px)'); - const isTablet = useMediaQuery('(max-width: 1024px)'); - - if (isMobile) { - return ; - } - - if (isTablet) { - return ; - } - - return ; -}; -``` - ---- - -## 11. Accessibility Features - -### 11.1 ARIA Labels -```html - - -
- -
-``` - -### 11.2 Keyboard Navigation -- Tab: Navigate between toolbar groups -- Arrow keys: Navigate within groups -- Enter/Space: Activate button -- Escape: Close dropdowns/dialogs -- F10: Focus toolbar - ---- - -## 12. Performance Optimizations - -### 12.1 Lazy Loading -```typescript -// Load advanced features on-demand -const AIAssistant = lazy(() => import('./AIAssistant')); -const TemplateBuilder = lazy(() => import('./TemplateBuilder')); -const TableEditor = lazy(() => import('./TableEditor')); -``` - -### 12.2 Virtualization -- Virtual scrolling for long documents -- Render only visible toolbar buttons -- Defer non-critical UI updates - ---- - -## Summary - -This comprehensive toolbar design leverages the TipTap SimpleEditor template while adding coaching-specific features: - -**Key Enhancements:** -1. **Template System** - Quick access to coaching templates -2. **Coaching Tools** - Action items, summaries, mentions -3. **Mobile-First** - Touch-optimized with gestures -4. **AI Integration** - Smart assistance panel -5. **Collaboration** - Real-time presence and comments -6. **Accessibility** - WCAG AAA compliant -7. **Performance** - Lazy loading and virtualization - -The design maintains the simplicity of the SimpleEditor while providing powerful features for professional coaching sessions. The progressive disclosure pattern ensures new users aren't overwhelmed while power users have quick access to advanced features. \ No newline at end of file