@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
TypeScript
/**
* 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 {};