undercity 1.0.0 → 1.0.2

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.
Files changed (287) hide show
  1. package/AGENTS.md +14 -0
  2. package/library/AGENTS.md +492 -0
  3. package/library/display/category.json +7 -0
  4. package/library/display/clear/action.json +11 -0
  5. package/library/display/clear/library.js +17 -0
  6. package/library/display/index.js +23 -0
  7. package/library/display/markdown/action.json +12 -0
  8. package/library/display/markdown/library.js +63 -0
  9. package/library/display/rawHtml/action.json +12 -0
  10. package/library/display/rawHtml/library.js +17 -0
  11. package/library/display/safeHtml/action.json +12 -0
  12. package/library/display/safeHtml/library.js +42 -0
  13. package/library/display/text/action.json +12 -0
  14. package/library/display/text/library.js +17 -0
  15. package/library/display/value/action.json +12 -0
  16. package/library/display/value/library.js +23 -0
  17. package/library/example-category/category.json +7 -0
  18. package/{actions/forms/getRange → library/example-category/example-action}/action.json +2 -5
  19. package/library/example-category/example-action/library.js +0 -0
  20. package/library/index.js +13 -0
  21. package/library/index.json +7 -0
  22. package/library/input/category.json +7 -0
  23. package/library/input/checkbox/action.json +13 -0
  24. package/library/input/checkbox/library.js +54 -0
  25. package/library/input/color/action.json +13 -0
  26. package/library/input/color/library.js +63 -0
  27. package/library/input/date/action.json +15 -0
  28. package/library/input/date/library.js +54 -0
  29. package/library/input/datetimeLocal/action.json +15 -0
  30. package/library/input/datetimeLocal/library.js +54 -0
  31. package/library/input/email/action.json +14 -0
  32. package/library/input/email/library.js +53 -0
  33. package/library/input/file/action.json +15 -0
  34. package/library/input/file/library.js +53 -0
  35. package/library/input/image/action.json +14 -0
  36. package/library/input/image/library.js +59 -0
  37. package/library/input/index.js +47 -0
  38. package/library/input/longText/action.json +16 -0
  39. package/library/input/longText/library.js +54 -0
  40. package/library/input/month/action.json +13 -0
  41. package/library/input/month/library.js +52 -0
  42. package/library/input/number/action.json +17 -0
  43. package/library/input/number/library.js +58 -0
  44. package/library/input/password/action.json +15 -0
  45. package/library/input/password/library.js +68 -0
  46. package/library/input/radio/action.json +14 -0
  47. package/library/input/radio/library.js +73 -0
  48. package/library/input/range/action.json +16 -0
  49. package/library/input/range/library.js +72 -0
  50. package/library/input/tel/action.json +15 -0
  51. package/library/input/tel/library.js +54 -0
  52. package/library/input/text/action.json +16 -0
  53. package/library/input/text/library.js +57 -0
  54. package/library/input/time/action.json +15 -0
  55. package/library/input/time/library.js +54 -0
  56. package/library/input/url/action.json +14 -0
  57. package/library/input/url/library.js +53 -0
  58. package/library/input/week/action.json +13 -0
  59. package/library/input/week/library.js +52 -0
  60. package/library/new-action.js +298 -0
  61. package/library/room/category.json +7 -0
  62. package/library/room/emit/action.json +12 -0
  63. package/library/room/emit/library.js +15 -0
  64. package/library/room/index.js +17 -0
  65. package/library/room/showNav/action.json +12 -0
  66. package/library/room/showNav/library.js +35 -0
  67. package/library/room/take/action.json +11 -0
  68. package/library/room/take/library.js +11 -0
  69. package/package.json +17 -2
  70. package/projects/test-a/project.json +47 -25
  71. package/public/index.html +1 -1
  72. package/src/generator/index.js +1 -1
  73. package/src/generator/page.js +10 -5
  74. package/src/generator/runtime.js +20 -22
  75. package/src/ide/app.js +62 -4
  76. package/src/ide/css/ide.css +21 -0
  77. package/src/ide/savant.js +51 -10
  78. package/src/ide/thing-library.js +14 -0
  79. package/src/server/index.js +1 -1
  80. package/src/server/routes/actions.js +18 -66
  81. package/things/form/thing.json +15 -0
  82. package/actions/AGENTS.md +0 -41
  83. package/actions/_shared/container.js +0 -16
  84. package/actions/auth/ask-login/action.json +0 -15
  85. package/actions/auth/ask-signup/action.json +0 -14
  86. package/actions/display/clear/action.json +0 -18
  87. package/actions/display/clear/action.test.js +0 -32
  88. package/actions/display/markdown/action.json +0 -12
  89. package/actions/display/markdown/action.test.js +0 -32
  90. package/actions/display/rawHtml/action.json +0 -24
  91. package/actions/display/rawHtml/action.test.js +0 -32
  92. package/actions/display/safeHtml/action.json +0 -24
  93. package/actions/display/safeHtml/action.test.js +0 -32
  94. package/actions/display/text/action.js +0 -9
  95. package/actions/display/text/action.json +0 -12
  96. package/actions/display/text/action.test.js +0 -40
  97. package/actions/display/value/action.json +0 -24
  98. package/actions/display/value/action.test.js +0 -32
  99. package/actions/dom/addClass/action.json +0 -23
  100. package/actions/dom/addClass/action.test.js +0 -32
  101. package/actions/dom/focus/action.json +0 -17
  102. package/actions/dom/focus/action.test.js +0 -32
  103. package/actions/dom/hide/action.json +0 -18
  104. package/actions/dom/hide/action.test.js +0 -32
  105. package/actions/dom/removeClass/action.json +0 -22
  106. package/actions/dom/removeClass/action.test.js +0 -32
  107. package/actions/dom/scroll/action.json +0 -29
  108. package/actions/dom/scroll/action.test.js +0 -32
  109. package/actions/dom/setAttr/action.json +0 -29
  110. package/actions/dom/setAttr/action.test.js +0 -32
  111. package/actions/dom/setHtml/action.json +0 -22
  112. package/actions/dom/setHtml/action.test.js +0 -32
  113. package/actions/dom/setStyle/action.json +0 -29
  114. package/actions/dom/setStyle/action.test.js +0 -32
  115. package/actions/dom/setText/action.json +0 -24
  116. package/actions/dom/setText/action.test.js +0 -32
  117. package/actions/dom/show/action.json +0 -18
  118. package/actions/dom/show/action.test.js +0 -32
  119. package/actions/dom/toggle/action.json +0 -17
  120. package/actions/dom/toggle/action.test.js +0 -32
  121. package/actions/dom/toggleClass/action.json +0 -22
  122. package/actions/dom/toggleClass/action.test.js +0 -32
  123. package/actions/event/emit/action.json +0 -24
  124. package/actions/event/emit/action.test.js +0 -32
  125. package/actions/event/on/action.json +0 -23
  126. package/actions/event/on/action.test.js +0 -32
  127. package/actions/event/waitFor/action.json +0 -28
  128. package/actions/event/waitFor/action.test.js +0 -32
  129. package/actions/forms/bindField/action.js +0 -27
  130. package/actions/forms/bindField/action.json +0 -24
  131. package/actions/forms/bindField/action.test.js +0 -20
  132. package/actions/forms/check/action.json +0 -22
  133. package/actions/forms/check/action.test.js +0 -32
  134. package/actions/forms/clearErrors/action.json +0 -11
  135. package/actions/forms/clearErrors/action.test.js +0 -35
  136. package/actions/forms/clearField/action.json +0 -17
  137. package/actions/forms/clearField/action.test.js +0 -32
  138. package/actions/forms/getField/action.json +0 -24
  139. package/actions/forms/getField/action.test.js +0 -32
  140. package/actions/forms/getRange/action.test.js +0 -32
  141. package/actions/forms/getSelect/action.json +0 -22
  142. package/actions/forms/getSelect/action.test.js +0 -32
  143. package/actions/forms/index.js +0 -140
  144. package/actions/forms/serialize/action.json +0 -24
  145. package/actions/forms/serialize/action.test.js +0 -32
  146. package/actions/forms/setCheck/action.json +0 -23
  147. package/actions/forms/setCheck/action.test.js +0 -32
  148. package/actions/forms/setError/action.json +0 -22
  149. package/actions/forms/setError/action.test.js +0 -32
  150. package/actions/forms/setField/action.js +0 -14
  151. package/actions/forms/setField/action.json +0 -24
  152. package/actions/forms/setField/action.test.js +0 -32
  153. package/actions/forms/submit/action.json +0 -18
  154. package/actions/forms/submit/action.test.js +0 -32
  155. package/actions/forms/validate/action.json +0 -24
  156. package/actions/forms/validate/action.test.js +0 -32
  157. package/actions/http/delete/action.json +0 -22
  158. package/actions/http/delete/action.test.js +0 -32
  159. package/actions/http/get/action.json +0 -24
  160. package/actions/http/get/action.test.js +0 -32
  161. package/actions/http/post/action.json +0 -30
  162. package/actions/http/post/action.test.js +0 -32
  163. package/actions/http/put/action.json +0 -27
  164. package/actions/http/put/action.test.js +0 -32
  165. package/actions/http/upload/action.json +0 -35
  166. package/actions/http/upload/action.test.js +0 -32
  167. package/actions/index.js +0 -306
  168. package/actions/index.json +0 -5
  169. package/actions/input/askChoice/action.json +0 -29
  170. package/actions/input/askChoice/action.test.js +0 -32
  171. package/actions/input/askConfirm/action.json +0 -24
  172. package/actions/input/askConfirm/action.test.js +0 -32
  173. package/actions/input/askDate/action.json +0 -24
  174. package/actions/input/askDate/action.test.js +0 -32
  175. package/actions/input/askEmail/action.json +0 -24
  176. package/actions/input/askEmail/action.test.js +0 -32
  177. package/actions/input/askNumber/action.json +0 -35
  178. package/actions/input/askNumber/action.test.js +0 -32
  179. package/actions/input/askPassword/action.json +0 -24
  180. package/actions/input/askPassword/action.test.js +0 -32
  181. package/actions/input/askText/action.json +0 -36
  182. package/actions/input/askText/action.test.js +0 -32
  183. package/actions/logic/delay/action.json +0 -18
  184. package/actions/logic/delay/action.test.js +0 -32
  185. package/actions/logic/if/action.json +0 -28
  186. package/actions/logic/if/action.test.js +0 -32
  187. package/actions/logic/log/action.json +0 -18
  188. package/actions/logic/log/action.test.js +0 -32
  189. package/actions/logic/random/action.json +0 -36
  190. package/actions/logic/random/action.test.js +0 -32
  191. package/actions/logic/transform/action.json +0 -24
  192. package/actions/logic/transform/action.test.js +0 -32
  193. package/actions/media/askAudioUpload/action.json +0 -30
  194. package/actions/media/askAudioUpload/action.test.js +0 -32
  195. package/actions/media/askFileUpload/action.json +0 -36
  196. package/actions/media/askFileUpload/action.test.js +0 -32
  197. package/actions/media/askImageUpload/action.json +0 -37
  198. package/actions/media/askImageUpload/action.test.js +0 -32
  199. package/actions/media/askVideoUpload/action.js +0 -74
  200. package/actions/media/askVideoUpload/action.json +0 -44
  201. package/actions/media/askVideoUpload/action.test.js +0 -51
  202. package/actions/media/captureWebcam/action.json +0 -24
  203. package/actions/media/captureWebcam/action.test.js +0 -32
  204. package/actions/nav/back/action.json +0 -11
  205. package/actions/nav/back/action.test.js +0 -35
  206. package/actions/nav/goto/action.json +0 -18
  207. package/actions/nav/goto/action.test.js +0 -32
  208. package/actions/nav/redirect/action.json +0 -28
  209. package/actions/nav/redirect/action.test.js +0 -32
  210. package/actions/nav/reload/action.json +0 -11
  211. package/actions/nav/reload/action.test.js +0 -35
  212. package/actions/nav/reset/action.json +0 -11
  213. package/actions/nav/reset/action.test.js +0 -35
  214. package/actions/render/alert/action.js +0 -12
  215. package/actions/render/alert/action.json +0 -36
  216. package/actions/render/alert/action.test.js +0 -32
  217. package/actions/render/button/action.js +0 -15
  218. package/actions/render/button/action.json +0 -44
  219. package/actions/render/button/action.test.js +0 -37
  220. package/actions/render/clear/action.js +0 -7
  221. package/actions/render/clear/action.json +0 -11
  222. package/actions/render/clear/action.test.js +0 -35
  223. package/actions/render/divider/action.js +0 -8
  224. package/actions/render/divider/action.json +0 -11
  225. package/actions/render/divider/action.test.js +0 -35
  226. package/actions/render/field/action.js +0 -17
  227. package/actions/render/field/action.json +0 -59
  228. package/actions/render/field/action.test.js +0 -57
  229. package/actions/render/link/action.js +0 -20
  230. package/actions/render/link/action.json +0 -30
  231. package/actions/render/link/action.test.js +0 -32
  232. package/actions/render/markdown/action.json +0 -18
  233. package/actions/render/markdown/action.test.js +0 -32
  234. package/actions/render/paragraph/action.js +0 -9
  235. package/actions/render/paragraph/action.json +0 -32
  236. package/actions/render/paragraph/action.test.js +0 -32
  237. package/actions/render/section/action.js +0 -10
  238. package/actions/render/section/action.json +0 -18
  239. package/actions/render/section/action.test.js +0 -32
  240. package/actions/render/subtitle/action.js +0 -9
  241. package/actions/render/subtitle/action.json +0 -18
  242. package/actions/render/subtitle/action.test.js +0 -32
  243. package/actions/render/title/action.js +0 -9
  244. package/actions/render/title/action.json +0 -30
  245. package/actions/render/title/action.test.js +0 -44
  246. package/actions/session/clear/action.json +0 -11
  247. package/actions/session/clear/action.test.js +0 -35
  248. package/actions/session/load/action.json +0 -22
  249. package/actions/session/load/action.test.js +0 -32
  250. package/actions/session/local/action.json +0 -22
  251. package/actions/session/local/action.test.js +0 -32
  252. package/actions/session/save/action.json +0 -22
  253. package/actions/session/save/action.test.js +0 -32
  254. package/actions/ui/accordion/action.json +0 -23
  255. package/actions/ui/accordion/action.test.js +0 -32
  256. package/actions/ui/badge/action.json +0 -22
  257. package/actions/ui/badge/action.test.js +0 -32
  258. package/actions/ui/collapse/action.json +0 -23
  259. package/actions/ui/collapse/action.test.js +0 -32
  260. package/actions/ui/hideModal/action.json +0 -17
  261. package/actions/ui/hideModal/action.test.js +0 -32
  262. package/actions/ui/loading/action.json +0 -18
  263. package/actions/ui/loading/action.test.js +0 -32
  264. package/actions/ui/modal/action.json +0 -18
  265. package/actions/ui/modal/action.test.js +0 -32
  266. package/actions/ui/progress/action.json +0 -24
  267. package/actions/ui/progress/action.test.js +0 -32
  268. package/actions/ui/toast/action.json +0 -29
  269. package/actions/ui/toast/action.test.js +0 -32
  270. package/actions/ui/tooltip/action.json +0 -17
  271. package/actions/ui/tooltip/action.test.js +0 -32
  272. package/actions/user/carry/action.json +0 -25
  273. package/actions/user/carry/action.test.js +0 -32
  274. package/actions/user/check/action.json +0 -24
  275. package/actions/user/check/action.test.js +0 -32
  276. package/actions/user/clear/action.json +0 -11
  277. package/actions/user/clear/action.test.js +0 -35
  278. package/actions/user/delete/action.json +0 -17
  279. package/actions/user/delete/action.test.js +0 -32
  280. package/actions/user/dump/action.json +0 -11
  281. package/actions/user/dump/action.test.js +0 -35
  282. package/actions/user/get/action.json +0 -24
  283. package/actions/user/get/action.test.js +0 -32
  284. package/actions/user/merge/action.json +0 -18
  285. package/actions/user/merge/action.test.js +0 -32
  286. package/actions/user/set/action.json +0 -24
  287. package/actions/user/set/action.test.js +0 -32
