diff --git a/src/components/dashboard/entity-info-drawer/expandable-list.tsx b/src/components/dashboard/entity-info-drawer/expandable-list.tsx new file mode 100644 index 00000000..4bd43e81 --- /dev/null +++ b/src/components/dashboard/entity-info-drawer/expandable-list.tsx @@ -0,0 +1,64 @@ +'use client' + +import { useState, type FC } from 'react' +import { cn } from '@/lib/utils' + +/** + * The expand/collapse pair behind every truncated list in the entity info + * drawer (GOAL-315). Before this, a list that overflowed its cap rendered a + * dead "+ N more" label: it told the user content existed but gave them no + * way to reach it. + * + * The closest precedent in the app is `active-pulses.tsx`, which flips + * between "View all" and "Show less" — except there the state is owned by + * the parent route. Here each drawer section owns its own. + */ + +/** Collapse state for one truncated list. */ +export function useExpandableList(items: T[], limit: number) { + const [expanded, setExpanded] = useState(false) + return { + visible: expanded ? items : items.slice(0, limit), + hiddenCount: Math.max(0, items.length - limit), + expanded, + toggle: () => setExpanded((prev) => !prev), + } +} + +/** + * Interactive "+ N more" / "Show less" toggle. The visible string is the + * accessible name (WCAG 2.5.3 Label in Name) with the item noun appended + * off-screen, so "click show less" spoken by a voice-input user matches. + */ +export const ShowMoreToggle: FC<{ + expanded: boolean + hiddenCount: number + onToggle: () => void + /** Plural noun appended to the accessible name, e.g. "pulses". */ + itemLabel: string + className?: string +}> = ({ expanded, hiddenCount, onToggle, itemLabel, className }) => ( + +) diff --git a/src/components/dashboard/entity-info-drawer/field-context-details-body.tsx b/src/components/dashboard/entity-info-drawer/field-context-details-body.tsx index 094f5a2a..df75673b 100644 --- a/src/components/dashboard/entity-info-drawer/field-context-details-body.tsx +++ b/src/components/dashboard/entity-info-drawer/field-context-details-body.tsx @@ -12,7 +12,6 @@ import { Users, Waves, } from 'lucide-react' -import { cn } from '@/lib/utils' import { GET_FIELD_CONTEXT_DETAILS } from '@/app/graphql/queries/FIELD_CONTEXT_DETAILS_QUERIES' import { GET_FIELD_CONTEXT_PEOPLE } from '@/app/graphql/queries/FIELD_CONTEXT_PEOPLE_QUERIES' import { GET_DOCUMENTS_BY_FIELD_CONTEXT } from '@/app/graphql/queries/DOCUMENT_QUERIES' @@ -20,7 +19,6 @@ import { UPDATE_FIELD_CONTEXT_MUTATION, LOG_FIELD_ACTIVITY, } from '@/app/graphql/mutations' -import { formatResonanceLabel } from '@/utils/graph-utils' import { BodySkeleton, EditCta, @@ -29,9 +27,16 @@ import { ErrorBody, NotFoundBody, PrimaryCta, - SectionHeader, StatCell, } from './shared' +import { + DocumentsSection, + PeopleSection, + PulsesSection, + ResonancesSection, + type FieldContextDocument, + type FieldContextPerson, +} from './field-context-sections' import { dispatchOpenInfoDrawer } from './types' /** @@ -61,14 +66,7 @@ export const FieldContextDetailsBody: FC<{ const { data: peopleData } = useQuery<{ fieldContexts?: { id: string - people?: { - id: string - name: string | null - firstName: string | null - lastName: string | null - email: string | null - photo: string | null - }[] + people?: FieldContextPerson[] }[] }>(GET_FIELD_CONTEXT_PEOPLE, { variables: { contextId }, @@ -76,11 +74,7 @@ export const FieldContextDetailsBody: FC<{ }) const { data: docsData } = useQuery<{ - documentsByFieldContext?: { - id: string - filename: string - uploadedAt: string - }[] + documentsByFieldContext?: FieldContextDocument[] }>(GET_DOCUMENTS_BY_FIELD_CONTEXT, { variables: { fieldContextId: contextId }, fetchPolicy: 'cache-and-network', @@ -237,163 +231,13 @@ export const FieldContextDetailsBody: FC<{ /> - {allPulses.length > 0 && ( -
- Recent pulses ({allPulses.length}) - -
- )} + - {resonances.length > 0 && ( -
- Resonances ({resonances.length}) -
    - {resonances.slice(0, 6).map((res) => { - const src = res.source?.[0] - const tgt = res.target?.[0] - const srcTitle = - (src && 'title' in src ? src.title : undefined) ?? 'Pulse' - const tgtTitle = - (tgt && 'title' in tgt ? tgt.title : undefined) ?? 'Pulse' - return ( -
  • - -
  • - ) - })} - {resonances.length > 6 && ( -
  • - + {resonances.length - 6} more -
  • - )} -
-
- )} + - {people.length > 0 && ( -
- People ({people.length}) -
    - {people.slice(0, 8).map((person) => { - const name = - person.name?.trim() || - `${person.firstName ?? ''} ${person.lastName ?? ''}`.trim() || - 'Member' - return ( -
  • - -
  • - ) - })} -
-
- )} + - {documents.length > 0 && ( -
- Documents ({documents.length}) -
    - {documents.slice(0, 5).map((doc) => ( -
  • - -
  • - ))} - {documents.length > 5 && ( -
  • - + {documents.length - 5} more -
  • - )} -
-
- )} +