Files

207 lines
10 KiB
TypeScript

/** What a field can hold. Store dates as ISO strings or numbers. */
export type JsonValue = string | number | boolean | null | JsonValue[] | JsonObject;
export interface JsonObject {
[field: string]: JsonValue;
}
/** A document's fields plus its `id`, the last path segment; read-only. */
export interface DocData extends JsonObject {
id: string;
}
export type FieldValue = {
"#delete": true;
} | {
"#text": string;
} | {
"#opaque": {
y: "xml" | "map";
of: string;
};
} | {
"#increment": number;
} | {
"#union": JsonValue[];
} | {
"#remove": JsonValue[];
};
/**
* The fields to write, each key on its own: a dotted key like `"votes.u1"`
* sets one nested value, a plain object replaces the whole nested object, a
* marker (`deleteField()`, `sharedText()`, `increment()`, `arrayUnion()`,
* `arrayRemove()` or another kit's) creates or changes that kind of field,
* and `undefined` is skipped. `id` is ignored, so an item can be spread back
* in; only the fields the spread changed are written.
*/
export interface Fields {
[field: string]: JsonValue | FieldValue | undefined;
}
/** Removes a field: `update({ [`votes.${uid}`]: deleteField() })`. */
export declare function deleteField(): FieldValue;
/** Creates a text everyone can type in together: `add({ body: sharedText() })`. Edit it with `SharedTextarea` or `SharedInput`; the field reads and writes as a string. */
export declare function sharedText(initial?: string): FieldValue;
/** Adds to a number without losing anyone else's additions: `update({ likes: increment(1) })`; negative subtracts. The field reads as a number, and writing a plain number to it adjusts it by the difference. */
export declare function increment(by: number): FieldValue;
/** Adds values to an array, each once, without losing anyone else's: `update({ tags: arrayUnion("bug") })`, or `arrayUnion(...tags)` for several. The field reads as an array in the order values were added, and writing a plain array to it adds and removes the difference. */
export declare function arrayUnion(...values: JsonValue[]): FieldValue;
/** Removes values from an array: `update({ tags: arrayRemove("bug") })`. */
export declare function arrayRemove(...values: JsonValue[]): FieldValue;
export interface SharedDoc {
/** False until the document is created with `set`; `update` throws before that. */
exists: boolean;
data: DocData;
/** Creates or replaces the whole document. */
set(fields: Fields): void;
/** Merges into the document; throws if it does not exist. */
update(fields: Fields | ((current: DocData) => Fields)): void;
/** Deletes the document and its subcollections; nothing happens if it does not exist. */
delete(): void;
}
/**
* One document at a path like `game/state` or `cards/k3f9`; `defaults` fill
* in missing top-level fields and are never written. A null path gives empty
* data and methods that do nothing; suspends until the shared state has
* loaded.
*/
export declare function useSharedDoc(path: string | null | undefined, defaults?: Fields): SharedDoc;
export interface SharedCollection {
/** The child documents, in order, as of this render; `shared.collection(path).items` is always current. */
items: readonly DocData[];
/** Adds a document at the end and returns its id. Pass `id` in the fields to choose it. */
add(fields: Fields): string;
/** Creates or replaces the document with this `id`; returns the id. */
set(id: string, fields: Fields): string;
/** Merges into the item; throws if the id is unknown. */
update(id: string, fields: Fields | ((current: DocData) => Fields)): void;
/** Deletes the item and its subcollections; nothing happens if the id is unknown. */
delete(id: string): void;
/** Moves an item to an index, or before or after another item; `items` returns the new order. Throws if either id is unknown. */
move(id: string, to: number | {
before: string;
} | {
after: string;
}): void;
}
/**
* The documents at a collection path like `cards` or `cards/k3f9/steps`, in
* order, and the methods to change them. A null path gives no items and
* methods that do nothing; suspends until the shared state has loaded.
*/
export declare function useSharedCollection(path: string | null | undefined): SharedCollection;
/** One person viewing the artifact right now: who they are and their presence. */
export interface Collaborator {
user: User;
/** Their presence; `{}` until they set one. */
presence: JsonObject;
}
/**
* This viewer: `user` is `clickup.viewer`, `presence` is what everyone else
* sees of them live, `{}` until set, never saved.
*/
export declare function useMe(): Collaborator & {
/** Sets what everyone else sees of you, like a `useState` setter: pass a new value or a function of the current one. Spread the current one to keep the keys other components write: `cursor`, `message` and `selection` from `Cursors`, `slide` from a slides `Deck`. */
setPresence(presence: JsonObject | ((current: JsonObject) => JsonObject)): void;
};
/**
* Everyone else viewing the artifact right now, one entry per person. With
* `select`, only what it returns, re-rendering when that changes:
* `useCollaborators((all) => all.filter((c) => c.presence.editing === id))`.
*/
export declare function useCollaborators<T = readonly Collaborator[]>(select?: (collaborators: readonly Collaborator[]) => T): T;
/**
* Runs `listener` for each `name` broadcast another person sends while
* mounted, with the payload and who sent it:
* `useBroadcast("buzz", (payload, from) => flash(from.name))`. Broadcasts
* are for what happens, documents for what must survive; a listener runs on
* every other page, so a write in it lands once per viewer.
*/
export declare function useBroadcast(name: string, listener: (payload: JsonValue, from: User) => void): void;
/**
* Undo and redo this viewer's own changes; all writes from one event handler
* are one step. While `canEdit` is false, both do nothing and both flags are
* false.
*/
export declare function useUndo(): {
undo(): void;
redo(): void;
canUndo: boolean;
canRedo: boolean;
};
export interface SharedStatus {
/** `connecting` until the shared state has loaded, and again while reconnecting; `disconnected` on a host without collaboration; otherwise the host's connection state. */
connection: "connecting" | "connected" | "disconnected";
/** False while loading, in read-only rooms and in outdated builds. Writes are then ignored. */
canEdit: boolean;
/** True when a newer build of this artifact is live. Ask the viewer to reload. */
outdated: boolean;
}
/** Whether the shared state is connected, whether you can edit it, and whether a newer build is live. */
export declare function useSharedStatus(): SharedStatus;
/**
* Shared state outside React: the same documents, collections, presence,
* broadcasts, undo and status as the hooks, always current. Use it in a draw
* loop or in an event listener set up outside a component; in render, use
* the hooks.
*/
export declare const shared: {
/** The document at `path`, as `useSharedDoc` gives it; `data` and `exists` read the current state. */
doc(path: string | null | undefined, defaults?: Fields): SharedDoc;
/** The collection at `path`, as `useSharedCollection` gives it; `items` reads the current state. */
collection(path: string | null | undefined): SharedCollection;
/** This viewer, as `useMe` gives them. */
readonly me: Collaborator & {
setPresence(presence: JsonObject | ((current: JsonObject) => JsonObject)): void;
};
readonly collaborators: readonly Collaborator[];
/** Sends `name` with `payload` to everyone else viewing the artifact right now; never saved, never replayed to anyone who arrives later. */
broadcast(name: string, payload?: JsonValue): void;
/** Calls `listener` with each `name` broadcast another person sends, as `useBroadcast` does. Returns a function that stops the calls. */
onBroadcast(name: string, listener: (payload: JsonValue, from: User) => void): () => void;
readonly status: SharedStatus;
undo(): void;
redo(): void;
readonly canUndo: boolean;
readonly canRedo: boolean;
/** Calls `listener` after every change to documents, presence, status or history. Returns a function that stops the calls. */
subscribe(listener: () => void): () => void;
/** Resolves once the shared state has loaded; until then writes are ignored. */
ready(): Promise<void>;
};
/** A person, the same shape as the ClickUp kit's `User`. */
export interface User {
kind: "user";
id: string;
name: string;
/** Link to the person's profile; empty when the host does not name one. */
url: string;
/** Avatar URL. */
image?: string;
}
/** A stored value of mine outside React: `get()`, `set()` and `remove()`. */
export interface MyState<T> {
/** The stored value, or `initial` while nothing is stored. */
get(): T;
/** Replaces the value; a function receives the current one, like a `useState` setter. */
set(next: T | ((current: T) => T)): void;
/** Deletes the key; `get()` returns `initial` again. */
remove(): void;
}
/**
* A value of mine that survives a reload, `useState` with a key:
* `const [tab, setTab] = useMyState("tab", "board")`, stored per viewer per
* artifact as JSON, up to 4,000 characters per key (`set` throws past that).
* Suspends until the stored value has loaded, so the first render never shows
* `initial` over a stored value; a key mounted later needs its own
* `<Suspense>`.
*/
export declare function useMyState<T>(key: string, initial: T): [T, (next: T | ((current: T) => T)) => void];
/**
* The same value outside React, always current: `myState("tab", "board").get()`
* in a draw loop or event listener, `.set(v)` to write it, `.remove()` to
* delete the key. Starts loading the value when called.
*/
export declare function myState<T>(key: string, initial: T): MyState<T>;
export declare function myState<T>(key: string): MyState<T | undefined>;
/** A value the kit keeps for this viewer under a `#` key, which authors are refused; resolves once the stored value has loaded. */
declare function kitState<T>(key: `#${string}`, initial: T): Promise<MyState<T>>;