@webwriter/guidance 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/cli.mjs +165 -0
- package/dist/widget-docs.json +1 -0
- package/docs.mjs +32 -0
- package/package.json +28 -4
- package/skills/webwriter-guidance/SKILL.md +39 -0
- package/skills/webwriter-guidance/references/widget-rework.md +40 -0
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Frederic Salmen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {spawnSync} from "node:child_process"
|
|
3
|
+
import {existsSync, fstatSync, mkdtempSync, readFileSync, writeFileSync} from "node:fs"
|
|
4
|
+
import {tmpdir} from "node:os"
|
|
5
|
+
import {delimiter, dirname, join} from "node:path"
|
|
6
|
+
import {fileURLToPath} from "node:url"
|
|
7
|
+
|
|
8
|
+
import {loadWidgetDocs, loadWidgetKeywords} from "./docs.mjs"
|
|
9
|
+
|
|
10
|
+
const skill = "webwriter-guidance"
|
|
11
|
+
const usage = `Usage: ${skill} <command>
|
|
12
|
+
|
|
13
|
+
search <query> Find WebWriter widget docs
|
|
14
|
+
list List documentation IDs and descriptions
|
|
15
|
+
retrieve <id[,id...]> Read the full Markdown for one or more pages
|
|
16
|
+
badges [package.json] List badges and guide IDs, optionally for one manifest
|
|
17
|
+
install [options] Install the skill using the Vercel Skills CLI
|
|
18
|
+
uninstall [options] Remove the skill using the Vercel Skills CLI`
|
|
19
|
+
const stopWords = new Set("a an and are as at be by can do for from how i in is it of on or the to use what which with you".split(" "))
|
|
20
|
+
|
|
21
|
+
function terms(text) {
|
|
22
|
+
const words = text
|
|
23
|
+
.replace(/[a-z][a-z\d]*(?:[A-Z][a-z\d]*)+/g, word => `${word} ${word.replace(/([a-z\d])([A-Z])/g, "$1 $2")}`)
|
|
24
|
+
.toLowerCase()
|
|
25
|
+
.match(/[a-z0-9]+/g) ?? []
|
|
26
|
+
return new Set(words.filter(word => !stopWords.has(word)).map(word => word.replace(/([a-z]{2,}[^s])s$/, "$1")))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function describe({id, source}) {
|
|
30
|
+
const body = source.replace(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/, "").replace(/<!--[\s\S]*?-->/g, "")
|
|
31
|
+
const prose = body.replace(/^ {0,3}(`{3,}|~{3,})[^\n]*\r?\n[\s\S]*?^ {0,3}\1[ \t]*\r?$/gm, "")
|
|
32
|
+
const titles = [...prose.matchAll(/^# (.+)$/gm)].map(([, title]) => title.trim().replace(/^\w+\. /, ""))
|
|
33
|
+
const title = titles.join(", ") || id
|
|
34
|
+
const paragraphs = prose.trim().split(/\r?\n\s*\r?\n/)
|
|
35
|
+
.filter(paragraph => /^(?:\*\*)?[A-Z`]/.test(paragraph))
|
|
36
|
+
.slice(0, 3)
|
|
37
|
+
.map(paragraph => paragraph.replace(/!?\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\s+/g, " "))
|
|
38
|
+
const description = paragraphs.length ? `${title} — ${paragraphs[0]}` : title
|
|
39
|
+
const headings = [...prose.matchAll(/^#{2,6} (.+)$/gm)].map(([, heading]) => heading).join(" ")
|
|
40
|
+
return {
|
|
41
|
+
id, description, source,
|
|
42
|
+
titleTerms: terms(`${id} ${title}`),
|
|
43
|
+
headingTerms: terms(headings),
|
|
44
|
+
summaryTerms: terms(paragraphs.join(" ")),
|
|
45
|
+
bodyTerms: terms(body),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function search(catalog, query) {
|
|
50
|
+
const queryTerms = [...terms(query)]
|
|
51
|
+
const weights = queryTerms.map(term => {
|
|
52
|
+
const matches = catalog.filter(guide => guide.bodyTerms.has(term) || guide.titleTerms.has(term)).length
|
|
53
|
+
return Math.log(1 + catalog.length / (1 + matches))
|
|
54
|
+
})
|
|
55
|
+
return catalog.map(guide => ({
|
|
56
|
+
guide,
|
|
57
|
+
score: queryTerms.reduce((score, term, index) => {
|
|
58
|
+
const weight = guide.titleTerms.has(term) ? 6
|
|
59
|
+
: guide.headingTerms.has(term) ? 3
|
|
60
|
+
: guide.summaryTerms.has(term) ? 2
|
|
61
|
+
: guide.bodyTerms.has(term) ? 1 : 0
|
|
62
|
+
return score + weight * weights[index]
|
|
63
|
+
}, 0),
|
|
64
|
+
})).filter(({score}) => score > 0)
|
|
65
|
+
.sort((a, b) => b.score - a.score)
|
|
66
|
+
.slice(0, 5)
|
|
67
|
+
.map(({guide}) => guide)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function printEntries(guides) {
|
|
71
|
+
console.log(JSON.stringify(guides.map(({id, description}) => ({id, description})), null, 2))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function printBadges(manifestPath) {
|
|
75
|
+
const {badges, guides} = loadWidgetKeywords()
|
|
76
|
+
const catalog = Object.entries(badges).map(([keyword, {label, pattern}]) => ({
|
|
77
|
+
keyword, label, pattern,
|
|
78
|
+
guides: Object.entries(guides).filter(([, keywords]) => keywords.includes(keyword)).map(([id]) => `guides/${id}`),
|
|
79
|
+
}))
|
|
80
|
+
if(manifestPath === undefined) {
|
|
81
|
+
console.log(JSON.stringify(catalog, null, 2))
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const {keywords = []} = JSON.parse(readFileSync(manifestPath, "utf8"))
|
|
86
|
+
if(!Array.isArray(keywords) || keywords.some(keyword => typeof keyword !== "string")) {
|
|
87
|
+
throw new Error("Expected package.json keywords to be an array of strings.")
|
|
88
|
+
}
|
|
89
|
+
const declared = [...new Set(keywords)].flatMap(keyword => {
|
|
90
|
+
const normalized = keyword.toLowerCase()
|
|
91
|
+
const badge = catalog.find(entry => entry.pattern
|
|
92
|
+
? normalized.startsWith(entry.keyword) && normalized.length > entry.keyword.length
|
|
93
|
+
: normalized === entry.keyword)
|
|
94
|
+
return badge ? [{...badge, keyword}] : []
|
|
95
|
+
})
|
|
96
|
+
console.log(JSON.stringify(declared, null, 2))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function retrieve(catalog, ids) {
|
|
100
|
+
const guides = new Map(catalog.map(guide => [guide.id, guide]))
|
|
101
|
+
const pages = [...new Set(ids)].flatMap(id => {
|
|
102
|
+
const guide = guides.get(id)
|
|
103
|
+
if(guide) return [`--- docs/widgets/${id}.md ---\n\n${guide.source.trimEnd()}\n`]
|
|
104
|
+
console.error(`Unknown documentation ID: ${id}`)
|
|
105
|
+
process.exitCode = 1
|
|
106
|
+
return []
|
|
107
|
+
})
|
|
108
|
+
const output = pages.join("\n")
|
|
109
|
+
if(Buffer.byteLength(output) > 9500 && !fstatSync(1).isFile()) {
|
|
110
|
+
const path = join(mkdtempSync(join(tmpdir(), "webwriter-guidance-")), "guides.md")
|
|
111
|
+
writeFileSync(path, output)
|
|
112
|
+
console.log(`Output saved to ${path}. Read the file in full.`)
|
|
113
|
+
}
|
|
114
|
+
else process.stdout.write(output)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function runSkills(args) {
|
|
118
|
+
let runner = "npx"
|
|
119
|
+
let runnerArgs = args
|
|
120
|
+
if(process.platform === "win32") {
|
|
121
|
+
// Invoke npx's JS entry point directly so paths and options keep their argument boundaries.
|
|
122
|
+
const paths = [dirname(process.execPath), ...(process.env.PATH ?? "").split(delimiter)]
|
|
123
|
+
const entry = paths.map(path => join(path, "node_modules/npm/bin/npx-cli.js")).find(existsSync)
|
|
124
|
+
if(!entry) throw new Error("Could not locate npx. Install npm with Node.js and make it available on PATH.")
|
|
125
|
+
runner = process.execPath
|
|
126
|
+
runnerArgs = [entry, ...args]
|
|
127
|
+
}
|
|
128
|
+
const result = spawnSync(runner, runnerArgs, {stdio: "inherit"})
|
|
129
|
+
if(result.error) throw result.error
|
|
130
|
+
process.exitCode = result.status ?? 1
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function main([command, ...args]) {
|
|
134
|
+
const argument = args.join(" ").trim()
|
|
135
|
+
if(command === "--help" || command === "-h") {
|
|
136
|
+
console.log(usage)
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
if(command === "install" || command === "uninstall") {
|
|
140
|
+
const source = fileURLToPath(new URL("./skills/webwriter-guidance", import.meta.url))
|
|
141
|
+
const action = command === "install" ? ["add", source, "--skill", skill] : ["remove", skill]
|
|
142
|
+
runSkills(["-y", "skills", ...action, ...args])
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
if(command === "badges" && args.length <= 1) {
|
|
146
|
+
printBadges(args[0])
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
const ids = argument.split(/[,\s]+/).filter(Boolean)
|
|
150
|
+
if(!(command === "list" && !args.length || command === "search" && argument || command === "retrieve" && ids.length)) {
|
|
151
|
+
throw new Error(usage)
|
|
152
|
+
}
|
|
153
|
+
const catalog = loadWidgetDocs().map(describe).sort((a, b) => a.id.localeCompare(b.id))
|
|
154
|
+
if(command === "search") printEntries(search(catalog, argument))
|
|
155
|
+
else if(command === "retrieve") retrieve(catalog, ids)
|
|
156
|
+
else printEntries(catalog)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
main(process.argv.slice(2))
|
|
161
|
+
}
|
|
162
|
+
catch(error) {
|
|
163
|
+
console.error(error.message)
|
|
164
|
+
process.exitCode = 1
|
|
165
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"docs":[{"id":"guides/content","source":"# Nest content\n\nA widget can show text, HTML elements, and other widgets in its interface. Its editing configuration specifies which children authors can add and edit. The widget can respond to changes in its child elements.\n\n\n\n<!-- live-example: Nested content -->\n\n```js vanilla\nclass CalloutWidget extends HTMLElement {\n constructor() {\n super()\n this.attachShadow({mode: \"open\"}).innerHTML = `\n <aside><slot></slot></aside>\n <output aria-live=\"polite\"></output>`\n this.shadowRoot.querySelector(\"slot\").addEventListener(\"slotchange\", event => {\n const count = event.target.assignedElements().length\n this.shadowRoot.querySelector(\"output\").textContent = `${count} authored items`\n })\n }\n}\n\ncustomElements.define(\"ww-callout\", CalloutWidget)\n```\n\n```html\n<ww-callout>\n <p>Bring a reusable water bottle.</p>\n <p>Meet beside the north gate.</p>\n</ww-callout>\n```\n\n<!-- /live-example -->\n\nKeep authored content in the widget's light DOM. Use a [slot](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/slot) to show that content. This callout widget lets authors edit a note in the document.\n\n```html\n<ww-callout><p>Bring a reusable water bottle.</p></ww-callout>\n```\n\n```html\n<!-- In the widget's shadow DOM -->\n<slot></slot>\n```\n\nDeclare the child elements that authors can insert and edit.\n\n```json\n{\n \"editingConfig\": {\n \"./widgets/ww-callout\": {\"content\": \"p+\", \"marks\": \"b i\"}\n }\n}\n```\n\nThe authoring tool treats the widget as a single element if the configuration has no `content` entry. For nested widgets, specify their tag names or groups in the [content expression](../reference/content-expressions.md). Set `uninsertable: true` if authors can insert the child widget only through a parent or snippet.\n\nUse [`slotchange`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/slotchange_event) to detect changes to slot assignments. Use a [`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) to detect changes to the content of those children. Disconnect the observers when the widget disconnects. Examine the current children before you change them. Keep unfamiliar children, comments, and grouping metadata.\n\n## Allowed child elements\n\n## Changes to child content\n"},{"id":"guides/conversions","source":"# Convert & import/export\n\nA widget can convert HTML content to an interactive element. It can also convert its content to native HTML. The widget can import and export files in the formats it declares.\n\n\n\n<!-- live-example: Convert a list -->\n\n```js vanilla\nclass PackingChecklist extends HTMLElement {\n static convertFrom = {ul: \"loadList\", \".json\": \"importJson\"}\n static convertTo = {ul: \"toList\", \"text/plain\": \"exportText\"}\n\n constructor() {\n super()\n this.attachShadow({mode: \"open\"}).innerHTML = `\n <slot></slot>\n <label>Import a JSON list <input type=\"file\" accept=\".json,application/json\"></label>\n <button type=\"button\">Preview exported list</button>\n <button type=\"button\">Preview text file</button>\n <pre role=\"status\"></pre>`\n const root = this.shadowRoot\n const result = root.querySelector(\"pre\")\n const [listButton, textButton] = root.querySelectorAll(\"button\")\n listButton.addEventListener(\"click\", () => { result.textContent = this.toList()?.outerHTML ?? \"\" })\n textButton.addEventListener(\"click\", async () => { result.textContent = await this.exportText().text() })\n root.querySelector(\"input\").addEventListener(\"change\", async event => {\n const file = event.target.files[0]\n if(!file) return\n try {\n const apply = await this.importJson(file)\n apply()\n result.textContent = \"List imported.\"\n }\n catch(error) { result.textContent = error.message }\n })\n }\n\n loadList(fragment) {\n this.replaceChildren(fragment)\n }\n\n toList() {\n return this.querySelector(\":scope > ul\")?.cloneNode(true) ?? null\n }\n\n async importJson(file) {\n const values = JSON.parse(await file.text())\n if(!Array.isArray(values) || !values.every(value => typeof value === \"string\")) {\n throw new Error(\"Expected a JSON array of strings.\")\n }\n const list = document.createElement(\"ul\")\n for(const value of values) {\n const item = document.createElement(\"li\")\n item.textContent = value\n list.append(item)\n }\n return () => this.loadList(list)\n }\n\n exportText() {\n const items = this.querySelectorAll(\":scope > ul > li\")\n return new File([Array.from(items, item => item.textContent).join(\"\\n\")],\n \"packing-list.txt\", {type: \"text/plain\"})\n }\n}\n\ncustomElements.define(\"ww-packing-checklist\", PackingChecklist)\n```\n\n```html\n<ww-packing-checklist>\n <ul><li>Water bottle</li><li>Notebook</li></ul>\n</ww-packing-checklist>\n```\n\n<!-- /live-example -->\n\nUse `convertFrom` to specify the formats that the widget can read. Use `convertTo` to specify the formats that it can write. Use tag names for HTML conversions. Use file extensions or [media types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types) for file conversions.\n\nThe authoring tool shows available conversions for the selected element. Authors can open the Widget drawer or select the element icon to find them. A dotted underline in the Packages drawer identifies an installed widget that can accept the selected element.\n\n## Convert between a native list and a packing checklist\n\nThe packing checklist keeps a native list in its light DOM. The authoring tool passes a detached copy of the selected list to `loadList`. It replaces the selected list with the widget. The `toList` method returns a detached copy of the widget's list. The authoring tool can undo the replacement in one step.\n\n```js\nclass PackingChecklist extends HTMLElement {\n static convertFrom = {ul: \"loadList\"}\n static convertTo = {ul: \"toList\"}\n\n loadList(fragment) {\n this.replaceChildren(fragment)\n }\n\n toList() {\n return this.querySelector(\":scope > ul\")?.cloneNode(true) ?? null\n }\n}\n\ncustomElements.define(\"ww-packing-checklist\", PackingChecklist)\n```\n\nDeclare `\"content\": \"ul\"` in the [editing configuration](../reference/editing-configuration.md) to let authors edit the list in place. To export adjacent elements, return a [`DocumentFragment`](https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment) with elements of the declared tag. A timeline, for example, can return `details` elements.\n\n## Add file formats to the same maps\n\nReplace the two maps with the declarations that follow. Add the methods to the class.\n\n```js\nstatic convertFrom = {ul: \"loadList\", \".json\": \"importJson\"}\nstatic convertTo = {ul: \"toList\", \"text/plain\": \"exportText\"}\n\nasync importJson(file) {\n const values = JSON.parse(await file.text())\n if(!Array.isArray(values) || !values.every(value => typeof value === \"string\")) {\n throw new Error(\"Expected a JSON array of strings.\")\n }\n\n const list = this.ownerDocument.createElement(\"ul\")\n for(const value of values) {\n const item = this.ownerDocument.createElement(\"li\")\n item.textContent = value\n list.append(item)\n }\n return () => this.replaceChildren(list)\n}\n\nexportText() {\n const items = this.querySelectorAll(\":scope > ul > li\")\n const text = Array.from(items, item => item.textContent).join(\"\\n\")\n return new File([text], \"packing-list.txt\", {type: \"text/plain\"})\n}\n```\n\nA file such as `packing-list.json` can contain `[\"Water bottle\", \"Notebook\", \"Rain jacket\"]`. The import method reads the file. It checks the data before it returns a function. The authoring tool runs that function synchronously if the authored content has not changed. The authoring tool can undo the import in one step.\n\nUse a file extension as the key if the filename identifies the format. For example, `\".json\": \"importJson\"` permits import when the file has no media type. The authoring tool uses the key to filter the file chooser and identify the file. Check the file contents in the import handler.\n\nFor an export, use the declared extension in the filename or set the declared media type. The authoring tool downloads the file.\n\nDeclare `\"importFormats\": [\".json\"]` and `\"exportFormats\": [\"text/plain\"]` in the widget's editing configuration. The configuration lists the file formats. The runtime methods do the conversions. See the [editing configuration reference](../reference/editing-configuration.md#advertise-file-formats).\n\nThe [runtime reference](../reference/runtime.md#conversions) lists return values and gives the order of precedence for instance and class declarations.\n\n## Supported conversion types\n\n## Validation of imported files\n"},{"id":"guides/editing","source":"# Support editing\n\nA widget can change its controls when the authoring tool changes the document mode. In edit mode, authors can use controls to change the content or settings. In read mode, readers can see the content and use its interactive controls.\n\n\n\nEdit the heading in the preview. Switch to **Reading** to use **Show note**.\n\n<!-- live-example: Edit mode -->\n\n```js vanilla\nclass EditNote extends HTMLElement {\n static observedAttributes = [\"contenteditable\"]\n\n constructor() {\n super()\n this.attachShadow({mode: \"open\"}).innerHTML = `\n <slot></slot>\n <p id=\"hint\"></p>\n <button id=\"toggle\" type=\"button\">Show note</button>\n <p id=\"note\" hidden>Meet at the north gate.</p>`\n this.shadowRoot.querySelector(\"#toggle\").addEventListener(\"click\", () => {\n const note = this.shadowRoot.querySelector(\"#note\")\n note.hidden = !note.hidden\n })\n }\n\n connectedCallback() {\n this.renderMode()\n }\n\n attributeChangedCallback() {\n this.renderMode()\n }\n\n renderMode() {\n const editing = this.isContentEditable\n this.shadowRoot.querySelector(\"#hint\").textContent = editing ? \"Edit the heading in the document.\" : \"\"\n this.shadowRoot.querySelector(\"#toggle\").hidden = editing\n }\n}\n\ncustomElements.define(\"ww-edit-note\", EditNote)\n```\n\n```html\n<ww-edit-note contenteditable=\"\">\n <p>Field trip meeting point</p>\n</ww-edit-note>\n```\n\n<!-- /live-example -->\n\n<!-- library:vanilla -->\nUse [`this.isContentEditable`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/isContentEditable) to identify the mode.\n\n```css\n/* In the widget's shadow styles */\n:host([contenteditable]) .user-controls { display: none; }\n:host(:not([contenteditable])) .author-hint { display: none; }\n```\n\nThese selectors check for the presence of the [`contenteditable`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/contenteditable) attribute. If the widget accepts `contenteditable=\"false\"`, use `isContentEditable` in the script to check the mode.\n\nObserve `contenteditable` if a change of mode requires a script update. Declare [options and actions](../reference/options-actions.md) for author settings. The authoring tool shows these controls in the Widget drawer.\n\n<!-- /library -->\n<!-- library:lit -->\nUse `this.editable` in your `LitElementWw` template and input handlers. The base class requests an update when the edit mode changes on the widget or an ancestor.\n\n```js\ntoggleCoordinates() {\n if(this.editable) return\n this.showCoordinates = !this.showCoordinates\n}\n\nrender() {\n return html`\n <slot></slot>\n <button ?hidden=${this.editable} @click=${this.toggleCoordinates}>\n ${this.showCoordinates ? \"Hide coordinates\" : \"Show coordinates\"}\n </button>\n <p ?hidden=${this.editable || !this.showCoordinates}>${this.coordinates}</p>\n <p ?hidden=${!this.editable}>Set the location, then use Preview.</p>`\n}\n```\n\nImport `html` from `lit`. Declare `showCoordinates` as [local reactive state](https://lit.dev/docs/components/properties/#internal-reactive-state). Declare [options and actions](../reference/options-actions.md) for author settings. The authoring tool shows these controls in the Widget drawer.\n<!-- /library -->\n\nKeep the widget controls in the [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM). Use the edit mode property for your library. Test both modes in **Preview**. Save the document. Open the document again. Check that the widget shows the saved content.\n\n## Controls in edit mode\n\n## Checks in read mode\n"},{"id":"guides/focus","source":"# Handle selection & input\n\nA widget can move keyboard focus to its controls. It can specify which input events the authoring tool must receive. It can also move the caret to the authored content.\n\n\n\nSelect **Focus first sound**, then press Enter to activate the focused button. Use Tab and Shift+Tab to move between the controls.\n\n<!-- live-example: Keyboard focus -->\n\n```js vanilla\nclass Soundboard extends HTMLElement {\n constructor() {\n super()\n this.attachShadow({mode: \"open\"}).innerHTML = `\n <button data-sound type=\"button\">Rain</button>\n <button data-sound type=\"button\">Wind</button>\n <button id=\"focus\" type=\"button\">Focus first sound</button>\n <output aria-live=\"polite\"></output>`\n this.shadowRoot.querySelector(\"#focus\").addEventListener(\"click\", () => this.focus())\n this.shadowRoot.addEventListener(\"click\", event => {\n if(event.target.id === \"focus\") return\n if(!event.target.matches(\"[data-sound]\")) return\n this.shadowRoot.querySelector(\"output\").textContent = `Selected ${event.target.textContent}`\n })\n }\n\n focus(options) {\n this.shadowRoot.querySelector(\"[data-sound]\")?.focus(options)\n }\n}\n\ncustomElements.define(\"ww-soundboard\", Soundboard)\n```\n\n```html\n<ww-soundboard></ww-soundboard>\n```\n\n<!-- /live-example -->\n\nUse native buttons, inputs, and labels in the shadow DOM to get standard keyboard behavior.\n\nImplement [`focus()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) to move focus from the widget to a control. In this example, a soundboard moves focus to its first sound button.\n\n```js\nfocus(options) {\n this.shadowRoot.querySelector(\"button[data-sound]\")?.focus(options)\n}\n```\n\nUse [`:focus-visible`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:focus-visible) to show which control has focus. Use [`<output aria-live=\"polite\">`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-live) for feedback that an assistive technology must announce.\n\nThe authoring tool ignores input events from the widget's shadow DOM by default. List the required event types in `propagateEvents`, for example `[\"keydown\"]`. Test the keyboard shortcuts in widget inputs. Make sure that typing causes only the intended actions.\n\n## Move the document selection\n\nWhen the widget host has direct focus, the authoring tool lets the widget handle input. When that host loses focus, the authoring tool selects the widget element. The widget can then use the native [Selection API](https://developer.mozilla.org/en-US/docs/Web/API/Selection) to change the document selection. This example moves the caret to an authored text node.\n\n```js\nthis.blur()\nconst selection = this.ownerDocument.getSelection()\nselection?.setPosition(textNode, 0)\n```\n\nGet `textNode` from the current document. Edits can replace or remove a node. Move focus away from the host before you set the selection. For this operation, the host must accept focus directly.\n\nThe authoring tool can follow selections in authored children while the widget has focus. To move the document selection outside the widget, first move focus away from the host.\n\nTest Tab, Shift+Tab, Enter, and Space in edit mode and in preview. Update the existing controls to keep their focus.\n\n## Keyboard focus order\n\n## Pointer and touch input\n"},{"id":"guides/index","source":"# Guides\n\nEach guide gives instructions for a task in an existing widget. To make your first widget, use the [tutorial](../tutorial/index.md).\n\n<!-- widget-capabilities -->\n"},{"id":"guides/libraries","source":"# Manage dependencies\n\nA widget can use native web APIs or a framework that produces a browser-ready custom element. It can include styles and media, and it can share compatible dependencies with other widgets. It can load optional features when readers need them and select which features an offline export includes.\n\n\n\nTry **Load orbit diagram**. The widget imports the optional module on the first request and reuses it on later requests. The demo stores the module in the example source so it can run inside the preview. In a package, put it in its own file.\n\n<!-- live-example: On-demand orbit diagram -->\n\n```js vanilla\nconst orbitModule = `\n export function drawOrbit() {\n return '<svg viewBox=\"0 0 160 64\" width=\"160\" role=\"img\" aria-label=\"Planet on an orbit\">'\n + '<ellipse cx=\"80\" cy=\"32\" rx=\"72\" ry=\"24\" fill=\"none\" stroke=\"currentColor\"/>'\n + '<circle cx=\"80\" cy=\"32\" r=\"9\" fill=\"gold\"/>'\n + '<circle cx=\"138\" cy=\"18\" r=\"6\" fill=\"royalblue\"/></svg>'\n }\n`\n\nclass OrbitDemo extends HTMLElement {\n #module\n constructor() {\n super()\n const root = this.attachShadow({mode: \"open\"})\n root.innerHTML = `\n <button type=\"button\">Load orbit diagram</button>\n <p role=\"status\">Optional module idle.</p>\n <div></div>\n `\n const button = root.querySelector(\"button\")\n const status = root.querySelector(\"p\")\n button.addEventListener(\"click\", async () => {\n button.disabled = true\n status.textContent = \"Loading optional module…\"\n try {\n this.#module ??= this.loadDiagram()\n const {drawOrbit} = await this.#module\n root.querySelector(\"div\").innerHTML = drawOrbit()\n status.textContent = \"Optional module ready.\"\n } catch {\n this.#module = undefined\n status.textContent = \"Could not load the diagram. Try again.\"\n } finally {\n button.disabled = false\n }\n })\n }\n\n async loadDiagram() {\n const url = URL.createObjectURL(new Blob([orbitModule], {type: \"text/javascript\"}))\n try { return await import(url) }\n finally { URL.revokeObjectURL(url) }\n }\n}\n\ncustomElements.define(\"ww-orbit-demo\", OrbitDemo)\n```\n\n```html\n<ww-orbit-demo></ww-orbit-demo>\n```\n\n<!-- /live-example -->\n\n<!-- library:vanilla -->\nThe [tutorial](../tutorial/index.md) uses [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) in its **JS** version. The library selector also offers [Lit](https://lit.dev/docs/components/overview/).\n<!-- /library -->\n<!-- library:lit -->\nThe [tutorial](../tutorial/index.md) uses `LitElementWw`. This base class gives access to edit mode, inherited language changes, and scoped data. Select **JS** in the library selector for the example that uses `HTMLElement` directly.\n<!-- /library -->\n\n<!-- library:react -->\nThe tutorial uses JSX function components, React hooks, and [`createRoot`](https://react.dev/reference/react-dom/client/createRoot) inside a native custom element. Mount the React component in the shadow DOM. Unmount it when the widget disconnects. Before publication, use the build script to compile JSX and bundle the CommonJS modules for React and React DOM.\n<!-- /library -->\n<!-- library:vue -->\nThe tutorial uses Vue single-file components with `<script setup>`, `<template>`, and `<style>`. The registration module calls [`defineCustomElement`](https://vuejs.org/api/custom-elements#definecustomelement). The build scripts compile the component in custom-element mode. They also include the shadow styles and bundle Vue.\n<!-- /library -->\n<!-- library:angular -->\nThe tutorial uses [Angular Elements](https://angular.dev/guide/elements) with shadow encapsulation and a native slot. Its TypeScript classes use `@Component` and `@Input` decorators. The build script compiles TypeScript. It also bundles the Angular runtime and template compiler. The authoring tool loads the browser-ready modules.\n<!-- /library -->\n<!-- library:svelte -->\nThe tutorial uses the [Svelte custom-element compiler](https://svelte.dev/docs/svelte/custom-elements). The build script compiles `.svelte` components and bundles their runtime. The authoring tool loads the browser-ready modules.\n<!-- /library -->\n\nDeclare framework dependencies in [`dependencies`](https://docs.npmjs.com/cli/v11/configuring-npm/package-json/#dependencies). Use bare module specifiers to import them. The build scripts compile the source files and bundle the runtime when necessary.\n\n<!-- library:vanilla -->\n```js\nimport {LitElement, html} from \"lit\"\n```\n<!-- /library -->\n<!-- library:lit -->\n```js\nimport {LitElementWw} from \"@webwriter/lit\"\nimport {html} from \"lit\"\n```\n<!-- /library -->\n<!-- library:react -->\n```js\nimport {useState} from \"react\"\nimport {createRoot} from \"react-dom/client\"\n```\n<!-- /library -->\n<!-- library:vue -->\n```js\nimport {defineCustomElement} from \"vue/dist/vue.runtime.esm-browser.js\"\n```\n<!-- /library -->\n<!-- library:angular -->\n```js\nimport {createCustomElement} from \"@angular/elements\"\n```\n<!-- /library -->\n<!-- library:svelte -->\n```js\nimport {onMount} from \"svelte\"\n```\n<!-- /library -->\n\n`@webwriter/build` bundles dependencies into each widget by default. If the authoring tool supports shared dependencies, use `--shared-dependencies`. The document's import map resolves the imports for compatible dependencies. Use `prebuiltDependencies` for dependencies that must be in the bundle, such as CommonJS code.\n\n<!-- library:lit -->\nDeclare `@webwriter/lit` and `lit` in `dependencies`. Register third-party shadow components with `static scopedElements` to prevent conflicts with global names.\n<!-- /library -->\n\nRender the widget interface in the shadow DOM.\n\nExport document-level CSS separately. For example, a field guide can use `./widgets/ww-field-guide.*` for its script and stylesheet. Include the theme if the component library requires one.\n\nUse a module-relative URL for a file in the same package as the module.\n\n```js\nconst imageUrl = new URL(\"./fern.svg\", import.meta.url).href\n```\n\nInclude the file in the published package. Check that the file loads in preview and in saved HTML. The offline export can include resources that it can resolve from the package. Other network requests still require network access. See the [package manifest reference](../reference/distribution.md).\n\n## Optimize the code bundle\n\nUse [`import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) to load large optional features when readers need them. Offline HTML usually includes these modules so they can run without a network connection. If saved settings restrict the available features, use `static offlineImports(element)` to select the necessary modules and dependencies.\n\n\n\n### Put optional features behind lazy imports\n\nPut the optional features in a separate loader module. Use a literal module specifier in each `import()` call. In this example, `widgets/ww-simulation.js` is the widget entry. `simulations/loaders.js` contains the loaders.\n\n```js\n// simulations/loaders.js\nexport const loaders = {\n orbits: () => import(\"./orbits.js\"),\n circuits: () => import(\"./circuits.js\"),\n}\n```\n\n### Select imports from the saved widget\n\nUse the same rule to select features when the widget loads and when the document exports. The authoring tool calls the hook synchronously with a detached copy of the saved element. Read its attributes or light-DOM children. Keep the copy unchanged.\n\n```js\n// widgets/ww-simulation.js\nimport {loaders} from \"../simulations/loaders.js\"\n\nconst simulationType = element => element.getAttribute(\"type\") ?? \"orbits\"\n\nclass WwSimulation extends HTMLElement {\n static offlineImports(element) {\n const type = simulationType(element)\n return [{\n importer: \"../simulations/loaders.js\",\n keep: Object.hasOwn(loaders, type) ? [`./${type}.js`] : [],\n }]\n }\n\n async loadSimulation() {\n const type = simulationType(this)\n if (Object.hasOwn(loaders, type)) return loaders[type]()\n }\n}\n\ncustomElements.define(\"ww-simulation\", WwSimulation)\n```\n\n`importer` identifies the file with the `import()` calls relative to the published widget entry. Each `keep` value matches a specifier in one of those calls. For example, `./orbits.js` is relative to `loaders.js`. Make sure that the published package keeps the loader module and its dynamic imports. The build can also put code directly in a module. In that case, the code is part of that module.\n\nThe offline export includes the selections for each widget instance and insertable template. A document with an Orbits widget and a Circuits widget contains both simulations. Include all modes that readers can select offline. Return `keep: \"*\"` if readers need all modes. See the [runtime interface](../reference/runtime.md#offline-imports).\n\n### Verify the offline export\n\n1. Load the built widget in the authoring tool.\n2. Select one variant.\n3. Set a value that you can identify when you open the document again.\n4. Save the document as **Offline HTML**.\n5. Open the file in a browser with the network disabled.\n6. Make sure that the selected variant starts and restores the saved state and settings.\n7. Compare the file size with a document that includes all variants.\n8. If an import fails with `OfflineImportError`, show a message that explains which variants are available offline.\n\n## Dependency requirements in the manifest\n\n## Files and styles in the published package\n"},{"id":"guides/live-sessions","source":"# Support live sessions\n\nA widget can give authors control over optional participant interactions in a live session. It can collect responses that participants submit from the reader. Hosts can see individual responses or compare them in an aggregated view.\n\n\n\nChoose **Shared** and **Reading**. Submit a different vote in each preview to see each participant's response.\n\n<!-- live-example: Workshop poll -->\n\n```js vanilla\nclass WorkshopPoll extends HTMLElement {\n static permissions = {\n allowGlossary: {\n attribute: \"allow-glossary\",\n label: {_: \"Allow glossary\"},\n description: {_: \"Let participants open a glossary.\"},\n },\n }\n\n static observedAttributes = [\"allow-glossary\", \"contenteditable\"]\n\n constructor() {\n super()\n this.attachShadow({mode: \"open\"}).innerHTML = `\n <label><input type=\"checkbox\"> Allow glossary</label>\n <p>Choose the next activity.</p>\n <label><input type=\"radio\" name=\"activity\" value=\"drawing\" checked> Drawing</label>\n <label><input type=\"radio\" name=\"activity\" value=\"collage\"> Collage</label>\n <button type=\"button\">Submit vote</button>\n <output aria-live=\"polite\"></output>\n <details hidden><summary>Activity glossary</summary><p>Drawing uses lines and shapes.</p></details>`\n this.shadowRoot.querySelector('input[type=\"checkbox\"]').addEventListener(\"change\", event => {\n this.toggleAttribute(\"allow-glossary\", event.currentTarget.checked)\n })\n this.shadowRoot.querySelector(\"button\").addEventListener(\"click\", () => {\n const choice = this.shadowRoot.querySelector('input[name=\"activity\"]:checked')?.value\n if(!choice) return\n const block = this.querySelector(':scope > script[slot=\"data\"]')\n if(block) block.textContent = JSON.stringify({activity: choice})\n })\n }\n\n #observer = new MutationObserver(() => this.showVote())\n\n connectedCallback() {\n this.#observer.observe(this, {childList: true, characterData: true, subtree: true})\n this.showVote()\n }\n\n disconnectedCallback() { this.#observer.disconnect() }\n\n showVote() {\n const block = this.querySelector(':scope > script[slot=\"data\"]')\n const output = this.shadowRoot.querySelector(\"output\")\n try {\n const {activity} = JSON.parse(block?.textContent ?? \"{}\")\n output.textContent = activity ? `Vote submitted: ${activity}.` : \"\"\n }\n catch { output.textContent = \"The saved vote is invalid.\" }\n }\n\n attributeChangedCallback() {\n const enabled = this.hasAttribute(\"allow-glossary\")\n const details = this.shadowRoot.querySelector(\"details\")\n details.hidden = !enabled\n if(!enabled) details.open = false\n this.shadowRoot.querySelector('input[type=\"checkbox\"]').checked = enabled\n this.shadowRoot.querySelector(\"label\").hidden = !this.isContentEditable\n }\n}\n\ncustomElements.define(\"ww-workshop-poll\", WorkshopPoll)\n```\n\n```html shared\n<ww-workshop-poll allow-glossary>\n <script type=\"application/json\" slot=\"data\">{\"activity\":\"\"}</script>\n</ww-workshop-poll>\n```\n\n<!-- /live-example -->\n\n## Let authors choose what participants can do\n\nDeclare `static permissions` to give the author control over an optional interaction. For example, an author can give workshop participants access to a glossary.\n\nThe author checkbox adds or removes `allow-glossary` on the widget. Open the glossary in the participant view. Clear **Allow glossary**. Make sure that the glossary closes and that the control disappears.\n\n1. Select the widget in the authoring tool.\n2. Open **Distribute**.\n3. Open the permissions drawer for the selected widget.\n4. Under **Permissions**, select **Allow glossary** below **Edit**.\n5. Start a live session from **Preview**.\n6. Join the live session as a participant.\n7. Make sure that the glossary control is available.\n\n**Allow glossary** works independently of **Edit**. It is available when **View** is on.\n\nUse the Boolean property name as the key in the declaration. Specify the HTML attribute for that property. The authoring tool adds an empty attribute when the author selects the checkbox. It removes the attribute when the author clears the checkbox. Reflect the widget property to the attribute. Observe attribute changes to update the participant view.\n\n```js\nstatic permissions = {\n allowGlossary: {\n attribute: \"allow-glossary\",\n label: {_: \"Allow glossary\"},\n description: {_: \"Let participants open a glossary during the activity.\"},\n },\n}\n\nstatic observedAttributes = [\"allow-glossary\"]\n\nget allowGlossary() {\n return this.hasAttribute(\"allow-glossary\")\n}\n\nset allowGlossary(value) {\n this.toggleAttribute(\"allow-glossary\", Boolean(value))\n}\n```\n\nIn `attributeChangedCallback()`, use `this.allowGlossary` to set the visibility of the glossary. In authored HTML, `<ww-workshop-glossary allow-glossary>` enables it. See the [element permissions reference](../reference/options-actions.md#element-permissions).\n\nThese flags control the interactions in the widget. The authoring tool sets the permissions for document edits. A service that the widget calls must do its own authorization checks.\n\n## Save participant responses\n\nStore participant data in a direct data block. Set `sharedData: true` in the [editing configuration](../reference/editing-configuration.md). A workshop poll, for example, can store each participant's vote in this block.\n\n```html\n<ww-workshop-poll>\n <p>Choose the next workshop activity.</p>\n <script type=\"application/json\" slot=\"data\">{\"activity\":\"\"}</script>\n</ww-workshop-poll>\n```\n\nFor separate data per participant, omit `shared`. Use the [participant data guide](sharing.md) to select a sharing scope. Set up the data handlers. Wait for the data to be ready before you write. Show accepted changes without writing them again. The poll's aggregated view reads the response data from the widget snapshot.\n\n## Submit responses from the reader\n\nSet `static formAssociated = true` on the custom element class to include the widget in **Submit**. An HTML form can group widgets and standard inputs for validation.\n\n```html\n<form>\n <ww-workshop-poll name=\"activity\" required>\n <p>Choose the next workshop activity.</p>\n <script type=\"application/json\" slot=\"data\">{\"activity\":\"\"}</script>\n </ww-workshop-poll>\n</form>\n```\n\nUse [`ElementInternals.setValidity()`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/setValidity) to report invalid input, such as a missing selection. Forward `reportValidity()` to let the reader show the validation message. For standard HTML forms, give the widget a `name`. Use [`ElementInternals.setFormValue()`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/setFormValue) to supply its response. For multiple values, pass [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) with one entry for each selected option.\n\nWhen the participant confirms, the reader adds the Boolean `submitted` attribute. The reader also dispatches a non-bubbling `submit` event on each participating widget. Use the event to show immediate feedback. Observe the attribute to show feedback when the authoring tool restores a snapshot. Remove the attribute when the response changes. For example, the workshop poll can show “Vote submitted.”\n\nThe widget snapshot includes the `submitted` attribute. Keep the response in the data block so the host can read it during the session or in replay.\n\n## Declare an aggregated view\n\nAdd `static aggregate` to the widget class. Select a shape for the view. Specify the value to compare. This declaration counts the workshop poll votes.\n\n```js\nstatic aggregate = {\n shape: \"selection\",\n title: \"Activity votes\",\n items: [\n {id: \"drawing\", label: \"Drawing\"},\n {id: \"collage\", label: \"Collage\"},\n ],\n value: data => data?.activity || undefined,\n}\n```\n\nReturn `undefined` from `value` to omit a sample with no selection. Set `reference` to mark a comparison value. An `aggregate` function receives read-only snapshots. It can use them to set labels and the reference value.\n\nReturn an array of configurations to supply several views for a widget. Give each configuration a `title`. Give each configuration a stable `id`. If you omit the ID, the authoring tool uses the shape name. The host selects a view with the widget's live session selector.\n\nThe [Aggregated View reference](../reference/aggregated-view.md) lists the seven shapes and their inputs. Select `custom` for widget-specific graphics. Register a separate element that receives the aggregate context.\n\n## Check the live view\n\nStart a live session from **Preview**. Join the session as several participants. Submit a different response as each participant. The host can select a declared view, **Thumbnail wall**, or an individual participant. The authoring tool shows participant thumbnails if it has no declaration or the declaration fails.\n\nChange the participant filter. Make sure that the chart shows the new selection. Select a time in replay. Check that replay shows the responses for that time. Test missing responses, remote data changes, undo, and redo. Shared data supply one response for each shared scope.\n\nKeep aggregate rendering read-only. Test a custom renderer in a narrow container. Disconnect its observers and remove its listeners when the renderer disconnects.\n\n## Participant permissions and controls\n\n## Response checks during replay\n"},{"id":"guides/localization","source":"# Localize a widget\n\nA widget can use its own language setting or the language of the document. It can update the text when the language changes. The authoring tool can translate option and action labels while the saved values stay the same.\n\n\n\n<!-- live-example: Localized greeting -->\n\n```js vanilla\nclass LocalizedGreeting extends HTMLElement {\n static observedAttributes = [\"lang\"]\n\n constructor() {\n super()\n this.attachShadow({mode: \"open\"})\n }\n\n connectedCallback() {\n this.render()\n }\n\n attributeChangedCallback() {\n this.render()\n }\n\n render() {\n const german = this.lang.toLowerCase().startsWith(\"de\")\n this.shadowRoot.innerHTML = `\n <style>\n :host { display: block; }\n section { display: grid; justify-items: start; gap: .5rem; padding: .75rem; border: 1px solid #b8c7d6; border-radius: .5rem; }\n </style>\n <section lang=\"${german ? \"de\" : \"en\"}\">\n <strong>${german ? \"Guten Morgen!\" : \"Good morning!\"}</strong>\n <button type=\"button\">${german ? \"Sprache wechseln\" : \"Change language\"}</button>\n </section>`\n this.shadowRoot.querySelector(\"button\").addEventListener(\"click\", () =>\n this.lang = german ? \"en\" : \"de\")\n }\n}\n\ncustomElements.define(\"ww-localized-greeting\", LocalizedGreeting)\n```\n\n```html\n<ww-localized-greeting lang=\"en\"></ww-localized-greeting>\n```\n\n<!-- /live-example -->\n\n<!-- library:vanilla -->\nRead the nearest [`lang`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/lang) attribute on the widget or one of its ancestors.\n\n```js\nconst locale = this.closest(\"[lang]\")?.getAttribute(\"lang\") || \"en\"\nconst language = locale.toLowerCase().split(\"-\")[0]\nconst downloadLabel = language === \"de\" ? \"Herunterladen\" : \"Download\"\n```\n\nUpdate labels when the language changes on the widget or an ancestor. Call `disconnect()` on the language observer when the widget disconnects.\n\n<!-- /library -->\n<!-- library:lit -->\n`LitElementWw` provides a reactive `lang` property. It uses the widget's `lang` value if one exists. Otherwise, it uses the language of the nearest ancestor with a language setting. Read `lang` in `render()`.\n\n```js\nrender() {\n const language = this.lang.toLowerCase().split(\"-\")[0]\n const downloadLabel = language === \"de\" ? \"Herunterladen\" : \"Download\"\n return html`<button>${downloadLabel}</button>`\n}\n```\n\nImport `html` from `lit`. The base class updates the template when the language changes. In a project that uses [`@lit/localize`](https://lit.dev/docs/localization/overview/), set `this.localize` to `{getLocale, setLocale}` in the constructor.\n<!-- /library -->\n\nUse translated labels for the options and actions in the authoring tool.\n\n```js\nstatic options = {\n speaker: {type: String, label: {_: \"Speaker\", de: \"Sprechername\"}},\n}\n```\n\n`LocalizedText` supports a locale, a language, and an `_` fallback. Keep the same option values and attribute names in all languages.\n\n<!-- library:lit optional -->\nFor a project that uses `@lit/localize`, run `npx @webwriter/build localize`. Build the widget again. The command can also translate snippets and metadata. See the [build commands](../reference/build-test.md).\n<!-- /library -->\n\n## Language selection and fallback\n\n## Checks after a language change\n"},{"id":"guides/migrations","source":"# Migrate widget content\n\nA migration updates saved widget markup when an author pastes it, drops it, or inserts a snippet. It can change old attributes or elements and keep the authored content. This lets authors use content from earlier [package versions](https://docs.npmjs.com/about-semantic-versioning/).\n\n\n\n<!-- live-example: Migrate an old glossary -->\n\n```js vanilla\ndocument.addEventListener(\"migrate\", event => {\n const widget = event.target\n if(widget.localName !== \"ww-glossary\" || !widget.hasAttribute(\"lang-code\")) return\n if(!widget.hasAttribute(\"term-lang\")) {\n widget.setAttribute(\"term-lang\", widget.getAttribute(\"lang-code\"))\n }\n widget.removeAttribute(\"lang-code\")\n})\n\nclass GlossaryWidget extends HTMLElement {\n constructor() {\n super()\n this.attachShadow({mode: \"open\"}).innerHTML = `\n <slot></slot>\n <button type=\"button\">Migrate content</button>`\n this.shadowRoot.querySelector(\"button\").addEventListener(\"click\", () => {\n this.dispatchEvent(new Event(\"migrate\", {bubbles: true}))\n })\n }\n}\n\ncustomElements.define(\"ww-glossary\", GlossaryWidget)\n```\n\n```html\n<ww-glossary lang-code=\"nl\">\n <p><strong>fiets</strong> — a bicycle</p>\n</ww-glossary>\n```\n\n<!-- /live-example -->\n\nExport `./migrate.js` to supply a migration. The migration runs in a sandbox outside the live document.\n\nFor example, a glossary widget can change the name of its language attribute and keep the authored definition.\n\n```js\ndocument.addEventListener(\"migrate\", event => {\n const widget = event.target\n if(widget.localName !== \"ww-glossary\" || !widget.hasAttribute(\"lang-code\")) return\n if(!widget.hasAttribute(\"term-lang\")) {\n widget.setAttribute(\"term-lang\", widget.getAttribute(\"lang-code\"))\n }\n widget.removeAttribute(\"lang-code\")\n})\n```\n\nThis example changes one attribute and keeps all other content. On a second run, the old attribute is absent and the migration makes no changes.\n\n[`event.detail`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail) contains `{packageName, version}` for the installed package. Examine the input element to identify its format. Complete the migration within three seconds. Use only data that is available in the sandbox.\n\nTest old, current, and unusual valid HTML. Keep unrelated attributes, namespaces, child nodes, and comments. See the [package manifest reference](../reference/distribution.md).\n\n## Package versions and migration rules\n\n## Tests for existing content\n"},{"id":"guides/optimize-code-bundle","source":"# Optimize the code bundle\n\nFor instructions on lazy imports and offline export checks, see [Manage dependencies](libraries.md#optimize-the-code-bundle).\n\n<!-- live-example-link: libraries.md -->\n"},{"id":"guides/printing","source":"# Support printing\n\nA widget can show a [print view](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Media_queries/Printing) that keeps important information easy to read on paper. It can show content that normally needs input and hide controls that require a screen. It can also give readers a way to complete an activity on paper.\n\n\n\nSelect **Preview print view** to swap the screen input for a line for handwritten notes.\n\n<!-- live-example: Print friendly notes -->\n\n```js vanilla\nclass PrintNotes extends HTMLElement {\n static observedAttributes = [\"data-print-preview\"]\n\n constructor() {\n super()\n this.attachShadow({mode: \"open\"}).innerHTML = `\n <style>\n :host { display: block; }\n section { padding: .75rem; border: 1px solid #b8c7d6; border-radius: .5rem; }\n .preview-controls { margin-block-end: .6rem; }\n .paper-notes { display: none; min-block-size: 3rem; border-block-end: 1px solid currentColor; }\n :host([data-print-preview]) .entry-controls { display: none; }\n :host([data-print-preview]) .paper-notes { display: block; }\n @media print {\n .entry-controls, .preview-controls { display: none; }\n .paper-notes { display: block; }\n }\n </style>\n <section>\n <div class=\"preview-controls\"><button type=\"button\" aria-pressed=\"false\">Preview print view</button></div>\n <slot></slot>\n <label class=\"entry-controls\">Observation <input type=\"text\"></label>\n <div class=\"paper-notes\">Observation:</div>\n </section>`\n this.shadowRoot.querySelector(\"button\").addEventListener(\"click\", () => {\n this.toggleAttribute(\"data-print-preview\")\n })\n }\n\n attributeChangedCallback() {\n const button = this.shadowRoot.querySelector(\"button\")\n button.setAttribute(\"aria-pressed\", String(this.hasAttribute(\"data-print-preview\")))\n }\n}\n\ncustomElements.define(\"ww-print-notes\", PrintNotes)\n```\n\n```html\n<ww-print-notes>\n <strong>Trail observation log</strong>\n <p>Record one detail from the walk.</p>\n</ww-print-notes>\n```\n\n<!-- /live-example -->\n\n```css\n/* In the widget's shadow styles */\n.paper-notes { display: none; }\n\n@media print {\n .entry-controls { display: none; }\n .paper-notes {\n display: block;\n min-block-size: 3rem;\n border-block-end: 1px solid currentColor;\n }\n}\n```\n\nAdd a `.paper-notes` element in the shadow DOM. Give it a label such as “Observations.” If the print view needs a script, listen for [`beforeprint`](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeprint_event) and [`afterprint`](https://developer.mozilla.org/en-US/docs/Web/API/Window/afterprint_event) on `window`. Remove the listeners when the widget disconnects.\n\nUse print preview. Check the text, borders, and page breaks. If the widget has no print view, show a short explanation on paper.\n\n## Content and controls on paper\n\n## Page breaks and print checks\n"},{"id":"guides/publishing","source":"# Publish a widget\n\nPublication makes a widget available for installation in an authoring tool. The browser-ready custom element can also run on another site. An embedded widget can save reader results in the browser or at a configured endpoint.\n\n\n\nThis field journal uses the same custom element tag in a document or on a host page. Switch the preview to **Reading** and write a note to update its data block. Use the standalone preview later in this guide to try saving across page loads.\n\n<!-- live-example: Embedded field journal -->\n\n```js vanilla\nclass FieldJournal extends HTMLElement {\n constructor() {\n super()\n const root = this.attachShadow({mode: \"open\"})\n root.innerHTML = `\n <style>:host([contenteditable]) textarea { display: none; }\n :host(:not([contenteditable])) p { display: none; }</style>\n <label>Field note <textarea></textarea></label>\n <p>Switch to Reading to write a note.</p>\n `\n root.querySelector(\"textarea\").addEventListener(\"input\", event => {\n if(this.isContentEditable) return\n const block = this.querySelector('script[slot=\"data\"]')\n if(block) block.textContent = JSON.stringify({note: event.target.value}).replaceAll(\"<\", \"\\\\u003c\")\n })\n }\n\n #observer = new MutationObserver(() => this.render())\n\n connectedCallback() {\n this.#observer.observe(this, {subtree: true, childList: true, characterData: true})\n this.render()\n }\n\n disconnectedCallback() { this.#observer.disconnect() }\n\n render() {\n const block = this.querySelector('script[slot=\"data\"]')\n const field = this.shadowRoot.querySelector(\"textarea\")\n try { field.value = JSON.parse(block?.textContent ?? \"{}\").note ?? \"\" }\n catch { field.value = \"\" }\n }\n}\n\ncustomElements.define(\"ww-field-journal\", FieldJournal)\n```\n\n```html\n<ww-field-journal id=\"garden-notes\" standalone>\n <script type=\"application/json\" slot=\"data\">{\"note\":\"Watch the first crocus open.\"}</script>\n</ww-field-journal>\n```\n\n<!-- /live-example -->\n\n## Package and publish\n\nBefore publication, load the widget from a local folder. Do the [widget checks](testing.md).\n\n1. Set a [scoped package name](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages/), a SemVer version, and the `webwriter-widget` keyword.\n2. Export `./widgets/<tag-name>.js` or `./widgets/<tag-name>.*`. The script must define the same tag.\n3. If the source requires compilation, run `npx @webwriter/build`.\n4. Run [`npm pack --dry-run`](https://docs.npmjs.com/cli/v11/commands/npm-pack/).\n5. Make sure that the package includes all scripts, styles, files, and configuration that the widget uses.\n6. Declare the runtime dependencies in the package manifest.\n7. Sign in to npm.\n8. Run [`npm publish --access public`](https://docs.npmjs.com/cli/v11/commands/npm-publish/).\n\nAn origami viewer, for example, can export `./widgets/ww-origami-viewer.*` and include its fold diagrams in the package. Add a short `README.md`, a description, author information, and a license. After publication, find the widget in the authoring tool. Insert the widget into a new document.\n\nFor each update, increase the package version. Add a [migration](migrations.md) if the saved content must change. See the [package manifest reference](../reference/distribution.md) for the export format and discovery keywords.\n\n## Embed on another site\n\nLoad the widget's browser-ready module on the host page. Insert the custom element. In this example, a field journal stores a note in a JSON data block. The `WidgetDrawer` can save the note in the browser or send it to an HTTP endpoint. Give each embedded widget a stable, unique `id`. Browser storage uses this ID to find the result when the page loads again.\n\nBundle the widget's dependencies into browser-ready modules. If the module uses bare imports such as `@webwriter/lit`, define them in an [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap). Load the import map before the widget. Use a fixed package version in the embed.\n\n```html\n<script type=\"module\" src=\"./widgets/ww-field-journal.js\"></script>\n<ww-field-journal id=\"garden-notes\" standalone>\n <script type=\"application/json\" slot=\"data\">{\"note\":\"Watch the first crocus open.\"}</script>\n</ww-field-journal>\n```\n\nThe widget uses the data methods from `LitElementWw`. It registers the drawer in its scoped registry.\n\n```js\nimport {LitElementWw, WidgetDrawer} from \"@webwriter/lit\"\nimport {html} from \"lit\"\n\nclass WwFieldJournal extends LitElementWw {\n static scopedElements = {\"ww-widget-drawer\": WidgetDrawer}\n static properties = {note: {state: true}}\n\n constructor() {\n super()\n this.note = \"\"\n }\n\n dataChanged(rawText) {\n try { this.note = JSON.parse(rawText || \"{}\").note || \"\" }\n catch { this.note = \"\" }\n }\n\n save(event) {\n if(!this.dataReady || this.editable) return\n const data = JSON.parse(this.data || \"{}\")\n this.data = JSON.stringify({...data, note: event.target.value})\n }\n\n render() {\n return html`\n <label for=\"note\">Field note</label>\n <textarea id=\"note\" .value=${this.note} @input=${this.save}></textarea>\n <ww-widget-drawer .widget=${this}></ww-widget-drawer>`\n }\n}\n\ncustomElements.define(\"ww-field-journal\", WwFieldJournal)\n```\n\nWrite a note in the standalone preview. Reload that page. Make sure that the browser restores the note.\n\n[Open the standalone preview](../examples/preview.html?guide=publishing) · [View the example source](../examples/guide-publishing.js)\n\nAn empty `standalone` value selects browser storage. To use a remote destination, set `standalone` to the endpoint URL. When the reader changes destinations, the drawer restores the result for the new destination. The saved result stays in its original destination. See [standalone storage](../reference/standalone.md#http-endpoint) for the endpoint requirements.\n\nBrowser storage applies to the current browser profile. Use an authenticated server if the same reader needs access to results on different devices.\n\n## Package checks before release\n\n## Storage destinations for embedded widgets\n"},{"id":"guides/reactivity","source":"# Make data reactive\n\nA widget can publish read-only signals for other widgets in the same document. Authors can select a source and save the connection in the document. The receiving widget can change its display or operation when the value changes.\n\n\n\nChange the source color. Select **Disconnected** to stop updates, then select **Palette color** to reconnect to the current value.\n\n<!-- live-example: Reactive color swatch -->\n\n```js vanilla\nimport {computed, LitElementWw, signal} from \"@webwriter/lit\"\nimport {html} from \"lit\"\n\nclass ColorSource extends HTMLElement {\n #color = signal(\"#4263a8\")\n signals = {color: computed(() => this.#color.get())}\n\n constructor() {\n super()\n const root = this.attachShadow({mode: \"open\"})\n root.innerHTML = '<label>Source color <input type=\"color\" value=\"#4263a8\"></label>'\n root.querySelector(\"input\").addEventListener(\"input\", event => {\n this.#color.set(event.currentTarget.value)\n })\n }\n}\n\nclass LinkedSwatch extends LitElementWw {\n #source = signal(null)\n\n static effects = {\n \"color-from\": {\n label: \"Swatch color\",\n connect(source, lifetime) {\n this.#source.set(source)\n lifetime.addEventListener(\"abort\", () => this.#source.set(null), {once: true})\n },\n },\n }\n\n render() {\n const color = this.#source.get()?.get()\n const connected = this.hasAttribute(\"color-from\")\n return html`\n <label>Swatch source\n <select @change=${event => {\n const value = event.currentTarget.value\n if(value) this.setAttribute(\"color-from\", value)\n else this.removeAttribute(\"color-from\")\n }}>\n <option value=\"#palette[color]\" ?selected=${connected}>Palette color</option>\n <option value=\"\" ?selected=${!connected}>Disconnected</option>\n </select>\n </label>\n <output style=${color ? `color:${color}` : \"\"}>${color ?? \"Swatch disconnected\"}</output>`\n }\n}\n\ncustomElements.define(\"ww-color-source\", ColorSource)\ncustomElements.define(\"ww-linked-swatch\", LinkedSwatch)\n\n// The preview connects the published signal using the saved source ID.\nconst connections = new Map()\nlet editing = document.getElementById(\"preview\")?.isContentEditable === true\nfunction updateConnections() {\n if(editing) return\n const receivers = new Set(document.querySelectorAll(\"ww-linked-swatch\"))\n for(const [widget, {source, lifetime}] of connections) {\n const id = /^#(.+)\\[color\\]$/.exec(widget.getAttribute(\"color-from\"))?.[1]\n if(receivers.has(widget) && document.getElementById(id) === source) continue\n lifetime.abort()\n connections.delete(widget)\n }\n for(const widget of receivers) {\n widget.requestUpdate()\n if(connections.has(widget)) continue\n const id = /^#(.+)\\[color\\]$/.exec(widget.getAttribute(\"color-from\"))?.[1]\n const source = id && document.getElementById(id)\n if(!source?.signals?.color) continue\n const lifetime = new AbortController()\n LinkedSwatch.effects[\"color-from\"].connect.call(widget, source.signals.color, lifetime.signal)\n connections.set(widget, {source, lifetime})\n }\n}\n\nnew MutationObserver(updateConnections).observe(document.body, {\n childList: true, subtree: true, attributes: true, attributeFilter: [\"color-from\", \"id\"],\n})\ndocument.addEventListener(\"live-example-editing\", event => {\n editing = event.detail\n for(const {lifetime} of connections.values()) lifetime.abort()\n connections.clear()\n updateConnections()\n})\nupdateConnections()\n```\n\n```html\n<ww-color-source id=\"palette\"></ww-color-source>\n<ww-linked-swatch color-from=\"#palette[color]\"></ww-linked-swatch>\n```\n\n<!-- /live-example -->\n\n## Publish a value\n\nA color picker can store its current color in a private signal. It can publish a stable, read-only [computed signal](https://lit.dev/docs/data/signals/) named `color`. Initialize both signals once for each widget instance.\n\n```js\nimport {computed, signal} from \"@webwriter/lit\"\n\nclass WwColorSource extends HTMLElement {\n #color = signal(\"#4263a8\")\n signals = {color: computed(() => this.#color.get())}\n\n set color(value) { this.#color.set(value) }\n}\n```\n\nCall the setter from the [color input](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/color)'s `input` handler. For separate builds, use compatible dependencies and [shared dependency builds](libraries.md). Both widgets must use the same Signals implementation.\n\n## Connect a receiver\n\nDeclare a `static effects` entry for the receiving attribute. Store the incoming signal. Clear the reference when the authoring tool ends the connection. Read both signals in `LitElementWw.render()` to detect changes to the connection and color.\n\n```js\nimport {LitElementWw, signal} from \"@webwriter/lit\"\nimport {html} from \"lit\"\n\nclass WwLinkedSwatch extends LitElementWw {\n #source = signal(null)\n\n static effects = {\n \"color-from\": {\n label: \"Swatch color\",\n connect(source, lifetime) {\n this.#source.set(source)\n lifetime.addEventListener(\"abort\", () => this.#source.set(null), {once: true})\n },\n },\n }\n\n render() {\n const color = this.#source.get()?.get()\n return html`<output>${color ?? \"Swatch disconnected\"}</output>`\n }\n}\n```\n\n[Register both custom elements](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define). Connect them in the authored HTML. In `#palette[color]`, `palette` is the source widget's unique ID. The public signal name is `color`.\n\n```html\n<ww-color-source id=\"palette\"></ww-color-source>\n<ww-linked-swatch color-from=\"#palette[color]\"></ww-linked-swatch>\n```\n\nSelect the swatch in the authoring tool. Select the color picker in **Swatch color** in the Widget drawer. To remove the link, select **Disconnected**.\n\nChange the color in the picker. Make sure that the swatch changes. Disconnect the swatch. Change the color again. Reconnect the swatch. Make sure that it shows the current color.\n\nMake sure that the widget shows a fallback value when the source is missing. The authoring tool restores valid connections when their sources become available. Circular connections stay disconnected. See the [Signals reference](../reference/runtime.md#signals).\n\n## Signal names and dependencies\n\n## Missing sources and circular connections\n"},{"id":"guides/responsiveness","source":"# Make layouts responsive\n\nA responsive widget changes its layout to fit the available space. It can move content into one column when the space is narrow or use more columns when space permits. Its controls stay available when the content scrolls.\n\n\n\n<!-- live-example: Responsive card -->\n\n```js vanilla\nclass ResponsiveCard extends HTMLElement {\n constructor() {\n super()\n this.attachShadow({mode: \"open\"}).innerHTML = `\n <style>\n :host { display: block; max-inline-size: 100%; container-type: inline-size; }\n :host([compact]) { inline-size: 18rem; }\n .card { display: grid; gap: .5rem; padding: .75rem; border: 1px solid #b8c7d6; border-radius: .5rem; }\n .items { display: grid; gap: .5rem; }\n article { padding: .6rem; border-radius: .3rem; background: #eef5f8; }\n button { justify-self: start; }\n @container (min-width: 28rem) {\n .items { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n }\n </style>\n <section class=\"card\">\n <button type=\"button\">Toggle width</button>\n <div class=\"items\">\n <article><strong>Today</strong><br>Sort the seed packets</article>\n <article><strong>Tomorrow</strong><br>Label the garden beds</article>\n </div>\n </section>`\n this.shadowRoot.querySelector(\"button\").addEventListener(\"click\", () =>\n this.toggleAttribute(\"compact\"))\n }\n\n connectedCallback() {\n this.toggleAttribute(\"compact\", this.hasAttribute(\"compact\"))\n }\n}\n\ncustomElements.define(\"ww-responsive-card\", ResponsiveCard)\n```\n\n```html\n<ww-responsive-card></ww-responsive-card>\n```\n\n<!-- /live-example -->\n\nDesign block widgets for widths from **300px to 2100px**. Set [`min-height`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/min-height) to the minimum height for access to the controls. Set `max-height` to limit the height. Let the content set the height within these limits. Use scrolling to give readers access to content outside the view.\n\nUse these host styles for a block widget. Change the height limits to suit the content.\n\n```css\n:host {\n display: block;\n box-sizing: border-box;\n min-width: 300px;\n max-width: 2100px;\n min-height: 12rem;\n max-height: 36rem;\n}\n```\n\nUse a [container query](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Containment/Container_queries) to add columns when more space is available. A weather display can show current conditions and the forecast side by side in a wide container. It can put them in one column in a narrow container. Apply the query to a descendant in the shadow DOM. Use [`minmax(0, 1fr)`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/minmax) for flexible columns. Let long text wrap.\n\nTest at 300px, 2100px, and intermediate widths. Test nested widgets. Test different browser zoom levels. Make sure that controls stay readable without horizontal scrolling. Check access to the content at the maximum widget height.\n\n## Layout at different widths\n\n## Overflow and nested widgets\n"},{"id":"guides/sharing","source":"# Share participant data\n\nA widget can keep separate data for each participant or share data with a group or everyone. In a managed session, participants in the same scope can see one another's updates. Authors can select a scope for individual or group work.\n\n\n\nChoose **Shared** and **Reading**, then add to the count from either participant. Turn off **Share with everyone** in Editing mode to give each reader a separate count.\n\n<!-- live-example: Participant data scopes -->\n\n```js vanilla\nclass SharedCounter extends HTMLElement {\n static observedAttributes = [\"shared\", \"contenteditable\"]\n #observer = new MutationObserver(() => this.render())\n\n constructor() {\n super()\n const root = this.attachShadow({mode: \"open\"})\n root.innerHTML = `\n <label><input type=\"checkbox\"> Share with everyone</label>\n <button type=\"button\">Add one</button>\n <output aria-live=\"polite\"></output>\n `\n root.querySelector(\"input\").addEventListener(\"change\", event => {\n this.toggleAttribute(\"shared\", event.currentTarget.checked)\n })\n root.querySelector(\"button\").addEventListener(\"click\", () => {\n const block = this.dataBlock\n if(block) block.textContent = JSON.stringify({count: this.count + 1})\n })\n }\n\n connectedCallback() {\n this.#observer.observe(this, {childList: true, characterData: true, subtree: true})\n this.render()\n }\n\n disconnectedCallback() { this.#observer.disconnect() }\n attributeChangedCallback() { this.render() }\n\n get dataBlock() { return this.querySelector(':scope > script[type=\"application/json\"][slot=\"data\"]') }\n\n get count() {\n try {\n const {count} = JSON.parse(this.dataBlock?.textContent ?? \"{}\")\n return Number.isSafeInteger(count) && count >= 0 ? count : 0\n } catch { return 0 }\n }\n\n render() {\n const root = this.shadowRoot\n const shared = this.hasAttribute(\"shared\")\n root.querySelector(\"label\").hidden = !this.isContentEditable\n root.querySelector(\"input\").checked = shared\n root.querySelector(\"button\").disabled = this.isContentEditable\n root.querySelector(\"output\").textContent = `${this.count} · ${shared ? \"shared\" : \"private\"}`\n }\n}\n\ncustomElements.define(\"ww-shared-counter\", SharedCounter)\n```\n\n```html shared\n<ww-shared-counter id=\"tally\" shared>\n <script type=\"application/json\" slot=\"data\">{\"count\":0}</script>\n</ww-shared-counter>\n```\n\n<!-- /live-example -->\n\nSet `sharedData: true` in the [editing configuration](../reference/editing-configuration.md). Add one direct [data block](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#embedding_data_in_html) to the authored HTML. A snippet can supply this block.\n\n<!-- library:vanilla -->\nListen for `datapending` and `datachange` on the widget. When the widget connects or replaces the direct data block, dispatch a [bubbling, cancelable](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) `datarequest` on the current block. The runtime reports the current pending, ready, or error state.\n\n`datapending` also occurs when the scope starts to change. `datachange` shows that the accepted data is ready. Make sure that each event is for the current direct data block. Wait for `datachange` before you write. An early write can be lost.\n\n```js\nconst block = this.querySelector(':scope > script[slot=\"data\"]')\nif(block) {\n const state = JSON.parse(block.textContent)\n const count = typeof state.count === \"number\" ? state.count : 0\n block.textContent = JSON.stringify({...state, count: count + 1})\n}\n```\n\nWrite from an interaction after the data is ready. Use `datachange` to show accepted local and remote changes. Keep the stored data unchanged when you render it. Use `dataerror` to show an error message.\n\n<!-- /library -->\n<!-- library:lit -->\nUse the `dataPending`, `dataChanged`, and `dataError` methods from `LitElementWw` to handle data updates. Wait for `this.dataReady` before an interaction writes through `this.data`. The base class handles data requests and scope changes. The complete example shows the three methods.\n<!-- /library -->\n\n| Attribute | Scope |\n|---|---|\n| No `shared` | The current participant. |\n| `shared` or `shared=\"\"` | Everyone. |\n| `shared=\"group\"` | The participant's server-assigned group. |\n\nWhen the scope changes, the widget gets the saved data for that scope or the authored defaults. The **Share** control shows group options when the server supplies a participant list. Keep any direct `ww-grouping` processing instruction when you change child nodes.\n\nThe [data reference](../reference/data.md) describes the data interface. The [tutorial](../tutorial/04-state.md#share-participant-data) gives a complete example.\n\n<!-- library:lit optional -->\n## Handler methods with LitElementWw\n\n`LitElementWw` requests the current state and reports when the data is ready. Implement the data handlers. Use `data` to write changes from an interaction.\n\n```js\nimport {LitElementWw} from \"@webwriter/lit\"\nimport {html} from \"lit\"\n\nclass WwCounter extends LitElementWw {\n static properties = {count: {state: true}, error: {state: true}}\n\n constructor() {\n super()\n this.count = 0\n this.error = \"\"\n }\n\n dataPending(context) { this.error = \"\" }\n\n dataChanged(rawText, context) {\n const value = JSON.parse(rawText || \"{}\")\n this.count = typeof value.count === \"number\" ? value.count : 0\n this.error = \"\"\n }\n\n dataError(error, context) { this.error = error.message }\n\n increment() {\n if(!this.dataReady || this.editable) return\n const value = JSON.parse(this.data || \"{}\")\n const count = typeof value.count === \"number\" ? value.count : 0\n this.data = JSON.stringify({...value, count: count + 1})\n }\n\n render() {\n return html`\n <button ?disabled=${!this.dataReady || this.editable} @click=${this.increment}>\n ${this.count}\n </button>\n <output>${this.error}</output>`\n }\n}\n\ncustomElements.define(\"ww-counter\", WwCounter)\n```\n\nExport `./widgets/ww-counter`. Set `sharedData: true` in its editing configuration. Specify the initial values in the authored HTML.\n\n```html\n<ww-counter shared>\n <script type=\"application/json\" slot=\"data\">{\"count\":0}</script>\n</ww-counter>\n```\n\nConcurrent writes to `count` give one assigned value. In a standalone document with no managed data runtime, the widget uses local authored data. The second argument to a data handler is `{block, mode}`.\n<!-- /library -->\n\n## Selection of a sharing scope\n\n## Data readiness and error handling\n"},{"id":"guides/state","source":"# Manage state\n\nA widget can keep temporary interaction state in memory and save author settings with the document. It can store participant results separately. Authors can use declared options and actions to make changes that the authoring tool can undo.\n\n\n\n<!-- live-example: Metronome settings -->\n\n```js vanilla\nclass GuideMetronome extends HTMLElement {\n static options = {tempo: {type: Number, label: {_: \"Tempo\"}, min: 40, max: 240}}\n static actions = {resetTempo: {label: {_: \"Reset tempo\"}}}\n static observedAttributes = [\"tempo\", \"sound\"]\n\n constructor() {\n super()\n this.beat = 0\n this.attachShadow({mode: \"open\"}).innerHTML = `\n <label>Tempo <input type=\"range\" min=\"40\" max=\"240\" value=\"96\"></label>\n <button type=\"button\">Next beat</button>\n <button type=\"button\">Reset tempo</button>\n <button type=\"button\" aria-expanded=\"false\">Sound details</button>\n <output aria-live=\"polite\"></output>\n <p hidden>Selected sound: <span></span></p>`\n this.tempoInput = this.shadowRoot.querySelector('input[type=\"range\"]')\n this.tempoInput.addEventListener(\"input\", () => this.setAttribute(\"tempo\", this.tempoInput.value))\n this.shadowRoot.querySelector(\"button\").addEventListener(\"click\", () => {\n this.beat = (this.beat + 1) % 4\n this.render()\n })\n this.shadowRoot.querySelectorAll(\"button\")[1].addEventListener(\"click\", () => this.resetTempo())\n this.shadowRoot.querySelectorAll(\"button\")[2].addEventListener(\"click\", event => {\n const details = this.shadowRoot.querySelector(\"p\")\n details.hidden = !details.hidden\n event.currentTarget.setAttribute(\"aria-expanded\", String(!details.hidden))\n })\n this.syncSettings()\n }\n\n resetTempo() { this.setAttribute(\"tempo\", \"120\") }\n attributeChangedCallback() { this.syncSettings() }\n\n syncSettings() {\n this.tempoInput.value = this.tempo\n this.shadowRoot.querySelector(\"p span\").textContent = this.sound\n this.render()\n }\n\n get tempo() { return Number(this.getAttribute(\"tempo\") ?? 120) }\n set tempo(value) { this.setAttribute(\"tempo\", value) }\n get sound() { return this.getAttribute(\"sound\") ?? \"woodblock\" }\n\n render() {\n this.shadowRoot.querySelector(\"output\").textContent =\n `${this.tempo} BPM · Beat ${this.beat + 1} of 4`\n }\n}\n\ncustomElements.define(\"ww-guide-metronome\", GuideMetronome)\n```\n\n```html\n<ww-guide-metronome tempo=\"96\" sound=\"woodblock\"></ww-guide-metronome>\n```\n\n<!-- /live-example -->\n\nUse properties for temporary state. Use attributes for settings that the authoring tool must save, share, and undo. For example, a metronome can save its selected sound in an attribute. It can keep the visibility of sound details in memory.\n\n<!-- library:vanilla -->\n```js\nget sound() { return this.getAttribute(\"sound\") ?? \"woodblock\" }\nset sound(value) { this.setAttribute(\"sound\", value) }\n```\n\n[Observe the attribute](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#responding_to_attribute_changes) to update the display when a collaborator or undo changes its value.\n\n```js\nstatic observedAttributes = [\"sound\"]\n\nattributeChangedCallback(name, previous, current) {\n if(previous !== current) this.update()\n}\n```\n<!-- /library -->\n<!-- library:lit -->\nDeclare a [reactive property](https://lit.dev/docs/components/properties/) in the `LitElementWw` class. Reflect changes to the property in an attribute.\n\n```js\nstatic properties = {sound: {type: String, reflect: true}}\n\nconstructor() {\n super()\n this.sound = \"woodblock\"\n}\n```\n\nLit updates the template after an attribute change. This includes changes from collaboration and undo. Use `{state: true}` for local reactive properties that have no attribute.\n<!-- /library -->\n\n<!-- library:react -->\nKeep saved settings on the custom element host. The bridge reads the `sound` attribute and passes the value to the React component after each change. This includes changes from collaboration and undo. Use [`useState`](https://react.dev/reference/react/useState) to store the visibility of the sound details.\n\n```jsx\nfunction Metronome({sound = \"woodblock\"}) {\n const [showDetails, setShowDetails] = useState(false)\n return <>\n <button aria-expanded={showDetails} onClick={() => setShowDetails(!showDetails)}>\n Sound details\n </button>\n {showDetails && <p>Selected sound: {sound}</p>}\n </>\n}\n```\n\nThe [tutorial state step](../tutorial/04-state.md) shows how React reads shared participant data.\n<!-- /library -->\n\n<!-- library:vue -->\nKeep saved settings on the custom element host. The registration bridge reads the `sound` attribute. It sends changes from collaboration and undo to the single-file component. Use a [Vue ref](https://vuejs.org/api/reactivity-core.html#ref) to store the visibility of the sound details.\n\n```vue\n<script setup>\nimport {ref} from \"vue\"\ndefineProps({sound: {type: String, default: \"woodblock\"}})\nconst showDetails = ref(false)\n</script>\n<template>\n <button :aria-expanded=\"showDetails\" @click=\"showDetails = !showDetails\">Sound details</button>\n <p v-if=\"showDetails\">Selected sound: {{ sound }}</p>\n</template>\n```\n\nThe [tutorial state step](../tutorial/04-state.md) has a Vue component and a registration module.\n<!-- /library -->\n\n<!-- library:angular -->\nDeclare the saved setting as an [Angular input with a decorator](https://angular.dev/guide/components/inputs#declaring-inputs-with-the-input-decorator). Angular Elements maps the host's `sound` attribute to this input. Attribute changes from collaboration or undo update the component. Store the visibility of the sound details in the component.\n\n```ts\n@Input() sound = \"woodblock\"\nshowDetails = false\n\ntoggleDetails() {\n this.showDetails = !this.showDetails\n}\n```\n\nThe [tutorial state step](../tutorial/04-state.md) shows how the Angular widget adapter reads shared data.\n<!-- /library -->\n\n<!-- library:svelte -->\nDeclare the saved setting as a custom-element prop that reflects its value to an attribute. Use [local Svelte state](https://svelte.dev/docs/svelte/$state) for the visibility of sound details.\n\n```svelte\n<svelte:options customElement={{\n tag: \"ww-metronome\",\n props: {sound: {type: \"String\", reflect: true}},\n}} />\n<script>\n let {sound = \"woodblock\"} = $props()\n let showDetails = $state(false)\n</script>\n```\n\nChanges to the attribute update the component prop. This includes changes from collaboration and undo. The [tutorial state step](../tutorial/04-state.md) has a complete Svelte example.\n<!-- /library -->\n\nUse the host attribute for changes to a saved setting. This applies to changes in the widget and in the authoring tool.\n\nKeep authored text in the light DOM. Use a [scoped data block](sharing.md) for persistent participant data. Normal exports retain the authored defaults from the block.\n\nSave the document. Open it again. Make sure that the widget restores its settings from the DOM.\n\n## Options, actions, and undo\n\nAn option lets the author change a saved setting. An action is an operation that the authoring tool can start. Declare both in static metadata on the custom element class. Keep the setting in an attribute so the authoring tool can record changes. A property getter can supply the default value until the author changes the setting.\n\nThe metronome saves `tempo` as an attribute. The play state and beat count stay in temporary properties. The option sets the `tempo` attribute. The action resets the same attribute. The authoring tool can undo or redo these attribute changes.\n\n```js\nstatic options = {\n tempo: {\n type: \"number\",\n label: {_: \"Tempo\"},\n min: 40,\n max: 240,\n step: 1,\n },\n}\n\nstatic actions = {\n resetTempo: {label: {_: \"Reset tempo\"}},\n}\n\nget tempo() { return Number(this.getAttribute(\"tempo\") ?? 120) }\nset tempo(value) { this.setAttribute(\"tempo\", String(value)) }\n\nresetTempo() { this.tempo = 120 }\n```\n\nChange **Tempo** in the preview and look for the new attribute in the HTML pane. Select **Next beat** to change the temporary beat count. Select **Reset tempo** to restore 120 BPM. In an authoring tool, the Widget drawer can use the same option and action declarations.\n\nSee the [options and actions reference](../reference/options-actions.md).\n\n## State in memory and in the document\n\n## Default values for settings\n"},{"id":"guides/testing","source":"# Test and debug\n\nPackage tests can check widget content and interactions in a document. The authoring tool can also show the widget's HTML and package warnings. These checks help identify problems before publication.\n\n\n\nSelect **Run checks** to test the conversion and the result after restoring the original unit. Change a conversion factor in the source to see a check fail.\n\n<!-- live-example: Unit converter checks -->\n\n```js vanilla\nclass UnitConverter extends HTMLElement {\n static observedAttributes = [\"unit\"]\n #factors = {cm: 100, mm: 1000}\n\n constructor() {\n super()\n const root = this.attachShadow({mode: \"open\"})\n root.innerHTML = `\n <p>1 m = <output></output></p>\n <button type=\"button\">Run checks</button>\n <p role=\"status\"></p>\n `\n root.querySelector(\"button\").addEventListener(\"click\", () => this.runChecks())\n }\n\n connectedCallback() { this.render() }\n attributeChangedCallback() { this.render() }\n\n render() {\n const unit = this.getAttribute(\"unit\") || \"cm\"\n const value = this.#factors[unit]\n this.shadowRoot.querySelector(\"output\").value = value ? `${value} ${unit}` : \"Unknown unit\"\n }\n\n runChecks() {\n const original = this.getAttribute(\"unit\")\n const output = this.shadowRoot.querySelector(\"output\")\n const originalValue = output.value\n this.setAttribute(\"unit\", \"mm\")\n const conversionPassed = output.value === \"1000 mm\"\n if(original === null) this.removeAttribute(\"unit\")\n else this.setAttribute(\"unit\", original)\n const restorePassed = output.value === originalValue\n this.shadowRoot.querySelector('[role=\"status\"]').textContent =\n !conversionPassed ? \"Fail: expected 1000 mm.\"\n : !restorePassed ? \"Fail: the original value did not return.\"\n : \"2 checks passed.\"\n }\n}\n\ncustomElements.define(\"ww-unit-converter\", UnitConverter)\n```\n\n```html\n<ww-unit-converter unit=\"cm\"></ww-unit-converter>\n```\n\n<!-- /live-example -->\n\nOpen the **Developer console** to examine package warnings, the selected element, and document HTML. Refresh the package when you change its built files. For automatic refresh, enable **Auto-reload**.\n\nDo these checks for each widget.\n\n- Save the document. Open it again. Make sure that the widget keeps the settings and authored content.\n- Change an attribute from outside the widget. Check the display.\n- Use undo and redo. Check the authored content.\n- Test keyboard input, narrow layouts, preview, and printing.\n- Wait for shared data before you write to it. Check that updates from other participants appear.\n\nExport test modules as `./tests/<name>.js` or `./tests/<name>.*`. Run the modules from **Tests** in the **Developer console**. Each module runs in a separate frame with a time limit of 60 seconds.\n\nTo test a unit converter, change the saved target unit. Check that the widget calculates the new output. Use undo. Check that the widget restores the previous output. The [Ghost Disco test module](../examples/ghost-disco-tests.js) has a test without a test-runner dependency. For [Mocha](https://mochajs.org/getting-started/), `getMochaConfig()` from `@webwriter/build/test` supplies the reporting hooks.\n\nThe [test reporting reference](../reference/build-test.md#test-reporting) gives details for other test runners.\n\n## Widget test cases\n\n## Checks after saving and opening\n"},{"id":"guides/theming","source":"# Follow the theme\n\nA widget can use the document's font, text color, and accent colors. A document stylesheet can use named parts to style specified elements inside the widget. Fallback values keep the text easy to read when a theme value is absent.\n\n\n\n<!-- live-example: Themeable garden timeline -->\n\n```js vanilla\nclass GardenTimeline extends HTMLElement {\n constructor() {\n super()\n this.attachShadow({mode: \"open\"}).innerHTML = `\n <style>\n :host { display: block; color: var(--ww-text-color, inherit); font-family: var(--ww-font-family, inherit); }\n section { display: grid; justify-items: start; gap: .6rem; padding: .75rem; border: 1px solid currentColor; border-radius: .5rem; }\n ol { display: grid; gap: .45rem; margin: 0; padding: 0; list-style: none; }\n li { display: flex; align-items: center; gap: .5rem; }\n .marker { inline-size: .8rem; block-size: .8rem; border: 1px solid currentColor; border-radius: 50%; background: var(--ww-theme-accent-1, currentColor); }\n li:nth-child(2) .marker { background: var(--ww-theme-accent-2, currentColor); }\n li:nth-child(3) .marker { background: var(--ww-theme-accent-3, currentColor); }\n li:last-child .marker { background: var(--ww-theme-accent-4, currentColor); }\n </style>\n <section>\n <strong>Community garden</strong>\n <ol>\n <li><span class=\"marker\" part=\"opening-marker\"></span>Plan the garden beds</li>\n <li><span class=\"marker\"></span>Plant the seedlings</li>\n <li><span class=\"marker\"></span>Open the garden</li>\n </ol>\n <button type=\"button\">Change document theme</button>\n </section>`\n this.shadowRoot.querySelector(\"button\").addEventListener(\"click\", () => {\n const documentTheme = this.parentElement\n const forest = documentTheme.dataset.theme !== \"forest\"\n documentTheme.dataset.theme = forest ? \"forest\" : \"ocean\"\n const theme = forest\n ? [\"#315d42\", \"#4f875e\", \"#8eb16a\", \"#c4a550\", \"Georgia, serif\"]\n : [\"#23486a\", \"#397da2\", \"#56a58e\", \"#d38a4d\", \"Arial, sans-serif\"]\n for(const [index, value] of theme.slice(0, 4).entries()) {\n documentTheme.style.setProperty(`--ww-theme-accent-${index + 1}`, value)\n }\n documentTheme.style.setProperty(\"--ww-text-color\", theme[0])\n documentTheme.style.setProperty(\"--ww-font-family\", theme[4])\n })\n }\n}\n\ncustomElements.define(\"ww-garden-timeline\", GardenTimeline)\n```\n\n```html\n<section style=\"\n --ww-text-color: #23486a;\n --ww-font-family: Arial, sans-serif;\n --ww-theme-accent-1: #397da2;\n --ww-theme-accent-2: #56a58e;\n --ww-theme-accent-3: #d38a4d;\n --ww-theme-accent-4: #23486a\n\">\n <ww-garden-timeline></ww-garden-timeline>\n</section>\n```\n\n<!-- /live-example -->\n\n## Inherit the theme\n\nLet text [inherit the font and color](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Cascade/Inheritance) from the widget host. Use a [CSS custom property](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Cascading_variables/Using_custom_properties) when a style needs a specific theme value. Give the property a fallback value. The first timeline marker uses the first accent color. If that color is absent, it uses the current text color.\n\n```css\n/* Inside the timeline's shadow stylesheet */\n:host {\n display: block;\n color: var(--ww-text-color, inherit);\n font-family: var(--ww-font-family, inherit);\n}\n\n.marker {\n background: var(--ww-theme-accent-1, currentColor);\n}\n```\n\nA document theme can set `--ww-font-family`, `--ww-font-size`, `--ww-text-color`, and `--ww-link-color`. It can also set `--ww-theme-accent-1` through `--ww-theme-accent-7`. Let the document set these properties. If text uses an accent color, check its contrast in each supported theme.\n\n## Expose a named part\n\nGive the first milestone marker a [part name](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/part) in the shadow DOM. A document stylesheet can [use the part name](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::part) to add an outline.\n\n```html\n<span class=\"marker\" part=\"opening-marker\" aria-hidden=\"true\"></span>\n```\n\n```css\n/* In the document stylesheet */\nww-garden-timeline::part(opening-marker) {\n outline: 2px solid currentColor;\n outline-offset: 2px;\n}\n```\n\nSelect **Change document theme** in the preview to switch the font and colors. Remove the theme properties from the HTML pane to see the fallback styles.\n\n[Design tokens reference](../reference/styling.md)\n\n## Document theme properties and fallback values\n\n## Theme checks for named parts\n"},{"id":"index","source":"# Overview: Widgets\n\nWidgets add interactive content to a web document. They are [Web Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components). An [npm package](https://docs.npmjs.com/about-packages-and-modules/) can contain one or more widgets.\n\nThis documentation has multiple sections: The [Library](introduction/library.md) page describes the six choices in the library selector. The [Quick start](introduction/quick-start.md) shows how to add a widget to a document. The [Tutorial](tutorial/index.md) shows how to make a complete widget. Both include source code and a live preview for each library. The [Guides](guides/index.md) give instructions for specific widget features. The [Reference](reference/index.md) describes the package interface, runtime interface, and tools. The [Sandbox](sandbox.md) lets you edit and run a basic widget.\n\n## What is a widget?\n\nA widget is an HTML [custom element](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements). Its class can extend [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) directly or use a base class from a library such as [Lit](https://lit.dev/docs/components/defining/). A widget can use the web APIs that the browser supplies.\n\n<!-- live-example: Weather -->\n\n```js vanilla\nclass WeatherWidget extends HTMLElement {\n static observedAttributes = [\"contenteditable\", \"fixed\"]\n static forecasts = {\n Utrecht: \"12 °C · Cloudy\",\n Amsterdam: \"11 °C · Rainy\",\n Rotterdam: \"13 °C · Windy\",\n }\n\n constructor() {\n super()\n const root = this.attachShadow({mode: \"open\"})\n const style = document.createElement(\"style\")\n style.textContent = `\n :host { display: block; }\n .forecast {\n user-select: none;\n display: grid;\n gap: .35rem;\n padding: .65rem;\n border: 1px solid #b8d4df;\n border-radius: .5rem;\n background: #f2f9fc;\n }\n .heading, .report {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: .35rem;\n }\n slot { font-weight: bold; }\n p { margin: 0; }\n label, button { font-size: .8125rem; }\n `\n const panel = document.createElement(\"div\")\n panel.className = \"forecast\"\n const heading = document.createElement(\"div\")\n heading.className = \"heading\"\n this.fixedLabel = document.createElement(\"label\")\n this.fixedLabel.hidden = true\n this.fixedInput = document.createElement(\"input\")\n this.fixedInput.type = \"checkbox\"\n this.fixedInput.addEventListener(\"change\", () => {\n this.toggleAttribute(\"fixed\", this.fixedInput.checked)\n })\n this.fixedLabel.append(this.fixedInput, \"Fixed\")\n heading.append(document.createElement(\"slot\"), this.fixedLabel)\n\n const report = document.createElement(\"div\")\n report.className = \"report\"\n this.forecast = document.createElement(\"p\")\n this.nextCity = document.createElement(\"button\")\n this.nextCity.type = \"button\"\n this.nextCity.textContent = \"Next city\"\n this.nextCity.addEventListener(\"click\", () => {\n const cities = Object.keys(WeatherWidget.forecasts)\n const data = this.readData()\n const current = cities.indexOf(data.place || \"Utrecht\")\n this.writeData({...data, place: cities[(current + 1) % cities.length]})\n })\n report.append(this.forecast, this.nextCity)\n panel.append(heading, report)\n root.append(style, panel)\n this.dataObserver = new MutationObserver(() => this.updateForecast())\n }\n\n connectedCallback() {\n this.dataObserver.observe(this, {\n childList: true,\n subtree: true,\n characterData: true,\n attributeFilter: [\"type\", \"slot\"],\n })\n this.fixedLabel.hidden = !this.isContentEditable\n this.updateForecast()\n }\n\n disconnectedCallback() {\n this.dataObserver.disconnect()\n }\n\n attributeChangedCallback() {\n this.fixedLabel.hidden = !this.isContentEditable\n this.fixedInput.checked = this.hasAttribute(\"fixed\")\n this.nextCity.disabled = this.fixedInput.checked\n }\n\n get dataElement() {\n return this.querySelector(':scope > script[type=\"application/json\"][slot=\"data\"]')\n }\n\n readData() {\n try {\n const data = JSON.parse(this.dataElement?.textContent ?? \"null\")\n return data && typeof data === \"object\" && !Array.isArray(data) ? data : {}\n } catch {\n return {}\n }\n }\n\n writeData(data) {\n const json = JSON.stringify(data).replaceAll(\"<\", \"\\\\u003c\")\n const existing = this.dataElement\n if(existing) {\n existing.textContent = json\n return\n }\n const script = document.createElement(\"script\")\n script.type = \"application/json\"\n script.slot = \"data\"\n script.textContent = json\n this.append(script)\n }\n\n updateForecast() {\n const value = this.readData().place\n const place = typeof value === \"string\" && value ? value : \"Utrecht\"\n const forecast = Object.hasOwn(WeatherWidget.forecasts, place)\n ? WeatherWidget.forecasts[place]\n : \"No sample forecast\"\n this.forecast.textContent = `${place} · ${forecast}`\n }\n}\n\ncustomElements.define(\"ww-weather\", WeatherWidget)\n```\n\n```js shared #weather-example weather-data.js\nconst selector = ':scope > script[type=\"application/json\"][slot=\"data\"]'\n\nexport function readWeatherData(host) {\n try {\n const data = JSON.parse(host.querySelector(selector)?.textContent ?? \"null\")\n return data && typeof data === \"object\" && !Array.isArray(data) ? data : {}\n } catch {\n return {}\n }\n}\n\nexport function writeWeatherData(host, data) {\n let block = host.querySelector(selector)\n if(!block) {\n block = document.createElement(\"script\")\n block.type = \"application/json\"\n block.slot = \"data\"\n host.append(block)\n }\n const json = JSON.stringify(data).replaceAll(\"<\", \"\\\\u003c\")\n block.textContent = json\n}\n\nexport function observeWeather(host, update) {\n const observer = new MutationObserver(update)\n observer.observe(host, {\n subtree: true,\n childList: true,\n characterData: true,\n attributeFilter: [\"contenteditable\", \"fixed\", \"slot\", \"type\"],\n })\n return () => observer.disconnect()\n}\n\nexport const weatherStyles = `\n:host { display: block; }\n.forecast {\n user-select: none;\n display: grid;\n gap: .35rem;\n padding: .65rem;\n border: 1px solid #b8d4df;\n border-radius: .5rem;\n background: #f2f9fc;\n}\n.heading, .report {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: .35rem;\n}\nslot { font-weight: bold; }\np { margin: 0; }\nlabel, button { font-size: .8125rem; }\n`\n```\n\n```js lit\nimport {LitElement, css, html} from \"lit\"\n\nclass WeatherWidget extends LitElement {\n static styles = css`\n :host { display: block; }\n .forecast {\n user-select: none;\n display: grid;\n gap: .35rem;\n padding: .65rem;\n border: 1px solid #b8d4df;\n border-radius: .5rem;\n background: #f2f9fc;\n }\n .heading, .report {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: .35rem;\n }\n slot { font-weight: bold; }\n p { margin: 0; }\n label, button { font-size: .8125rem; }\n `\n\n static forecasts = {\n Utrecht: \"12 °C · Cloudy\",\n Amsterdam: \"11 °C · Rainy\",\n Rotterdam: \"13 °C · Windy\",\n }\n\n dataObserver = new MutationObserver(() => this.requestUpdate())\n\n connectedCallback() {\n super.connectedCallback()\n this.dataObserver.observe(this, {\n childList: true,\n subtree: true,\n characterData: true,\n attributeFilter: [\"contenteditable\", \"fixed\", \"slot\", \"type\"],\n })\n this.requestUpdate()\n }\n\n disconnectedCallback() {\n this.dataObserver.disconnect()\n super.disconnectedCallback()\n }\n\n get dataElement() {\n return this.querySelector(':scope > script[type=\"application/json\"][slot=\"data\"]')\n }\n\n readData() {\n try {\n const data = JSON.parse(this.dataElement?.textContent ?? \"null\")\n return data && typeof data === \"object\" && !Array.isArray(data) ? data : {}\n } catch {\n return {}\n }\n }\n\n writeData(data) {\n const existing = this.dataElement\n const script = existing ?? document.createElement(\"script\")\n const json = JSON.stringify(data).replaceAll(\"<\", \"\\\\u003c\")\n script.textContent = json\n if(!existing) {\n script.type = \"application/json\"\n script.slot = \"data\"\n this.append(script)\n }\n }\n\n nextCity() {\n const data = this.readData()\n const cities = Object.keys(WeatherWidget.forecasts)\n const current = cities.indexOf(data.place || \"Utrecht\")\n this.writeData({...data, place: cities[(current + 1) % cities.length]})\n }\n\n render() {\n const {place: value} = this.readData()\n const place = typeof value === \"string\" && value ? value : \"Utrecht\"\n const forecasts = WeatherWidget.forecasts\n const forecast = Object.hasOwn(forecasts, place) ? forecasts[place] : \"No sample forecast\"\n const fixed = this.hasAttribute(\"fixed\")\n\n return html`\n <div class=\"forecast\">\n <div class=\"heading\">\n <slot></slot>\n <label ?hidden=${!this.isContentEditable}>\n <input\n type=\"checkbox\"\n .checked=${fixed}\n @change=${event => this.toggleAttribute(\"fixed\", event.target.checked)}\n >\n Fixed\n </label>\n </div>\n <div class=\"report\">\n <p>${place} · ${forecast}</p>\n <button type=\"button\" ?disabled=${fixed} @click=${this.nextCity}>Next city</button>\n </div>\n </div>\n `\n }\n}\n\ncustomElements.define(\"ww-weather\", WeatherWidget)\n```\n\n```js react\nimport {createElement as h} from \"react\"\nimport {flushSync} from \"react-dom\"\nimport {createRoot} from \"react-dom/client\"\nimport {\n observeWeather, readWeatherData, writeWeatherData, weatherStyles,\n} from \"#weather-example\"\n\nfunction Forecast({place, forecast, editing, fixed, fixedInput, nextCity}) {\n return h(\"div\", {className: \"forecast\"},\n h(\"div\", {className: \"heading\"},\n h(\"slot\"),\n h(\"label\", {hidden: !editing},\n h(\"input\", {\n type: \"checkbox\",\n defaultChecked: fixed,\n ref: fixedInput,\n }),\n \"Fixed\",\n ),\n ),\n h(\"div\", {className: \"report\"},\n h(\"p\", null, `${place} · ${forecast}`),\n h(\"button\", {type: \"button\", disabled: fixed, onClick: nextCity}, \"Next city\"),\n ),\n )\n}\n\nclass WeatherWidget extends HTMLElement {\n static forecasts = {\n Utrecht: \"12 °C · Cloudy\",\n Amsterdam: \"11 °C · Rainy\",\n Rotterdam: \"13 °C · Windy\",\n }\n\n constructor() {\n super()\n const shadow = this.attachShadow({mode: \"open\"})\n const style = document.createElement(\"style\")\n style.textContent = weatherStyles\n this.mount = document.createElement(\"div\")\n shadow.append(style, this.mount)\n }\n\n connectedCallback() {\n this.root = createRoot(this.mount)\n this.stopObserving = observeWeather(this, () => this.render())\n flushSync(() => this.render())\n }\n\n disconnectedCallback() {\n this.stopObserving()\n this.root.unmount()\n }\n\n nextCity() {\n const data = readWeatherData(this)\n const cities = Object.keys(WeatherWidget.forecasts)\n const current = cities.indexOf(data.place || \"Utrecht\")\n writeWeatherData(this, {...data, place: cities[(current + 1) % cities.length]})\n }\n\n render() {\n const {place: value} = readWeatherData(this)\n const place = typeof value === \"string\" && value ? value : \"Utrecht\"\n const forecasts = WeatherWidget.forecasts\n const forecast = Object.hasOwn(forecasts, place) ? forecasts[place] : \"No sample forecast\"\n\n this.root.render(h(Forecast, {\n place,\n forecast,\n editing: this.isContentEditable,\n fixed: this.hasAttribute(\"fixed\"),\n fixedInput: input => {\n if(!input) return\n input.checked = this.hasAttribute(\"fixed\")\n input.onchange = () => this.toggleAttribute(\"fixed\", input.checked)\n },\n nextCity: () => this.nextCity(),\n }))\n }\n}\n\ncustomElements.define(\"ww-weather\", WeatherWidget)\n```\n\n```vue vue\n<script setup>\nimport {computed, onMounted, onBeforeUnmount, ref, useHost} from \"vue\"\nimport {readWeatherData, writeWeatherData, observeWeather, weatherStyles} from \"#weather-example\"\n\nconst host = useHost()\nconst forecasts = {\n Utrecht: \"12 °C · Cloudy\",\n Amsterdam: \"11 °C · Rainy\",\n Rotterdam: \"13 °C · Windy\",\n}\nconst cities = Object.keys(forecasts)\nconst data = ref(readWeatherData(host))\nconst fixed = ref(host.hasAttribute(\"fixed\"))\nconst editing = ref(host.isContentEditable)\nconst place = computed(() => data.value.place || \"Utrecht\")\nconst forecast = computed(() => Object.hasOwn(forecasts, place.value) ? forecasts[place.value] : \"No sample forecast\")\n\nlet stopObserving\nlet styleElement\nonMounted(() => {\n styleElement = document.createElement(\"style\")\n styleElement.textContent = weatherStyles\n host.shadowRoot.append(styleElement)\n stopObserving = observeWeather(host, () => {\n data.value = readWeatherData(host)\n fixed.value = host.hasAttribute(\"fixed\")\n editing.value = host.isContentEditable\n })\n})\nonBeforeUnmount(() => {\n stopObserving?.()\n styleElement?.remove()\n})\n\nfunction nextCity() {\n if(fixed.value) return\n const index = cities.indexOf(place.value)\n writeWeatherData(host, {...readWeatherData(host), place: cities[(index + 1) % cities.length]})\n}\n</script>\n\n<template>\n <section class=\"forecast\">\n <div class=\"heading\">\n <slot></slot>\n <label v-if=\"editing\"><input type=\"checkbox\" :checked=\"fixed\" @change=\"host.toggleAttribute('fixed', $event.target.checked); fixed = $event.target.checked\"> Fixed</label>\n </div>\n <div class=\"report\">\n <p>{{ place }} · {{ forecast }}</p>\n <button type=\"button\" :disabled=\"fixed\" @click=\"nextCity\">Next city</button>\n </div>\n </section>\n</template>\n```\n\n```js vue-registration\nimport {defineCustomElement} from \"vue\"\nimport WeatherWidget from \"./WeatherWidget.vue\"\n\ncustomElements.define(\"ww-weather\", defineCustomElement(WeatherWidget))\n```\n\n```js angular\nimport \"@angular/compiler\"\nimport {\n ChangeDetectorRef,\n Component,\n ElementRef,\n inject,\n ViewEncapsulation,\n} from \"@angular/core\"\nimport {createApplication} from \"@angular/platform-browser\"\nimport {createCustomElement} from \"@angular/elements\"\nimport {\n observeWeather,\n readWeatherData,\n weatherStyles,\n writeWeatherData,\n} from \"#weather-example\"\n\nclass WwWeather {\n static forecasts = {\n Utrecht: \"12 °C · Cloudy\",\n Amsterdam: \"11 °C · Rainy\",\n Rotterdam: \"13 °C · Windy\",\n }\n\n host = inject(ElementRef).nativeElement\n changeDetector = inject(ChangeDetectorRef)\n report = \"\"\n stopObserving\n\n ngOnInit() {\n this.stopObserving = observeWeather(this.host, () => {\n this.updateReport()\n this.changeDetector.detectChanges()\n })\n this.updateReport()\n }\n\n ngOnDestroy() {\n this.stopObserving?.()\n }\n\n setFixed(event) {\n const checkbox = event.currentTarget\n this.host.toggleAttribute(\"fixed\", checkbox.checked)\n }\n\n nextCity() {\n const cities = Object.keys(WwWeather.forecasts)\n const data = readWeatherData(this.host)\n const current = cities.indexOf(data.place || \"Utrecht\")\n writeWeatherData(this.host, {\n ...data,\n place: cities[(current + 1) % cities.length],\n })\n this.updateReport()\n }\n\n updateReport() {\n const data = readWeatherData(this.host)\n const value = data.place\n const place = typeof value === \"string\" && value ? value : \"Utrecht\"\n const forecast = Object.hasOwn(WwWeather.forecasts, place)\n ? WwWeather.forecasts[place]\n : \"No sample forecast\"\n this.report = `${place} · ${forecast}`\n }\n}\n\nComponent({\n selector: \"ww-weather-view\",\n standalone: true,\n template: `\n <div class=\"forecast\">\n <div class=\"heading\">\n <slot></slot>\n <label [hidden]=\"!host.isContentEditable\">\n <input\n type=\"checkbox\"\n [checked]=\"host.hasAttribute('fixed')\"\n (change)=\"setFixed($event)\"\n >\n Fixed\n </label>\n </div>\n <div class=\"report\">\n <p aria-live=\"polite\">{{ report }}</p>\n <button type=\"button\" [disabled]=\"host.hasAttribute('fixed')\" (click)=\"nextCity()\">\n Next city\n </button>\n </div>\n </div>\n `,\n styles: [weatherStyles],\n encapsulation: ViewEncapsulation.ShadowDom,\n})(WwWeather)\n\nasync function register() {\n if(customElements.get(\"ww-weather\")) return\n\n const app = await createApplication()\n const AngularWeatherElement = createCustomElement(WwWeather, {\n injector: app.injector,\n })\n\n class WwWeatherElement extends AngularWeatherElement {\n connectedCallback() {\n const version = this.getAttribute(\"ng-version\")\n super.connectedCallback()\n // Keep Angular's runtime stamp out of the authored HTML.\n if(version === null) this.removeAttribute(\"ng-version\")\n else this.setAttribute(\"ng-version\", version)\n }\n }\n\n customElements.define(\"ww-weather\", WwWeatherElement)\n}\n\nawait register()\n```\n\n```svelte svelte\n<svelte:options customElement={{tag: \"ww-weather\"}} />\n\n<script>\n import {onMount} from \"svelte\"\n import {readWeatherData, writeWeatherData, observeWeather, weatherStyles} from \"#weather-example\"\n\n const host = $host()\n const forecasts = {\n Utrecht: \"12 °C · Cloudy\",\n Amsterdam: \"11 °C · Rainy\",\n Rotterdam: \"13 °C · Windy\",\n }\n const cities = Object.keys(forecasts)\n let data = $state(readWeatherData(host))\n let fixed = $state(host.hasAttribute(\"fixed\"))\n let editing = $state(host.isContentEditable)\n let place = $derived(data.place || \"Utrecht\")\n let forecast = $derived(Object.hasOwn(forecasts, place) ? forecasts[place] : \"No sample forecast\")\n\n onMount(() => {\n const style = document.createElement(\"style\")\n style.textContent = weatherStyles\n host.shadowRoot.append(style)\n const stopObserving = observeWeather(host, () => {\n data = readWeatherData(host)\n fixed = host.hasAttribute(\"fixed\")\n editing = host.isContentEditable\n })\n return () => {\n stopObserving()\n style.remove()\n }\n })\n\n function nextCity() {\n if(fixed) return\n const index = cities.indexOf(place)\n writeWeatherData(host, {...readWeatherData(host), place: cities[(index + 1) % cities.length]})\n }\n\n function setFixed(event) {\n host.toggleAttribute(\"fixed\", event.currentTarget.checked)\n fixed = event.currentTarget.checked\n }\n</script>\n\n<section class=\"forecast\">\n <div class=\"heading\">\n <slot></slot>\n {#if editing}\n <label><input type=\"checkbox\" checked={fixed} onchange={setFixed} /> Fixed</label>\n {/if}\n </div>\n <div class=\"report\">\n <p>{place} · {forecast}</p>\n <button type=\"button\" disabled={fixed} onclick={nextCity}>Next city</button>\n </div>\n</section>\n```\n\n```html\n<ww-weather>\n <p>Schoolyard forecast</p>\n <script type=\"application/json\" slot=\"data\">{\"place\":\"Utrecht\"}</script>\n</ww-weather>\n```\n\n<!-- /live-example -->\n\nThe tag name identifies the widget. A [JSON data block](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#embedding_data_in_html) stores the city in `place`. Click **Next city** in the preview to change the forecast and update `place` in the Document HTML pane. In editing mode, select **Fixed** to set the `fixed` [attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes) and keep the current city. The widget uses a [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) for its user interface. The [slot](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/slot) displays the authored paragraph.\n\nA widget package can also include HTML snippets, style sheets, and tests. The authoring tool uses the package to find and load the widgets.\n\nThe HTML document holds each widget and its saved settings. Widgets and authors can change this HTML. The authoring tool can undo or redo a change.\n\n### What widgets can do\n\n<nav class=\"widget-capabilities\" aria-labelledby=\"what-widgets-can-do\">\n <div class=\"widget-capabilities-map\">\n <div class=\"widget-capabilities-core\">\n <div class=\"widget-capabilities-document\">\n <strong class=\"widget-capabilities-document-title\">HTML document</strong>\n <div class=\"widget-capabilities-lines\" aria-hidden=\"true\"></div>\n <div class=\"widget-capabilities-example\">\n <div class=\"widget-capabilities-example-title\"><strong>Widget</strong><code><ww-weather></code></div>\n <div class=\"widget-capabilities-example-output\" aria-hidden=\"true\">\n <strong>Schoolyard forecast</strong>\n <span>Utrecht · 12 °C · Cloudy</span>\n </div>\n </div>\n <div class=\"widget-capabilities-lines\" aria-hidden=\"true\"></div>\n </div>\n </div>\n <div class=\"widget-capabilities-rail widget-capabilities-left\">\n <h4>Authoring</h4>\n <ul>\n <li>\n <a href=\"guides/editing.md\" aria-describedby=\"widget-guide-editing\">\n <svg aria-hidden=\"true\" width=\"32\" height=\"32\"><use href=\"../images/widgets/capability-icons.svg#editing\"></use></svg>\n <template><img src=\"../images/widgets/editing.svg\" alt=\"\"></template>\n <span>Support editing</span>\n </a>\n <p id=\"widget-guide-editing\" class=\"widget-capabilities-summary\">A widget can show different controls to authors and readers.</p>\n </li>\n <li>\n <a href=\"guides/focus.md\" aria-describedby=\"widget-guide-focus\">\n <svg aria-hidden=\"true\" width=\"32\" height=\"32\"><use href=\"../images/widgets/capability-icons.svg#focus\"></use></svg>\n <template><img src=\"../images/widgets/focus.svg\" alt=\"\"></template>\n <span>Handle selection & input</span>\n </a>\n <p id=\"widget-guide-focus\" class=\"widget-capabilities-summary\">A widget can move focus to a control and handle input from a keyboard.</p>\n </li>\n <li>\n <a href=\"guides/content.md\" aria-describedby=\"widget-guide-content\">\n <svg aria-hidden=\"true\" width=\"32\" height=\"32\"><use href=\"../images/widgets/capability-icons.svg#content\"></use></svg>\n <template><img src=\"../images/widgets/content.svg\" alt=\"\"></template>\n <span>Nest content</span>\n </a>\n <p id=\"widget-guide-content\" class=\"widget-capabilities-summary\">Slots can show editable text and other widgets from the light DOM.</p>\n </li>\n <li>\n <a href=\"guides/conversions.md\" aria-describedby=\"widget-guide-conversions\">\n <svg aria-hidden=\"true\" width=\"32\" height=\"32\"><use href=\"../images/widgets/capability-icons.svg#conversions\"></use></svg>\n <template><img src=\"../images/widgets/conversions.svg\" alt=\"\"></template>\n <span>Convert & import/export</span>\n </a>\n <p id=\"widget-guide-conversions\" class=\"widget-capabilities-summary\">A widget can convert content to and from HTML or a file.</p>\n </li>\n </ul>\n <h4>Presentation</h4>\n <ul>\n <li>\n <a href=\"guides/responsiveness.md\" aria-describedby=\"widget-guide-responsiveness\">\n <svg aria-hidden=\"true\" width=\"32\" height=\"32\"><use href=\"../images/widgets/capability-icons.svg#responsiveness\"></use></svg>\n <template><img src=\"../images/widgets/responsiveness.svg\" alt=\"\"></template>\n <span>Make layouts responsive</span>\n </a>\n <p id=\"widget-guide-responsiveness\" class=\"widget-capabilities-summary\">A widget can change its layout to fit the available space.</p>\n </li>\n <li>\n <a href=\"guides/theming.md\" aria-describedby=\"widget-guide-theming\">\n <svg aria-hidden=\"true\" width=\"32\" height=\"32\"><use href=\"../images/widgets/capability-icons.svg#theming\"></use></svg>\n <template><img src=\"../images/widgets/theming.svg\" alt=\"\"></template>\n <span>Follow the theme</span>\n </a>\n <p id=\"widget-guide-theming\" class=\"widget-capabilities-summary\">A widget can use the fonts and colors from the document theme.</p>\n </li>\n <li>\n <a href=\"guides/localization.md\" aria-describedby=\"widget-guide-localization\">\n <svg aria-hidden=\"true\" width=\"32\" height=\"32\"><use href=\"../images/widgets/capability-icons.svg#localization\"></use></svg>\n <template><img src=\"../images/widgets/localization.svg\" alt=\"\"></template>\n <span>Localize a widget</span>\n </a>\n <p id=\"widget-guide-localization\" class=\"widget-capabilities-summary\">A widget can show its text in the document language.</p>\n </li>\n <li>\n <a href=\"guides/printing.md\" aria-describedby=\"widget-guide-printing\">\n <svg aria-hidden=\"true\" width=\"32\" height=\"32\"><use href=\"../images/widgets/capability-icons.svg#printing\"></use></svg>\n <template><img src=\"../images/widgets/printing.svg\" alt=\"\"></template>\n <span>Support printing</span>\n </a>\n <p id=\"widget-guide-printing\" class=\"widget-capabilities-summary\">Print styles can show the widget content on paper or in a PDF.</p>\n </li>\n </ul>\n </div>\n <div class=\"widget-capabilities-rail widget-capabilities-right\">\n <h4>State and participation</h4>\n <ul>\n <li>\n <a href=\"guides/state.md\" aria-describedby=\"widget-guide-state\">\n <svg aria-hidden=\"true\" width=\"32\" height=\"32\"><use href=\"../images/widgets/capability-icons.svg#state\"></use></svg>\n <template><img src=\"../images/widgets/state.svg\" alt=\"\"></template>\n <span>Manage state</span>\n </a>\n <p id=\"widget-guide-state\" class=\"widget-capabilities-summary\">A widget can store its state and supply options and actions to the authoring tool.</p>\n </li>\n <li>\n <a href=\"guides/sharing.md\" aria-describedby=\"widget-guide-sharing\">\n <svg aria-hidden=\"true\" width=\"32\" height=\"32\"><use href=\"../images/widgets/capability-icons.svg#sharing\"></use></svg>\n <template><img src=\"../images/widgets/sharing.svg\" alt=\"\"></template>\n <span>Share participant data</span>\n </a>\n <p id=\"widget-guide-sharing\" class=\"widget-capabilities-summary\">A widget can store data for one participant, a group, or all participants.</p>\n </li>\n <li>\n <a href=\"guides/live-sessions.md\" aria-describedby=\"widget-guide-live-sessions\">\n <svg aria-hidden=\"true\" width=\"32\" height=\"32\"><use href=\"../images/widgets/capability-icons.svg#live-sessions\"></use></svg>\n <template><img src=\"../images/widgets/live-sessions.svg\" alt=\"\"></template>\n <span>Support live sessions</span>\n </a>\n <p id=\"widget-guide-live-sessions\" class=\"widget-capabilities-summary\">A widget can give participants controls and show their responses to the host.</p>\n </li>\n <li>\n <a href=\"guides/reactivity.md\" aria-describedby=\"widget-guide-reactivity\">\n <svg aria-hidden=\"true\" width=\"32\" height=\"32\"><use href=\"../images/widgets/capability-icons.svg#reactivity\"></use></svg>\n <template><img src=\"../images/widgets/reactivity.svg\" alt=\"\"></template>\n <span>Make data reactive</span>\n </a>\n <p id=\"widget-guide-reactivity\" class=\"widget-capabilities-summary\">A signal can update one widget when a value changes in another widget.</p>\n </li>\n </ul>\n <h4>Build and distribution</h4>\n <ul>\n <li>\n <a href=\"guides/libraries.md\" aria-describedby=\"widget-guide-libraries\">\n <svg aria-hidden=\"true\" width=\"32\" height=\"32\"><use href=\"../images/widgets/capability-icons.svg#libraries\"></use></svg>\n <template><img src=\"../images/widgets/libraries.svg\" alt=\"\"></template>\n <span>Manage dependencies</span>\n </a>\n <p id=\"widget-guide-libraries\" class=\"widget-capabilities-summary\">A widget can load libraries and assets as necessary. An offline document can include only the code it needs.</p>\n </li>\n <li>\n <a href=\"guides/testing.md\" aria-describedby=\"widget-guide-testing\">\n <svg aria-hidden=\"true\" width=\"32\" height=\"32\"><use href=\"../images/widgets/capability-icons.svg#testing\"></use></svg>\n <template><img src=\"../images/widgets/testing.svg\" alt=\"\"></template>\n <span>Test and debug</span>\n </a>\n <p id=\"widget-guide-testing\" class=\"widget-capabilities-summary\">Package tests can help you find problems in a widget.</p>\n </li>\n <li>\n <a href=\"guides/migrations.md\" aria-describedby=\"widget-guide-migrations\">\n <svg aria-hidden=\"true\" width=\"32\" height=\"32\"><use href=\"../images/widgets/capability-icons.svg#migrations\"></use></svg>\n <template><img src=\"../images/widgets/migrations.svg\" alt=\"\"></template>\n <span>Migrate widget content</span>\n </a>\n <p id=\"widget-guide-migrations\" class=\"widget-capabilities-summary\">A migration can update widget HTML from an earlier version and keep the document content.</p>\n </li>\n <li>\n <a href=\"guides/publishing.md\" aria-describedby=\"widget-guide-publishing\">\n <svg aria-hidden=\"true\" width=\"32\" height=\"32\"><use href=\"../images/widgets/capability-icons.svg#publishing\"></use></svg>\n <template><img src=\"../images/widgets/publishing.svg\" alt=\"\"></template>\n <span>Publish a widget</span>\n </a>\n <p id=\"widget-guide-publishing\" class=\"widget-capabilities-summary\">You can publish a widget package or embed the widget on a website.</p>\n </li>\n </ul>\n </div>\n </div>\n <div class=\"widget-capabilities-preview\" popover=\"manual\" hidden aria-hidden=\"true\"></div>\n</nav>\n"},{"id":"introduction/library","source":"# Library\n\nPlain JavaScript and Lit can define a widget directly. You can use any library that can define a custom element with the necessary interface. A Web Component adapter can connect a library's user interface to a custom element.\n\nThe widget package must supply a [JavaScript module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) that the browser can load. This module must register the custom element.\n\nThe library selector controls the examples on other pages. This page shows the final Ghost Disco source for all six libraries. Each version reads dance moves with the same [data helper](../examples/disco/reading.js). The [build step](../tutorial/05-build.md) shows how to package the files.\n\n<!-- library-card:vanilla -->\n\nThe [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide) language runs in web browsers. JavaScript can use native browser APIs to define and register a custom element. It does not need a UI library.\n\nThe Ghost Disco sample defines and registers the widget in one file. It uses browser APIs to create the interface and `@webwriter/widget` to read and write participant data.\n\n**`04-disco-sharing.js`**\n\n<!-- script:../examples/disco/04-disco-sharing.js?library=vanilla -->\n\n<!-- /library-card -->\n\n<!-- library-card:lit -->\n\nThe [Lit](https://lit.dev/docs/) library helps you build Web Components. It uses HTML templates to describe a component's interface. Reactive properties update the interface when their values change.\n\nThe Ghost Disco sample extends `LitElementWw`. The base class updates the view when the participant data changes.\n\n**`04-disco-sharing.js`**\n\n<!-- script:../examples/disco/lit/04-disco-sharing.js -->\n\n<!-- /library-card -->\n\n<!-- library-card:react -->\n\n[React](https://react.dev/learn) is a library for building user interfaces. A React interface consists of components. Components can use JSX for markup and hooks for state.\n\nThe Ghost Disco sample uses `createReactWidget()` from `@webwriter/react` to mount the dance floor inside the widget's shadow root. Its `useWidget(host)` hook follows editing mode and shared participant data.\n\n**`04-disco-sharing.jsx`**\n\n<!-- script:../examples/disco/react/04-disco-sharing.jsx -->\n\n<!-- /library-card -->\n\n<!-- library-card:vue -->\n\n[Vue](https://vuejs.org/guide/extras/web-components.html) is a framework for building user interfaces. Its single-file components can keep templates, code, and styles together. Vue can also define custom elements.\n\nThe Ghost Disco sample uses `createVueWidget()` from `@webwriter/vue` in its registration module. The single-file component reads the widget's reactive state with `useWidget()`.\n\n**`04-disco-sharing.vue`**\n\n<!-- script:../examples/disco/vue/04-disco-sharing.vue -->\n\n**`04-disco-sharing.js`**\n\n<!-- script:../examples/disco/vue/04-disco-sharing.js -->\n\n<!-- /library-card -->\n\n<!-- library-card:angular -->\n\n[Angular](https://angular.dev/guide/elements) is a framework for building web applications. It uses TypeScript components and templates to define an interface. Angular Elements can expose an Angular component as a custom element.\n\nThe Ghost Disco sample uses `createAngularWidget()` from `@webwriter/angular` to mount its Angular component inside the widget's shadow root. `injectWidget()` gives the component an Angular signal for editing mode and shared participant data.\n\n**`04-disco-sharing.ts`**\n\n<!-- script:../examples/disco/angular/04-disco-sharing.ts -->\n\n<!-- /library-card -->\n\n<!-- library-card:svelte -->\n\n[Svelte](https://svelte.dev/docs/svelte/custom-elements) is a framework for user interfaces. A component file can contain markup, code, and styles. Svelte uses a compiler to turn the component into JavaScript.\n\nThe Ghost Disco sample extends Svelte's compiled custom element with `withWidgetRuntime()` from `@webwriter/svelte`. Inside the component, `widgetStore($host())` follows the authored DOM and live-session data.\n\n**`04-disco-sharing.svelte`**\n\n<!-- script:../examples/disco/svelte/04-disco-sharing.svelte -->\n\n<!-- /library-card -->\n\n## Use another Web Component library\n\nUse your library's custom element build or adapter if it supplies one. If the library renders into a DOM container, define an `HTMLElement` that mounts the component in its shadow root. Mount it when the element connects and release its listeners and resources when it disconnects. Register the element under a name with a hyphen, such as `ww-my-widget`. [Custom elements on MDN](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements)\n\nKeep document content in the element's light DOM and display it through a slot. Put controls and other interface elements in the shadow DOM. Reflect author settings to HTML attributes and update the interface when those attributes change. Read the element's editing state to decide which controls to show. [Runtime interface](../reference/runtime.md)\n\nBuild the source with your library's tools. Export the browser-ready module under `./widgets/ww-my-widget.js` in `package.json` and add the `webwriter-widget` keyword. The module must register the same element name as the export. Bundle the runtime or declare its dependencies so the authoring tool can load them. Check that the built module also runs on a plain HTML page. [Package manifest](../reference/distribution.md) · [Manage dependencies](../guides/libraries.md)\n\nUse the [quick start](quick-start.md) to load your first widget. Use the [Ghost Disco tutorial](../tutorial/index.md) to make a complete widget.\n\n## Widget runtime for React, Vue, Angular, and Svelte\n\nEach adapter exposes `host.widgetRuntime`. Add the authored attributes your view reads to `static widgetAttributes`. Read them in the reactive snapshot's `attributes` map and update them through the host's attributes or reflected properties. The snapshot also includes `editable`, inherited `lang`, and the current `data`, `dataReady`, and `dataError`.\n\nUse `host.widgetRuntime.setData(type, text)` from an interaction to update the widget's inert data block. Wait for `dataReady` before writing live-session data. Declare options and actions on the custom element so WebWriter can use them in its Widget drawer. The adapters export the shared TypeScript declaration types. [Data blocks](../reference/data.md) · [Options and actions](../reference/options-actions.md)\n"},{"id":"introduction/quick-start","source":"# Quick start\n\nDownload and install [Node.js LTS with npm](https://nodejs.org/en/download) before you start. You also need a text editor and a browser that can open local folders in WebWriter, such as Chrome or Edge.\n\n## Step 1: Set up the widget package\n\nMake a folder for the widget. Save these files in that folder.\n\n<!-- library:vanilla -->\n**`package.json`**\n\n```json package\n{\n \"name\": \"@webwriter/hello\",\n \"version\": \"1.0.0\",\n \"keywords\": [\"webwriter-widget\"],\n \"exports\": {\n \"./widgets/ww-hello.js\": {\"source\": \"./ww-hello.js\", \"default\": \"./dist/ww-hello.js\"}\n }\n}\n```\n\n**`ww-hello.js`**\n\n```js\nclass WwHello extends HTMLElement {\n constructor() {\n super()\n this.attachShadow({mode: \"open\"}).innerHTML = \"<p>Hello, <slot>world</slot>!</p>\"\n }\n}\n\nif(!customElements.get(\"ww-hello\")) customElements.define(\"ww-hello\", WwHello)\n```\n<!-- /library -->\n\n<!-- library:lit -->\n**`package.json`**\n\n```json package\n{\n \"name\": \"@webwriter/hello\",\n \"version\": \"1.0.0\",\n \"keywords\": [\"webwriter-widget\"],\n \"dependencies\": {\"@webwriter/lit\": \"^3.0.0\", \"lit\": \"^3.3.2\"},\n \"exports\": {\n \"./widgets/ww-hello.js\": {\"source\": \"./ww-hello.js\", \"default\": \"./dist/ww-hello.js\"}\n }\n}\n```\n\n**`ww-hello.js`**\n\n```js\nimport {LitElementWw} from \"@webwriter/lit\"\nimport {html} from \"lit\"\n\nclass WwHello extends LitElementWw {\n render() {\n return html`<p>Hello, <slot>world</slot>!</p>`\n }\n}\n\nif(!customElements.get(\"ww-hello\")) customElements.define(\"ww-hello\", WwHello)\n```\n<!-- /library -->\n\n<!-- library:react -->\n**`package.json`**\n\n```json package\n{\n \"name\": \"@webwriter/hello-react\",\n \"version\": \"1.0.0\",\n \"keywords\": [\n \"webwriter-widget\"\n ],\n \"exports\": {\n \"./widgets/ww-hello.js\": {\"source\": \"./hello.jsx\", \"default\": \"./dist/ww-hello.js\"}\n },\n \"dependencies\": {\n \"react\": \"^19.3.1\",\n \"react-dom\": \"^19.3.1\"\n }\n}\n```\n\n**`hello.jsx`**\n\n<!-- script:../examples/hello.js -->\n\nSave this file as `tsconfig.json`.\n\n```json file tsconfig.json\n{\"compilerOptions\": {\"jsx\": \"react-jsx\"}}\n```\n<!-- /library -->\n\n<!-- library:vue -->\n**`package.json`**\n\n```json package\n{\n \"name\": \"@webwriter/hello-vue\",\n \"version\": \"1.0.0\",\n \"keywords\": [\n \"webwriter-widget\"\n ],\n \"exports\": {\n \"./widgets/ww-hello.js\": {\"source\": \"./hello.js\", \"default\": \"./dist/ww-hello.js\"}\n },\n \"dependencies\": {\n \"vue\": \"^3.5.44\"\n },\n \"devDependencies\": {\"@vue/compiler-sfc\": \"^3.5.44\"}\n}\n```\n\n**`hello.vue`**\n\n<!-- script:../examples/hello.js -->\n\n**`hello.js`**\n\n```js file hello.js\nimport {defineCustomElement} from \"vue\"\nimport Hello from \"./hello-component.js\"\n\ncustomElements.define(\"ww-hello\", defineCustomElement(Hello))\n```\n\nSave this file as `compile.mjs`. It compiles the component to JavaScript.\n\n```js file compile.mjs\nimport {readFile, writeFile} from \"node:fs/promises\"\nimport {parse, compileTemplate} from \"@vue/compiler-sfc\"\n\nconst filename = \"hello.vue\"\nconst {descriptor, errors} = parse(await readFile(filename, \"utf8\"), {filename})\nif(errors.length) throw new Error(errors.join(\"\\n\"))\n\nconst template = compileTemplate({\n source: descriptor.template.content,\n filename,\n id: \"ww-hello\",\n})\nif(template.errors.length) throw new Error(template.errors.join(\"\\n\"))\n\nawait writeFile(\"hello-component.js\", template.code + \"\\nexport default {render}\\n\")\n```\n<!-- /library -->\n\n<!-- library:angular -->\n**`package.json`**\n\n```json package\n{\n \"name\": \"@webwriter/hello-angular\",\n \"version\": \"1.0.0\",\n \"keywords\": [\n \"webwriter-widget\"\n ],\n \"exports\": {\n \"./widgets/ww-hello.js\": {\"source\": \"./hello.ts\", \"default\": \"./dist/ww-hello.js\"}\n },\n \"dependencies\": {\n \"@angular/common\": \"^22.2.2\",\n \"@angular/compiler\": \"^22.2.2\",\n \"@angular/core\": \"^22.2.2\",\n \"@angular/elements\": \"^22.2.2\",\n \"@angular/platform-browser\": \"^22.2.2\",\n \"rxjs\": \"^7.8.2\"\n }\n}\n```\n\n**`hello.ts`**\n\n<!-- script:../examples/hello.js -->\n\nSave this file as `tsconfig.json`.\n\n```json file tsconfig.json\n{\"compilerOptions\": {\"experimentalDecorators\": true}}\n```\n<!-- /library -->\n\n<!-- library:svelte -->\n**`package.json`**\n\n```json package\n{\n \"name\": \"@webwriter/hello-svelte\",\n \"version\": \"1.0.0\",\n \"keywords\": [\n \"webwriter-widget\"\n ],\n \"exports\": {\n \"./widgets/ww-hello.js\": {\"source\": \"./hello-component.js\", \"default\": \"./dist/ww-hello.js\"}\n },\n \"dependencies\": {\n \"svelte\": \"^5.57.2\"\n }\n}\n```\n\n**`hello.svelte`**\n\n<!-- script:../examples/hello.js -->\n\nSave this file as `compile.mjs`. It compiles the component to JavaScript.\n\n```js file compile.mjs\nimport {readFile, writeFile} from \"node:fs/promises\"\nimport {compile} from \"svelte/compiler\"\n\nconst filename = \"hello.svelte\"\nconst source = await readFile(filename, \"utf8\")\nconst {js} = compile(source, {filename, generate: \"client\", customElement: true})\nawait writeFile(\"hello-component.js\", js.code)\n```\n<!-- /library -->\n\nOpen a terminal in the widget folder. Install the dependencies listed in `package.json`.\n\n```sh prompt\nnpm install\n```\n\n<!-- library:vue optional -->\nCompile the Vue template.\n\n```sh prompt\nnode compile.mjs\n```\n<!-- /library -->\n\n<!-- library:svelte optional -->\nCompile the Svelte component.\n\n```sh prompt\nnode compile.mjs\n```\n<!-- /library -->\n\nStart the development build. Wait for `dist/ww-hello.js` to appear. Keep the command running while you work.\n\n```sh prompt\nnpx @webwriter/build dev\n```\n\n## Step 2: Insert the widget\n\n1. Open the **Developer console** in WebWriter.\n2. Select **Add package**.\n3. Select **Add from local folder**.\n4. Select the widget folder.\n5. Insert **Hello** from the insertion menu.\n6. Make sure that the widget shows “Hello, world!”.\n\n## Step 3: Change the greeting\n\n1. In the widget's markup, change the greeting from `Hello,` to `Welcome,`.\n2. Save the file.\n\n<!-- library:vue optional -->\nIn a second terminal in the widget folder, compile `hello.vue` again. The development build watches the generated JavaScript.\n\n```sh prompt\nnode compile.mjs\n```\n<!-- /library -->\n\n<!-- library:svelte optional -->\nIn a second terminal in the widget folder, compile `hello.svelte` again. The development build watches the generated JavaScript.\n\n```sh prompt\nnode compile.mjs\n```\n<!-- /library -->\n\nWait for the development build to finish. With **Auto-reload** on, WebWriter loads the new widget code. You can also refresh the package manually.\n\nChange the greeting to `Welcome,` in the widget source, or put your name between the tags in the HTML pane. The preview updates as you type.\n\n<!-- live-example: Hello widget -->\n\n<!-- live-package: . -->\n\n<!-- live-build-files -->\n\n<!-- live-source: vanilla ../examples/hello.js -->\n\n<!-- live-source: lit ../examples/lit/hello.js -->\n\n<!-- live-source: react ../examples/react/hello.jsx -->\n\n<!-- live-source: vue ../examples/vue/hello.vue -->\n\n<!-- live-source: vue-registration ../examples/vue/hello.js -->\n\n<!-- live-source: angular ../examples/angular/hello.ts -->\n\n<!-- live-source: svelte ../examples/svelte/hello.svelte -->\n\n```html\n<ww-hello></ww-hello>\n```\n\n<!-- /live-example -->\n"},{"id":"reference/adapters","source":"<!-- library:vanilla -->\n# C2. @webwriter/widget\n\nA widget can use a native custom element. The optional `@webwriter/widget` package adds the shared runtime for observing the authored DOM and participating in live sessions.\n\n## `withWidgetRuntime(Base)`\n\nApply the mixin to your element class before registration. It adds `widgetRuntime` and observes the host attributes listed in `static widgetAttributes`.\n\n```js\nimport {withWidgetRuntime} from \"@webwriter/widget\"\n\nclass WwLabel extends withWidgetRuntime(HTMLElement) {\n static widgetAttributes = [\"label\"]\n\n #label = document.createElement(\"span\")\n #unsubscribe\n\n constructor() {\n super()\n this.attachShadow({mode: \"open\"}).append(document.createElement(\"slot\"), this.#label)\n }\n\n connectedCallback() {\n super.connectedCallback()\n const {getSnapshot, subscribe} = this.widgetRuntime\n const render = () => { this.#label.textContent = getSnapshot().attributes.label ?? \"\" }\n this.#unsubscribe = subscribe(render)\n render()\n }\n\n disconnectedCallback() {\n this.#unsubscribe?.()\n super.disconnectedCallback()\n }\n}\n\ncustomElements.define(\"ww-label\", WwLabel)\n```\n\n<!-- /library -->\n\n<!-- library:react -->\n# C2. @webwriter/react\n\n`@webwriter/react` mounts a React component in a widget's shadow root and exposes a hook for reading its authored DOM and live-session data. It supports React 19.\n\n## `createReactWidget(Component, shadowOptions?)`\n\nThe factory returns a custom element class. It passes the widget element to the React component as the `host` prop. Optional shadow root options such as `delegatesFocus` apply when the host is created.\n\nThe adapter keeps the React tree during a synchronous DOM move. It unmounts the tree if the host stays detached.\n\n## `useWidget(host)`\n\nRead the shared runtime snapshot in a React component. The hook subscribes to changes so the view updates when the editor, another participant, or the widget changes the DOM.\n\n```jsx\nimport {createReactWidget, useWidget} from \"@webwriter/react\"\n\nfunction Label({host}) {\n const {attributes} = useWidget(host)\n return <>\n <slot />\n <span>{attributes.label ?? \"\"}</span>\n </>\n}\n\nclass WwLabel extends createReactWidget(Label) {\n static widgetAttributes = [\"label\"]\n}\n\ncustomElements.define(\"ww-label\", WwLabel)\n```\n\nSee the [React Ghost Disco source](../examples/disco/react/08-disco-dependencies.jsx) for an interactive widget.\n\n<!-- /library -->\n\n<!-- library:vue -->\n# C2. @webwriter/vue\n\n`@webwriter/vue` wraps Vue's custom element API and provides a composable for following the authored DOM and live-session data. It supports Vue 3.5 and later within Vue 3.\n\n## `createVueWidget(component, options?)`\n\nThe factory returns a custom element class with an open shadow root. Pass a Vue component and any custom element options such as `styles` or `shadowRootOptions`. For single-file components, use your build tool's custom element mode to include the styles in the shadow root.\n\n**`ww-label.js`**\n\n```js\nimport {createVueWidget} from \"@webwriter/vue\"\nimport Label from \"./Label.ce.vue\"\n\nclass WwLabel extends createVueWidget(Label) {\n static widgetAttributes = [\"label\"]\n}\n\ncustomElements.define(\"ww-label\", WwLabel)\n```\n\n## `useWidget(host?)`\n\nCall the composable in component setup. It returns a readonly ref and uses Vue's current custom element host when you omit the argument. Vue disposes of the subscription with the component.\n\n**`Label.ce.vue`**\n\n```vue\n<script setup>\nimport {useWidget} from \"@webwriter/vue\"\n\nconst widget = useWidget()\n</script>\n\n<template>\n <slot></slot>\n <span>{{ widget.attributes.label ?? \"\" }}</span>\n</template>\n```\n\nSee the [Vue Ghost Disco source](../examples/disco/vue/08-disco-dependencies.vue) for an interactive widget.\n\n<!-- /library -->\n\n<!-- library:angular -->\n# C2. @webwriter/angular\n\n`@webwriter/angular` renders a standalone Angular component inside a widget's shadow root. It supports Angular 22.2 and later within Angular 22.\n\n## `createAngularWidget(Component, options?)`\n\nAwait the factory to get a custom element class. The adapter shares an Angular application across widgets by default. Pass an existing `ApplicationRef` as `options.application` to use its providers. Pass the widget's shadow CSS as `options.styles`.\n\nThe Angular component renders inside the widget's existing shadow root. Use a native `<slot>` in its template to display the authored children. The adapter keeps the Angular view during a synchronous DOM move and releases it when the host stays detached.\n\n## `injectWidget()` and `injectWidgetHost()`\n\nCall these functions from an Angular injection context such as a component field initializer. `injectWidget()` returns a readonly signal for the shared snapshot. `injectWidgetHost()` returns the widget element, which exposes `widgetRuntime` for writes.\n\n```ts\nimport \"@angular/compiler\"\nimport {Component} from \"@angular/core\"\nimport {createAngularWidget, injectWidget} from \"@webwriter/angular\"\n\n@Component({\n selector: \"ww-label-view\",\n standalone: true,\n template: `\n <slot></slot>\n <span>{{ widget().attributes.label ?? \"\" }}</span>\n `,\n})\nclass Label {\n readonly widget = injectWidget()\n}\n\nconst LabelElement = await createAngularWidget(Label)\n\nclass WwLabel extends LabelElement {\n static widgetAttributes = [\"label\"]\n}\n\ncustomElements.define(\"ww-label\", WwLabel)\n```\n\nThe [Angular Ghost Disco source](../examples/disco/angular/08-disco-dependencies.ts) uses `@angular/compiler` with its build to compile its template in the browser.\n\n<!-- /library -->\n\n<!-- library:svelte -->\n# C2. @webwriter/svelte\n\n`@webwriter/svelte` adds the shared widget runtime to a compiled Svelte 5 custom element.\n\n## `withWidgetRuntime(Base)`\n\nUse the mixin in the Svelte compiler's `customElement.extend` option. It adds `widgetRuntime` to the generated element and observes the attributes listed in `static widgetAttributes`.\n\n## `widgetStore(host)`\n\nPass `$host()` to the store helper. Read the current snapshot with Svelte's `$store` syntax. Svelte releases the subscription when the component unmounts.\n\n```svelte\n<script module>\nimport {withWidgetRuntime, widgetStore} from \"@webwriter/svelte\"\n</script>\n\n<svelte:options customElement={{\n tag: \"ww-label\",\n extend: Base => class extends withWidgetRuntime(Base) {\n static widgetAttributes = [\"label\"]\n },\n}} />\n\n<script>\n const widget = widgetStore($host())\n</script>\n\n<slot></slot>\n<span>{$widget.attributes.label ?? \"\"}</span>\n```\n\nSee the [Svelte Ghost Disco source](../examples/disco/svelte/08-disco-dependencies.svelte) for an interactive widget.\n\n<!-- /library -->\n\n## Shared widget state\n\nThe snapshot describes the current DOM. Use attribute names in `static widgetAttributes` and read their values from `attributes`. Values are strings or `null` when an attribute is absent. Set an authored attribute on the host to persist a setting in the document.\n\n| Snapshot fields | Purpose |\n| --- | --- |\n| `editable`, `lang`, `standalone` | The host's editing mode, effective language, and standalone attribute. |\n| `attributes` | The values for the attributes listed in `static widgetAttributes`. |\n| `data`, `dataType`, `dataBlock`, `dataSlot` | The current inert data block and its text, type, and slot. |\n| `dataMode` | The current scope. One of `individual`, `all`, `group`, or `local`. |\n| `dataReady`, `dataError` | Whether the data scope accepts writes, and the latest error if present. |\n\nUse `host.widgetRuntime.setData(type, text, attrs?)` to write to the data block from an interaction. It updates an existing block in place and rejects writes while a managed scope is pending. For live-session data, use a direct JSON or XML script with `slot=\"data\"` and set `sharedData: true` in the editing configuration. Standalone documents can use the same block as local data. [Data and events](data.md)\n\nPut `static options`, `static optionGroups`, `static actions`, and their handler methods on the custom element. WebWriter uses these declarations in the Widget drawer. The runtime and adapters export the corresponding TypeScript declaration types. [Options and actions](options-actions.md)\n"},{"id":"reference/aggregated-view","source":"# B3. Aggregated View\n\nThe authoring tool can show participant data from a widget in an Aggregated View. Declare `static aggregate` on the [custom element](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements) class.\n\nThe declaration can be one configuration or a read-only list of configurations. It can also be a synchronous function that receives the current context and returns either form.\n\n```ts\nstatic aggregate = {\n shape: \"selection\",\n items: [\n {id: \"a\", label: \"Option A\"},\n {id: \"b\", label: \"Option B\"},\n ],\n}\n```\n\nFor a function declaration, the context contains the current widget and response snapshots.\n\n## Multiple views\n\nEach configuration can have an `id`. Use a unique string of 1 to 128 characters for each ID. A configuration with no ID uses its shape name. Give repeated shapes explicit IDs.\n\nA list can contain up to 50 configurations. The authoring tool skips unsupported shapes. The authoring tool needs at least one supported configuration to show an Aggregated View. An existing widget can continue to use one configuration.\n\nThe authoring tool selects the first supported configuration by default. A view ID selects its configuration. If the authoring tool cannot find the ID, it selects the first supported configuration. A view uses `title` as its display name. If the configuration has no title, the view uses the shape name. Other choices remain available if one configuration is invalid.\n\n```ts\nstatic aggregate = [\n {\n id: \"answer-choice\",\n title: \"Answer choices\",\n shape: \"selection\",\n items: [\n {id: \"a\", label: \"Option A\"},\n {id: \"b\", label: \"Option B\"},\n ],\n },\n {\n id: \"answer-text\",\n title: \"Written explanations\",\n shape: \"text\",\n value: data => data?.explanation,\n },\n] as const\n```\n\nThe context for declaration functions contains these fields.\n\n| Field | Contents |\n|---|---|\n| `widget` | The widget's `id`, string `attributes`, and authored `html`. |\n| `participants` | `{id, name}` records for the participants in this view. |\n| `samples` | Independent response records, each with `id`, `learners`, parsed `value`, and a `data` snapshot. |\n| `missing` | Number of participant snapshots that are pending, unavailable, invalid, or cannot be assigned to a response scope. |\n| `reference` | The explicitly configured reference value, when one is provided. |\n\nThe context and its snapshots are read-only. A sample's `data` contains its `status`, `format`, `mode`, optional `scopeId`, and raw `text`. The authoring tool parses [JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON) into `value` with the type `unknown`. For XML, `value` is the raw string.\n\nThe `value(data, sample)` extractor receives the sample's value. It returns the value for the shape. For XML input in shapes other than `distancemap`, provide an extractor that converts the string. If the extractor returns `undefined`, the view omits that sample. The participants for that sample add to `missing`.\n\n## Shapes\n\nEach configuration can set a `title` and a `value(data, sample)` extractor. It can also set an explicit `reference` in the same format as the extracted value. Without an extractor, the view uses the parsed JSON or the raw XML string. Supply a reference explicitly when the view needs one.\n\n| Shape | Configuration | Accepted extracted values and display |\n|---|---|---|\n| `selection` | `items: [{id, label}, ...]` | A scalar item ID or an array of item IDs. Shows the distribution of selected items. |\n| `ordering` | `items: [{id, label}, ...]` | An ordered array of item IDs. Shows submitted orderings against the declared items. |\n| `number` | Optional `bins` | Finite numbers. Shows the distribution of the numbers. `bins` sets the number of bins. |\n| `text` | None | Strings. Shows submitted text and word frequencies. |\n| `spatial` | `bounds: {width, height}`, optional `columns` and `rows` | A point `{x, y}` or an array of points. Shows a spatial distribution within the declared bounds. `columns` and `rows` set the grid. |\n| `distancemap` | Optional `distance(a, b)` or `distances` matrix | Uses Euclidean distance for numeric feature vectors by default. For other values, the default is structural distance. Supply a distance function or a symmetric distance matrix to change the metric. The view supports up to 300 independent responses. |\n| `custom` | `element: \"tag-name\"` | Creates the named custom element and assigns the read-only aggregate context to its `aggregate` property. |\n\nFor a distance matrix, supply one row and one column per sample in `context.samples` order. Use a square and symmetric matrix. Use only finite, non-negative values. Put zeros on the diagonal.\n\nA custom distance function receives pairs of extracted sample values. For more than 300 independent responses, the authoring tool shows participant thumbnails.\n\n## Quiz example\n\nThis quiz records a choice in its JSON data block. The authored `answer` attribute gives the correct answer. The configuration uses it as the explicit reference.\n\n```js\nclass WwQuiz extends HTMLElement {\n static aggregate = context => ({\n shape: \"selection\",\n title: \"Quiz answers\",\n items: [\n {id: \"a\", label: \"Option A\"},\n {id: \"b\", label: \"Option B\"},\n {id: \"c\", label: \"Option C\"},\n ],\n value: data => data?.choice || undefined,\n reference: context.widget.attributes.answer,\n });\n}\n```\n\n```html\n<ww-quiz answer=\"b\">\n <script type=\"application/json\" slot=\"data\">{\"choice\":\"\"}</script>\n</ww-quiz>\n```\n\nWith no `shared` attribute, each participant has an independent answer. [Data and events](data.md) describes the data block and its scopes.\n\n## Custom renderer\n\nRegister the custom element in the widget's [scoped registry](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/CustomElementRegistry) or in the global registry. With [`LitElementWw`](lit.md#litelementww), use `static scopedElements` to register it in the widget's registry.\n\n```js\nimport {LitElementWw} from \"@webwriter/lit\";\n\nclass AnswerSummary extends HTMLElement {\n connectedCallback() {\n this.renderSummary(this.aggregate);\n }\n\n renderSummary(context) {\n this.textContent = `${context.samples.length} responses`;\n }\n}\n\nclass WwQuiz extends LitElementWw {\n static scopedElements = {\"ww-answer-summary\": AnswerSummary};\n static aggregate = {\n shape: \"custom\",\n element: \"ww-answer-summary\",\n title: \"Answer summary\",\n };\n}\n```\n\nThe authoring tool sets `aggregate` before it connects the renderer. The snapshot is read-only. Use it as input to the renderer only.\n"},{"id":"reference/build-test","source":"# C1. @webwriter/build\n\n> [!NPM]\n>\n> [View `@webwriter/build` on npm](https://www.npmjs.com/package/@webwriter/build)\n\n`@webwriter/build` compiles widget and test exports. It reads the path in `source` and writes the result to the path in `default`. Export plain browser JavaScript directly if it needs no build.\n\n| Command / flag | Effect |\n|---|---|\n| `npx @webwriter/build` | Build exports. |\n| `npx @webwriter/build dev` | Watch source files and build again when they change. |\n| `npx @webwriter/build preview <tag-name>` | Serve a standalone preview for a `./widgets/<tag-name>.*` export. |\n| `npx @webwriter/build localize` | Generate and apply configured widget, snippet, and metadata translations. |\n| `npx @webwriter/build document` | Generate widget documentation and custom element metadata. |\n| `--shared-dependencies` | Use shared dependency imports and chunks for builds and `dev` in WebWriter 2. |\n| `--yes`, `-y` | Confirm interactive proposals, including saving proposed prebuilt dependencies. |\n| `--prebuild-threshold=<modules>` | With `--shared-dependencies`, reconsider whether to bundle dependencies above this module count. The default is 200. |\n| `--help`, `-h` | Show CLI help. |\n\nBy default, the build puts dependencies in each widget for compatibility with WebWriter 1. With `--shared-dependencies`, shared dependencies stay as bare imports. The build bundles the prebuilt dependencies.\n\nThe build uses an unconfirmed prebuild choice for the current build only. This also applies to a build without a terminal. Standalone previews include all dependencies in the bundle.\n\nThe **Developer console** reports invalid configuration, export keys, tag names, and content expressions. It also reports widget tags without definitions and [Lit properties](https://lit.dev/docs/components/properties/) that fail to reflect changes to attributes.\n\n## Test reporting\n\nExport tests through `./tests/<name>.js` or `./tests/<name>.*`. WebWriter gives each test module 60 seconds to run.\n\nFor [Mocha](https://mochajs.org/running/browsers/), import `getMochaConfig` from `@webwriter/build/test`. Pass its result to `mocha.setup()`. Define the tests. Call `mocha.run()`. The helper supplies reporting hooks. Install Mocha separately if necessary.\n\nFor other runners, dispatch [`CustomEvent(\"test-update\", {detail})`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) on the test frame's `window`.\n\n| `detail.type` | Fields |\n|---|---|\n| `beforeAll` | No additional fields. |\n| `beforeOne` | `id: string`, `path: string[]`. Optional `sync: boolean`. |\n| `afterOne` | Same `id` and `path`, plus `passed: boolean`. Optional `duration: number` in milliseconds, `sync: boolean`, `timedOut: boolean`. |\n| `afterAll` | No additional fields. |\n\nSend one `beforeAll` and one `afterAll` per module. Send one `beforeOne` and one `afterOne` per test. See the [Ghost Disco test module](../examples/ghost-disco-tests.js) for a complete example.\n"},{"id":"reference/content-expressions","source":"# A4. Content expressions\n\n`editingConfig.content` describes which light DOM children authors can edit. Without a content expression, the authoring tool treats the widget as one unit.\n\n| Expression | Meaning | Example |\n|---|---|---|\n| Node name | One element of that type. | `p`, `ww-option` |\n| Group name | One member of the group. | [`flow`, `phrasing`, `heading`](https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Content_categories), `widget`, `widgetinline` |\n| `a b` | Sequence. | `p ww-option` |\n| `a \\| b` | Choice. | `p \\| ul` |\n| `(…)` | Grouping. | `(p \\| ul)+` |\n| `?` | Zero or one. | `h2?` |\n| `*` | Zero or more. | `phrasing*` |\n| `+` | One or more. | `p+` |\n| `{n}` | Exactly n. | `ww-option{2}` |\n| `{n,}` | At least n. | `ww-option{2,}` |\n| `{n,m}` | Between n and m. | `ww-option{2,6}` |\n\nA widget declares custom groups in its `group` setting. Separate the group names with spaces. Use only node names and groups that exist. The largest finite repetition limit is 1,000.\n\n```json\n{\n \"./widgets/ww-quiz\": {\"content\": \"p+ ww-option{2,}\"},\n \"./widgets/ww-option\": {\"group\": \"quizoption\", \"content\": \"phrasing*\", \"uninsertable\": true}\n}\n```\n\nA widget can contain inert data blocks and children with [`contenteditable=\"false\"`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/contenteditable) in addition to the content in the expression. Keep unrelated document content unchanged.\n"},{"id":"reference/data","source":"# B4. Data and events\n\nScoped data requires `sharedData: true` in the widget's editing configuration. The widget has one [data block](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#embedding_data_in_html) as a direct child.\n\n```html\n<ww-quiz id=\"quiz-a\" shared>\n <script type=\"application/json\" slot=\"data\">{\"choice\":\"\"}</script>\n</ww-quiz>\n```\n\n| Setting | Values / behavior |\n|---|---|\n| `type` | Exactly `application/json` or `application/xml`. |\n| `slot` | `data`. |\n| `src` | Must be absent. |\n| Widget `id` | A stable identifier. The authoring tool assigns an ID if the widget has none. Copies receive new IDs. |\n| Widget `shared` | No attribute selects individual data. An empty value selects shared data for everyone. `group` selects a group that the server assigns. Other values, including `false`, are invalid. |\n| [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) | Contains the current snapshot. Assign a complete serialized value to update it. |\n\nWait for the initial `datachange` event before you write data. A scope change keeps data in the old scope. The destination loads its own data or the authored defaults.\n\nThe [Aggregated View interface](aggregated-view.md) uses available participant snapshots as independent samples. A sample for a shared group uses the `scopeId` that the connection supplies.\n\n## Events\n\nThese events start at the data block and [bubble](https://developer.mozilla.org/en-US/docs/Web/API/Event/bubbles) through the widget. Process only events from the widget's current direct data block.\n\n| Event | `detail` | Meaning |\n|---|---|---|\n| `datarequest` | — | Request the current state with a bubbling, cancelable event on the current direct data block. The authoring tool prevents the default action when it manages the block. |\n| `datapending` | `{mode: \"individual\" \\| \"all\" \\| \"group\"}` | A scope is loading. The event occurs before its first `datachange`, including after a change to the sharing or group scope. |\n| `datachange` | `{mode: \"individual\" \\| \"all\" \\| \"group\", scopeId?: string}` | The initial data or an accepted local or remote snapshot is ready. The event includes `scopeId` when the connection supplies it. Update the display without writing data. |\n| `dataerror` | `{message: string}` | Reports invalid data, an invalid mode, or a connection failure. An invalid write leaves the last accepted data in storage. |\n| `datapersistence` | `{state: \"pending\" \\| \"saved\" \\| \"error\", message?: string}` | Reports the offline save status. Use `saved` to confirm a save. A `datarequest` after initialization also returns this status. |\n\n## Merge and persistence\n\nJSON object fields merge recursively. Arrays, strings, numbers, booleans, and null are atomic values. When writes conflict, the participants get the same result. A numeric assignment sets a value. Use a different mechanism for a counter that adds concurrent changes. An empty JSON block starts as `{}`.\n\nUse well-formed XML. Root attributes merge separately. A change to the child list replaces the complete list. The authoring tool keeps namespaces, comments, and processing instructions. Store only data that can go in an HTML script block.\n\nThe authoring tool stores participant data separately from the shared document body. Changes to participant data support undo and redo. Standard HTML exports and clipboard copies keep the authored defaults. An export with the current state includes participant data.\n\nOffline data for an individual scope belongs to a specific document, widget, and participant. Group scopes require a server connection.\n\nThe authoring tool checks for stored data before it uses authored defaults. If a read fails, the scope stays pending. The authoring tool reports `datapersistence` with `state: \"error\"`. A save can fail for one scope while other scopes continue to save.\n\nIf offline storage is unavailable, participants can continue to edit data in memory. The authoring tool reports a storage error. Wait for `datapersistence` with `state: \"saved\"` to confirm that an offline write has finished.\n\nThe server assigns participants to groups. The [sharing guide](../guides/sharing.md) describes the available scopes.\n"},{"id":"reference/distribution","source":"# A1. Package manifest\n\nAn [npm package](https://docs.npmjs.com/cli/v11/configuring-npm/package-json/) can contain one or more widgets. Each widget export identifies a tag name and the script that defines it.\n\n## Manifest\n\n| `package.json` field | Purpose |\n|---|---|\n| `name`, `version` | The npm package name and SemVer version. The name can include a [scope](https://docs.npmjs.com/about-scopes/) such as `@webwriter/…`. |\n| `keywords` | Feature badges and discovery information. Include `webwriter-widget` for catalog discovery. The [widget keyword reference](keywords.md) lists the badges. |\n| [`exports`](https://docs.npmjs.com/cli/v11/configuring-npm/package-json/#exports) | Exports for widgets and related files. |\n| `editingConfig` | Inline [editing configuration](editing-configuration.md). |\n| `dependencies`, `peerDependencies` | The document's [import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap) resolves bare imports for these dependencies. |\n| `prebuiltDependencies` | `@webwriter/build` bundles these dependencies. |\n| `sideEffects` | Standard module side-effect metadata. `false` permits loading only the modules in use. |\n| `description`, `author`, `contributors`, `license`, `homepage`, `repository` | Package card metadata. |\n| `customElements` | Path to a custom elements manifest. |\n\nA `README.md`, `.markdown`, or `.txt` file supplies package documentation.\n\n## Exports\n\n| Key | Purpose |\n|---|---|\n| `./widgets/<tag>.*` | Matching `<path>.js` and optional `<path>.css`. |\n| `./widgets/<tag>.js`, `./widgets/<tag>` | Widget script. |\n| `./widgets/<tag>.css` | Widget stylesheet. It has priority over the wildcard stylesheet. |\n| `./snippets/<name>.html` | HTML that the author can insert. The authoring tool removes active scripts, handlers, and styles. It keeps inert widget data blocks. |\n| `./icon` | Package and member icon. |\n| `./editing-config.json` | External editing configuration. |\n| `./migrate.js` | Script for [content migration](../guides/migrations.md) in a sandbox. |\n| `./tests/<name>.*`, `./tests/<name>.js` | Test module with optional stylesheet. |\n| `./themes/<name>.html` | Theme for the development tools. |\n| `./custom-elements.json` | Custom elements manifest for the development tools. |\n\nAn export target can be a string or a condition object. The authoring tool checks `browser`, `import`, and `default` in that order. A `source` condition identifies the input for the development tools and the build command.\n\n```json\n{\n \"exports\": {\n \"./widgets/ww-quiz.*\": {\n \"source\": \"./src/widgets/ww-quiz.ts\",\n \"default\": \"./dist/widgets/ww-quiz.*\"\n }\n }\n}\n```\n\nExports with the same tag form one widget. Include each referenced file in the package. Use a separate export for a stylesheet that a dependency needs. Import maps resolve only JavaScript modules.\n\n## Discovery keywords\n\nManifest `keywords` give a widget feature badges. These badges show features, requirements, and categories. The [widget keyword reference](keywords.md) lists all badges and their syntax.\n\nEducational classification keywords use `isced2011-<0–9>` for education levels and `iscedf2013-<code>` for educational fields.\n"},{"id":"reference/editing-configuration","source":"# A3. Editing configuration\n\nDeclare `editingConfig` in [`package.json`](https://docs.npmjs.com/cli/v11/configuring-npm/package-json/) or export `./editing-config.json`. The authoring tool merges object fields recursively. Inline values have priority over values from the external file.\n\nA key identifies an export with or without its extension. The key `.` supplies package metadata.\n\n```json\n{\n \"editingConfig\": {\n \".\": {\"label\": {\"_\": \"Quiz\", \"de\": \"Quiz\"}},\n \"./widgets/ww-quiz\": {\"label\": \"Quiz\", \"content\": \"p+\", \"sharedData\": true}\n }\n}\n```\n\n| Option | Type | Default | Effect |\n|---|---|---|---|\n| `label` | `LocalizedText` | Member name in title case. For a widget, this omits the part before the first hyphen. | Label in the insertion menu. On `.`, this is the package label. |\n| `description` | `LocalizedText` | None. | Member description. On `.`, this is the package description. |\n| `uninsertable` | `boolean` | `false` | Hides the member from the insertion menu. |\n| `group` | `string` | `\"flow\"` for block widgets. | Content groups separated by spaces. An explicit value replaces the default group. The authoring tool always adds `widget` or `widgetinline`. |\n| `inline` | `boolean` | `false` | Makes a [phrasing](https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Content_categories#phrasing_content) widget in `widgetinline`, without automatically adding `flow`. |\n| `content` | `string` | None. The authoring tool treats the widget as one unit. | [Content expression](content-expressions.md) for editable light DOM children. |\n| `isolating` | `boolean` | `true` | Prevents editing from splitting or joining across the widget boundary. |\n| `marks` | `string` | `\"_\"` | Permitted formatting marks. Use names separated by spaces, `\"\"` for none, or `\"_\"` for all. |\n| `propagateEvents` | `string[]` | `[]` | Types of shadow DOM events that the authoring tool can handle, for example `[\"keydown\"]`. |\n| `sharedData` | `boolean` | `false` | Enables [scoped data](data.md) in a direct `script[slot=\"data\"]` child. |\n| `importFormats` | `string[]` | `[]` | File format keys that the widget's `convertFrom` handlers support. |\n| `exportFormats` | `string[]` | `[]` | File format keys that the widget's `convertTo` handlers support. |\n\n`LocalizedText` is a string or a locale map such as `{\"en\": \"Answer\", \"de\": \"Antwort\", \"_\": \"Answer\"}`. The authoring tool tries the exact locale first. It then tries the language. It uses `_` as the default.\n\nMarks use element names such as `b`, `i`, `a`, and `span` for text styles. The configuration controls how authors edit the current document.\n\n## Advertise file formats\n\nDeclare `importFormats` and `exportFormats` in the widget's export entry. Package tools can read these fields before they load the widget. Use a file extension such as `.js` or `.h5p`, or a [media type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types) such as `text/javascript`. Use the same keys in the matching [runtime conversion map](runtime.md#conversions). An extension can identify the format when a file has no media type.\n\n```json\n{\n \"editingConfig\": {\n \"./widgets/ww-code\": {\n \"importFormats\": [\"text/javascript\", \".js\"],\n \"exportFormats\": [\"text/javascript\"]\n }\n }\n}\n```\n\nAn inline list replaces the corresponding list from `./editing-config.json`. Use an empty list to clear an inherited declaration.\n"},{"id":"reference/guidance","source":"# C3. @webwriter/guidance\n\n`@webwriter/guidance` provides the `webwriter-guidance` CLI and an Agent Skill for developing WebWriter widgets. Use Node.js 22.20 or newer.\n\n## Install the skill\n\nRun the installer from your widget project. Choose a coding agent when prompted, or pass agent names directly.\n\n```sh\nnpx -y @webwriter/guidance@latest install\nnpx -y @webwriter/guidance@latest install --agent codex claude-code cursor --yes\n```\n\nThe installer uses the same [Vercel Skills CLI](https://github.com/vercel-labs/skills/blob/main/docs/commands.md#skills-add) as Modern Web Guidance. It supports [Codex, Claude Code, Cursor, Gemini CLI, GitHub Copilot, and other agents](https://github.com/vercel-labs/skills/blob/main/docs/agents.md). Pass `--global` for a user-wide installation. Run `install` again with the latest package to refresh the skill, or run `uninstall` with the same scope and agent options to remove it.\n\n## Query the documentation\n\n```sh\nnpx -y @webwriter/guidance search \"widget package exports\"\nnpx -y @webwriter/guidance retrieve \"reference/distribution,guides/publishing\"\nnpx -y @webwriter/guidance list\n```\n\n| Command | Output |\n| --- | --- |\n| `search <query>` | Up to five ranked results with `id` and `description` fields. Use one topic or API name per query. |\n| `retrieve <id[,id...]>` | Full Markdown for the named pages. The CLI prints a temporary file path for large results. |\n| `list` | All searchable page IDs and descriptions. |\n| `badges [package.json]` | Widget badge keywords and their guide IDs. Pass a manifest path to look up its declared badges. |\n\nThe npm package bundles a snapshot of the widget guides, tutorials, and reference pages. Once the package is installed, document lookup works offline. From the WebWriter repository root, use `node @webwriter/guidance/cli.mjs` with the same commands to query the current checkout.\n\n## Rework an existing widget\n\nAsk your coding agent to rework the widget using the WebWriter Guidance skill. The agent uses the declared [widget keywords](keywords.md) and the affected capabilities to check the widget against the guides. It asks you to decide open design questions and continues fixes that can be verified independently.\n"},{"id":"reference/index","source":"# Reference\n\nThis reference describes the widget interface. The [guides](../guides/index.md) give instructions for specific tasks.\n\n## A. Package interface\n\n- [A1. Package manifest](distribution.md) lists manifest fields, exports, dependencies, and discovery rules.\n- [A2. Widget keywords](keywords.md) lists feature badges and the syntax for manifest `keywords`.\n- [A3. Editing configuration](editing-configuration.md) lists configuration fields and default values.\n- [A4. Content expressions](content-expressions.md) describes groups, sequences, choices, and repetition.\n\n## B. Runtime interface\n\n- [B1. Runtime interface](runtime.md) describes custom elements, lifecycle, and editing mode.\n- [B2. Options and actions](options-actions.md) describes declarations, controls, element permissions, attribute conversion, and undo.\n- [B3. Aggregated View](aggregated-view.md) describes declarations, response shapes, and custom renderers.\n- [B4. Data and events](data.md) describes data blocks, sharing modes, data merge rules, and events.\n- [B5. Design tokens](styling.md) describes widget sizes, shadow DOM styles, and styles for editing.\n- [B6. Standalone storage](standalone.md) describes browser and HTTP result storage for standalone widgets.\n\n## C. Tooling\n\n- [C1. @webwriter/build](build-test.md) lists commands, flags, and test reporting requirements.\n- [C2. Widget adapters](adapters.md) describes the widget tooling for the selected library.\n- [C3. @webwriter/guidance](guidance.md) explains how to install the coding agent skill and query the widget documentation.\n"},{"id":"reference/keywords","source":"# A2. Widget keywords\n\nKeywords give a widget feature badges. These badges show what the widget does, what it needs, and its purpose.\n\nAdd the keywords for your widget to the [`keywords` array](https://docs.npmjs.com/cli/v11/configuring-npm/package-json/#keywords) in `package.json`. The authoring tool shows the badges in the package details tooltip.\n\n| Keyword | Meaning |\n|---|---|\n| `widget-editable` | Authors can edit the widget's content or behavior in the authoring tool. |\n| `widget-allows-pointer-input` | The widget accepts pointer input. |\n| `widget-allows-keyboard-input` | The widget accepts keyboard input. |\n| `widget-allows-touch-input` | The widget accepts touch input. |\n| `widget-has-content` | The widget accepts authored text, HTML elements, or nested widgets as content. |\n| `widget-convertible-to-[media/type \\| <element-name>]` | The widget can convert its content to the specified media type or element. |\n| `widget-convertible-from-[media/type \\| <element-name>]` | The widget supports creation or content import from the specified media type or element. |\n| `widget-responsive` | The widget adjusts its layout to the available space. |\n| `widget-themeable` | The widget uses the document theme or supplies styling hooks for a document stylesheet. |\n| `widget-lang-[bcp47]` | The widget's content uses the language in the [BCP 47 language tag](https://developer.mozilla.org/en-US/docs/Glossary/BCP_47_language_tag). |\n| `widget-printable` | The widget has a display suitable for printing. |\n| `widget-stateful` | The widget keeps state such as saved settings, content, or interaction results. |\n| `widget-configurable` | Authors can configure the widget through declared options or actions. |\n| `widget-reactive` | The widget publishes or reads signals to respond to changes across widgets. |\n| `widget-shareable` | The widget can share participant data with a group or with all participants. |\n| `widget-distributable` | The widget supports distribution in a live session. |\n| `widget-online`, `widget-online-edit`, `widget-online-use` | The widget needs a network connection for all operations, for editing, or for use, respectively. |\n| `widget-supports-dynamic-loading` | The widget loads optional code or resources when necessary. |\n| `widget-supports-shared-dependencies` | The widget supports dependencies shared with other widgets. |\n| `widget-testable` | The package exports tests for the authoring tool's **Developer console**. |\n| `widget-migrates-from-[semver]` | The widget can migrate saved content from the specified version. |\n| `webwriter-widget` | The package contains a widget that the catalog can find. |\n| `widget-standalone` | The widget can operate on a web page outside the authoring tool. |\n| `widget-presentational` | The widget's main purpose is presentation. |\n| `widget-practical` | The widget's main purpose is a practical activity or application. |\n| `widget-simulational` | The widget's main purpose is simulation. |\n| `widget-conceptual` | The widget's main purpose is to help readers examine concepts. |\n| `widget-informational` | The widget's main purpose is to give information. |\n| `widget-contextual` | The widget's main purpose is to give context. |\n\n## Values in keyword forms\n\nReplace the text in brackets with a value. For example, use `widget-lang-en-US` for a language tag. Use `widget-migrates-from-1.2.0` for a version.\n\nFor `widget-convertible-to-…` and `widget-convertible-from-…`, use a [MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types) or an element name without angle brackets. Examples include `widget-convertible-to-image/png`, `widget-convertible-from-audio/mpeg`, `widget-convertible-to-video`, and `widget-convertible-from-ww-quiz`.\n\nThe online forms are `widget-online`, `widget-online-edit`, and `widget-online-use`. The keywords `isced2011-<0–9>` and `iscedf2013-<code>` identify education levels and fields. [Package manifest](distribution.md#discovery-keywords) describes these discovery keywords.\n"},{"id":"reference/lit","source":"# C2. @webwriter/lit\n\n> [!NPM]\n>\n> [View `@webwriter/lit` on npm](https://www.npmjs.com/package/@webwriter/lit)\n\n`@webwriter/lit` supplies a Lit base class for widgets. It also supplies signal helpers and a standalone drawer for embedded widgets.\n\n## `LitElementWw`\n\nImport `LitElementWw` from `@webwriter/lit`. Call `super` when you override a [lifecycle callback](https://lit.dev/docs/components/lifecycle/).\n\n| Member | Purpose |\n|---|---|\n| `standalone` | The reflected attribute value. Its value is `null` when the attribute is absent. Render a [standalone widget drawer](#standalone-widget-drawer) to show the controls. |\n| `editable` | Getter for `isContentEditable`. Updates when the widget or an ancestor changes editing mode. |\n| `lang` | The reactive language. Without an explicit value, it uses the language from the nearest ancestor that sets it. |\n| `static scopedElements` | Maps tag names to component classes. All instances of the same widget class share its native registry. |\n| `registry` | The widget class's native `CustomElementRegistry`. |\n| `static properties` | Lit [reactive property](https://lit.dev/docs/components/properties/) declarations. Declare option metadata separately. |\n| `static options` / `static actions` | Widget drawer declarations. Use static fields for these declarations. |\n| `localize` | Optional `{getLocale, setLocale}` connection from [`@lit/localize`](https://lit.dev/docs/localization/overview/). |\n| `static dataType` | Default MIME type for an inert data block. The initial value is undefined. |\n| `dataType` | Type of the current data block. It uses the class default when necessary. |\n| `data` | Raw text of the direct scoped data block. Without a scoped block, it uses the first direct inert script child that has no `src`. The value is `null` when no block exists. A write uses `dataType` or `text/plain`. |\n| `setData(type, value, attrs?)` | Creates a block or updates the current block. An update keeps the block's identity and attributes. The method rejects executable types, `src`, and unsafe closing script tags. It escapes `<` characters in JSON. |\n| `dataReady` | Indicates that the current data scope accepts writes. |\n| `dataPending(context)` | Override to show that a participant scope is loading. |\n| `dataChanged(text, context)` | Override to read initial, accepted local, remote, and undo or redo snapshots. Update the display without writing data. |\n| `dataError(error, context)` | Override to display a connection or data validation error. |\n\nData hooks receive raw text and a `{block, mode}` context. Modes are `individual`, `all`, `group`, or `local`. Override these hooks to respond to changes in the data block. The base class requests the current state on connection. This also applies to late custom element upgrades. The hooks apply only to the widget's own data block.\n\nA write through `data` or `setData()` throws `InvalidStateError` while a managed scope is pending. After a validation error on a ready scope, the widget can try a corrected write.\n\nFor scoped data, use a [data block](data.md) with JSON or XML and `slot=\"data\"`. Set `sharedData: true` in the editing configuration. The authoring tool supplies the scoped data connection. Outside the managed runtime, authored data stays local. The hooks use `mode: \"local\"`.\n\nChanges to the data block through the DOM also cause a call to `dataChanged`. A replacement block starts a new data connection. A write through a helper keeps the current block.\n\nThe base class uses [native scoped registry APIs](runtime.md#scoped-custom-element-registries) directly.\n\nThe widget keeps an explicit `lang=\"\"` value. When it inherits a language, it reads the value from its ancestor. A change to the effective language updates the optional localization connection. If the update fails, the widget dispatches `localizationerror` with `{error}`.\n\n## Options and actions in Lit\n\nDeclare Lit reactive properties in `static properties`. Set fields such as `reflect`, `state`, and `converter` there.\n\nIf an option has no `attribute`, the authoring tool uses the attribute name from the matching Lit property declaration. A property with `attribute: false` or `state: true` has no option attribute by default.\n\nReplace legacy `option` and `action` decorators with `static options` and `static actions`. Use `static optionGroups` for group declarations. The authoring tool records the DOM changes from an action as one undo step. The drawer ignores `placeholder` in a Lit action declaration. [Options and actions](options-actions.md) describes the interface.\n\n## Signals\n\nThe `@webwriter/lit` package re-exports [Lit Signals](https://lit.dev/docs/data/signals/). Its exports include `SignalWatcher`, `signal`, `computed`, `watch`, and a version of `html` that reads signals.\n\nWhen you build widgets separately, use compatible dependency versions. Use `@webwriter/build --shared-dependencies` so the widgets use one polyfill instance. With a different bundler, keep `@webwriter/lit`, Lit Signals, and the polyfill external.\n\nThe [runtime signal interface](runtime.md#signals) describes effect declarations and connection syntax. The example clears its source when the connection aborts.\n\n`LitElementWw` tracks the signals that `render()` reads with `.get()`. A change to one of these signals starts an update. The display uses these updates to format the converter's Fahrenheit output. To update one binding, pass the signal to the `html` template.\n\n```html\n<ww-temperature-sensor id=\"sensor\"></ww-temperature-sensor>\n<ww-fahrenheit-converter id=\"converter\" effect=\"#sensor[celsius]\"></ww-fahrenheit-converter>\n<ww-temperature-display effect=\"#converter[fahrenheit]\"></ww-temperature-display>\n```\n\n```js\nimport {LitElementWw, computed, html, signal} from \"@webwriter/lit\"\n\nclass WwTemperatureSensor extends HTMLElement {\n #temperature = signal(20)\n signals = {celsius: computed(() => this.#temperature.get())}\n\n set celsius(value) {\n this.#temperature.set(value)\n }\n}\n\nclass WwFahrenheitConverter extends HTMLElement {\n #input = signal(null)\n signals = {\n fahrenheit: computed(() => {\n const celsius = this.#input.get()?.get()\n return celsius == null ? null : celsius * 1.8 + 32\n })\n }\n\n static effects = {\n effect: {\n label: {en: \"Temperature sensor\"},\n connect(source, lifetime) {\n this.#input.set(source)\n lifetime.addEventListener(\"abort\", () => this.#input.set(null), {once: true})\n }\n }\n }\n}\n\nclass WwTemperatureDisplay extends LitElementWw {\n #input = signal(null)\n\n render() {\n const value = this.#input.get()?.get()\n return html`<output>${value == null ? \"—\" : `${value} °F`}</output>`\n }\n\n static effects = {\n effect: {\n label: {en: \"Temperature reading\"},\n connect(source, lifetime) {\n this.#input.set(source)\n lifetime.addEventListener(\"abort\", () => this.#input.set(null), {once: true})\n }\n }\n }\n}\n\ncustomElements.define(\"ww-temperature-sensor\", WwTemperatureSensor)\ncustomElements.define(\"ww-fahrenheit-converter\", WwFahrenheitConverter)\ncustomElements.define(\"ww-temperature-display\", WwTemperatureDisplay)\n```\n\nUse computed signals for values that depend on other signals. A watcher that writes to a source can cause a feedback loop outside the declared connections.\n\n## Standalone widget drawer\n\nRender `WidgetDrawer` in an embedded widget to show settings, file operations, and result storage. Set `standalone` on the widget host. Register the drawer in the widget's scoped registry. Pass the widget instance to the drawer.\n\n```js\nimport {LitElementWw, WidgetDrawer} from \"@webwriter/lit\"\nimport {html} from \"lit\"\n\nclass WwQuiz extends LitElementWw {\n static scopedElements = {\"ww-widget-drawer\": WidgetDrawer}\n\n render() {\n return html`\n <slot></slot>\n <!-- Render the quiz here. -->\n <ww-widget-drawer .widget=${this}></ww-widget-drawer>\n `\n }\n}\n```\n\nThe drawer appears when the widget has `standalone` and has controls to show. The `LitElementWw.standalone` property reflects the attribute as a string. If the attribute is absent, the property is `null`. WebWriter supplies the drawer for widgets in its documents.\n\n### Settings, actions, and file conversions\n\nUse the same [option and action declarations](options-actions.md) for the standalone drawer. The drawer uses the standard validation, groups, and action confirmation. When dynamic declarations change, dispatch `optionschange` from the widget. Reference options list matching, defined custom elements with unique IDs. These elements come from the widget's document or shadow root.\n\nThe **Import and export** group lists the file formats with handlers in `convertFrom` and `convertTo`. The authoring tool handles native tag conversions. An import handler receives one `File` and returns a function that applies the result. An export handler returns a `File` for download. The authoring tool and standalone drawer use the same [conversion interface](runtime.md#conversions).\n\n### Saving results\n\nSupply an inert data block as a direct child. Wait until `dataReady` is true. Write through `data` or `setData()`. The drawer restores the result for the current destination before it saves updates. [Standalone storage](standalone.md) describes browser storage, endpoints, and save notifications.\n\n### Custom storage\n\nFor a different authentication method, pass a `storageFor(destination, key)` function to `WidgetDrawer`. For an HTTP destination, return an `HttpWidgetResultStorage` that uses your authenticated fetch function. Keep credentials in the host's authentication flow. The [HTTP endpoint interface](standalone.md#http-endpoint) describes the requests and responses.\n"},{"id":"reference/options-actions","source":"# B2. Options and actions\n\nThe authoring tool uses option and action declarations to show controls for a widget. An embedded Lit widget can use the same declarations in a [standalone drawer](lit.md#standalone-widget-drawer).\n\nDeclare option groups, options, actions, and permissions on the class or instance. An instance declaration has priority over the matching class declaration. This also applies to an empty map.\n\nA getter can return declarations that change. When a change affects the result, dispatch a [bubbling](https://developer.mozilla.org/en-US/docs/Web/API/Event/bubbles) `optionschange` event from the widget host. This tells the authoring tool to read the declarations again.\n\n`dynamicOptions` is a legacy alias for `options`. When both declare the same name, `options` has priority.\n\n```js\nstatic optionGroups = {\n appearance: {\n label: {_: \"Appearance\"},\n description: {_: \"Control how the widget looks.\"},\n collapsible: true,\n collapsed: false,\n advanced: false,\n },\n}\n\nget options() {\n return {\n spacing: {\n type: Number,\n group: \"appearance\",\n label: {_: \"Spacing\"},\n min: 0,\n max: 24,\n unit: \"px\",\n disabled: this.compact,\n disabledReason: {_: \"Spacing is unavailable in compact mode.\"},\n },\n }\n}\n\nstatic actions = {\n reset: {\n label: {_: \"Reset\"},\n group: \"appearance\",\n intent: \"danger\",\n confirm: {_: \"Reset this widget's appearance?\"},\n },\n}\n\nreset() { /* update the widget DOM */ }\n\nrefreshOptions() {\n this.dispatchEvent(new Event(\"optionschange\", {bubbles: true}))\n}\n```\n\n## Option declaration\n\n| Field | Type | Default / meaning |\n|---|---|---|\n| `type` | Constructor or type string | `String` / `\"string\"`. See supported types below. |\n| `attribute` | `string` or `false` | An explicit declaration has priority. The default is the matching Lit property's [attribute mapping](https://lit.dev/docs/components/properties/#setting-the-attribute-name), or the lowercase option name if there is no mapping. `false` writes only the property. |\n| `label` | `LocalizedText` | Option name in title case. |\n| `description`, `placeholder` | `LocalizedText` | No description or placeholder. |\n| `group` | `string` | No group. Matches an ID in `optionGroups`. Options and actions can use the same group. |\n| `hidden`, `disabled`, `readOnly` | `boolean` | `false`. These fields control visibility and access to the control. |\n| `disabledReason` | `LocalizedText` | No explanation shown for a disabled option. |\n| `control` | `\"checkbox\"`, `\"switch\"`, `\"select\"`, `\"radio\"`, `\"segmented\"`, `\"input\"`, `\"range\"`, `\"duration\"`, or `\"tree\"` | The default depends on the option type. This field selects the control and keeps the value type the same. `radio` and `segmented` show one choice. See [Specialized options](#specialized-options) for `duration` and `tree`. |\n| `unit` | `string` | No unit label. The label applies to the display only. |\n| `steps` | `number[]` | No list of values. Use this field for a number range with unequal intervals. |\n| `multiline` | `boolean` | `false`. Permits string input on multiple lines. |\n| `min`, `max`, `step` | `number` | No numeric input limits. |\n| `pattern` | `string` | No input pattern. |\n| `minlength`, `maxlength` | `number` | No text length limits. |\n| `swatches` | `string[]` | No suggested colors. |\n| `multiple` | `boolean` | `false`. Permits more than one value in a selection. |\n| `options` | Leaf or branch entries | Lists choices for `select`. A leaf has `value: string` and can have a `label` and `description` as `LocalizedText`. A tree also accepts branches with an optional `label` and recursive `children`. A leaf without a label shows its value. |\n| `selector`, `excludeSelf` | `string`, `boolean` | Apply to `reference`. The default [selector](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors/Selectors_and_combinators) is `\"*\"`. The default for `excludeSelf` is `true`. |\n\nSupported constructors are `Boolean`, `String`, `Number`, `Object`, and `Array`. Supported type strings are `boolean`, `string`, `number`, `date`, `datetime-local`, `email`, `password`, `tel`, `time`, `url`, `color`, `select`, `reference`, `object`, and `array`.\n\nFor a number option, `control: \"input\"` shows a standard number field. The choice of control keeps the stored value format and action arguments the same.\n\n### Specialized options\n\nThe widget supplies a property for each option. With Lit, declare duration properties as `Number` in `static properties`. Declare tree selections as `Array`. Declare references as `String`. Use the same reflected attribute names as the options.\n\nThe `duration` control stores a number in seconds. Use `control: \"duration\"` with `type: Number` or `type: \"number\"`. The control accepts seconds, `M:SS`, or `H:MM:SS`, with or without fractional seconds. It can display durations of 24 hours or more.\n\n`min`, `max`, and `step` also use seconds. The control rejects invalid and negative values. `null` or a blank value resets the option.\n\nThe `tree` control uses `type: \"select\"` and `multiple: true`. A leaf has a `value` and can have a `label` or `description`. A branch has `children` and can have a `label`. A tree can have up to eight levels and 1,000 entries.\n\nThe control stores only leaf values, as a JSON array of strings. A branch shows the selection state of its leaves. If a leaf value occurs more than once, the control uses the first occurrence. The control keeps unknown stored IDs when the user changes a known selection.\n\n```js\nstatic options = {\n duration: {type: Number, control: \"duration\", min: 0, max: 7200, step: 1},\n permissions: {\n type: \"select\", multiple: true, control: \"tree\",\n options: [\n {label: {_: \"Pages\"}, children: [\n {value: \"read\", label: {_: \"Read\"}},\n {value: \"write\", label: {_: \"Write\"}},\n ]},\n {value: \"settings\", label: {_: \"Settings\"}},\n ],\n },\n}\n```\n\n`reference` selects an installed widget by its `id`. The control lists installed widgets in the current document body that match `selector`. The default selector is `\"*\"`. With `excludeSelf: true`, the list excludes the widget that has the option. This is the default. Each candidate must have a unique ID in the document.\n\nThe control uses `aria-label`, `title`, or the tag name, in that order. It adds `#ID` to each label. The control shows a missing or invalid current ID and keeps it until the user clears or replaces it. The authoring tool checks a candidate again when the user selects it.\n\n```js\nstatic options = {\n relatedSection: {\n type: \"reference\", selector: \"webwriter-model-training\", excludeSelf: true,\n label: {_: \"Related section\"},\n },\n}\n```\n\nFile input actions declare `input: {type: \"file\", accept?: string, multiple?: boolean}`. The method receives a `File[]` even when `multiple` is false. [`accept`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept) controls which files the chooser shows. Check the contents in the widget.\n\nIf the user selects **Cancel**, the chooser closes without calling the method or adding an undo step. The files are temporary input. Before the method runs, the authoring tool checks that the widget is still connected. The authoring tool also checks the action declaration, its availability, and confirmation. Changes to the DOM use the action's standard undo behavior. An action without `input` receives no arguments.\n\n```js\nstatic actions = {\n importData: {\n label: {_: \"Import data\"},\n input: {type: \"file\", accept: \".json,application/json\", multiple: false},\n },\n}\n\nasync importData(files) {\n const [file] = files\n if (!file) return\n const data = JSON.parse(await file.text())\n // Apply parsed data to this widget's DOM.\n}\n```\n\nBefore a write, the authoring tool checks `hidden`, `disabled`, and `readOnly`. It checks the value against the current type, limits, number steps, and choices. `null` and an empty string are valid reset values. If a declaration getter throws an error, the authoring tool rejects the write.\n\nThe authoring tool reads `optionschange` from the widget host. Dispatch the event there when the authoring tool must read the declarations again.\n\nThe authoring tool writes an empty attribute for a true boolean. A false boolean removes the attribute. Objects, arrays, and multiple selections use JSON. The authoring tool converts other values to strings. `null` and an empty string remove the attribute. An option with `attribute: false` has temporary instance state.\n\nThe authoring tool reads the current property value to display an option. Make the property reflect the attribute or supply a getter. The authoring tool rejects `style`, `class`, `is`, authoring-only attributes, event handlers, and unsafe URLs as option attributes.\n\n[Options and actions in Lit](lit.md#options-and-actions-in-lit) describes reactive properties and attribute mapping. Drawer controls ignore the input hints `autocapitalize`, `spellcheck`, `autocomplete`, and `inputmode`.\n\n## Option groups\n\n`optionGroups` is a record that uses group IDs as keys. A group can have a `label` and `description` as `LocalizedText`. It can also have `collapsible`, `collapsed`, and `advanced` booleans.\n\nThe drawer shows ungrouped items first. It then shows groups in `optionGroups` declaration order. Options and actions keep their declaration order within each group. If a group has no matching declaration, the drawer shows its ID as the label. The drawer hides empty groups.\n\n`advanced: true` makes a group collapsible and closes it initially. For other collapsible groups, `collapsed` sets the initial state.\n\nVisibility and availability control the widget interface only. Use participant permissions for document editing rights.\n\n## Element permissions\n\nDeclare `permissions` on the class or instance. Use the boolean property names as keys. The authoring tool shows the checkboxes under **Distribute → selected element permissions → Edit**. Each checkbox works independently of **Edit**. If the element is hidden from participants, the authoring tool disables these checkboxes. The checkboxes appear only in the element permissions controls.\n\n```js\nclass QuizWidget extends LitElementWw {\n static properties = {\n allowHints: {type: Boolean, attribute: \"allow-hints\", reflect: true},\n }\n\n static permissions = {\n allowHints: {\n label: {_: \"Allow hints\"},\n description: {_: \"Let learners reveal a hint.\"},\n },\n }\n\n constructor() {\n super()\n this.allowHints = false\n }\n}\n```\n\nA selected permission adds an empty attribute to the widget. Clearing the permission removes the attribute. The default attribute name comes from the matching Lit property. If there is no Lit declaration, the authoring tool uses the lowercase property name. If the declaration specifies `attribute`, use the same attribute in the widget. Each permission requires an attribute.\n\nA permission can have a `label` and `description` as `LocalizedText`. It can also have `hidden`, `disabled`, `disabledReason`, and `readOnly`. The widget reads the boolean to select which interactions to show. Manage document editing rights and network access separately.\n\n## Action declaration\n\nActions support `label` and `description` as `LocalizedText`. The default label is the method name in title case. The `group` field assigns the action to an option group. The `hidden` and `disabled` fields control availability. The `disabledReason` field supplies the tooltip for a disabled action. Set `icon` to a supported ribbon icon name.\n\n`intent` is `\"default\"` or `\"danger\"`. `confirm` supplies a `LocalizedText` prompt before the action runs. The optional `input` declaration opens a file chooser.\n\nGive adjacent actions the same `buttonGroup` string to join their buttons. Each option group shows its buttons separately. The drawer omits hidden actions. A group with one visible action shows one button.\n\n```js\nstatic actions = {\n completeDfaWithSink: {group: \"editing\"},\n switchToNfa: {group: \"editing\", buttonGroup: \"automaton-type\", label: {_: \"Switch to NFA\"}},\n switchToPda: {group: \"editing\", buttonGroup: \"automaton-type\", label: {_: \"Switch to PDA\"}},\n}\n```\n\nSupply a method with the same name. The authoring tool checks the current availability of the action before it runs. If the action requires confirmation, the authoring tool asks the user first. Changes to the DOM form one undo step.\n\nA returned promise keeps the undo group open until it settles or ten seconds pass. The method can continue after that limit. The authoring tool rejects a second action on the same widget while the first action runs.\n\nSee [localized text](editing-configuration.md) for locale matching.\n"},{"id":"reference/runtime","source":"# B1. Runtime interface\n\nA widget script defines the [custom element](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements) for its export. For example, `./widgets/ww-quiz.js` defines `ww-quiz`.\n\nThe widget class extends `HTMLElement` or a compatible class such as [`LitElementWw`](lit.md#litelementww).\n\n| Interface | Purpose |\n|---|---|\n| `constructor()` | Creates shadow DOM and local state. The widget reads authored child content after connection. |\n| `connectedCallback()` | Starts observers and external event listeners. The browser can call it more than once. |\n| `disconnectedCallback()` | Stops observers and removes external event listeners. |\n| `static observedAttributes` | Names of attributes that cause a call to `attributeChangedCallback` when they change. |\n| `attributeChangedCallback(name, oldValue, value)` | Receives local, remote, and undo changes to observed attributes. |\n| `isContentEditable` | Indicates whether a user can edit the widget. |\n| `focus(options?)` | Can move focus to a control in the widget. |\n| `static options` / `options` | [Option declarations](options-actions.md) for the Widget drawer. |\n| `static actions` / `actions` | [Action declarations](options-actions.md). Each action has a method with the same name. |\n| `static effects` / `effects`, `signals` | [Reactive links](#signals) between authored widgets. |\n| `static convertFrom` / `convertFrom`, `static convertTo` / `convertTo` | [DOM conversions and file import or export](../guides/conversions.md). |\n| `static aggregate` | [Aggregated View](aggregated-view.md) declaration for aggregate views. |\n| `static offlineImports(element)` | [Select optional dynamic imports](#offline-imports) for Offline HTML. |\n\nThe authoring tool gives an installed widget a persistent `id` if the widget has none. During editing, the authoring tool adds `contenteditable=\"\"` to widgets that have no `contenteditable` attribute. The authoring tool saves and shares authored values such as `contenteditable=\"false\"`. The authoring tool omits its temporary value from saved and shared HTML.\n\nThe authoring tool handles shadow DOM events of the types in `propagateEvents`. The authoring tool also synchronizes changes that the widget makes to the authored DOM.\n\n## Signals\n\nWidgets can publish signals. Other widgets can read these signals and respond to changes.\n\nDeclare one entry in `static effects` for each supported lowercase effect attribute. Each entry needs a `connect(signal, lifetime)` callback. It can also have a localized `label` and `description`. The authoring tool calls `connect` with the receiving widget as `this`. The `lifetime` argument is an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). Clear the source when the signal aborts.\n\nThe source widget's `signals` object maps public names to read-only signals with a `get()` method. Keep these signal instances stable.\n\nAn effect attribute uses CSS selector syntax, such as `#sensor[celsius]`. The ID identifies one source widget in the document. The name in brackets identifies a public signal in `source.signals`.\n\nThe authoring tool keeps unresolved or circular links in the HTML. The authoring tool connects these links when the connections become valid.\n\nThe [Make data reactive guide](../guides/reactivity.md) includes a working example. [Lit signals](lit.md#signals) describes the Lit interface and the shared dependency requirements.\n\n## Offline imports\n\nThe optional `offlineImports` method limits the code in an Offline HTML export. It returns its result synchronously.\n\nThe authoring tool passes a detached copy of each authored widget element. This includes widgets in HTML templates and templates that readers can insert. Read the attributes or child content of the copy. Keep the copy unchanged. Include all choices that a reader can use offline.\n\n```js\nstatic offlineImports(element) {\n const engine = element.getAttribute(\"engine\") ?? \"basic\"\n return [{importer: \"./engine-loaders.js\", keep: [`./engines/${engine}.js`]}]\n}\n```\n\n`importer` identifies the module with the [`import()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import) calls. Use a path relative to the widget entry module or an absolute HTTP(S) URL. If widgets share a loader module, declare the requirements for that loader in each widget.\n\n`keep` lists the specifiers exactly as they appear in the module's `import()` calls. An empty list excludes all targets. `\"*\"` keeps all targets. The authoring tool combines the lists for the same importer across all widget instances. `\"*\"` has priority.\n\nThe authoring tool bundles the selected imports and their dependencies. In the standalone file, an excluded call rejects with `OfflineImportError`. The usual bundle rules apply to static imports and to dynamic imports from modules without an `offlineImports` rule.\n\n[Manage dependencies](../guides/libraries.md#optimize-the-code-bundle) gives a loader example and steps to test the export.\n\n## Conversions\n\nEach key identifies a format and specifies what the handler receives or returns. The import and export maps are independent. A conversion can keep only the content that the target format can represent.\n\n| Key | `convertFrom` | `convertTo` |\n|---|---|---|\n| Lowercase native HTML tag, such as `ul` | Receives a detached `DocumentFragment` with a copy of the source element. Sets the content of the new widget synchronously. Returns nothing or `true` for success. Returns `false` to decline. | Returns a detached element or a detached fragment with one or more sibling elements. Each element uses the declared tag. The fragment can contain comments and formatting whitespace. Returns `null` to decline. |\n| Extension such as `.h5p` or concrete media type such as `image/png` | Receives one `File`. Returns a synchronous function that applies the prepared content to the existing widget. Returns `null` or `false` to decline. Can prepare its result asynchronously. | Returns a `File` with a matching extension or media type. Returns `null` or `false` to decline. Can return a promise. |\n\nUse method names or function references. The authoring tool calls the handlers with the widget as `this`. An instance map has priority over a static map. Use `static convertFrom` to declare the DOM tags that a new widget accepts.\n\nThe authoring tool checks that the replacement is valid at the source location. For a file import, the authoring tool checks the format and confirms that the original widget can still receive the result. Apply authored changes only in the function that the file handler returns. The authoring tool records these changes as one undo step.\n\nAn export handler reads the widget state and returns the result for download. A file handler has ten seconds to return its result. The [conversion guide](../guides/conversions.md) gives examples.\n\n## Scoped custom element registries\n\nWidget documents require a browser with native scoped custom element registries. Create the registry with [`new CustomElementRegistry()`](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/CustomElementRegistry). Attach the shadow root with `attachShadow({mode: \"open\", customElementRegistry: registry})`.\n\nOlder widget packages can use the legacy interfaces in the table when the authoring tool provides compatibility support.\n\n| Legacy API | Native operation |\n|---|---|\n| `attachShadow({customElements: registry})` or `{registry}` | `attachShadow({customElementRegistry: registry})` |\n| `shadowRoot.customElements` or `.registry` | Read `shadowRoot.customElementRegistry` |\n| `shadowRoot.createElement()` / `.createElementNS()` | Owning document's creation method with the root's registry |\n| `shadowRoot.importNode(node, deep)` | Owning document's `importNode(node, {customElementRegistry, selfOnly: !deep})` |\n\nThe native `customElementRegistry` option has priority over the legacy options. Registry aliases are read-only. The browser applies its standard registry rules.\n\nRemove `@webcomponents/scoped-custom-element-registry` from the widget's imports and dependencies. Use the native interfaces in new standalone widgets. If a standalone page uses legacy widgets, load a compatibility adapter before their modules.\n"},{"id":"reference/standalone","source":"# B6. Standalone storage\n\nAn embedded widget can save results in the browser or to an HTTP endpoint through the [standalone widget drawer](lit.md#standalone-widget-drawer).\n\nSet `standalone` on the widget to enable the drawer. An empty attribute selects browser storage. A URL selects the initial HTTP destination.\n\n```html\n<ww-quiz id=\"practice-quiz\" standalone></ww-quiz>\n<ww-quiz id=\"assignment-quiz\" standalone=\"https://example.org/api/results/assignment-quiz\"></ww-quiz>\n```\n\nSupply an inert [data block](data.md) as a direct child. Set `slot=\"data\"` and use type `application/json` or `application/xml`. The widget can listen for `datapersistence` on the block to check the save status.\n\n## Browser storage\n\nBrowser storage uses the page's [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). Give each widget a stable, unique `id` to keep its storage key when it moves on the page. For a widget without an ID, the storage key uses its position.\n\nUnder **Autosave to**, select **This browser** to change to browser storage immediately. For a **Remote endpoint**, enter its URL. Select **Apply**. The drawer remembers the destination when local storage is available.\n\nEach destination loads its own result or the widget's initial data. When you change the destination, results stay in their original store.\n\nRun [`npm start`](https://docs.npmjs.com/cli/v11/commands/npm-start/) from the authoring tool package. Open the [standalone quiz demo](http://127.0.0.1:1234/dev-server/standalone-quiz/) to try both storage modes. The demo HTTP endpoint keeps one result until the local development server stops.\n\n## HTTP endpoint\n\nThe endpoint URL identifies a result resource. If there are multiple participants, use the authenticated request to identify the participant. Permit access only to the matching result. Use the same URL for both methods.\n\n| Request | Response |\n| --- | --- |\n| `GET` | Return `200` with the raw JSON or XML data, the matching `Content-Type`, and a strong [`ETag`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag). If no saved result exists, return `404`. |\n| `PUT` | Accept the raw data with its `Content-Type`. Apply `If-None-Match: *` when you create a result. Apply `If-Match: \"<revision>\"` when you update a result. Return `201` for a new result. Return `200` or `204` for an update. If a precondition fails, return `412`. |\n\nIf the server stores the data as sent, return the new strong `ETag` after a successful write. If the write response has no `ETag`, the drawer reads the result again. It uses the revision only if the data from the read matches the write.\n\nReturn `Cache-Control: no-store` for participant results. If a newer result exists, the drawer keeps the pending edit. It also sends a `datapersistence` error.\n\nThe drawer accepts HTTP and HTTPS URLs. It resolves relative URLs against the page base URL. Use HTTPS outside local development.\n\nFor a [cross-origin endpoint](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS), permit the origin of the page. Permit the methods `GET` and `PUT`. Permit the request headers `Content-Type`, `If-Match`, and `If-None-Match`. Expose the `ETag` response header. The supplied storage adapter sends session cookies for requests to the same origin. [Custom storage in the Lit reference](lit.md#custom-storage) describes how to use a different authentication method.\n\nThis endpoint stores the latest result. The widget can use a separate action to submit a final result.\n"},{"id":"reference/styling","source":"# B5. Design tokens\n\nPut the widget interface and its styles in [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM). Show authored child content through slots. Export a separate stylesheet for styles that apply to light DOM or to the document.\n\n| Hook | Meaning |\n|---|---|\n| `:host` | The widget element. |\n| `:host([contenteditable])` | A widget with the `contenteditable` attribute. [Support editing](../guides/editing.md) describes how to handle `contenteditable=\"false\"`. |\n| `::slotted(…)` | Light DOM elements that the browser assigns to a slot. |\n| [`part` / `::part(…)`](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Shadow_parts) | Named parts that a document stylesheet can style. Declare controls separately. |\n| `@media print` | Printed presentation. |\n| [`@container`](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Containment/Container_queries) | Layout based on allocated space. |\n\n[Follow the theme](../guides/theming.md) describes document fonts, accent colors, and named parts.\n\nThe base theme supplies these properties. A widget can use them in its host styles.\n\n| Property | Default | Purpose |\n|---|---|---|\n| `--ww-page-max` | `2160px` | Maximum page content width, excluding gutters. |\n| `--ww-prose-max` | `45rem` | Normal reading width. |\n| `--ww-page-gutter` | `clamp(1rem, 2vw, 2rem)` | Responsive page gutters. |\n| `--ww-widget-max` | Uses the prose width by default. | Maximum width of one widget. `none` permits the available page width. |\n\nLet the content of a block widget wrap to fit its parent. Set `min-height` and `max-height` to limit its height. Inline widgets set their own inline styles. The [responsive layout guide](../guides/responsiveness.md) gives recommended widths and a stylesheet example.\n\nUse authored classes, widget parts, or [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Cascading_variables/Using_custom_properties) for styles that the document must keep. Keep temporary interface state in the widget's shadow DOM.\n"},{"id":"sandbox","source":"# Sandbox\n\nChoose a library and an example. Edit the source or HTML to see your changes in the preview.\n\n<!-- live-example: Hello world -->\n\n```js vanilla\nclass HelloWidget extends HTMLElement {\n constructor() {\n super()\n this.attachShadow({mode: \"open\"}).textContent = \"Hello world\"\n }\n}\n\ncustomElements.define(\"ww-hello\", HelloWidget)\n```\n\n```js lit\nimport {LitElement, html} from \"lit\"\n\nclass HelloWidget extends LitElement {\n render() {\n return html`Hello world`\n }\n}\n\ncustomElements.define(\"ww-hello\", HelloWidget)\n```\n\n```js react\nimport {createRoot} from \"react-dom/client\"\n\nclass HelloWidget extends HTMLElement {\n constructor() {\n super()\n createRoot(this.attachShadow({mode: \"open\"})).render(\"Hello world\")\n }\n}\n\ncustomElements.define(\"ww-hello\", HelloWidget)\n```\n\n```js vue\nimport {defineCustomElement, h} from \"vue\"\n\nconst HelloWidget = defineCustomElement({\n render: () => h(\"span\", \"Hello world\"),\n})\n\ncustomElements.define(\"ww-hello\", HelloWidget)\n```\n\n```js angular\nimport \"@angular/compiler\"\nimport {Component, ViewEncapsulation} from \"@angular/core\"\nimport {createCustomElement} from \"@angular/elements\"\nimport {createApplication} from \"@angular/platform-browser\"\n\nclass HelloWidget {}\n\nComponent({\n selector: \"hello-view\",\n template: \"Hello world\",\n encapsulation: ViewEncapsulation.ShadowDom,\n})(HelloWidget)\n\nconst {injector} = await createApplication()\ncustomElements.define(\"ww-hello\", createCustomElement(HelloWidget, {injector}))\n```\n\n```svelte svelte\n<svelte:options customElement={{tag: \"ww-hello\"}} />\n\nHello world\n```\n\n```html\n<ww-hello></ww-hello>\n```\n\n<!-- /live-example -->\n"},{"id":"tutorial/01-setup","source":"# 1. Set up the widget\n\nMake **Ghost Disco** available in WebWriter as a widget that displays a placeholder. Create a [local package](https://docs.npmjs.com/about-packages-and-modules/) for the [custom element](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements) `ww-ghost-disco`. Select a library in the sidebar before you copy the files. You will need Node.js and npm to build and test the package in later steps.\n\n## Declare the widget in the package\n\nTell WebWriter which module to load for `ww-ghost-disco`. Create a folder for the package and save the following manifest as `package.json`. The `webwriter-widget` keyword identifies the package. The key `./widgets/ww-ghost-disco.js` gives the widget its name, and its value points to the module.\n\n<!-- library:vanilla -->\n\n**`package.json`**\n\n```json package\n{\n \"name\": \"@example/ghost-disco\",\n \"version\": \"1.0.0\",\n \"keywords\": [\n \"webwriter-widget\"\n ],\n \"exports\": {\n \"./widgets/ww-ghost-disco.js\": \"./ww-ghost-disco.js\"\n }\n}\n```\n<!-- /library -->\n\n<!-- library:lit -->\n\n**`package.json`**\n\n```json package\n{\n \"name\": \"@example/ghost-disco-lit\",\n \"version\": \"1.0.0\",\n \"keywords\": [\n \"webwriter-widget\"\n ],\n \"exports\": {\n \"./widgets/ww-ghost-disco.js\": \"./ww-ghost-disco.js\"\n },\n \"dependencies\": {\n \"@webwriter/lit\": \"^3.0.0\",\n \"lit\": \"^3.3.2\"\n }\n}\n```\n<!-- /library -->\n\n<!-- library:react -->\n\n**`package.json`**\n\n```json package\n{\n \"name\": \"@example/ghost-disco-react\",\n \"version\": \"1.0.0\",\n \"keywords\": [\n \"webwriter-widget\",\n \"widget-practical\",\n \"widget-lang-en\"\n ],\n \"exports\": {\n \"./widgets/ww-ghost-disco.js\": \"./dist/ww-ghost-disco.js\"\n },\n \"dependencies\": {\n \"react\": \"^19.3.1\",\n \"react-dom\": \"^19.3.1\"\n },\n \"scripts\": {\n \"build\": \"node build.mjs ghost-disco\"\n },\n \"devDependencies\": {\n \"esbuild-wasm\": \"^0.25.12\"\n }\n}\n```\n<!-- /library -->\n\n<!-- library:vue -->\n\n**`package.json`**\n\n```json package\n{\n \"name\": \"@example/ghost-disco-vue\",\n \"version\": \"1.0.0\",\n \"keywords\": [\n \"webwriter-widget\",\n \"widget-practical\",\n \"widget-lang-en\"\n ],\n \"exports\": {\n \"./widgets/ww-ghost-disco.js\": \"./dist/ww-ghost-disco.js\"\n },\n \"dependencies\": {\n \"vue\": \"^3.5.44\"\n },\n \"scripts\": {\n \"build\": \"node build.mjs ghost-disco\"\n },\n \"devDependencies\": {\n \"@vue/compiler-sfc\": \"^3.5.44\",\n \"esbuild-wasm\": \"^0.25.12\"\n }\n}\n```\n<!-- /library -->\n\n<!-- library:angular -->\n\n**`package.json`**\n\n```json package\n{\n \"name\": \"@example/ghost-disco-angular\",\n \"version\": \"1.0.0\",\n \"keywords\": [\n \"webwriter-widget\",\n \"widget-practical\",\n \"widget-lang-en\"\n ],\n \"exports\": {\n \"./widgets/ww-ghost-disco.js\": \"./dist/ww-ghost-disco.js\"\n },\n \"dependencies\": {\n \"@angular/compiler\": \"^22.2.2\",\n \"@angular/core\": \"^22.2.2\",\n \"@angular/elements\": \"^22.2.2\",\n \"@angular/platform-browser\": \"^22.2.2\",\n \"rxjs\": \"^7.8.2\",\n \"@angular/common\": \"^22.2.2\"\n },\n \"scripts\": {\n \"build\": \"node build.mjs ghost-disco\"\n },\n \"devDependencies\": {\n \"esbuild-wasm\": \"^0.25.12\"\n }\n}\n```\n<!-- /library -->\n\n<!-- library:svelte -->\n\n**`package.json`**\n\n```json package\n{\n \"name\": \"@example/ghost-disco-svelte\",\n \"version\": \"1.0.0\",\n \"keywords\": [\n \"webwriter-widget\",\n \"widget-practical\",\n \"widget-lang-en\"\n ],\n \"exports\": {\n \"./widgets/ww-ghost-disco.js\": \"./dist/ww-ghost-disco.js\"\n },\n \"dependencies\": {\n \"svelte\": \"^5.57.2\"\n },\n \"scripts\": {\n \"build\": \"node build.mjs ghost-disco\"\n },\n \"devDependencies\": {\n \"esbuild-wasm\": \"^0.25.12\"\n }\n}\n```\n<!-- /library -->\n\n## Create and register the custom element\n\nGive the `ww-ghost-disco` tag a class that renders the placeholder.\n\n<!-- library:vanilla optional -->\nSave this file as `ww-ghost-disco.js`. Extend `HTMLElement`. In the constructor, call `super()` before you attach an open shadow root. Put the placeholder in the shadow root and register the class.\n\n**`ww-ghost-disco.js`**\n\n```js\nclass WwGhostDisco extends HTMLElement {\n constructor() {\n super()\n this.attachShadow({mode: \"open\"}).innerHTML = \"<p>Ghost Disco widget</p>\"\n }\n}\n\nif(!customElements.get(\"ww-ghost-disco\")) customElements.define(\"ww-ghost-disco\", WwGhostDisco)\n```\n<!-- /library -->\n\n<!-- library:lit optional -->\nSave this file as `ww-ghost-disco.js`. Extend `LitElementWw`. Return the placeholder from `render()`. Lit creates the shadow root and renders the template when the element connects.\n\n**`ww-ghost-disco.js`**\n\n```js\nimport {LitElementWw} from \"@webwriter/lit\"\nimport {html} from \"lit\"\n\nclass WwGhostDisco extends LitElementWw {\n render() { return html`<p>Ghost Disco widget</p>` }\n}\n\nif(!customElements.get(\"ww-ghost-disco\")) customElements.define(\"ww-ghost-disco\", WwGhostDisco)\n```\n<!-- /library -->\n\n<!-- library:react optional -->\nSave this file as `ww-ghost-disco.jsx`. Define a component that renders the placeholder. Create the React root when the host connects and unmount it when the host disconnects.\n\n**`ww-ghost-disco.jsx`**\n\n```jsx\nimport {createRoot} from \"react-dom/client\"\n\nfunction GhostDisco() {\n return <p>Ghost Disco widget</p>\n}\n\nclass WwGhostDisco extends HTMLElement {\n constructor() {\n super()\n this.attachShadow({mode: \"open\"})\n }\n\n connectedCallback() {\n this.root = createRoot(this.shadowRoot)\n this.refresh()\n }\n\n disconnectedCallback() {\n this.root.unmount()\n this.root = null\n }\n\n refresh() {\n this.root.render(<GhostDisco />)\n }\n}\n\nif(!customElements.get(\"ww-ghost-disco\")) customElements.define(\"ww-ghost-disco\", WwGhostDisco)\n```\n<!-- /library -->\n\n<!-- library:vue optional -->\nSave this file as `ww-ghost-disco.vue`. Put the placeholder in the component template. In the registration module, adapt the component with `defineCustomElement()` and register the resulting class.\n\n**`ww-ghost-disco.vue`**\n\n```vue\n<template>\n <p>Ghost Disco widget</p>\n</template>\n```\n\nSave the module beside the component. Keep the import relative to this file.\n\n**`ww-ghost-disco.js`**\n\n```js\nimport {defineCustomElement} from \"vue/dist/vue.runtime.esm-browser.js\"\nimport GhostDisco from \"./ww-ghost-disco.vue\"\n\nconst WwGhostDisco = defineCustomElement(GhostDisco)\n\nif(!customElements.get(\"ww-ghost-disco\")) customElements.define(\"ww-ghost-disco\", WwGhostDisco)\n```\n<!-- /library -->\n\n<!-- library:angular optional -->\nSave this file as `ww-ghost-disco.ts`. Declare a standalone component with shadow DOM encapsulation. Adapt it with `createCustomElement()`. The host removes Angular's runtime version stamp from the saved markup.\n\n**`ww-ghost-disco.ts`**\n\n```ts\nimport \"@angular/compiler\"\nimport {Component, ViewEncapsulation} from \"@angular/core\"\nimport {createApplication} from \"@angular/platform-browser\"\nimport {createCustomElement} from \"@angular/elements\"\n\n@Component({\n selector: \"ww-ghost-disco-view\",\n standalone: true,\n template: \"<p>Ghost Disco widget</p>\",\n encapsulation: ViewEncapsulation.ShadowDom,\n})\nclass GhostDisco {}\n\nconst app = await createApplication()\nconst AngularGhostDiscoElement = createCustomElement(GhostDisco, {injector: app.injector}) as unknown as {\n new(): HTMLElement & {connectedCallback(): void}\n}\nclass GhostDiscoElement extends AngularGhostDiscoElement {\n connectedCallback(): void {\n const version = this.getAttribute(\"ng-version\")\n super.connectedCallback()\n // Keep Angular's runtime stamp out of the authored document.\n if(version === null) this.removeAttribute(\"ng-version\")\n else this.setAttribute(\"ng-version\", version)\n }\n}\n\nif(!customElements.get(\"ww-ghost-disco\")) customElements.define(\"ww-ghost-disco\", GhostDiscoElement)\n```\n<!-- /library -->\n\n<!-- library:svelte optional -->\nSave this file as `ww-ghost-disco.svelte`. Set the tag in `<svelte:options>`. The build compiles this file as a custom element. Put the placeholder after the options.\n\n**`ww-ghost-disco.svelte`**\n\n```svelte\n<svelte:options customElement={{ tag: \"ww-ghost-disco\" }} />\n\n<p>Ghost Disco widget</p>\n```\n<!-- /library -->\n\n## Load the package and check the placeholder\n\nConfirm that inserting the widget in WebWriter displays **Ghost Disco widget**.\n\n<!-- library:vanilla optional -->\nLoad the JavaScript module through the package export. The JS version uses browser APIs.\n<!-- /library -->\n\n<!-- library:lit optional -->\nLoad the JavaScript module through the package export. The manifest declares the Lit dependencies so WebWriter can resolve the imports.\n<!-- /library -->\n\n<!-- library:react optional -->\nSave [build.mjs](../examples/react/build.mjs) beside the manifest. Install the dependencies. Then build the widget.\n\n**Terminal**\n\n```sh prompt\nnpm install\nnpm run build\n```\n\nCheck that the build created `dist/ww-ghost-disco.js`. This is the file named in the widget export.\n<!-- /library -->\n\n<!-- library:vue optional -->\nSave [build.mjs](../examples/vue/build.mjs) and [compile.mjs](../examples/vue/compile.mjs) beside the manifest. Install the dependencies. Then build the widget.\n\n**Terminal**\n\n```sh prompt\nnpm install\nnpm run build\n```\n\nCheck that the build created `dist/ww-ghost-disco.js`. This is the file named in the widget export.\n<!-- /library -->\n\n<!-- library:angular optional -->\nSave [build.mjs](../examples/angular/build.mjs) beside the manifest. Install the dependencies. Then build the widget.\n\n**Terminal**\n\n```sh prompt\nnpm install\nnpm run build\n```\n\nCheck that the build created `dist/ww-ghost-disco.js`. This is the file named in the widget export.\n<!-- /library -->\n\n<!-- library:svelte optional -->\nSave [build.mjs](../examples/svelte/build.mjs) beside the manifest. Install the dependencies. Then build the widget.\n\n**Terminal**\n\n```sh prompt\nnpm install\nnpm run build\n```\n\nCheck that the build created `dist/ww-ghost-disco.js`. This is the file named in the widget export.\n<!-- /library -->\n\nIn WebWriter, open **Developer console → Add package → Add from local folder**. Select the folder that contains `package.json` and insert Ghost Disco in a document. In the live example, switch to **package.json** to check the manifest and back to **Source** to check the registration.\n\n## Results\n\n<!-- live-example: Ghost Disco · Set up the widget -->\n\n<!-- live-package: . -->\n\n<!-- live-file: react build.mjs ../examples/react/build.mjs -->\n\n<!-- live-file: vue build.mjs ../examples/vue/build.mjs -->\n\n<!-- live-file: angular build.mjs ../examples/angular/build.mjs -->\n\n<!-- live-file: svelte build.mjs ../examples/svelte/build.mjs -->\n\n<!-- live-file: vue compile.mjs ../examples/vue/compile.mjs -->\n\n<!-- live-source: vanilla ../examples/01-disco-setup.js -->\n\n<!-- live-source: lit ../examples/lit/01-disco-setup.js -->\n\n<!-- live-source: react ../examples/react/01-disco-setup.jsx -->\n\n<!-- live-source: vue ../examples/vue/01-disco-setup.vue -->\n\n<!-- live-source: vue-registration ../examples/vue/01-disco-setup.js -->\n\n<!-- live-source: angular ../examples/angular/01-disco-setup.ts -->\n\n<!-- live-source: svelte ../examples/svelte/01-disco-setup.svelte -->\n\n```html\n<ww-ghost-disco></ww-ghost-disco>\n```\n\n<!-- /live-example -->\n"},{"id":"tutorial/02-rendering","source":"# 2. Authoring\n\n[Support editing](../guides/editing.md)\n\nLet authors edit the invitation in the document while readers see the dance controls.\n\n## Put the invitation in the document\n\nDisplay the document’s invitation through the widget’s default slot. Add a paragraph to Ghost Disco in the document. The slot’s own paragraph appears when the widget has no invitation.\n\n**`index.html`**\n\n```html\n<ww-ghost-disco>\n <p>Join the midnight dance party.</p>\n</ww-ghost-disco>\n```\n\n\nMerge this entry into `package.json`. The content expression `p+` allows one or more paragraphs. WebWriter lets the author edit those paragraphs in the document.\n\n**`package.json`**\n\n```json package\n{\n \"editingConfig\": {\n \"./widgets/ww-ghost-disco\": {\n \"label\": \"Ghost Disco\",\n \"content\": \"p+\"\n }\n }\n}\n```\n\n\n## Add the dance floor\n\nShow the invitation hint in Editing mode and the dance buttons in Reading mode. Replace the placeholder with a slot, a decorative ghost, and two native buttons. The buttons will select a move in step 4.\n\n<!-- library:vanilla optional -->\nReplace the placeholder string inside the constructor with a template literal containing the markup below. Put the styles from the next subsection before the markup, inside the same string. Then add the methods to the class.\n\n**`ww-ghost-disco.js`**\n\n```html\n<div class=\"stage\">\n <slot><p>Join the midnight dance party.</p></slot>\n <span class=\"ghost\" aria-hidden=\"true\">👻</span>\n <div class=\"controls\">\n <button type=\"button\" value=\"float\">Float</button>\n <button type=\"button\" value=\"twirl\">Twirl</button>\n </div>\n <p class=\"author-hint\" hidden>Edit the invitation in the document.</p>\n <output aria-live=\"polite\">Pick a dance move.</output>\n</div>\n```\n\n**`ww-ghost-disco.js`**\n\n```js\nstatic observedAttributes = [\"contenteditable\"]\n\nconnectedCallback() { this.updateMode() }\nattributeChangedCallback() { this.updateMode() }\n\nupdateMode() {\n this.shadowRoot.querySelector(\".controls\").hidden = this.isContentEditable\n this.shadowRoot.querySelector(\".author-hint\").hidden = !this.isContentEditable\n}\n```\n<!-- /library -->\n\n<!-- library:lit optional -->\nReplace `render()`. The base class supplies `this.editable`. Import `css` beside `html` for the next subsection.\n\n**`ww-ghost-disco.js`**\n\n```js\nrender() {\n return html`\n <div class=\"stage\">\n <slot><p>Join the midnight dance party.</p></slot>\n <span class=\"ghost\" aria-hidden=\"true\">👻</span>\n <div class=\"controls\" ?hidden=${this.editable}>\n <button type=\"button\" value=\"float\">Float</button>\n <button type=\"button\" value=\"twirl\">Twirl</button>\n </div>\n <p class=\"author-hint\" ?hidden=${!this.editable}>Edit the invitation in the document.</p>\n <output aria-live=\"polite\">Pick a dance move.</output>\n </div>`\n}\n```\n<!-- /library -->\n\n<!-- library:react optional -->\nReplace the `GhostDisco` function. The style string comes from the next subsection. Add the observed attribute and callback to the host class, then replace `refresh()`.\n\n**`ww-ghost-disco.jsx`**\n\n```jsx\nfunction GhostDisco({editing}) {\n return (\n <div className=\"stage\">\n <style>{styles}</style>\n <slot><p>Join the midnight dance party.</p></slot>\n <span className=\"ghost\" aria-hidden=\"true\">👻</span>\n <div className=\"controls\" hidden={editing}>\n <button type=\"button\" value=\"float\">Float</button>\n <button type=\"button\" value=\"twirl\">Twirl</button>\n </div>\n <p className=\"author-hint\" hidden={!editing}>Edit the invitation in the document.</p>\n <output aria-live=\"polite\">Pick a dance move.</output>\n </div>\n )\n}\n```\n\n**`ww-ghost-disco.jsx`**\n\n```jsx\nstatic observedAttributes = [\"contenteditable\"]\n\nattributeChangedCallback() { if(this.root) this.refresh() }\n\nrefresh() {\n this.root.render(<GhostDisco editing={this.isContentEditable} />)\n}\n```\n<!-- /library -->\n\n<!-- library:vue optional -->\nReplace the component with this script and template. The host passes its editing state to the component. Replace the registration module with the second block.\n\n**`ww-ghost-disco.vue`**\n\n```vue\n<script setup>\ndefineProps({editing: Boolean})\n</script>\n\n<template>\n <div class=\"stage\">\n <slot><p>Join the midnight dance party.</p></slot>\n <span class=\"ghost\" aria-hidden=\"true\">👻</span>\n <div class=\"controls\" :hidden=\"editing\">\n <button type=\"button\" value=\"float\">Float</button>\n <button type=\"button\" value=\"twirl\">Twirl</button>\n </div>\n <p class=\"author-hint\" :hidden=\"!editing\">Edit the invitation in the document.</p>\n <output aria-live=\"polite\">Pick a dance move.</output>\n </div>\n</template>\n```\n\n**`ww-ghost-disco.js`**\n\n```js\nimport {defineCustomElement} from \"vue/dist/vue.runtime.esm-browser.js\"\nimport GhostDisco from \"./ww-ghost-disco.vue\"\n\nclass WwGhostDisco extends defineCustomElement(GhostDisco) {\n static observedAttributes = [\"contenteditable\"]\n\n connectedCallback() {\n this.editing = this.isContentEditable\n super.connectedCallback()\n }\n\n attributeChangedCallback() { this.editing = this.isContentEditable }\n}\n\ncustomElements.define(\"ww-ghost-disco\", WwGhostDisco)\n```\n<!-- /library -->\n\n<!-- library:angular optional -->\nReplace the decorator’s `template` string and the empty component class. Add `ChangeDetectorRef`, `ElementRef`, and `inject` to the imports from `@angular/core`. Keep the custom element registration from setup.\n\n**`ww-ghost-disco.ts`**\n\n```html\n<div class=\"stage\">\n <slot><p>Join the midnight dance party.</p></slot>\n <span class=\"ghost\" aria-hidden=\"true\">👻</span>\n <div class=\"controls\" [hidden]=\"editing\">\n <button type=\"button\" value=\"float\">Float</button>\n <button type=\"button\" value=\"twirl\">Twirl</button>\n </div>\n <p class=\"author-hint\" [hidden]=\"!editing\">Edit the invitation in the document.</p>\n <output aria-live=\"polite\">Pick a dance move.</output>\n</div>\n```\n\n**`ww-ghost-disco.ts`**\n\n```ts\nclass GhostDisco {\n private readonly host = inject(ElementRef<HTMLElement>).nativeElement\n private readonly changeDetector = inject(ChangeDetectorRef)\n private readonly observer = new MutationObserver(() => this.changeDetector.markForCheck())\n\n get editing(): boolean { return this.host.isContentEditable }\n\n ngOnInit(): void {\n this.observer.observe(this.host, {attributes: true, attributeFilter: [\"contenteditable\"]})\n }\n\n ngOnDestroy(): void { this.observer.disconnect() }\n}\n```\n<!-- /library -->\n\n<!-- library:svelte optional -->\nKeep `<svelte:options>`. Replace the placeholder with this script and markup.\n\n**`ww-ghost-disco.svelte`**\n\n```svelte\n<script>\n const host = $host()\n let editing = $state(false)\n $effect(() => {\n const update = () => { editing = host.isContentEditable }\n const observer = new MutationObserver(update)\n observer.observe(host, {attributes: true, attributeFilter: [\"contenteditable\"]})\n update()\n return () => observer.disconnect()\n })\n</script>\n\n<div class=\"stage\">\n <slot><p>Join the midnight dance party.</p></slot>\n <span class=\"ghost\" aria-hidden=\"true\">👻</span>\n <div class=\"controls\" hidden={editing}>\n <button type=\"button\" value=\"float\">Float</button>\n <button type=\"button\" value=\"twirl\">Twirl</button>\n </div>\n <p class=\"author-hint\" hidden={!editing}>Edit the invitation in the document.</p>\n <output aria-live=\"polite\">Pick a dance move.</output>\n</div>\n```\n<!-- /library -->\n\n## Add the shadow styles\n\nGive the dance floor its stage layout and make sure the controls respect the `hidden` attribute. Use the same rules for each library.\n\n<!-- library:vanilla optional -->\nPut the rules in a `<style>` element before the stage inside the constructor string.\n<!-- /library -->\n\n<!-- library:lit optional -->\nPut the rules in a `static styles` field with the `css` tag.\n<!-- /library -->\n\n<!-- library:react optional -->\nPut the rules in a `const styles` template string above the component.\n<!-- /library -->\n\n<!-- library:vue optional -->\nPut the rules in a `<style>` block after the template.\n<!-- /library -->\n\n<!-- library:angular optional -->\nPut the rules in a template string in the decorator's `styles` array.\n<!-- /library -->\n\n<!-- library:svelte optional -->\nPut the rules in a `<style>` block after the markup.\n<!-- /library -->\n\n<!-- library:vanilla optional -->\n**`ww-ghost-disco.js`**\n<!-- /library -->\n\n<!-- library:lit optional -->\n**`ww-ghost-disco.js`**\n<!-- /library -->\n\n<!-- library:react optional -->\n**`ww-ghost-disco.jsx`**\n<!-- /library -->\n\n<!-- library:vue optional -->\n**`ww-ghost-disco.vue`**\n<!-- /library -->\n\n<!-- library:angular optional -->\n**`ww-ghost-disco.ts`**\n<!-- /library -->\n\n<!-- library:svelte optional -->\n**`ww-ghost-disco.svelte`**\n<!-- /library -->\n\n```css\n:host { display: block; }\n.stage { padding: 1rem; border: 1px solid #90a6cd; border-radius: .75rem; background: #e5edff; color: #172c5a; text-align: center; }\n.ghost { display: block; margin-block: .75rem; font-size: 2.75rem; }\n.controls { display: flex; flex-wrap: wrap; justify-content: center; gap: .5rem; }\n.controls[hidden] { display: none; }\nbutton { font: inherit; padding: .375rem .75rem; }\noutput { display: block; margin-block-start: .75rem; }\n```\n\nSave your changes. If setup used a build command, run it again. Refresh the local package. In the result, edit the invitation in **Editing** and switch to **Reading** to reveal the buttons.\n\n## Results\n\n<!-- live-example: Ghost Disco · Authoring -->\n\n<!-- live-package: 01-setup.md . -->\n\n<!-- live-file: react build.mjs ../examples/react/build.mjs -->\n\n<!-- live-file: vue build.mjs ../examples/vue/build.mjs -->\n\n<!-- live-file: angular build.mjs ../examples/angular/build.mjs -->\n\n<!-- live-file: svelte build.mjs ../examples/svelte/build.mjs -->\n\n<!-- live-file: vue compile.mjs ../examples/vue/compile.mjs -->\n\n<!-- live-source: vanilla ../examples/disco/02-disco-editing.js -->\n\n<!-- live-source: lit ../examples/disco/lit/02-disco-editing.js -->\n\n<!-- live-source: react ../examples/disco/react/02-disco-editing.jsx -->\n\n<!-- live-source: vue ../examples/disco/vue/02-disco-editing.vue -->\n\n<!-- live-source: vue-registration ../examples/disco/vue/02-disco-editing.js -->\n\n<!-- live-source: angular ../examples/disco/angular/02-disco-editing.ts -->\n\n<!-- live-source: svelte ../examples/disco/svelte/02-disco-editing.svelte -->\n\n```html\n<ww-ghost-disco>\n <p>Join the midnight dance party.</p>\n</ww-ghost-disco>\n```\n\n<!-- /live-example -->\n"},{"id":"tutorial/03-responsive","source":"# 3. Presentation\n\n[Make layouts responsive](../guides/responsiveness.md)\n\nFit the dance controls to the width of the widget so they work in a narrow column on a wide page.\n\n## Stack the buttons in a narrow widget\n\nStack the buttons in a grid to fit narrow layouts. Append these rules to the shadow styles from step 2 to make the host a size container. Keep the hidden rule for Editing mode.\n\n<!-- library:vanilla optional -->\n**`ww-ghost-disco.js`**\n<!-- /library -->\n\n<!-- library:lit optional -->\n**`ww-ghost-disco.js`**\n<!-- /library -->\n\n<!-- library:react optional -->\n**`ww-ghost-disco.jsx`**\n<!-- /library -->\n\n<!-- library:vue optional -->\n**`ww-ghost-disco.vue`**\n<!-- /library -->\n\n<!-- library:angular optional -->\n**`ww-ghost-disco.ts`**\n<!-- /library -->\n\n<!-- library:svelte optional -->\n**`ww-ghost-disco.svelte`**\n<!-- /library -->\n\n```css\n:host { container-type: inline-size; }\n.controls { display: grid; }\n.controls[hidden] { display: none; }\n```\n\n## Use a row when there is room\n\nArrange the buttons in a row once the widget is at least `24rem` wide. Add this container query after the grid rule.\n\n<!-- library:vanilla optional -->\n**`ww-ghost-disco.js`**\n<!-- /library -->\n\n<!-- library:lit optional -->\n**`ww-ghost-disco.js`**\n<!-- /library -->\n\n<!-- library:react optional -->\n**`ww-ghost-disco.jsx`**\n<!-- /library -->\n\n<!-- library:vue optional -->\n**`ww-ghost-disco.vue`**\n<!-- /library -->\n\n<!-- library:angular optional -->\n**`ww-ghost-disco.ts`**\n<!-- /library -->\n\n<!-- library:svelte optional -->\n**`ww-ghost-disco.svelte`**\n<!-- /library -->\n\n```css\n@container (min-width: 24rem) {\n .controls { display: flex; justify-content: center; }\n}\n```\n\n## Check both widths\n\nCheck that the buttons switch from a column to a row as the widget widens. Put the widget in this wrapper, switch to **Reading** in the result, and change `18rem` to `32rem` in **HTML**.\n\n**`index.html`**\n\n```html\n<div style=\"max-inline-size: 18rem\">\n <ww-ghost-disco>\n <p>Join the midnight dance party.</p>\n </ww-ghost-disco>\n</div>\n```\n\n## Results\n\n<!-- live-example: Ghost Disco · Presentation -->\n\n<!-- live-package: 01-setup.md 02-rendering.md -->\n\n<!-- live-file: react build.mjs ../examples/react/build.mjs -->\n\n<!-- live-file: vue build.mjs ../examples/vue/build.mjs -->\n\n<!-- live-file: angular build.mjs ../examples/angular/build.mjs -->\n\n<!-- live-file: svelte build.mjs ../examples/svelte/build.mjs -->\n\n<!-- live-file: vue compile.mjs ../examples/vue/compile.mjs -->\n\n<!-- live-source: vanilla ../examples/disco/04-disco-responsive.js -->\n\n<!-- live-source: lit ../examples/disco/lit/04-disco-responsive.js -->\n\n<!-- live-source: react ../examples/disco/react/04-disco-responsive.jsx -->\n\n<!-- live-source: vue ../examples/disco/vue/04-disco-responsive.vue -->\n\n<!-- live-source: vue-registration ../examples/disco/vue/04-disco-responsive.js -->\n\n<!-- live-source: angular ../examples/disco/angular/04-disco-responsive.ts -->\n\n<!-- live-source: svelte ../examples/disco/svelte/04-disco-responsive.svelte -->\n\n```html\n<div style=\"max-inline-size: 18rem\">\n <ww-ghost-disco>\n <p>Join the midnight dance party.</p>\n </ww-ghost-disco>\n</div>\n```\n\n<!-- /live-example -->\n"},{"id":"tutorial/04-state","source":"# 4. State and participation\n\n[Manage state](../guides/state.md)\n\nRemember the reader’s chosen dance move and let participants share it in a live session.\n\n## Store the selected move in a data block\n\nGive the widget a place to read and write the selected move. Replace the test markup with the version below, which starts with an empty `move` field in the JSON data block.\n\n**`index.html`**\n\n```html\n<ww-ghost-disco id=\"ghost-disco\" shared>\n <p>Join the midnight dance party.</p>\n <script type=\"application/json\" slot=\"data\">{\"move\":\"\"}</script>\n</ww-ghost-disco>\n```\n\n## Read and write the move\n\nTurn the stored move into a ghost pose and a status message. Create `disco/reading.js` and wait for the current data block before enabling the buttons. On click, read the latest snapshot and preserve the other JSON fields when you write the move.\n\n**`disco/reading.js`**\n\n```js\nexport function readDance(data, ready, error) {\n if(!ready || !data) {\n return {move: \"\", ready: false, status: error?.message || \"Waiting for dance data…\"}\n }\n\n try {\n const {move} = JSON.parse(data)\n if(move === \"float\") return {move, ready, status: \"The ghost floats!\"}\n if(move === \"twirl\") return {move, ready, status: \"The ghost twirls!\"}\n return {move: \"\", ready, status: \"Pick a dance move.\"}\n }\n catch {\n return {move: \"\", ready: false, status: \"The dance data is invalid.\"}\n }\n}\n\nexport function chooseDance(runtime, move) {\n const {editable, data, dataReady, dataError} = runtime.getSnapshot()\n if(editable || !readDance(data, dataReady, dataError).ready) return\n runtime.setData(\"application/json\", JSON.stringify({...JSON.parse(data), move}))\n}\n```\n\nMerge this import alias into the package manifest. Keep your existing widget export and editing configuration.\n\n**`package.json`**\n\n```json package\n{\n \"imports\": {\n \"#disco-reading\": \"./disco/reading.js\"\n }\n}\n```\n\n<!-- library:vanilla optional -->\nAdd the runtime dependency to the existing `dependencies` map in `package.json`.\n\n**`package.json`**\n\n```json package\n{\n \"dependencies\": {\n \"@webwriter/widget\": \"^1.0.0\"\n }\n}\n```\n<!-- /library -->\n\n<!-- library:react optional -->\nAdd the runtime dependency to the existing `dependencies` map in `package.json`.\n\n**`package.json`**\n\n```json package\n{\n \"dependencies\": {\n \"@webwriter/react\": \"^1.0.0\"\n }\n}\n```\n<!-- /library -->\n\n<!-- library:vue optional -->\nAdd the runtime dependency to the existing `dependencies` map in `package.json`.\n\n**`package.json`**\n\n```json package\n{\n \"dependencies\": {\n \"@webwriter/vue\": \"^1.0.0\"\n }\n}\n```\n<!-- /library -->\n\n<!-- library:angular optional -->\nAdd the runtime dependency to the existing `dependencies` map in `package.json`.\n\n**`package.json`**\n\n```json package\n{\n \"dependencies\": {\n \"@webwriter/angular\": \"^1.0.0\"\n }\n}\n```\n<!-- /library -->\n\n<!-- library:svelte optional -->\nAdd the runtime dependency to the existing `dependencies` map in `package.json`.\n\n**`package.json`**\n\n```json package\n{\n \"dependencies\": {\n \"@webwriter/svelte\": \"^1.0.0\"\n }\n}\n```\n<!-- /library -->\n\n## Connect the buttons to the data\n\nMake each button save its move and update the dance floor from the data block.\n\n<!-- library:vanilla optional -->\nAdd the imports and change the class base to `withWidgetRuntime(HTMLElement)`. Remove the observed-attributes list, `attributeChangedCallback()`, and `updateMode()`. Replace `connectedCallback()` and add the other methods below. Keep the registration and shadow markup.\n\n**`ww-ghost-disco.js`**\n\n```js\nimport {withWidgetRuntime} from \"@webwriter/widget\"\nimport {readDance, chooseDance} from \"#disco-reading\"\n```\n\n**`ww-ghost-disco.js`**\n\n```js\nconnectedCallback() {\n super.connectedCallback()\n this.unsubscribe = this.widgetRuntime.subscribe(() => this.update())\n this.update()\n}\n\ndisconnectedCallback() {\n this.unsubscribe?.()\n super.disconnectedCallback()\n}\n\nupdate() {\n const {editable, data, dataReady, dataError} = this.widgetRuntime.getSnapshot()\n const {move, ready, status} = readDance(data, dataReady, dataError)\n const root = this.shadowRoot\n root.querySelector(\".controls\").hidden = editable\n root.querySelector(\".author-hint\").hidden = !editable\n root.querySelector(\".ghost\").dataset.move = move\n root.querySelector(\"output\").textContent = status\n for(const button of root.querySelectorAll(\".controls button\")) {\n button.disabled = !ready\n button.setAttribute(\"aria-pressed\", String(button.value === move))\n }\n}\n```\n\nAdd the click listeners at the end of the constructor, after the shadow markup is in place.\n\n**`ww-ghost-disco.js`**\n\n```js\nfor(const button of this.shadowRoot.querySelectorAll(\".controls button\")) {\n button.addEventListener(\"click\", () => chooseDance(this.widgetRuntime, button.value))\n}\n```\n<!-- /library -->\n\n<!-- library:lit optional -->\nImport `readDance` from the helper. Add the error property and data callbacks to the class. They keep the error message current when the data scope changes. Then replace `render()`.\n\n**`ww-ghost-disco.js`**\n\n```js\nimport {readDance} from \"#disco-reading\"\n```\n\n**`ww-ghost-disco.js`**\n\n```js\nstatic properties = {error: {state: true}}\n\ndataPending() { this.error = undefined }\ndataChanged() { this.error = undefined }\ndataError(error) { this.error = error }\n\nchoose(move) {\n if(this.editable || !readDance(this.data, this.dataReady, this.error).ready) return\n this.data = JSON.stringify({...JSON.parse(this.data), move})\n}\n```\n\n**`ww-ghost-disco.js`**\n\n```js\nrender() {\n const {move, ready, status} = readDance(this.data, this.dataReady, this.error)\n return html`\n <div class=\"stage\">\n <slot><p>Join the midnight dance party.</p></slot>\n <span class=\"ghost\" aria-hidden=\"true\" data-move=\"${move}\">👻</span>\n <div class=\"controls\" ?hidden=${this.editable}>\n <button type=\"button\" value=\"float\" ?disabled=${!ready} aria-pressed=${move === \"float\"} @click=${() => this.choose(\"float\")}>Float</button>\n <button type=\"button\" value=\"twirl\" ?disabled=${!ready} aria-pressed=${move === \"twirl\"} @click=${() => this.choose(\"twirl\")}>Twirl</button>\n </div>\n <p class=\"author-hint\" ?hidden=${!this.editable}>Edit the invitation in the document.</p>\n <output aria-live=\"polite\">${status}</output>\n </div>`\n}\n```\n<!-- /library -->\n\n<!-- library:react optional -->\nReplace the `createRoot` import with the imports below. Replace the component and remove the old custom element class. The adapter mounts the component and `useWidget()` updates it when the data or editing mode changes. Keep the style string.\n\n**`ww-ghost-disco.jsx`**\n\n```jsx\nimport {createReactWidget, useWidget} from \"@webwriter/react\"\nimport {readDance, chooseDance} from \"#disco-reading\"\n\nfunction GhostDisco({host}) {\n const {editable: editing, data, dataReady, dataError} = useWidget(host)\n const {move, ready, status} = readDance(data, dataReady, dataError)\n const choose = move => chooseDance(host.widgetRuntime, move)\n return (\n <div className=\"stage\">\n <style>{styles}</style>\n <slot><p>Join the midnight dance party.</p></slot>\n <span className=\"ghost\" aria-hidden=\"true\" data-move={move}>👻</span>\n <div className=\"controls\" hidden={editing}>\n <button type=\"button\" value=\"float\" disabled={!ready} aria-pressed={move === \"float\"} onClick={() => choose(\"float\")}>Float</button>\n <button type=\"button\" value=\"twirl\" disabled={!ready} aria-pressed={move === \"twirl\"} onClick={() => choose(\"twirl\")}>Twirl</button>\n </div>\n <p className=\"author-hint\" hidden={!editing}>Edit the invitation in the document.</p>\n <output aria-live=\"polite\">{status}</output>\n </div>\n )\n}\n```\n\nRegister the class returned by the adapter.\n\n**`ww-ghost-disco.jsx`**\n\n```jsx\ncustomElements.define(\"ww-ghost-disco\", createReactWidget(GhostDisco))\n```\n<!-- /library -->\n\n<!-- library:vue optional -->\nReplace the registration module so the adapter can expose widget state. In the component, replace the script and template. Keep the styles.\n\n**`ww-ghost-disco.js`**\n\n```js\nimport {createVueWidget} from \"@webwriter/vue\"\nimport GhostDisco from \"./ww-ghost-disco.vue\"\n\ncustomElements.define(\"ww-ghost-disco\", createVueWidget(GhostDisco))\n```\n\n**`ww-ghost-disco.vue`**\n\n```vue\n<script setup>\nimport {computed, useHost} from \"vue\"\nimport {useWidget} from \"@webwriter/vue\"\nimport {readDance, chooseDance} from \"#disco-reading\"\n\nconst host = useHost()\nconst widget = useWidget(host)\nconst editing = computed(() => widget.value.editable)\nconst dance = computed(() => {\n const {data, dataReady, dataError} = widget.value\n return readDance(data, dataReady, dataError)\n})\nconst status = computed(() => dance.value.status)\nconst choose = move => chooseDance(host.widgetRuntime, move)\n</script>\n\n<template>\n <div class=\"stage\">\n <slot><p>Join the midnight dance party.</p></slot>\n <span class=\"ghost\" aria-hidden=\"true\" :data-move=\"dance.move\">👻</span>\n <div class=\"controls\" :hidden=\"editing\">\n <button type=\"button\" value=\"float\" :disabled=\"!dance.ready\" :aria-pressed=\"dance.move === 'float'\" @click=\"choose('float')\">Float</button>\n <button type=\"button\" value=\"twirl\" :disabled=\"!dance.ready\" :aria-pressed=\"dance.move === 'twirl'\" @click=\"choose('twirl')\">Twirl</button>\n </div>\n <p class=\"author-hint\" :hidden=\"!editing\">Edit the invitation in the document.</p>\n <output aria-live=\"polite\">{{ status }}</output>\n </div>\n</template>\n```\n<!-- /library -->\n\n<!-- library:angular optional -->\nUse the Angular widget adapter to mount the component and inject its host and state. Replace the imports, component, and registration as shown. Move the existing CSS from the decorator to a `const styles` template string before the registration line.\n\n**`ww-ghost-disco.ts`**\n\n```ts\nimport \"@angular/compiler\"\nimport {Component, computed} from \"@angular/core\"\nimport {createAngularWidget, injectWidget, injectWidgetHost} from \"@webwriter/angular\"\nimport {readDance, chooseDance} from \"#disco-reading\"\n\n@Component({\n standalone: true,\n template: `\n <div class=\"stage\">\n <slot><p>Join the midnight dance party.</p></slot>\n <span class=\"ghost\" aria-hidden=\"true\" [attr.data-move]=\"dance().move\">👻</span>\n <div class=\"controls\" [hidden]=\"widget().editable\">\n <button type=\"button\" value=\"float\" [disabled]=\"!dance().ready\" [attr.aria-pressed]=\"dance().move === 'float'\" (click)=\"choose('float')\">Float</button>\n <button type=\"button\" value=\"twirl\" [disabled]=\"!dance().ready\" [attr.aria-pressed]=\"dance().move === 'twirl'\" (click)=\"choose('twirl')\">Twirl</button>\n </div>\n <p class=\"author-hint\" [hidden]=\"!widget().editable\">Edit the invitation in the document.</p>\n <output aria-live=\"polite\">{{ status() }}</output>\n </div>\n `,\n})\nclass GhostDisco {\n private readonly host = injectWidgetHost()\n readonly widget = injectWidget()\n readonly dance = computed(() => {\n const {data, dataReady, dataError} = this.widget()\n return readDance(data, dataReady, dataError)\n })\n readonly status = computed(() => this.dance().status)\n\n choose(move: \"float\" | \"twirl\") { chooseDance(this.host.widgetRuntime, move) }\n}\n```\n\n**`ww-ghost-disco.ts`**\n\n```ts\ncustomElements.define(\"ww-ghost-disco\", await createAngularWidget(GhostDisco, {styles}))\n```\n<!-- /library -->\n\n<!-- library:svelte optional -->\nReplace the custom element options and script blocks with these. The store updates the component when the data or editing mode changes. Replace the stage markup and keep the styles.\n\n**`ww-ghost-disco.svelte`**\n\n```svelte\n<script module>\nimport {widgetStore, withWidgetRuntime} from \"@webwriter/svelte\"\nimport {readDance, chooseDance} from \"#disco-reading\"\n</script>\n\n<svelte:options customElement={{ tag: \"ww-ghost-disco\", extend: withWidgetRuntime }} />\n\n<script>\n const host = $host()\n const widget = widgetStore(host)\n const editing = $derived($widget.editable)\n const {move, ready, status} = $derived(readDance($widget.data, $widget.dataReady, $widget.dataError))\n const choose = move => chooseDance(host.widgetRuntime, move)\n</script>\n```\n\n**`ww-ghost-disco.svelte`**\n\n```svelte\n<div class=\"stage\">\n <slot><p>Join the midnight dance party.</p></slot>\n <span class=\"ghost\" aria-hidden=\"true\" data-move={move}>👻</span>\n <div class=\"controls\" hidden={editing}>\n <button type=\"button\" value=\"float\" disabled={!ready} aria-pressed={move === \"float\"} onclick={() => choose(\"float\")}>Float</button>\n <button type=\"button\" value=\"twirl\" disabled={!ready} aria-pressed={move === \"twirl\"} onclick={() => choose(\"twirl\")}>Twirl</button>\n </div>\n <p class=\"author-hint\" hidden={!editing}>Edit the invitation in the document.</p>\n <output aria-live=\"polite\">{status}</output>\n</div>\n```\n<!-- /library -->\n\nAppend these rules to the shadow styles. The selected move changes the ghost's pose and marks its button as pressed.\n\n<!-- library:vanilla optional -->\n**`ww-ghost-disco.js`**\n<!-- /library -->\n\n<!-- library:lit optional -->\n**`ww-ghost-disco.js`**\n<!-- /library -->\n\n<!-- library:react optional -->\n**`ww-ghost-disco.jsx`**\n<!-- /library -->\n\n<!-- library:vue optional -->\n**`ww-ghost-disco.vue`**\n<!-- /library -->\n\n<!-- library:angular optional -->\n**`ww-ghost-disco.ts`**\n<!-- /library -->\n\n<!-- library:svelte optional -->\n**`ww-ghost-disco.svelte`**\n<!-- /library -->\n\n```css\n.ghost[data-move=\"float\"] { transform: translateY(-.3rem); }\n.ghost[data-move=\"twirl\"] { transform: rotate(24deg); }\nbutton[aria-pressed=\"true\"] { outline: 2px solid currentColor; outline-offset: 2px; }\n```\n\n## Share participant data\n\n[Share participant data](../guides/sharing.md)\n\nGive readers the same move through the `shared` attribute in the markup above. Set `sharedData` in the existing widget entry to enable participation. Remove `shared` for separate participant data.\n\n**`package.json`**\n\n```json package\n{\n \"editingConfig\": {\n \"./widgets/ww-ghost-disco\": {\n \"sharedData\": true\n }\n }\n}\n```\n\n## Rebuild and try sharing\n\nCheck that a move chosen in one preview appears in the other. The widget now imports a helper module, so build the package before refreshing it in WebWriter.\n\n<!-- library:vanilla optional -->\nSave the [build script](../examples/disco/build.mjs) as `build.mjs` beside the manifest. Update the widget export to point to the build output. Keep your existing dependencies and settings.\n\n**`package.json`**\n\n```json package\n{\n \"exports\": {\n \"./widgets/ww-ghost-disco.js\": \"./dist/ww-ghost-disco.js\"\n },\n \"scripts\": {\n \"build\": \"node build.mjs\"\n },\n \"devDependencies\": {\n \"esbuild-wasm\": \"^0.25.12\"\n }\n}\n```\n<!-- /library -->\n\n<!-- library:lit optional -->\nSave the [build script](../examples/disco/build.mjs) as `build.mjs` beside the manifest. Update the widget export to point to the build output. Keep your existing dependencies and settings.\n\n**`package.json`**\n\n```json package\n{\n \"exports\": {\n \"./widgets/ww-ghost-disco.js\": \"./dist/ww-ghost-disco.js\"\n },\n \"scripts\": {\n \"build\": \"node build.mjs\"\n },\n \"devDependencies\": {\n \"esbuild-wasm\": \"^0.25.12\"\n }\n}\n```\n<!-- /library -->\n\n**Terminal**\n\n```sh prompt\nnpm install\nnpm run build\n```\n\nIn the result, choose **Shared** and **Reading**, then select **Float** or **Twirl**. Remove `shared` in **HTML** and check that each participant can now choose separately.\n\n## Results\n\n<!-- live-example: Ghost Disco · Shared dance moves -->\n\n<!-- live-package: 01-setup.md 02-rendering.md . -->\n\n<!-- live-file: react build.mjs ../examples/react/build.mjs -->\n\n<!-- live-file: vue build.mjs ../examples/vue/build.mjs -->\n\n<!-- live-file: angular build.mjs ../examples/angular/build.mjs -->\n\n<!-- live-file: svelte build.mjs ../examples/svelte/build.mjs -->\n\n<!-- live-file: vanilla build.mjs ../examples/disco/build.mjs -->\n\n<!-- live-file: lit build.mjs ../examples/disco/build.mjs -->\n\n<!-- live-file: vue compile.mjs ../examples/vue/compile.mjs -->\n\n<!-- live-source: vanilla ../examples/disco/04-disco-sharing.js -->\n\n<!-- live-source: lit ../examples/disco/lit/04-disco-sharing.js -->\n\n<!-- live-source: react ../examples/disco/react/04-disco-sharing.jsx -->\n\n<!-- live-source: vue ../examples/disco/vue/04-disco-sharing.vue -->\n\n<!-- live-source: vue-registration ../examples/disco/vue/04-disco-sharing.js -->\n\n<!-- live-source: angular ../examples/disco/angular/04-disco-sharing.ts -->\n\n<!-- live-source: svelte ../examples/disco/svelte/04-disco-sharing.svelte -->\n\n<!-- live-source: shared-disco-reading ../examples/disco/reading.js -->\n\n```html shared\n<ww-ghost-disco id=\"ghost-disco\" shared>\n <p>Join the midnight dance party.</p>\n <script type=\"application/json\" slot=\"data\">{\"move\":\"\"}</script>\n</ww-ghost-disco>\n```\n\n<!-- /live-example -->\n"},{"id":"tutorial/05-build","source":"# 5. Build and distribution\n\n[Manage dependencies](../guides/libraries.md)\n\nPackage Ghost Disco so it can load in WebWriter and on a standalone page.\n\n## Build the browser module\n\nBundle the widget and its dependencies into `dist/ww-ghost-disco.js`. Run the build script from the previous step after any change to the widget.\n\nCheck that `dist/ww-ghost-disco.js` exists, then refresh the local package in WebWriter and try the invitation and dance controls.\n\n**Terminal**\n\n```sh prompt\nnpm run build\n```\n\n## Check the package contents\n\nMake sure the package archive includes the widget module. Add `dist/` to the manifest’s `files` list and check the result with `npm pack --dry-run`.\n\n**`package.json`**\n\n```json package\n{\n \"files\": [\n \"dist/\"\n ]\n}\n```\n\n**Terminal**\n\n```sh prompt\nnpm pack --dry-run\n```\n\n## Load the build on a page\n\nCheck that readers can choose a move on a standalone page. Use the invitation and data block from step 4. Serve `index.html` beside the `dist` directory, open it through your local HTTP server, and try **Float** or **Twirl**.\n\n**`index.html`**\n\n```html\n<script type=\"module\" src=\"./dist/ww-ghost-disco.js\"></script>\n<ww-ghost-disco id=\"ghost-disco\" shared>\n <p>Join the midnight dance party.</p>\n <script type=\"application/json\" slot=\"data\">{\"move\":\"\"}</script>\n</ww-ghost-disco>\n```\n\n## Results\n\n<!-- live-example: Ghost Disco -->\n\n<!-- live-package: 01-setup.md 02-rendering.md 04-state.md . -->\n\n<!-- live-file: react build.mjs ../examples/react/build.mjs -->\n\n<!-- live-file: vue build.mjs ../examples/vue/build.mjs -->\n\n<!-- live-file: angular build.mjs ../examples/angular/build.mjs -->\n\n<!-- live-file: svelte build.mjs ../examples/svelte/build.mjs -->\n\n<!-- live-file: vanilla build.mjs ../examples/disco/build.mjs -->\n\n<!-- live-file: lit build.mjs ../examples/disco/build.mjs -->\n\n<!-- live-file: vue compile.mjs ../examples/vue/compile.mjs -->\n\n<!-- live-source: vanilla ../examples/disco/04-disco-sharing.js -->\n\n<!-- live-source: lit ../examples/disco/lit/04-disco-sharing.js -->\n\n<!-- live-source: react ../examples/disco/react/04-disco-sharing.jsx -->\n\n<!-- live-source: vue ../examples/disco/vue/04-disco-sharing.vue -->\n\n<!-- live-source: vue-registration ../examples/disco/vue/04-disco-sharing.js -->\n\n<!-- live-source: angular ../examples/disco/angular/04-disco-sharing.ts -->\n\n<!-- live-source: svelte ../examples/disco/svelte/04-disco-sharing.svelte -->\n\n<!-- live-source: shared-disco-reading ../examples/disco/reading.js -->\n\n```html shared\n<ww-ghost-disco id=\"ghost-disco\" shared>\n <p>Join the midnight dance party.</p>\n <script type=\"application/json\" slot=\"data\">{\"move\":\"\"}</script>\n</ww-ghost-disco>\n```\n\n<!-- /live-example -->\n"},{"id":"tutorial/index","source":"# Tutorial: Ghost Disco\n\nBuild a small dance floor for a ghost. Authors can edit the invitation. Readers can make the ghost float or twirl, and two readers can share the same move.\n\nChoose **Shared** and **Reading**, then select a move to see it on both dance floors. In **Editing**, change the invitation text.\n\n<!-- live-example-link: 05-build.md -->\n\nChoose a library in the sidebar and work through the steps in order. Add the static code samples to the files named on their labels. Use `index.html` for your test document, or try its markup in the live example's **HTML** pane. Each step ends with a result you can try.\n\n- [1. Set up the widget](01-setup.md)\n- [2. Authoring](02-rendering.md)\n- [3. Presentation](03-responsive.md)\n- [4. State and participation](04-state.md)\n- [5. Build and distribution](05-build.md)\n"}],"widgetKeywords":{"badges":{"widget-editable":{"label":"Editable","kind":"editing","icon":"Pencil","file":"pencil"},"widget-allows-pointer-input":{"label":"Pointer input","kind":"input","icon":"KeywordPointer","file":"mouse"},"widget-allows-keyboard-input":{"label":"Keyboard input","kind":"input","icon":"KeywordKeyboard","file":"keyboard"},"widget-allows-touch-input":{"label":"Touch input","kind":"input","icon":"Image hotspot","file":"hand-click"},"widget-has-content":{"label":"Has content","kind":"content","icon":"Order","file":"stack"},"widget-convertible-to-":{"label":"Converts to","kind":"conversion","icon":"KeywordConvertTo","file":"file-export","pattern":"widget-convertible-to-[media/type | <element-name>]"},"widget-convertible-from-":{"label":"Converts from","kind":"conversion","icon":"KeywordConvertFrom","file":"file-import","pattern":"widget-convertible-from-[media/type | <element-name>]"},"widget-responsive":{"label":"Responsive","kind":"display","icon":"KeywordResponsive","file":"devices"},"widget-themeable":{"label":"Themeable","kind":"display","icon":"Theme","file":"palette"},"widget-lang-":{"label":"Content languages","kind":"language","icon":"KeywordLanguage","file":"language","pattern":"widget-lang-[bcp47]"},"widget-printable":{"label":"Printable","kind":"display","icon":"Print","file":"printer"},"widget-stateful":{"label":"Stateful","kind":"state","icon":"MarkData","file":"database"},"widget-configurable":{"label":"Configurable","kind":"state","icon":"Preferences","file":"adjustments"},"widget-reactive":{"label":"Reactive","kind":"state","icon":"KeywordReactive","file":"activity"},"widget-shareable":{"label":"Shareable","kind":"participation","icon":"Share","file":"share"},"widget-distributable":{"label":"Distributable","kind":"participation","icon":"KeywordDistributable","file":"users"},"widget-online":{"label":"Online only","kind":"online","icon":"KeywordOnline","file":"wifi"},"widget-online-edit":{"label":"Online when editing","kind":"online","icon":"KeywordOnline","file":"wifi"},"widget-online-use":{"label":"Online when using","kind":"online","icon":"KeywordOnline","file":"wifi"},"widget-supports-dynamic-loading":{"label":"Dynamic loading","kind":"dependency","icon":"KeywordDynamicLoading","file":"bolt"},"widget-supports-shared-dependencies":{"label":"Shared dependencies","kind":"dependency","icon":"KeywordSharedDependencies","file":"packages"},"widget-testable":{"label":"Testable","kind":"development","icon":"KeywordTestable","file":"flask"},"widget-migrates-from-":{"label":"Migrates from","kind":"development","icon":"KeywordMigration","file":"history","pattern":"widget-migrates-from-[semver]"},"webwriter-widget":{"label":"Widget","kind":"widget","icon":"Packages","file":"package"},"widget-standalone":{"label":"Standalone","kind":"discovery","icon":"KeywordStandalone","file":"external-link"},"widget-presentational":{"label":"Presentation","kind":"type","icon":"KeywordPresentation","file":"alert-square"},"widget-practical":{"label":"Practice","kind":"type","icon":"KeywordPractice","file":"help-square"},"widget-simulational":{"label":"Simulation","kind":"type","icon":"KeywordSimulation","file":"square-chevron-right"},"widget-conceptual":{"label":"Concept","kind":"type","icon":"KeywordConcept","file":"square-dot"},"widget-informational":{"label":"Information","kind":"type","icon":"KeywordInformation","file":"info-square"},"widget-contextual":{"label":"Context","kind":"type","icon":"KeywordContext","file":"square-asterisk"}},"guides":{"editing":["widget-editable"],"focus":["widget-allows-pointer-input","widget-allows-keyboard-input","widget-allows-touch-input"],"content":["widget-has-content"],"conversions":["widget-convertible-to-","widget-convertible-from-"],"responsiveness":["widget-responsive"],"theming":["widget-themeable"],"localization":["widget-lang-"],"printing":["widget-printable"],"state":["widget-stateful","widget-configurable"],"sharing":["widget-shareable"],"live-sessions":["widget-distributable"],"reactivity":["widget-reactive"],"libraries":["widget-online","widget-online-edit","widget-online-use","widget-supports-dynamic-loading","widget-supports-shared-dependencies"],"testing":["widget-testable"],"migrations":["widget-migrates-from-"],"publishing":["webwriter-widget","widget-standalone","widget-presentational","widget-practical","widget-simulational","widget-conceptual","widget-informational","widget-contextual"]}}}
|
package/docs.mjs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync} from "node:fs"
|
|
2
|
+
import {join, relative, sep} from "node:path"
|
|
3
|
+
import {fileURLToPath, pathToFileURL} from "node:url"
|
|
4
|
+
|
|
5
|
+
const checkoutDocs = fileURLToPath(new URL("../domeditor/docs/widgets/", import.meta.url))
|
|
6
|
+
const checkoutKeywords = new URL("../domeditor/src/widget-keywords.json", import.meta.url)
|
|
7
|
+
const snapshot = new URL("./dist/widget-docs.json", import.meta.url)
|
|
8
|
+
const readJSON = file => JSON.parse(readFileSync(file, "utf8"))
|
|
9
|
+
|
|
10
|
+
function readWidgetDocs(directory = checkoutDocs) {
|
|
11
|
+
return readdirSync(directory, {withFileTypes: true}).flatMap(entry => {
|
|
12
|
+
const path = join(directory, entry.name)
|
|
13
|
+
if(entry.isDirectory() && entry.name !== "examples") return readWidgetDocs(path)
|
|
14
|
+
if(!entry.isFile() || !entry.name.endsWith(".md")) return []
|
|
15
|
+
const id = relative(checkoutDocs, path).split(sep).join("/").slice(0, -3)
|
|
16
|
+
return [{id, source: readFileSync(path, "utf8")}]
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function loadWidgetDocs() {
|
|
21
|
+
return existsSync(checkoutDocs) ? readWidgetDocs() : readJSON(snapshot).docs
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function loadWidgetKeywords() {
|
|
25
|
+
return existsSync(checkoutKeywords) ? readJSON(checkoutKeywords) : readJSON(snapshot).widgetKeywords
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if(process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
|
|
29
|
+
const docs = readWidgetDocs().sort((a, b) => a.id.localeCompare(b.id))
|
|
30
|
+
mkdirSync(new URL(".", snapshot), {recursive: true})
|
|
31
|
+
writeFileSync(snapshot, JSON.stringify({docs, widgetKeywords: readJSON(checkoutKeywords)}) + "\n")
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webwriter/guidance",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "WebWriter widget documentation search and coding agent skill",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/webwriter-app/webwriter.git",
|
|
10
|
+
"directory": "@webwriter/guidance"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22.20.0"
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"webwriter-guidance": "cli.mjs"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"cli.mjs",
|
|
23
|
+
"docs.mjs",
|
|
24
|
+
"dist/widget-docs.json",
|
|
25
|
+
"skills"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"prepack": "node docs.mjs"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webwriter-guidance
|
|
3
|
+
description: Search and retrieve WebWriter's widget documentation and audit or rework existing widgets using their capability badges. Use for WebWriter widget questions and changes involving documented widget interfaces, including package exports, framework adapters, editing controls, state, styling, and live sessions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# WebWriter Guidance
|
|
7
|
+
|
|
8
|
+
Use the WebWriter widget guides, tutorials, and interface reference through the `@webwriter/guidance` CLI. Run it with Node.js 22.20 or newer.
|
|
9
|
+
|
|
10
|
+
## Rework an existing widget
|
|
11
|
+
|
|
12
|
+
For a widget audit, modernization, or rework, use the [widget rework checklist](references/widget-rework.md). Resolve the manifest's badges to their guides, verify the claims in scope, and ask the user to decide open design questions while continuing independent fixes.
|
|
13
|
+
|
|
14
|
+
## Search
|
|
15
|
+
|
|
16
|
+
Search for one capability or API at a time. Use WebWriter terminology and exact identifiers when known.
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npx -y @webwriter/guidance search "widget package exports"
|
|
20
|
+
npx -y @webwriter/guidance search "optionschange"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
In a WebWriter checkout, run `node @webwriter/guidance/cli.mjs` from the repository root in place of `npx -y @webwriter/guidance` to consult the docs on the current branch.
|
|
24
|
+
|
|
25
|
+
Search and list return JSON arrays of `{id, description}` entries. If the descriptions are weak, try a more specific term or alternate wording, such as `"participant data scopes"` for shared widget state. Use `list` to browse the catalog when search terms are unclear.
|
|
26
|
+
|
|
27
|
+
## Retrieve
|
|
28
|
+
|
|
29
|
+
Retrieve the full Markdown for the matching IDs. Pair a task guide with an interface reference when you need exact fields or event behavior.
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npx -y @webwriter/guidance retrieve "guides/live-sessions,reference/options-actions"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
If the CLI saves long output to a temporary file, read that file in full. Retrieved pages are source Markdown. Select the example for the relevant framework and follow linked docs and example files as needed.
|
|
36
|
+
|
|
37
|
+
## Apply
|
|
38
|
+
|
|
39
|
+
Check the answer or implementation against the retrieved pages and cite the relevant documents. When the docs leave behavior unclear, inspect the matching code and tests if the source is available and identify any conclusion based on those sources.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Widget rework
|
|
2
|
+
|
|
3
|
+
## Establish the current behavior
|
|
4
|
+
|
|
5
|
+
Identify the target widget exports, their package manifest, existing tests and fixtures, and the author and reader tasks affected by the request. Start with the declared badges and add checks for capabilities the request affects. Use the whole catalog for a broad review and mark unrelated capabilities out of scope.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx -y @webwriter/guidance badges ./package.json
|
|
9
|
+
npx -y @webwriter/guidance badges
|
|
10
|
+
npx -y @webwriter/guidance retrieve "reference/keywords,guides/focus,guides/sharing"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`badges` uses the documentation's badge-to-guide mapping and reports guide IDs for `retrieve`. With a manifest path it resolves declared keywords, retaining specific language tags, formats, and migration versions. Check the keyword reference for valid values. Retrieve the guides for the checks in scope. For a package with several widget exports, verify which exports support each relevant package-level claim.
|
|
14
|
+
|
|
15
|
+
Keep an audit row for each relevant badge and each declared value. Record the affected export, expected behavior, observed behavior and evidence, and status. Use `Verified`, `Gap`, `Needs decision`, `Out of scope`, or `Unverified` when evidence is unavailable. A manifest entry alone is a claim. Verify it against behavior.
|
|
16
|
+
|
|
17
|
+
## Work through the capabilities
|
|
18
|
+
|
|
19
|
+
| Badges | Evidence to collect | Decisions for the human when open |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| Editable, Configurable, Has content | Exercise author controls in edit mode and reader controls in read mode. Save and reopen authored text and nested widgets. Check option defaults, actions, and undo. | What authors can change, which controls to expose, and accepted content. |
|
|
22
|
+
| Pointer, Keyboard, Touch input | Complete the primary task with each claimed input method. Check activation, focus order, visible focus, and interaction with editor selection. | Interaction changes such as new controls, gestures, or a different task flow. |
|
|
23
|
+
| Responsive, Themeable, Printable, Content languages | Check narrow and wide document widths, inherited theme or documented styling hooks, print preview, and each claimed language. | What gets priority in a narrow layout, what appears in print, public styling hooks, and supported languages. |
|
|
24
|
+
| Stateful, Reactive | Check saved state after reopening and undo, temporary state during interaction, and updates between connected widgets. | What persists or resets and which signals form a public interface. |
|
|
25
|
+
| Shareable, Distributable | Exercise the claimed scopes with participants in distinct roles or groups. Check permissions, submission, and what the host can see. | Default sharing scope, participation permissions, and what participants and hosts can see. |
|
|
26
|
+
| Converts from, Converts to, Migrates from | Use a fixture per claimed format or version. Inspect what survives conversion or migration and how invalid imports are handled. | Supported formats and versions and acceptable data loss. |
|
|
27
|
+
| Online variants, Dynamic loading, Shared dependencies, Standalone | Exercise the claimed modes online and offline. Check optional imports, dependency sharing, and standalone use on a plain page as applicable. | Network requirements, offline behavior, and where standalone results are stored. |
|
|
28
|
+
| Testable, Widget, purpose badges | Check manifest exports and discovery, run exported tests in the Developer console when claimed, and compare the purpose badge with the reader's primary task. | Intended audience and purpose, and whether to publish. |
|
|
29
|
+
|
|
30
|
+
## Ask and keep working
|
|
31
|
+
|
|
32
|
+
Use the request, accepted decisions, documented behavior, and existing tests or saved content to establish intent. On a rework, fix gaps with a clear expected outcome within scope, including compatibility and accessibility regressions. On an audit, report the findings. Preserve existing behavior and authored data when the request leaves them open.
|
|
33
|
+
|
|
34
|
+
When fixing a gap would choose a user-visible design, change a documented default, or add or drop a supported capability, ask before making that change unless the user already decided it. Group related questions. State the observed behavior, give two or three concrete options with tradeoffs when available, and recommend one. Use the host's question mechanism if available. While the answer is pending, continue independent fixes and verification. Keep dependent changes pending until the user answers. If the host requires a reply before work can resume, complete the independent work first and ask with a summary of what remains.
|
|
35
|
+
|
|
36
|
+
For example, if a poll's default sharing scope is unclear, ask who should see votes by default and explain the individual, group, and everyone options. While waiting, verify keyboard submission and that existing saved polls still open, provided those checks do not depend on the choice.
|
|
37
|
+
|
|
38
|
+
## Verify and report
|
|
39
|
+
|
|
40
|
+
Test the affected flows with a fresh widget and representative saved content. Recheck the declared badge claims against the result. Update manifest keywords to reflect verified capabilities within the agreed scope. If a badge is unsupported and retaining or dropping the capability is a design decision, record the gap and ask. Report completed fixes and evidence alongside any pending decisions or unverified claims.
|
package/README.md
DELETED