Skip to content
kerfjsGitHub

EventEditor

A side sheet with every field of an event: title, time, repeats, zone, place.

SourcePackage @kerf-js/veneer

Preview

tsx
import { state } from '@kerf-js/core'import {  Button,  Card,  EventEditor,  Row,  Stack,  Text} from '@kerf-js/veneer'import type { CalEvent } from '@kerf-js/veneer-core'export function Change() {  const open = state(false)  const event = state(REVIEW)  return (    <Card      title={event.value.title}      sweep={false}    >      <Stack gap="related">        <Text subtle>          {when(event.value)}          {event.value.location            ? `, ${event.value.location}`            : ''}        </Text>        <Row>          <Button            emphasis="outline"            tone="neutral"            icon="settings"            onClick={() => (open.value = true)}          >            Edit event          </Button>        </Row>      </Stack>      <EventEditor        open={open}        event={event.value}        calendars={TEAM_CALENDARS}        timeZone={TEAM_ZONE}        today={TODAY}        locale="en-GB"        onSave={(e) => (event.value = e)}      />    </Card>  )}const TODAY = '2026-11-03'const TEAM_ZONE = 'Europe/Stockholm'const TEAM_CALENDARS = [  'Platform',  'Web',  'Data',  'Mobile',  'Releases',  'Time off',].map((name, i) => ({ id: i + 1, name }))const TEAM_EVENTS: CalEvent[] = [  {    id: 'e1',    title: 'Platform standup',    start: '2026-11-02T09:00',    end: '2026-11-02T09:15',    cal: 1,    recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR',    location: 'Room Atlas',    attendees: ['Mara Lindqvist', 'Jun Tanaka'],  },  {    id: 'e2',    title: 'Platform standup',    start: '2026-11-03T09:00',    end: '2026-11-03T09:15',    cal: 1,    recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR',    location: 'Room Atlas',    attendees: ['Mara Lindqvist', 'Jun Tanaka'],  },  {    id: 'e3',    title: 'Checkout review',    start: '2026-11-03T10:00',    end: '2026-11-03T11:00',    cal: 2,    video: true,    attendees: [      'Priya Raman',      'Tomás Oliveira',      'Mara Lindqvist',    ],  },  {    id: 'e4',    title: 'Incident review: mobile-bff',    start: '2026-11-03T13:00',    end: '2026-11-03T13:45',    cal: 4,    video: true,    location: 'Room Birch',    attendees: ['Sam Okafor', 'Inés Vidal', 'Jun Tanaka'],  },  {    id: 'e5',    title: 'Ranking v2 handover',    start: '2026-11-03T15:00',    end: '2026-11-03T16:00',    cal: 3,    location: 'Room Cedar',    attendees: ['Lena Fischer', 'Noor Haddad'],  },  {    id: 'e6',    title: 'Priya out of office',    start: '2026-11-03',    end: '2026-11-03',    allDay: true,    cal: 6,  },  {    id: 'e7',    title: 'Release 2026.11',    start: '2026-11-04T11:00',    end: '2026-11-04T12:00',    cal: 5,    attendees: [      'Mara Lindqvist',      'Jun Tanaka',      'Tomás Oliveira',    ],  },  {    id: 'e8',    title: 'Design crit',    start: '2026-11-04T14:00',    end: '2026-11-04T15:00',    cal: 2,    tentative: true,    video: true,  },  {    id: 'e9',    title: 'Cost review',    start: '2026-11-05T10:00',    end: '2026-11-05T11:00',    cal: 1,    recurrence: 'FREQ=MONTHLY',    attendees: ['Mara Lindqvist', 'Lena Fischer'],  },  {    id: 'e10',    title: 'On-call handover',    start: '2026-11-06T16:00',    end: '2026-11-06T16:30',    cal: 1,    recurrence: 'FREQ=WEEKLY',    video: true,  },]const REVIEW = TEAM_EVENTS.find((e) => e.id === 'e4')!const when = (e: CalEvent) => {  const date = e.start.slice(0, 10)  const start = e.start.slice(11)  const end = e.end.slice(11)  return `${date}, ${start} to ${end}`}

Installation

npm install @kerf-js/veneer

Usage

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

A new event

A title is required, and the end must come after the start.

tsx
import { state } from '@kerf-js/core'import {  Button,  EventEditor,  Row,  Stack,  Text} from '@kerf-js/veneer'import type { CalEvent } from '@kerf-js/veneer-core'export function New() {  const open = state(false)  const added = state<CalEvent[]>([])  const save = (e: CalEvent) =>    (added.value = [      ...added.value,      { ...e, id: `new-${added.value.length}` },    ])  return (    <Stack gap="related">      {added.value.map((e) => (        <Text key={e.id}>          {e.title}: {when(e)}        </Text>      ))}      {!added.value.length && (        <Text subtle>Nothing added yet.</Text>      )}      <Row>        <Button          icon="plus"          onClick={() => (open.value = true)}        >          New event        </Button>      </Row>      <EventEditor        open={open}        event={BLANK}        isNew        calendars={TEAM_CALENDARS}        timeZone={TEAM_ZONE}        today={TODAY}        locale="en-GB"        onSave={save}      />    </Stack>  )}const TODAY = '2026-11-03'const TEAM_ZONE = 'Europe/Stockholm'const TEAM_CALENDARS = [  'Platform',  'Web',  'Data',  'Mobile',  'Releases',  'Time off',].map((name, i) => ({ id: i + 1, name }))const when = (e: CalEvent) => {  const date = e.start.slice(0, 10)  const start = e.start.slice(11)  const end = e.end.slice(11)  return `${date}, ${start} to ${end}`}const BLANK: CalEvent = {  id: 'new',  title: '',  start: `${TODAY}T14:00`,  end: `${TODAY}T14:30`,  cal: 1,}

When to use

Use it for

  • Creating or changing an event
  • The way to do anything a drag can do

Accessibility

  • Focus goes to the title on open and to the first problem on a failed save.

API Reference

PropTypeDefaultDescription
openState<boolean>—A state you own.
eventCalEvent | null—The event to edit. Passing a new object rebuilds the fields.
isNewbooleanfalseTitles it New event.
calendarsCalendarSource[]—The calendars on offer.
timeZonestring—The IANA zone the times are edited in, such as Europe/Berlin.
localestring—The language and region used to format values.
todaystring—Pins today's date, for server rendering and tests. The browser's clock otherwise.
EventTypeDefaultDescription
onSave(e: CalEvent) => void—Gets the finished event.
onCancel(() => void)—Cancel was pressed.

RelatedEvent popoverMove sheetDrawer