fix(kb-ui): P0 provenance crash + AA badges + a11y + first-run polish
Provenance/claims carried created_by/verified_by as {type,id} principal
refs from the service, but the types said string|null and ProvenancePanel
rendered them raw → React #31 crash on every object view. Widen to
string|PrincipalRef|null and add formatPrincipal() (barrel-exported).
Badges: mode-aware semantic tones (mix toward --foreground) so success/
warning/info/danger text clears WCAG AA on the subtle tint in BOTH light
and dark; Active badge 2.81→5.25/5.67:1. SensitivityBadge renders nothing
for an unset level (objects inherit from their collection) instead of a
misleading "—" pill. TierBadge tolerates an unknown tier.
Accessibility: Badge gains ariaLabel (screen readers announce the meaning,
not just the terse label); Field links label↔control via htmlFor + useId
and wires aria-describedby to the hint; OwnerToggle gets aria-pressed;
icons aria-hidden.
First-run: CollectionForm leads with Name and auto-derives the Identifier
(was slug-first); placeholder text uses --muted-foreground. Drop the
Sparkles "AI-magic" icons from the review empty state and the agent tier
badge (Inbox / Bot instead), per the no-magic-framing principle.
Verified live on dev-knowledge.sky-ai.com.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,8 +4,8 @@
|
||||
// dep) so a fresh consumer needs only this lib's alias.
|
||||
// ===========================================================================
|
||||
import type { FC, ReactNode } from "react";
|
||||
import { Lock, ShieldAlert, Globe, Sparkles, UserCheck, FileText } from "lucide-react";
|
||||
import type { AssertionTier, Curation, ObjectStatus, Sensitivity } from "./types";
|
||||
import { Lock, ShieldAlert, Globe, Bot, UserCheck, FileText } from "lucide-react";
|
||||
import type { AssertionTier, Curation, ObjectStatus, PrincipalRef, Sensitivity } from "./types";
|
||||
|
||||
export function cn(...parts: (string | false | null | undefined)[]): string {
|
||||
return parts.filter(Boolean).join(" ");
|
||||
@@ -58,6 +58,14 @@ function rel(n: number, unit: string): string {
|
||||
return n <= 0 ? `${abs} ${u} ago` : `in ${abs} ${u}`;
|
||||
}
|
||||
|
||||
/** Render a principal (`"account:you"` or the service's `{type, id}` ref) as display text. */
|
||||
export function formatPrincipal(p?: string | PrincipalRef | null): string | null {
|
||||
if (p == null) return null;
|
||||
if (typeof p === "string") return p;
|
||||
const id = typeof p.id === "string" && p.id.length > 12 ? `${p.id.slice(0, 8)}…` : String(p.id ?? "");
|
||||
return id ? `${p.type} · ${id}` : p.type;
|
||||
}
|
||||
|
||||
/** Render a claim/JSON value compactly for cards and one-liners. */
|
||||
export function renderValue(value: unknown): string {
|
||||
if (value == null) return "—";
|
||||
@@ -82,15 +90,33 @@ export function isImageMime(mime?: string | null): boolean {
|
||||
const badgeBase =
|
||||
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium leading-none";
|
||||
|
||||
export const Badge: FC<{ tone?: string; icon?: ReactNode; children: ReactNode; className?: string; title?: string }> = ({
|
||||
tone,
|
||||
icon,
|
||||
children,
|
||||
className,
|
||||
title,
|
||||
}) => (
|
||||
<span className={cn(badgeBase, tone, className)} title={title}>
|
||||
{icon}
|
||||
// Semantic badge tones. The text mixes the semantic colour toward --foreground
|
||||
// so it clears WCAG AA (4.5:1) on the subtle tint in BOTH light and dark mode:
|
||||
// in light mode --foreground is near-black (darkens the text), in dark mode it's
|
||||
// near-white (lightens it). A static black mix would fail dark mode.
|
||||
export const successTone =
|
||||
"bg-[color-mix(in_oklab,var(--success)_16%,transparent)] text-[color-mix(in_oklab,var(--success),var(--foreground)_22%)]";
|
||||
export const warningTone =
|
||||
"bg-[color-mix(in_oklab,var(--warning)_18%,transparent)] text-[color-mix(in_oklab,var(--warning),var(--foreground)_34%)]";
|
||||
export const infoTone =
|
||||
"bg-[color-mix(in_oklab,var(--info,var(--primary))_16%,transparent)] text-[color-mix(in_oklab,var(--info,var(--primary)),var(--foreground)_16%)]";
|
||||
export const dangerTone =
|
||||
"bg-[color-mix(in_oklab,var(--destructive)_15%,transparent)] text-[color-mix(in_oklab,var(--destructive),var(--foreground)_16%)]";
|
||||
|
||||
export const Badge: FC<{
|
||||
tone?: string;
|
||||
icon?: ReactNode;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
/** Mouse-hover explanation. */
|
||||
title?: string;
|
||||
/** Accessible name for assistive tech; falls back to `title`. Set this so a
|
||||
* screen reader announces the meaning ("Open — may go to any AI model"),
|
||||
* not just the terse visible label. */
|
||||
ariaLabel?: string;
|
||||
}> = ({ tone, icon, children, className, title, ariaLabel }) => (
|
||||
<span className={cn(badgeBase, tone, className)} title={title} aria-label={ariaLabel ?? title}>
|
||||
{icon != null && <span aria-hidden="true" className="inline-flex">{icon}</span>}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
@@ -104,31 +130,27 @@ const SENSITIVITY: Record<Sensitivity, { label: string; tone: string; icon: Reac
|
||||
},
|
||||
restricted: {
|
||||
label: "Restricted",
|
||||
tone: "bg-[color-mix(in_oklab,var(--warning)_18%,transparent)] text-[var(--warning)]",
|
||||
tone: warningTone,
|
||||
icon: <ShieldAlert className="size-3" />,
|
||||
title: "Restricted — amounts and personal detail; approved AI destinations only, via redaction.",
|
||||
},
|
||||
vault: {
|
||||
label: "Vault",
|
||||
tone: "bg-[color-mix(in_oklab,var(--destructive)_15%,transparent)] text-destructive",
|
||||
tone: dangerTone,
|
||||
icon: <Lock className="size-3" />,
|
||||
title: "Vault — identity/medical grade; never sent to any AI model.",
|
||||
},
|
||||
};
|
||||
|
||||
export const SensitivityBadge: FC<{ level: Sensitivity; className?: string }> = ({ level, className }) => {
|
||||
// A row can arrive without a sensitivity (e.g. a claim/note, or older data);
|
||||
// fall back to a neutral badge rather than crash — and never default an
|
||||
// unknown value to "Open", which would misstate its exposure.
|
||||
const s =
|
||||
SENSITIVITY[level] ?? {
|
||||
label: level ?? "—",
|
||||
tone: "bg-muted text-muted-foreground",
|
||||
icon: null,
|
||||
title: "Sensitivity not set.",
|
||||
};
|
||||
// A row can arrive without a sensitivity: objects inherit it from their
|
||||
// collection, and claims/notes carry none. Render NOTHING in that case — a
|
||||
// "—" pill reads as broken data. Never default an unknown value to "Open",
|
||||
// which would misstate its exposure.
|
||||
const s = SENSITIVITY[level];
|
||||
if (!s) return null;
|
||||
return (
|
||||
<Badge tone={s.tone} icon={s.icon} className={className} title={s.title}>
|
||||
<Badge tone={s.tone} icon={s.icon} className={className} title={s.title} ariaLabel={`Sensitivity: ${s.title}`}>
|
||||
{s.label}
|
||||
</Badge>
|
||||
);
|
||||
@@ -136,11 +158,15 @@ export const SensitivityBadge: FC<{ level: Sensitivity; className?: string }> =
|
||||
|
||||
export const CurationBadge: FC<{ curation: Curation; className?: string }> = ({ curation, className }) => {
|
||||
const gated = curation === "gated";
|
||||
const title = gated
|
||||
? "Gated — an agent's claims stay hidden until you confirm them."
|
||||
: "Live — an agent's claims appear immediately, labelled by tier.";
|
||||
return (
|
||||
<Badge
|
||||
tone={gated ? "bg-[color-mix(in_oklab,var(--info,var(--primary))_16%,transparent)] text-[var(--info,var(--primary))]" : "bg-muted text-muted-foreground"}
|
||||
tone={gated ? infoTone : "bg-muted text-muted-foreground"}
|
||||
className={className}
|
||||
title={gated ? "Gated — an agent's claims stay hidden until you confirm them." : "Live — an agent's claims appear immediately, labelled by tier."}
|
||||
title={title}
|
||||
ariaLabel={`Claim curation: ${title}`}
|
||||
>
|
||||
{gated ? "Gated" : "Live"}
|
||||
</Badge>
|
||||
@@ -148,12 +174,12 @@ export const CurationBadge: FC<{ curation: Curation; className?: string }> = ({
|
||||
};
|
||||
|
||||
const STATUS: Record<ObjectStatus, { label: string; tone: string }> = {
|
||||
active: { label: "Active", tone: "bg-[color-mix(in_oklab,var(--success)_16%,transparent)] text-[var(--success)]" },
|
||||
active: { label: "Active", tone: successTone },
|
||||
ingesting: { label: "Processing", tone: "bg-muted text-muted-foreground" },
|
||||
proposed: { label: "Proposed", tone: "bg-[color-mix(in_oklab,var(--warning)_18%,transparent)] text-[var(--warning)]" },
|
||||
proposed: { label: "Proposed", tone: warningTone },
|
||||
superseded: { label: "Superseded", tone: "bg-muted text-muted-foreground" },
|
||||
archived: { label: "Archived", tone: "bg-muted text-muted-foreground" },
|
||||
failed: { label: "Failed", tone: "bg-[color-mix(in_oklab,var(--destructive)_15%,transparent)] text-destructive" },
|
||||
failed: { label: "Failed", tone: dangerTone },
|
||||
};
|
||||
|
||||
export const StatusBadge: FC<{ status: ObjectStatus; className?: string }> = ({ status, className }) => {
|
||||
@@ -174,13 +200,13 @@ const TIER: Record<AssertionTier, { label: string; tone: string; icon: ReactNode
|
||||
},
|
||||
agent_asserted: {
|
||||
label: "Agent-asserted",
|
||||
tone: "bg-[color-mix(in_oklab,var(--info,var(--primary))_16%,transparent)] text-[var(--info,var(--primary))]",
|
||||
icon: <Sparkles className="size-3" />,
|
||||
tone: infoTone,
|
||||
icon: <Bot className="size-3" />,
|
||||
title: "Asserted by an agent — not yet confirmed by a person.",
|
||||
},
|
||||
human_confirmed: {
|
||||
label: "You confirmed",
|
||||
tone: "bg-[color-mix(in_oklab,var(--success)_16%,transparent)] text-[var(--success)]",
|
||||
tone: successTone,
|
||||
icon: <UserCheck className="size-3" />,
|
||||
title: "Confirmed by a person.",
|
||||
},
|
||||
@@ -188,9 +214,9 @@ const TIER: Record<AssertionTier, { label: string; tone: string; icon: ReactNode
|
||||
|
||||
/** The trust surface — categorical, never a score ([[feedback_trust_is_human]]). */
|
||||
export const TierBadge: FC<{ tier: AssertionTier; className?: string }> = ({ tier, className }) => {
|
||||
const t = TIER[tier];
|
||||
const t = TIER[tier] ?? TIER.extracted;
|
||||
return (
|
||||
<Badge tone={t.tone} icon={t.icon} className={className} title={t.title}>
|
||||
<Badge tone={t.tone} icon={t.icon} className={className} title={t.title} ariaLabel={`Trust: ${t.title}`}>
|
||||
{t.label}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// via the general claims list. Props in, callbacks out.
|
||||
// ===========================================================================
|
||||
import { type FC, type ReactNode } from "react";
|
||||
import { AlertTriangle, ArrowUpRight, Check, RotateCcw, Sparkles, X } from "lucide-react";
|
||||
import { AlertTriangle, ArrowUpRight, Check, Inbox, RotateCcw, X } from "lucide-react";
|
||||
import type { Claim } from "./types";
|
||||
import { Badge, TierBadge, cn, formatDate, renderValue } from "./_internal";
|
||||
|
||||
@@ -114,7 +114,7 @@ export const ConflictPair: FC<ConflictPairProps> = ({ claims, busy, onKeep, onDi
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{claims.map((c, i) => (
|
||||
<div key={c.claim_id} className="flex flex-col gap-2 rounded-lg border border-border bg-card p-3">
|
||||
<p className={cn("text-sm font-medium", values.filter((v) => v === values[i]).length === 1 && "text-[var(--warning)]")}>
|
||||
<p className={cn("text-sm font-medium", values.filter((v) => v === values[i]).length === 1 && "text-[color-mix(in_oklab,var(--warning),var(--foreground)_34%)]")}>
|
||||
{values[i]}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
@@ -277,7 +277,7 @@ const Section: FC<{ title: string; count: number; hint: string; children: ReactN
|
||||
|
||||
const Empty: FC = () => (
|
||||
<div className="flex flex-col items-center gap-2 rounded-xl border border-dashed border-border py-12 text-center">
|
||||
<Sparkles className="size-6 text-muted-foreground" />
|
||||
<Inbox className="size-6 text-muted-foreground" aria-hidden="true" />
|
||||
<p className="text-sm font-medium text-foreground">Nothing to review</p>
|
||||
<p className="text-xs text-muted-foreground">Your agents' claims will appear here when there's something to confirm.</p>
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
// owner — "Your corpuses" (account) then each organisation (tenant).
|
||||
// Props in, callbacks out; the app owns fetching + routing.
|
||||
// ===========================================================================
|
||||
import { useState, type FC, type ReactNode } from "react";
|
||||
import { cloneElement, isValidElement, useId, useState, type FC, type ReactElement, type ReactNode } from "react";
|
||||
import { Plus, Users, User, ChevronRight, Loader2 } from "lucide-react";
|
||||
import type { Collection, CollectionInput, CollectionPatch, Curation, Sensitivity } from "./types";
|
||||
import { Badge, CurationBadge, SensitivityBadge, cn } from "./_internal";
|
||||
import { Badge, CurationBadge, SensitivityBadge, cn, warningTone } from "./_internal";
|
||||
|
||||
const SENS_ORDER: Sensitivity[] = ["open", "restricted", "vault"];
|
||||
|
||||
@@ -42,7 +42,11 @@ export const CollectionCard: FC<CollectionCardProps> = ({ collection, pendingCou
|
||||
<SensitivityBadge level={collection.sensitivity} />
|
||||
<CurationBadge curation={collection.curation} />
|
||||
{collection.claim_extraction === "auto" && (
|
||||
<Badge tone="bg-muted text-muted-foreground" title="New documents are scanned for claims automatically.">
|
||||
<Badge
|
||||
tone="bg-muted text-muted-foreground"
|
||||
title="New documents are scanned for claims automatically."
|
||||
ariaLabel="Auto-claims: new documents are scanned for claims automatically."
|
||||
>
|
||||
Auto-claims
|
||||
</Badge>
|
||||
)}
|
||||
@@ -50,7 +54,11 @@ export const CollectionCard: FC<CollectionCardProps> = ({ collection, pendingCou
|
||||
{collection.object_count ?? 0} {collection.object_count === 1 ? "item" : "items"}
|
||||
</span>
|
||||
{pendingCount != null && pendingCount > 0 && (
|
||||
<Badge tone="bg-[color-mix(in_oklab,var(--warning)_18%,transparent)] text-[var(--warning)]" title="Claims awaiting your review.">
|
||||
<Badge
|
||||
tone={warningTone}
|
||||
title="Claims awaiting your review."
|
||||
ariaLabel={`${pendingCount} claim${pendingCount === 1 ? "" : "s"} awaiting your review.`}
|
||||
>
|
||||
{pendingCount} to review
|
||||
</Badge>
|
||||
)}
|
||||
@@ -211,8 +219,12 @@ export const CollectionForm: FC<CollectionFormProps> = ({
|
||||
const readOnly = isEdit && initial?.owner_type === "tenant" && !canManageTenant;
|
||||
|
||||
const [slug, setSlug] = useState(initial?.slug ?? "");
|
||||
const [slugTouched, setSlugTouched] = useState(false);
|
||||
const [name, setName] = useState(initial?.name ?? "");
|
||||
const [description, setDescription] = useState(initial?.description ?? "");
|
||||
|
||||
const slugify = (s: string) =>
|
||||
s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
const [sensitivity, setSensitivity] = useState<Sensitivity>(initial?.sensitivity ?? "open");
|
||||
const [curation, setCuration] = useState<Curation>(initial?.curation ?? "live");
|
||||
const [claimAuto, setClaimAuto] = useState((initial?.claim_extraction ?? "off") === "auto");
|
||||
@@ -262,23 +274,41 @@ export const CollectionForm: FC<CollectionFormProps> = ({
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field label="Name" hint="What you'll call this corpus — e.g. “Supplier documents”.">
|
||||
<input
|
||||
className={inputCls}
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
// Auto-fill the identifier from the name until the user edits it by hand.
|
||||
if (!isEdit && !slugTouched) setSlug(slugify(e.target.value));
|
||||
}}
|
||||
disabled={readOnly}
|
||||
placeholder="Supplier documents"
|
||||
autoFocus={!isEdit}
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{!isEdit && (
|
||||
<Field label="Slug" hint="Lower-case, hyphenated. Can't be changed later.">
|
||||
<Field
|
||||
label="Identifier"
|
||||
hint="Used in links and by agents. Filled in from the name — edit if you like, but it can't be changed later."
|
||||
>
|
||||
<input
|
||||
data-action="knowledge-corpus-slug"
|
||||
className={inputCls}
|
||||
className={cn(inputCls, "font-mono")}
|
||||
value={slug}
|
||||
onChange={(e) => setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "-"))}
|
||||
placeholder="supplier-docs"
|
||||
onChange={(e) => {
|
||||
setSlugTouched(true);
|
||||
setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "-"));
|
||||
}}
|
||||
placeholder="supplier-documents"
|
||||
required
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field label="Name">
|
||||
<input className={inputCls} value={name} onChange={(e) => setName(e.target.value)} disabled={readOnly} required />
|
||||
</Field>
|
||||
|
||||
<Field label="Description" hint="What belongs in this corpus?">
|
||||
<textarea
|
||||
className={cn(inputCls, "min-h-[64px] resize-y")}
|
||||
@@ -345,20 +375,39 @@ export const CollectionForm: FC<CollectionFormProps> = ({
|
||||
};
|
||||
|
||||
const inputCls =
|
||||
"w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground outline-none transition focus:border-primary/50 focus:ring-2 focus:ring-primary/30 disabled:opacity-60";
|
||||
"w-full rounded-lg border border-border bg-background px-3 py-2 text-sm text-foreground outline-none transition placeholder:text-muted-foreground focus:border-primary/50 focus:ring-2 focus:ring-primary/30 disabled:opacity-60";
|
||||
|
||||
const Field: FC<{ label: string; hint?: string; children: ReactNode }> = ({ label, hint, children }) => (
|
||||
const Field: FC<{ label: string; hint?: string; children: ReactNode }> = ({ label, hint, children }) => {
|
||||
const id = useId();
|
||||
const hintId = `${id}-hint`;
|
||||
// Link the label (and any hint) to the single control child for screen readers.
|
||||
const control =
|
||||
isValidElement(children) && (children.type === "input" || children.type === "select" || children.type === "textarea")
|
||||
? cloneElement(children as ReactElement<Record<string, unknown>>, {
|
||||
id,
|
||||
"aria-describedby": hint ? hintId : undefined,
|
||||
})
|
||||
: children;
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium text-foreground">{label}</label>
|
||||
{children}
|
||||
{hint && <p className="text-xs text-muted-foreground">{hint}</p>}
|
||||
<label htmlFor={id} className="text-sm font-medium text-foreground">
|
||||
{label}
|
||||
</label>
|
||||
{control}
|
||||
{hint && (
|
||||
<p id={hintId} className="text-xs text-muted-foreground">
|
||||
{hint}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const OwnerToggle: FC<{ active: boolean; onClick: () => void; icon: ReactNode; label: string }> = ({ active, onClick, icon, label }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-pressed={active}
|
||||
className={cn(
|
||||
"inline-flex flex-1 items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-sm font-medium transition",
|
||||
active ? "border-primary bg-primary/10 text-primary" : "border-border text-muted-foreground hover:bg-muted",
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
cn,
|
||||
formatBytes,
|
||||
formatDate,
|
||||
formatPrincipal,
|
||||
isImageMime,
|
||||
} from "./_internal";
|
||||
|
||||
@@ -153,7 +154,7 @@ export const ProvenancePanel: FC<{ outline: ObjectOutline; className?: string }>
|
||||
const p = outline.provenance;
|
||||
const rows: [string, ReactNode][] = [
|
||||
["Source", p.source_ref ? `${p.source_type} · ${p.source_ref}` : p.source_type],
|
||||
["Added by", p.created_by ?? "—"],
|
||||
["Added by", formatPrincipal(p.created_by) ?? "—"],
|
||||
["Added", formatDate(p.inserted_at)],
|
||||
["Effective", p.effective_at ? formatDate(p.effective_at) : "—"],
|
||||
["Last verified", p.verified_at ? formatDate(p.verified_at) : "Not confirmed"],
|
||||
@@ -187,7 +188,7 @@ export const SupersessionBanner: FC<{
|
||||
<div className={cn("flex flex-col gap-2", className)}>
|
||||
{outline.superseded_by && (
|
||||
<div className="flex items-center gap-2 rounded-lg border border-[var(--warning)]/40 bg-[color-mix(in_oklab,var(--warning)_10%,transparent)] px-3 py-2 text-sm">
|
||||
<span className="text-[var(--warning)]">A newer version of this document exists.</span>
|
||||
<span className="text-[color-mix(in_oklab,var(--warning),var(--foreground)_34%)]">A newer version of this document exists.</span>
|
||||
<button
|
||||
type="button"
|
||||
data-action="knowledge-open-newer"
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
// EXPORTS
|
||||
// Types + transport: all of ./types, KnowledgeTransport, MockKnowledgeTransport
|
||||
// Badges/formatters: SensitivityBadge, CurationBadge, StatusBadge, TierBadge,
|
||||
// Badge, Spinner, cn, formatBytes, formatDate, formatRelative,
|
||||
// renderValue, isImageMime
|
||||
// Badge, Spinner, cn, formatBytes, formatDate, formatPrincipal,
|
||||
// formatRelative, renderValue, isImageMime
|
||||
// Collections (W3): CollectionList, CollectionCard, CollectionForm
|
||||
// Object viewer (W4):ObjectList, ObjectViewer, CatalogCard, ProvenancePanel,
|
||||
// SupersessionBanner, OutlineNav, TextReader, CitationLink
|
||||
@@ -27,6 +27,7 @@ export {
|
||||
TierBadge,
|
||||
formatBytes,
|
||||
formatDate,
|
||||
formatPrincipal,
|
||||
formatRelative,
|
||||
renderValue,
|
||||
isImageMime,
|
||||
|
||||
12
src/types.ts
12
src/types.ts
@@ -94,10 +94,16 @@ export interface Entity {
|
||||
type: string;
|
||||
}
|
||||
|
||||
/** A principal reference as the service emits it: `{type: "account"|"app"|"agent"|…, id}`. */
|
||||
export interface PrincipalRef {
|
||||
type: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface Provenance {
|
||||
source_type: string;
|
||||
source_ref?: string | null;
|
||||
created_by?: string | null;
|
||||
created_by?: string | PrincipalRef | null;
|
||||
inserted_at?: string;
|
||||
effective_at?: string | null;
|
||||
verified_at?: string | null;
|
||||
@@ -210,8 +216,8 @@ export interface Claim {
|
||||
sensitivity: Sensitivity;
|
||||
effective_at?: string | null;
|
||||
verified_at?: string | null;
|
||||
verified_by?: string | null;
|
||||
created_by?: string | null;
|
||||
verified_by?: string | PrincipalRef | null;
|
||||
created_by?: string | PrincipalRef | null;
|
||||
updated_at?: string;
|
||||
cite?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user