@honua/app-platform 0.1.2-beta.0 → 0.1.4-beta.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.
Files changed (383) hide show
  1. package/collaboration/client.d.ts.map +1 -1
  2. package/connect-url-safety.d.ts +53 -0
  3. package/connect-url-safety.js +189 -0
  4. package/contract/discovery.d.ts +34 -0
  5. package/contract/discovery.d.ts.map +1 -1
  6. package/contract/index.d.ts +7 -5
  7. package/contract/index.d.ts.map +1 -1
  8. package/contract/index.js +3 -2
  9. package/contract/pmtiles.d.ts +10 -1
  10. package/contract/pmtiles.d.ts.map +1 -1
  11. package/contract/pmtiles.js +10 -2
  12. package/contract/protocol-module.d.ts +84 -9
  13. package/contract/protocol-module.d.ts.map +1 -1
  14. package/contract/query-filter-fes.d.ts +18 -0
  15. package/contract/query-filter-fes.d.ts.map +1 -0
  16. package/contract/query-filter-fes.js +122 -0
  17. package/contract/query-filter.d.ts +300 -0
  18. package/contract/query-filter.d.ts.map +1 -0
  19. package/contract/query-filter.js +883 -0
  20. package/contract/schema.d.ts +12 -0
  21. package/contract/schema.d.ts.map +1 -1
  22. package/contract/schema.js +34 -0
  23. package/contract/source-capability-support.d.ts.map +1 -1
  24. package/contract/source-capability-support.js +15 -3
  25. package/contract/source.d.ts +45 -25
  26. package/contract/source.d.ts.map +1 -1
  27. package/contract/source.js +719 -479
  28. package/contract/types.d.ts +87 -18
  29. package/contract/types.d.ts.map +1 -1
  30. package/contract/widget-source.d.ts.map +1 -1
  31. package/controls/basemap-switcher.d.ts +19 -11
  32. package/controls/basemap-switcher.d.ts.map +1 -1
  33. package/controls/basemap-switcher.js +58 -25
  34. package/controls/catalog.d.ts +105 -0
  35. package/controls/catalog.d.ts.map +1 -0
  36. package/controls/catalog.js +341 -0
  37. package/controls/element-utils.d.ts +9 -0
  38. package/controls/element-utils.d.ts.map +1 -1
  39. package/controls/element-utils.js +17 -0
  40. package/controls/index.d.ts +29 -8
  41. package/controls/index.d.ts.map +1 -1
  42. package/controls/index.js +26 -8
  43. package/controls/layer-list.d.ts +13 -5
  44. package/controls/layer-list.d.ts.map +1 -1
  45. package/controls/layer-list.js +72 -15
  46. package/controls/legend.d.ts +32 -5
  47. package/controls/legend.d.ts.map +1 -1
  48. package/controls/legend.js +95 -25
  49. package/controls/qualification.d.ts +152 -0
  50. package/controls/qualification.d.ts.map +1 -0
  51. package/controls/qualification.js +1265 -0
  52. package/controls/registry.d.ts +104 -0
  53. package/controls/registry.d.ts.map +1 -0
  54. package/controls/registry.js +150 -0
  55. package/controls/swipe-control.d.ts +6 -1
  56. package/controls/swipe-control.d.ts.map +1 -1
  57. package/controls/swipe-control.js +82 -20
  58. package/core/client.d.ts +45 -21
  59. package/core/client.d.ts.map +1 -1
  60. package/core/client.js +224 -46
  61. package/core/error-classifications.d.ts +6 -0
  62. package/core/error-classifications.d.ts.map +1 -1
  63. package/core/error-classifications.js +6 -0
  64. package/core/error-code-registry.d.ts +6 -0
  65. package/core/error-code-registry.d.ts.map +1 -1
  66. package/core/error-code-registry.js +6 -0
  67. package/core/error-envelope.js +2 -0
  68. package/core/errors.d.ts +1 -1
  69. package/core/errors.js +1 -1
  70. package/core/geoparquet-sql.d.ts +68 -4
  71. package/core/geoparquet-sql.d.ts.map +1 -1
  72. package/core/geoparquet-sql.js +325 -8
  73. package/core/odata-write-codec.d.ts.map +1 -1
  74. package/core/odata-write-codec.js +0 -1
  75. package/core/odata.d.ts +6 -0
  76. package/core/odata.d.ts.map +1 -1
  77. package/core/odata.js +6 -1
  78. package/core/ogc-processes.d.ts +182 -25
  79. package/core/ogc-processes.d.ts.map +1 -1
  80. package/core/ogc-processes.js +602 -30
  81. package/core/process-runner.d.ts +8 -1
  82. package/core/process-runner.d.ts.map +1 -1
  83. package/core/process-runner.js +1 -0
  84. package/core/protocol-transport.d.ts +2 -0
  85. package/core/protocol-transport.d.ts.map +1 -1
  86. package/core/stac.d.ts +31 -0
  87. package/core/stac.d.ts.map +1 -1
  88. package/core/stac.js +136 -19
  89. package/core/surfaces.d.ts.map +1 -1
  90. package/core/surfaces.js +273 -68
  91. package/core/types.d.ts +123 -9
  92. package/core/types.d.ts.map +1 -1
  93. package/core/wfs-axis-order.d.ts +18 -0
  94. package/core/wfs-axis-order.d.ts.map +1 -0
  95. package/core/wfs-axis-order.js +108 -0
  96. package/core/wfs-capabilities.d.ts +65 -24
  97. package/core/wfs-capabilities.d.ts.map +1 -1
  98. package/core/wfs-capabilities.js +237 -27
  99. package/core/wfs-filter.d.ts +26 -3
  100. package/core/wfs-filter.d.ts.map +1 -1
  101. package/core/wfs-filter.js +48 -53
  102. package/core/wfs-protocol-error.d.ts +12 -0
  103. package/core/wfs-protocol-error.d.ts.map +1 -0
  104. package/core/wfs-protocol-error.js +13 -0
  105. package/core/wfs.d.ts +53 -1
  106. package/core/wfs.d.ts.map +1 -1
  107. package/core/wfs.js +251 -29
  108. package/core/wms-feature-info.d.ts +92 -0
  109. package/core/wms-feature-info.d.ts.map +1 -0
  110. package/core/wms-feature-info.js +285 -0
  111. package/core/wms.d.ts +9 -0
  112. package/core/wms.d.ts.map +1 -1
  113. package/core/wms.js +17 -5
  114. package/esri-compat/event-bus.d.ts +20 -0
  115. package/esri-compat/event-bus.d.ts.map +1 -1
  116. package/esri-compat/feature-filter.d.ts +46 -1
  117. package/esri-compat/feature-filter.d.ts.map +1 -1
  118. package/esri-compat/feature-filter.js +49 -0
  119. package/esri-compat/geojson-layer.d.ts +4 -2
  120. package/esri-compat/geojson-layer.d.ts.map +1 -1
  121. package/esri-compat/geojson-layer.js +4 -2
  122. package/esri-compat/identity-manager.js +2 -2
  123. package/esri-compat/imagery-layer.d.ts +2 -1
  124. package/esri-compat/imagery-layer.d.ts.map +1 -1
  125. package/esri-compat/imagery-layer.js +2 -1
  126. package/esri-compat/layer-list.d.ts +8 -1
  127. package/esri-compat/layer-list.d.ts.map +1 -1
  128. package/esri-compat/layer-list.js +11 -4
  129. package/esri-compat/legend.d.ts.map +1 -1
  130. package/esri-compat/legend.js +1 -1
  131. package/esri-compat/locator.d.ts +297 -0
  132. package/esri-compat/locator.d.ts.map +1 -0
  133. package/esri-compat/locator.js +579 -0
  134. package/esri-compat/map-image-layer.d.ts.map +1 -1
  135. package/esri-compat/map-view.d.ts.map +1 -1
  136. package/esri-compat/map-view.js +1 -1
  137. package/esri-compat/scene-view.d.ts.map +1 -1
  138. package/esri-compat/time-slider.d.ts +29 -0
  139. package/esri-compat/time-slider.d.ts.map +1 -1
  140. package/esri-compat/time-slider.js +117 -0
  141. package/esri-compat/wfs-layer.d.ts +2 -1
  142. package/esri-compat/wfs-layer.d.ts.map +1 -1
  143. package/esri-compat/wfs-layer.js +2 -1
  144. package/esri-compat/widget-host.d.ts +42 -6
  145. package/esri-compat/widget-host.d.ts.map +1 -1
  146. package/esri-compat/widget-host.js +99 -10
  147. package/generated-app/projection.d.ts.map +1 -1
  148. package/generated-app/runtime.d.ts.map +1 -1
  149. package/map/data-to-map-bridge.d.ts +1 -1
  150. package/map/data-to-map-bridge.d.ts.map +1 -1
  151. package/map/data-to-map-bridge.js +1 -2
  152. package/map/honua-map.d.ts.map +1 -1
  153. package/map/honua-map.js +1 -1
  154. package/map/index.d.ts +2 -0
  155. package/map/index.d.ts.map +1 -1
  156. package/map/index.js +2 -0
  157. package/map/realtime-reconciliation-adapter.d.ts +98 -0
  158. package/map/realtime-reconciliation-adapter.d.ts.map +1 -0
  159. package/map/realtime-reconciliation-adapter.js +84 -0
  160. package/map/temporal-playback.d.ts +25 -0
  161. package/map/temporal-playback.d.ts.map +1 -1
  162. package/map/temporal-playback.js +29 -1
  163. package/operator/workspace/workspace.d.ts.map +1 -1
  164. package/operator/workspace/workspace.js +0 -2
  165. package/package.json +4 -3
  166. package/query-planner/canonical-filter.d.ts +30 -0
  167. package/query-planner/canonical-filter.d.ts.map +1 -0
  168. package/query-planner/canonical-filter.js +30 -0
  169. package/query-planner/columnar-plan.d.ts +63 -0
  170. package/query-planner/columnar-plan.d.ts.map +1 -0
  171. package/query-planner/columnar-plan.js +94 -0
  172. package/query-planner/diagnostics.d.ts +3 -3
  173. package/query-planner/diagnostics.d.ts.map +1 -1
  174. package/query-planner/diagnostics.js +17 -4
  175. package/query-planner/duckdb.d.ts.map +1 -1
  176. package/query-planner/duckdb.js +16 -2
  177. package/query-planner/execution-context.d.ts.map +1 -1
  178. package/query-planner/execution-context.js +3 -1
  179. package/query-planner/executor.d.ts.map +1 -1
  180. package/query-planner/executor.js +7 -1
  181. package/query-planner/geoservices.d.ts.map +1 -1
  182. package/query-planner/geoservices.js +24 -5
  183. package/query-planner/grpc.d.ts.map +1 -1
  184. package/query-planner/grpc.js +29 -5
  185. package/query-planner/index.d.ts +20 -5
  186. package/query-planner/index.d.ts.map +1 -1
  187. package/query-planner/index.js +16 -3
  188. package/query-planner/ir.d.ts +6 -1
  189. package/query-planner/ir.d.ts.map +1 -1
  190. package/query-planner/ir.js +63 -1
  191. package/query-planner/odata-protocol-module.d.ts +54 -0
  192. package/query-planner/odata-protocol-module.d.ts.map +1 -0
  193. package/query-planner/odata-protocol-module.js +199 -0
  194. package/query-planner/odata-v1.d.ts +23 -0
  195. package/query-planner/odata-v1.d.ts.map +1 -0
  196. package/query-planner/odata-v1.js +202 -0
  197. package/query-planner/odata.d.ts +0 -3
  198. package/query-planner/odata.d.ts.map +1 -1
  199. package/query-planner/odata.js +0 -143
  200. package/query-planner/ogc-features.d.ts.map +1 -1
  201. package/query-planner/ogc-features.js +23 -1
  202. package/query-planner/planner.d.ts +2 -2
  203. package/query-planner/planner.d.ts.map +1 -1
  204. package/query-planner/planner.js +146 -27
  205. package/query-planner/representation.d.ts +59 -0
  206. package/query-planner/representation.d.ts.map +1 -0
  207. package/query-planner/representation.js +140 -0
  208. package/query-planner/types.d.ts +153 -3
  209. package/query-planner/types.d.ts.map +1 -1
  210. package/query-planner/wfs-protocol-module.d.ts +57 -0
  211. package/query-planner/wfs-protocol-module.d.ts.map +1 -0
  212. package/query-planner/wfs-protocol-module.js +731 -0
  213. package/query-planner/wfs-v1.d.ts +41 -0
  214. package/query-planner/wfs-v1.d.ts.map +1 -0
  215. package/query-planner/wfs-v1.js +366 -0
  216. package/query-planner/wfs.d.ts +1 -3
  217. package/query-planner/wfs.d.ts.map +1 -1
  218. package/query-planner/wfs.js +1 -70
  219. package/realtime/checkpoint-store.d.ts +206 -0
  220. package/realtime/checkpoint-store.d.ts.map +1 -0
  221. package/realtime/checkpoint-store.js +504 -0
  222. package/realtime/honua-server.d.ts +8 -0
  223. package/realtime/honua-server.d.ts.map +1 -1
  224. package/realtime/honua-server.js +123 -14
  225. package/realtime/index.d.ts +4 -0
  226. package/realtime/index.d.ts.map +1 -1
  227. package/realtime/index.js +2 -0
  228. package/realtime/reconciliation.d.ts +245 -0
  229. package/realtime/reconciliation.d.ts.map +1 -0
  230. package/realtime/reconciliation.js +377 -0
  231. package/realtime/resumable-transport.d.ts.map +1 -1
  232. package/realtime/sse.d.ts.map +1 -1
  233. package/realtime/websocket.d.ts.map +1 -1
  234. package/realtime/wire.d.ts +2 -1
  235. package/realtime/wire.d.ts.map +1 -1
  236. package/realtime/wire.js +24 -8
  237. package/replica-sync/errors.d.ts +19 -1
  238. package/replica-sync/errors.d.ts.map +1 -1
  239. package/replica-sync/errors.js +16 -0
  240. package/replica-sync/geoservices.d.ts +260 -0
  241. package/replica-sync/geoservices.d.ts.map +1 -0
  242. package/replica-sync/geoservices.js +1093 -0
  243. package/replica-sync/index.d.ts +5 -1
  244. package/replica-sync/index.d.ts.map +1 -1
  245. package/replica-sync/index.js +3 -1
  246. package/replica-sync/transport-conformance.d.ts +58 -0
  247. package/replica-sync/transport-conformance.d.ts.map +1 -0
  248. package/replica-sync/transport-conformance.js +265 -0
  249. package/replica-sync/types.d.ts +22 -0
  250. package/replica-sync/types.d.ts.map +1 -1
  251. package/runtime/index.d.ts +6 -2
  252. package/runtime/index.d.ts.map +1 -1
  253. package/runtime/index.js +3 -1
  254. package/runtime/load-package.d.ts +24 -0
  255. package/runtime/load-package.d.ts.map +1 -1
  256. package/runtime/load-package.js +45 -1
  257. package/runtime/maplibre-renderer.d.ts +11 -0
  258. package/runtime/maplibre-renderer.d.ts.map +1 -1
  259. package/runtime/maplibre-renderer.js +32 -7
  260. package/runtime/offline-region-protocol.d.ts +73 -0
  261. package/runtime/offline-region-protocol.d.ts.map +1 -0
  262. package/runtime/offline-region-protocol.js +63 -0
  263. package/runtime/offline-region-style.d.ts +78 -0
  264. package/runtime/offline-region-style.d.ts.map +1 -0
  265. package/runtime/offline-region-style.js +147 -0
  266. package/runtime/pmtiles-protocol.d.ts +7 -8
  267. package/runtime/pmtiles-protocol.d.ts.map +1 -1
  268. package/runtime/pmtiles-protocol.js +19 -66
  269. package/runtime/protocol-registry.d.ts +55 -0
  270. package/runtime/protocol-registry.d.ts.map +1 -0
  271. package/runtime/protocol-registry.js +103 -0
  272. package/runtime/runtime.d.ts +2 -0
  273. package/runtime/runtime.d.ts.map +1 -1
  274. package/runtime/runtime.js +6 -1
  275. package/scene-workspace/analysis-widgets.d.ts +4 -2
  276. package/scene-workspace/analysis-widgets.d.ts.map +1 -1
  277. package/scene-workspace/analysis-widgets.js +4 -2
  278. package/scene-workspace/camera-correspondence.d.ts +151 -0
  279. package/scene-workspace/camera-correspondence.d.ts.map +1 -0
  280. package/scene-workspace/camera-correspondence.js +220 -0
  281. package/scene-workspace/cesium-adapter.d.ts +230 -29
  282. package/scene-workspace/cesium-adapter.d.ts.map +1 -1
  283. package/scene-workspace/cesium-adapter.js +741 -56
  284. package/scene-workspace/cesium-mount.d.ts +205 -0
  285. package/scene-workspace/cesium-mount.d.ts.map +1 -0
  286. package/scene-workspace/cesium-mount.js +392 -0
  287. package/scene-workspace/cesium-scene-owner.d.ts +160 -0
  288. package/scene-workspace/cesium-scene-owner.d.ts.map +1 -0
  289. package/scene-workspace/cesium-scene-owner.js +205 -0
  290. package/scene-workspace/cesium-state-sync.d.ts +101 -0
  291. package/scene-workspace/cesium-state-sync.d.ts.map +1 -0
  292. package/scene-workspace/cesium-state-sync.js +432 -0
  293. package/scene-workspace/cesium-time.d.ts +346 -0
  294. package/scene-workspace/cesium-time.d.ts.map +1 -0
  295. package/scene-workspace/cesium-time.js +403 -0
  296. package/scene-workspace/index.d.ts +42 -10
  297. package/scene-workspace/index.d.ts.map +1 -1
  298. package/scene-workspace/index.js +32 -7
  299. package/scene-workspace/maplibre-state-sync.d.ts +112 -0
  300. package/scene-workspace/maplibre-state-sync.d.ts.map +1 -0
  301. package/scene-workspace/maplibre-state-sync.js +400 -0
  302. package/scene-workspace/primitives.d.ts +237 -3
  303. package/scene-workspace/primitives.d.ts.map +1 -1
  304. package/scene-workspace/primitives.js +1087 -17
  305. package/scene-workspace/scene-discovery.d.ts +5 -2
  306. package/scene-workspace/scene-discovery.d.ts.map +1 -1
  307. package/scene-workspace/scene-discovery.js +5 -2
  308. package/scene-workspace/scene-view.d.ts +5 -2
  309. package/scene-workspace/scene-view.d.ts.map +1 -1
  310. package/scene-workspace/scene-view.js +5 -2
  311. package/scene-workspace/source-to-cesium.d.ts +141 -8
  312. package/scene-workspace/source-to-cesium.d.ts.map +1 -1
  313. package/scene-workspace/source-to-cesium.js +497 -72
  314. package/scene-workspace/state-sync-port-kit.d.ts +156 -0
  315. package/scene-workspace/state-sync-port-kit.d.ts.map +1 -0
  316. package/scene-workspace/state-sync-port-kit.js +319 -0
  317. package/scene-workspace/state-sync.d.ts +34 -1
  318. package/scene-workspace/state-sync.d.ts.map +1 -1
  319. package/scene-workspace/state-sync.js +48 -0
  320. package/scene-workspace/types.d.ts +8 -0
  321. package/scene-workspace/types.d.ts.map +1 -1
  322. package/scene-workspace/types.js +2 -0
  323. package/scene-workspace/workspace.d.ts +12 -1
  324. package/scene-workspace/workspace.d.ts.map +1 -1
  325. package/scene-workspace/workspace.js +13 -0
  326. package/studio/index.d.ts +24 -7
  327. package/studio/index.d.ts.map +1 -1
  328. package/studio/index.js +21 -7
  329. package/studio/lifecycle-client.d.ts +164 -0
  330. package/studio/lifecycle-client.d.ts.map +1 -0
  331. package/studio/lifecycle-client.js +299 -0
  332. package/studio/lifecycle-errors.d.ts +91 -0
  333. package/studio/lifecycle-errors.d.ts.map +1 -0
  334. package/studio/lifecycle-errors.js +77 -0
  335. package/studio/lifecycle-types.d.ts +321 -0
  336. package/studio/lifecycle-types.d.ts.map +1 -0
  337. package/studio/lifecycle-types.js +22 -0
  338. package/web-components/controller.d.ts.map +1 -1
  339. package/web-components/csp-styles.d.ts +6 -0
  340. package/web-components/csp-styles.d.ts.map +1 -0
  341. package/web-components/csp-styles.js +53 -0
  342. package/web-components/elements.d.ts +158 -2
  343. package/web-components/elements.d.ts.map +1 -1
  344. package/web-components/elements.js +1302 -215
  345. package/web-components/export-redaction.d.ts +252 -0
  346. package/web-components/export-redaction.d.ts.map +1 -0
  347. package/web-components/export-redaction.js +588 -0
  348. package/web-components/export.d.ts +417 -0
  349. package/web-components/export.d.ts.map +1 -0
  350. package/web-components/export.js +719 -0
  351. package/web-components/feature-editor-model.d.ts +265 -0
  352. package/web-components/feature-editor-model.d.ts.map +1 -0
  353. package/web-components/feature-editor-model.js +438 -0
  354. package/web-components/feature-editor-sketch.d.ts +125 -0
  355. package/web-components/feature-editor-sketch.d.ts.map +1 -0
  356. package/web-components/feature-editor-sketch.js +194 -0
  357. package/web-components/feature-editor-workflow.d.ts +286 -0
  358. package/web-components/feature-editor-workflow.d.ts.map +1 -0
  359. package/web-components/feature-editor-workflow.js +889 -0
  360. package/web-components/feature-editor.d.ts +83 -0
  361. package/web-components/feature-editor.d.ts.map +1 -0
  362. package/web-components/feature-editor.js +829 -0
  363. package/web-components/feature-table-engine.d.ts +465 -0
  364. package/web-components/feature-table-engine.d.ts.map +1 -0
  365. package/web-components/feature-table-engine.js +1360 -0
  366. package/web-components/feature-table-view.d.ts +95 -0
  367. package/web-components/feature-table-view.d.ts.map +1 -0
  368. package/web-components/feature-table-view.js +343 -0
  369. package/web-components/index.d.ts +34 -3
  370. package/web-components/index.d.ts.map +1 -1
  371. package/web-components/index.js +35 -2
  372. package/web-components/maplibre-renderer.d.ts.map +1 -1
  373. package/web-components/maplibre-renderer.js +13 -4
  374. package/web-components/measurement.d.ts +3 -1
  375. package/web-components/measurement.d.ts.map +1 -1
  376. package/web-components/measurement.js +62 -18
  377. package/web-components/time-slider.d.ts +122 -0
  378. package/web-components/time-slider.d.ts.map +1 -0
  379. package/web-components/time-slider.js +740 -0
  380. package/web-components/types.d.ts +310 -0
  381. package/web-components/types.d.ts.map +1 -1
  382. package/webmap/convert-label.js +0 -1
  383. package/webmap/convert-symbol.d.ts.map +1 -1
