Skip to content
kerfjsGitHub

CommentRail

A margin of comment threads beside a document. On a phone it becomes a sheet.

SourcePackage @kerf-js/veneer

Preview

tsx
import { state } from '@kerf-js/core'import {  CommentAnchor,  CommentRail,  Heading,  Row,  Stack,  Text} from '@kerf-js/veneer'import type { CommentThreadData } from '@kerf-js/veneer'import type { Peer } from '@kerf-js/veneer'export function Document() {  const active = state<string | null>('c-timeouts')  const threads = state<CommentThreadData[]>([    COMMENT_TIMEOUTS,    COMMENT_OWNER,  ])  const reply = (id: string, text: string) =>    (threads.value = threads.value.map((t) =>      t.id === id        ? {            ...t,            comments: [...t.comments, newComment(text)],          }        : t,    ))  return (    <Row      gap="group"      align="start"      reflow="tablet"    >      <Stack gap="related">        <Heading level={3}>Rollback runbook</Heading>        <Text>          The cache keeps a page for{' '}          <CommentAnchor            thread="c-timeouts"            active={active}          >            60 seconds          </CommentAnchor>{' '}          after a deploy, so a rollback can take a minute          to reach everyone.        </Text>        <Text>          <CommentAnchor            thread="c-owner"            active={active}          >            The person on call          </CommentAnchor>{' '}          decides, and tells support once it is done.        </Text>      </Stack>      <CommentRail        threads={threads}        active={active}        onReply={reply}      />    </Row>  )}const PEOPLE = [  {    id: 'mara',    name: 'Mara Lindqvist',    email: 'mara@kerf.dev',    role: 'Owner',    team: 'Platform',    initials: 'ML',  },  {    id: 'jun',    name: 'Jun Tanaka',    email: 'jun@kerf.dev',    role: 'Admin',    team: 'Platform',    initials: 'JT',  },  {    id: 'priya',    name: 'Priya Raman',    email: 'priya@kerf.dev',    role: 'Member',    team: 'Web',    initials: 'PR',  },  {    id: 'tomas',    name: 'Tomás Oliveira',    email: 'tomas@kerf.dev',    role: 'Member',    team: 'Web',    initials: 'TO',  },  {    id: 'lena',    name: 'Lena Fischer',    email: 'lena@kerf.dev',    role: 'Member',    team: 'Data',    initials: 'LF',  },  {    id: 'noor',    name: 'Noor Haddad',    email: 'noor@kerf.dev',    role: 'Viewer',    team: 'Data',    initials: 'NH',  },  {    id: 'sam',    name: 'Sam Okafor',    email: 'sam@kerf.dev',    role: 'Member',    team: 'Mobile',    initials: 'SO',  },  {    id: 'ines',    name: 'Inés Vidal',    email: 'ines@kerf.dev',    role: 'Viewer',    team: 'Mobile',    initials: 'IV',  },]const PEERS: Peer[] = PEOPLE.map((p, i) => ({  id: p.id,  name: p.name,  color: i + 1,}))const [  MARA,  JUN,  PRIYA,  TOMAS,  LENA,  NOOR,  SAM,  INES,] = PEERS as [  Peer,  Peer,  Peer,  Peer,  Peer,  Peer,  Peer,  Peer,]const COMMENT_TIMEOUTS: CommentThreadData = {  id: 'c-timeouts',  comments: [    {      id: 'a',      author: LENA,      time: '10:12',      text:        'Should this say 30 seconds? ' +        'The stale window changed on ' +        'Friday.',    },    {      id: 'b',      author: JUN,      time: '10:20',      text:        'Yes. d-4817 lowered it, I ' +        'will update the number.',    },  ],}const COMMENT_OWNER: CommentThreadData = {  id: 'c-owner',  comments: [    {      id: 'c',      author: PRIYA,      time: 'Mon',      text:        'Who signs off on the rollback ' +        'rule, Mara or the on-call ' +        'person?',    },  ],}const newComment = (text: string) => ({  id: text,  author: MARA,  time: 'Now',  text,})

Installation

npm install @kerf-js/veneer

Usage

tsx
import { CommentRail } from '@kerf-js/veneer'

Accessibility

  • This has no shortcuts of its own: controls inside it use Tab, Enter and Space as usual, and veneer's parts keep their roles and labels.

API Reference

PropTypeDefaultDescription
threadsLive<CommentThreadData[]>—The threads.
activeState<string | null>—The open thread, shared with its CommentAnchor.
labelstring'Comments'Names the landmark.
EventTypeDefaultDescription
onReply((thread: string, text: string) => void)—Called with the thread and the text.
onResolve((thread: string, resolved: boolean) => void)—Called when a thread is resolved or reopened.

RelatedComment anchorComment thread