Log
Follow a record's history: who changed what and when, with complete old and new values shown together.
- Today · Tue, Oct 27
Anna declined
StatusWaitingbecameDeclinedCaptions run a second late after 5:37.
Captions Bureau uploaded Captions
- Yesterday · Mon, Oct 26
Oleg commented
Please check the second half, the speaker changes.
Riverside set
StatusWaitingSystem created the record
<script setup lang="ts">import { computed } from 'vue'import type { LogEvent } from 'grani'import { UiCard, UiLog, useToday } from 'grani' const HISTORY: LogEvent[] = [ { id: 9, at: '27 Sep, 13:21', kind: 'decision', tone: 'danger', actor: { name: 'Anna' }, action: 'declined', changes: [{ field: 'Status', from: 'Waiting', to: 'Declined', tone: 'danger' }], note: 'Captions run a second late after 5:37.', }, { id: 8, at: '27 Sep, 13:18', kind: 'change', actor: { name: 'Anna' }, action: 'changed', changes: [ { field: 'Format', from: 'Portrait', to: 'Landscape' }, { field: 'Price', from: '$2.49', to: '$3.99' }, { field: 'Priority', from: '5', to: '1' }, ], }, { id: 7, at: '27 Sep, 10:15', kind: 'file', actor: { name: 'Captions Bureau' }, action: 'replaced', target: 'Captions', changes: [{ field: 'File', from: 'clip_99776_v1.srt', to: 'clip_99776_v2.srt' }], }, { id: 6, at: '27 Sep, 09:33', kind: 'file', actor: { name: 'Captions Bureau' }, action: 'uploaded', target: 'Captions', }, { id: 5, at: '26 Sep, 18:40', kind: 'comment', actor: { name: 'Oleg' }, action: 'commented', note: 'Please check the second half, the speaker changes.', }, { id: 4, at: '26 Sep, 18:10', kind: 'change', actor: { name: 'Riverside' }, action: 'set', changes: [{ field: 'Status', to: 'Waiting', tone: 'warning' }], }, { id: 3, at: '26 Sep, 18:08', kind: 'system', action: 'created the record' },] const PLAIN = HISTORY.slice(0, 1).concat(HISTORY.slice(3)) // The same history with real times, counted back from the showcase's today (27 Sep is today, the// 26th yesterday): events with a Date stand under their days.const { now } = useToday() // A time on today's date, moved by whole days. `new Date` reads it in the browser's zone; an app// with `createGrani({ timeZone })` builds its Dates in that zone, or takes them from the server.const on = (days: number, hhmm: string) => { const at = new Date(`${now.value}T${hhmm}:00`) at.setDate(at.getDate() + days) return at} const dated = (list: LogEvent[]) => computed(() => list.map((e) => { const [, day, hhmm] = /^(\d+) \w+, (\d\d:\d\d)$/.exec(String(e.at)) ?? [] return { ...e, at: on(Number(day) - 27, hhmm!) } }), ) const PLAIN_DATED = dated(PLAIN)</script> <template> <UiCard class="w-full max-w-md"> <UiLog :items="PLAIN_DATED" aria-label="History" /> </UiCard></template>
Also called timeline, activity log, history, audit log.
Usage
<UiLog :items="items" /> With GraniResolver in the Vite config, a template needs no import: set it up once.
API
| Name · type | Default | Description |
|---|---|---|
| Value | ||
| filterstring= "" | "" | The kind that is on: an event kind, or '' for all. |
| Data | ||
| items*LogEvent[] | The events, newest first. | |
| State | ||
| emptystring= "" | "" | The line when there are none; defaults to "No activity yet". |
| loadingboolean= false | false | The first load is on its way: rows in the shape of events, nothing else. |
| Behavior | ||
| nodedot | actor= "dot" | "dot" | dot: a dot in the event's tone. actor: the face of who did it, the system a plate with a glyph. |
| filters{ value: string; label: string; }[]= [] | [] | Kinds to choose from over the list, matched against each event's kind; "All" comes first. Up to three stand as segments, more go into a select. |
| hasMoreboolean= false | false | Older events exist: the list ends in "Load older events", which calls @more. |
| onMore(() => unknown) | "Load older events" (@more): load the events before the last one. Return a promise to hold the button while it runs and to say a failure; nothing returned, it is not held. | |
| failedboolean= false | false | The first load failed: says so, with Retry. |
| onRetry(() => unknown) | Retry (@retry): load the history again. Return a promise to hold the button while it runs. | |
| Form and access | ||
| ariaLabelstring= "" | "" | The list's name for a screen reader, when no heading names it. |
Attributes: class and style stay on the outer element; the rest (native attributes, aria-*, data-testid) reach the control inside it.