opsinjs
ComponentsHealth input

LogSheet

A small sheet that slides up so you can record something in a few seconds without leaving the screen you were on. One reading, one dose, one note.

Preview

Theme
Density
Text
Status
log-sheet · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/log-sheet

The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.

Usage

import { LogSheet } from "@/components/ui/log-sheet"
<LogSheet
  open={open}
  onOpenChange={setOpen}
  title="An example entry"
  values={values}
  saveLabel="Save the example entry"
  onSave={(entry) => store(entry)}
>
  <Field label="Example measurement">
    <Field.Control value={held ?? ""} onChange={write} />
  </Field>
</LogSheet>

When to use it

Use it when

  • Recording one thing the reader does repeatedly: a reading, a dose, a symptom or a note.
  • Capture that must not lose the reader's place, from a dashboard or a notification.
  • An entry that should take under fifteen seconds one-handed.

Do not use it when

  • The entry needs more than about five fields, or branching questions.Use field instead.
  • The questions form an assessment rather than a record.Use questionnaire instead.
  • You need permission before you can store what is being logged.Use consent-sheet instead.
  • There is exactly one numeric value with a unit and nothing else.Use reading-input instead.
  • You are showing what was logged rather than capturing it.Use result-card instead.

Clinical meaning

Asserts. That the reader recorded this value, at this time, themselves. LogEntry.provenance is the literal "self-reported" and no prop changes it. See Data provenance and device accuracy.

Never read as. A clinical observation or an assessment. The sheet never evaluates what is entered or says whether it is worth acting on.

Colour axis. Category only, on one band beneath the heading carrying data-category with no word and no glyph. The status axis never appears here: an amber field mid-keystroke teaches a person to stop logging honestly.

Thresholds. None. LogSheet holds no ranges and never blocks a save, so an unlikely value still saves. A plausibility warning belongs on ReadingInput. maxBackdateDays has no default and constrains the platform picker, not the save.

Timing. occurredAt is the time the entry is about and recordedAt the save time. backdated is true only when the reader moved the time control.

Anatomy

  1. LogSheetThe capture region inside Sheet's scrolling area. Header, title, close control and footer are Sheet's.
  2. LogSheet.CategoryOne aria-hidden tinted band carrying data-category. Rendered only when category is supplied.Controlled by category
  3. LogSheet.FieldsThe product's own controls, rendered in order and never inspected.Controlled by children
  4. LogSheet.TimeA Field wrapping a datetime-local control, defaulted to when the sheet opened.Controlled by timeLabel
  5. LogSheet.NoteA Field wrapping an optional textarea. Rendered only when noteLabel is supplied.Controlled by noteLabel
  6. LogSheet.SaveThe one primary action, full width in Sheet's footer. No default label.Controlled by saveLabel
  7. LogSheet.ConfirmReplaces the save action when a dismissal arrives over unsaved input.
  8. LogSheet.AnswersThe two answers stacked vertically with 16px between them.
  9. LogSheet.KeepThe safe answer, where the save action was, and the one focus lands on.
  10. LogSheet.DiscardThe destructive answer, first in tab order. Calls onDiscard with the entry, then closes.Controlled by onDiscard

Examples

Leaving with unsaved input

Escape, the background, a drag and the close control all ask the same question in the footer. onDiscard receives the entry about to be lost.

Theme
Density
Text
Status
log-sheet-leaving-with-unsaved-input · base base · style base-lyraOpen under the product theme

Two fields, and the window the product owns

Two controls write two keys into one entry through values. maxBackdateDays sets the picker's earliest day, and the sheet still refuses no save.

Theme
Density
Text
Status
log-sheet-two-fields-and-a-backdate-window · base base · style base-lyraOpen under the product theme

Content guidelines

The title names what is being logged: Blood pressure, How you slept. saveLabel says what it saves, Save reading, never Done, so it has no default. Ask for nothing you can infer; the fourth field stops people logging.

