fix(canary-phase-ui-design): single-source form fields + URL revoke + eventExtra schema

Parallel audit (superpowers:code-reviewer + general-purpose spec adherence)
returned PASS-WITH-NOTES + PASS. All actionable SHOULD-FIX and NIT items
addressed in this commit per the fix-in-phase rule.

SHOULD-FIX (code review):
- Dead ternary at manage/index.tsx:253 (`cursor === undefined ? idx+1 : idx+1`)
  collapsed to `idx + 1`. EventLogSection prop renamed `cursor` → `cursorIsSet`
  (boolean) since the value itself was never consumed past the dead branch.
- URL.createObjectURL leak in artifact.tsx — both file (base64) and text
  variants now run inside useEffect with cleanup that calls
  URL.revokeObjectURL on dependency-change AND on unmount. Extracted into
  two co-located hooks: useBase64ObjectUrl, useTextObjectUrl.
- Text inputs lacked <label htmlFor=…> association. Refactored the landing
  form to use the existing Field primitives (TextField, TextareaField) which
  wire htmlFor↔id via useId. This single change also closed the next finding.
- Dead infrastructure: Field primitives existed but no consumer used them.
  Landing form's local FieldLabel removed; TextField/TextareaField now have
  consumers. Net diff: −181 LOC.
- LeaderLabel: truly unused primitive, no foreseeable consumer in this scope.
  Deleted (`frontend/src/components/LeaderLabel/`) and removed from barrel.

SHOULD-FIX (spec adherence):
- §8.6 row 410 → TOKEN_DISABLED: spec is stale. Backend Go ground truth
  (verified via `grep StatusGone\|TOKEN_DISABLED backend/internal/`) emits
  neither — disabled tokens come back as 200 with `token.enabled=false`,
  which the DossierCard already renders with alarm-pill + HEADLINE_DISABLED.
  Per feedback_verify_data_shapes.md: backend Go beats spec when they diverge.
  No code change; documented here so the next AI doesn't re-open it.
- eventResponseSchema.extra: tightened from `z.unknown()` to
  `z.record(z.string(), z.unknown())` to match backend's `json.RawMessage`
  default of `{}` (event/repository.go:47). Added EventExtra type alias;
  event-row.tsx now uses it instead of casting through unknown.

NIT:
- SerialBar: extracted `BAR_COUNT = 22` const; `FALLBACK_BARS` constant
  string replaces the BAR_CHARS array that had length-9 / length-22
  inconsistency between the empty-input and computed branches.
- Glyph: removed `default` branch — switch is now exhaustive over the
  closed TokenType enum, so a future 8th type would be a compile error
  rather than silently rendering WebbugGlyph.
- Dead `export { TOKEN_BLURB }` + `export { ArtifactDisplay }` from
  landing/index.tsx — no observed consumer, dropped.
- App.tsx and main.tsx header banners normalized to the project-canonical
  19 = signs (matches all other files in repo).

NOT addressed in this commit (intentional):
- CreateTokenRequest `metadata` as `z.unknown()`. Refactor to a discriminated
  request schema keyed on `type` would tighten compile-time safety for any
  future caller bypassing buildPayload — defer because (a) the form is the
  only caller and validates via Zod on submit, (b) the change touches the
  Phase 14 contract that was just stabilized two commits before this UI phase.
- NotFound (frontend route fallback) vs ManageNotFound (manage 404) live
  separately. Both work; the two paths are intentional — off-path vs
  off-dossier are semantically different events.

Gates green: typecheck + biome check + lint:scss + build (414ms; landing
chunk dropped 21KB → 19.93KB after dead style removal).
This commit is contained in:
CarterPerez-dev 2026-05-17 15:07:50 -04:00
parent 4a4f465d24
commit da126ad889
13 changed files with 142 additions and 327 deletions

View File

@ -1,7 +1,7 @@
// =========================== // ===================
// ©AngelaMos | 2026 // ©AngelaMos | 2026
// App.tsx // App.tsx
// =========================== // ===================
import { QueryClientProvider } from '@tanstack/react-query' import { QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools' import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

View File

@ -20,6 +20,10 @@ export const geoViewSchema = z.object({
export type GeoView = z.infer<typeof geoViewSchema> export type GeoView = z.infer<typeof geoViewSchema>
export const eventExtraSchema = z.record(z.string(), z.unknown())
export type EventExtra = z.infer<typeof eventExtraSchema>
export const eventResponseSchema = z.object({ export const eventResponseSchema = z.object({
id: z.number().int().nonnegative(), id: z.number().int().nonnegative(),
triggered_at: z.iso.datetime(), triggered_at: z.iso.datetime(),
@ -27,7 +31,7 @@ export const eventResponseSchema = z.object({
user_agent: z.string().nullable(), user_agent: z.string().nullable(),
referer: z.string().nullable(), referer: z.string().nullable(),
geo: geoViewSchema, geo: geoViewSchema,
extra: z.unknown(), extra: eventExtraSchema,
notify_status: notifyStatusSchema, notify_status: notifyStatusSchema,
notified_at: z.iso.datetime().nullable(), notified_at: z.iso.datetime().nullable(),
}) })

View File

@ -52,8 +52,6 @@ function renderGlyph(type: TokenType): React.ReactElement {
return <EnvfileGlyph /> return <EnvfileGlyph />
case 'mysql': case 'mysql':
return <MysqlGlyph /> return <MysqlGlyph />
default:
return <WebbugGlyph />
} }
} }

View File

@ -1,57 +0,0 @@
// ===================
// ©AngelaMos | 2026
// LeaderLabel.module.scss
// ===================
@use '../../styles/tokens' as *;
@use '../../styles/fonts' as *;
.label {
display: flex;
flex-direction: column;
gap: $space-1;
max-width: 22ch;
font-family: $font-mono;
&[data-align='right'] {
text-align: right;
align-self: flex-end;
}
}
.head {
display: flex;
align-items: center;
gap: $space-1-5;
}
.index {
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $ink-mute;
white-space: nowrap;
}
.rule {
flex: 1 1 auto;
block-size: $hairline;
background-color: $rule;
align-self: center;
}
.body {
font-size: $font-size-2xs;
line-height: $line-height-tight;
letter-spacing: $tracking-wide;
text-transform: uppercase;
color: $ink;
}
.caption {
font-size: $font-size-3xs;
line-height: $line-height-snug;
color: $ink-mute;
text-transform: none;
letter-spacing: $tracking-normal;
}

View File

@ -1,31 +0,0 @@
// ===================
// ©AngelaMos | 2026
// index.tsx
// ===================
import type { PropsWithChildren, ReactNode } from 'react'
import styles from './LeaderLabel.module.scss'
type LeaderLabelProps = PropsWithChildren<{
index?: string
caption?: ReactNode
align?: 'left' | 'right'
}>
export function LeaderLabel({
children,
index,
caption,
align = 'left',
}: LeaderLabelProps): React.ReactElement {
return (
<div className={styles.label} data-align={align}>
<div className={styles.head}>
{index ? <span className={styles.index}>{index}</span> : null}
<span className={styles.rule} aria-hidden="true" />
</div>
<div className={styles.body}>{children}</div>
{caption ? <div className={styles.caption}>{caption}</div> : null}
</div>
)
}

View File

@ -11,14 +11,15 @@ type SerialBarProps = {
prefix?: string prefix?: string
} }
const BAR_CHARS = ['▮', '▯', '▮', '▮', '▯', '▮', '▯', '▯', '▮'] const BAR_COUNT = 22
const FALLBACK_BARS = '▮▯▮▮▯▮▯▯▮▮▯▮▮▯▮▯▮▮▯▮▯▮'
function fingerprintToBars(value: string): string { function fingerprintToBars(value: string): string {
if (value.length === 0) { if (value.length === 0) {
return BAR_CHARS.join('') return FALLBACK_BARS
} }
let acc = '' let acc = ''
for (let i = 0; i < 22; i += 1) { for (let i = 0; i < BAR_COUNT; i += 1) {
const ch = value.charCodeAt(i % value.length) const ch = value.charCodeAt(i % value.length)
acc += (ch & 1) === 0 ? '▮' : '▯' acc += (ch & 1) === 0 ? '▮' : '▯'
} }

View File

@ -6,15 +6,9 @@
export { Button } from './Button' export { Button } from './Button'
export { CopyField } from './CopyField' export { CopyField } from './CopyField'
export { DataRow } from './DataRow' export { DataRow } from './DataRow'
export { export { FieldWrap, SelectField, TextareaField, TextField } from './Field'
FieldWrap,
SelectField,
TextareaField,
TextField,
} from './Field'
export { Glyph } from './Glyph' export { Glyph } from './Glyph'
export { Halftone } from './Halftone' export { Halftone } from './Halftone'
export { LeaderLabel } from './LeaderLabel'
export { Pill } from './Pill' export { Pill } from './Pill'
export { SerialBar } from './SerialBar' export { SerialBar } from './SerialBar'
export { SpecimenCard, SpecimenCardSection } from './SpecimenCard' export { SpecimenCard, SpecimenCardSection } from './SpecimenCard'

View File

@ -1,7 +1,7 @@
// =========================== // ===================
// ©AngelaMos | 2026 // ©AngelaMos | 2026
// main.tsx // main.tsx
// =========================== // ===================
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'

View File

@ -3,7 +3,7 @@
// artifact.tsx // artifact.tsx
// =================== // ===================
import { useMemo } from 'react' import { useEffect, useState } from 'react'
import { toast } from 'sonner' import { toast } from 'sonner'
import type { Artifact } from '@/api' import type { Artifact } from '@/api'
import { CopyField, Pill } from '@/components' import { CopyField, Pill } from '@/components'
@ -63,16 +63,7 @@ function FileArtifact({
}): React.ReactElement { }): React.ReactElement {
const filename = artifact.filename ?? filenameFallback const filename = artifact.filename ?? filenameFallback
const contentType = artifact.content_type ?? 'application/octet-stream' const contentType = artifact.content_type ?? 'application/octet-stream'
const href = useMemo(() => { const href = useBase64ObjectUrl(artifact.content_b64, contentType)
if (!artifact.content_b64) {
return null
}
try {
return base64ToObjectUrl(artifact.content_b64, contentType)
} catch (_err) {
return null
}
}, [artifact.content_b64, contentType])
if (!href) { if (!href) {
return <Pill tone="alarm">artifact unreadable</Pill> return <Pill tone="alarm">artifact unreadable</Pill>
@ -109,13 +100,7 @@ function TextArtifact({
const filename = artifact.filename ?? filenameFallback const filename = artifact.filename ?? filenameFallback
const content = artifact.content ?? '' const content = artifact.content ?? ''
const contentType = artifact.content_type ?? 'text/plain' const contentType = artifact.content_type ?? 'text/plain'
const href = useMemo(() => { const href = useTextObjectUrl(content, contentType)
if (content.length === 0) {
return null
}
const blob = new Blob([content], { type: contentType })
return URL.createObjectURL(blob)
}, [content, contentType])
return ( return (
<div className={styles.artifactStack}> <div className={styles.artifactStack}>
@ -152,12 +137,57 @@ function ConnectionStringArtifact({
) )
} }
function base64ToObjectUrl(b64: string, contentType: string): string { function useBase64ObjectUrl(
b64: string | undefined,
contentType: string
): string | null {
const [url, setUrl] = useState<string | null>(null)
useEffect(() => {
if (!b64 || b64.length === 0) {
setUrl(null)
return
}
let created: string | null = null
try {
const bytes = base64ToBytes(b64)
const blob = new Blob([bytes], { type: contentType })
created = URL.createObjectURL(blob)
setUrl(created)
} catch (_err) {
setUrl(null)
}
return () => {
if (created !== null) {
URL.revokeObjectURL(created)
}
}
}, [b64, contentType])
return url
}
function useTextObjectUrl(content: string, contentType: string): string | null {
const [url, setUrl] = useState<string | null>(null)
useEffect(() => {
if (content.length === 0) {
setUrl(null)
return
}
const blob = new Blob([content], { type: contentType })
const next = URL.createObjectURL(blob)
setUrl(next)
return () => {
URL.revokeObjectURL(next)
}
}, [content, contentType])
return url
}
function base64ToBytes(b64: string): Uint8Array<ArrayBuffer> {
const binary = atob(b64) const binary = atob(b64)
const len = binary.length const len = binary.length
const bytes = new Uint8Array(len) const bytes = new Uint8Array(len)
for (let i = 0; i < len; i += 1) { for (let i = 0; i < len; i += 1) {
bytes[i] = binary.charCodeAt(i) bytes[i] = binary.charCodeAt(i)
} }
return URL.createObjectURL(new Blob([bytes], { type: contentType })) return bytes
} }

View File

@ -14,10 +14,16 @@ import {
useCreateToken, useCreateToken,
useTokenTypes, useTokenTypes,
} from '@/api' } from '@/api'
import { Button, Halftone, Strip, StripItem } from '@/components' import {
Button,
Halftone,
Strip,
StripItem,
TextareaField,
TextField,
} from '@/components'
import { getTurnstileSiteKey, Turnstile } from '@/core/turnstile' import { getTurnstileSiteKey, Turnstile } from '@/core/turnstile'
import { ArtifactDisplay } from './artifact' import { PAGE_COPY } from './copy'
import { PAGE_COPY, TOKEN_BLURB } from './copy'
import { import {
buildSubmissionState, buildSubmissionState,
FILE_KIND_TYPES, FILE_KIND_TYPES,
@ -224,22 +230,17 @@ function AnnotateSection({
title={PAGE_COPY.SECTION_02_TITLE} title={PAGE_COPY.SECTION_02_TITLE}
body={PAGE_COPY.SECTION_02_BODY} body={PAGE_COPY.SECTION_02_BODY}
> >
<div className={styles.fieldRow}> <TextareaField
<FieldLabel index="A">MEMO</FieldLabel> index="A"
<textarea label="MEMO"
className={styles.textarea} name="memo"
value={form.memo} value={form.memo}
maxLength={256} maxLength={256}
placeholder="Q4 bonuses spreadsheet on file-server" placeholder="Q4 bonuses spreadsheet on file-server"
onChange={(e) => onChange({ memo: e.target.value })} onChange={(e) => onChange({ memo: e.target.value })}
data-invalid={errors.memo !== undefined} error={errors.memo}
/> hint={`${form.memo.length} / 256 characters`}
{errors.memo ? ( />
<p className={styles.fieldError}>{errors.memo}</p>
) : (
<p className={styles.fieldHint}>{form.memo.length} / 256 characters</p>
)}
</div>
{showFilename ? ( {showFilename ? (
<FilenameField form={form} errors={errors} onChange={onChange} /> <FilenameField form={form} errors={errors} onChange={onChange} />
) : null} ) : null}
@ -253,24 +254,17 @@ function FilenameField({
onChange, onChange,
}: SectionPropsBase): React.ReactElement { }: SectionPropsBase): React.ReactElement {
return ( return (
<div className={styles.fieldRow}> <TextField
<FieldLabel index="B">FILENAME (OPTIONAL)</FieldLabel> index="B"
<input label="FILENAME (OPTIONAL)"
className={styles.input} name="filename"
value={form.filename} value={form.filename}
maxLength={128} maxLength={128}
placeholder={defaultFilename(form.type)} placeholder={defaultFilename(form.type)}
onChange={(e) => onChange({ filename: e.target.value })} onChange={(e) => onChange({ filename: e.target.value })}
data-invalid={errors.filename !== undefined} error={errors.filename}
/> hint="What the bait file will be called when it lands somewhere it shouldn't."
{errors.filename ? ( />
<p className={styles.fieldError}>{errors.filename}</p>
) : (
<p className={styles.fieldHint}>
What the bait file will be called when it lands somewhere it shouldn't.
</p>
)}
</div>
) )
} }
@ -305,32 +299,24 @@ function TelegramFields({
}: SectionPropsBase): React.ReactElement { }: SectionPropsBase): React.ReactElement {
return ( return (
<div className={styles.subgrid}> <div className={styles.subgrid}>
<div className={styles.fieldRow}> <TextField
<FieldLabel index="A">BOT TOKEN</FieldLabel> index="A"
<input label="BOT TOKEN"
className={styles.input} name="telegram_bot"
value={form.telegramBot} value={form.telegramBot}
placeholder="123456789:ABCdefGHIjklMNO..." placeholder="123456789:ABCdefGHIjklMNO..."
onChange={(e) => onChange({ telegramBot: e.target.value })} onChange={(e) => onChange({ telegramBot: e.target.value })}
data-invalid={errors.telegram_bot !== undefined} error={errors.telegram_bot}
/> />
{errors.telegram_bot ? ( <TextField
<p className={styles.fieldError}>{errors.telegram_bot}</p> index="B"
) : null} label="CHAT ID"
</div> name="telegram_chat"
<div className={styles.fieldRow}> value={form.telegramChat}
<FieldLabel index="B">CHAT ID</FieldLabel> placeholder="-1001234567890"
<input onChange={(e) => onChange({ telegramChat: e.target.value })}
className={styles.input} error={errors.telegram_chat}
value={form.telegramChat} />
placeholder="-1001234567890"
onChange={(e) => onChange({ telegramChat: e.target.value })}
data-invalid={errors.telegram_chat !== undefined}
/>
{errors.telegram_chat ? (
<p className={styles.fieldError}>{errors.telegram_chat}</p>
) : null}
</div>
</div> </div>
) )
} }
@ -341,25 +327,17 @@ function WebhookField({
onChange, onChange,
}: SectionPropsBase): React.ReactElement { }: SectionPropsBase): React.ReactElement {
return ( return (
<div className={styles.fieldRow}> <TextField
<FieldLabel index="A">WEBHOOK URL</FieldLabel> index="A"
<input label="WEBHOOK URL"
className={styles.input} name="webhook_url"
type="url" type="url"
value={form.webhookUrl} value={form.webhookUrl}
placeholder="https://hooks.example.com/canary" placeholder="https://hooks.example.com/canary"
onChange={(e) => onChange({ webhookUrl: e.target.value })} onChange={(e) => onChange({ webhookUrl: e.target.value })}
data-invalid={errors.webhook_url !== undefined} error={errors.webhook_url}
/> hint="We POST the alert envelope JSON. Optionally HMAC-signed if the server has WEBHOOK_HMAC_SECRET set."
{errors.webhook_url ? ( />
<p className={styles.fieldError}>{errors.webhook_url}</p>
) : (
<p className={styles.fieldHint}>
We POST the alert envelope JSON. Optionally HMAC-signed if the server
has WEBHOOK_HMAC_SECRET set.
</p>
)}
</div>
) )
} }
@ -395,20 +373,17 @@ function SlowRedirectField({
errors, errors,
onChange, onChange,
}: SectionPropsBase): React.ReactElement { }: SectionPropsBase): React.ReactElement {
const err = errors['metadata.destination_url']
return ( return (
<div className={styles.fieldRow}> <TextField
<FieldLabel index="A">DESTINATION URL</FieldLabel> index="A"
<input label="DESTINATION URL"
className={styles.input} name="metadata_destination_url"
type="url" type="url"
value={form.slowredirectDestination} value={form.slowredirectDestination}
placeholder="https://example.com/landing" placeholder="https://example.com/landing"
onChange={(e) => onChange({ slowredirectDestination: e.target.value })} onChange={(e) => onChange({ slowredirectDestination: e.target.value })}
data-invalid={err !== undefined} error={errors['metadata.destination_url']}
/> />
{err ? <p className={styles.fieldError}>{err}</p> : null}
</div>
) )
} }
@ -469,19 +444,6 @@ function Section({
) )
} }
function FieldLabel({
index,
children,
}: React.PropsWithChildren<{ index?: string }>): React.ReactElement {
return (
<div className={styles.fieldLabel}>
{index ? <span className={styles.fieldIndex}>{index}</span> : null}
<span className={styles.fieldName}>{children}</span>
<span className={styles.fieldRule} aria-hidden="true" />
</div>
)
}
type ChannelToggleProps = { type ChannelToggleProps = {
value: 'telegram' | 'webhook' value: 'telegram' | 'webhook'
onChange: (next: 'telegram' | 'webhook') => void onChange: (next: 'telegram' | 'webhook') => void
@ -608,6 +570,3 @@ function descriptorTitle(type: TokenType | ''): string {
} }
return 'CONFIG' return 'CONFIG'
} }
export { TOKEN_BLURB }
export { ArtifactDisplay }

