@feedclip/activity 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ActivityPanel.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type Action, type Activity, type ActivityRecord, type ResourceReference } from "./activity";
|
|
2
|
-
export type ActivityPanelMessages =
|
|
2
|
+
export type ActivityPanelMessages = {
|
|
3
|
+
[Key in keyof typeof defaultMessages]: string;
|
|
4
|
+
};
|
|
3
5
|
export type ActivityPanelProps = {
|
|
4
6
|
activity: Activity;
|
|
5
7
|
resource: ResourceReference;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActivityPanel.d.ts","sourceRoot":"","sources":["../src/ActivityPanel.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,MAAM,EACX,KAAK,QAAQ,EACb,KAAK,cAAc,EAInB,KAAK,iBAAiB,EACvB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,qBAAqB,GAAG,OAAO,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"ActivityPanel.d.ts","sourceRoot":"","sources":["../src/ActivityPanel.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,MAAM,EACX,KAAK,QAAQ,EACb,KAAK,cAAc,EAInB,KAAK,iBAAiB,EACvB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,qBAAqB,GAAG;KACjC,GAAG,IAAI,MAAM,OAAO,eAAe,GAAG,MAAM;CAC9C,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,aAAa,CAAC;IAChD,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAC/C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACjC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACpD,QAAQ,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,EAAE,CAAC,EAAE,IAAI,CAAC;CACX,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,EAAE,CAAC,EAAE,IAAI,CAAC;CACX,CAAC;AAgBF,QAAA,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCX,CAAC;AAEX,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,QAAQ,EACR,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EACP,YAAY,EACZ,OAAO,EACP,aAAa,EACb,MAAM,EACN,OAAmB,EACnB,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,GACP,EAAE,kBAAkB,+BAwJpB"}
|
package/dist/react.cjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/react.ts", "../src/ActivityPanel.tsx", "../src/activity.ts"],
|
|
4
|
-
"sourcesContent": ["export {\n ActivityPanel,\n type ActivityPanelMessages,\n type ActivityPanelProps,\n type ActivityPanelQuery,\n} from \"./ActivityPanel\";\n", "import { useEffect, useMemo, useState } from \"react\";\nimport {\n type Action,\n type Activity,\n type ActivityRecord,\n ActivityError,\n type BuiltInAction,\n type Change,\n type ResourceReference,\n} from \"./activity\";\n\nexport type ActivityPanelMessages = typeof defaultMessages;\n\nexport type ActivityPanelProps = {\n activity: Activity;\n resource: ResourceReference;\n entries?: ActivityRecord[];\n search?: string;\n filters?: ActivityFilters;\n variant?: \"default\" | \"compact\" | \"comfortable\";\n onEntryClick?: (entry: ActivityRecord) => void;\n onError?: (error: Error) => void;\n onQueryChange?: (query: ActivityPanelQuery) => void;\n messages?: Partial<ActivityPanelMessages>;\n locale?: string;\n};\n\ntype ActivityFilters = {\n actions?: readonly Action[];\n actor?: string;\n from?: Date;\n to?: Date;\n};\n\nexport type ActivityPanelQuery = {\n resource: ResourceReference;\n search?: string;\n actions?: readonly Action[];\n actor?: string;\n from?: Date;\n to?: Date;\n};\n\ntype Filter = \"all\" | \"updates\" | \"content\" | \"lifecycle\";\ntype IconName = \"add\" | \"attachment\" | \"box\" | \"chevron-right\" | \"comment\" | \"copy\" | \"file-document\";\ntype EntrySummary = {\n title: string;\n icon: IconName;\n family: \"update\" | \"content\" | \"lifecycle\";\n};\n\nconst filterActions: Record<Exclude<Filter, \"all\">, BuiltInAction[]> = {\n updates: [\"update\"],\n content: [\"comment\", \"attachment\"],\n lifecycle: [\"create\", \"delete\", \"archive\", \"restore\"],\n};\n\nconst defaultMessages = {\n panelLabel: \"Activity history\",\n title: \"Activity\",\n developer: \"Developer\",\n searchLabel: \"Search activity\",\n searchPlaceholder: \"Search...\",\n filterLabel: \"Filter by action family\",\n filterAll: \"All\",\n filterUpdates: \"Updates\",\n filterContent: \"Content\",\n filterLifecycle: \"Lifecycle\",\n loadingLabel: \"Loading activity\",\n entryActionsLabel: \"Entry actions\",\n copyIdLabel: \"Copy activity id\",\n copyIdTitle: \"Copy ID\",\n changedBy: \"Changed by\",\n at: \"At\",\n source: \"Source\",\n version: \"Version\",\n application: \"Application\",\n unversioned: \"Unversioned\",\n noActivity: \"No activity yet\",\n noMatches: \"No matching activity\",\n noActivityHelp: \"Tracked entries for this resource will appear here.\",\n noMatchesHelp: \"Try a broader search or a different action family.\",\n loadError: \"Activity could not load\",\n loadErrorFallback: \"The activity query returned an error.\",\n none: \"None\",\n moreChanges: \"more changes\",\n updated: \"Updated\",\n updatedSuffix: \"updated\",\n comment: \"Comment\",\n attachment: \"Attachment\",\n reason: \"Reason\",\n createdSuffix: \"created\",\n} as const;\n\nexport function ActivityPanel({\n activity,\n resource,\n entries: controlledEntries,\n filters,\n onEntryClick,\n onError,\n onQueryChange,\n search,\n variant = \"default\",\n messages: messageOverrides,\n locale,\n}: ActivityPanelProps) {\n const messages = { ...defaultMessages, ...messageOverrides };\n const actionLabels: Record<Filter, string> = {\n all: messages.filterAll,\n updates: messages.filterUpdates,\n content: messages.filterContent,\n lifecycle: messages.filterLifecycle,\n };\n const [entries, setEntries] = useState<ActivityRecord[]>(controlledEntries ?? []);\n const [status, setStatus] = useState<\"loading\" | \"ready\" | \"error\">(\n controlledEntries ? \"ready\" : \"loading\",\n );\n const [error, setError] = useState<Error | null>(null);\n const [localSearch, setLocalSearch] = useState(\"\");\n const [filter, setFilter] = useState<Filter>(\"all\");\n const [developerMode, setDeveloperMode] = useState(false);\n\n const query = search ?? localSearch;\n const activeActions = useMemo(() => {\n if (filters?.actions?.length) {\n return filters.actions;\n }\n\n return filter === \"all\" ? undefined : filterActions[filter];\n }, [filter, filters?.actions]);\n\n useEffect(() => {\n if (controlledEntries) {\n setEntries(controlledEntries);\n setStatus(\"ready\");\n return;\n }\n\n let cancelled = false;\n setStatus(\"loading\");\n setError(null);\n const queryOptions: ActivityPanelQuery = {\n resource,\n search: query,\n actions: activeActions,\n actor: filters?.actor,\n from: filters?.from,\n to: filters?.to,\n };\n onQueryChange?.(queryOptions);\n\n activity\n .query(queryOptions)\n .then((nextEntries) => {\n if (!cancelled) {\n setEntries(nextEntries);\n setStatus(\"ready\");\n }\n })\n .catch((unknownError: unknown) => {\n const nextError =\n unknownError instanceof Error\n ? unknownError\n : new ActivityError(\"UNKNOWN_ERROR\", \"Activity could not load\");\n\n if (!cancelled) {\n setError(nextError);\n setStatus(\"error\");\n onError?.(nextError);\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [\n activity,\n activeActions,\n controlledEntries,\n filters?.actor,\n filters?.from,\n filters?.to,\n onError,\n onQueryChange,\n query,\n resource,\n ]);\n\n return (\n <section\n className={`activity-panel activity-panel--${variant}`}\n aria-label={messages.panelLabel}\n >\n <header className=\"activity-panel__header\">\n <div>\n <p className=\"eyebrow\">{resource.type}</p>\n <h1>{messages.title}</h1>\n </div>\n <label className=\"developer-toggle\">\n <input\n checked={developerMode}\n onChange={(event) => setDeveloperMode(event.currentTarget.checked)}\n type=\"checkbox\"\n />\n {messages.developer}\n </label>\n </header>\n\n <div className=\"activity-toolbar\">\n <label className=\"search-field\">\n <span aria-hidden=\"true\">/</span>\n <input\n aria-label={messages.searchLabel}\n disabled={search !== undefined}\n onChange={(event) => setLocalSearch(event.currentTarget.value)}\n placeholder={messages.searchPlaceholder}\n value={query}\n />\n </label>\n <div className=\"segmented-control\" aria-label={messages.filterLabel}>\n {(Object.keys(actionLabels) as Filter[]).map((key) => (\n <button\n aria-pressed={filter === key}\n className={filter === key ? \"is-active\" : \"\"}\n disabled={Boolean(filters?.actions?.length)}\n key={key}\n onClick={() => setFilter(key)}\n type=\"button\"\n >\n {actionLabels[key]}\n </button>\n ))}\n </div>\n </div>\n\n {status === \"loading\" ? <ActivityLoading messages={messages} /> : null}\n {status === \"error\" ? <ActivityErrorState error={error} messages={messages} /> : null}\n {status === \"ready\" && entries.length === 0 ? (\n <ActivityEmpty hasQuery={query.trim().length > 0 || filter !== \"all\"} messages={messages} />\n ) : null}\n\n {status === \"ready\" && entries.length > 0 ? (\n <div className=\"activity-stream\">\n {entries.map((entry) => (\n <ActivityEntryRow\n developerMode={developerMode}\n entry={entry}\n key={entry.id}\n onEntryClick={onEntryClick}\n messages={messages}\n locale={locale}\n />\n ))}\n </div>\n ) : null}\n </section>\n );\n}\n\nfunction ActivityEntryRow({\n developerMode,\n entry,\n onEntryClick,\n messages,\n locale,\n}: {\n developerMode: boolean;\n entry: ActivityRecord;\n onEntryClick?: (entry: ActivityRecord) => void;\n messages: ActivityPanelMessages;\n locale?: string;\n}) {\n const [expanded, setExpanded] = useState(false);\n const summary = getEntrySummary(entry, messages);\n return (\n <article className=\"activity-entry\">\n <button\n aria-expanded={expanded}\n className=\"entry-main\"\n onClick={() => {\n setExpanded((value) => !value);\n onEntryClick?.(entry);\n }}\n onKeyDown={(event) => {\n if (event.key === \"Escape\") {\n setExpanded(false);\n return;\n }\n\n if ([\"ArrowDown\", \"ArrowUp\", \"Home\", \"End\"].includes(event.key)) {\n const stream = event.currentTarget.closest(\".activity-stream\");\n const toggles = Array.from(\n stream?.querySelectorAll<HTMLButtonElement>(\"button.entry-main\") ?? [],\n );\n const currentIndex = toggles.indexOf(event.currentTarget);\n const nextIndex =\n event.key === \"Home\"\n ? 0\n : event.key === \"End\"\n ? toggles.length - 1\n : event.key === \"ArrowDown\"\n ? Math.min(currentIndex + 1, toggles.length - 1)\n : Math.max(currentIndex - 1, 0);\n\n if (toggles[nextIndex]) {\n event.preventDefault();\n toggles[nextIndex].focus();\n }\n }\n }}\n type=\"button\"\n >\n <span className={`entry-icon entry-icon--${summary.family}`}>\n <span className={`gg-icon gg-icon--${summary.icon}`} aria-hidden=\"true\" />\n </span>\n <span className=\"entry-body\">\n <span className=\"entry-kicker\">{summary.title}</span>\n <EntryContent entry={entry} expanded={expanded} messages={messages} locale={locale} />\n <span className=\"entry-meta\">\n {entry.actor.name} <span aria-hidden=\"true\">-</span>{\" \"}\n {formatTime(entry.timestamp, locale)}\n </span>\n </span>\n <span className=\"expand-indicator\" aria-hidden=\"true\">\n <span\n className={`gg-icon gg-icon--chevron ${expanded ? \"is-open\" : \"\"}`}\n />\n </span>\n </button>\n <div className=\"entry-actions\" aria-label={messages.entryActionsLabel}>\n <button\n aria-label={messages.copyIdLabel}\n onClick={() => void copyText(entry.id)}\n title={messages.copyIdTitle}\n type=\"button\"\n >\n <span className=\"gg-icon gg-icon--copy\" aria-hidden=\"true\" />\n </button>\n </div>\n\n {expanded ? (\n <div className=\"entry-details\">\n <dl>\n <div>\n <dt>{messages.changedBy}</dt>\n <dd>\n {entry.actor.name} <span>{entry.actor.type}</span>\n </dd>\n </div>\n <div>\n <dt>{messages.at}</dt>\n <dd>{formatFullDate(entry.timestamp, locale)}</dd>\n </div>\n <div>\n <dt>{messages.source}</dt>\n <dd>{readMetadata(entry, \"source\") ?? messages.application}</dd>\n </div>\n <div>\n <dt>{messages.version}</dt>\n <dd>{readMetadata(entry, \"version\") ?? messages.unversioned}</dd>\n </div>\n </dl>\n\n {developerMode ? (\n <pre className=\"developer-json\">\n {JSON.stringify(serializeEntry(entry), null, 2)}\n </pre>\n ) : null}\n </div>\n ) : null}\n </article>\n );\n}\n\nfunction EntryContent({\n entry,\n expanded,\n messages,\n locale,\n}: {\n entry: ActivityRecord;\n expanded: boolean;\n messages: ActivityPanelMessages;\n locale?: string;\n}) {\n if (entry.action === \"update\") {\n return <UpdateChanges changes={entry.changes ?? []} expanded={expanded} messages={messages} locale={locale} />;\n }\n\n if (entry.action === \"comment\" && entry.content?.type === \"comment\") {\n return (\n <span className=\"content-preview\">\n <q>{entry.content.text}</q>\n </span>\n );\n }\n\n if (entry.action === \"attachment\" && entry.content?.type === \"attachment\") {\n return (\n <span className=\"content-preview\">\n <strong>{entry.content.fileName}</strong>\n <small>\n {formatBytes(entry.content.size)} - {entry.content.mimeType}\n </small>\n </span>\n );\n }\n\n return <span className=\"content-preview\">{getLifecycleBody(entry, messages)}</span>;\n}\n\nfunction UpdateChanges({\n changes,\n expanded,\n messages,\n locale,\n}: {\n changes: readonly Readonly<Change>[];\n expanded: boolean;\n messages: ActivityPanelMessages;\n locale?: string;\n}) {\n const visibleChanges = expanded ? changes : changes.slice(0, 3);\n const hiddenCount = changes.length - visibleChanges.length;\n\n return (\n <span className=\"changes-list\">\n {visibleChanges.map((change) => (\n <span className=\"change-row\" key={`${change.field}-${change.label}`}>\n <span>{change.label}</span>\n <span>\n <del>{formatValue(change.before, messages, locale)}</del>\n <ins>{formatValue(change.after, messages, locale)}</ins>\n </span>\n </span>\n ))}\n {hiddenCount > 0 ? (\n <span className=\"more-changes\">+{hiddenCount} {messages.moreChanges}</span>\n ) : null}\n </span>\n );\n}\n\nfunction ActivityLoading({ messages }: { messages: ActivityPanelMessages }) {\n return (\n <div className=\"activity-skeleton\" aria-label={messages.loadingLabel}>\n {Array.from({ length: 4 }, (_, index) => (\n <div className=\"skeleton-row\" key={index}>\n <span />\n <div>\n <b />\n <i />\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nfunction ActivityEmpty({ hasQuery, messages }: { hasQuery: boolean; messages: ActivityPanelMessages }) {\n return (\n <div className=\"empty-state\">\n <span aria-hidden=\"true\">-</span>\n <h2>{hasQuery ? messages.noMatches : messages.noActivity}</h2>\n <p>\n {hasQuery\n ? messages.noMatchesHelp\n : messages.noActivityHelp}\n </p>\n </div>\n );\n}\n\nfunction ActivityErrorState({ error, messages }: { error: Error | null; messages: ActivityPanelMessages }) {\n return (\n <div className=\"empty-state\">\n <h2>{messages.loadError}</h2>\n <p>{error?.message ?? messages.loadErrorFallback}</p>\n </div>\n );\n}\n\nfunction getEntrySummary(entry: ActivityRecord, messages: ActivityPanelMessages): EntrySummary {\n if (entry.action === \"update\") {\n return {\n title:\n (entry.changes?.length ?? 0) > 1\n ? `${titleize(entry.resource.type)} ${messages.updatedSuffix}`\n : entry.changes?.[0]?.label ?? messages.updated,\n icon: \"file-document\",\n family: \"update\",\n };\n }\n\n if (entry.action === \"comment\") {\n return { title: messages.comment, icon: \"comment\", family: \"content\" };\n }\n\n if (entry.action === \"attachment\") {\n return { title: messages.attachment, icon: \"attachment\", family: \"content\" };\n }\n\n return {\n title: titleize(entry.action),\n icon: entry.action === \"archive\" ? \"box\" : \"add\",\n family: \"lifecycle\",\n };\n}\n\nfunction getLifecycleBody(entry: ActivityRecord, messages: ActivityPanelMessages) {\n if (entry.action === \"create\") {\n return entry.resource.title ?? `${entry.resource.type} ${messages.createdSuffix}`;\n }\n\n const reason = readMetadata(entry, \"reason\");\n return reason ? `${messages.reason}: ${reason}` : entry.resource.title ?? titleize(entry.action);\n}\n\nfunction readMetadata(entry: ActivityRecord, key: string) {\n const value = entry.metadata?.[key];\n return typeof value === \"string\" || typeof value === \"number\" ? String(value) : undefined;\n}\n\nfunction formatTime(date: Date, locale?: string) {\n return new Intl.DateTimeFormat(locale, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n }).format(date);\n}\n\nfunction formatFullDate(date: Date, locale?: string) {\n return new Intl.DateTimeFormat(locale, {\n dateStyle: \"medium\",\n timeStyle: \"short\",\n }).format(date);\n}\n\nfunction formatBytes(bytes: number) {\n if (bytes < 1024) {\n return `${bytes} B`;\n }\n\n const kilobytes = bytes / 1024;\n return `${Math.round(kilobytes)} KB`;\n}\n\nfunction formatValue(value: unknown, messages: ActivityPanelMessages, locale?: string) {\n if (value === null || value === undefined) {\n return messages.none;\n }\n\n if (value instanceof Date) {\n return new Intl.DateTimeFormat(locale, { dateStyle: \"medium\" }).format(value);\n }\n\n return typeof value === \"string\" || typeof value === \"number\"\n ? String(value)\n : JSON.stringify(value);\n}\n\nfunction titleize(value: string) {\n return value\n .replace(/[._-]/g, \" \")\n .replace(/\\b\\w/g, (letter) => letter.toUpperCase());\n}\n\nfunction serializeEntry(entry: ActivityRecord) {\n return {\n ...entry,\n timestamp: entry.timestamp.toISOString(),\n };\n}\n\nasync function copyText(value: string) {\n if (globalThis.navigator?.clipboard) {\n await globalThis.navigator.clipboard.writeText(value);\n }\n}\n", "export type ActorType = \"user\" | \"system\" | \"api\" | \"agent\";\n\nexport type BuiltInAction =\n | \"create\"\n | \"update\"\n | \"delete\"\n | \"archive\"\n | \"restore\"\n | \"comment\"\n | \"attachment\";\n\nexport type Action = BuiltInAction | (string & {});\n\nexport type ValueType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"date\"\n | \"currency\"\n | \"user\"\n | \"enum\"\n | \"json\"\n | \"custom\";\n\nexport type Resource = {\n type: string;\n id: string;\n title?: string;\n};\n\nexport type ResourceReference = Pick<Resource, \"type\" | \"id\"> & {\n title?: string;\n};\n\nexport type Actor = {\n type: ActorType;\n id: string;\n name: string;\n avatarUrl?: string;\n};\n\nexport type Change = {\n field: string;\n label: string;\n before?: unknown;\n after?: unknown;\n valueType: ValueType;\n};\n\nexport type CommentContent = {\n type: \"comment\";\n text: string;\n};\n\nexport type AttachmentContent = {\n type: \"attachment\";\n fileName: string;\n mimeType: string;\n size: number;\n url?: string;\n};\n\nexport type CustomContent = {\n type: \"custom\";\n [key: string]: unknown;\n};\n\nexport type Content = CommentContent | AttachmentContent | CustomContent;\n\nexport type Metadata = {\n source?: string;\n version?: string;\n requestId?: string;\n ipAddress?: string;\n custom?: Record<string, unknown>;\n [key: string]: unknown;\n};\n\nexport type ActivityRecord = Readonly<{\n id: string;\n resource: Readonly<Resource>;\n action: Action;\n actor: Readonly<Actor>;\n timestamp: Date;\n changes?: readonly Readonly<Change>[];\n content?: Readonly<Content>;\n metadata?: Readonly<Metadata>;\n}>;\n\nexport type TrackInput = {\n resource: ResourceReference;\n actor: Actor;\n action: Action;\n changes?: Change[];\n content?: Content;\n metadata?: Metadata;\n timestamp?: Date;\n};\n\nexport type QueryOptions = {\n resource: ResourceReference;\n search?: string;\n actor?: string;\n actorId?: string;\n actions?: readonly Action[];\n from?: Date;\n to?: Date;\n limit?: number;\n offset?: number;\n};\n\nexport type QueryResult = {\n entries: ActivityRecord[];\n total: number;\n hasMore: boolean;\n};\n\nexport type StorageAdapter = {\n insert(entry: ActivityRecord): Promise<void>;\n query(options: QueryOptions): Promise<QueryResult>;\n};\n\nexport type Activity = {\n track(input: TrackInput): Promise<ActivityRecord>;\n query(options: QueryOptions): Promise<ActivityRecord[]>;\n};\n\nexport type ActivityOptions = {\n adapter: StorageAdapter;\n clock?: () => Date;\n idGenerator?: () => string;\n};\n\nexport class ActivityError extends Error {\n readonly code: string;\n readonly field?: string;\n\n constructor(code: string, message: string, field?: string) {\n super(message);\n this.name = \"ActivityError\";\n this.code = code;\n this.field = field;\n }\n}\n\nexport function createActivity({\n adapter,\n clock = () => new Date(),\n idGenerator = createId,\n}: ActivityOptions): Activity {\n return {\n async track(input) {\n const record = createRecord(input, clock, idGenerator);\n await adapter.insert(record);\n return record;\n },\n async query(options) {\n const normalized = normalizeQuery(options);\n const result = await adapter.query(normalized);\n return result.entries;\n },\n };\n}\n\nexport function createMemoryStorageAdapter(\n initialEntries: ActivityRecord[] = [],\n latency = 0,\n): StorageAdapter {\n let entries = initialEntries.map(freezeRecord);\n\n return {\n async insert(entry) {\n await wait(latency);\n entries = [freezeRecord(entry), ...entries];\n },\n async query(options) {\n await wait(latency);\n const normalized = normalizeQuery(options);\n const filtered = entries\n .filter((entry) => entry.resource.type === normalized.resource.type)\n .filter((entry) => entry.resource.id === normalized.resource.id)\n .filter((entry) => matchesActions(entry, normalized.actions))\n .filter((entry) => matchesActor(entry, normalized.actorId ?? normalized.actor))\n .filter((entry) => matchesDates(entry, normalized.from, normalized.to))\n .filter((entry) => matchesSearch(entry, normalized.search))\n .sort(compareRecords);\n const offset = normalized.offset ?? 0;\n const limit = normalized.limit ?? 50;\n const page = filtered.slice(offset, offset + limit);\n\n return {\n entries: page,\n total: filtered.length,\n hasMore: offset + page.length < filtered.length,\n };\n },\n };\n}\n\ntype Queryable = {\n query(sql: string, params?: readonly unknown[]): Promise<{ rows: unknown[] }>;\n};\n\ntype ReleasableQueryable = Queryable & { release(): void };\ntype Connectable = Queryable & { connect(): Promise<ReleasableQueryable> };\n\nexport function postgresAdapter(client: Queryable | Connectable): StorageAdapter {\n return {\n async insert(entry) {\n const connection = \"connect\" in client ? await client.connect() : client;\n await connection.query(\"BEGIN\");\n try {\n await connection.query(\n `insert into activity_entries (\n id, resource_type, resource_id, resource_title, action,\n actor_type, actor_id, actor_name, actor_avatar_url,\n content_type, content_json, metadata_json, created_at\n ) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,\n [\n entry.id,\n entry.resource.type,\n entry.resource.id,\n entry.resource.title ?? null,\n entry.action,\n entry.actor.type,\n entry.actor.id,\n entry.actor.name,\n entry.actor.avatarUrl ?? null,\n entry.content?.type ?? null,\n entry.content ? JSON.stringify(entry.content) : null,\n entry.metadata ? JSON.stringify(entry.metadata) : null,\n entry.timestamp.toISOString(),\n ],\n );\n\n for (const [position, change] of (entry.changes ?? []).entries()) {\n await connection.query(\n `insert into activity_changes (\n id, entry_id, position, field, label, before_value, after_value, value_type\n ) values ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [\n createId(),\n entry.id,\n position,\n change.field,\n change.label,\n JSON.stringify(change.before ?? null),\n JSON.stringify(change.after ?? null),\n change.valueType,\n ],\n );\n }\n\n await connection.query(\"COMMIT\");\n } catch (error) {\n await connection.query(\"ROLLBACK\");\n throw toActivityError(error, \"STORAGE_FAILURE\", \"Could not insert activity record\");\n } finally {\n if (\"release\" in connection && typeof connection.release === \"function\") {\n connection.release();\n }\n }\n },\n async query(options) {\n const normalized = normalizeQuery(options);\n const limit = normalized.limit ?? 50;\n const offset = normalized.offset ?? 0;\n const params: unknown[] = [normalized.resource.type, normalized.resource.id];\n const where = [\"e.resource_type = $1\", \"e.resource_id = $2\"];\n\n if (normalized.actions?.length) {\n params.push(normalized.actions);\n where.push(`e.action = any($${params.length}::text[])`);\n }\n\n const actorId = normalized.actorId ?? normalized.actor;\n if (actorId) {\n params.push(actorId);\n where.push(`e.actor_id = $${params.length}`);\n }\n\n if (normalized.from) {\n params.push(normalized.from.toISOString());\n where.push(`e.created_at >= $${params.length}`);\n }\n\n if (normalized.to) {\n params.push(normalized.to.toISOString());\n where.push(`e.created_at <= $${params.length}`);\n }\n\n if (normalized.search) {\n params.push(`%${normalized.search}%`);\n const index = params.length;\n where.push(`(\n e.actor_name ilike $${index}\n or e.resource_title ilike $${index}\n or e.content_json::text ilike $${index}\n or exists (\n select 1 from activity_changes c\n where c.entry_id = e.id\n and (\n c.label ilike $${index}\n or c.before_value::text ilike $${index}\n or c.after_value::text ilike $${index}\n )\n )\n )`);\n }\n\n const whereSql = where.join(\" and \");\n const count = await client.query(\n `select count(*)::int as total from activity_entries e where ${whereSql}`,\n params,\n );\n params.push(limit + 1, offset);\n const rows = await client.query(\n `select\n e.*,\n coalesce(\n json_agg(\n json_build_object(\n 'field', c.field,\n 'label', c.label,\n 'before', c.before_value,\n 'after', c.after_value,\n 'valueType', c.value_type\n )\n order by c.position\n ) filter (where c.id is not null),\n '[]'\n ) as changes\n from activity_entries e\n left join activity_changes c on c.entry_id = e.id\n where ${whereSql}\n group by e.id\n order by e.created_at desc, e.id desc\n limit $${params.length - 1} offset $${params.length}`,\n params,\n );\n\n const total = readTotal(count.rows[0]);\n const entries = rows.rows.slice(0, limit).map(mapPostgresRecord);\n\n return {\n entries,\n total,\n hasMore: rows.rows.length > limit,\n };\n },\n };\n}\n\nfunction createRecord(\n input: TrackInput,\n clock: () => Date,\n idGenerator: () => string,\n): ActivityRecord {\n validateTrackInput(input);\n\n const record: ActivityRecord = {\n id: idGenerator(),\n resource: freezeObject({\n type: input.resource.type.trim(),\n id: input.resource.id.trim(),\n title: input.resource.title?.trim(),\n }),\n action: input.action.trim().toLowerCase() as Action,\n actor: freezeObject({ ...input.actor, name: input.actor.name.trim() }),\n timestamp: input.timestamp ? new Date(input.timestamp) : clock(),\n changes: input.changes?.map(normalizeChange).map(freezeObject),\n content: input.content ? freezeObject(input.content) : undefined,\n metadata: input.metadata ? freezeObject(input.metadata) : undefined,\n };\n\n return freezeRecord(record);\n}\n\nfunction validateTrackInput(input: TrackInput) {\n if (!input.resource?.type?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"resource.type is required\", \"resource.type\");\n }\n\n if (!input.resource?.id?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"resource.id is required\", \"resource.id\");\n }\n\n if (![\"user\", \"system\", \"api\", \"agent\"].includes(input.actor?.type)) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.type is invalid\", \"actor.type\");\n }\n\n if (!input.actor?.id?.trim()) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.id is required\", \"actor.id\");\n }\n\n if (!input.actor?.name?.trim()) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.name is required\", \"actor.name\");\n }\n\n if (!input.action?.trim()) {\n throw new ActivityError(\"INVALID_ACTION\", \"action is required\", \"action\");\n }\n\n if (input.timestamp && Number.isNaN(input.timestamp.getTime())) {\n throw new ActivityError(\"INVALID_TIMESTAMP\", \"timestamp must be a valid Date\", \"timestamp\");\n }\n\n if (input.action === \"update\" && !input.changes?.length) {\n throw new ActivityError(\"INVALID_ACTION\", \"update requires at least one change\", \"changes\");\n }\n\n if ((input.action === \"comment\" || input.action === \"attachment\") && !input.content) {\n throw new ActivityError(\"INVALID_ACTION\", `${input.action} requires content`, \"content\");\n }\n}\n\nfunction normalizeChange(change: Change): Change {\n if (!change.field.trim()) {\n throw new ActivityError(\"INVALID_CHANGE\", \"change.field is required\", \"changes.field\");\n }\n\n if (!change.label.trim()) {\n throw new ActivityError(\"INVALID_CHANGE\", \"change.label is required\", \"changes.label\");\n }\n\n return {\n ...change,\n field: change.field.trim(),\n label: change.label.trim(),\n valueType: change.valueType ?? inferValueType(change.after ?? change.before),\n };\n}\n\nfunction normalizeQuery(options: QueryOptions): QueryOptions {\n if (!options.resource?.type?.trim() || !options.resource?.id?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"query.resource is required\", \"resource\");\n }\n\n if (options.limit !== undefined && (options.limit < 1 || options.limit > 500)) {\n throw new ActivityError(\"INVALID_LIMIT\", \"limit must be between 1 and 500\", \"limit\");\n }\n\n if (options.offset !== undefined && options.offset < 0) {\n throw new ActivityError(\"INVALID_OFFSET\", \"offset must not be negative\", \"offset\");\n }\n\n if (options.from && options.to && options.from > options.to) {\n throw new ActivityError(\"INVALID_DATE_RANGE\", \"from must be before to\", \"from\");\n }\n\n return freezeObject({\n ...options,\n resource: freezeObject({\n type: options.resource.type.trim(),\n id: options.resource.id.trim(),\n title: options.resource.title?.trim(),\n }),\n search: options.search?.trim() || undefined,\n actor: options.actor?.trim() || undefined,\n actorId: options.actorId?.trim() || undefined,\n actions: options.actions\n ? Object.freeze(\n Array.from(\n new Set(options.actions.map((action) => action.trim().toLowerCase() as Action)),\n ),\n )\n : undefined,\n limit: options.limit ?? 50,\n offset: options.offset ?? 0,\n });\n}\n\nfunction matchesActions(entry: ActivityRecord, actions?: readonly Action[]) {\n return !actions?.length || actions.includes(entry.action);\n}\n\nfunction matchesActor(entry: ActivityRecord, actorId?: string) {\n return !actorId || entry.actor.id === actorId;\n}\n\nfunction matchesDates(entry: ActivityRecord, from?: Date, to?: Date) {\n return (!from || entry.timestamp >= from) && (!to || entry.timestamp <= to);\n}\n\nfunction matchesSearch(entry: ActivityRecord, search?: string) {\n if (!search) {\n return true;\n }\n\n return searchableText(entry).toLowerCase().includes(search.toLowerCase());\n}\n\nfunction searchableText(entry: ActivityRecord) {\n return [\n entry.action,\n entry.resource.title,\n entry.actor.name,\n entry.content ? JSON.stringify(entry.content) : \"\",\n ...(entry.changes ?? []).flatMap((change) => [\n change.field,\n change.label,\n stringifyValue(change.before),\n stringifyValue(change.after),\n ]),\n ].join(\" \");\n}\n\nfunction compareRecords(a: ActivityRecord, b: ActivityRecord) {\n const time = b.timestamp.getTime() - a.timestamp.getTime();\n return time || b.id.localeCompare(a.id);\n}\n\nfunction inferValueType(value: unknown): ValueType {\n if (value instanceof Date) {\n return \"date\";\n }\n\n const valueType = typeof value;\n if (valueType === \"string\" || valueType === \"number\" || valueType === \"boolean\") {\n return valueType;\n }\n\n return \"json\";\n}\n\nfunction freezeRecord(record: ActivityRecord): ActivityRecord {\n return freezeObject({\n ...record,\n resource: freezeObject(record.resource),\n actor: freezeObject(record.actor),\n changes: record.changes?.map((change) => freezeObject(change)),\n content: record.content ? freezeObject(record.content) : undefined,\n metadata: record.metadata ? freezeObject(record.metadata) : undefined,\n });\n}\n\nfunction freezeObject<T extends object>(value: T): Readonly<T> {\n return Object.freeze(value);\n}\n\nfunction stringifyValue(value: unknown) {\n if (value === null || value === undefined) {\n return \"\";\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n return typeof value === \"string\" ? value : JSON.stringify(value);\n}\n\nfunction createId() {\n if (globalThis.crypto?.randomUUID) {\n return globalThis.crypto.randomUUID();\n }\n\n return `act_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;\n}\n\nfunction wait(ms: number) {\n return new Promise((resolve) => globalThis.setTimeout(resolve, ms));\n}\n\nfunction readTotal(row: unknown) {\n if (isRecord(row) && typeof row.total === \"number\") {\n return row.total;\n }\n\n if (isRecord(row) && typeof row.total === \"string\") {\n return Number(row.total);\n }\n\n return 0;\n}\n\nfunction mapPostgresRecord(row: unknown): ActivityRecord {\n if (!isRecord(row)) {\n throw new ActivityError(\"STORAGE_FAILURE\", \"Invalid activity row returned by storage\");\n }\n\n return freezeRecord({\n id: String(row.id),\n resource: {\n type: String(row.resource_type),\n id: String(row.resource_id),\n title: typeof row.resource_title === \"string\" ? row.resource_title : undefined,\n },\n action: String(row.action) as Action,\n actor: {\n type: String(row.actor_type) as ActorType,\n id: String(row.actor_id),\n name: String(row.actor_name),\n avatarUrl: typeof row.actor_avatar_url === \"string\" ? row.actor_avatar_url : undefined,\n },\n timestamp: new Date(String(row.created_at)),\n changes: Array.isArray(row.changes) ? row.changes.map(mapPostgresChange) : undefined,\n content: isContent(row.content_json) ? row.content_json : undefined,\n metadata: isRecord(row.metadata_json) ? row.metadata_json : undefined,\n });\n}\n\nfunction mapPostgresChange(change: unknown): Change {\n if (!isRecord(change)) {\n throw new ActivityError(\"STORAGE_FAILURE\", \"Invalid change row returned by storage\");\n }\n\n return {\n field: String(change.field),\n label: String(change.label),\n before: change.before,\n after: change.after,\n valueType: String(change.valueType) as ValueType,\n };\n}\n\nfunction isContent(value: unknown): value is Content {\n return isRecord(value) && typeof value.type === \"string\";\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction toActivityError(error: unknown, code: string, fallback: string) {\n if (error instanceof ActivityError) {\n return error;\n }\n\n if (error instanceof Error) {\n return new ActivityError(code, error.message);\n }\n\n return new ActivityError(code, fallback);\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAA6C;;;ACqItC,IAAM,gBAAN,cAA4B,MAAM;AAAA,EAIvC,YAAY,MAAc,SAAiB,OAAgB;AACzD,UAAM,OAAO;AAJf,wBAAS;AACT,wBAAS;AAIP,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,QAAQ;AAAA,EACf;AACF;;;
|
|
4
|
+
"sourcesContent": ["export {\n ActivityPanel,\n type ActivityPanelMessages,\n type ActivityPanelProps,\n type ActivityPanelQuery,\n} from \"./ActivityPanel\";\n", "import { useEffect, useMemo, useState } from \"react\";\nimport {\n type Action,\n type Activity,\n type ActivityRecord,\n ActivityError,\n type BuiltInAction,\n type Change,\n type ResourceReference,\n} from \"./activity\";\n\nexport type ActivityPanelMessages = {\n [Key in keyof typeof defaultMessages]: string;\n};\n\nexport type ActivityPanelProps = {\n activity: Activity;\n resource: ResourceReference;\n entries?: ActivityRecord[];\n search?: string;\n filters?: ActivityFilters;\n variant?: \"default\" | \"compact\" | \"comfortable\";\n onEntryClick?: (entry: ActivityRecord) => void;\n onError?: (error: Error) => void;\n onQueryChange?: (query: ActivityPanelQuery) => void;\n messages?: Partial<ActivityPanelMessages>;\n locale?: string;\n};\n\ntype ActivityFilters = {\n actions?: readonly Action[];\n actor?: string;\n from?: Date;\n to?: Date;\n};\n\nexport type ActivityPanelQuery = {\n resource: ResourceReference;\n search?: string;\n actions?: readonly Action[];\n actor?: string;\n from?: Date;\n to?: Date;\n};\n\ntype Filter = \"all\" | \"updates\" | \"content\" | \"lifecycle\";\ntype IconName = \"add\" | \"attachment\" | \"box\" | \"chevron-right\" | \"comment\" | \"copy\" | \"file-document\";\ntype EntrySummary = {\n title: string;\n icon: IconName;\n family: \"update\" | \"content\" | \"lifecycle\";\n};\n\nconst filterActions: Record<Exclude<Filter, \"all\">, BuiltInAction[]> = {\n updates: [\"update\"],\n content: [\"comment\", \"attachment\"],\n lifecycle: [\"create\", \"delete\", \"archive\", \"restore\"],\n};\n\nconst defaultMessages = {\n panelLabel: \"Activity history\",\n title: \"Activity\",\n developer: \"Developer\",\n searchLabel: \"Search activity\",\n searchPlaceholder: \"Search...\",\n filterLabel: \"Filter by action family\",\n filterAll: \"All\",\n filterUpdates: \"Updates\",\n filterContent: \"Content\",\n filterLifecycle: \"Lifecycle\",\n loadingLabel: \"Loading activity\",\n entryActionsLabel: \"Entry actions\",\n copyIdLabel: \"Copy activity id\",\n copyIdTitle: \"Copy ID\",\n changedBy: \"Changed by\",\n at: \"At\",\n source: \"Source\",\n version: \"Version\",\n application: \"Application\",\n unversioned: \"Unversioned\",\n noActivity: \"No activity yet\",\n noMatches: \"No matching activity\",\n noActivityHelp: \"Tracked entries for this resource will appear here.\",\n noMatchesHelp: \"Try a broader search or a different action family.\",\n loadError: \"Activity could not load\",\n loadErrorFallback: \"The activity query returned an error.\",\n none: \"None\",\n moreChanges: \"more changes\",\n updated: \"Updated\",\n updatedSuffix: \"updated\",\n comment: \"Comment\",\n attachment: \"Attachment\",\n reason: \"Reason\",\n createdSuffix: \"created\",\n} as const;\n\nexport function ActivityPanel({\n activity,\n resource,\n entries: controlledEntries,\n filters,\n onEntryClick,\n onError,\n onQueryChange,\n search,\n variant = \"default\",\n messages: messageOverrides,\n locale,\n}: ActivityPanelProps) {\n const messages = { ...defaultMessages, ...messageOverrides };\n const actionLabels: Record<Filter, string> = {\n all: messages.filterAll,\n updates: messages.filterUpdates,\n content: messages.filterContent,\n lifecycle: messages.filterLifecycle,\n };\n const [entries, setEntries] = useState<ActivityRecord[]>(controlledEntries ?? []);\n const [status, setStatus] = useState<\"loading\" | \"ready\" | \"error\">(\n controlledEntries ? \"ready\" : \"loading\",\n );\n const [error, setError] = useState<Error | null>(null);\n const [localSearch, setLocalSearch] = useState(\"\");\n const [filter, setFilter] = useState<Filter>(\"all\");\n const [developerMode, setDeveloperMode] = useState(false);\n\n const query = search ?? localSearch;\n const activeActions = useMemo(() => {\n if (filters?.actions?.length) {\n return filters.actions;\n }\n\n return filter === \"all\" ? undefined : filterActions[filter];\n }, [filter, filters?.actions]);\n\n useEffect(() => {\n if (controlledEntries) {\n setEntries(controlledEntries);\n setStatus(\"ready\");\n return;\n }\n\n let cancelled = false;\n setStatus(\"loading\");\n setError(null);\n const queryOptions: ActivityPanelQuery = {\n resource,\n search: query,\n actions: activeActions,\n actor: filters?.actor,\n from: filters?.from,\n to: filters?.to,\n };\n onQueryChange?.(queryOptions);\n\n activity\n .query(queryOptions)\n .then((nextEntries) => {\n if (!cancelled) {\n setEntries(nextEntries);\n setStatus(\"ready\");\n }\n })\n .catch((unknownError: unknown) => {\n const nextError =\n unknownError instanceof Error\n ? unknownError\n : new ActivityError(\"UNKNOWN_ERROR\", \"Activity could not load\");\n\n if (!cancelled) {\n setError(nextError);\n setStatus(\"error\");\n onError?.(nextError);\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [\n activity,\n activeActions,\n controlledEntries,\n filters?.actor,\n filters?.from,\n filters?.to,\n onError,\n onQueryChange,\n query,\n resource,\n ]);\n\n return (\n <section\n className={`activity-panel activity-panel--${variant}`}\n aria-label={messages.panelLabel}\n >\n <header className=\"activity-panel__header\">\n <div>\n <p className=\"eyebrow\">{resource.type}</p>\n <h1>{messages.title}</h1>\n </div>\n <label className=\"developer-toggle\">\n <input\n checked={developerMode}\n onChange={(event) => setDeveloperMode(event.currentTarget.checked)}\n type=\"checkbox\"\n />\n {messages.developer}\n </label>\n </header>\n\n <div className=\"activity-toolbar\">\n <label className=\"search-field\">\n <span aria-hidden=\"true\">/</span>\n <input\n aria-label={messages.searchLabel}\n disabled={search !== undefined}\n onChange={(event) => setLocalSearch(event.currentTarget.value)}\n placeholder={messages.searchPlaceholder}\n value={query}\n />\n </label>\n <div className=\"segmented-control\" aria-label={messages.filterLabel}>\n {(Object.keys(actionLabels) as Filter[]).map((key) => (\n <button\n aria-pressed={filter === key}\n className={filter === key ? \"is-active\" : \"\"}\n disabled={Boolean(filters?.actions?.length)}\n key={key}\n onClick={() => setFilter(key)}\n type=\"button\"\n >\n {actionLabels[key]}\n </button>\n ))}\n </div>\n </div>\n\n {status === \"loading\" ? <ActivityLoading messages={messages} /> : null}\n {status === \"error\" ? <ActivityErrorState error={error} messages={messages} /> : null}\n {status === \"ready\" && entries.length === 0 ? (\n <ActivityEmpty hasQuery={query.trim().length > 0 || filter !== \"all\"} messages={messages} />\n ) : null}\n\n {status === \"ready\" && entries.length > 0 ? (\n <div className=\"activity-stream\">\n {entries.map((entry) => (\n <ActivityEntryRow\n developerMode={developerMode}\n entry={entry}\n key={entry.id}\n onEntryClick={onEntryClick}\n messages={messages}\n locale={locale}\n />\n ))}\n </div>\n ) : null}\n </section>\n );\n}\n\nfunction ActivityEntryRow({\n developerMode,\n entry,\n onEntryClick,\n messages,\n locale,\n}: {\n developerMode: boolean;\n entry: ActivityRecord;\n onEntryClick?: (entry: ActivityRecord) => void;\n messages: ActivityPanelMessages;\n locale?: string;\n}) {\n const [expanded, setExpanded] = useState(false);\n const summary = getEntrySummary(entry, messages);\n return (\n <article className=\"activity-entry\">\n <button\n aria-expanded={expanded}\n className=\"entry-main\"\n onClick={() => {\n setExpanded((value) => !value);\n onEntryClick?.(entry);\n }}\n onKeyDown={(event) => {\n if (event.key === \"Escape\") {\n setExpanded(false);\n return;\n }\n\n if ([\"ArrowDown\", \"ArrowUp\", \"Home\", \"End\"].includes(event.key)) {\n const stream = event.currentTarget.closest(\".activity-stream\");\n const toggles = Array.from(\n stream?.querySelectorAll<HTMLButtonElement>(\"button.entry-main\") ?? [],\n );\n const currentIndex = toggles.indexOf(event.currentTarget);\n const nextIndex =\n event.key === \"Home\"\n ? 0\n : event.key === \"End\"\n ? toggles.length - 1\n : event.key === \"ArrowDown\"\n ? Math.min(currentIndex + 1, toggles.length - 1)\n : Math.max(currentIndex - 1, 0);\n\n if (toggles[nextIndex]) {\n event.preventDefault();\n toggles[nextIndex].focus();\n }\n }\n }}\n type=\"button\"\n >\n <span className={`entry-icon entry-icon--${summary.family}`}>\n <span className={`gg-icon gg-icon--${summary.icon}`} aria-hidden=\"true\" />\n </span>\n <span className=\"entry-body\">\n <span className=\"entry-kicker\">{summary.title}</span>\n <EntryContent entry={entry} expanded={expanded} messages={messages} locale={locale} />\n <span className=\"entry-meta\">\n {entry.actor.name} <span aria-hidden=\"true\">-</span>{\" \"}\n {formatTime(entry.timestamp, locale)}\n </span>\n </span>\n <span className=\"expand-indicator\" aria-hidden=\"true\">\n <span\n className={`gg-icon gg-icon--chevron ${expanded ? \"is-open\" : \"\"}`}\n />\n </span>\n </button>\n <div className=\"entry-actions\" aria-label={messages.entryActionsLabel}>\n <button\n aria-label={messages.copyIdLabel}\n onClick={() => void copyText(entry.id)}\n title={messages.copyIdTitle}\n type=\"button\"\n >\n <span className=\"gg-icon gg-icon--copy\" aria-hidden=\"true\" />\n </button>\n </div>\n\n {expanded ? (\n <div className=\"entry-details\">\n <dl>\n <div>\n <dt>{messages.changedBy}</dt>\n <dd>\n {entry.actor.name} <span>{entry.actor.type}</span>\n </dd>\n </div>\n <div>\n <dt>{messages.at}</dt>\n <dd>{formatFullDate(entry.timestamp, locale)}</dd>\n </div>\n <div>\n <dt>{messages.source}</dt>\n <dd>{readMetadata(entry, \"source\") ?? messages.application}</dd>\n </div>\n <div>\n <dt>{messages.version}</dt>\n <dd>{readMetadata(entry, \"version\") ?? messages.unversioned}</dd>\n </div>\n </dl>\n\n {developerMode ? (\n <pre className=\"developer-json\">\n {JSON.stringify(serializeEntry(entry), null, 2)}\n </pre>\n ) : null}\n </div>\n ) : null}\n </article>\n );\n}\n\nfunction EntryContent({\n entry,\n expanded,\n messages,\n locale,\n}: {\n entry: ActivityRecord;\n expanded: boolean;\n messages: ActivityPanelMessages;\n locale?: string;\n}) {\n if (entry.action === \"update\") {\n return <UpdateChanges changes={entry.changes ?? []} expanded={expanded} messages={messages} locale={locale} />;\n }\n\n if (entry.action === \"comment\" && entry.content?.type === \"comment\") {\n return (\n <span className=\"content-preview\">\n <q>{entry.content.text}</q>\n </span>\n );\n }\n\n if (entry.action === \"attachment\" && entry.content?.type === \"attachment\") {\n return (\n <span className=\"content-preview\">\n <strong>{entry.content.fileName}</strong>\n <small>\n {formatBytes(entry.content.size)} - {entry.content.mimeType}\n </small>\n </span>\n );\n }\n\n return <span className=\"content-preview\">{getLifecycleBody(entry, messages)}</span>;\n}\n\nfunction UpdateChanges({\n changes,\n expanded,\n messages,\n locale,\n}: {\n changes: readonly Readonly<Change>[];\n expanded: boolean;\n messages: ActivityPanelMessages;\n locale?: string;\n}) {\n const visibleChanges = expanded ? changes : changes.slice(0, 3);\n const hiddenCount = changes.length - visibleChanges.length;\n\n return (\n <span className=\"changes-list\">\n {visibleChanges.map((change) => (\n <span className=\"change-row\" key={`${change.field}-${change.label}`}>\n <span>{change.label}</span>\n <span>\n <del>{formatValue(change.before, messages, locale)}</del>\n <ins>{formatValue(change.after, messages, locale)}</ins>\n </span>\n </span>\n ))}\n {hiddenCount > 0 ? (\n <span className=\"more-changes\">+{hiddenCount} {messages.moreChanges}</span>\n ) : null}\n </span>\n );\n}\n\nfunction ActivityLoading({ messages }: { messages: ActivityPanelMessages }) {\n return (\n <div className=\"activity-skeleton\" aria-label={messages.loadingLabel}>\n {Array.from({ length: 4 }, (_, index) => (\n <div className=\"skeleton-row\" key={index}>\n <span />\n <div>\n <b />\n <i />\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nfunction ActivityEmpty({ hasQuery, messages }: { hasQuery: boolean; messages: ActivityPanelMessages }) {\n return (\n <div className=\"empty-state\">\n <span aria-hidden=\"true\">-</span>\n <h2>{hasQuery ? messages.noMatches : messages.noActivity}</h2>\n <p>\n {hasQuery\n ? messages.noMatchesHelp\n : messages.noActivityHelp}\n </p>\n </div>\n );\n}\n\nfunction ActivityErrorState({ error, messages }: { error: Error | null; messages: ActivityPanelMessages }) {\n return (\n <div className=\"empty-state\">\n <h2>{messages.loadError}</h2>\n <p>{error?.message ?? messages.loadErrorFallback}</p>\n </div>\n );\n}\n\nfunction getEntrySummary(entry: ActivityRecord, messages: ActivityPanelMessages): EntrySummary {\n if (entry.action === \"update\") {\n return {\n title:\n (entry.changes?.length ?? 0) > 1\n ? `${titleize(entry.resource.type)} ${messages.updatedSuffix}`\n : entry.changes?.[0]?.label ?? messages.updated,\n icon: \"file-document\",\n family: \"update\",\n };\n }\n\n if (entry.action === \"comment\") {\n return { title: messages.comment, icon: \"comment\", family: \"content\" };\n }\n\n if (entry.action === \"attachment\") {\n return { title: messages.attachment, icon: \"attachment\", family: \"content\" };\n }\n\n return {\n title: titleize(entry.action),\n icon: entry.action === \"archive\" ? \"box\" : \"add\",\n family: \"lifecycle\",\n };\n}\n\nfunction getLifecycleBody(entry: ActivityRecord, messages: ActivityPanelMessages) {\n if (entry.action === \"create\") {\n return entry.resource.title ?? `${entry.resource.type} ${messages.createdSuffix}`;\n }\n\n const reason = readMetadata(entry, \"reason\");\n return reason ? `${messages.reason}: ${reason}` : entry.resource.title ?? titleize(entry.action);\n}\n\nfunction readMetadata(entry: ActivityRecord, key: string) {\n const value = entry.metadata?.[key];\n return typeof value === \"string\" || typeof value === \"number\" ? String(value) : undefined;\n}\n\nfunction formatTime(date: Date, locale?: string) {\n return new Intl.DateTimeFormat(locale, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n }).format(date);\n}\n\nfunction formatFullDate(date: Date, locale?: string) {\n return new Intl.DateTimeFormat(locale, {\n dateStyle: \"medium\",\n timeStyle: \"short\",\n }).format(date);\n}\n\nfunction formatBytes(bytes: number) {\n if (bytes < 1024) {\n return `${bytes} B`;\n }\n\n const kilobytes = bytes / 1024;\n return `${Math.round(kilobytes)} KB`;\n}\n\nfunction formatValue(value: unknown, messages: ActivityPanelMessages, locale?: string) {\n if (value === null || value === undefined) {\n return messages.none;\n }\n\n if (value instanceof Date) {\n return new Intl.DateTimeFormat(locale, { dateStyle: \"medium\" }).format(value);\n }\n\n return typeof value === \"string\" || typeof value === \"number\"\n ? String(value)\n : JSON.stringify(value);\n}\n\nfunction titleize(value: string) {\n return value\n .replace(/[._-]/g, \" \")\n .replace(/\\b\\w/g, (letter) => letter.toUpperCase());\n}\n\nfunction serializeEntry(entry: ActivityRecord) {\n return {\n ...entry,\n timestamp: entry.timestamp.toISOString(),\n };\n}\n\nasync function copyText(value: string) {\n if (globalThis.navigator?.clipboard) {\n await globalThis.navigator.clipboard.writeText(value);\n }\n}\n", "export type ActorType = \"user\" | \"system\" | \"api\" | \"agent\";\n\nexport type BuiltInAction =\n | \"create\"\n | \"update\"\n | \"delete\"\n | \"archive\"\n | \"restore\"\n | \"comment\"\n | \"attachment\";\n\nexport type Action = BuiltInAction | (string & {});\n\nexport type ValueType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"date\"\n | \"currency\"\n | \"user\"\n | \"enum\"\n | \"json\"\n | \"custom\";\n\nexport type Resource = {\n type: string;\n id: string;\n title?: string;\n};\n\nexport type ResourceReference = Pick<Resource, \"type\" | \"id\"> & {\n title?: string;\n};\n\nexport type Actor = {\n type: ActorType;\n id: string;\n name: string;\n avatarUrl?: string;\n};\n\nexport type Change = {\n field: string;\n label: string;\n before?: unknown;\n after?: unknown;\n valueType: ValueType;\n};\n\nexport type CommentContent = {\n type: \"comment\";\n text: string;\n};\n\nexport type AttachmentContent = {\n type: \"attachment\";\n fileName: string;\n mimeType: string;\n size: number;\n url?: string;\n};\n\nexport type CustomContent = {\n type: \"custom\";\n [key: string]: unknown;\n};\n\nexport type Content = CommentContent | AttachmentContent | CustomContent;\n\nexport type Metadata = {\n source?: string;\n version?: string;\n requestId?: string;\n ipAddress?: string;\n custom?: Record<string, unknown>;\n [key: string]: unknown;\n};\n\nexport type ActivityRecord = Readonly<{\n id: string;\n resource: Readonly<Resource>;\n action: Action;\n actor: Readonly<Actor>;\n timestamp: Date;\n changes?: readonly Readonly<Change>[];\n content?: Readonly<Content>;\n metadata?: Readonly<Metadata>;\n}>;\n\nexport type TrackInput = {\n resource: ResourceReference;\n actor: Actor;\n action: Action;\n changes?: Change[];\n content?: Content;\n metadata?: Metadata;\n timestamp?: Date;\n};\n\nexport type QueryOptions = {\n resource: ResourceReference;\n search?: string;\n actor?: string;\n actorId?: string;\n actions?: readonly Action[];\n from?: Date;\n to?: Date;\n limit?: number;\n offset?: number;\n};\n\nexport type QueryResult = {\n entries: ActivityRecord[];\n total: number;\n hasMore: boolean;\n};\n\nexport type StorageAdapter = {\n insert(entry: ActivityRecord): Promise<void>;\n query(options: QueryOptions): Promise<QueryResult>;\n};\n\nexport type Activity = {\n track(input: TrackInput): Promise<ActivityRecord>;\n query(options: QueryOptions): Promise<ActivityRecord[]>;\n};\n\nexport type ActivityOptions = {\n adapter: StorageAdapter;\n clock?: () => Date;\n idGenerator?: () => string;\n};\n\nexport class ActivityError extends Error {\n readonly code: string;\n readonly field?: string;\n\n constructor(code: string, message: string, field?: string) {\n super(message);\n this.name = \"ActivityError\";\n this.code = code;\n this.field = field;\n }\n}\n\nexport function createActivity({\n adapter,\n clock = () => new Date(),\n idGenerator = createId,\n}: ActivityOptions): Activity {\n return {\n async track(input) {\n const record = createRecord(input, clock, idGenerator);\n await adapter.insert(record);\n return record;\n },\n async query(options) {\n const normalized = normalizeQuery(options);\n const result = await adapter.query(normalized);\n return result.entries;\n },\n };\n}\n\nexport function createMemoryStorageAdapter(\n initialEntries: ActivityRecord[] = [],\n latency = 0,\n): StorageAdapter {\n let entries = initialEntries.map(freezeRecord);\n\n return {\n async insert(entry) {\n await wait(latency);\n entries = [freezeRecord(entry), ...entries];\n },\n async query(options) {\n await wait(latency);\n const normalized = normalizeQuery(options);\n const filtered = entries\n .filter((entry) => entry.resource.type === normalized.resource.type)\n .filter((entry) => entry.resource.id === normalized.resource.id)\n .filter((entry) => matchesActions(entry, normalized.actions))\n .filter((entry) => matchesActor(entry, normalized.actorId ?? normalized.actor))\n .filter((entry) => matchesDates(entry, normalized.from, normalized.to))\n .filter((entry) => matchesSearch(entry, normalized.search))\n .sort(compareRecords);\n const offset = normalized.offset ?? 0;\n const limit = normalized.limit ?? 50;\n const page = filtered.slice(offset, offset + limit);\n\n return {\n entries: page,\n total: filtered.length,\n hasMore: offset + page.length < filtered.length,\n };\n },\n };\n}\n\ntype Queryable = {\n query(sql: string, params?: readonly unknown[]): Promise<{ rows: unknown[] }>;\n};\n\ntype ReleasableQueryable = Queryable & { release(): void };\ntype Connectable = Queryable & { connect(): Promise<ReleasableQueryable> };\n\nexport function postgresAdapter(client: Queryable | Connectable): StorageAdapter {\n return {\n async insert(entry) {\n const connection = \"connect\" in client ? await client.connect() : client;\n await connection.query(\"BEGIN\");\n try {\n await connection.query(\n `insert into activity_entries (\n id, resource_type, resource_id, resource_title, action,\n actor_type, actor_id, actor_name, actor_avatar_url,\n content_type, content_json, metadata_json, created_at\n ) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,\n [\n entry.id,\n entry.resource.type,\n entry.resource.id,\n entry.resource.title ?? null,\n entry.action,\n entry.actor.type,\n entry.actor.id,\n entry.actor.name,\n entry.actor.avatarUrl ?? null,\n entry.content?.type ?? null,\n entry.content ? JSON.stringify(entry.content) : null,\n entry.metadata ? JSON.stringify(entry.metadata) : null,\n entry.timestamp.toISOString(),\n ],\n );\n\n for (const [position, change] of (entry.changes ?? []).entries()) {\n await connection.query(\n `insert into activity_changes (\n id, entry_id, position, field, label, before_value, after_value, value_type\n ) values ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [\n createId(),\n entry.id,\n position,\n change.field,\n change.label,\n JSON.stringify(change.before ?? null),\n JSON.stringify(change.after ?? null),\n change.valueType,\n ],\n );\n }\n\n await connection.query(\"COMMIT\");\n } catch (error) {\n await connection.query(\"ROLLBACK\");\n throw toActivityError(error, \"STORAGE_FAILURE\", \"Could not insert activity record\");\n } finally {\n if (\"release\" in connection && typeof connection.release === \"function\") {\n connection.release();\n }\n }\n },\n async query(options) {\n const normalized = normalizeQuery(options);\n const limit = normalized.limit ?? 50;\n const offset = normalized.offset ?? 0;\n const params: unknown[] = [normalized.resource.type, normalized.resource.id];\n const where = [\"e.resource_type = $1\", \"e.resource_id = $2\"];\n\n if (normalized.actions?.length) {\n params.push(normalized.actions);\n where.push(`e.action = any($${params.length}::text[])`);\n }\n\n const actorId = normalized.actorId ?? normalized.actor;\n if (actorId) {\n params.push(actorId);\n where.push(`e.actor_id = $${params.length}`);\n }\n\n if (normalized.from) {\n params.push(normalized.from.toISOString());\n where.push(`e.created_at >= $${params.length}`);\n }\n\n if (normalized.to) {\n params.push(normalized.to.toISOString());\n where.push(`e.created_at <= $${params.length}`);\n }\n\n if (normalized.search) {\n params.push(`%${normalized.search}%`);\n const index = params.length;\n where.push(`(\n e.actor_name ilike $${index}\n or e.resource_title ilike $${index}\n or e.content_json::text ilike $${index}\n or exists (\n select 1 from activity_changes c\n where c.entry_id = e.id\n and (\n c.label ilike $${index}\n or c.before_value::text ilike $${index}\n or c.after_value::text ilike $${index}\n )\n )\n )`);\n }\n\n const whereSql = where.join(\" and \");\n const count = await client.query(\n `select count(*)::int as total from activity_entries e where ${whereSql}`,\n params,\n );\n params.push(limit + 1, offset);\n const rows = await client.query(\n `select\n e.*,\n coalesce(\n json_agg(\n json_build_object(\n 'field', c.field,\n 'label', c.label,\n 'before', c.before_value,\n 'after', c.after_value,\n 'valueType', c.value_type\n )\n order by c.position\n ) filter (where c.id is not null),\n '[]'\n ) as changes\n from activity_entries e\n left join activity_changes c on c.entry_id = e.id\n where ${whereSql}\n group by e.id\n order by e.created_at desc, e.id desc\n limit $${params.length - 1} offset $${params.length}`,\n params,\n );\n\n const total = readTotal(count.rows[0]);\n const entries = rows.rows.slice(0, limit).map(mapPostgresRecord);\n\n return {\n entries,\n total,\n hasMore: rows.rows.length > limit,\n };\n },\n };\n}\n\nfunction createRecord(\n input: TrackInput,\n clock: () => Date,\n idGenerator: () => string,\n): ActivityRecord {\n validateTrackInput(input);\n\n const record: ActivityRecord = {\n id: idGenerator(),\n resource: freezeObject({\n type: input.resource.type.trim(),\n id: input.resource.id.trim(),\n title: input.resource.title?.trim(),\n }),\n action: input.action.trim().toLowerCase() as Action,\n actor: freezeObject({ ...input.actor, name: input.actor.name.trim() }),\n timestamp: input.timestamp ? new Date(input.timestamp) : clock(),\n changes: input.changes?.map(normalizeChange).map(freezeObject),\n content: input.content ? freezeObject(input.content) : undefined,\n metadata: input.metadata ? freezeObject(input.metadata) : undefined,\n };\n\n return freezeRecord(record);\n}\n\nfunction validateTrackInput(input: TrackInput) {\n if (!input.resource?.type?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"resource.type is required\", \"resource.type\");\n }\n\n if (!input.resource?.id?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"resource.id is required\", \"resource.id\");\n }\n\n if (![\"user\", \"system\", \"api\", \"agent\"].includes(input.actor?.type)) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.type is invalid\", \"actor.type\");\n }\n\n if (!input.actor?.id?.trim()) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.id is required\", \"actor.id\");\n }\n\n if (!input.actor?.name?.trim()) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.name is required\", \"actor.name\");\n }\n\n if (!input.action?.trim()) {\n throw new ActivityError(\"INVALID_ACTION\", \"action is required\", \"action\");\n }\n\n if (input.timestamp && Number.isNaN(input.timestamp.getTime())) {\n throw new ActivityError(\"INVALID_TIMESTAMP\", \"timestamp must be a valid Date\", \"timestamp\");\n }\n\n if (input.action === \"update\" && !input.changes?.length) {\n throw new ActivityError(\"INVALID_ACTION\", \"update requires at least one change\", \"changes\");\n }\n\n if ((input.action === \"comment\" || input.action === \"attachment\") && !input.content) {\n throw new ActivityError(\"INVALID_ACTION\", `${input.action} requires content`, \"content\");\n }\n}\n\nfunction normalizeChange(change: Change): Change {\n if (!change.field.trim()) {\n throw new ActivityError(\"INVALID_CHANGE\", \"change.field is required\", \"changes.field\");\n }\n\n if (!change.label.trim()) {\n throw new ActivityError(\"INVALID_CHANGE\", \"change.label is required\", \"changes.label\");\n }\n\n return {\n ...change,\n field: change.field.trim(),\n label: change.label.trim(),\n valueType: change.valueType ?? inferValueType(change.after ?? change.before),\n };\n}\n\nfunction normalizeQuery(options: QueryOptions): QueryOptions {\n if (!options.resource?.type?.trim() || !options.resource?.id?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"query.resource is required\", \"resource\");\n }\n\n if (options.limit !== undefined && (options.limit < 1 || options.limit > 500)) {\n throw new ActivityError(\"INVALID_LIMIT\", \"limit must be between 1 and 500\", \"limit\");\n }\n\n if (options.offset !== undefined && options.offset < 0) {\n throw new ActivityError(\"INVALID_OFFSET\", \"offset must not be negative\", \"offset\");\n }\n\n if (options.from && options.to && options.from > options.to) {\n throw new ActivityError(\"INVALID_DATE_RANGE\", \"from must be before to\", \"from\");\n }\n\n return freezeObject({\n ...options,\n resource: freezeObject({\n type: options.resource.type.trim(),\n id: options.resource.id.trim(),\n title: options.resource.title?.trim(),\n }),\n search: options.search?.trim() || undefined,\n actor: options.actor?.trim() || undefined,\n actorId: options.actorId?.trim() || undefined,\n actions: options.actions\n ? Object.freeze(\n Array.from(\n new Set(options.actions.map((action) => action.trim().toLowerCase() as Action)),\n ),\n )\n : undefined,\n limit: options.limit ?? 50,\n offset: options.offset ?? 0,\n });\n}\n\nfunction matchesActions(entry: ActivityRecord, actions?: readonly Action[]) {\n return !actions?.length || actions.includes(entry.action);\n}\n\nfunction matchesActor(entry: ActivityRecord, actorId?: string) {\n return !actorId || entry.actor.id === actorId;\n}\n\nfunction matchesDates(entry: ActivityRecord, from?: Date, to?: Date) {\n return (!from || entry.timestamp >= from) && (!to || entry.timestamp <= to);\n}\n\nfunction matchesSearch(entry: ActivityRecord, search?: string) {\n if (!search) {\n return true;\n }\n\n return searchableText(entry).toLowerCase().includes(search.toLowerCase());\n}\n\nfunction searchableText(entry: ActivityRecord) {\n return [\n entry.action,\n entry.resource.title,\n entry.actor.name,\n entry.content ? JSON.stringify(entry.content) : \"\",\n ...(entry.changes ?? []).flatMap((change) => [\n change.field,\n change.label,\n stringifyValue(change.before),\n stringifyValue(change.after),\n ]),\n ].join(\" \");\n}\n\nfunction compareRecords(a: ActivityRecord, b: ActivityRecord) {\n const time = b.timestamp.getTime() - a.timestamp.getTime();\n return time || b.id.localeCompare(a.id);\n}\n\nfunction inferValueType(value: unknown): ValueType {\n if (value instanceof Date) {\n return \"date\";\n }\n\n const valueType = typeof value;\n if (valueType === \"string\" || valueType === \"number\" || valueType === \"boolean\") {\n return valueType;\n }\n\n return \"json\";\n}\n\nfunction freezeRecord(record: ActivityRecord): ActivityRecord {\n return freezeObject({\n ...record,\n resource: freezeObject(record.resource),\n actor: freezeObject(record.actor),\n changes: record.changes?.map((change) => freezeObject(change)),\n content: record.content ? freezeObject(record.content) : undefined,\n metadata: record.metadata ? freezeObject(record.metadata) : undefined,\n });\n}\n\nfunction freezeObject<T extends object>(value: T): Readonly<T> {\n return Object.freeze(value);\n}\n\nfunction stringifyValue(value: unknown) {\n if (value === null || value === undefined) {\n return \"\";\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n return typeof value === \"string\" ? value : JSON.stringify(value);\n}\n\nfunction createId() {\n if (globalThis.crypto?.randomUUID) {\n return globalThis.crypto.randomUUID();\n }\n\n return `act_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;\n}\n\nfunction wait(ms: number) {\n return new Promise((resolve) => globalThis.setTimeout(resolve, ms));\n}\n\nfunction readTotal(row: unknown) {\n if (isRecord(row) && typeof row.total === \"number\") {\n return row.total;\n }\n\n if (isRecord(row) && typeof row.total === \"string\") {\n return Number(row.total);\n }\n\n return 0;\n}\n\nfunction mapPostgresRecord(row: unknown): ActivityRecord {\n if (!isRecord(row)) {\n throw new ActivityError(\"STORAGE_FAILURE\", \"Invalid activity row returned by storage\");\n }\n\n return freezeRecord({\n id: String(row.id),\n resource: {\n type: String(row.resource_type),\n id: String(row.resource_id),\n title: typeof row.resource_title === \"string\" ? row.resource_title : undefined,\n },\n action: String(row.action) as Action,\n actor: {\n type: String(row.actor_type) as ActorType,\n id: String(row.actor_id),\n name: String(row.actor_name),\n avatarUrl: typeof row.actor_avatar_url === \"string\" ? row.actor_avatar_url : undefined,\n },\n timestamp: new Date(String(row.created_at)),\n changes: Array.isArray(row.changes) ? row.changes.map(mapPostgresChange) : undefined,\n content: isContent(row.content_json) ? row.content_json : undefined,\n metadata: isRecord(row.metadata_json) ? row.metadata_json : undefined,\n });\n}\n\nfunction mapPostgresChange(change: unknown): Change {\n if (!isRecord(change)) {\n throw new ActivityError(\"STORAGE_FAILURE\", \"Invalid change row returned by storage\");\n }\n\n return {\n field: String(change.field),\n label: String(change.label),\n before: change.before,\n after: change.after,\n valueType: String(change.valueType) as ValueType,\n };\n}\n\nfunction isContent(value: unknown): value is Content {\n return isRecord(value) && typeof value.type === \"string\";\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction toActivityError(error: unknown, code: string, fallback: string) {\n if (error instanceof ActivityError) {\n return error;\n }\n\n if (error instanceof Error) {\n return new ActivityError(code, error.message);\n }\n\n return new ActivityError(code, fallback);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/react.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/ActivityPanel.tsx", "../src/activity.ts"],
|
|
4
|
-
"sourcesContent": ["import { useEffect, useMemo, useState } from \"react\";\nimport {\n type Action,\n type Activity,\n type ActivityRecord,\n ActivityError,\n type BuiltInAction,\n type Change,\n type ResourceReference,\n} from \"./activity\";\n\nexport type ActivityPanelMessages = typeof defaultMessages;\n\nexport type ActivityPanelProps = {\n activity: Activity;\n resource: ResourceReference;\n entries?: ActivityRecord[];\n search?: string;\n filters?: ActivityFilters;\n variant?: \"default\" | \"compact\" | \"comfortable\";\n onEntryClick?: (entry: ActivityRecord) => void;\n onError?: (error: Error) => void;\n onQueryChange?: (query: ActivityPanelQuery) => void;\n messages?: Partial<ActivityPanelMessages>;\n locale?: string;\n};\n\ntype ActivityFilters = {\n actions?: readonly Action[];\n actor?: string;\n from?: Date;\n to?: Date;\n};\n\nexport type ActivityPanelQuery = {\n resource: ResourceReference;\n search?: string;\n actions?: readonly Action[];\n actor?: string;\n from?: Date;\n to?: Date;\n};\n\ntype Filter = \"all\" | \"updates\" | \"content\" | \"lifecycle\";\ntype IconName = \"add\" | \"attachment\" | \"box\" | \"chevron-right\" | \"comment\" | \"copy\" | \"file-document\";\ntype EntrySummary = {\n title: string;\n icon: IconName;\n family: \"update\" | \"content\" | \"lifecycle\";\n};\n\nconst filterActions: Record<Exclude<Filter, \"all\">, BuiltInAction[]> = {\n updates: [\"update\"],\n content: [\"comment\", \"attachment\"],\n lifecycle: [\"create\", \"delete\", \"archive\", \"restore\"],\n};\n\nconst defaultMessages = {\n panelLabel: \"Activity history\",\n title: \"Activity\",\n developer: \"Developer\",\n searchLabel: \"Search activity\",\n searchPlaceholder: \"Search...\",\n filterLabel: \"Filter by action family\",\n filterAll: \"All\",\n filterUpdates: \"Updates\",\n filterContent: \"Content\",\n filterLifecycle: \"Lifecycle\",\n loadingLabel: \"Loading activity\",\n entryActionsLabel: \"Entry actions\",\n copyIdLabel: \"Copy activity id\",\n copyIdTitle: \"Copy ID\",\n changedBy: \"Changed by\",\n at: \"At\",\n source: \"Source\",\n version: \"Version\",\n application: \"Application\",\n unversioned: \"Unversioned\",\n noActivity: \"No activity yet\",\n noMatches: \"No matching activity\",\n noActivityHelp: \"Tracked entries for this resource will appear here.\",\n noMatchesHelp: \"Try a broader search or a different action family.\",\n loadError: \"Activity could not load\",\n loadErrorFallback: \"The activity query returned an error.\",\n none: \"None\",\n moreChanges: \"more changes\",\n updated: \"Updated\",\n updatedSuffix: \"updated\",\n comment: \"Comment\",\n attachment: \"Attachment\",\n reason: \"Reason\",\n createdSuffix: \"created\",\n} as const;\n\nexport function ActivityPanel({\n activity,\n resource,\n entries: controlledEntries,\n filters,\n onEntryClick,\n onError,\n onQueryChange,\n search,\n variant = \"default\",\n messages: messageOverrides,\n locale,\n}: ActivityPanelProps) {\n const messages = { ...defaultMessages, ...messageOverrides };\n const actionLabels: Record<Filter, string> = {\n all: messages.filterAll,\n updates: messages.filterUpdates,\n content: messages.filterContent,\n lifecycle: messages.filterLifecycle,\n };\n const [entries, setEntries] = useState<ActivityRecord[]>(controlledEntries ?? []);\n const [status, setStatus] = useState<\"loading\" | \"ready\" | \"error\">(\n controlledEntries ? \"ready\" : \"loading\",\n );\n const [error, setError] = useState<Error | null>(null);\n const [localSearch, setLocalSearch] = useState(\"\");\n const [filter, setFilter] = useState<Filter>(\"all\");\n const [developerMode, setDeveloperMode] = useState(false);\n\n const query = search ?? localSearch;\n const activeActions = useMemo(() => {\n if (filters?.actions?.length) {\n return filters.actions;\n }\n\n return filter === \"all\" ? undefined : filterActions[filter];\n }, [filter, filters?.actions]);\n\n useEffect(() => {\n if (controlledEntries) {\n setEntries(controlledEntries);\n setStatus(\"ready\");\n return;\n }\n\n let cancelled = false;\n setStatus(\"loading\");\n setError(null);\n const queryOptions: ActivityPanelQuery = {\n resource,\n search: query,\n actions: activeActions,\n actor: filters?.actor,\n from: filters?.from,\n to: filters?.to,\n };\n onQueryChange?.(queryOptions);\n\n activity\n .query(queryOptions)\n .then((nextEntries) => {\n if (!cancelled) {\n setEntries(nextEntries);\n setStatus(\"ready\");\n }\n })\n .catch((unknownError: unknown) => {\n const nextError =\n unknownError instanceof Error\n ? unknownError\n : new ActivityError(\"UNKNOWN_ERROR\", \"Activity could not load\");\n\n if (!cancelled) {\n setError(nextError);\n setStatus(\"error\");\n onError?.(nextError);\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [\n activity,\n activeActions,\n controlledEntries,\n filters?.actor,\n filters?.from,\n filters?.to,\n onError,\n onQueryChange,\n query,\n resource,\n ]);\n\n return (\n <section\n className={`activity-panel activity-panel--${variant}`}\n aria-label={messages.panelLabel}\n >\n <header className=\"activity-panel__header\">\n <div>\n <p className=\"eyebrow\">{resource.type}</p>\n <h1>{messages.title}</h1>\n </div>\n <label className=\"developer-toggle\">\n <input\n checked={developerMode}\n onChange={(event) => setDeveloperMode(event.currentTarget.checked)}\n type=\"checkbox\"\n />\n {messages.developer}\n </label>\n </header>\n\n <div className=\"activity-toolbar\">\n <label className=\"search-field\">\n <span aria-hidden=\"true\">/</span>\n <input\n aria-label={messages.searchLabel}\n disabled={search !== undefined}\n onChange={(event) => setLocalSearch(event.currentTarget.value)}\n placeholder={messages.searchPlaceholder}\n value={query}\n />\n </label>\n <div className=\"segmented-control\" aria-label={messages.filterLabel}>\n {(Object.keys(actionLabels) as Filter[]).map((key) => (\n <button\n aria-pressed={filter === key}\n className={filter === key ? \"is-active\" : \"\"}\n disabled={Boolean(filters?.actions?.length)}\n key={key}\n onClick={() => setFilter(key)}\n type=\"button\"\n >\n {actionLabels[key]}\n </button>\n ))}\n </div>\n </div>\n\n {status === \"loading\" ? <ActivityLoading messages={messages} /> : null}\n {status === \"error\" ? <ActivityErrorState error={error} messages={messages} /> : null}\n {status === \"ready\" && entries.length === 0 ? (\n <ActivityEmpty hasQuery={query.trim().length > 0 || filter !== \"all\"} messages={messages} />\n ) : null}\n\n {status === \"ready\" && entries.length > 0 ? (\n <div className=\"activity-stream\">\n {entries.map((entry) => (\n <ActivityEntryRow\n developerMode={developerMode}\n entry={entry}\n key={entry.id}\n onEntryClick={onEntryClick}\n messages={messages}\n locale={locale}\n />\n ))}\n </div>\n ) : null}\n </section>\n );\n}\n\nfunction ActivityEntryRow({\n developerMode,\n entry,\n onEntryClick,\n messages,\n locale,\n}: {\n developerMode: boolean;\n entry: ActivityRecord;\n onEntryClick?: (entry: ActivityRecord) => void;\n messages: ActivityPanelMessages;\n locale?: string;\n}) {\n const [expanded, setExpanded] = useState(false);\n const summary = getEntrySummary(entry, messages);\n return (\n <article className=\"activity-entry\">\n <button\n aria-expanded={expanded}\n className=\"entry-main\"\n onClick={() => {\n setExpanded((value) => !value);\n onEntryClick?.(entry);\n }}\n onKeyDown={(event) => {\n if (event.key === \"Escape\") {\n setExpanded(false);\n return;\n }\n\n if ([\"ArrowDown\", \"ArrowUp\", \"Home\", \"End\"].includes(event.key)) {\n const stream = event.currentTarget.closest(\".activity-stream\");\n const toggles = Array.from(\n stream?.querySelectorAll<HTMLButtonElement>(\"button.entry-main\") ?? [],\n );\n const currentIndex = toggles.indexOf(event.currentTarget);\n const nextIndex =\n event.key === \"Home\"\n ? 0\n : event.key === \"End\"\n ? toggles.length - 1\n : event.key === \"ArrowDown\"\n ? Math.min(currentIndex + 1, toggles.length - 1)\n : Math.max(currentIndex - 1, 0);\n\n if (toggles[nextIndex]) {\n event.preventDefault();\n toggles[nextIndex].focus();\n }\n }\n }}\n type=\"button\"\n >\n <span className={`entry-icon entry-icon--${summary.family}`}>\n <span className={`gg-icon gg-icon--${summary.icon}`} aria-hidden=\"true\" />\n </span>\n <span className=\"entry-body\">\n <span className=\"entry-kicker\">{summary.title}</span>\n <EntryContent entry={entry} expanded={expanded} messages={messages} locale={locale} />\n <span className=\"entry-meta\">\n {entry.actor.name} <span aria-hidden=\"true\">-</span>{\" \"}\n {formatTime(entry.timestamp, locale)}\n </span>\n </span>\n <span className=\"expand-indicator\" aria-hidden=\"true\">\n <span\n className={`gg-icon gg-icon--chevron ${expanded ? \"is-open\" : \"\"}`}\n />\n </span>\n </button>\n <div className=\"entry-actions\" aria-label={messages.entryActionsLabel}>\n <button\n aria-label={messages.copyIdLabel}\n onClick={() => void copyText(entry.id)}\n title={messages.copyIdTitle}\n type=\"button\"\n >\n <span className=\"gg-icon gg-icon--copy\" aria-hidden=\"true\" />\n </button>\n </div>\n\n {expanded ? (\n <div className=\"entry-details\">\n <dl>\n <div>\n <dt>{messages.changedBy}</dt>\n <dd>\n {entry.actor.name} <span>{entry.actor.type}</span>\n </dd>\n </div>\n <div>\n <dt>{messages.at}</dt>\n <dd>{formatFullDate(entry.timestamp, locale)}</dd>\n </div>\n <div>\n <dt>{messages.source}</dt>\n <dd>{readMetadata(entry, \"source\") ?? messages.application}</dd>\n </div>\n <div>\n <dt>{messages.version}</dt>\n <dd>{readMetadata(entry, \"version\") ?? messages.unversioned}</dd>\n </div>\n </dl>\n\n {developerMode ? (\n <pre className=\"developer-json\">\n {JSON.stringify(serializeEntry(entry), null, 2)}\n </pre>\n ) : null}\n </div>\n ) : null}\n </article>\n );\n}\n\nfunction EntryContent({\n entry,\n expanded,\n messages,\n locale,\n}: {\n entry: ActivityRecord;\n expanded: boolean;\n messages: ActivityPanelMessages;\n locale?: string;\n}) {\n if (entry.action === \"update\") {\n return <UpdateChanges changes={entry.changes ?? []} expanded={expanded} messages={messages} locale={locale} />;\n }\n\n if (entry.action === \"comment\" && entry.content?.type === \"comment\") {\n return (\n <span className=\"content-preview\">\n <q>{entry.content.text}</q>\n </span>\n );\n }\n\n if (entry.action === \"attachment\" && entry.content?.type === \"attachment\") {\n return (\n <span className=\"content-preview\">\n <strong>{entry.content.fileName}</strong>\n <small>\n {formatBytes(entry.content.size)} - {entry.content.mimeType}\n </small>\n </span>\n );\n }\n\n return <span className=\"content-preview\">{getLifecycleBody(entry, messages)}</span>;\n}\n\nfunction UpdateChanges({\n changes,\n expanded,\n messages,\n locale,\n}: {\n changes: readonly Readonly<Change>[];\n expanded: boolean;\n messages: ActivityPanelMessages;\n locale?: string;\n}) {\n const visibleChanges = expanded ? changes : changes.slice(0, 3);\n const hiddenCount = changes.length - visibleChanges.length;\n\n return (\n <span className=\"changes-list\">\n {visibleChanges.map((change) => (\n <span className=\"change-row\" key={`${change.field}-${change.label}`}>\n <span>{change.label}</span>\n <span>\n <del>{formatValue(change.before, messages, locale)}</del>\n <ins>{formatValue(change.after, messages, locale)}</ins>\n </span>\n </span>\n ))}\n {hiddenCount > 0 ? (\n <span className=\"more-changes\">+{hiddenCount} {messages.moreChanges}</span>\n ) : null}\n </span>\n );\n}\n\nfunction ActivityLoading({ messages }: { messages: ActivityPanelMessages }) {\n return (\n <div className=\"activity-skeleton\" aria-label={messages.loadingLabel}>\n {Array.from({ length: 4 }, (_, index) => (\n <div className=\"skeleton-row\" key={index}>\n <span />\n <div>\n <b />\n <i />\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nfunction ActivityEmpty({ hasQuery, messages }: { hasQuery: boolean; messages: ActivityPanelMessages }) {\n return (\n <div className=\"empty-state\">\n <span aria-hidden=\"true\">-</span>\n <h2>{hasQuery ? messages.noMatches : messages.noActivity}</h2>\n <p>\n {hasQuery\n ? messages.noMatchesHelp\n : messages.noActivityHelp}\n </p>\n </div>\n );\n}\n\nfunction ActivityErrorState({ error, messages }: { error: Error | null; messages: ActivityPanelMessages }) {\n return (\n <div className=\"empty-state\">\n <h2>{messages.loadError}</h2>\n <p>{error?.message ?? messages.loadErrorFallback}</p>\n </div>\n );\n}\n\nfunction getEntrySummary(entry: ActivityRecord, messages: ActivityPanelMessages): EntrySummary {\n if (entry.action === \"update\") {\n return {\n title:\n (entry.changes?.length ?? 0) > 1\n ? `${titleize(entry.resource.type)} ${messages.updatedSuffix}`\n : entry.changes?.[0]?.label ?? messages.updated,\n icon: \"file-document\",\n family: \"update\",\n };\n }\n\n if (entry.action === \"comment\") {\n return { title: messages.comment, icon: \"comment\", family: \"content\" };\n }\n\n if (entry.action === \"attachment\") {\n return { title: messages.attachment, icon: \"attachment\", family: \"content\" };\n }\n\n return {\n title: titleize(entry.action),\n icon: entry.action === \"archive\" ? \"box\" : \"add\",\n family: \"lifecycle\",\n };\n}\n\nfunction getLifecycleBody(entry: ActivityRecord, messages: ActivityPanelMessages) {\n if (entry.action === \"create\") {\n return entry.resource.title ?? `${entry.resource.type} ${messages.createdSuffix}`;\n }\n\n const reason = readMetadata(entry, \"reason\");\n return reason ? `${messages.reason}: ${reason}` : entry.resource.title ?? titleize(entry.action);\n}\n\nfunction readMetadata(entry: ActivityRecord, key: string) {\n const value = entry.metadata?.[key];\n return typeof value === \"string\" || typeof value === \"number\" ? String(value) : undefined;\n}\n\nfunction formatTime(date: Date, locale?: string) {\n return new Intl.DateTimeFormat(locale, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n }).format(date);\n}\n\nfunction formatFullDate(date: Date, locale?: string) {\n return new Intl.DateTimeFormat(locale, {\n dateStyle: \"medium\",\n timeStyle: \"short\",\n }).format(date);\n}\n\nfunction formatBytes(bytes: number) {\n if (bytes < 1024) {\n return `${bytes} B`;\n }\n\n const kilobytes = bytes / 1024;\n return `${Math.round(kilobytes)} KB`;\n}\n\nfunction formatValue(value: unknown, messages: ActivityPanelMessages, locale?: string) {\n if (value === null || value === undefined) {\n return messages.none;\n }\n\n if (value instanceof Date) {\n return new Intl.DateTimeFormat(locale, { dateStyle: \"medium\" }).format(value);\n }\n\n return typeof value === \"string\" || typeof value === \"number\"\n ? String(value)\n : JSON.stringify(value);\n}\n\nfunction titleize(value: string) {\n return value\n .replace(/[._-]/g, \" \")\n .replace(/\\b\\w/g, (letter) => letter.toUpperCase());\n}\n\nfunction serializeEntry(entry: ActivityRecord) {\n return {\n ...entry,\n timestamp: entry.timestamp.toISOString(),\n };\n}\n\nasync function copyText(value: string) {\n if (globalThis.navigator?.clipboard) {\n await globalThis.navigator.clipboard.writeText(value);\n }\n}\n", "export type ActorType = \"user\" | \"system\" | \"api\" | \"agent\";\n\nexport type BuiltInAction =\n | \"create\"\n | \"update\"\n | \"delete\"\n | \"archive\"\n | \"restore\"\n | \"comment\"\n | \"attachment\";\n\nexport type Action = BuiltInAction | (string & {});\n\nexport type ValueType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"date\"\n | \"currency\"\n | \"user\"\n | \"enum\"\n | \"json\"\n | \"custom\";\n\nexport type Resource = {\n type: string;\n id: string;\n title?: string;\n};\n\nexport type ResourceReference = Pick<Resource, \"type\" | \"id\"> & {\n title?: string;\n};\n\nexport type Actor = {\n type: ActorType;\n id: string;\n name: string;\n avatarUrl?: string;\n};\n\nexport type Change = {\n field: string;\n label: string;\n before?: unknown;\n after?: unknown;\n valueType: ValueType;\n};\n\nexport type CommentContent = {\n type: \"comment\";\n text: string;\n};\n\nexport type AttachmentContent = {\n type: \"attachment\";\n fileName: string;\n mimeType: string;\n size: number;\n url?: string;\n};\n\nexport type CustomContent = {\n type: \"custom\";\n [key: string]: unknown;\n};\n\nexport type Content = CommentContent | AttachmentContent | CustomContent;\n\nexport type Metadata = {\n source?: string;\n version?: string;\n requestId?: string;\n ipAddress?: string;\n custom?: Record<string, unknown>;\n [key: string]: unknown;\n};\n\nexport type ActivityRecord = Readonly<{\n id: string;\n resource: Readonly<Resource>;\n action: Action;\n actor: Readonly<Actor>;\n timestamp: Date;\n changes?: readonly Readonly<Change>[];\n content?: Readonly<Content>;\n metadata?: Readonly<Metadata>;\n}>;\n\nexport type TrackInput = {\n resource: ResourceReference;\n actor: Actor;\n action: Action;\n changes?: Change[];\n content?: Content;\n metadata?: Metadata;\n timestamp?: Date;\n};\n\nexport type QueryOptions = {\n resource: ResourceReference;\n search?: string;\n actor?: string;\n actorId?: string;\n actions?: readonly Action[];\n from?: Date;\n to?: Date;\n limit?: number;\n offset?: number;\n};\n\nexport type QueryResult = {\n entries: ActivityRecord[];\n total: number;\n hasMore: boolean;\n};\n\nexport type StorageAdapter = {\n insert(entry: ActivityRecord): Promise<void>;\n query(options: QueryOptions): Promise<QueryResult>;\n};\n\nexport type Activity = {\n track(input: TrackInput): Promise<ActivityRecord>;\n query(options: QueryOptions): Promise<ActivityRecord[]>;\n};\n\nexport type ActivityOptions = {\n adapter: StorageAdapter;\n clock?: () => Date;\n idGenerator?: () => string;\n};\n\nexport class ActivityError extends Error {\n readonly code: string;\n readonly field?: string;\n\n constructor(code: string, message: string, field?: string) {\n super(message);\n this.name = \"ActivityError\";\n this.code = code;\n this.field = field;\n }\n}\n\nexport function createActivity({\n adapter,\n clock = () => new Date(),\n idGenerator = createId,\n}: ActivityOptions): Activity {\n return {\n async track(input) {\n const record = createRecord(input, clock, idGenerator);\n await adapter.insert(record);\n return record;\n },\n async query(options) {\n const normalized = normalizeQuery(options);\n const result = await adapter.query(normalized);\n return result.entries;\n },\n };\n}\n\nexport function createMemoryStorageAdapter(\n initialEntries: ActivityRecord[] = [],\n latency = 0,\n): StorageAdapter {\n let entries = initialEntries.map(freezeRecord);\n\n return {\n async insert(entry) {\n await wait(latency);\n entries = [freezeRecord(entry), ...entries];\n },\n async query(options) {\n await wait(latency);\n const normalized = normalizeQuery(options);\n const filtered = entries\n .filter((entry) => entry.resource.type === normalized.resource.type)\n .filter((entry) => entry.resource.id === normalized.resource.id)\n .filter((entry) => matchesActions(entry, normalized.actions))\n .filter((entry) => matchesActor(entry, normalized.actorId ?? normalized.actor))\n .filter((entry) => matchesDates(entry, normalized.from, normalized.to))\n .filter((entry) => matchesSearch(entry, normalized.search))\n .sort(compareRecords);\n const offset = normalized.offset ?? 0;\n const limit = normalized.limit ?? 50;\n const page = filtered.slice(offset, offset + limit);\n\n return {\n entries: page,\n total: filtered.length,\n hasMore: offset + page.length < filtered.length,\n };\n },\n };\n}\n\ntype Queryable = {\n query(sql: string, params?: readonly unknown[]): Promise<{ rows: unknown[] }>;\n};\n\ntype ReleasableQueryable = Queryable & { release(): void };\ntype Connectable = Queryable & { connect(): Promise<ReleasableQueryable> };\n\nexport function postgresAdapter(client: Queryable | Connectable): StorageAdapter {\n return {\n async insert(entry) {\n const connection = \"connect\" in client ? await client.connect() : client;\n await connection.query(\"BEGIN\");\n try {\n await connection.query(\n `insert into activity_entries (\n id, resource_type, resource_id, resource_title, action,\n actor_type, actor_id, actor_name, actor_avatar_url,\n content_type, content_json, metadata_json, created_at\n ) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,\n [\n entry.id,\n entry.resource.type,\n entry.resource.id,\n entry.resource.title ?? null,\n entry.action,\n entry.actor.type,\n entry.actor.id,\n entry.actor.name,\n entry.actor.avatarUrl ?? null,\n entry.content?.type ?? null,\n entry.content ? JSON.stringify(entry.content) : null,\n entry.metadata ? JSON.stringify(entry.metadata) : null,\n entry.timestamp.toISOString(),\n ],\n );\n\n for (const [position, change] of (entry.changes ?? []).entries()) {\n await connection.query(\n `insert into activity_changes (\n id, entry_id, position, field, label, before_value, after_value, value_type\n ) values ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [\n createId(),\n entry.id,\n position,\n change.field,\n change.label,\n JSON.stringify(change.before ?? null),\n JSON.stringify(change.after ?? null),\n change.valueType,\n ],\n );\n }\n\n await connection.query(\"COMMIT\");\n } catch (error) {\n await connection.query(\"ROLLBACK\");\n throw toActivityError(error, \"STORAGE_FAILURE\", \"Could not insert activity record\");\n } finally {\n if (\"release\" in connection && typeof connection.release === \"function\") {\n connection.release();\n }\n }\n },\n async query(options) {\n const normalized = normalizeQuery(options);\n const limit = normalized.limit ?? 50;\n const offset = normalized.offset ?? 0;\n const params: unknown[] = [normalized.resource.type, normalized.resource.id];\n const where = [\"e.resource_type = $1\", \"e.resource_id = $2\"];\n\n if (normalized.actions?.length) {\n params.push(normalized.actions);\n where.push(`e.action = any($${params.length}::text[])`);\n }\n\n const actorId = normalized.actorId ?? normalized.actor;\n if (actorId) {\n params.push(actorId);\n where.push(`e.actor_id = $${params.length}`);\n }\n\n if (normalized.from) {\n params.push(normalized.from.toISOString());\n where.push(`e.created_at >= $${params.length}`);\n }\n\n if (normalized.to) {\n params.push(normalized.to.toISOString());\n where.push(`e.created_at <= $${params.length}`);\n }\n\n if (normalized.search) {\n params.push(`%${normalized.search}%`);\n const index = params.length;\n where.push(`(\n e.actor_name ilike $${index}\n or e.resource_title ilike $${index}\n or e.content_json::text ilike $${index}\n or exists (\n select 1 from activity_changes c\n where c.entry_id = e.id\n and (\n c.label ilike $${index}\n or c.before_value::text ilike $${index}\n or c.after_value::text ilike $${index}\n )\n )\n )`);\n }\n\n const whereSql = where.join(\" and \");\n const count = await client.query(\n `select count(*)::int as total from activity_entries e where ${whereSql}`,\n params,\n );\n params.push(limit + 1, offset);\n const rows = await client.query(\n `select\n e.*,\n coalesce(\n json_agg(\n json_build_object(\n 'field', c.field,\n 'label', c.label,\n 'before', c.before_value,\n 'after', c.after_value,\n 'valueType', c.value_type\n )\n order by c.position\n ) filter (where c.id is not null),\n '[]'\n ) as changes\n from activity_entries e\n left join activity_changes c on c.entry_id = e.id\n where ${whereSql}\n group by e.id\n order by e.created_at desc, e.id desc\n limit $${params.length - 1} offset $${params.length}`,\n params,\n );\n\n const total = readTotal(count.rows[0]);\n const entries = rows.rows.slice(0, limit).map(mapPostgresRecord);\n\n return {\n entries,\n total,\n hasMore: rows.rows.length > limit,\n };\n },\n };\n}\n\nfunction createRecord(\n input: TrackInput,\n clock: () => Date,\n idGenerator: () => string,\n): ActivityRecord {\n validateTrackInput(input);\n\n const record: ActivityRecord = {\n id: idGenerator(),\n resource: freezeObject({\n type: input.resource.type.trim(),\n id: input.resource.id.trim(),\n title: input.resource.title?.trim(),\n }),\n action: input.action.trim().toLowerCase() as Action,\n actor: freezeObject({ ...input.actor, name: input.actor.name.trim() }),\n timestamp: input.timestamp ? new Date(input.timestamp) : clock(),\n changes: input.changes?.map(normalizeChange).map(freezeObject),\n content: input.content ? freezeObject(input.content) : undefined,\n metadata: input.metadata ? freezeObject(input.metadata) : undefined,\n };\n\n return freezeRecord(record);\n}\n\nfunction validateTrackInput(input: TrackInput) {\n if (!input.resource?.type?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"resource.type is required\", \"resource.type\");\n }\n\n if (!input.resource?.id?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"resource.id is required\", \"resource.id\");\n }\n\n if (![\"user\", \"system\", \"api\", \"agent\"].includes(input.actor?.type)) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.type is invalid\", \"actor.type\");\n }\n\n if (!input.actor?.id?.trim()) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.id is required\", \"actor.id\");\n }\n\n if (!input.actor?.name?.trim()) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.name is required\", \"actor.name\");\n }\n\n if (!input.action?.trim()) {\n throw new ActivityError(\"INVALID_ACTION\", \"action is required\", \"action\");\n }\n\n if (input.timestamp && Number.isNaN(input.timestamp.getTime())) {\n throw new ActivityError(\"INVALID_TIMESTAMP\", \"timestamp must be a valid Date\", \"timestamp\");\n }\n\n if (input.action === \"update\" && !input.changes?.length) {\n throw new ActivityError(\"INVALID_ACTION\", \"update requires at least one change\", \"changes\");\n }\n\n if ((input.action === \"comment\" || input.action === \"attachment\") && !input.content) {\n throw new ActivityError(\"INVALID_ACTION\", `${input.action} requires content`, \"content\");\n }\n}\n\nfunction normalizeChange(change: Change): Change {\n if (!change.field.trim()) {\n throw new ActivityError(\"INVALID_CHANGE\", \"change.field is required\", \"changes.field\");\n }\n\n if (!change.label.trim()) {\n throw new ActivityError(\"INVALID_CHANGE\", \"change.label is required\", \"changes.label\");\n }\n\n return {\n ...change,\n field: change.field.trim(),\n label: change.label.trim(),\n valueType: change.valueType ?? inferValueType(change.after ?? change.before),\n };\n}\n\nfunction normalizeQuery(options: QueryOptions): QueryOptions {\n if (!options.resource?.type?.trim() || !options.resource?.id?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"query.resource is required\", \"resource\");\n }\n\n if (options.limit !== undefined && (options.limit < 1 || options.limit > 500)) {\n throw new ActivityError(\"INVALID_LIMIT\", \"limit must be between 1 and 500\", \"limit\");\n }\n\n if (options.offset !== undefined && options.offset < 0) {\n throw new ActivityError(\"INVALID_OFFSET\", \"offset must not be negative\", \"offset\");\n }\n\n if (options.from && options.to && options.from > options.to) {\n throw new ActivityError(\"INVALID_DATE_RANGE\", \"from must be before to\", \"from\");\n }\n\n return freezeObject({\n ...options,\n resource: freezeObject({\n type: options.resource.type.trim(),\n id: options.resource.id.trim(),\n title: options.resource.title?.trim(),\n }),\n search: options.search?.trim() || undefined,\n actor: options.actor?.trim() || undefined,\n actorId: options.actorId?.trim() || undefined,\n actions: options.actions\n ? Object.freeze(\n Array.from(\n new Set(options.actions.map((action) => action.trim().toLowerCase() as Action)),\n ),\n )\n : undefined,\n limit: options.limit ?? 50,\n offset: options.offset ?? 0,\n });\n}\n\nfunction matchesActions(entry: ActivityRecord, actions?: readonly Action[]) {\n return !actions?.length || actions.includes(entry.action);\n}\n\nfunction matchesActor(entry: ActivityRecord, actorId?: string) {\n return !actorId || entry.actor.id === actorId;\n}\n\nfunction matchesDates(entry: ActivityRecord, from?: Date, to?: Date) {\n return (!from || entry.timestamp >= from) && (!to || entry.timestamp <= to);\n}\n\nfunction matchesSearch(entry: ActivityRecord, search?: string) {\n if (!search) {\n return true;\n }\n\n return searchableText(entry).toLowerCase().includes(search.toLowerCase());\n}\n\nfunction searchableText(entry: ActivityRecord) {\n return [\n entry.action,\n entry.resource.title,\n entry.actor.name,\n entry.content ? JSON.stringify(entry.content) : \"\",\n ...(entry.changes ?? []).flatMap((change) => [\n change.field,\n change.label,\n stringifyValue(change.before),\n stringifyValue(change.after),\n ]),\n ].join(\" \");\n}\n\nfunction compareRecords(a: ActivityRecord, b: ActivityRecord) {\n const time = b.timestamp.getTime() - a.timestamp.getTime();\n return time || b.id.localeCompare(a.id);\n}\n\nfunction inferValueType(value: unknown): ValueType {\n if (value instanceof Date) {\n return \"date\";\n }\n\n const valueType = typeof value;\n if (valueType === \"string\" || valueType === \"number\" || valueType === \"boolean\") {\n return valueType;\n }\n\n return \"json\";\n}\n\nfunction freezeRecord(record: ActivityRecord): ActivityRecord {\n return freezeObject({\n ...record,\n resource: freezeObject(record.resource),\n actor: freezeObject(record.actor),\n changes: record.changes?.map((change) => freezeObject(change)),\n content: record.content ? freezeObject(record.content) : undefined,\n metadata: record.metadata ? freezeObject(record.metadata) : undefined,\n });\n}\n\nfunction freezeObject<T extends object>(value: T): Readonly<T> {\n return Object.freeze(value);\n}\n\nfunction stringifyValue(value: unknown) {\n if (value === null || value === undefined) {\n return \"\";\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n return typeof value === \"string\" ? value : JSON.stringify(value);\n}\n\nfunction createId() {\n if (globalThis.crypto?.randomUUID) {\n return globalThis.crypto.randomUUID();\n }\n\n return `act_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;\n}\n\nfunction wait(ms: number) {\n return new Promise((resolve) => globalThis.setTimeout(resolve, ms));\n}\n\nfunction readTotal(row: unknown) {\n if (isRecord(row) && typeof row.total === \"number\") {\n return row.total;\n }\n\n if (isRecord(row) && typeof row.total === \"string\") {\n return Number(row.total);\n }\n\n return 0;\n}\n\nfunction mapPostgresRecord(row: unknown): ActivityRecord {\n if (!isRecord(row)) {\n throw new ActivityError(\"STORAGE_FAILURE\", \"Invalid activity row returned by storage\");\n }\n\n return freezeRecord({\n id: String(row.id),\n resource: {\n type: String(row.resource_type),\n id: String(row.resource_id),\n title: typeof row.resource_title === \"string\" ? row.resource_title : undefined,\n },\n action: String(row.action) as Action,\n actor: {\n type: String(row.actor_type) as ActorType,\n id: String(row.actor_id),\n name: String(row.actor_name),\n avatarUrl: typeof row.actor_avatar_url === \"string\" ? row.actor_avatar_url : undefined,\n },\n timestamp: new Date(String(row.created_at)),\n changes: Array.isArray(row.changes) ? row.changes.map(mapPostgresChange) : undefined,\n content: isContent(row.content_json) ? row.content_json : undefined,\n metadata: isRecord(row.metadata_json) ? row.metadata_json : undefined,\n });\n}\n\nfunction mapPostgresChange(change: unknown): Change {\n if (!isRecord(change)) {\n throw new ActivityError(\"STORAGE_FAILURE\", \"Invalid change row returned by storage\");\n }\n\n return {\n field: String(change.field),\n label: String(change.label),\n before: change.before,\n after: change.after,\n valueType: String(change.valueType) as ValueType,\n };\n}\n\nfunction isContent(value: unknown): value is Content {\n return isRecord(value) && typeof value.type === \"string\";\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction toActivityError(error: unknown, code: string, fallback: string) {\n if (error instanceof ActivityError) {\n return error;\n }\n\n if (error instanceof Error) {\n return new ActivityError(code, error.message);\n }\n\n return new ActivityError(code, fallback);\n}\n"],
|
|
5
|
-
"mappings": ";;;;;AAAA,SAAS,WAAW,SAAS,gBAAgB;;;ACqItC,IAAM,gBAAN,cAA4B,MAAM;AAAA,EAIvC,YAAY,MAAc,SAAiB,OAAgB;AACzD,UAAM,OAAO;AAJf,wBAAS;AACT,wBAAS;AAIP,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,QAAQ;AAAA,EACf;AACF;;;
|
|
4
|
+
"sourcesContent": ["import { useEffect, useMemo, useState } from \"react\";\nimport {\n type Action,\n type Activity,\n type ActivityRecord,\n ActivityError,\n type BuiltInAction,\n type Change,\n type ResourceReference,\n} from \"./activity\";\n\nexport type ActivityPanelMessages = {\n [Key in keyof typeof defaultMessages]: string;\n};\n\nexport type ActivityPanelProps = {\n activity: Activity;\n resource: ResourceReference;\n entries?: ActivityRecord[];\n search?: string;\n filters?: ActivityFilters;\n variant?: \"default\" | \"compact\" | \"comfortable\";\n onEntryClick?: (entry: ActivityRecord) => void;\n onError?: (error: Error) => void;\n onQueryChange?: (query: ActivityPanelQuery) => void;\n messages?: Partial<ActivityPanelMessages>;\n locale?: string;\n};\n\ntype ActivityFilters = {\n actions?: readonly Action[];\n actor?: string;\n from?: Date;\n to?: Date;\n};\n\nexport type ActivityPanelQuery = {\n resource: ResourceReference;\n search?: string;\n actions?: readonly Action[];\n actor?: string;\n from?: Date;\n to?: Date;\n};\n\ntype Filter = \"all\" | \"updates\" | \"content\" | \"lifecycle\";\ntype IconName = \"add\" | \"attachment\" | \"box\" | \"chevron-right\" | \"comment\" | \"copy\" | \"file-document\";\ntype EntrySummary = {\n title: string;\n icon: IconName;\n family: \"update\" | \"content\" | \"lifecycle\";\n};\n\nconst filterActions: Record<Exclude<Filter, \"all\">, BuiltInAction[]> = {\n updates: [\"update\"],\n content: [\"comment\", \"attachment\"],\n lifecycle: [\"create\", \"delete\", \"archive\", \"restore\"],\n};\n\nconst defaultMessages = {\n panelLabel: \"Activity history\",\n title: \"Activity\",\n developer: \"Developer\",\n searchLabel: \"Search activity\",\n searchPlaceholder: \"Search...\",\n filterLabel: \"Filter by action family\",\n filterAll: \"All\",\n filterUpdates: \"Updates\",\n filterContent: \"Content\",\n filterLifecycle: \"Lifecycle\",\n loadingLabel: \"Loading activity\",\n entryActionsLabel: \"Entry actions\",\n copyIdLabel: \"Copy activity id\",\n copyIdTitle: \"Copy ID\",\n changedBy: \"Changed by\",\n at: \"At\",\n source: \"Source\",\n version: \"Version\",\n application: \"Application\",\n unversioned: \"Unversioned\",\n noActivity: \"No activity yet\",\n noMatches: \"No matching activity\",\n noActivityHelp: \"Tracked entries for this resource will appear here.\",\n noMatchesHelp: \"Try a broader search or a different action family.\",\n loadError: \"Activity could not load\",\n loadErrorFallback: \"The activity query returned an error.\",\n none: \"None\",\n moreChanges: \"more changes\",\n updated: \"Updated\",\n updatedSuffix: \"updated\",\n comment: \"Comment\",\n attachment: \"Attachment\",\n reason: \"Reason\",\n createdSuffix: \"created\",\n} as const;\n\nexport function ActivityPanel({\n activity,\n resource,\n entries: controlledEntries,\n filters,\n onEntryClick,\n onError,\n onQueryChange,\n search,\n variant = \"default\",\n messages: messageOverrides,\n locale,\n}: ActivityPanelProps) {\n const messages = { ...defaultMessages, ...messageOverrides };\n const actionLabels: Record<Filter, string> = {\n all: messages.filterAll,\n updates: messages.filterUpdates,\n content: messages.filterContent,\n lifecycle: messages.filterLifecycle,\n };\n const [entries, setEntries] = useState<ActivityRecord[]>(controlledEntries ?? []);\n const [status, setStatus] = useState<\"loading\" | \"ready\" | \"error\">(\n controlledEntries ? \"ready\" : \"loading\",\n );\n const [error, setError] = useState<Error | null>(null);\n const [localSearch, setLocalSearch] = useState(\"\");\n const [filter, setFilter] = useState<Filter>(\"all\");\n const [developerMode, setDeveloperMode] = useState(false);\n\n const query = search ?? localSearch;\n const activeActions = useMemo(() => {\n if (filters?.actions?.length) {\n return filters.actions;\n }\n\n return filter === \"all\" ? undefined : filterActions[filter];\n }, [filter, filters?.actions]);\n\n useEffect(() => {\n if (controlledEntries) {\n setEntries(controlledEntries);\n setStatus(\"ready\");\n return;\n }\n\n let cancelled = false;\n setStatus(\"loading\");\n setError(null);\n const queryOptions: ActivityPanelQuery = {\n resource,\n search: query,\n actions: activeActions,\n actor: filters?.actor,\n from: filters?.from,\n to: filters?.to,\n };\n onQueryChange?.(queryOptions);\n\n activity\n .query(queryOptions)\n .then((nextEntries) => {\n if (!cancelled) {\n setEntries(nextEntries);\n setStatus(\"ready\");\n }\n })\n .catch((unknownError: unknown) => {\n const nextError =\n unknownError instanceof Error\n ? unknownError\n : new ActivityError(\"UNKNOWN_ERROR\", \"Activity could not load\");\n\n if (!cancelled) {\n setError(nextError);\n setStatus(\"error\");\n onError?.(nextError);\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [\n activity,\n activeActions,\n controlledEntries,\n filters?.actor,\n filters?.from,\n filters?.to,\n onError,\n onQueryChange,\n query,\n resource,\n ]);\n\n return (\n <section\n className={`activity-panel activity-panel--${variant}`}\n aria-label={messages.panelLabel}\n >\n <header className=\"activity-panel__header\">\n <div>\n <p className=\"eyebrow\">{resource.type}</p>\n <h1>{messages.title}</h1>\n </div>\n <label className=\"developer-toggle\">\n <input\n checked={developerMode}\n onChange={(event) => setDeveloperMode(event.currentTarget.checked)}\n type=\"checkbox\"\n />\n {messages.developer}\n </label>\n </header>\n\n <div className=\"activity-toolbar\">\n <label className=\"search-field\">\n <span aria-hidden=\"true\">/</span>\n <input\n aria-label={messages.searchLabel}\n disabled={search !== undefined}\n onChange={(event) => setLocalSearch(event.currentTarget.value)}\n placeholder={messages.searchPlaceholder}\n value={query}\n />\n </label>\n <div className=\"segmented-control\" aria-label={messages.filterLabel}>\n {(Object.keys(actionLabels) as Filter[]).map((key) => (\n <button\n aria-pressed={filter === key}\n className={filter === key ? \"is-active\" : \"\"}\n disabled={Boolean(filters?.actions?.length)}\n key={key}\n onClick={() => setFilter(key)}\n type=\"button\"\n >\n {actionLabels[key]}\n </button>\n ))}\n </div>\n </div>\n\n {status === \"loading\" ? <ActivityLoading messages={messages} /> : null}\n {status === \"error\" ? <ActivityErrorState error={error} messages={messages} /> : null}\n {status === \"ready\" && entries.length === 0 ? (\n <ActivityEmpty hasQuery={query.trim().length > 0 || filter !== \"all\"} messages={messages} />\n ) : null}\n\n {status === \"ready\" && entries.length > 0 ? (\n <div className=\"activity-stream\">\n {entries.map((entry) => (\n <ActivityEntryRow\n developerMode={developerMode}\n entry={entry}\n key={entry.id}\n onEntryClick={onEntryClick}\n messages={messages}\n locale={locale}\n />\n ))}\n </div>\n ) : null}\n </section>\n );\n}\n\nfunction ActivityEntryRow({\n developerMode,\n entry,\n onEntryClick,\n messages,\n locale,\n}: {\n developerMode: boolean;\n entry: ActivityRecord;\n onEntryClick?: (entry: ActivityRecord) => void;\n messages: ActivityPanelMessages;\n locale?: string;\n}) {\n const [expanded, setExpanded] = useState(false);\n const summary = getEntrySummary(entry, messages);\n return (\n <article className=\"activity-entry\">\n <button\n aria-expanded={expanded}\n className=\"entry-main\"\n onClick={() => {\n setExpanded((value) => !value);\n onEntryClick?.(entry);\n }}\n onKeyDown={(event) => {\n if (event.key === \"Escape\") {\n setExpanded(false);\n return;\n }\n\n if ([\"ArrowDown\", \"ArrowUp\", \"Home\", \"End\"].includes(event.key)) {\n const stream = event.currentTarget.closest(\".activity-stream\");\n const toggles = Array.from(\n stream?.querySelectorAll<HTMLButtonElement>(\"button.entry-main\") ?? [],\n );\n const currentIndex = toggles.indexOf(event.currentTarget);\n const nextIndex =\n event.key === \"Home\"\n ? 0\n : event.key === \"End\"\n ? toggles.length - 1\n : event.key === \"ArrowDown\"\n ? Math.min(currentIndex + 1, toggles.length - 1)\n : Math.max(currentIndex - 1, 0);\n\n if (toggles[nextIndex]) {\n event.preventDefault();\n toggles[nextIndex].focus();\n }\n }\n }}\n type=\"button\"\n >\n <span className={`entry-icon entry-icon--${summary.family}`}>\n <span className={`gg-icon gg-icon--${summary.icon}`} aria-hidden=\"true\" />\n </span>\n <span className=\"entry-body\">\n <span className=\"entry-kicker\">{summary.title}</span>\n <EntryContent entry={entry} expanded={expanded} messages={messages} locale={locale} />\n <span className=\"entry-meta\">\n {entry.actor.name} <span aria-hidden=\"true\">-</span>{\" \"}\n {formatTime(entry.timestamp, locale)}\n </span>\n </span>\n <span className=\"expand-indicator\" aria-hidden=\"true\">\n <span\n className={`gg-icon gg-icon--chevron ${expanded ? \"is-open\" : \"\"}`}\n />\n </span>\n </button>\n <div className=\"entry-actions\" aria-label={messages.entryActionsLabel}>\n <button\n aria-label={messages.copyIdLabel}\n onClick={() => void copyText(entry.id)}\n title={messages.copyIdTitle}\n type=\"button\"\n >\n <span className=\"gg-icon gg-icon--copy\" aria-hidden=\"true\" />\n </button>\n </div>\n\n {expanded ? (\n <div className=\"entry-details\">\n <dl>\n <div>\n <dt>{messages.changedBy}</dt>\n <dd>\n {entry.actor.name} <span>{entry.actor.type}</span>\n </dd>\n </div>\n <div>\n <dt>{messages.at}</dt>\n <dd>{formatFullDate(entry.timestamp, locale)}</dd>\n </div>\n <div>\n <dt>{messages.source}</dt>\n <dd>{readMetadata(entry, \"source\") ?? messages.application}</dd>\n </div>\n <div>\n <dt>{messages.version}</dt>\n <dd>{readMetadata(entry, \"version\") ?? messages.unversioned}</dd>\n </div>\n </dl>\n\n {developerMode ? (\n <pre className=\"developer-json\">\n {JSON.stringify(serializeEntry(entry), null, 2)}\n </pre>\n ) : null}\n </div>\n ) : null}\n </article>\n );\n}\n\nfunction EntryContent({\n entry,\n expanded,\n messages,\n locale,\n}: {\n entry: ActivityRecord;\n expanded: boolean;\n messages: ActivityPanelMessages;\n locale?: string;\n}) {\n if (entry.action === \"update\") {\n return <UpdateChanges changes={entry.changes ?? []} expanded={expanded} messages={messages} locale={locale} />;\n }\n\n if (entry.action === \"comment\" && entry.content?.type === \"comment\") {\n return (\n <span className=\"content-preview\">\n <q>{entry.content.text}</q>\n </span>\n );\n }\n\n if (entry.action === \"attachment\" && entry.content?.type === \"attachment\") {\n return (\n <span className=\"content-preview\">\n <strong>{entry.content.fileName}</strong>\n <small>\n {formatBytes(entry.content.size)} - {entry.content.mimeType}\n </small>\n </span>\n );\n }\n\n return <span className=\"content-preview\">{getLifecycleBody(entry, messages)}</span>;\n}\n\nfunction UpdateChanges({\n changes,\n expanded,\n messages,\n locale,\n}: {\n changes: readonly Readonly<Change>[];\n expanded: boolean;\n messages: ActivityPanelMessages;\n locale?: string;\n}) {\n const visibleChanges = expanded ? changes : changes.slice(0, 3);\n const hiddenCount = changes.length - visibleChanges.length;\n\n return (\n <span className=\"changes-list\">\n {visibleChanges.map((change) => (\n <span className=\"change-row\" key={`${change.field}-${change.label}`}>\n <span>{change.label}</span>\n <span>\n <del>{formatValue(change.before, messages, locale)}</del>\n <ins>{formatValue(change.after, messages, locale)}</ins>\n </span>\n </span>\n ))}\n {hiddenCount > 0 ? (\n <span className=\"more-changes\">+{hiddenCount} {messages.moreChanges}</span>\n ) : null}\n </span>\n );\n}\n\nfunction ActivityLoading({ messages }: { messages: ActivityPanelMessages }) {\n return (\n <div className=\"activity-skeleton\" aria-label={messages.loadingLabel}>\n {Array.from({ length: 4 }, (_, index) => (\n <div className=\"skeleton-row\" key={index}>\n <span />\n <div>\n <b />\n <i />\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nfunction ActivityEmpty({ hasQuery, messages }: { hasQuery: boolean; messages: ActivityPanelMessages }) {\n return (\n <div className=\"empty-state\">\n <span aria-hidden=\"true\">-</span>\n <h2>{hasQuery ? messages.noMatches : messages.noActivity}</h2>\n <p>\n {hasQuery\n ? messages.noMatchesHelp\n : messages.noActivityHelp}\n </p>\n </div>\n );\n}\n\nfunction ActivityErrorState({ error, messages }: { error: Error | null; messages: ActivityPanelMessages }) {\n return (\n <div className=\"empty-state\">\n <h2>{messages.loadError}</h2>\n <p>{error?.message ?? messages.loadErrorFallback}</p>\n </div>\n );\n}\n\nfunction getEntrySummary(entry: ActivityRecord, messages: ActivityPanelMessages): EntrySummary {\n if (entry.action === \"update\") {\n return {\n title:\n (entry.changes?.length ?? 0) > 1\n ? `${titleize(entry.resource.type)} ${messages.updatedSuffix}`\n : entry.changes?.[0]?.label ?? messages.updated,\n icon: \"file-document\",\n family: \"update\",\n };\n }\n\n if (entry.action === \"comment\") {\n return { title: messages.comment, icon: \"comment\", family: \"content\" };\n }\n\n if (entry.action === \"attachment\") {\n return { title: messages.attachment, icon: \"attachment\", family: \"content\" };\n }\n\n return {\n title: titleize(entry.action),\n icon: entry.action === \"archive\" ? \"box\" : \"add\",\n family: \"lifecycle\",\n };\n}\n\nfunction getLifecycleBody(entry: ActivityRecord, messages: ActivityPanelMessages) {\n if (entry.action === \"create\") {\n return entry.resource.title ?? `${entry.resource.type} ${messages.createdSuffix}`;\n }\n\n const reason = readMetadata(entry, \"reason\");\n return reason ? `${messages.reason}: ${reason}` : entry.resource.title ?? titleize(entry.action);\n}\n\nfunction readMetadata(entry: ActivityRecord, key: string) {\n const value = entry.metadata?.[key];\n return typeof value === \"string\" || typeof value === \"number\" ? String(value) : undefined;\n}\n\nfunction formatTime(date: Date, locale?: string) {\n return new Intl.DateTimeFormat(locale, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n }).format(date);\n}\n\nfunction formatFullDate(date: Date, locale?: string) {\n return new Intl.DateTimeFormat(locale, {\n dateStyle: \"medium\",\n timeStyle: \"short\",\n }).format(date);\n}\n\nfunction formatBytes(bytes: number) {\n if (bytes < 1024) {\n return `${bytes} B`;\n }\n\n const kilobytes = bytes / 1024;\n return `${Math.round(kilobytes)} KB`;\n}\n\nfunction formatValue(value: unknown, messages: ActivityPanelMessages, locale?: string) {\n if (value === null || value === undefined) {\n return messages.none;\n }\n\n if (value instanceof Date) {\n return new Intl.DateTimeFormat(locale, { dateStyle: \"medium\" }).format(value);\n }\n\n return typeof value === \"string\" || typeof value === \"number\"\n ? String(value)\n : JSON.stringify(value);\n}\n\nfunction titleize(value: string) {\n return value\n .replace(/[._-]/g, \" \")\n .replace(/\\b\\w/g, (letter) => letter.toUpperCase());\n}\n\nfunction serializeEntry(entry: ActivityRecord) {\n return {\n ...entry,\n timestamp: entry.timestamp.toISOString(),\n };\n}\n\nasync function copyText(value: string) {\n if (globalThis.navigator?.clipboard) {\n await globalThis.navigator.clipboard.writeText(value);\n }\n}\n", "export type ActorType = \"user\" | \"system\" | \"api\" | \"agent\";\n\nexport type BuiltInAction =\n | \"create\"\n | \"update\"\n | \"delete\"\n | \"archive\"\n | \"restore\"\n | \"comment\"\n | \"attachment\";\n\nexport type Action = BuiltInAction | (string & {});\n\nexport type ValueType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"date\"\n | \"currency\"\n | \"user\"\n | \"enum\"\n | \"json\"\n | \"custom\";\n\nexport type Resource = {\n type: string;\n id: string;\n title?: string;\n};\n\nexport type ResourceReference = Pick<Resource, \"type\" | \"id\"> & {\n title?: string;\n};\n\nexport type Actor = {\n type: ActorType;\n id: string;\n name: string;\n avatarUrl?: string;\n};\n\nexport type Change = {\n field: string;\n label: string;\n before?: unknown;\n after?: unknown;\n valueType: ValueType;\n};\n\nexport type CommentContent = {\n type: \"comment\";\n text: string;\n};\n\nexport type AttachmentContent = {\n type: \"attachment\";\n fileName: string;\n mimeType: string;\n size: number;\n url?: string;\n};\n\nexport type CustomContent = {\n type: \"custom\";\n [key: string]: unknown;\n};\n\nexport type Content = CommentContent | AttachmentContent | CustomContent;\n\nexport type Metadata = {\n source?: string;\n version?: string;\n requestId?: string;\n ipAddress?: string;\n custom?: Record<string, unknown>;\n [key: string]: unknown;\n};\n\nexport type ActivityRecord = Readonly<{\n id: string;\n resource: Readonly<Resource>;\n action: Action;\n actor: Readonly<Actor>;\n timestamp: Date;\n changes?: readonly Readonly<Change>[];\n content?: Readonly<Content>;\n metadata?: Readonly<Metadata>;\n}>;\n\nexport type TrackInput = {\n resource: ResourceReference;\n actor: Actor;\n action: Action;\n changes?: Change[];\n content?: Content;\n metadata?: Metadata;\n timestamp?: Date;\n};\n\nexport type QueryOptions = {\n resource: ResourceReference;\n search?: string;\n actor?: string;\n actorId?: string;\n actions?: readonly Action[];\n from?: Date;\n to?: Date;\n limit?: number;\n offset?: number;\n};\n\nexport type QueryResult = {\n entries: ActivityRecord[];\n total: number;\n hasMore: boolean;\n};\n\nexport type StorageAdapter = {\n insert(entry: ActivityRecord): Promise<void>;\n query(options: QueryOptions): Promise<QueryResult>;\n};\n\nexport type Activity = {\n track(input: TrackInput): Promise<ActivityRecord>;\n query(options: QueryOptions): Promise<ActivityRecord[]>;\n};\n\nexport type ActivityOptions = {\n adapter: StorageAdapter;\n clock?: () => Date;\n idGenerator?: () => string;\n};\n\nexport class ActivityError extends Error {\n readonly code: string;\n readonly field?: string;\n\n constructor(code: string, message: string, field?: string) {\n super(message);\n this.name = \"ActivityError\";\n this.code = code;\n this.field = field;\n }\n}\n\nexport function createActivity({\n adapter,\n clock = () => new Date(),\n idGenerator = createId,\n}: ActivityOptions): Activity {\n return {\n async track(input) {\n const record = createRecord(input, clock, idGenerator);\n await adapter.insert(record);\n return record;\n },\n async query(options) {\n const normalized = normalizeQuery(options);\n const result = await adapter.query(normalized);\n return result.entries;\n },\n };\n}\n\nexport function createMemoryStorageAdapter(\n initialEntries: ActivityRecord[] = [],\n latency = 0,\n): StorageAdapter {\n let entries = initialEntries.map(freezeRecord);\n\n return {\n async insert(entry) {\n await wait(latency);\n entries = [freezeRecord(entry), ...entries];\n },\n async query(options) {\n await wait(latency);\n const normalized = normalizeQuery(options);\n const filtered = entries\n .filter((entry) => entry.resource.type === normalized.resource.type)\n .filter((entry) => entry.resource.id === normalized.resource.id)\n .filter((entry) => matchesActions(entry, normalized.actions))\n .filter((entry) => matchesActor(entry, normalized.actorId ?? normalized.actor))\n .filter((entry) => matchesDates(entry, normalized.from, normalized.to))\n .filter((entry) => matchesSearch(entry, normalized.search))\n .sort(compareRecords);\n const offset = normalized.offset ?? 0;\n const limit = normalized.limit ?? 50;\n const page = filtered.slice(offset, offset + limit);\n\n return {\n entries: page,\n total: filtered.length,\n hasMore: offset + page.length < filtered.length,\n };\n },\n };\n}\n\ntype Queryable = {\n query(sql: string, params?: readonly unknown[]): Promise<{ rows: unknown[] }>;\n};\n\ntype ReleasableQueryable = Queryable & { release(): void };\ntype Connectable = Queryable & { connect(): Promise<ReleasableQueryable> };\n\nexport function postgresAdapter(client: Queryable | Connectable): StorageAdapter {\n return {\n async insert(entry) {\n const connection = \"connect\" in client ? await client.connect() : client;\n await connection.query(\"BEGIN\");\n try {\n await connection.query(\n `insert into activity_entries (\n id, resource_type, resource_id, resource_title, action,\n actor_type, actor_id, actor_name, actor_avatar_url,\n content_type, content_json, metadata_json, created_at\n ) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,\n [\n entry.id,\n entry.resource.type,\n entry.resource.id,\n entry.resource.title ?? null,\n entry.action,\n entry.actor.type,\n entry.actor.id,\n entry.actor.name,\n entry.actor.avatarUrl ?? null,\n entry.content?.type ?? null,\n entry.content ? JSON.stringify(entry.content) : null,\n entry.metadata ? JSON.stringify(entry.metadata) : null,\n entry.timestamp.toISOString(),\n ],\n );\n\n for (const [position, change] of (entry.changes ?? []).entries()) {\n await connection.query(\n `insert into activity_changes (\n id, entry_id, position, field, label, before_value, after_value, value_type\n ) values ($1, $2, $3, $4, $5, $6, $7, $8)`,\n [\n createId(),\n entry.id,\n position,\n change.field,\n change.label,\n JSON.stringify(change.before ?? null),\n JSON.stringify(change.after ?? null),\n change.valueType,\n ],\n );\n }\n\n await connection.query(\"COMMIT\");\n } catch (error) {\n await connection.query(\"ROLLBACK\");\n throw toActivityError(error, \"STORAGE_FAILURE\", \"Could not insert activity record\");\n } finally {\n if (\"release\" in connection && typeof connection.release === \"function\") {\n connection.release();\n }\n }\n },\n async query(options) {\n const normalized = normalizeQuery(options);\n const limit = normalized.limit ?? 50;\n const offset = normalized.offset ?? 0;\n const params: unknown[] = [normalized.resource.type, normalized.resource.id];\n const where = [\"e.resource_type = $1\", \"e.resource_id = $2\"];\n\n if (normalized.actions?.length) {\n params.push(normalized.actions);\n where.push(`e.action = any($${params.length}::text[])`);\n }\n\n const actorId = normalized.actorId ?? normalized.actor;\n if (actorId) {\n params.push(actorId);\n where.push(`e.actor_id = $${params.length}`);\n }\n\n if (normalized.from) {\n params.push(normalized.from.toISOString());\n where.push(`e.created_at >= $${params.length}`);\n }\n\n if (normalized.to) {\n params.push(normalized.to.toISOString());\n where.push(`e.created_at <= $${params.length}`);\n }\n\n if (normalized.search) {\n params.push(`%${normalized.search}%`);\n const index = params.length;\n where.push(`(\n e.actor_name ilike $${index}\n or e.resource_title ilike $${index}\n or e.content_json::text ilike $${index}\n or exists (\n select 1 from activity_changes c\n where c.entry_id = e.id\n and (\n c.label ilike $${index}\n or c.before_value::text ilike $${index}\n or c.after_value::text ilike $${index}\n )\n )\n )`);\n }\n\n const whereSql = where.join(\" and \");\n const count = await client.query(\n `select count(*)::int as total from activity_entries e where ${whereSql}`,\n params,\n );\n params.push(limit + 1, offset);\n const rows = await client.query(\n `select\n e.*,\n coalesce(\n json_agg(\n json_build_object(\n 'field', c.field,\n 'label', c.label,\n 'before', c.before_value,\n 'after', c.after_value,\n 'valueType', c.value_type\n )\n order by c.position\n ) filter (where c.id is not null),\n '[]'\n ) as changes\n from activity_entries e\n left join activity_changes c on c.entry_id = e.id\n where ${whereSql}\n group by e.id\n order by e.created_at desc, e.id desc\n limit $${params.length - 1} offset $${params.length}`,\n params,\n );\n\n const total = readTotal(count.rows[0]);\n const entries = rows.rows.slice(0, limit).map(mapPostgresRecord);\n\n return {\n entries,\n total,\n hasMore: rows.rows.length > limit,\n };\n },\n };\n}\n\nfunction createRecord(\n input: TrackInput,\n clock: () => Date,\n idGenerator: () => string,\n): ActivityRecord {\n validateTrackInput(input);\n\n const record: ActivityRecord = {\n id: idGenerator(),\n resource: freezeObject({\n type: input.resource.type.trim(),\n id: input.resource.id.trim(),\n title: input.resource.title?.trim(),\n }),\n action: input.action.trim().toLowerCase() as Action,\n actor: freezeObject({ ...input.actor, name: input.actor.name.trim() }),\n timestamp: input.timestamp ? new Date(input.timestamp) : clock(),\n changes: input.changes?.map(normalizeChange).map(freezeObject),\n content: input.content ? freezeObject(input.content) : undefined,\n metadata: input.metadata ? freezeObject(input.metadata) : undefined,\n };\n\n return freezeRecord(record);\n}\n\nfunction validateTrackInput(input: TrackInput) {\n if (!input.resource?.type?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"resource.type is required\", \"resource.type\");\n }\n\n if (!input.resource?.id?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"resource.id is required\", \"resource.id\");\n }\n\n if (![\"user\", \"system\", \"api\", \"agent\"].includes(input.actor?.type)) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.type is invalid\", \"actor.type\");\n }\n\n if (!input.actor?.id?.trim()) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.id is required\", \"actor.id\");\n }\n\n if (!input.actor?.name?.trim()) {\n throw new ActivityError(\"INVALID_ACTOR\", \"actor.name is required\", \"actor.name\");\n }\n\n if (!input.action?.trim()) {\n throw new ActivityError(\"INVALID_ACTION\", \"action is required\", \"action\");\n }\n\n if (input.timestamp && Number.isNaN(input.timestamp.getTime())) {\n throw new ActivityError(\"INVALID_TIMESTAMP\", \"timestamp must be a valid Date\", \"timestamp\");\n }\n\n if (input.action === \"update\" && !input.changes?.length) {\n throw new ActivityError(\"INVALID_ACTION\", \"update requires at least one change\", \"changes\");\n }\n\n if ((input.action === \"comment\" || input.action === \"attachment\") && !input.content) {\n throw new ActivityError(\"INVALID_ACTION\", `${input.action} requires content`, \"content\");\n }\n}\n\nfunction normalizeChange(change: Change): Change {\n if (!change.field.trim()) {\n throw new ActivityError(\"INVALID_CHANGE\", \"change.field is required\", \"changes.field\");\n }\n\n if (!change.label.trim()) {\n throw new ActivityError(\"INVALID_CHANGE\", \"change.label is required\", \"changes.label\");\n }\n\n return {\n ...change,\n field: change.field.trim(),\n label: change.label.trim(),\n valueType: change.valueType ?? inferValueType(change.after ?? change.before),\n };\n}\n\nfunction normalizeQuery(options: QueryOptions): QueryOptions {\n if (!options.resource?.type?.trim() || !options.resource?.id?.trim()) {\n throw new ActivityError(\"INVALID_RESOURCE\", \"query.resource is required\", \"resource\");\n }\n\n if (options.limit !== undefined && (options.limit < 1 || options.limit > 500)) {\n throw new ActivityError(\"INVALID_LIMIT\", \"limit must be between 1 and 500\", \"limit\");\n }\n\n if (options.offset !== undefined && options.offset < 0) {\n throw new ActivityError(\"INVALID_OFFSET\", \"offset must not be negative\", \"offset\");\n }\n\n if (options.from && options.to && options.from > options.to) {\n throw new ActivityError(\"INVALID_DATE_RANGE\", \"from must be before to\", \"from\");\n }\n\n return freezeObject({\n ...options,\n resource: freezeObject({\n type: options.resource.type.trim(),\n id: options.resource.id.trim(),\n title: options.resource.title?.trim(),\n }),\n search: options.search?.trim() || undefined,\n actor: options.actor?.trim() || undefined,\n actorId: options.actorId?.trim() || undefined,\n actions: options.actions\n ? Object.freeze(\n Array.from(\n new Set(options.actions.map((action) => action.trim().toLowerCase() as Action)),\n ),\n )\n : undefined,\n limit: options.limit ?? 50,\n offset: options.offset ?? 0,\n });\n}\n\nfunction matchesActions(entry: ActivityRecord, actions?: readonly Action[]) {\n return !actions?.length || actions.includes(entry.action);\n}\n\nfunction matchesActor(entry: ActivityRecord, actorId?: string) {\n return !actorId || entry.actor.id === actorId;\n}\n\nfunction matchesDates(entry: ActivityRecord, from?: Date, to?: Date) {\n return (!from || entry.timestamp >= from) && (!to || entry.timestamp <= to);\n}\n\nfunction matchesSearch(entry: ActivityRecord, search?: string) {\n if (!search) {\n return true;\n }\n\n return searchableText(entry).toLowerCase().includes(search.toLowerCase());\n}\n\nfunction searchableText(entry: ActivityRecord) {\n return [\n entry.action,\n entry.resource.title,\n entry.actor.name,\n entry.content ? JSON.stringify(entry.content) : \"\",\n ...(entry.changes ?? []).flatMap((change) => [\n change.field,\n change.label,\n stringifyValue(change.before),\n stringifyValue(change.after),\n ]),\n ].join(\" \");\n}\n\nfunction compareRecords(a: ActivityRecord, b: ActivityRecord) {\n const time = b.timestamp.getTime() - a.timestamp.getTime();\n return time || b.id.localeCompare(a.id);\n}\n\nfunction inferValueType(value: unknown): ValueType {\n if (value instanceof Date) {\n return \"date\";\n }\n\n const valueType = typeof value;\n if (valueType === \"string\" || valueType === \"number\" || valueType === \"boolean\") {\n return valueType;\n }\n\n return \"json\";\n}\n\nfunction freezeRecord(record: ActivityRecord): ActivityRecord {\n return freezeObject({\n ...record,\n resource: freezeObject(record.resource),\n actor: freezeObject(record.actor),\n changes: record.changes?.map((change) => freezeObject(change)),\n content: record.content ? freezeObject(record.content) : undefined,\n metadata: record.metadata ? freezeObject(record.metadata) : undefined,\n });\n}\n\nfunction freezeObject<T extends object>(value: T): Readonly<T> {\n return Object.freeze(value);\n}\n\nfunction stringifyValue(value: unknown) {\n if (value === null || value === undefined) {\n return \"\";\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n return typeof value === \"string\" ? value : JSON.stringify(value);\n}\n\nfunction createId() {\n if (globalThis.crypto?.randomUUID) {\n return globalThis.crypto.randomUUID();\n }\n\n return `act_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;\n}\n\nfunction wait(ms: number) {\n return new Promise((resolve) => globalThis.setTimeout(resolve, ms));\n}\n\nfunction readTotal(row: unknown) {\n if (isRecord(row) && typeof row.total === \"number\") {\n return row.total;\n }\n\n if (isRecord(row) && typeof row.total === \"string\") {\n return Number(row.total);\n }\n\n return 0;\n}\n\nfunction mapPostgresRecord(row: unknown): ActivityRecord {\n if (!isRecord(row)) {\n throw new ActivityError(\"STORAGE_FAILURE\", \"Invalid activity row returned by storage\");\n }\n\n return freezeRecord({\n id: String(row.id),\n resource: {\n type: String(row.resource_type),\n id: String(row.resource_id),\n title: typeof row.resource_title === \"string\" ? row.resource_title : undefined,\n },\n action: String(row.action) as Action,\n actor: {\n type: String(row.actor_type) as ActorType,\n id: String(row.actor_id),\n name: String(row.actor_name),\n avatarUrl: typeof row.actor_avatar_url === \"string\" ? row.actor_avatar_url : undefined,\n },\n timestamp: new Date(String(row.created_at)),\n changes: Array.isArray(row.changes) ? row.changes.map(mapPostgresChange) : undefined,\n content: isContent(row.content_json) ? row.content_json : undefined,\n metadata: isRecord(row.metadata_json) ? row.metadata_json : undefined,\n });\n}\n\nfunction mapPostgresChange(change: unknown): Change {\n if (!isRecord(change)) {\n throw new ActivityError(\"STORAGE_FAILURE\", \"Invalid change row returned by storage\");\n }\n\n return {\n field: String(change.field),\n label: String(change.label),\n before: change.before,\n after: change.after,\n valueType: String(change.valueType) as ValueType,\n };\n}\n\nfunction isContent(value: unknown): value is Content {\n return isRecord(value) && typeof value.type === \"string\";\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction toActivityError(error: unknown, code: string, fallback: string) {\n if (error instanceof ActivityError) {\n return error;\n }\n\n if (error instanceof Error) {\n return new ActivityError(code, error.message);\n }\n\n return new ActivityError(code, fallback);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED