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
log-sheet · base base · style base-lyraOpen under the product themeInstallation
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
fieldinstead. - The questions form an assessment rather than a record.Use
questionnaireinstead. - You need permission before you can store what is being logged.Use
consent-sheetinstead. - There is exactly one numeric value with a unit and nothing else.Use
reading-inputinstead. - You are showing what was logged rather than capturing it.Use
result-cardinstead.
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
LogSheetThe capture region inside Sheet's scrolling area. Header, title, close control and footer are Sheet's.LogSheet.CategoryOne aria-hidden tinted band carrying data-category. Rendered only when category is supplied.Controlled bycategoryLogSheet.FieldsThe product's own controls, rendered in order and never inspected.Controlled bychildrenLogSheet.TimeA Field wrapping a datetime-local control, defaulted to when the sheet opened.Controlled bytimeLabelLogSheet.NoteA Field wrapping an optional textarea. Rendered only when noteLabel is supplied.Controlled bynoteLabelLogSheet.SaveThe one primary action, full width in Sheet's footer. No default label.Controlled bysaveLabelLogSheet.ConfirmReplaces the save action when a dismissal arrives over unsaved input.LogSheet.AnswersThe two answers stacked vertically with 16px between them.LogSheet.KeepThe safe answer, where the save action was, and the one focus lands on.LogSheet.DiscardThe destructive answer, first in tab order. Calls onDiscard with the entry, then closes.Controlled byonDiscard
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.
log-sheet-leaving-with-unsaved-input · base base · style base-lyraOpen under the product themeTwo 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.
log-sheet-two-fields-and-a-backdate-window · base base · style base-lyraOpen under the product themeContent 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:
onSavenever 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.
| Key | Action | Notes |
|---|---|---|
| Tab | Moves through the sheet's parts in visual order | Close control, fields, time, note, action. A datetime-local can cost several presses. |
| Shift+Tab | The same sequence, backwards | Sheet's close control is first forwards, last backwards. |
| Escape | Asks before discarding when there is unsaved input; closes when there is not | Same as a background tap. With the question up, it answers Keep editing. |
| Enter | Activates the focused control | Never saves from a field. No form element, no implicit submit. |
| Space | Activates the focused button | Real buttons, so the sheet does not scroll. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| activity ink on activity surface | light | 88.5 | 9.61:1 | Pass |
| activity ink on activity surface | dark | -83.1 | 12.58:1 | Pass |
| activity line on activity surface | light | 67.6 | 4.42:1 | Pass |
| activity line on activity surface | dark | -69.0 | 9.93:1 | Pass |
| activity accent on activity surface | light | 44.1 | 2.34:1 | Below floor |
| activity accent on activity surface | dark | -53.3 | 7.26:1 | Pass |
| activity accent on the page | light | 46.1 | 2.41:1 | Below floor |
| activity accent on the page | dark | -54.6 | 8.22:1 | Pass |
| activity ink on the page | light | 90.5 | 9.90:1 | Pass |
| activity ink on the page | dark | -84.4 | 14.24:1 | Pass |
| activity line on the band | light | 66.1 | 4.32:1 | Pass |
| activity line on the band | dark | -67.4 | 8.60:1 | Pass |
| heart ink on heart surface | light | 89.1 | 10.51:1 | Pass |
| heart ink on heart surface | dark | -80.5 | 12.55:1 | Pass |
| heart line on heart surface | light | 70.4 | 5.05:1 | Pass |
| heart line on heart surface | dark | -64.5 | 9.46:1 | Pass |
| heart accent on heart surface | light | 60.3 | 3.64:1 | Pass |
| heart accent on heart surface | dark | -48.2 | 6.68:1 | Pass |
| heart accent on the page | light | 63.2 | 3.81:1 | Pass |
| heart accent on the page | dark | -49.5 | 7.27:1 | Pass |
| heart ink on the page | light | 92.0 | 10.97:1 | Pass |
| heart ink on the page | dark | -81.8 | 13.65:1 | Pass |
| heart line on the band | light | 69.9 | 5.01:1 | Pass |
| heart line on the band | dark | -62.8 | 7.88:1 | Pass |
| labs ink on labs surface | light | 88.8 | 9.78:1 | Pass |
| labs ink on labs surface | dark | -83.1 | 12.69:1 | Pass |
| labs line on labs surface | light | 67.9 | 4.51:1 | Pass |
| labs line on labs surface | dark | -68.4 | 9.90:1 | Pass |
| labs accent on labs surface | light | 59.3 | 3.49:1 | Pass |
| labs accent on labs surface | dark | -52.7 | 7.21:1 | Pass |
| labs accent on the page | light | 61.5 | 3.60:1 | Pass |
| labs accent on the page | dark | -53.9 | 8.07:1 | Pass |
| labs ink on the page | light | 90.9 | 10.09:1 | Pass |
| labs ink on the page | dark | -84.3 | 14.22:1 | Pass |
| labs line on the band | light | 66.5 | 4.42:1 | Pass |
| labs line on the band | dark | -66.7 | 8.49:1 | Pass |
| mind ink on mind surface | light | 89.9 | 10.36:1 | Pass |
| mind ink on mind surface | dark | -81.3 | 12.56:1 | Pass |
| mind line on mind surface | light | 70.7 | 4.94:1 | Pass |
| mind line on mind surface | dark | -65.4 | 9.54:1 | Pass |
| mind accent on mind surface | light | 60.0 | 3.55:1 | Pass |
| mind accent on mind surface | dark | -49.1 | 6.77:1 | Pass |
| mind accent on the page | light | 62.8 | 3.70:1 | Pass |
| mind accent on the page | dark | -50.0 | 7.42:1 | Pass |
| mind ink on the page | light | 92.7 | 10.80:1 | Pass |
| mind ink on the page | dark | -82.2 | 13.77:1 | Pass |
| mind line on the band | light | 70.0 | 4.89:1 | Pass |
| mind line on the band | dark | -63.4 | 8.00:1 | Pass |
| nutrition ink on nutrition surface | light | 89.3 | 10.06:1 | Pass |
| nutrition ink on nutrition surface | dark | -81.9 | 12.55:1 | Pass |
| nutrition line on nutrition surface | light | 69.7 | 4.78:1 | Pass |
| nutrition line on nutrition surface | dark | -66.7 | 9.66:1 | Pass |
| nutrition accent on nutrition surface | light | 39.2 | 2.11:1 | Below floor |
| nutrition accent on nutrition surface | dark | -50.2 | 6.88:1 | Pass |
| nutrition accent on the page | light | 41.8 | 2.19:1 | Below floor |
| nutrition accent on the page | dark | -51.3 | 7.63:1 | Pass |
| nutrition ink on the page | light | 91.9 | 10.44:1 | Pass |
| nutrition ink on the page | dark | -82.9 | 13.91:1 | Pass |
| nutrition line on the band | light | 68.8 | 4.71:1 | Pass |
| nutrition line on the band | dark | -64.8 | 8.19:1 | Pass |
| sleep ink on sleep surface | light | 89.6 | 10.19:1 | Pass |
| sleep ink on sleep surface | dark | -81.9 | 12.60:1 | Pass |
| sleep line on sleep surface | light | 70.0 | 4.80:1 | Pass |
| sleep line on sleep surface | dark | -66.4 | 9.65:1 | Pass |
| sleep accent on sleep surface | light | 64.6 | 4.05:1 | Pass |
| sleep accent on sleep surface | dark | -50.4 | 6.94:1 | Pass |
| sleep accent on the page | light | 67.3 | 4.22:1 | Pass |
| sleep accent on the page | dark | -51.4 | 7.67:1 | Pass |
| sleep ink on the page | light | 92.3 | 10.60:1 | Pass |
| sleep ink on the page | dark | -82.9 | 13.92:1 | Pass |
| sleep line on the band | light | 69.1 | 4.75:1 | Pass |
| sleep line on the band | dark | -64.5 | 8.16:1 | Pass |
| body text on the page | light | 101.6 | 17.18:1 | Pass |
| body text on the page | dark | -100.5 | 18.00:1 | Pass |
| body text on a card | light | 104.7 | 17.96:1 | Pass |
| body text on a card | dark | -99.6 | 16.32:1 | Pass |
| body text on the muted ground | light | 98.1 | 16.32:1 | Pass |
| body text on the muted ground | dark | -97.5 | 13.77:1 | Pass |
| secondary text on the page | light | 83.9 | 7.42:1 | Pass |
| secondary text on the page | dark | -80.7 | 13.44:1 | Pass |
| secondary text on a card | light | 87.0 | 7.76:1 | Pass |
| secondary text on a card | dark | -79.8 | 12.19:1 | Pass |
| secondary text on the muted ground | light | 80.4 | 7.05:1 | Pass |
| secondary text on the muted ground | dark | -77.8 | 10.28:1 | Pass |
| a hairline boundary on the page | light | 19.3 | 1.41:1 | Below floor |
| a hairline boundary on the page | dark | -8.2 | 1.90:1 | Below floor |
| a hairline boundary on a card | light | 22.4 | 1.47:1 | Below floor |
| a hairline boundary on a card | dark | -7.3 | 1.72:1 | Below floor |
| a hairline boundary on the muted ground | light | 15.8 | 1.34:1 | Below floor |
| a hairline boundary on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on the page | light | 69.8 | 4.52:1 | Pass |
| a placeholder boundary on the page | dark | -51.0 | 7.62:1 | Pass |
| a placeholder boundary on a card | light | 72.8 | 4.72:1 | Pass |
| a placeholder boundary on a card | dark | -50.1 | 6.91:1 | Pass |
| a placeholder boundary on the muted ground | light | 66.2 | 4.29:1 | Pass |
| a placeholder boundary on the muted ground | dark | -48.1 | 5.83:1 | Pass |
| a placeholder fill on the page | light | 8.3 | 1.18:1 | Below floor |
| a placeholder fill on the page | dark | -8.2 | 1.90:1 | Below floor |
| a placeholder fill on a card | light | 11.4 | 1.23:1 | Below floor |
| a placeholder fill on a card | dark | -7.3 | 1.72:1 | Below floor |
| a placeholder fill on the muted ground | light | 0.0 | 1.12:1 | Below floor |
| a placeholder fill on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on its own fill | light | 58.9 | 3.84:1 | Pass |
| a placeholder boundary on its own fill | dark | -41.5 | 4.01:1 | Below floor |
| the card hairline on the page | light | 69.8 | 4.52:1 | Pass |
| the card hairline on the page | dark | -28.7 | 4.19:1 | Below floor |
| the card hairline on a card | light | 72.8 | 4.72:1 | Pass |
| the card hairline on a card | dark | -27.9 | 3.80:1 | Below floor |
| activity ink on the card material | light | 93.6 | 10.34:1 | Pass |
| activity ink on the card material | dark | -83.5 | 12.91:1 | Pass |
| heart ink on the card material | light | 95.1 | 11.47:1 | Pass |
| heart ink on the card material | dark | -80.9 | 12.38:1 | Pass |
| labs ink on the card material | light | 94.0 | 10.54:1 | Pass |
| labs ink on the card material | dark | -83.5 | 12.90:1 | Pass |
| mind ink on the card material | light | 95.7 | 11.28:1 | Pass |
| mind ink on the card material | dark | -81.4 | 12.49:1 | Pass |
| nutrition ink on the card material | light | 95.0 | 10.91:1 | Pass |
| nutrition ink on the card material | dark | -82.0 | 12.61:1 | Pass |
| sleep ink on the card material | light | 95.4 | 11.08:1 | Pass |
| sleep ink on the card material | dark | -82.1 | 12.62:1 | Pass |
These are the measured token pairs this component draws colour from, not a measurement of the component itself.
Data attributes
| Attribute | Condition | Value |
|---|---|---|
| data-slot | On the capture region, fields wrapper and time field, always | log-sheet, log-sheet-fields, log-sheet-time |
| data-slot | On the note, category band and footer parts, only while rendered | log-sheet-note, log-sheet-category, log-sheet-save, log-sheet-confirm, log-sheet-answers, log-sheet-discard, log-sheet-keep |
| data-category | On log-sheet-category, when a recognised category is supplied | sleep | 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
}Related
- 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.
SourceCitation
Where a piece of health information came from, in plain words, with an optional link to the fuller citation and an optional date it was last checked.
ReadingInput
A field for typing in a measurement, with the unit shown and switchable beside the number, and room for an advisory the product wrote.