UNPKG

@veltdev/types

Version:

Velt is an SDK to add collaborative features to your product within minutes. Example: Comments like Figma, Frame.io, Google docs or sheets, Recording like Loom, Huddles like Slack and much more.

268 lines (267 loc) 11.4 kB
/** * Suggestions feature — public type surface for v1. * * Source contract: docs/suggestions-api-contract.md * Source flows: docs/suggestions-implementation-flows.md * * Types are exported from the SDK's public barrel. Customer code interacts * with these via Snippyly.getSuggestionElement(); the SDK constructs all * Suggestion objects internally and customers do not build them directly. */ import { User } from './user.data.model'; /** * Lifecycle state machine for a Suggestion. * * - pending: Just created, awaiting owner action. * - approved: Owner clicked Approve. Customer apply handler ran successfully. * - rejected: Owner clicked Reject (with optional reason). * - stale: Target was unresolvable at approve time. Owner can dismiss. * - apply_failed: Customer apply handler threw during a suggestionApproved event. * Status set by the SDK after the throw was caught. */ export type SuggestionStatus = 'pending' | 'accepted' | 'rejected' | 'stale' | 'apply_failed'; /** * Global per-user-per-session suggestion mode. Not persisted; reload returns to 'editing'. */ export type SuggestionMode = 'editing' | 'suggesting'; /** * Discriminator for the substrate that produced a suggestion. v1: always 'custom'. * Wrapper libraries (e.g. tiptap-velt-comments) widen this union when they integrate. * * Customers should treat unknown values as opaque; the field exists so customer code * can differentiate substrate when domain-specific rendering matters. */ export type SuggestionTargetType = 'custom'; /** * Single-arg config passed to `SuggestionElement.registerTarget(config)`. * Carrying both `targetId` and `getter` in one object so the API can grow * (e.g., metadata, target-specific options) without breaking customers. */ export interface RegisterTargetConfig<T = unknown> { targetId: string; getter: TargetGetter<T>; } /** * Function the customer registers via SuggestionElement.registerTarget(config). * Required for any non-primitive (wrapper) target. * * The SDK calls this getter twice in a typical edit cycle: * 1. On focus of the tagged element — to snapshot the pre-edit value. * 2. On commit (focusout / change) — to read the current value for the diff. * * IMPORTANT — edit-time state, not persisted state: the getter must reflect * what the user is currently editing, not what's persisted in the customer's * app store. If the customer's state is only updated on commit/approve (as * is typical when suggesting mode is on), reading from that state would * return the snapshot value at commit time, the diff check would short- * circuit, and no suggestion would ever fire. * * Recommended: read from the DOM (controlled or uncontrolled inputs) so the * getter always returns what's visible to the user. * * el.registerTarget('row.123', () => ({ * qty: parseInt(qtyInput.value, 10), * price: parseInt(priceInput.value, 10), * })); * * For controlled inputs (state updates on every keystroke), reading from * the customer state is fine: `() => myState.row123`. The contract is * "edit-time state" — whichever source of truth has it. */ export type TargetGetter<T = unknown> = () => T; /** * Returned from SuggestionElement.on(...). Calling it removes the handler. */ export type Unsubscribe = () => void; /** * Details about a target edit, passed to the customer's resolver handler and * to subscribers of `targetEditStart` / `targetEditCommit` events. * * `element` is the DOM element that produced the edit. It's the tagged * descendant — or, for wrappers that nest interactive controls inside a * tagged container, the inner element that actually fired the event. In * either case the SDK walks up to the nearest ancestor carrying * `data-velt-suggestion-target` to resolve `targetId`. * * `element` is provided for read access only — customers should not retain * the reference past the synchronous handler call. */ export interface TargetEditDetails<T = unknown> { targetId: string; oldValue: T; newValue: T; element: Element | null; } /** * Return type of `onTargetEditCommit`. Returning a value auto-commits the * suggestion using the (optional) overrides; returning null skips the * auto-commit so a subscriber to `targetEditCommit` can drive it explicitly. */ export interface TargetEditCommitResult { /** Override the SDK's default `${targetId}: ${old} → ${new}` summary. */ summary?: string; /** Customer metadata persisted on the resulting Suggestion. */ metadata?: Record<string, unknown>; } /** * Return type of `onTargetEditStart`. Reserved for future fields; v1 has * no behavior-bearing return values, so customers may omit a return. * * Roadmap: future versions may accept `{ oldValue }` here so customers can * override the SDK's auto-snapshot with a domain-canonical value. */ export interface TargetEditStartResult { } export type TargetEditStartHandler<T = unknown> = (details: TargetEditDetails<T>) => TargetEditStartResult | void | null; export type TargetEditCommitHandler<T = unknown> = (details: TargetEditDetails<T>) => TargetEditCommitResult | null; /** * Config passed to `SuggestionElement.enableSuggestionMode(config?)`. * Both callbacks are optional. If `onTargetEditCommit` is omitted, customers * can still drive auto-commit by subscribing to the `targetEditCommit` event * and calling the pre-bound `commitSuggestion` builder on the payload. */ export interface EnableSuggestionModeConfig { /** * Invoked on focus of a tagged element, after the SDK captures the * snapshot. v1: informational — return value is reserved for future * use (e.g. `{ oldValue }` to override the SDK's auto-snapshot). */ onTargetEditStart?: TargetEditStartHandler; /** * Invoked once per detected commit (on `change` for atomic inputs, on * `focusout` for text-like inputs). Returning a `TargetEditCommitResult` * auto-commits with the supplied summary/metadata; returning null defers * to event subscribers. */ onTargetEditCommit?: TargetEditCommitHandler; } /** * SDK-managed suggestion data persisted on a CommentAnnotation. * Present iff annotation.type === 'suggestion'. * * Customer code must not write to this directly — use the SuggestionElement API. */ export interface SuggestionData { /** Lifecycle state machine. */ status: SuggestionStatus; /** Stable, customer-owned target identifier. */ targetId: string; /** v1: always 'custom'. Wrappers widen later. */ targetType: SuggestionTargetType; /** Snapshot taken at startSuggestion / focus time. Frozen via structuredClone. */ oldValue: any; /** Value submitted via commitSuggestion. */ newValue: any; /** Optional human-readable description for logs / notifications. */ summary: string | null; /** * True if the live value at approve time differed from oldValue. * v1 records flag only; v1.1 will surface a confirmation prompt. */ driftDetected: boolean; /** Populated only when status === 'rejected'. */ rejectReason: string | null; /** * Full User snapshot of the resolver, populated when status moves to * approved | rejected | stale | apply_failed. Snapshot rather than userId * so customer code can render name/email/photoUrl without an extra lookup, * and so historical suggestions retain their resolver record even if the * user later updates their profile. */ resolvedBy: User | null; resolvedAt: number | null; } /** * Fields shared across every Suggestion regardless of status. * Internal — used to build the per-status discriminated types below. */ interface SuggestionBase<T = unknown> { /** Annotation document id (same as the underlying CommentAnnotation.annotationId). */ annotationId: string; /** Stable, customer-owned target identifier. */ targetId: string; /** v1: always 'custom'. */ targetType: SuggestionTargetType; /** Snapshot taken at startSuggestion / focus time. */ oldValue: T; /** Value submitted via commitSuggestion. */ newValue: T; /** Optional human-readable description. */ summary: string | null; /** Customer-defined metadata supplied via CommitSuggestionConfig.metadata. */ metadata: Record<string, any>; /** True iff the live value at approve time differed from oldValue. */ driftDetected: boolean; /** * User of the suggestion's creator (sourced from annotation.from). */ createdBy?: User; createdAt: number; } /** A suggestion in the 'pending' state — newly created, no owner action yet. */ export interface PendingSuggestion<T = unknown> extends SuggestionBase<T> { status: 'pending'; rejectReason: null; resolvedBy: null; resolvedAt: null; } /** A suggestion that has been approved (apply handler success or pending invocation). */ export interface ApprovedSuggestion<T = unknown> extends SuggestionBase<T> { status: 'accepted' | 'apply_failed'; rejectReason: null; resolvedBy: User; resolvedAt: number; } /** * A suggestion that has been rejected. `rejectReason` may be null when the * rejecter dismissed without supplying a reason — matches the persisted * `SuggestionData.rejectReason: string | null` shape. */ export interface RejectedSuggestion<T = unknown> extends SuggestionBase<T> { status: 'rejected'; rejectReason: string | null; resolvedBy: User; resolvedAt: number; } /** A suggestion whose target was unresolvable at approve time. */ export interface StaleSuggestion<T = unknown> extends SuggestionBase<T> { status: 'stale'; rejectReason: null; resolvedBy: User | null; resolvedAt: number | null; } /** * Public Suggestion — discriminated union keyed by `status`. TypeScript narrows * field types per status (e.g. `resolvedBy: User` is non-null on approved/rejected, * `rejectReason: string | null` on rejected since one-click reject without a reason * is supported). */ export type Suggestion<T = unknown> = PendingSuggestion<T> | ApprovedSuggestion<T> | RejectedSuggestion<T> | StaleSuggestion<T>; export interface CommitSuggestionConfig<T = unknown> { /** * Must be registered (via data-velt-target attribute or registerTarget call) * before commit, otherwise the suggestion is rejected with a dev-mode warning. */ targetId: string; /** Any JSON-serializable value. Customer's apply handler interprets it. */ newValue: T; /** Optional human-readable string for logs and notifications. */ summary?: string; /** Optional customer-defined metadata. Stored on Suggestion.metadata. */ metadata?: Record<string, unknown>; } /** * Pre-bound builder attached to `targetEditCommit` payloads. Calling it * commits the suggestion with the SDK's default summary/metadata, optionally * overridden by `result`. If the customer's `onTargetEditCommit` handler * already returned a non-null result for this edit, this builder is a no-op * (resolves immediately) so subscribers can't double-commit. */ export type TargetEditCommitBuilder = (result?: TargetEditCommitResult) => Promise<{ id: string; } | null>; export interface SuggestionGetSuggestionsFilter { targetId?: string; status?: SuggestionStatus | SuggestionStatus[]; } export {};