View File

@ -262,83 +262,6 @@
color: $ink-soft; color: $ink-soft;
} }
.fieldRow {
display: flex;
flex-direction: column;
gap: $space-2;
}
.fieldLabel {
display: flex;
align-items: center;
gap: $space-2;
}
.fieldIndex {
font-family: $font-mono;
font-size: $font-size-3xs;
letter-spacing: $tracking-wider;
color: $ink-mute;
white-space: nowrap;
}
.fieldName {
font-family: $font-mono;
font-size: $font-size-3xs;
font-weight: $font-weight-semibold;
letter-spacing: $tracking-wider;
text-transform: uppercase;
color: $ink;
white-space: nowrap;
}
.fieldRule {
flex: 1 1 auto;
block-size: $hairline;
background-color: $rule-hair;
}
.input,
.textarea {
inline-size: 100%;
padding-block: $space-3;
padding-inline: $space-3;
border: $hairline solid $rule;
background-color: $paper;
font-family: $font-sans;
font-size: $font-size-base;
color: $ink;
appearance: none;
border-radius: 0;
transition: border-color $duration-fast $ease-out;
&::placeholder {
color: $ink-faint;
}
&:focus {
outline: none;
box-shadow: inset 0 0 0 $hairline $ink;
border-color: $ink;
}
&[data-invalid='true'] {
border-color: $alarm;
}
}
.textarea {
min-block-size: 5lh;
resize: vertical;
line-height: $line-height-normal;
}
.fieldHint {
font-size: $font-size-xs;
color: $ink-mute;
margin: 0;
}
.fieldError { .fieldError {
font-family: $font-mono; font-family: $font-mono;
font-size: $font-size-3xs; font-size: $font-size-3xs;

View File

@ -3,7 +3,7 @@
// event-row.tsx // event-row.tsx
// =================== // ===================
import type { EventResponse, GeoView } from '@/api' import type { EventExtra, EventResponse, GeoView } from '@/api'
import { Pill } from '@/components' import { Pill } from '@/components'
import { NOTIFY_TONE } from './copy' import { NOTIFY_TONE } from './copy'
import styles from './manage.module.scss' import styles from './manage.module.scss'
@ -115,8 +115,6 @@ function formatGeo(geo: GeoView): string {
return parts.join(' · ') return parts.join(' · ')
} }
function hasExtraDetails(extra: unknown): boolean { function hasExtraDetails(extra: EventExtra): boolean {
if (extra == null) return false return Object.keys(extra).length > 0
if (typeof extra !== 'object') return false
return Object.keys(extra as Record<string, unknown>).length > 0
} }

View File

@ -110,7 +110,7 @@ function ManageView({
<EventLogSection <EventLogSection
events={events} events={events}
cursor={cursor} cursorIsSet={cursor !== undefined}
hasMore={page.has_more} hasMore={page.has_more}
isFetchingNextPage={isFetchingNextPage} isFetchingNextPage={isFetchingNextPage}
onLoadOlder={onLoadOlder} onLoadOlder={onLoadOlder}
@ -218,7 +218,7 @@ function DossierCard({
type EventLogSectionProps = { type EventLogSectionProps = {
events: ManageResponse['events'] events: ManageResponse['events']
cursor: string | undefined cursorIsSet: boolean
hasMore: boolean hasMore: boolean
isFetchingNextPage: boolean isFetchingNextPage: boolean
onLoadOlder: () => void onLoadOlder: () => void
@ -227,7 +227,7 @@ type EventLogSectionProps = {
function EventLogSection({ function EventLogSection({
events, events,
cursor, cursorIsSet,
hasMore, hasMore,
isFetchingNextPage, isFetchingNextPage,
onLoadOlder, onLoadOlder,
@ -247,16 +247,12 @@ function EventLogSection({
) : ( ) : (
<div className={styles.eventList}> <div className={styles.eventList}>
{events.map((event, idx) => ( {events.map((event, idx) => (
<EventRow <EventRow key={event.id} event={event} index={idx + 1} />
key={event.id}
event={event}
index={cursor === undefined ? idx + 1 : idx + 1}
/>
))} ))}
</div> </div>
)} )}
<footer className={styles.eventLogFoot}> <footer className={styles.eventLogFoot}>
{cursor !== undefined ? ( {cursorIsSet ? (
<Button variant="ghost" size="sm" onClick={onResetCursor}> <Button variant="ghost" size="sm" onClick={onResetCursor}>
back to most recent back to most recent
</Button> </Button>