Do

"Systolic, Diastolic, Time (now), Save reading." Three inputs and an action that says what it does.

Don’t

"Systolic, Diastolic, Pulse, Arm, Position, Cuff size, Mood, Notes, Submit." Defensible fields, unusable set.

Accessibility

Audited against WCAG 2.2 AA in a source pass and a rendered pass. This audit is author-run, not an independent review, and clinical review is still pending.

The audit found nothing to change here. Both inputs take their name and description from Field, so name, role and value hold; the only coloured element is the aria-hidden category band; and the single tokenised column reflows and scales.

Three items are argued, not measured.

  • The discard question is announced by a focus move and aria-describedby, not a live region; a second modal was declined on purpose.
  • No Saved status is announced: onSave never closes the sheet, so the component cannot know the save landed and that feedback belongs to the product.
  • Save is one explicit tap with no confirm step: no autosave, every field visible, sheet stays open for undo.

The open sheet was not re-measured in a browser: port 4000 holds a stale build. axe on the static rendered page is clean.

KeyActionNotes
TabMoves through the sheet's parts in visual orderClose control, fields, time, note, action. A datetime-local can cost several presses.
Shift+TabThe same sequence, backwardsSheet's close control is first forwards, last backwards.
EscapeAsks before discarding when there is unsaved input; closes when there is notSame as a background tap. With the question up, it answers Keep editing.
EnterActivates the focused controlNever saves from a field. No form element, no implicit submit.
SpaceActivates the focused buttonReal buttons, so the sheet does not scroll.
PairThemeAPCA LcWCAG 2.2Floor
activity ink on activity surfacelight88.59.61:1Pass
activity ink on activity surfacedark-83.112.58:1Pass
activity line on activity surfacelight67.64.42:1Pass
activity line on activity surfacedark-69.09.93:1Pass
activity accent on activity surfacelight44.12.34:1Below floor
activity accent on activity surfacedark-53.37.26:1Pass
activity accent on the pagelight46.12.41:1Below floor
activity accent on the pagedark-54.68.22:1Pass
activity ink on the pagelight90.59.90:1Pass
activity ink on the pagedark-84.414.24:1Pass
activity line on the bandlight66.14.32:1Pass
activity line on the banddark-67.48.60:1Pass
heart ink on heart surfacelight89.110.51:1Pass
heart ink on heart surfacedark-80.512.55:1Pass
heart line on heart surfacelight70.45.05:1Pass
heart line on heart surfacedark-64.59.46:1Pass
heart accent on heart surfacelight60.33.64:1Pass
heart accent on heart surfacedark-48.26.68:1Pass
heart accent on the pagelight63.23.81:1Pass
heart accent on the pagedark-49.57.27:1Pass
heart ink on the pagelight92.010.97:1Pass
heart ink on the pagedark-81.813.65:1Pass
heart line on the bandlight69.95.01:1Pass
heart line on the banddark-62.87.88:1Pass
labs ink on labs surfacelight88.89.78:1Pass
labs ink on labs surfacedark-83.112.69:1Pass
labs line on labs surfacelight67.94.51:1Pass
labs line on labs surfacedark-68.49.90:1Pass
labs accent on labs surfacelight59.33.49:1Pass
labs accent on labs surfacedark-52.77.21:1Pass
labs accent on the pagelight61.53.60:1Pass
labs accent on the pagedark-53.98.07:1Pass
labs ink on the pagelight90.910.09:1Pass
labs ink on the pagedark-84.314.22:1Pass
labs line on the bandlight66.54.42:1Pass
labs line on the banddark-66.78.49:1Pass
mind ink on mind surfacelight89.910.36:1Pass
mind ink on mind surfacedark-81.312.56:1Pass
mind line on mind surfacelight70.74.94:1Pass
mind line on mind surfacedark-65.49.54:1Pass
mind accent on mind surfacelight60.03.55:1Pass
mind accent on mind surfacedark-49.16.77:1Pass
mind accent on the pagelight62.83.70:1Pass
mind accent on the pagedark-50.07.42:1Pass
mind ink on the pagelight92.710.80:1Pass
mind ink on the pagedark-82.213.77:1Pass
mind line on the bandlight70.04.89:1Pass
mind line on the banddark-63.48.00:1Pass
nutrition ink on nutrition surfacelight89.310.06:1Pass
nutrition ink on nutrition surfacedark-81.912.55:1Pass
nutrition line on nutrition surfacelight69.74.78:1Pass
nutrition line on nutrition surfacedark-66.79.66:1Pass
nutrition accent on nutrition surfacelight39.22.11:1Below floor
nutrition accent on nutrition surfacedark-50.26.88:1Pass
nutrition accent on the pagelight41.82.19:1Below floor
nutrition accent on the pagedark-51.37.63:1Pass
nutrition ink on the pagelight91.910.44:1Pass
nutrition ink on the pagedark-82.913.91:1Pass
nutrition line on the bandlight68.84.71:1Pass
nutrition line on the banddark-64.88.19:1Pass
sleep ink on sleep surfacelight89.610.19:1Pass
sleep ink on sleep surfacedark-81.912.60:1Pass
sleep line on sleep surfacelight70.04.80:1Pass
sleep line on sleep surfacedark-66.49.65:1Pass
sleep accent on sleep surfacelight64.64.05:1Pass
sleep accent on sleep surfacedark-50.46.94:1Pass
sleep accent on the pagelight67.34.22:1Pass
sleep accent on the pagedark-51.47.67:1Pass
sleep ink on the pagelight92.310.60:1Pass
sleep ink on the pagedark-82.913.92:1Pass
sleep line on the bandlight69.14.75:1Pass
sleep line on the banddark-64.58.16:1Pass
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor
activity ink on the card materiallight93.610.34:1Pass
activity ink on the card materialdark-83.512.91:1Pass
heart ink on the card materiallight95.111.47:1Pass
heart ink on the card materialdark-80.912.38:1Pass
labs ink on the card materiallight94.010.54:1Pass
labs ink on the card materialdark-83.512.90:1Pass
mind ink on the card materiallight95.711.28:1Pass
mind ink on the card materialdark-81.412.49:1Pass
nutrition ink on the card materiallight95.010.91:1Pass
nutrition ink on the card materialdark-82.012.61:1Pass
sleep ink on the card materiallight95.411.08:1Pass
sleep ink on the card materialdark-82.112.62:1Pass

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

Data attributes

AttributeConditionValue
data-slotOn the capture region, fields wrapper and time field, alwayslog-sheet, log-sheet-fields, log-sheet-time
data-slotOn the note, category band and footer parts, only while renderedlog-sheet-note, log-sheet-category, log-sheet-save, log-sheet-confirm, log-sheet-answers, log-sheet-discard, log-sheet-keep
data-categoryOn log-sheet-category, when a recognised category is suppliedsleep | heart | activity | nutrition | mind | labs

API reference

Prop

Type

Generated from LogSheetProps in registry/bases/base/log-sheet.tsx.

open, onOpenChange, title, detents and className come from Sheet. footer, modal and dismissible are omitted on purpose. onSave never closes the sheet; only the product knows whether the save landed. Both handlers receive a LogEntry:

export interface LogEntry {
  values: Record<string, number | string | null>
  occurredAt: string
  recordedAt: string
  backdated: boolean
  provenance: "self-reported"
  note?: string
}
  • Sheet is the general surface. LogSheet adds a capture contract and one save.
  • ReadingInput is the single control that goes inside.
  • Questionnaire is an assessment, not a record.
  • Field is for entries that belong on a page, and wires every control here.
  • ConsentSheet asks for permission rather than a record.

On this page