package/AGENTS.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Undercity — AGENTS.md
2
2
 
3
+ ## DO NOT INSTALL NPM DEPENDENCIES
4
+
5
+ npm packages are unsafe and should not be installed
6
+
7
+ ## PUSH ARCHITECTURE
8
+
9
+ use Signal, combineLatest, Scope, Events, Disposable, CompositeDisposable, Repeater and similar
10
+
11
+ See src/lib/signal.js src/lib/scope.js
12
+
13
+ ## USE WEB COMPONENTS
14
+
15
+ Use web-components/custom-elements to create reusable and protable code
16
+
3
17
  ## The MUD Agent Model
4
18
 
5
19
  Undercity's architecture is grounded in the **Multi-User Dungeon (MUD) metaphor**:
@@ -0,0 +1,492 @@
1
+ # library/ — Action Library Specification
2
+
3
+ **Agents: read this entire file before touching anything in `library/`.**
4
+
5
+ This is the authoritative specification for Undercity's action library.
6
+ `./library/` replaces `./actions/`. Do not modify `./actions/`.
7
+
8
+ ---
9
+
10
+ ## Directory layout
11
+
12
+ ```
13
+ library/
14
+ AGENTS.md ← this file (do not edit)
15
+ new-action.js ← scaffolding CLI — the ONLY way to create actions
16
+ index.js ← single entry point export default library
17
+ index.json ← registry: which categories and actions exist
18
+ <category>/
19
+ category.json ← category metadata
20
+ <actionName>/
21
+ action.json ← IDE metadata: id, label, desc, params, icon, color
22
+ library.js ← runtime implementation (Emitter API — see below)
23
+ ```
24
+
25
+ Every file except `AGENTS.md` and `new-action.js` is either generated by
26
+ `new-action.js` or maintained according to the schemas in this document.
27
+
28
+ ---
29
+
30
+ ## Categories
31
+
32
+ Three categories are defined. Category id = directory name = UI label slug.
33
+
34
+ | Directory | id | UI Label | Icon | Color |
35
+ |------------|-----------|-----------|-------------------|--------------------|
36
+ | `input/` | `input` | Input | `pencil-square` | `var(--sol-blue)` |
37
+ | `room/` | `room` | Room | `broadcast` | `var(--sol-orange)`|
38
+ | `display/` | `display` | Display | `type` | `var(--sol-cyan)` |
39
+
40
+ - **input** — 18 smart inline form inputs. Each renders directly onto the page
41
+ and reacts to Inventory in real time (two-way binding). Use `room.take` to
42
+ package them up.
43
+ - **room** — Core room behaviours: emit events, show nav buttons, take a form.
44
+ Pinned to top in the UI because room wiring is foundational.
45
+ - **display** — Read inventory values and display them on the page as text,
46
+ markdown, sanitised HTML, or raw HTML.
47
+
48
+ ---
49
+
50
+ ## Import map — no relative paths in `library.js`
51
+
52
+ All `library.js` files import via bare specifiers. Never use `../../../` paths.
53
+
54
+ | Bare specifier | Resolved file | What it exports |
55
+ |-----------------|-----------------------------|-------------------------------------------------------------|
56
+ | `framework` | `src/lib/signal.js` | `Signal`, `Emitter`, `Disposable`, `CompositeDisposable`, `Repeater`, `on` |
57
+ | `scope` | `src/lib/scope.js` | `Scope` |
58
+
59
+ The importmap is already declared in `public/index.html` for the IDE. The
60
+ generator (`src/generator/page.js`) must inject an equivalent importmap into
61
+ every generated page's `<head>`, pointing to the copied files in `./js/`:
62
+
63
+ ```html
64
+ <script type="importmap">
65
+ {
66
+ "imports": {
67
+ "framework": "./js/signal.js",
68
+ "scope": "./js/scope.js"
69
+ }
70
+ }
71
+ </script>
72
+ ```
73
+
74
+ The generator must also copy `src/lib/signal.js` and `src/lib/scope.js` into
75
+ the `js/` output directory alongside `runtime.js` and `af-icons.js`.
76
+
77
+ ---
78
+
79
+ ## Scaffolding: `new-action.js` is mandatory
80
+
81
+ **Agents MUST run `new-action.js` to create every new action. Never create
82
+ `action.json` or `library.js` by hand.** The tool ensures consistent structure,
83
+ updates `index.json`, and keeps the category `index.js` in sync.
84
+
85
+ ```
86
+ node library/new-action.js --category <id> --name <camelCaseName>
87
+ ```
88
+
89
+ Examples:
90
+ ```
91
+ node library/new-action.js --category input --name text
92
+ node library/new-action.js --category room --name emit
93
+ node library/new-action.js --category display --name value
94
+ ```
95
+
96
+ The tool:
97
+ 1. Creates `library/<category>/<name>/` with `action.json` and `library.js`.
98
+ 2. Registers the action in `library/<category>/index.js`.
99
+ 3. Registers the category in `library/index.json` if not already present.
100
+ 4. Prints a checklist of fields to fill in.
101
+
102
+ ---
103
+
104
+ ## `index.json` schema
105
+
106
+ Tracks which categories are active. Maintained by `new-action.js`.
107
+
108
+ ```json
109
+ {
110
+ "categories": ["input", "room", "display"]
111
+ }
112
+ ```
113
+
114
+ ---
115
+
116
+ ## `category.json` schema
117
+
118
+ One per category directory. Created by `new-action.js` on first action in the
119
+ category.
120
+
121
+ ```json
122
+ {
123
+ "id": "input",
124
+ "name": "Input",
125
+ "icon": "pencil-square",
126
+ "color": "var(--sol-blue)",
127
+ "description": "Smart inline form inputs, each two-way bound to Inventory."
128
+ }
129
+ ```
130
+
131
+ Rules:
132
+ - `id` must match the directory name exactly.
133
+ - `name` is the human-readable UI label shown in the Savant sidebar.
134
+ - `icon` is a Bootstrap Icons name (no `.svg` extension).
135
+ - `color` is a CSS variable from the Solarized palette.
136
+
137
+ ---
138
+
139
+ ## `action.json` schema
140
+
141
+ One per action directory. Describes the action to the IDE — no runtime code.
142
+
143
+ ```json
144
+ {
145
+ "id": "input.text",
146
+ "icon": "input-cursor-text",
147
+ "color": "var(--sol-blue)",
148
+ "label": "Ask For Text",
149
+ "desc": "Render a single-line text field on the page. Two-way bound to an inventory key — as the user types, inventory updates instantly.",
150
+ "version": "1.0.0",
151
+ "params": [
152
+ { "name": "key", "label": "Inventory key", "type": "text", "placeholder": "firstName" },
153
+ { "name": "label", "label": "Label", "type": "text", "placeholder": "First Name" },
154
+ { "name": "placeholder", "label": "Placeholder", "type": "text", "placeholder": "Type here…" },
155
+ { "name": "required", "label": "Required", "type": "boolean", "default": false }
156
+ ]
157
+ }
158
+ ```
159
+
160
+ Rules:
161
+ - `id` format: `<categoryId>.<camelCaseName>` — must match directory path exactly.
162
+ - `desc` states WHEN the action runs and WHERE results go.
163
+ - `params[].type` is one of: `text`, `code`, `textarea`, `json`, `boolean`,
164
+ `number`, `select`, `inventory-key`, `room`.
165
+ - `params[].default` is optional; omit if there is no sensible default.
166
+ - `params[].placeholder` is optional; include for text/code/textarea types.
167
+ - `params[].options` is required when `type` is `select`.
168
+
169
+ ---
170
+
171
+ ## `library.js` — Emitter API (runtime implementation)
172
+
173
+ Every action's runtime lives in `library.js`. This is the browser-side module
174
+ loaded at page runtime. All actions share the same API contract.
175
+
176
+ ### API contract
177
+
178
+ ```js
179
+ // library/<category>/<actionName>/library.js
180
+ import { Emitter } from 'framework';
181
+
182
+ /**
183
+ * run(params, ctx) — execute this action.
184
+ *
185
+ * @param {object} params — resolved param values from action.json (user config)
186
+ * @param {object} ctx — runtime context (see below)
187
+ * @returns {Emitter} — lifecycle emitter (callers listen on this)
188
+ */
189
+ export function run(params, ctx) {
190
+ const emitter = new Emitter();
191
+
192
+ // --- action logic here ---
193
+ // Use ctx.container, ctx.inventory, ctx.room, ctx.scope (see Context below)
194
+ // When done: emitter.emit('done', optionalResult)
195
+ // On error: emitter.emit('error', new Error('reason'))
196
+
197
+ return emitter; // return BEFORE async work completes
198
+ }
199
+ ```
200
+
201
+ ### Standard emitted events
202
+
203
+ | Event | Payload | Meaning |
204
+ |----------|---------------------|----------------------------------------------|
205
+ | `render` | `HTMLElement` | Action produced a DOM element to display |
206
+ | `done` | `any` (optional) | Action completed successfully |
207
+ | `error` | `Error` | Action failed; include a descriptive message |
208
+
209
+ Callers subscribe like:
210
+ ```js
211
+ const em = run(params, ctx);
212
+ em.on('render', el => ctx.container.appendChild(el));
213
+ em.on('done', result => next());
214
+ em.on('error', err => showError(err.message));
215
+ ```
216
+
217
+ ### Context object (`ctx`)
218
+
219
+ ```js
220
+ ctx.container // HTMLElement — the room's content area; append rendered UI here
221
+ ctx.inventory // Signal<object> — read/write user inventory
222
+ // read: ctx.inventory.value.myKey
223
+ // write: ctx.inventory.value = { ...ctx.inventory.value, myKey: v }
224
+ ctx.room // Emitter — room event bus
225
+ // emit: ctx.room.emit('myEvent', data)
226
+ // listen: ctx.room.on('myEvent', fn) → returns Disposable
227
+ ctx.scope // Scope — action-instance lifetime (from 'scope')
228
+ // add subscriptions: ctx.scope.add(disposable)
229
+ // disposed automatically when the room tears down
230
+ ```
231
+
232
+ ### Two-way binding pattern (used by all input actions)
233
+
234
+ Input actions are **Web Components** so the DOM owns its own cleanup via
235
+ `disconnectedCallback`. The component uses a `Scope` for subscriptions and
236
+ disposes them when removed. `Signal.combineLatest` handles multi-signal reads.
237
+
238
+ ```js
239
+ import { Emitter, Signal, on } from 'framework';
240
+ import { Scope } from 'scope';
241
+
242
+ const TAG = 'uc-input-text';
243
+
244
+ if (!customElements.get(TAG)) {
245
+ customElements.define(TAG, class extends HTMLElement {
246
+ #scope = new Scope();
247
+
248
+ connectedCallback() {
249
+ const { params, ctx } = this;
250
+
251
+ // Build DOM
252
+ const label = document.createElement('label');
253
+ label.textContent = params.label ?? '';
254
+ const input = document.createElement('input');
255
+ input.type = 'text';
256
+ input.name = params.key;
257
+ input.placeholder = params.placeholder ?? '';
258
+ input.required = params.required ?? false;
259
+ this.append(label, input);
260
+
261
+ // Push: Inventory → DOM (subscribe fires immediately with current value)
262
+ this.#scope.add(
263
+ ctx.inventory.subscribe(inv => {
264
+ const v = String(inv[params.key] ?? '');
265
+ if (input.value !== v) input.value = v;
266
+ })
267
+ );
268
+
269
+ // Push: DOM → Inventory
270
+ this.#scope.add(
271
+ on(input, 'input', () => {
272
+ ctx.inventory.value = { ...ctx.inventory.value, [params.key]: input.value };
273
+ })
274
+ );
275
+ }
276
+
277
+ disconnectedCallback() {
278
+ this.#scope.dispose();
279
+ }
280
+ });
281
+ }
282
+
283
+ export function run(params, ctx) {
284
+ const emitter = new Emitter();
285
+ const el = Object.assign(document.createElement(TAG), { params, ctx });
286
+ emitter.emit('render', el);
287
+ emitter.emit('done');
288
+ return emitter;
289
+ }
290
+ ```
291
+
292
+ #### `combineLatest` — when an action depends on multiple signals
293
+
294
+ Use `Signal.combineLatest` when the component needs to react to two or more
295
+ signals simultaneously, e.g. inventory + a separate config signal:
296
+
297
+ ```js
298
+ connectedCallback() {
299
+ const combined = Signal.combineLatest([ctx.inventory, ctx.config]);
300
+ this.#scope.add(combined); // disposes the combiner's internal subs
301
+ this.#scope.add(
302
+ combined.subscribe(([inv, cfg]) => {
303
+ // update DOM from both signals at once
304
+ })
305
+ );
306
+ }
307
+ ```
308
+
309
+ Always `scope.add(combined)` before `scope.add(combined.subscribe(...))` so the
310
+ combiner is torn down before its subscriber.
311
+
312
+ ### Rules for `library.js`
313
+
314
+ - Import only from `'framework'`, `'scope'`, or browser built-ins. No relative paths.
315
+ - No shared helpers between actions — each `library.js` is fully self-contained.
316
+ Three similar lines of code is better than a premature abstraction.
317
+ - Input actions MUST be Web Components (one `customElements.define` per action,
318
+ guarded with `if (!customElements.get(TAG))`).
319
+ - Web Components manage their own cleanup in `disconnectedCallback` via `Scope`.
320
+ - Always `return emitter` before any async operations begin.
321
+ - Always emit either `done` or `error` — never leave callers hanging.
322
+ - Render actions must emit `render` before `done`.
323
+ - Rely on push architecture — subscribe to signals, do not poll.
324
+
325
+ ---
326
+
327
+ ## Category `index.js` — plugin entry point
328
+
329
+ Each category directory has an `index.js` that aggregates all actions in that
330
+ category and exports a plugin object compatible with `App.use()`.
331
+
332
+ `new-action.js` generates and maintains this file. Do not edit by hand.
333
+
334
+ ```js
335
+ // library/input/index.js (generated — maintained by new-action.js)
336
+ import { run as textRun } from './text/library.js';
337
+ import textMeta from './text/action.json' assert { type: 'json' };
338
+ // ... one pair per action
339
+
340
+ import categoryMeta from './category.json' assert { type: 'json' };
341
+
342
+ export const inputCategory = {
343
+ name: `library/${categoryMeta.id}`,
344
+ install(app) {
345
+ app.registerCategory(categoryMeta, {
346
+ [textMeta.id]: { ...textMeta, run: textRun },
347
+ // ...
348
+ });
349
+ },
350
+ };
351
+ ```
352
+
353
+ ---
354
+
355
+ ## `library/index.js` — single entry point
356
+
357
+ **Do not touch this file.** It is the only file the application imports. To add
358
+ a new category: run `new-action.js` — it keeps `index.json` up to date and
359
+ `index.js` is regenerated from `index.json` automatically.
360
+
361
+ ```js
362
+ // library/index.js
363
+ import { inputCategory } from './input/index.js';
364
+ import { roomCategory } from './room/index.js';
365
+ import { displayCategory } from './display/index.js';
366
+
367
+ export default {
368
+ name: 'undercity/library',
369
+ install(app) {
370
+ app.use(roomCategory); // pinned first — foundational
371
+ app.use(inputCategory);
372
+ app.use(displayCategory);
373
+ },
374
+ };
375
+ ```
376
+
377
+ Application usage:
378
+ ```js
379
+ import library from './library/index.js';
380
+ app.use(library);
381
+ ```
382
+
383
+ To switch to a hypothetical `library-v2`, change only this one import path in
384
+ the application.
385
+
386
+ ---
387
+
388
+ ## Naming rules
389
+
390
+ | Thing | Convention | Example |
391
+ |---------------------|---------------------------------------------|----------------------|
392
+ | Category id | lowercase, no spaces, no hyphens | `input` |
393
+ | Category directory | same as id | `input/` |
394
+ | Action id | `<categoryId>.<camelCaseName>` | `input.text` |
395
+ | Action directory | camelCase name only (no category prefix) | `text/` |
396
+ | Web Component tag | `uc-<category>-<name>` (kebab-case) | `uc-input-text` |
397
+ | `library.js` export | named export `run` | `export function run`|
398
+
399
+ ---
400
+
401
+ ## Actions to port (reference)
402
+
403
+ ### `input/` — 18 actions (from `actions/index.js` → `InputPlugin`)
404
+
405
+ | Directory | action.json `id` | Label |
406
+ |-------------------|--------------------------|-------------------------|
407
+ | `text/` | `input.text` | Ask For Text |
408
+ | `longText/` | `input.longText` | Ask For Long Text |
409
+ | `email/` | `input.email` | Ask For Email Address |
410
+ | `password/` | `input.password` | Ask For Password |
411
+ | `tel/` | `input.tel` | Ask For Phone Number |
412
+ | `url/` | `input.url` | Ask For Web Address |
413
+ | `number/` | `input.number` | Ask For Number |
414
+ | `range/` | `input.range` | Ask For Numeric Range |
415
+ | `date/` | `input.date` | Ask For Date |
416
+ | `datetimeLocal/` | `input.datetimeLocal` | Ask For Date & Time |
417
+ | `time/` | `input.time` | Ask For Time |
418
+ | `month/` | `input.month` | Ask For Month |
419
+ | `week/` | `input.week` | Ask For Week |
420
+ | `color/` | `input.color` | Ask For Color |
421
+ | `checkbox/` | `input.checkbox` | Ask With Checkbox |
422
+ | `radio/` | `input.radio` | Ask With Radio Buttons |
423
+ | `file/` | `input.file` | Ask For File |
424
+ | `image/` | `input.image` | Ask For Image |
425
+
426
+ Params for each are defined in `actions/index.js` → `INPUT_CATEGORY`. Copy
427
+ them verbatim into `action.json`, then implement `library.js` using the
428
+ two-way binding / Web Component pattern above.
429
+
430
+ ### `room/` — 3 actions (from `actions/index.js` → `RoomPlugin`)
431
+
432
+ | Directory | action.json `id` | Label |
433
+ |-------------|-------------------|--------------------------|
434
+ | `emit/` | `room.emit` | Emit Room Event |
435
+ | `showNav/` | `room.showNav` | Show Navigation Buttons |
436
+ | `take/` | `room.take` | Take Form |
437
+
438
+ Params from `actions/index.js` → `ROOM_CATEGORY`.
439
+
440
+ ### `display/` — 6 actions (from `src/ide/action-library.js` → `display`)
441
+
442
+ | Directory | action.json `id` | Label |
443
+ |-------------|---------------------|------------------------|
444
+ | `text/` | `display.text` | Print Text |
445
+ | `markdown/` | `display.markdown` | Print Markdown |
446
+ | `safeHtml/` | `display.safeHtml` | Print Safe HTML |
447
+ | `rawHtml/` | `display.rawHtml` | Print Raw HTML |
448
+ | `clear/` | `display.clear` | Clear Content |
449
+ | `value/` | `display.value` | Show Inventory Value |
450
+
451
+ Params from `src/ide/action-library.js` → `ACTION_LIBRARY.display`.
452
+
453
+ ---
454
+
455
+ ## Example: complete action
456
+
457
+ `library/display/text/action.json`:
458
+ ```json
459
+ {
460
+ "id": "display.text",
461
+ "icon": "body-text",
462
+ "color": "var(--sol-cyan)",
463
+ "label": "Print Text",
464
+ "desc": "Set plain text content inside a DOM selector. Safe — no HTML interpretation.",
465
+ "version": "1.0.0",
466
+ "params": [
467
+ { "name": "selector", "label": "Target selector", "type": "text", "placeholder": "#my-label" },
468
+ { "name": "text", "label": "Text", "type": "code", "placeholder": "\"Hello \" + inventory.firstName" }
469
+ ]
470
+ }
471
+ ```
472
+
473
+ `library/display/text/library.js`:
474
+ ```js
475
+ import { Emitter } from 'framework';
476
+
477
+ export function run(params, ctx) {
478
+ const emitter = new Emitter();
479
+
480
+ try {
481
+ const el = document.querySelector(params.selector);
482
+ if (!el) throw new Error(`display.text: selector not found: ${params.selector}`);
483
+ // params.text is already evaluated by the runtime before run() is called
484
+ el.textContent = params.text;
485
+ emitter.emit('done');
486
+ } catch (err) {
487
+ emitter.emit('error', err);
488
+ }
489
+
490
+ return emitter;
491
+ }
492
+ ```
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "display",
3
+ "name": "Display",
4
+ "icon": "type",
5
+ "color": "var(--sol-cyan)",
6
+ "description": "Display inventory values as text, markdown, or HTML."
7
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "display.clear",
3
+ "icon": "eraser",
4
+ "color": "var(--sol-cyan)",
5
+ "label": "Clear Content",
6
+ "desc": "Empty the innerHTML of a selector.",
7
+ "version": "1.0.0",
8
+ "params": [
9
+ { "name": "selector", "label": "Target selector", "type": "text", "placeholder": "#my-area" }
10
+ ]
11
+ }
@@ -0,0 +1,17 @@
1
+ // library/display/clear/library.js
2
+ import { Emitter } from 'framework';
3
+
4
+ export function run(params, ctx) {
5
+ const emitter = new Emitter();
6
+
7
+ try {
8
+ const el = document.querySelector(params.selector);
9
+ if (!el) throw new Error(`display.clear: selector not found: ${params.selector}`);
10
+ el.innerHTML = '';
11
+ emitter.emit('done');
12
+ } catch (err) {
13
+ emitter.emit('error', err);
14
+ }
15
+
16
+ return emitter;
17
+ }
@@ -0,0 +1,23 @@
1
+ // library/display/index.js — generated by new-action.js, do not edit by hand
2
+ import { run as clearRun } from './clear/library.js';
3
+ import { run as markdownRun } from './markdown/library.js';
4
+ import { run as rawHtmlRun } from './rawHtml/library.js';
5
+ import { run as safeHtmlRun } from './safeHtml/library.js';
6
+ import { run as textRun } from './text/library.js';
7
+ import { run as valueRun } from './value/library.js';
8
+
9
+ const categoryMeta = {"id":"display","name":"Display","icon":"type","color":"var(--sol-cyan)","description":"Display inventory values as text, markdown, or HTML."};
10
+
11
+ export const displayCategory = {
12
+ name: `library/${categoryMeta.id}`,
13
+ install(app) {
14
+ app.registerCategory(categoryMeta, {
15
+ "display.clear": { ...{"id":"display.clear","icon":"eraser","color":"var(--sol-cyan)","label":"Clear Content","desc":"Empty the innerHTML of a selector.","version":"1.0.0","params":[{"name":"selector","label":"Target selector","type":"text","placeholder":"#my-area"}]}, run: clearRun },
16
+ "display.markdown": { ...{"id":"display.markdown","icon":"markdown","color":"var(--sol-cyan)","label":"Print Markdown","desc":"Render a Markdown string as HTML inside a selector.","version":"1.0.0","params":[{"name":"selector","label":"Target selector","type":"text","placeholder":"#my-area"},{"name":"content","label":"Markdown content","type":"textarea","placeholder":"**Bold** and _italic_"}]}, run: markdownRun },
17
+ "display.rawHtml": { ...{"id":"display.rawHtml","icon":"code-slash","color":"var(--sol-cyan)","label":"Print Raw HTML","desc":"Inject trusted HTML directly (no sanitization). Use only with controlled content.","version":"1.0.0","params":[{"name":"selector","label":"Target selector","type":"text","placeholder":"#my-area"},{"name":"html","label":"HTML","type":"textarea","placeholder":"<b>Trusted HTML</b>"}]}, run: rawHtmlRun },
18
+ "display.safeHtml": { ...{"id":"display.safeHtml","icon":"shield-check","color":"var(--sol-cyan)","label":"Print Safe HTML","desc":"Sanitize and inject HTML — script/event attributes stripped.","version":"1.0.0","params":[{"name":"selector","label":"Target selector","type":"text","placeholder":"#my-area"},{"name":"html","label":"HTML","type":"textarea","placeholder":"<b>Bold</b>"}]}, run: safeHtmlRun },
19
+ "display.text": { ...{"id":"display.text","icon":"body-text","color":"var(--sol-cyan)","label":"Print Text","desc":"Set plain text content inside a selector. Safe — no HTML interpretation.","version":"1.0.0","params":[{"name":"selector","label":"Target selector","type":"text","placeholder":"#my-label"},{"name":"text","label":"Text","type":"code","placeholder":"\"Hello \" + inventory.firstName"}]}, run: textRun },
20
+ "display.value": { ...{"id":"display.value","icon":"database","color":"var(--sol-cyan)","label":"Show Inventory Value","desc":"Print an inventory key's value as text into a selector.","version":"1.0.0","params":[{"name":"selector","label":"Target selector","type":"text","placeholder":"#name-label"},{"name":"key","label":"Inventory key","type":"text","placeholder":"firstName"}]}, run: valueRun },
21
+ });
22
+ },
23
+ };
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "display.markdown",
3
+ "icon": "markdown",
4
+ "color": "var(--sol-cyan)",
5
+ "label": "Print Markdown",
6
+ "desc": "Render a Markdown string as HTML inside a selector.",
7
+ "version": "1.0.0",
8
+ "params": [
9
+ { "name": "selector", "label": "Target selector", "type": "text", "placeholder": "#my-area" },
10
+ { "name": "content", "label": "Markdown content","type": "textarea", "placeholder": "**Bold** and _italic_" }
11
+ ]
12
+ }
@@ -0,0 +1,63 @@
1
+ // library/display/markdown/library.js
2
+ import { Emitter } from 'framework';
3
+
4
+ function markdownToHtml(md) {
5
+ // Process line by line for headings, then handle inline elements.
6
+ const lines = md.split('\n');
7
+ const output = [];
8
+ let inPara = false;
9
+
10
+ const flushPara = () => {
11
+ if (inPara) { output.push('</p>'); inPara = false; }
12
+ };
13
+
14
+ const inline = str =>
15
+ str
16
+ // Bold: **text**
17
+ .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
18
+ // Italic: _text_ (not preceded by word char to avoid mid-word matches)
19
+ .replace(/(?<!\w)_(.+?)_(?!\w)/g, '<em>$1</em>')
20
+ // Inline code: `code`
21
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
22
+ // Links: [text](url)
23
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
24
+
25
+ for (const line of lines) {
26
+ // Blank line
27
+ if (line.trim() === '') {
28
+ flushPara();
29
+ continue;
30
+ }
31
+
32
+ // Headings
33
+ const h = line.match(/^(#{1,6})\s+(.*)/);
34
+ if (h) {
35
+ flushPara();
36
+ const level = h[1].length;
37
+ output.push(`<h${level}>${inline(h[2])}</h${level}>`);
38
+ continue;
39
+ }
40
+
41
+ // Normal text — accumulate into paragraph
42
+ if (!inPara) { output.push('<p>'); inPara = true; }
43
+ output.push(inline(line));
44
+ }
45
+
46
+ flushPara();
47
+ return output.join('');
48
+ }
49
+
50
+ export function run(params, ctx) {
51
+ const emitter = new Emitter();
52
+
53
+ try {
54
+ const el = document.querySelector(params.selector);
55
+ if (!el) throw new Error(`display.markdown: selector not found: ${params.selector}`);
56
+ el.innerHTML = markdownToHtml(params.content ?? '');
57
+ emitter.emit('done');
58
+ } catch (err) {
59
+ emitter.emit('error', err);
60
+ }
61
+
62
+ return emitter;
63
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "display.rawHtml",
3
+ "icon": "code-slash",
4
+ "color": "var(--sol-cyan)",
5
+ "label": "Print Raw HTML",
6
+ "desc": "Inject trusted HTML directly (no sanitization). Use only with controlled content.",
7
+ "version": "1.0.0",
8
+ "params": [
9
+ { "name": "selector", "label": "Target selector", "type": "text", "placeholder": "#my-area" },
10
+ { "name": "html", "label": "HTML", "type": "textarea", "placeholder": "<b>Trusted HTML</b>" }
11
+ ]
12
+ }