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
18 changes: 18 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "playhtml",
"owner": {
"name": "Spencer Chang",
"url": "https://github.com/spencerc99"
},
"metadata": {
"description": "Plugins for building with the playhtml library"
},
"plugins": [
{
"name": "playhtml",
"source": "./claude-plugin",
"description": "Skill for building collaborative, real-time HTML elements with playhtml. Covers APIs, data types, and common mistakes.",
"version": "1.0.0"
}
]
}
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,26 @@ Cursors can be scoped to a page, domain, section, or custom room. You can filter

See the full cursor documentation for configuration options and patterns [cursors.md](https://github.com/spencerc99/playhtml/blob/main/docs/cursors.md).

## Building with AI / LLMs

playhtml works well with AI coding assistants. We provide two resources:

### Claude Code Plugin (recommended for Claude Code users)

If you use [Claude Code](https://docs.anthropic.com/en/docs/claude-code), install the playhtml plugin to get a skill that automatically activates when you ask Claude to build playhtml elements. It covers the APIs, data types, and common mistakes.

```bash
# Add the playhtml marketplace
claude plugin marketplace add spencerc99/playhtml

# Install the plugin
claude plugin install playhtml@playhtml
```

### Prompting Guide (for any LLM)

For other AI tools (ChatGPT, Copilot, Cursor, etc.), copy the prompt template from [`docs/llm-prompting-guide.md`](https://github.com/spencerc99/playhtml/blob/main/docs/llm-prompting-guide.md) into your conversation. It gives the LLM the context it needs to generate working playhtml code.

## Help & Community

Join our [Discord community](https://discord.gg/SKbsSf4ptU) to get help and show off what you've built!
Expand Down
13 changes: 13 additions & 0 deletions claude-plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "playhtml",
"version": "1.0.0",
"description": "Skill for building collaborative, real-time HTML elements with playhtml",
"author": {
"name": "Spencer Chang",
"url": "https://github.com/spencerc99"
},
"homepage": "https://playhtml.fun",
"repository": "https://github.com/spencerc99/playhtml",
"license": "MIT",
"keywords": ["playhtml", "collaborative", "real-time", "interactive", "html"]
}
104 changes: 104 additions & 0 deletions claude-plugin/skills/building-playhtml-elements/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
name: building-playhtml-elements
description: Use when building, creating, or implementing a playhtml element or component, whether in vanilla HTML or React. Triggers include requests to make interactive, collaborative, real-time, or multiplayer HTML elements.
---

# Building playhtml Elements

playhtml makes HTML elements collaborative and real-time via Yjs CRDTs.

## Before Implementing — Ask These Questions

If the user's request is ambiguous on ANY of these, **stop and ask**:

1. **Persistence**: Should data survive page refresh? (defaultData=yes, awareness=no)
2. **Shared vs per-user**: Should all users see the same state, or does each user have their own?
3. **Vanilla HTML or React?**

These determine which API and data type to use. Getting them wrong means a rewrite.

## Data Types

| Type | Persists? | Syncs? | Use for |
|------|-----------|--------|---------|
| `defaultData` | Yes | Yes | Positions, counts, messages, toggles |
| `myDefaultAwareness` | No | Yes | Who's online, typing, hover state |
| `dispatchPlayEvent` | No | One-shot | Confetti, notifications |
| `localStorage` | Yes | No | Per-user flags ("has reacted") |

## Critical Rules

- Every element MUST have a unique `id` attribute — without it, sync silently fails
- Vanilla HTML: Configure element properties BEFORE `playhtml.init()` (the #1 mistake)
- React: Wrap app in `<PlayProvider>`

## Quick Reference — Vanilla HTML (can-play)

```javascript
const el = document.getElementById("myElement");
el.defaultData = { count: 0 }; // REQUIRED
el.updateElement = ({ element, data }) => { ... }; // REQUIRED
el.onClick = (e, { data, setData }) => { ... };
el.onDrag = (e, { data, setData, localData, setLocalData }) => { ... };
el.onDragStart = (e, { setLocalData }) => { ... };
el.onMount = ({ getData, setData, getElement }) => { ... };
el.resetShortcut = "shiftKey"; // "shiftKey"|"ctrlKey"|"altKey"|"metaKey"

// THEN import — ordering matters!
import { playhtml } from "https://unpkg.com/playhtml@latest";
playhtml.init();
```

## Quick Reference — React (withSharedState)

```tsx
import { PlayProvider, withSharedState, usePlayContext } from "@playhtml/react";

const Counter = withSharedState(
{ defaultData: { count: 0 } },
({ data, setData, ref }) => (
<button ref={ref} onClick={() => setData({ count: data.count + 1 })}>
{data.count}
</button>
)
);

// Component receives: data, setData, awareness, setMyAwareness, ref
// For events: usePlayContext() → { dispatchPlayEvent, registerPlayEventListener }
// For cursors: usePlayContext() → { cursors, configureCursors }
```

## setData — Two Forms

```javascript
// Value form: replaces ALL data (spread to preserve other fields!)
setData({ ...data, count: data.count + 1 });

// Mutator form: modify in place (preferred for arrays/nested)
setData((draft) => { draft.items.push(newItem); });
```

## Built-in Capabilities

Use instead of `can-play` when they fit: `can-move`, `can-toggle`, `can-spin`, `can-grow`, `can-duplicate`, `can-mirror`. See `packages/common/src/index.ts` for implementations.

## Cursors (optional)

```javascript
playhtml.init({ cursors: { enabled: true, room: "page" } }); // "page"|"domain"|"section"
window.cursors.allColors.length; // user count
```

See `docs/cursors.md` for full API.

## Common Mistakes

1. **Config after init** (vanilla): Properties set after `playhtml.init()` are ignored. Configure FIRST.
2. **Missing `id`**: No id = no sync. Silent failure.
3. **Wrong data type**: Awareness for persistent data (disappears on disconnect) or defaultData for ephemeral presence (leaves stale data). Refer to the Data Types table.
4. **Bad array mutations**: In mutator form, the draft is a Yjs CRDT proxy. Use `push()`/`splice()` only — `shift()`, `pop()`, and `items[i] = x` don't sync correctly.
5. **Value form loses fields**: `setData({ x: 5 })` erases `y`. Always spread: `setData({ ...data, x: 5 })` or use mutator form.
6. **Deep nesting**: CRDTs work best with flat data. Avoid deeply nested objects.
7. **High-frequency updates**: Don't `setData` on every mousemove. Debounce, or use `setLocalData`/awareness.
8. **Computed values in state**: Don't store what you can calculate. Compute in `updateElement`/render.
9. **Missing PlayProvider** (React): `withSharedState` silently fails without it.
10 changes: 7 additions & 3 deletions docs/llm-prompting-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ React (withSharedState):

- withSharedState({ defaultData: {...} }, ({ data, setData, ref }) => JSX)
- For awareness: { myDefaultAwareness: value } in config, use setMyAwareness
- For events: useContext(PlayContext) → { registerPlayEventListener, dispatchPlayEvent }
- For events: usePlayContext() → { registerPlayEventListener, dispatchPlayEvent }
- For cursors in React: usePlayContext() → { cursors, configureCursors, getMyPlayerIdentity }

DATA UPDATES:

Expand All @@ -72,7 +73,8 @@ BUILT-IN CAPABILITIES (if they fit the use case):
- can-spin: Rotatable element
- can-grow: Click to scale up/down
- can-duplicate: Click to clone element
- can-hover: Shows who's hovering
- can-hover: Hover to toggle on/off state
- can-mirror: Syncs all element changes automatically
- Use these instead of can-play when possible

CURSOR CONFIGURATION (optional):
Expand All @@ -82,7 +84,9 @@ Enable collaborative cursors to show where other users are:
- Vanilla HTML: playhtml.init({ cursors: { enabled: true, room: "page" } })
- React: <PlayProvider initOptions={{ cursors: { enabled: true, room: "page" } }}>
- room options: "page" (same URL only), "domain" (entire site), "section" (same path prefix)
- Access cursor data: window.cursors.allColors, window.cursors.count
- Access cursor data: window.cursors.allColors (array of colors), window.cursors.color, window.cursors.name
- Get user count: window.cursors.allColors.length
- Listen for changes: window.cursors.on('allColors', callback)
- See docs/cursors.md for proximity detection, filtering, styling

DATA PERFORMANCE TIPS:
Expand Down
Loading