@@ -0,0 +1,829 @@
1
+ /**
2
+ * `<honua-feature-editor>` — the production-tier capability-aware editing and
3
+ * sketch widget (issue #680, parent epic #678).
4
+ *
5
+ * The element is pure presentation over
6
+ * {@link HonuaFeatureEditorWorkflow}: it renders the schema/domain/subtype
7
+ * derived form, the geometry workflow, staged attachments, validation,
8
+ * per-operation availability, failure and conflict state, and turns user input
9
+ * back into workflow calls. It imports no protocol adapter, performs no
10
+ * transport of its own, and never reports a rejected edit as a success.
11
+ *
12
+ * ## Wiring
13
+ * ```ts
14
+ * const workflow = createFeatureEditorWorkflow({ source, subtypes });
15
+ * const editor = document.createElement("honua-feature-editor");
16
+ * editor.workflow = workflow; // element subscribes and renders
17
+ * document.body.append(editor);
18
+ * ```
19
+ * Geometry may additionally be driven from the map through a sketch adapter
20
+ * (`createTerraDrawEditorSketch` in `./feature-editor-sketch.js`) — the element
21
+ * itself never needs the map.
22
+ *
23
+ * ## Accessibility (NFR-001)
24
+ * The entire workflow is reachable without touching the map canvas:
25
+ *
26
+ * - operation, sketch-tool, undo/redo, conflict, and submit affordances are
27
+ * native `<button>`s in labelled `role="group"` toolbars, with `aria-pressed`
28
+ * for the active operation/tool and `aria-disabled` + a visible reason
29
+ * whenever an operation is unavailable;
30
+ * - every field is a labelled native control with `aria-required`,
31
+ * `aria-invalid`, and `aria-describedby` pointing at its own error text;
32
+ * - geometry can be entered, replaced, or cleared entirely from the keyboard
33
+ * through the GeoJSON geometry textarea plus **Apply geometry** / **Clear**;
34
+ * - validation problems and service failures render in `role="alert"` regions,
35
+ * the workflow message in a `role="status" aria-live="polite"` region;
36
+ * - `Escape` cancels the draft and `Ctrl`/`Cmd`+`Z` / `Shift`+`Ctrl`/`Cmd`+`Z`
37
+ * drive undo / redo from anywhere inside the panel;
38
+ * - focus and text selection survive re-renders, so typing is never
39
+ * interrupted by an unrelated realtime change.
40
+ *
41
+ * @module
42
+ */
43
+ import { renderCspSafeShadowHtml } from "./csp-styles.js";
44
+ const globalDom = globalThis;
45
+ const HTMLElementBase = globalDom.HTMLElement ?? class {
46
+ };
47
+ const OPERATION_LABELS = {
48
+ create: "New",
49
+ update: "Edit",
50
+ delete: "Delete",
51
+ };
52
+ export class HonuaFeatureEditorElement extends HTMLElementBase {
53
+ static get observedAttributes() {
54
+ return ["label"];
55
+ }
56
+ #workflow;
57
+ #subscription;
58
+ #snapshot;
59
+ #connected = false;
60
+ #geometryDraft;
61
+ #geometryError;
62
+ /**
63
+ * The shadow-root keydown listener. Held as a stored reference because it is
64
+ * bound to the shadow root itself, which survives every `innerHTML`
65
+ * re-render — re-adding it per render would stack one handler per render and
66
+ * fire the shortcut N times per keystroke.
67
+ */
68
+ #keydownListener;
69
+ #messages = {};
70
+ get messages() {
71
+ return this.#messages;
72
+ }
73
+ set messages(messages) {
74
+ this.#messages = messages ?? {};
75
+ this.render();
76
+ }
77
+ /** The workflow this editor renders. Setting it re-subscribes and re-renders. */
78
+ get workflow() {
79
+ return this.#workflow;
80
+ }
81
+ set workflow(workflow) {
82
+ if (this.#workflow === workflow)
83
+ return;
84
+ this.#subscription?.remove();
85
+ this.#subscription = undefined;
86
+ this.#workflow = workflow;
87
+ this.#geometryDraft = undefined;
88
+ this.#geometryError = undefined;
89
+ this.#snapshot = workflow?.snapshot();
90
+ // Only hold a subscription while connected; `connectedCallback` re-takes it
91
+ // so a detached-then-reinserted element resumes updating.
92
+ if (this.#connected)
93
+ this.#subscribe();
94
+ this.render();
95
+ }
96
+ /** Takes the workflow subscription, unless one is already held. */
97
+ #subscribe() {
98
+ const workflow = this.#workflow;
99
+ if (!workflow || this.#subscription)
100
+ return;
101
+ // State may have moved on while detached; re-read before listening.
102
+ this.#snapshot = workflow.snapshot();
103
+ this.#subscription = workflow.subscribe((snapshot) => {
104
+ this.#snapshot = snapshot;
105
+ this.#dispatch("honua-feature-edit-change", { snapshot });
106
+ this.render();
107
+ });
108
+ }
109
+ /** Equivalent to setting `.workflow`. */
110
+ connect(workflow) {
111
+ this.workflow = workflow;
112
+ }
113
+ /**
114
+ * The feature `update` / `delete` drafts open on. Hosts assign this from
115
+ * their own selection state (table row, map click, search result); it is
116
+ * forwarded to the workflow so per-operation availability reflects it
117
+ * immediately. The element never reads the map.
118
+ */
119
+ get selectedFeature() {
120
+ return this.#workflow?.selection();
121
+ }
122
+ set selectedFeature(feature) {
123
+ this.#workflow?.setSelection(feature);
124
+ }
125
+ /** The last snapshot rendered. */
126
+ get snapshot() {
127
+ return this.#snapshot;
128
+ }
129
+ attributeChangedCallback() {
130
+ this.render();
131
+ }
132
+ connectedCallback() {
133
+ this.#connected = true;
134
+ if (!this.shadowRoot && typeof this.attachShadow === "function")
135
+ this.attachShadow({ mode: "open" });
136
+ this.#subscribe();
137
+ this.#armKeyboard();
138
+ this.render();
139
+ }
140
+ disconnectedCallback() {
141
+ this.#connected = false;
142
+ this.#subscription?.remove();
143
+ this.#subscription = undefined;
144
+ this.#releaseKeyboard();
145
+ }
146
+ /**
147
+ * Attaches the panel-wide keyboard shortcuts once per connection.
148
+ *
149
+ * Deliberately not part of `#bind()`: `#bind()` runs on every render and
150
+ * re-attaches listeners to freshly created children (which the `innerHTML`
151
+ * swap discarded), but the shadow root persists, so a listener added there
152
+ * per render would accumulate. The handler reads `this.#workflow` at event
153
+ * time rather than closing over it, so replacing the workflow never leaves a
154
+ * shortcut driving the previous one.
155
+ */
156
+ #armKeyboard() {
157
+ const root = this.shadowRoot;
158
+ if (!root || this.#keydownListener)
159
+ return;
160
+ const listener = (event) => {
161
+ const workflow = this.#workflow;
162
+ if (!workflow)
163
+ return;
164
+ const keyboard = event;
165
+ if (keyboard.key === "Escape") {
166
+ workflow.cancel();
167
+ return;
168
+ }
169
+ if ((keyboard.ctrlKey || keyboard.metaKey) && keyboard.key.toLowerCase() === "z") {
170
+ keyboard.preventDefault();
171
+ if (keyboard.shiftKey)
172
+ workflow.redo();
173
+ else
174
+ workflow.undo();
175
+ }
176
+ };
177
+ root.addEventListener("keydown", listener);
178
+ this.#keydownListener = listener;
179
+ }
180
+ /** Releases the shadow-root keyboard listener. Re-armed by `connectedCallback`. */
181
+ #releaseKeyboard() {
182
+ const listener = this.#keydownListener;
183
+ if (!listener)
184
+ return;
185
+ this.shadowRoot?.removeEventListener("keydown", listener);
186
+ this.#keydownListener = undefined;
187
+ }
188
+ // ── rendering ──────────────────────────────────────────────────────────
189
+ render() {
190
+ const root = this.shadowRoot;
191
+ if (!root || !this.#connected)
192
+ return;
193
+ const focus = captureFocus(root);
194
+ renderCspSafeShadowHtml(root, this.#html());
195
+ this.#bind(root);
196
+ restoreFocus(root, focus);
197
+ }
198
+ #html() {
199
+ const label = this.#messages.label ?? this.#attr("label") ?? "Feature editor";
200
+ const snapshot = this.#snapshot;
201
+ if (!snapshot) {
202
+ return `
203
+ <style>${editorStyles()}</style>
204
+ <section class="panel" part="panel" role="region" aria-label="${escapeAttribute(label)}">
205
+ <div class="bar"><h2>${escapeHtml(label)}</h2></div>
206
+ <p class="empty" role="status">${escapeHtml(this.#messages.noWorkflow ?? "No edit workflow is attached. Set .workflow to an editable source's workflow.")}</p>
207
+ </section>
208
+ `;
209
+ }
210
+ return `
211
+ <style>${editorStyles()}</style>
212
+ <section class="panel" part="panel" role="region" aria-label="${escapeAttribute(label)}">
213
+ <div class="bar">
214
+ <h2>${escapeHtml(label)}</h2>
215
+ <span class="state" data-state>${escapeHtml(this.#messages.status?.[snapshot.status] ?? snapshot.status)}${snapshot.dirty ? ` • ${escapeHtml(this.#messages.unsaved ?? "unsaved")}` : ""}</span>
216
+ </div>
217
+ ${this.#operationsHtml(snapshot, label)}
218
+ ${this.#conflictHtml(snapshot)}
219
+ ${this.#formHtml(snapshot)}
220
+ ${this.#geometryHtml(snapshot, label)}
221
+ ${this.#attachmentsHtml(snapshot)}
222
+ ${this.#problemsHtml(snapshot)}
223
+ <p class="status" part="status" role="status" aria-live="polite" data-status>${escapeHtml(snapshot.message)}</p>
224
+ ${this.#actionsHtml(snapshot)}
225
+ </section>
226
+ `;
227
+ }
228
+ #operationsHtml(snapshot, label) {
229
+ const buttons = snapshot.operations
230
+ .map((availability) => {
231
+ const active = snapshot.operation === availability.operation && snapshot.status !== "idle";
232
+ const disabled = availability.available ? "" : ' disabled aria-disabled="true"';
233
+ const title = availability.reason ? ` title="${escapeAttribute(availability.reason)}"` : "";
234
+ return `<button type="button" part="operation" data-operation="${availability.operation}" aria-pressed="${String(active)}"${disabled}${title}>${escapeHtml(this.#messages.operationLabels?.[availability.operation] ?? OPERATION_LABELS[availability.operation])}</button>`;
235
+ })
236
+ .join("");
237
+ const blocked = snapshot.operations.filter((availability) => !availability.available && availability.reason);
238
+ const reasons = blocked.length === 0
239
+ ? ""
240
+ : `<ul class="reasons" data-operation-reasons>${blocked
241
+ .map((availability) => `<li data-operation-reason="${availability.operation}" data-code="${escapeAttribute(availability.code ?? "")}">${escapeHtml(`${this.#messages.operationLabels?.[availability.operation] ?? OPERATION_LABELS[availability.operation]}: ${availability.reason}`)}</li>`)
242
+ .join("")}</ul>`;
243
+ return `
244
+ <div class="segmented" role="group" aria-label="${escapeAttribute(this.#messages.operationsLabel?.(label) ?? `${label} operations`)}">${buttons}</div>
245
+ ${reasons}
246
+ `;
247
+ }
248
+ #conflictHtml(snapshot) {
249
+ const conflict = snapshot.conflict;
250
+ if (!conflict)
251
+ return "";
252
+ const detail = conflict.versionField && (conflict.mine !== undefined || conflict.theirs !== undefined)
253
+ ? `<p class="muted" data-conflict-detail>${escapeHtml(`${conflict.versionField}: yours ${describeToken(conflict.mine)}, theirs ${describeToken(conflict.theirs)}`)}</p>`
254
+ : "";
255
+ const choices = conflict.choices
256
+ .map((choice) => `<button type="button" part="conflict" data-conflict="${choice}">${escapeHtml(this.#messages.conflictChoices?.[choice] ?? CONFLICT_LABELS[choice])}</button>`)
257
+ .join("");
258
+ return `
259
+ <div class="conflict" part="conflict-panel" role="group" aria-label="${escapeAttribute(this.#messages.conflictLabel ?? "Resolve conflicting change")}" data-conflict-panel>
260
+ <p role="alert" data-conflict-reason>${escapeHtml(conflict.reason)}</p>
261
+ ${detail}
262
+ <div class="actions">${choices}</div>
263
+ </div>
264
+ `;
265
+ }
266
+ #formHtml(snapshot) {
267
+ const form = snapshot.form;
268
+ if (!form)
269
+ return "";
270
+ if (form.controls.length === 0) {
271
+ return `<p class="empty" role="status">${escapeHtml(this.#messages.noFields ?? "This source publishes no editable field metadata.")}</p>`;
272
+ }
273
+ return `
274
+ <fieldset class="fields" part="fields">
275
+ <legend>${escapeHtml(form.subtype?.name ? `Attributes — ${form.subtype.name}` : "Attributes")}</legend>
276
+ ${form.controls.map((control) => this.#controlHtml(control)).join("")}
277
+ </fieldset>
278
+ `;
279
+ }
280
+ #controlHtml(control) {
281
+ const id = `honua-field-${cssSafe(control.name)}`;
282
+ const errorId = `${id}-error`;
283
+ const invalid = control.errors.length > 0;
284
+ const described = invalid ? ` aria-describedby="${errorId}"` : "";
285
+ const common = `id="${id}" data-field="${escapeAttribute(control.name)}"` +
286
+ `${control.required ? ' aria-required="true" required' : ""}` +
287
+ `${invalid ? ' aria-invalid="true"' : ""}` +
288
+ `${control.readOnly ? " disabled" : ""}${described}`;
289
+ const value = stringValue(control.value);
290
+ let field;
291
+ if (control.kind === "select") {
292
+ const options = [
293
+ control.required && value === "" ? '<option value="">Select…</option>' : "",
294
+ ...(control.choices ?? []).map((choice) => `<option value="${escapeAttribute(choice.value)}"${choice.value === value ? " selected" : ""}>${escapeHtml(choice.label)}</option>`),
295
+ ...(control.required ? [] : ['<option value="">(none)</option>']),
296
+ ].join("");
297
+ field = `<select ${common}>${options}</select>`;
298
+ }
299
+ else if (control.kind === "checkbox") {
300
+ field = `<input type="checkbox" ${common}${value === "true" ? " checked" : ""} />`;
301
+ }
302
+ else if (control.kind === "textarea") {
303
+ field = `<textarea ${common}${control.maxLength ? ` maxlength="${control.maxLength}"` : ""} rows="3">${escapeHtml(value)}</textarea>`;
304
+ }
305
+ else {
306
+ const type = control.kind === "number" ? "number" : control.kind === "text" ? "text" : control.kind;
307
+ const bounds = control.kind === "number"
308
+ ? `${control.min !== undefined ? ` min="${control.min}"` : ""}${control.max !== undefined ? ` max="${control.max}"` : ""}`
309
+ : "";
310
+ field = `<input type="${type}" ${common} value="${escapeAttribute(value)}"${control.maxLength ? ` maxlength="${control.maxLength}"` : ""}${bounds} />`;
311
+ }
312
+ const hints = [
313
+ control.readOnly ? "read-only" : "",
314
+ control.required ? "required" : control.nullable ? "" : "not nullable",
315
+ control.subtypeField ? "subtype" : "",
316
+ ].filter(Boolean);
317
+ return `
318
+ <div class="field" part="field">
319
+ <label for="${id}">${escapeHtml(control.label)}${control.required ? '<span aria-hidden="true"> *</span>' : ""}</label>
320
+ ${field}
321
+ ${hints.length > 0 ? `<p class="hint">${escapeHtml(hints.join(" • "))}</p>` : ""}
322
+ ${invalid ? `<p class="error" id="${errorId}" data-error-for="${escapeAttribute(control.name)}">${escapeHtml(control.errors.join(" "))}</p>` : ""}
323
+ </div>
324
+ `;
325
+ }
326
+ #geometryHtml(snapshot, label) {
327
+ const sketch = snapshot.sketch;
328
+ if (!sketch)
329
+ return "";
330
+ const tools = sketch.tools
331
+ .map((capability) => {
332
+ const disabled = capability.state === "supported" ? "" : ' disabled aria-disabled="true"';
333
+ const title = capability.reason ? ` title="${escapeAttribute(capability.reason)}"` : "";
334
+ return `<button type="button" part="sketch-tool" data-sketch-tool="${capability.tool}" aria-pressed="${String(sketch.activeTool === capability.tool)}"${disabled}${title}>${escapeHtml(this.#messages.toolLabels?.[capability.tool] ?? toolLabel(capability.tool))}</button>`;
335
+ })
336
+ .join("");
337
+ const text = this.#geometryDraft ?? (sketch.geometry ? JSON.stringify(sketch.geometry) : "");
338
+ return `
339
+ <fieldset class="geometry" part="geometry">
340
+ <legend>${escapeHtml(this.#messages.geometryLabel ?? "Geometry")}</legend>
341
+ <div class="segmented" role="group" aria-label="${escapeAttribute(this.#messages.sketchToolsLabel?.(label) ?? `${label} sketch tools`)}">${tools}</div>
342
+ <label for="honua-geometry-json">${escapeHtml(this.#messages.geometryJsonLabel ?? "Geometry (GeoJSON)")}</label>
343
+ <textarea id="honua-geometry-json" data-geometry-json rows="3" spellcheck="false" placeholder='{"type":"Point","coordinates":[0,0]}'>${escapeHtml(text)}</textarea>
344
+ <div class="actions">
345
+ <button type="button" data-action="apply-geometry">${escapeHtml(this.#messages.applyGeometry ?? "Apply geometry")}</button>
346
+ <button type="button" data-action="clear-geometry"${sketch.geometry ? "" : " disabled"}>${escapeHtml(this.#messages.clearGeometry ?? "Clear geometry")}</button>
347
+ </div>
348
+ <p class="hint" data-geometry-state>${escapeHtml(geometryStateText(sketch))}</p>
349
+ ${this.#geometryError ? `<p class="error" role="alert" data-geometry-error>${escapeHtml(this.#geometryError)}</p>` : ""}
350
+ </fieldset>
351
+ `;
352
+ }
353
+ #attachmentsHtml(snapshot) {
354
+ if (!snapshot.form)
355
+ return "";
356
+ if (!snapshot.attachmentsSupported) {
357
+ return `
358
+ <fieldset class="attachments" part="attachments">
359
+ <legend>${escapeHtml(this.#messages.attachmentsLabel ?? "Attachments")}</legend>
360
+ <p class="empty" role="status" data-attachments-unsupported>${escapeHtml(this.#messages.attachmentsUnsupported ?? "This source does not support attachments.")}</p>
361
+ </fieldset>
362
+ `;
363
+ }
364
+ const staged = snapshot.attachments.length === 0
365
+ ? `<p class="hint">${escapeHtml(this.#messages.noAttachments ?? "No attachments staged.")}</p>`
366
+ : `<ul class="staged" role="list">${snapshot.attachments
367
+ .map((draft) => `<li role="listitem" data-attachment-index="${draft.index}" data-attachment-status="${draft.status}">${escapeHtml(`${draft.name} — ${draft.status}${draft.error ? `: ${draft.error}` : ""}`)}</li>`)
368
+ .join("")}</ul>`;
369
+ return `
370
+ <fieldset class="attachments" part="attachments">
371
+ <legend>${escapeHtml(this.#messages.attachmentsLabel ?? "Attachments")}</legend>
372
+ <label for="honua-attachment-file">${escapeHtml(this.#messages.addAttachment ?? "Add an attachment")}</label>
373
+ <input type="file" id="honua-attachment-file" data-attachment-input />
374
+ ${staged}
375
+ </fieldset>
376
+ `;
377
+ }
378
+ #problemsHtml(snapshot) {
379
+ const formErrors = snapshot.form?.formErrors ?? [];
380
+ const fieldErrorCount = (snapshot.form?.controls ?? []).filter((control) => control.errors.length > 0).length;
381
+ const validation = formErrors.length === 0 && fieldErrorCount === 0
382
+ ? ""
383
+ : `<div class="problems" role="alert" data-validation>
384
+ <p>${escapeHtml(fieldErrorCount > 0
385
+ ? (this.#messages.validation?.(fieldErrorCount) ??
386
+ `${fieldErrorCount} field${fieldErrorCount === 1 ? "" : "s"} need attention before this edit can be sent.`)
387
+ : (this.#messages.validationDraft ?? "This draft cannot be sent yet."))}</p>
388
+ ${formErrors.length > 0 ? `<ul>${formErrors.map((message) => `<li>${escapeHtml(message)}</li>`).join("")}</ul>` : ""}
389
+ </div>`;
390
+ const failures = snapshot.failures.length === 0
391
+ ? ""
392
+ : `<ul class="problems" role="alert" data-failures>${snapshot.failures
393
+ .map((failure) => `<li data-failure-kind="${failure.kind}">${escapeHtml(`${failure.kind}: ${failure.description}`)}</li>`)
394
+ .join("")}</ul>`;
395
+ return `${validation}${failures}`;
396
+ }
397
+ #actionsHtml(snapshot) {
398
+ const hasDraft = snapshot.form !== undefined;
399
+ const blockedByConflict = snapshot.conflict !== undefined;
400
+ const submitDisabled = !hasDraft || snapshot.busy || blockedByConflict || snapshot.form?.valid === false;
401
+ return `
402
+ <div class="actions" role="group" aria-label="${escapeAttribute(this.#messages.editActionsLabel ?? "Edit actions")}">
403
+ <button type="button" part="submit" data-action="submit"${submitDisabled ? " disabled" : ""}>${escapeHtml(snapshot.operation === "delete"
404
+ ? (this.#messages.deleteFeature ?? "Delete feature")
405
+ : (this.#messages.submit ?? "Submit"))}</button>
406
+ <button type="button" data-action="retry"${snapshot.status === "rejected" && !snapshot.busy ? "" : " disabled"}>${escapeHtml(this.#messages.retry ?? "Retry")}</button>
407
+ <button type="button" data-action="undo"${snapshot.undo.canUndo ? "" : " disabled"}>${escapeHtml(this.#messages.undo ?? "Undo")}</button>
408
+ <button type="button" data-action="redo"${snapshot.undo.canRedo ? "" : " disabled"}>${escapeHtml(this.#messages.redo ?? "Redo")}</button>
409
+ <button type="button" data-action="discard"${hasDraft && snapshot.dirty ? "" : " disabled"}>${escapeHtml(this.#messages.revert ?? "Revert")}</button>
410
+ <button type="button" data-action="cancel"${hasDraft && !snapshot.busy ? "" : " disabled"}>${escapeHtml(this.#messages.cancel ?? "Cancel")}</button>
411
+ </div>
412
+ `;
413
+ }
414
+ // ── event binding ──────────────────────────────────────────────────────
415
+ #bind(root) {
416
+ const workflow = this.#workflow;
417
+ if (!workflow)
418
+ return;
419
+ root.querySelectorAll("button[data-operation]").forEach((button) => {
420
+ button.addEventListener("click", () => {
421
+ const operation = button.dataset.operation;
422
+ if (operation)
423
+ this.#dispatchOperation(operation);
424
+ });
425
+ });
426
+ root.querySelectorAll("button[data-conflict]").forEach((button) => {
427
+ button.addEventListener("click", () => {
428
+ const choice = button.dataset.conflict;
429
+ if (choice)
430
+ workflow.resolveConflict(choice);
431
+ });
432
+ });
433
+ root.querySelectorAll("button[data-sketch-tool]").forEach((button) => {
434
+ button.addEventListener("click", () => {
435
+ const tool = button.dataset.sketchTool;
436
+ if (tool)
437
+ workflow.startSketch(tool);
438
+ });
439
+ });
440
+ root.querySelectorAll("[data-field]").forEach((input) => {
441
+ const name = input.dataset.field;
442
+ if (!name)
443
+ return;
444
+ input.addEventListener("change", () => {
445
+ workflow.setValue(name, controlValue(input));
446
+ });
447
+ });
448
+ root.querySelectorAll("button[data-action]").forEach((button) => {
449
+ button.addEventListener("click", () => {
450
+ void this.#runAction(button.dataset.action ?? "");
451
+ });
452
+ });
453
+ root.querySelector("[data-attachment-input]")?.addEventListener("change", (event) => {
454
+ const input = event.currentTarget;
455
+ const file = input?.files?.[0];
456
+ if (file)
457
+ workflow.stageAttachment(file, { name: file.name, contentType: file.type || undefined });
458
+ if (input)
459
+ input.value = "";
460
+ });
461
+ const geometry = root.querySelector("[data-geometry-json]");
462
+ geometry?.addEventListener("input", () => {
463
+ this.#geometryDraft = geometry.value;
464
+ });
465
+ // No shadow-root-level listener here: those survive the `innerHTML` swap
466
+ // and would stack per render. See `#armKeyboard()`.
467
+ }
468
+ #dispatchOperation(operation) {
469
+ const workflow = this.#workflow;
470
+ if (!workflow)
471
+ return;
472
+ this.#geometryDraft = undefined;
473
+ this.#geometryError = undefined;
474
+ // `update` / `delete` open on the workflow's recorded selection, which the
475
+ // host feeds through `.selectedFeature` — the element never reaches into
476
+ // the map itself.
477
+ workflow.begin(operation);
478
+ }
479
+ async #runAction(action) {
480
+ const workflow = this.#workflow;
481
+ if (!workflow)
482
+ return;
483
+ if (action === "submit" || action === "retry") {
484
+ const commit = action === "retry" ? await workflow.retry() : await workflow.submit();
485
+ this.#dispatch("honua-feature-edit-commit", { commit });
486
+ return;
487
+ }
488
+ if (action === "undo") {
489
+ workflow.undo();
490
+ this.#geometryDraft = undefined;
491
+ return;
492
+ }
493
+ if (action === "redo") {
494
+ workflow.redo();
495
+ this.#geometryDraft = undefined;
496
+ return;
497
+ }
498
+ if (action === "discard") {
499
+ workflow.discardChanges();
500
+ this.#geometryDraft = undefined;
501
+ return;
502
+ }
503
+ if (action === "cancel") {
504
+ workflow.cancel();
505
+ this.#geometryDraft = undefined;
506
+ return;
507
+ }
508
+ if (action === "clear-geometry") {
509
+ this.#geometryDraft = undefined;
510
+ this.#geometryError = undefined;
511
+ workflow.setGeometry(this.#activeTool(), null);
512
+ return;
513
+ }
514
+ if (action === "apply-geometry")
515
+ this.#applyGeometryFromText();
516
+ }
517
+ #applyGeometryFromText() {
518
+ const workflow = this.#workflow;
519
+ if (!workflow)
520
+ return;
521
+ const text = (this.#geometryDraft ?? "").trim();
522
+ if (text === "") {
523
+ this.#geometryError = "Enter a GeoJSON geometry, or use Clear geometry.";
524
+ this.render();
525
+ return;
526
+ }
527
+ let parsed;
528
+ try {
529
+ parsed = JSON.parse(text);
530
+ }
531
+ catch {
532
+ this.#geometryError = "That is not valid JSON.";
533
+ this.render();
534
+ return;
535
+ }
536
+ if (!isEditableGeometry(parsed)) {
537
+ this.#geometryError =
538
+ 'A geometry needs a GeoJSON "type" (for example {"type":"Point","coordinates":[0,0]}) or an Esri x/y, rings, or paths shape.';
539
+ this.render();
540
+ return;
541
+ }
542
+ this.#geometryError = undefined;
543
+ this.#geometryDraft = undefined;
544
+ workflow.setGeometry(this.#activeTool(parsed), parsed);
545
+ }
546
+ #activeTool(geometry) {
547
+ const snapshot = this.#snapshot;
548
+ const active = snapshot?.sketch?.activeTool;
549
+ if (active)
550
+ return active;
551
+ const inferred = geometry ? inferSketchTool(geometry) : undefined;
552
+ if (inferred)
553
+ return inferred;
554
+ const supported = snapshot?.sketch?.tools.find((capability) => capability.state === "supported");
555
+ return supported?.tool ?? "point";
556
+ }
557
+ #dispatch(name, detail) {
558
+ if (!globalDom.CustomEvent || typeof this.dispatchEvent !== "function")
559
+ return;
560
+ this.dispatchEvent(new globalDom.CustomEvent(name, { bubbles: true, composed: true, detail }));
561
+ }
562
+ #attr(name) {
563
+ return typeof this.getAttribute === "function" ? this.getAttribute(name) : null;
564
+ }
565
+ }
566
+ /** Registers `<honua-feature-editor>`; skipped when the tag is already defined. */
567
+ export function defineHonuaFeatureEditor(registry = globalDom.customElements) {
568
+ if (!registry)
569
+ return;
570
+ if (!registry.get("honua-feature-editor")) {
571
+ registry.define("honua-feature-editor", HonuaFeatureEditorElement);
572
+ }
573
+ }
574
+ // ── helpers ──────────────────────────────────────────────────────────────
575
+ const CONFLICT_LABELS = {
576
+ "keep-mine": "Keep my changes",
577
+ "discard-mine": "Discard my changes",
578
+ reload: "Reload the feature",
579
+ };
580
+ function toolLabel(tool) {
581
+ return tool.charAt(0).toUpperCase() + tool.slice(1);
582
+ }
583
+ /**
584
+ * Whether a parsed geometry may be handed back to the workflow.
585
+ *
586
+ * The contract carries protocol-shaped geometry (`Record<string, unknown>`), so
587
+ * a GeoServices source round-trips Esri geometry, not GeoJSON. Requiring a
588
+ * GeoJSON `type` here would make the element render a geometry in the textarea
589
+ * that **Apply geometry** then refuses — which also strands the keyboard-only
590
+ * geometry path (NFR-001) on every Esri-backed source.
591
+ */
592
+ function isEditableGeometry(value) {
593
+ if (typeof value !== "object" || value === null || Array.isArray(value))
594
+ return false;
595
+ const candidate = value;
596
+ if (typeof candidate.type === "string")
597
+ return true;
598
+ if (typeof candidate.x === "number" && typeof candidate.y === "number")
599
+ return true;
600
+ return Array.isArray(candidate.rings) || Array.isArray(candidate.paths) || Array.isArray(candidate.points);
601
+ }
602
+ /** Best-effort tool for a geometry, across GeoJSON and Esri shapes. */
603
+ function inferSketchTool(geometry) {
604
+ const type = typeof geometry.type === "string" ? geometry.type.toLowerCase() : "";
605
+ if (type.includes("polygon"))
606
+ return "polygon";
607
+ if (type.includes("line"))
608
+ return "line";
609
+ if (type.includes("point"))
610
+ return "point";
611
+ if (Array.isArray(geometry.rings))
612
+ return "polygon";
613
+ if (Array.isArray(geometry.paths))
614
+ return "line";
615
+ if (Array.isArray(geometry.points) || (typeof geometry.x === "number" && typeof geometry.y === "number")) {
616
+ return "point";
617
+ }
618
+ return undefined;
619
+ }
620
+ function geometryStateText(sketch) {
621
+ const snapping = sketch.snapping.enabled ? `snapping on (${sketch.snapping.tolerance}px)` : "snapping off";
622
+ if (sketch.status === "sketching")
623
+ return `Sketching with ${sketch.activeTool ?? "a tool"} — ${snapping}`;
624
+ if (sketch.geometry === null)
625
+ return `Geometry cleared — ${snapping}`;
626
+ if (sketch.geometry === undefined)
627
+ return `No geometry yet — ${snapping}`;
628
+ return `Geometry ready — ${snapping}`;
629
+ }
630
+ function describeToken(token) {
631
+ if (token === undefined)
632
+ return "unknown";
633
+ if (token === null)
634
+ return "none";
635
+ return String(token);
636
+ }
637
+ function stringValue(value) {
638
+ if (value === undefined || value === null)
639
+ return "";
640
+ if (typeof value === "boolean")
641
+ return String(value);
642
+ if (typeof value === "object")
643
+ return JSON.stringify(value);
644
+ return String(value);
645
+ }
646
+ function controlValue(input) {
647
+ const candidate = input;
648
+ if (candidate.type === "checkbox")
649
+ return candidate.checked === true;
650
+ return candidate.value ?? null;
651
+ }
652
+ function captureFocus(root) {
653
+ const active = root.activeElement;
654
+ if (!active?.id)
655
+ return undefined;
656
+ return {
657
+ id: active.id,
658
+ ...(typeof active.selectionStart === "number" ? { selectionStart: active.selectionStart } : {}),
659
+ ...(typeof active.selectionEnd === "number" ? { selectionEnd: active.selectionEnd } : {}),
660
+ };
661
+ }
662
+ function restoreFocus(root, memento) {
663
+ if (!memento)
664
+ return;
665
+ const next = root.querySelector(`#${cssEscape(memento.id)}`);
666
+ if (!next || typeof next.focus !== "function")
667
+ return;
668
+ next.focus();
669
+ if (memento.selectionStart !== undefined && typeof next.setSelectionRange === "function") {
670
+ try {
671
+ next.setSelectionRange(memento.selectionStart, memento.selectionEnd ?? memento.selectionStart);
672
+ }
673
+ catch {
674
+ // Controls without a text selection model (checkbox, select) throw here.
675
+ }
676
+ }
677
+ }
678
+ /** Field names become element ids; keep them selector-safe without a regex backtrack. */
679
+ function cssSafe(value) {
680
+ let out = "";
681
+ for (const char of value) {
682
+ out += /[A-Za-z0-9_-]/.test(char) ? char : "_";
683
+ }
684
+ return out;
685
+ }
686
+ function cssEscape(value) {
687
+ return cssSafe(value);
688
+ }
689
+ function escapeHtml(value) {
690
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
691
+ }
692
+ function escapeAttribute(value) {
693
+ return escapeHtml(value).replace(/'/g, "&#39;");
694
+ }
695
+ /** Structural styles, themable through the shared `--honua-ui-*` properties. */
696
+ function editorStyles() {
697
+ return `
698
+ :host {
699
+ --honua-ui-bg: #ffffff;
700
+ --honua-ui-fg: #172033;
701
+ --honua-ui-muted: #667085;
702
+ --honua-ui-border: #d0d5dd;
703
+ --honua-ui-accent: #1d4ed8;
704
+ --honua-ui-accent-fg: #ffffff;
705
+ --honua-ui-danger: #b42318;
706
+ box-sizing: border-box;
707
+ color: var(--honua-ui-fg);
708
+ display: block;
709
+ font: 14px/1.4 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
710
+ }
711
+ *, *::before, *::after { box-sizing: inherit; }
712
+ .panel {
713
+ background: var(--honua-ui-bg);
714
+ border: 1px solid var(--honua-ui-border);
715
+ border-radius: 8px;
716
+ display: grid;
717
+ gap: 10px;
718
+ min-width: 240px;
719
+ padding: 12px;
720
+ }
721
+ .bar { align-items: center; display: flex; gap: 8px; justify-content: space-between; }
722
+ h2 { font-size: 14px; margin: 0; }
723
+ .state { color: var(--honua-ui-muted); font-size: 12px; }
724
+ button {
725
+ background: var(--honua-ui-bg);
726
+ border: 1px solid var(--honua-ui-border);
727
+ border-radius: 6px;
728
+ color: inherit;
729
+ cursor: pointer;
730
+ font: inherit;
731
+ min-height: 32px;
732
+ padding: 0 10px;
733
+ }
734
+ button:disabled { cursor: not-allowed; opacity: 0.55; }
735
+ button[aria-pressed="true"] {
736
+ background: var(--honua-ui-accent);
737
+ border-color: var(--honua-ui-accent);
738
+ color: var(--honua-ui-accent-fg);
739
+ }
740
+ .segmented { display: flex; flex-wrap: wrap; gap: 6px; }
741
+ .actions { display: flex; flex-wrap: wrap; gap: 6px; }
742
+ fieldset {
743
+ border: 1px solid var(--honua-ui-border);
744
+ border-radius: 6px;
745
+ display: grid;
746
+ gap: 8px;
747
+ margin: 0;
748
+ padding: 8px 10px 10px;
749
+ }
750
+ legend { color: var(--honua-ui-muted); font-size: 12px; padding: 0 4px; }
751
+ label { display: block; font-size: 12px; font-weight: 600; }
752
+ input, select, textarea {
753
+ border: 1px solid var(--honua-ui-border);
754
+ border-radius: 6px;
755
+ color: inherit;
756
+ font: inherit;
757
+ min-height: 32px;
758
+ padding: 4px 8px;
759
+ width: 100%;
760
+ }
761
+ input[type="checkbox"] { min-height: 0; width: auto; }
762
+ textarea { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; resize: vertical; }
763
+ [aria-invalid="true"] { border-color: var(--honua-ui-danger); }
764
+ .field { display: grid; gap: 4px; }
765
+ .hint { color: var(--honua-ui-muted); font-size: 12px; margin: 0; }
766
+ .empty { color: var(--honua-ui-muted); margin: 0; }
767
+ .error { color: var(--honua-ui-danger); font-size: 12px; margin: 0; }
768
+ .status { margin: 0; }
769
+ .reasons, .staged { display: grid; gap: 4px; font-size: 12px; list-style: none; margin: 0; padding: 0; }
770
+ .reasons li { color: var(--honua-ui-muted); }
771
+ .problems {
772
+ background: rgba(180, 35, 24, 0.06);
773
+ border: 1px solid var(--honua-ui-danger);
774
+ border-radius: 6px;
775
+ color: var(--honua-ui-danger);
776
+ display: grid;
777
+ gap: 4px;
778
+ list-style: none;
779
+ margin: 0;
780
+ padding: 8px 10px;
781
+ }
782
+ .problems p, .problems ul { margin: 0; }
783
+ .conflict {
784
+ border: 1px solid var(--honua-ui-danger);
785
+ border-radius: 6px;
786
+ display: grid;
787
+ gap: 6px;
788
+ padding: 8px 10px;
789
+ }
790
+ .conflict p { margin: 0; }
791
+ @media (max-width: 320px) {
792
+ .panel { min-width: 0; }
793
+ .bar { align-items: flex-start; flex-direction: column; }
794
+ .segmented, .actions { width: 100%; }
795
+ .segmented button, .actions button { flex: 1 1 120px; min-width: 0; }
796
+ }
797
+ @media (forced-colors: active), (prefers-contrast: more) {
798
+ :host {
799
+ forced-color-adjust: auto;
800
+ }
801
+ button[aria-pressed="true"] {
802
+ background: Highlight;
803
+ border-color: Highlight;
804
+ color: HighlightText;
805
+ outline: 2px solid Highlight;
806
+ outline-offset: 1px;
807
+ }
808
+ button:disabled {
809
+ border-style: dashed;
810
+ color: GrayText;
811
+ opacity: 1;
812
+ }
813
+ input, select, textarea {
814
+ background: Canvas;
815
+ border-color: CanvasText;
816
+ color: CanvasText;
817
+ }
818
+ [aria-invalid="true"] {
819
+ border-color: Mark;
820
+ outline: 1px solid Mark;
821
+ }
822
+ .problems {
823
+ background: Canvas;
824
+ border-color: Mark;
825
+ color: Mark;
826
+ }
827
+ }
828
+ `;
829
+ }