@webwriter/guidance 0.0.0-stage → 1.0.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 ADDED
@@ -0,0 +1,179 @@
1
+ GNU AFFERO GENERAL PUBLIC LICENSE
2
+ Version 3, 19 November 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
5
+
6
+ Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
7
+
8
+ Preamble
9
+
10
+ The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software.
11
+ The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users.
12
+ When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things.
13
+ Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software.
14
+ A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about.
15
+ The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public.
16
+ The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version.
17
+ An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license.
18
+
19
+ The precise terms and conditions for copying, distribution and modification follow.
20
+
21
+ TERMS AND CONDITIONS
22
+
23
+ 0. Definitions.
24
+ "This License" refers to version 3 of the GNU Affero General Public License.
25
+
26
+ "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks.
27
+
28
+ "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations.
29
+ To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work.
30
+
31
+ A "covered work" means either the unmodified Program or a work based on the Program.
32
+ To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well.
33
+ To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying.
34
+ An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License.
35
+ If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion.
36
+ 1. Source Code.
37
+ The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work.
38
+
39
+ A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language.
40
+ The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form.
41
+ A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it.
42
+ The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work.
43
+ For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those
44
+ subprograms and other parts of the work.
45
+ The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source.
46
+
47
+ The Corresponding Source for a work in source code form is that same work.
48
+ 2. Basic Permissions.
49
+ All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law.
50
+ You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright.
51
+ Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you.
52
+ Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
53
+
54
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
55
+ No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures.
56
+ When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures.
57
+ 4. Conveying Verbatim Copies.
58
+ You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program.
59
+ You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee.
60
+
61
+ 5. Conveying Modified Source Versions.
62
+ You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions:
63
+
64
+ a) The work must carry prominent notices stating that you modified it, and giving a relevant date.
65
+ b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices".
66
+ c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it.
67
+ d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so.
68
+ A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit.
69
+ Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate.
70
+ 6. Conveying Non-Source Forms.
71
+ You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways:
72
+
73
+ a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange.
74
+ b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge.
75
+ c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b.
76
+ d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code.
77
+ If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements.
78
+ e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d.
79
+
80
+ A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work.
81
+ A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage.
82
+ For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product.
83
+ "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made.
84
+ If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information.
85
+ But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM).
86
+ The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network.
87
+ Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying.
88
+ 7. Additional Terms.
89
+ "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law.
90
+ If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions.
91
+ When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission.
92
+ Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms:
93
+
94
+ a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or
95
+
96
+ b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or
97
+ c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or
98
+
99
+ d) Limiting the use for publicity purposes of names of licensors or authors of the material; or
100
+
101
+ e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or
102
+ f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors.
103
+ All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term.
104
+ If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying.
105
+ If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms.
106
+
107
+ Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way.
108
+
109
+ 8. Termination.
110
+ You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11).
111
+ However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
112
+ Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
113
+ Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10.
114
+
115
+ 9. Acceptance Not Required for Having Copies.
116
+ You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License.
117
+ Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so.
118
+ 10. Automatic Licensing of Downstream Recipients.
119
+
120
+ Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License.
121
+ An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations.
122
+ If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts.
123
+ You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it.
124
+
125
+ 11. Patents.
126
+ A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version".
127
+ A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version.
128
+ For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License.
129
+ Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version.
130
+ In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party.
131
+ If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent
132
+ license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid.
133
+ If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it.
134
+ A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License.
135
+ You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007.
136
+ Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law.
137
+
138
+ 12. No Surrender of Others' Freedom.
139
+ If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may
140
+ not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program.
141
+ 13. Remote Network Interaction; Use with the GNU General Public License.
142
+ Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software.
143
+ This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph.
144
+ Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License.
145
+
146
+ 14. Revised Versions of this License.
147
+ The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
148
+ Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation.
149
+ If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation.
150
+ If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program.
151
+
152
+ Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version.
153
+
154
+ 15. Disclaimer of Warranty.
155
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.
156
+ SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
157
+ 16. Limitation of Liability.
158
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
159
+ 17. Interpretation of Sections 15 and 16.
160
+
161
+ If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee.
162
+
163
+ END OF TERMS AND CONDITIONS
164
+
165
+ How to Apply These Terms to Your New Programs
166
+ If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
167
+
168
+ To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
169
+ <one line to give the program's name and a brief idea of what it does.>
170
+ Copyright (C) <year> <name of author>
171
+
172
+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
173
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
174
+
175
+ You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
176
+
177
+ Also add information on how to contact you by electronic and paper mail.
178
+ If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements.
179
+ You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see <http://www.gnu.org/licenses/>.
package/cli.mjs ADDED
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ import {spawnSync} from "node:child_process"
3
+ import {existsSync, fstatSync, mkdtempSync, 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} 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
+ install [options] Install the skill using the Vercel Skills CLI
17
+ uninstall [options] Remove the skill using the Vercel Skills CLI`
18
+ 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(" "))
19
+
20
+ function terms(text) {
21
+ const words = text
22
+ .replace(/[a-z][a-z\d]*(?:[A-Z][a-z\d]*)+/g, word => `${word} ${word.replace(/([a-z\d])([A-Z])/g, "$1 $2")}`)
23
+ .toLowerCase()
24
+ .match(/[a-z0-9]+/g) ?? []
25
+ return new Set(words.filter(word => !stopWords.has(word)).map(word => word.replace(/([a-z]{2,}[^s])s$/, "$1")))
26
+ }
27
+
28
+ function describe({id, source}) {
29
+ const body = source.replace(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/, "").replace(/<!--[\s\S]*?-->/g, "")
30
+ const prose = body.replace(/^ {0,3}(`{3,}|~{3,})[^\n]*\r?\n[\s\S]*?^ {0,3}\1[ \t]*\r?$/gm, "")
31
+ const titles = [...prose.matchAll(/^# (.+)$/gm)].map(([, title]) => title.trim().replace(/^\w+\. /, ""))
32
+ const title = titles.join(", ") || id
33
+ const paragraphs = prose.trim().split(/\r?\n\s*\r?\n/)
34
+ .filter(paragraph => /^(?:\*\*)?[A-Z`]/.test(paragraph))
35
+ .slice(0, 3)
36
+ .map(paragraph => paragraph.replace(/!?\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\s+/g, " "))
37
+ const description = paragraphs.length ? `${title} — ${paragraphs[0]}` : title
38
+ const headings = [...prose.matchAll(/^#{2,6} (.+)$/gm)].map(([, heading]) => heading).join(" ")
39
+ return {
40
+ id, description, source,
41
+ titleTerms: terms(`${id} ${title}`),
42
+ headingTerms: terms(headings),
43
+ summaryTerms: terms(paragraphs.join(" ")),
44
+ bodyTerms: terms(body),
45
+ }
46
+ }
47
+
48
+ function search(catalog, query) {
49
+ const queryTerms = [...terms(query)]
50
+ const weights = queryTerms.map(term => {
51
+ const matches = catalog.filter(guide => guide.bodyTerms.has(term) || guide.titleTerms.has(term)).length
52
+ return Math.log(1 + catalog.length / (1 + matches))
53
+ })
54
+ return catalog.map(guide => ({
55
+ guide,
56
+ score: queryTerms.reduce((score, term, index) => {
57
+ const weight = guide.titleTerms.has(term) ? 6
58
+ : guide.headingTerms.has(term) ? 3
59
+ : guide.summaryTerms.has(term) ? 2
60
+ : guide.bodyTerms.has(term) ? 1 : 0
61
+ return score + weight * weights[index]
62
+ }, 0),
63
+ })).filter(({score}) => score > 0)
64
+ .sort((a, b) => b.score - a.score)
65
+ .slice(0, 5)
66
+ .map(({guide}) => guide)
67
+ }
68
+
69
+ function printEntries(guides) {
70
+ console.log(JSON.stringify(guides.map(({id, description}) => ({id, description})), null, 2))
71
+ }
72
+
73
+ function retrieve(catalog, ids) {
74
+ const guides = new Map(catalog.map(guide => [guide.id, guide]))
75
+ const pages = [...new Set(ids)].flatMap(id => {
76
+ const guide = guides.get(id)
77
+ if(guide) return [`--- docs/widgets/${id}.md ---\n\n${guide.source.trimEnd()}\n`]
78
+ console.error(`Unknown documentation ID: ${id}`)
79
+ process.exitCode = 1
80
+ return []
81
+ })
82
+ const output = pages.join("\n")
83
+ if(Buffer.byteLength(output) > 9500 && !fstatSync(1).isFile()) {
84
+ const path = join(mkdtempSync(join(tmpdir(), "webwriter-guidance-")), "guides.md")
85
+ writeFileSync(path, output)
86
+ console.log(`Output saved to ${path}. Read the file in full.`)
87
+ }
88
+ else process.stdout.write(output)
89
+ }
90
+
91
+ function runSkills(args) {
92
+ let runner = "npx"
93
+ let runnerArgs = args
94
+ if(process.platform === "win32") {
95
+ // Invoke npx's JS entry point directly so paths and options keep their argument boundaries.
96
+ const paths = [dirname(process.execPath), ...(process.env.PATH ?? "").split(delimiter)]
97
+ const entry = paths.map(path => join(path, "node_modules/npm/bin/npx-cli.js")).find(existsSync)
98
+ if(!entry) throw new Error("Could not locate npx. Install npm with Node.js and make it available on PATH.")
99
+ runner = process.execPath
100
+ runnerArgs = [entry, ...args]
101
+ }
102
+ const result = spawnSync(runner, runnerArgs, {stdio: "inherit"})
103
+ if(result.error) throw result.error
104
+ process.exitCode = result.status ?? 1
105
+ }
106
+
107
+ function main([command, ...args]) {
108
+ const argument = args.join(" ").trim()
109
+ if(command === "--help" || command === "-h") {
110
+ console.log(usage)
111
+ return
112
+ }
113
+ if(command === "install" || command === "uninstall") {
114
+ const source = fileURLToPath(new URL("./skills/webwriter-guidance", import.meta.url))
115
+ const action = command === "install" ? ["add", source, "--skill", skill] : ["remove", skill]
116
+ runSkills(["-y", "skills", ...action, ...args])
117
+ return
118
+ }
119
+ const ids = argument.split(/[,\s]+/).filter(Boolean)
120
+ if(!(command === "list" && !args.length || command === "search" && argument || command === "retrieve" && ids.length)) {
121
+ throw new Error(usage)
122
+ }
123
+ const catalog = loadWidgetDocs().map(describe).sort((a, b) => a.id.localeCompare(b.id))
124
+ if(command === "search") printEntries(search(catalog, argument))
125
+ else if(command === "retrieve") retrieve(catalog, ids)
126
+ else printEntries(catalog)
127
+ }
128
+
129
+ try {
130
+ main(process.argv.slice(2))
131
+ }
132
+ catch(error) {
133
+ console.error(error.message)
134
+ process.exitCode = 1
135
+ }
@@ -0,0 +1 @@
1
+ [{"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![A nested widget and editable text inside an outer widget](../../images/widgets/content.svg)\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![A widget exchanges content with a native HTML list and with files](../../images/widgets/conversions.svg)\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![Pencil editing interface beside reader controls](../../images/widgets/editing.svg)\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![Tab advances focus between widget controls](../../images/widgets/focus.svg)\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![Code and image assets connect to a widget](../../images/widgets/libraries.svg)\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![A full bundle with four code blocks shrinks to a smaller bundle with one block](../../images/widgets/optimize-code-bundle.svg)\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![Three workshop votes feed a live tally](../../images/widgets/live-sessions.svg)\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![Document language maps to translated widget text](../../images/widgets/localization.svg)\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![The old lang-code attribute becomes term-lang while child content is preserved](../../images/widgets/migrations.svg)\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![An observation log provides space for handwritten notes on paper](../../images/widgets/printing.svg)\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![A widget package uploaded to a registry shelf](../../images/widgets/publishing.svg)\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![A selected color flows from a color picker into a separate swatch](../../images/widgets/reactivity.svg)\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![Widget columns reflow into a stack at narrow widths](../../images/widgets/responsiveness.svg)\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![Two participants linked to one shared tally](../../images/widgets/sharing.svg)\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![A widget linked to properties, attributes, content, and data](../../images/widgets/state.svg)\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![A widget under a magnifying glass with a passing check](../../images/widgets/testing.svg)\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![The document’s typography and palette carry over to a timeline widget](../../images/widgets/theming.svg)\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>&lt;ww-weather&gt;</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 &amp; 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 &amp; 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\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"},{"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"}]
package/docs.mjs ADDED
@@ -0,0 +1,26 @@
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 snapshot = new URL("./dist/widget-docs.json", import.meta.url)
7
+
8
+ function readWidgetDocs(directory = checkoutDocs) {
9
+ return readdirSync(directory, {withFileTypes: true}).flatMap(entry => {
10
+ const path = join(directory, entry.name)
11
+ if(entry.isDirectory() && entry.name !== "examples") return readWidgetDocs(path)
12
+ if(!entry.isFile() || !entry.name.endsWith(".md")) return []
13
+ const id = relative(checkoutDocs, path).split(sep).join("/").slice(0, -3)
14
+ return [{id, source: readFileSync(path, "utf8")}]
15
+ })
16
+ }
17
+
18
+ export function loadWidgetDocs() {
19
+ return existsSync(checkoutDocs) ? readWidgetDocs() : JSON.parse(readFileSync(snapshot, "utf8"))
20
+ }
21
+
22
+ if(process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
23
+ const docs = readWidgetDocs().sort((a, b) => a.id.localeCompare(b.id))
24
+ mkdirSync(new URL(".", snapshot), {recursive: true})
25
+ writeFileSync(snapshot, JSON.stringify(docs) + "\n")
26
+ }
package/package.json CHANGED
@@ -1,6 +1,30 @@
1
1
  {
2
2
  "name": "@webwriter/guidance",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "WebWriter widget documentation search and coding agent skill",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "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,35 @@
1
+ ---
2
+ name: webwriter-guidance
3
+ description: Search and retrieve WebWriter's widget documentation. Use before answering WebWriter widget questions or implementing changes that rely on its 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
+ ## Search
11
+
12
+ Search for one capability or API at a time. Use WebWriter terminology and exact identifiers when known.
13
+
14
+ ```sh
15
+ npx -y @webwriter/guidance search "widget package exports"
16
+ npx -y @webwriter/guidance search "optionschange"
17
+ ```
18
+
19
+ 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.
20
+
21
+ 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.
22
+
23
+ ## Retrieve
24
+
25
+ Retrieve the full Markdown for the matching IDs. Pair a task guide with an interface reference when you need exact fields or event behavior.
26
+
27
+ ```sh
28
+ npx -y @webwriter/guidance retrieve "guides/live-sessions,reference/options-actions"
29
+ ```
30
+
31
+ 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.
32
+
33
+ ## Apply
34
+
35
+ 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.
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.