Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/docs/src/content/docs/capabilities.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ import { CanHoverElement } from "@playhtml/react";
</TabItem>
</Tabs>

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";
Expand Down Expand Up @@ -572,7 +572,7 @@ These capabilities are the building blocks. The best way to see them in concert
<a class="ph-composed-card" href="https://playhtml.fun/experiments/4/" target="_blank" rel="noopener">
<span class="ph-composed-card__kicker">EXPERIMENT · 04</span>
<span class="ph-composed-card__title">Every color</span>
<span class="ph-composed-card__body">A page where every visitor adds one color. can-play + per-user awareness + an ever-growing shared palette.</span>
<span class="ph-composed-card__body">A page where every visitor adds one color. can-play + element awareness + an ever-growing shared palette.</span>
</a>
<a class="ph-composed-card" href="https://playhtml.fun/fridge" target="_blank" rel="noopener">
<span class="ph-composed-card__kicker">COMMUNITY · FRIDGE</span>
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/content/docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 2 additions & 2 deletions apps/docs/src/content/docs/data/data-essentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/content/docs/data/presence/cursors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down
55 changes: 48 additions & 7 deletions apps/docs/src/content/docs/data/presence/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ 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.

## The unified API

Expand Down Expand Up @@ -63,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) => {
Expand Down Expand Up @@ -113,6 +113,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
<button can-play id="presence-count">0 readers here</button>

<script>
const element = document.getElementById("presence-count");
element.defaultData = {};
element.myDefaultAwareness = { color: "#3b82f6" };
element.updateElement = () => {};
element.onClick = (_event, { setMyAwareness }) => {
setMyAwareness({ color: "#f97316" });
};
element.updateElementAwareness = ({ element, awareness }) => {
const label = awareness.length === 1 ? "reader" : "readers";
element.textContent = `${awareness.length} ${label} here`;
};
</script>
```

In React, the same fields are available from `withSharedState` and `<CanPlayElement>` render props:

```tsx
<CanPlayElement id="presence-count" defaultData={{}} myDefaultAwareness="#3b82f6">
{({ awareness, setMyAwareness }) => {
const label = awareness.length === 1 ? "reader" : "readers";
return (
<button onClick={() => setMyAwareness("#f97316")}>
{awareness.length} {label} here
</button>
);
}}
</CanPlayElement>
```

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.
Expand Down Expand Up @@ -159,6 +200,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).
11 changes: 6 additions & 5 deletions apps/docs/src/content/docs/integrations/building-with-ai.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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 }

Expand Down Expand Up @@ -141,15 +142,15 @@ 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
- Use localStorage for per-user preferences that shouldn't sync

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

Expand Down
9 changes: 6 additions & 3 deletions apps/docs/src/content/docs/reference/react-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ interface WithSharedStateConfig<T, V> {
```

- **`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/).

Expand All @@ -78,6 +78,8 @@ interface ReactElementEventHandlerData<T, V> {

`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:
Expand Down Expand Up @@ -109,6 +111,7 @@ interface CanPlayElementProps<T, V> {
```

- **`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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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`.
2 changes: 1 addition & 1 deletion apps/docs/src/content/docs/using-react.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

### `<CanPlayElement>`: for when you need JSX children, not a wrapper

Expand Down
Loading