From 0892db099d9d80db1480b11ceab6c3c58f5694f8 Mon Sep 17 00:00:00 2001 From: Spencer Chang Date: Mon, 29 Jun 2026 18:07:57 -0700 Subject: [PATCH 1/2] docs: clarify presence and awareness --- apps/docs/src/content/docs/capabilities.mdx | 4 +- apps/docs/src/content/docs/concepts.md | 2 +- .../src/content/docs/data/data-essentials.md | 4 +- .../content/docs/data/presence/cursors.mdx | 2 +- .../src/content/docs/data/presence/index.mdx | 47 ++++++++++++++++++- .../docs/integrations/building-with-ai.md | 11 +++-- .../src/content/docs/reference/react-api.md | 9 ++-- apps/docs/src/content/docs/using-react.md | 2 +- 8 files changed, 64 insertions(+), 17 deletions(-) diff --git a/apps/docs/src/content/docs/capabilities.mdx b/apps/docs/src/content/docs/capabilities.mdx index fbb79b62..c7a2ebc8 100644 --- a/apps/docs/src/content/docs/capabilities.mdx +++ b/apps/docs/src/content/docs/capabilities.mdx @@ -302,7 +302,7 @@ import { CanHoverElement } from "@playhtml/react"; -If you want the hover effect to reflect *who* is hovering (e.g. tint the element with each viewer's cursor color) rather than a plain on/off, read the hover roster off awareness with a custom `can-play` element: +If you want the hover effect to reflect *who* is hovering (e.g. tint the element with each viewer's cursor color) rather than a plain on/off, read the hover roster off element awareness. Awareness is presence scoped to one element, so it clears when readers leave and does not persist: ```tsx import { CanPlayElement } from "@playhtml/react"; @@ -572,7 +572,7 @@ These capabilities are the building blocks. The best way to see them in concert EXPERIMENT · 04 Every color - A page where every visitor adds one color. can-play + per-user awareness + an ever-growing shared palette. + A page where every visitor adds one color. can-play + element awareness + an ever-growing shared palette. COMMUNITY · FRIDGE diff --git a/apps/docs/src/content/docs/concepts.md b/apps/docs/src/content/docs/concepts.md index ee031318..2171bdfc 100644 --- a/apps/docs/src/content/docs/concepts.md +++ b/apps/docs/src/content/docs/concepts.md @@ -13,7 +13,7 @@ Once you can reach for the right one, everything else is just attribute names. - **Element data** (`defaultData` / [`can-play`](/docs/capabilities/)): persistent state scoped to a single DOM element. A toggle's on/off, a draggable's position, a shared count. Survives reload. See [data essentials](/docs/data/data-essentials/) for shape, updates, and cleanup. - **Page data** (`playhtml.createPageData`): persistent state keyed by a name, not tied to any element. A page-level counter, a shared prompt, an open vote. See [page-level data](/docs/data/page-data/). -- **Presence** (`playhtml.presence` / cursor awareness): ephemeral per-user state: "who's online", "who's typing", "where's my cursor". Disappears when users disconnect. See [presence](/docs/data/presence/) and [cursors](/docs/data/presence/cursors/). +- **Presence** (`playhtml.presence`, cursors, element awareness): ephemeral per-user state: "who's online", "who's typing", "where's my cursor". Element APIs call this same kind of state _awareness_ when it is scoped to one element. Disappears when users disconnect. See [presence](/docs/data/presence/) and [cursors](/docs/data/presence/cursors/). - **Events** (`playhtml.dispatchPlayEvent`): one-off broadcasts with no persisted state. Confetti, chimes, notifications. See [events](/docs/data/events/). Not sure which one you want? The [decision table on data essentials](/docs/data/data-essentials/#when-to-use-which-primitive) lays out the tradeoffs side by side. diff --git a/apps/docs/src/content/docs/data/data-essentials.md b/apps/docs/src/content/docs/data/data-essentials.md index 8fd2e4d5..ea2cd04c 100644 --- a/apps/docs/src/content/docs/data/data-essentials.md +++ b/apps/docs/src/content/docs/data/data-essentials.md @@ -18,7 +18,7 @@ playhtml has four primitives for moving state between readers. Pick by **lifetim | A toggle, position, count, or any state tied to one element | [Element data](/docs/data/data-essentials/) (`defaultData` / `can-play`) | Yes | One element | | A page-wide counter, prompt, or vote not tied to a DOM node | [Page data](/docs/data/page-data/) (`playhtml.createPageData`) | Yes | One page | | "Who is connected right now?" / "How many readers?" | [Presence](/docs/data/presence/) (`playhtml.presence.getPresences()`) | No | Per-user | -| "Who's typing in this input?" / live status | [Custom presence channel](/docs/data/presence/#custom-channels) | No | Per-user | +| "Who's typing in this input?" / live status | [Custom presence channel](/docs/data/presence/#custom-channels) or element awareness | No | Per-user | | "Where is everyone's cursor?" | [Cursors](/docs/data/presence/cursors/) | No | Per-user | | Confetti burst, chime, notification | [Events](/docs/data/events/) (`dispatchPlayEvent`) | No (fires once) | Broadcast | | "How many people reacted to this post?" | Element data (a `count` field) | Yes | One element | @@ -135,7 +135,7 @@ playhtml gives you three places to put state. Use the one that matches the lifet | Type | Survives reload | Use for | |---|---|---| | Persistent (`defaultData`) | Yes | Positions, counts, messages, settings, toggles | -| Presence / awareness | No | Who's online, typing indicators, colors, per-user cursor data | +| Presence, including element awareness | No | Who's online, typing indicators, colors, per-user cursor data | | Events | No (fire once) | Confetti bursts, notifications, chimes | If someone refreshes the page and expects the state to still be there, it's persistent data. If a new reader opening the page for the first time should _not_ see a historical replay, it's presence or an event. diff --git a/apps/docs/src/content/docs/data/presence/cursors.mdx b/apps/docs/src/content/docs/data/presence/cursors.mdx index 45c076ff..6db0ce61 100644 --- a/apps/docs/src/content/docs/data/presence/cursors.mdx +++ b/apps/docs/src/content/docs/data/presence/cursors.mdx @@ -453,7 +453,7 @@ playhtml.init({ /> ``` -### Section-Specific Awareness +### Section-Specific Presence Show cursors only to users in the same section of your site (e.g., all `/blog/*` pages): diff --git a/apps/docs/src/content/docs/data/presence/index.mdx b/apps/docs/src/content/docs/data/presence/index.mdx index 9e1bd492..0fb2ffa1 100644 --- a/apps/docs/src/content/docs/data/presence/index.mdx +++ b/apps/docs/src/content/docs/data/presence/index.mdx @@ -12,6 +12,8 @@ Presence is playhtml's "who's here right now" layer. Every connected user has an Reach for presence when the lifetime you want is "while this person is on the page". Reach for [persistent data](/docs/data/data-essentials/) when you want state to survive a reload, and [events](/docs/data/events/) for one-shot signals. +Element APIs use the word **awareness** for presence scoped to one element. `myDefaultAwareness`, `awareness`, and `setMyAwareness` are still ephemeral per-user state; they just belong to a specific element instead of the page-level `playhtml.presence` object. + ## The unified API You get one view of everyone connected, with both system fields (identity, cursor) and any custom channels you add. In vanilla JS that's the `playhtml.presence` object; in React it's the [`usePresence`](/docs/reference/react-api/#usepresence) hook. @@ -113,6 +115,47 @@ setMyPresence(null); There's no partial/merge update for a channel. When you call `setMyPresence`, you overwrite that channel's value for your user. +## Element awareness + +Use page-level presence when the signal belongs to the room: online status, cursor position, lobby readiness, or a typing indicator that several parts of the page may read. Use element awareness when the signal only matters for one playhtml element, like "who has joined this widget" or "which color is this reader contributing here". + +In vanilla `can-play`, element awareness appears on the element handler data: + +```html + + + +``` + +In React, the same fields are available from `withSharedState` and `` render props: + +```tsx + + {({ awareness, setMyAwareness }) => { + const label = awareness.length === 1 ? "reader" : "readers"; + return ( + + ); + }} + +``` + +Awareness has the same lifetime as presence: it clears when the user leaves, and a late visitor does not replay it. + ## Isolated presence rooms The main presence layer tracks everyone in the page's room. When you want a presence channel **scoped to something other than the page** (a lobby, a document, a game table that several pages share), create a separate presence room. @@ -159,6 +202,6 @@ Each dot is one reader; the yellow-glowing dot is **you**. Pick a color and watc ## When to use data instead -Presence is for _ambient awareness_ of other people on the page. If you find yourself reaching for `localStorage` or a refresh-survivor, you want data, not presence. +Presence is for the live sense of other people on the page. If you find yourself reaching for `localStorage` or a refresh-survivor, you want data, not presence. -Not sure which primitive fits? The full decision table, covering element data, page data, presence, cursors, and events, is on [data essentials](/docs/data/data-essentials/#when-to-use-which-primitive). +Not sure which primitive fits? The full decision table, covering element data, page data, presence, element awareness, cursors, and events, is on [data essentials](/docs/data/data-essentials/#when-to-use-which-primitive). diff --git a/apps/docs/src/content/docs/integrations/building-with-ai.md b/apps/docs/src/content/docs/integrations/building-with-ai.md index 419c6337..1232b944 100644 --- a/apps/docs/src/content/docs/integrations/building-with-ai.md +++ b/apps/docs/src/content/docs/integrations/building-with-ai.md @@ -81,8 +81,9 @@ SETUP — React: DATA TYPES (choose the right one): 1. Persistent data (defaultData): State that syncs and persists (position, count, messages, etc.) -2. Awareness: Temporary presence data (which users are online, their colors, cursor positions) -3. Events: One-time triggers (confetti, notifications, animations) — use dispatchPlayEvent/registerPlayEventListener +2. Presence: Temporary per-user state (which users are online, their colors, cursor positions) +3. Element awareness: Presence scoped to one element (who is hovering this card, this user's color in this widget) +4. Events: One-time triggers (confetti, notifications, animations) — use dispatchPlayEvent/registerPlayEventListener KEY APIs: @@ -96,7 +97,7 @@ Vanilla HTML (can-play): React (withSharedState): - withSharedState({ defaultData: {...} }, ({ data, setData, ref }) => JSX) -- For awareness: { myDefaultAwareness: value } in config, use setMyAwareness +- For element awareness: { myDefaultAwareness: value } in config, use setMyAwareness - For events: usePlayContext() → { registerPlayEventListener, dispatchPlayEvent } - For cursors in React: usePlayContext() → { cursors, configureCursors, getMyPlayerIdentity } @@ -141,7 +142,7 @@ DATA PERFORMANCE TIPS: - Keep data shapes simple and flat (avoid deep nesting) - Don't store computed/derived values — calculate them in render/updateElement - Use events for ephemeral actions (confetti, notifications), not persistent data -- Use awareness for temporary presence, not defaultData +- Use presence or element awareness for temporary per-user state, not defaultData - Don't update data on high-frequency events (mousemove, scroll) — debounce - For growing lists (messages, history), consider limiting size or implementing cleanup - Store only what needs to sync — use component state for UI-only state @@ -149,7 +150,7 @@ DATA PERFORMANCE TIPS: INSTRUCTIONS: - If the behavior description is unclear, ASK clarifying questions before implementing -- Choose the right data type (persistent vs awareness vs events) +- Choose the right data type (persistent data, presence or element awareness, or events) - Provide complete, working code - Include all necessary imports and setup diff --git a/apps/docs/src/content/docs/reference/react-api.md b/apps/docs/src/content/docs/reference/react-api.md index 0e5a0ab9..cd145e59 100644 --- a/apps/docs/src/content/docs/reference/react-api.md +++ b/apps/docs/src/content/docs/reference/react-api.md @@ -59,7 +59,7 @@ interface WithSharedStateConfig { ``` - **`defaultData`**: required. The initial value of `data`. Survives reload. -- **`myDefaultAwareness`**: optional. Initial value for this user's ephemeral per-user field. Does _not_ persist. +- **`myDefaultAwareness`**: optional. Initial value for this user's element awareness. This is ephemeral per-user presence scoped to the element. Does _not_ persist. - **`id`**: optional. Stable id for the element. If omitted, playhtml derives one from the rendered DOM; see [Dynamic elements](/docs/advanced/dynamic-elements/) for why stable ids matter. - **`tagInfo`**: optional. Marks the element as one of the built-in capabilities (e.g. `[TagType.CanToggle]`). See [Capabilities](/docs/capabilities/). @@ -78,6 +78,8 @@ interface ReactElementEventHandlerData { `setData` accepts either a replacement value or a mutator function. See [Data essentials](/docs/data/data-essentials/) for the merge semantics. +`awareness`, `myAwareness`, and `setMyAwareness` are the element-scoped form of [presence](/docs/data/presence/#element-awareness). Use them for live per-user signals tied to this element, not state that should survive reload. + ### Props-dependent config Pass a callback instead of a config object when `defaultData` needs to derive from props: @@ -109,6 +111,7 @@ interface CanPlayElementProps { ``` - **`id`**: required if the top-level child is a React Fragment. Otherwise defaults to the child's id, or a hash of the child's content. A stable id matters for cross-browser sync; see [Dynamic elements](/docs/advanced/dynamic-elements/). +- **`myDefaultAwareness`**: optional. Initial element awareness for this user. Same lifetime as presence; it clears when the user leaves. - **`standalone`**: when `true`, the element initializes playhtml itself if no `PlayProvider` is present. Use it for one-off components mounted outside your provider tree (e.g. an Astro island). A no-op when a provider already exists. - **`loading`**: controls the loading affordance shown before the element's first sync. See [Loading options](#loading-options). - **`dataSource`**, **`shared`**, **`dataSourceReadOnly`**: wire the element to a shared source across pages or sites. See [Shared data props](#shared-data-props) and the [Shared elements](/docs/advanced/shared-elements/) guide. @@ -360,7 +363,7 @@ setCounter((draft) => { draft.count += 1; }); ### `usePresenceRoom` -Join an isolated [presence room](/docs/data/presence/) with its own awareness, separate from the page's main presence. Returns `null` until synced (and briefly during a room change). +Join an isolated [presence room](/docs/data/presence/) separate from the page's main presence. Returns `null` until synced (and briefly during a room change). ```tsx function usePresenceRoom(name: string): PresenceRoom | null; @@ -427,6 +430,6 @@ The repo has a collection of runnable React examples at [`packages/react/example A few things still in flux in the React package: -- **Per-key persistence config.** Currently persistence is a whole-store choice: `setMyAwareness` for ephemeral, `setData` for persistent, no local-only mode. A future `persistenceOptions` object might let you configure per-key (`none` / `local` / `global`). +- **Per-key persistence config.** Currently persistence is a whole-store choice: `setMyAwareness` for element-scoped presence, `setData` for persistent data, no local-only mode. A future `persistenceOptions` object might let you configure per-key (`none` / `local` / `global`). - **`awareness` splitting.** `awareness` currently includes the local user; it may split into `myAwareness` + `othersAwareness` for clarity. - **Hook ergonomics.** A pure-hook interface (`useSharedState({ id, defaultData })`) is being evaluated as an alternative to the HOC form. The blocker is that hooks have no natural place to pin a stable `id`. diff --git a/apps/docs/src/content/docs/using-react.md b/apps/docs/src/content/docs/using-react.md index 60eb9588..86858de8 100644 --- a/apps/docs/src/content/docs/using-react.md +++ b/apps/docs/src/content/docs/using-react.md @@ -81,7 +81,7 @@ export const ReactionView = withSharedState( Use the mutator form for counters and other `+/-` updates so each interaction edits the draft value at write time. -Add `myDefaultAwareness` to the config to get presence-style ephemeral per-user data alongside your persistent data. +Add `myDefaultAwareness` to the config to get element awareness: ephemeral per-user presence scoped to this element, alongside its persistent data. ### ``: for when you need JSX children, not a wrapper From e86d3daacb79e4fd5ba87ce835fb8c4c38692f3a Mon Sep 17 00:00:00 2001 From: Spencer Chang Date: Tue, 30 Jun 2026 10:18:31 +0800 Subject: [PATCH 2/2] Revise presence documentation for clarity Clarify the explanation of presence and its usage in real-time scenarios. Update examples and descriptions for better understanding. --- apps/docs/src/content/docs/data/presence/index.mdx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/docs/src/content/docs/data/presence/index.mdx b/apps/docs/src/content/docs/data/presence/index.mdx index 0fb2ffa1..8a5065ba 100644 --- a/apps/docs/src/content/docs/data/presence/index.mdx +++ b/apps/docs/src/content/docs/data/presence/index.mdx @@ -8,9 +8,9 @@ sidebar: import { Tabs, TabItem } from '@astrojs/starlight/components'; import { OnlineIndicatorDemo } from "@/components/react/data-demos/OnlineIndicatorDemo"; -Presence is playhtml's "who's here right now" layer. Every connected user has an identity, an optional cursor position, and any number of custom named channels you define. None of it persists. When the user disconnects, their presence clears. +Presence is used for anything that is real-time and ephemeral (meaning it doesn't stick around after you leave). For example, cursors only matter when you are actually on the page. When you leave, they are gone and there's no record of them. -Reach for presence when the lifetime you want is "while this person is on the page". Reach for [persistent data](/docs/data/data-essentials/) when you want state to survive a reload, and [events](/docs/data/events/) for one-shot signals. +When you want things to stay after refreshing, use [persistent data](/docs/data/data-essentials/) and use [events](/docs/data/events/) for real-time events that don't rely on updating data. Element APIs use the word **awareness** for presence scoped to one element. `myDefaultAwareness`, `awareness`, and `setMyAwareness` are still ephemeral per-user state; they just belong to a specific element instead of the page-level `playhtml.presence` object. @@ -65,9 +65,7 @@ The hook subscribes and unsubscribes with the component lifecycle. Your channel ### Cursor presence subscribes the same way -Cursor movements are exposed as a special channel. Playhtml sends cursor motion through -its realtime presence layer so cursor rendering can stay responsive without writing -pointer movement into persistent shared data: +Cursor movements are included as a special example of this presence. ```js const unsub = playhtml.presence.onPresenceChange("cursor", (presences) => {