import { CanDuplicateTo } from "@playhtml/common";
import { CanMoveBounds } from "@playhtml/common";
import { CanMoveBoundsMinVisible } from "@playhtml/common";
import { CanMoveBoundsMinVisiblePx } from "@playhtml/common";
import { classMap } from 'lit-html/directives/class-map.js';
import { Cursor } from "@playhtml/common";
import { CursorEvents } from "@playhtml/common";
import { CursorEvents as CursorEvents_2 } from "@playhtml/common";
import { CursorPresence } from "@playhtml/common";
import { CursorPresenceView } from "@playhtml/common";
import { CursorPresenceView as CursorPresenceView_2 } from "@playhtml/common";
import { default as default_2 } from 'y-partyserver/provider';
import { ElementAwarenessEventHandlerData } from "@playhtml/common";
import { ElementData } from "@playhtml/common";
import { ElementEventHandlerData } from "@playhtml/common";
import { ElementInitializer } from "@playhtml/common";
import { ElementSetupData } from "@playhtml/common";
import { EventMessage } from "@playhtml/common";
import { getIdForElement } from "@playhtml/common";
import { html } from 'lit-html';
import { ModifierKey } from "@playhtml/common";
import { nothing } from 'lit-html';
import { PageDataChannel } from "@playhtml/common";
import { PlayerIdentity } from "@playhtml/common";
import { PlayerIdentity as PlayerIdentity_2 } from "@playhtml/common";
import { PlayEvent } from "@playhtml/common";
import { PresenceAPI } from "@playhtml/common";
import { PresenceRoom } from "@playhtml/common";
import { PresenceServerMessage } from "@playhtml/common";
import { PresenceView } from "@playhtml/common";
import { RegisteredPlayEvent } from "@playhtml/common";
import { repeat } from 'lit-html/directives/repeat.js';
import { styleMap } from 'lit-html/directives/style-map.js';
import { svg } from 'lit-html';
import { TagType } from "@playhtml/common";
import { TagTypeToElement } from "@playhtml/common";
import { ViewTemplate } from "@playhtml/common";
export { CanDuplicateTo }
export { CanMoveBounds }
export { CanMoveBoundsMinVisible }
export { CanMoveBoundsMinVisiblePx }
export { classMap }
/**
* Declare config for this playhtml instance without connecting. Idempotent and
* framework-agnostic: call it once, up front, from wherever owns the config (a
*
script, PlayProvider, an island). Connection happens later via init()
* / standalone component mount, which read whatever config was declared here.
*
* Use this when you can't put a single init() at the top of your app — Astro
* islands, multi-page apps, multiple React roots — so config doesn't depend on
* which "ensure running" call happens to run first. The first declaration wins;
* a later conflicting one warns and is ignored.
*/
declare function configurePlayHTML(options?: InitOptions): void;
declare function createPageData(name: string, defaultValue: T): PageDataChannel;
declare function createPresenceRoom(name: string): PresenceRoom;
export { Cursor }
declare class CursorClientAwareness {
private provider;
private options;
private presenceTransport?;
private cursors;
private cursorAnimators;
private spatialGrid;
private proximityUsers;
private currentCursor;
private playerIdentity;
private awarenessUpdateTimeout;
private lastUpdate;
private pointerFrame;
private pendingPointerSample;
private cursorEventCleanups;
private ownCursorSvgCache;
private visibilityThreshold;
private isStylesAdded;
private globalApiListeners;
private activeAnimationCleanups;
private allPlayerColors;
private chat;
private currentMessage;
private otherUsersWithMessages;
private lastSentMessage;
private cursorPresenceChangeCallbacks;
private coordinateMode;
private presenceStore;
private presenceTransportUnsubscribe;
private presenceExpiryInterval;
private serverCursorMaxHz;
private getContainerMatrix;
private clientToStorage;
private storageToClient;
private zones;
private currentZone;
private cursorZoneState;
private cursorStyleKeys;
private lastKnownContainer;
private clientIdToStableId;
private pendingRemovals;
constructor(provider: default_2, options?: CursorOptions, presenceTransport?: CursorPresenceTransport | undefined);
private initialize;
private setupAwarenessHandling;
private setupPresenceTransportHandling;
private handlePresenceTransportMessage;
private handlePresenceRate;
private startPresenceExpiryTimer;
private removeExpiredPresenceCursors;
private renderPresenceStore;
private refreshPresenceTransportColors;
private syncExistingAwareness;
private handleAwarenessChange;
private hasOtherClientForStableId;
private cursorPresenceEntries;
private activeCursorPresenceEntries;
private isFreshCursorPresence;
private rebuildSpatialGrid;
private checkProximityOptimized;
private addCursorStyles;
private createCursorPositionCallback;
private resolveTargetCoords;
private applyZoneStyling;
private hitTestZones;
private getOwnCursorSvg;
private addCursorEventListener;
private requestPointerFrame;
private cancelPointerFrame;
private queuePointerSample;
private processPointerSample;
private setupCursorTracking;
private repositionAllCursors;
private snapCursorToPresence;
private getActiveCursorConnectionCount;
private scheduleCursorAwarenessUpdate;
private updateCursorAwareness;
private publishPresenceTransportState;
private getContainer;
refreshContainer(): void;
refreshCursorStyles(): void;
private findAwarenessByStableId;
private updateCursor;
private createCursorElement;
private getMouseCursorSVG;
private getTouchCursorSVG;
private getCustomCursorSVG;
private removeCursor;
private savePlayerIdentityToStorage;
private setupGlobalAPI;
private emitGlobalEvent;
private updateGlobalColors;
private updateChatCTA;
private updateCursorMessage;
private updateCursorName;
private opacifyColor;
private hslToRgb;
private getLuminance;
private getContrastColor;
private updateAllCursorVisibility;
private showAllCursors;
configure(options: Partial): void;
private publishPlayerIdentity;
registerZone(element: HTMLElement, options?: CursorZoneOptions): void;
unregisterZone(elementId: string): void;
hideCursor(connectionId: string): void;
showCursor(connectionId: string): void;
destroy(): void;
getDebugInfo(): {
totalCursors: number;
gridCells: number;
avgCursorsPerCell: number;
};
on(event: K, callback: (value: CursorEvents_2[K]) => void): void;
off(event: K, callback: (value: CursorEvents_2[K]) => void): void;
getSnapshot(): CursorEvents_2;
getMyPlayerIdentity(): PlayerIdentity_2;
getProvider(): default_2;
getCursorPresences(): Map;
onCursorPresencesChange(callback: (presences: Map) => void): () => void;
private notifyCursorPresenceListeners;
/**
* Apply a CSS class to a specific cursor element identified by the player's stableId (publicKey).
* The class is added to the actual rendered cursor DOM element and removed after `durationMs`.
* Returns true if the cursor element was found and the animation was applied.
*/
triggerCursorAnimation(stableId: string, animationClass: string, durationMs?: number): boolean;
private triggerSelfCursorAnimation;
}
export declare type CursorContainer = HTMLElement | string | (() => HTMLElement | null);
export declare type CursorCoordinateMode = "relative" | "absolute";
export { CursorEvents }
export declare interface CursorOptions {
enabled?: boolean;
playerIdentity?: PlayerIdentity;
proximityThreshold?: number;
visibilityThreshold?: number;
cursorStyle?: string;
coordinateMode?: CursorCoordinateMode;
onProximityEntered?: (playerIdentity?: PlayerIdentity, positions?: {
ours: {
x: number;
y: number;
};
theirs: {
x: number;
y: number;
};
}, angle?: number) => void;
onProximityLeft?: (connectionId: string) => void;
onCustomCursorRender?: (connectionId: string, element: HTMLElement) => HTMLElement | null;
enableChat?: boolean;
room?: CursorRoom;
shouldRenderCursor?: (presence: CursorPresence) => boolean;
getCursorStyle?: (presence: CursorPresence) => Partial | Record;
/**
* Where to mount cursor DOM and the cursor style tag. Defaults to
* document.body / document.head. Pass a container you control (and mark
* with transition:persist or equivalent) to survive SPA body-swaps.
*
* If the container has its own CSS transform (e.g. a pannable canvas
* applies `transform: translate(...) scale(...)`), cursors are stored
* AND rendered in the container's local coordinate space. The library
* reads the live transform matrix from getComputedStyle, so two clients
* with different pan/zoom agree on a cursor's content position; the
* container's CSS transform then maps that position into each viewer's
* own pixels — anchoring cursors to content rather than to the viewport.
*/
container?: CursorContainer;
}
export { CursorPresence }
declare type CursorPresenceTransport = {
join(input: {
identity: PlayerIdentity_2;
page?: string;
}): void;
update(channel: string, value: unknown): void;
clear(channel: string): void;
subscribe(listener: (message: PresenceServerMessage) => void): () => void;
destroy(): void;
};
export { CursorPresenceView }
export declare type CursorRoom = "page" | "domain" | "section" | ((context: {
domain: string;
pathname: string;
search: string;
}) => string);
export declare interface CursorZoneOptions {
onCustomCursorRender?: (connectionId: string, element: HTMLElement) => HTMLElement | null;
getCursorStyle?: (presence: CursorPresence) => Partial | Record;
}
declare type DeepReadonlyStore = T extends (...args: any[]) => any ? T : T extends readonly (infer U)[] ? ReadonlyArray> : T extends object ? {
readonly [K in keyof T]: DeepReadonlyStore;
} : T;
declare interface DefaultRoomOptions {
includeSearch?: boolean;
}
/**
* Registers a reusable capability under an attribute name (e.g. "can-note").
* Every element carrying that attribute gets the capability — including ones
* added to the DOM later. The imperative counterpart of
* `init({ extraCapabilities })`.
*
* @param capabilityName - The attribute name elements use to opt in (used in an
* attribute selector, e.g. `[can-note]`).
* @experimental New view API; signature may change in a future minor release.
*/
declare function definePlayCapability(capabilityName: string, init: ElementInitializer): void;
/**
* Completely deletes all shared collaborative data for an element.
* This is a destructive operation that removes data across all clients.
* This includes:
* - SyncedStore data (store.play[tag][elementId])
* - Observer subscriptions
* - Element handlers
*
* Use this when you want to permanently delete an element's data.
* For just removing a DOM element while keeping data, use removePlayElement instead.
*
* @param tag - The capability tag (e.g., "can-move", "can-toggle")
* @param elementId - The element ID
*/
declare function deleteElementData(tag: string, elementId: string): void;
declare function dispatchPlayEvent(message: EventMessage): void;
export declare type EditableStateLeafValue = string | number | boolean | null;
export { ElementAwarenessEventHandlerData }
declare type ElementDataWrite = T | ((draft: T) => void);
declare class ElementHandler {
defaultData: T | undefined;
localData: U;
awareness: V[];
awarenessByStableId: Map;
selfAwareness?: V;
element: HTMLElement;
_data: T;
onChange: (data: T) => void;
onAwarenessChange: (data: V) => void;
debouncedOnChange: (data: T) => void;
resetShortcut?: ModifierKey;
updateElement?: (data: ElementEventHandlerData) => void;
view?: (data: ElementEventHandlerData) => ViewTemplate;
updateElementAwareness?: (data: ElementAwarenessEventHandlerData) => void;
triggerAwarenessUpdate?: () => void;
devMode?: boolean;
private isRendering;
private onUnmount?;
onAfterRender?: (element: HTMLElement) => void;
private descendantObserver?;
private dataUpdateListeners;
private scheduleSetupDataWrite?;
private clickListener?;
private touchStartListener?;
private mouseDownListener?;
private resetShortcutListener?;
private activeDragCleanup?;
onClick?: (e: MouseEvent, eventData: ElementEventHandlerData) => void;
onDrag?: (e: MouseEvent | TouchEvent, eventData: ElementEventHandlerData) => void;
onDragStart?: (e: MouseEvent | TouchEvent, eventData: ElementEventHandlerData) => void;
constructor(elementData: ElementData, options?: ElementHandlerOptions);
/**
* Tears down anything onMount set up (rAF loops, timers, listeners).
* Called by removePlayElement / unregister(). Idempotent.
*/
destroy(): void;
reinitializeElementData({ element, onChange, onAwarenessChange, updateElement, view, updateElementAwareness, onClick, onDrag, onDragStart, resetShortcut, debounceMs, triggerAwarenessUpdate, devMode, }: ElementData): void;
setEventHandlers({ onClick, onDrag, onDragStart, }: Pick, "onClick" | "onDrag" | "onDragStart">): void;
private removeActiveDragListeners;
get data(): T;
onDataUpdate(listener: () => void): () => void;
setLocalData(localData: U | ((draft: U) => void)): void;
/**
* // PRIVATE USE ONLY \\
*
* Updates the internal state with the given data and handles all the downstream effects. Should only be used by the sync code to ensure one-way
* reactivity.
* (e.g. calling `updateElement`/`view` and `onChange`)
*/
set __data(data: T);
/**
* Renders the element from current state: runs `view` and patches the
* result into the DOM via lit-html, or falls back to imperative
* `updateElement`. Safe to call repeatedly — lit-html diffs.
*/
render(): void;
/**
* Begins binding capability descendants emitted by this view (mount points
* for `define`d capabilities / `register`ed ids). Binds the current children
* once, then re-binds only when the subtree's child structure changes — so a
* text/attribute-only re-render (a ticking timer) does no scanning at all.
* Called once by the runtime after `onAfterRender` is wired.
*/
observeDescendants(): void;
/**
* Re-runs the view and repaints from current state. No-op for elements
* without a `view` (it's the view-repaint primitive), and a no-op during an
* in-flight render (calling it from inside `view` would recurse).
*/
requestUpdate(): void;
/** Warns and returns true if a write was attempted during a view render. */
private rejectWriteDuringRender;
updateAwareness(data: V[], byStableId: Map): void;
getEventHandlerData(): ElementEventHandlerData;
getAwarenessEventHandlerData(): ElementAwarenessEventHandlerData;
getSetupData(): ElementSetupData;
private setSetupData;
/**
* Public setter for element data.
*
* Semantics:
* - Mutator form: setData((draft) => { ... })
* When data is backed by SyncedStore/Yjs (dataMode = "syncedstore"),
* the draft is a live CRDT proxy. You can mutate nested arrays/objects
* and the change will be merged across clients without conflicts.
* Example:
* setData(d => { d.list.push(item); });
*
* - Value form: setData(value)
* Replaces the entire data snapshot. Use this when you need canonical
* replacement semantics (e.g., snapshot from a mirror) or when running
* in legacy plain mode. Example:
* setData({ on: true });
*
* Notes:
* - In plain mode, only the value form results in a sync; mutating draft
* is a no-op. Prefer the mutator form for merge-friendly edits.
* - Directly mutating eventData.data may work in SyncedStore mode, but the
* recommended portable pattern is setData(draft => { ... }).
*/
setData(data: ElementDataWrite): void;
setMyAwareness(data: V): void;
setDataDebounced(data: T): void;
/**
* Resets the element to its default state.
*/
reset(): void;
}
declare interface ElementHandlerOptions {
scheduleSetupDataWrite?: (write: () => void) => void;
}
export { ElementInitializer }
export declare function formatStateLeafValue(value: EditableStateLeafValue): string;
export { getIdForElement }
export { html }
export declare interface InitOptions {
/**
* The room to connect users to (this should be a string that matches the other users
* that you want a given user to connect with).
*
* All rooms are automatically prefixed with their host (`window.location.hostname`) to prevent
* conflicting with other people's sites.
* Defaults to `window.location.pathname + window.location.search. You can customize this by
* passing in your own room dynamically.
*
* Pass a function to make the room recompute on SPA navigation: it is called
* at init and again on each route change, so a path-derived room follows the
* URL the same way the default room does. A static string stays fixed for the
* page's lifetime.
*/
room?: string | (() => string);
/**
* Provide your own partykit host if you'd like to run your own server and customize the logic.
*/
host?: string;
/**
* Optionally provide your own map of capabilities
*/
extraCapabilities?: Record;
/**
* A mapping of event types to PlayEvents. Allows specifying of imperative logic to trigger when a
* client triggers some event. Automatically listens to native DOM events to trigger these.
*
*/
events?: Record>;
/**
* configuration for the default room which is based on the window's url
*/
defaultRoomOptions?: DefaultRoomOptions;
/**
* Runs if playhtml fails to connect. Useful to show error messages and debugging.
*/
onError?: () => void;
/**
* If true, will render some helpful development UI.
*/
developmentMode?: boolean;
/**
* Cursor tracking and proximity detection configuration
*/
cursors?: CursorOptions;
}
declare function initPlayHTML(options?: InitOptions): Promise | Promise;
export declare function isEditableStateLeaf(value: unknown): value is EditableStateLeafValue;
export { nothing }
export { PageDataChannel }
export declare function parseStateLeafValue(input: string): StateLeafParseResult;
/**
* Handle returned by `playhtml.register`, and by `playhtml.getHandle` for any
* bound element. Reads/writes resolve the live handler lazily, so a handle
* obtained before the element binds still works once it does.
*
* @experimental Part of the new view API; subject to change in a future minor.
*/
export declare interface PlayElementHandle {
id: string;
/** The bound DOM element, or null until it exists and binds. */
getElement(): HTMLElement | null;
/** Current shared data. Do not mutate it directly — mutations are not synced and may corrupt state; write via setData. */
getData(): T | undefined;
setData(next: T | ((draft: T) => void)): void;
setLocalData(next: U | ((draft: U) => void)): void;
setMyAwareness(next: V): void;
/** Re-run the view now (for clock-driven views). No-op without a view. */
requestUpdate(): void;
/** Detach the handler (shared data is preserved) and drop the registration. */
unregister(): void;
}
export { PlayerIdentity }
export declare const playhtml: PlayHTMLComponents;
export declare interface PlayHTMLComponents {
init: typeof initPlayHTML;
configure: typeof configurePlayHTML;
readonly isLoading: boolean;
readonly ready: Promise;
handleNavigation: () => Promise;
setupPlayElements: typeof setupElements;
setupPlayElement: typeof setupPlayElement;
removePlayElement: typeof removePlayElement;
deleteElementData: typeof deleteElementData;
setupPlayElementForTag: typeof setupPlayElementForTag;
/** @experimental View API — register a custom element by id. */
register: typeof registerPlayElement;
/** @experimental View API — register a reusable capability by attribute name. */
define: typeof definePlayCapability;
/** @experimental View API — get a handle for a bound element. */
getHandle: (elementId: string, tag?: string) => PlayElementHandle;
syncedStore: ReadOnlyStore;
elementHandlers: Map>;
eventHandlers: Map>;
dispatchPlayEvent: typeof dispatchPlayEvent;
registerPlayEventListener: typeof registerPlayEventListener;
removePlayEventListener: typeof removePlayEventListener;
cursorClient: CursorClientAwareness | null;
presence: PresenceAPI;
createPageData: typeof createPageData;
createPresenceRoom: typeof createPresenceRoom;
roomId: string;
host: string;
listSharedElements: () => Array<{
type: "source" | "consumer";
elementId: string;
dataSource: string;
normalized: string;
permissions?: "read-only" | "read-write";
element: HTMLElement;
}>;
}
declare type PlayStore = {
readonly play: Partial>>;
};
export { PresenceRoom }
export { PresenceView }
declare type ReadOnlyStore = DeepReadonlyStore;
/**
* Registers a `view`/`updateElement` initializer for a single element by id.
* Callable before or after `init()` and before or after the element exists;
* binding happens once both are present. Returns a handle for reads/writes
* from outside the view (e.g. form submit handlers).
*
* @experimental New view API; signature may change in a future minor release.
*/
declare function registerPlayElement(elementId: string, init: ElementInitializer): PlayElementHandle;
/**
* Registers the given event listener.
* Returns a unique ID corresponding to the listener.
*/
declare function registerPlayEventListener(type: string, event: Omit): string;
/**
* Removes the element handler for a DOM element from local state.
* This unregisters the element but preserves all shared collaborative data.
* Use this when a DOM element is removed/unmounted but you want to keep the data.
*
* @param element - The DOM element to unregister
*/
declare function removePlayElement(element: Element | null): void;
/**
* Removes the event listener with the given type and id.
*/
declare function removePlayEventListener(type: string, id: string): void;
export { repeat }
export declare function replaceStateLeafValue(data: T, path: StatePathSegment[], value: EditableStateLeafValue): StateLeafReplaceResult;
/**
* Full teardown of all playhtml state. Not part of the public API —
* the singleton design means no single caller can safely own teardown
* without reference counting. Exported as a named function for test
* isolation (beforeEach resets) only.
*/
export declare function resetPlayHTML(): Promise;
/**
* Sets up any playhtml elements that are currently on the page.
*
* Should be called only once. If you'd like to set up new elements, use `setupPlayElement`, which is exposed
* on the `playhtml` object on `window`.
*/
declare function setupElements(): void;
declare function setupPlayElement(element: Element, { ignoreIfAlreadySetup }?: {
ignoreIfAlreadySetup?: boolean;
}): void;
/**
* Sets up a playhtml element to handle the given tag's capabilities.
*/
declare function setupPlayElementForTag(element: HTMLElement, tag: T): Promise;
export declare const sharedPermissions: Map;
declare type StateLeafParseResult = {
ok: true;
value: EditableStateLeafValue;
} | {
ok: false;
error: string;
};
declare type StateLeafReplaceResult = {
ok: true;
data: T;
} | {
ok: false;
error: string;
};
export declare type StatePathSegment = string | number;
export { styleMap }
export { svg }
export { TagType }
export { TagTypeToElement }
export { }