@honua/app-platform 0.1.1-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.
- package/collaboration/client.d.ts.map +1 -1
- package/connect-url-safety.d.ts +53 -0
- package/connect-url-safety.js +189 -0
- package/contract/discovery.d.ts +34 -0
- package/contract/discovery.d.ts.map +1 -1
- package/contract/index.d.ts +7 -5
- package/contract/index.d.ts.map +1 -1
- package/contract/index.js +3 -2
- package/contract/pmtiles.d.ts +10 -1
- package/contract/pmtiles.d.ts.map +1 -1
- package/contract/pmtiles.js +10 -2
- package/contract/protocol-module.d.ts +84 -9
- package/contract/protocol-module.d.ts.map +1 -1
- package/contract/query-filter-fes.d.ts +18 -0
- package/contract/query-filter-fes.d.ts.map +1 -0
- package/contract/query-filter-fes.js +122 -0
- package/contract/query-filter.d.ts +300 -0
- package/contract/query-filter.d.ts.map +1 -0
- package/contract/query-filter.js +883 -0
- package/contract/schema.d.ts +12 -0
- package/contract/schema.d.ts.map +1 -1
- package/contract/schema.js +34 -0
- package/contract/source-capability-support.d.ts.map +1 -1
- package/contract/source-capability-support.js +15 -3
- package/contract/source.d.ts +45 -25
- package/contract/source.d.ts.map +1 -1
- package/contract/source.js +719 -479
- package/contract/types.d.ts +87 -18
- package/contract/types.d.ts.map +1 -1
- package/contract/widget-source.d.ts.map +1 -1
- package/controls/basemap-switcher.d.ts +19 -11
- package/controls/basemap-switcher.d.ts.map +1 -1
- package/controls/basemap-switcher.js +58 -25
- package/controls/catalog.d.ts +105 -0
- package/controls/catalog.d.ts.map +1 -0
- package/controls/catalog.js +341 -0
- package/controls/element-utils.d.ts +9 -0
- package/controls/element-utils.d.ts.map +1 -1
- package/controls/element-utils.js +17 -0
- package/controls/index.d.ts +29 -8
- package/controls/index.d.ts.map +1 -1
- package/controls/index.js +26 -8
- package/controls/layer-list.d.ts +13 -5
- package/controls/layer-list.d.ts.map +1 -1
- package/controls/layer-list.js +72 -15
- package/controls/legend.d.ts +32 -5
- package/controls/legend.d.ts.map +1 -1
- package/controls/legend.js +95 -25
- package/controls/qualification.d.ts +152 -0
- package/controls/qualification.d.ts.map +1 -0
- package/controls/qualification.js +1265 -0
- package/controls/registry.d.ts +104 -0
- package/controls/registry.d.ts.map +1 -0
- package/controls/registry.js +150 -0
- package/controls/swipe-control.d.ts +6 -1
- package/controls/swipe-control.d.ts.map +1 -1
- package/controls/swipe-control.js +82 -20
- package/core/client.d.ts +45 -21
- package/core/client.d.ts.map +1 -1
- package/core/client.js +224 -46
- package/core/error-classifications.d.ts +6 -0
- package/core/error-classifications.d.ts.map +1 -1
- package/core/error-classifications.js +6 -0
- package/core/error-code-registry.d.ts +6 -0
- package/core/error-code-registry.d.ts.map +1 -1
- package/core/error-code-registry.js +6 -0
- package/core/error-envelope.js +2 -0
- package/core/errors.d.ts +1 -1
- package/core/errors.js +1 -1
- package/core/geoparquet-sql.d.ts +68 -4
- package/core/geoparquet-sql.d.ts.map +1 -1
- package/core/geoparquet-sql.js +325 -8
- package/core/odata-write-codec.d.ts.map +1 -1
- package/core/odata-write-codec.js +0 -1
- package/core/odata.d.ts +6 -0
- package/core/odata.d.ts.map +1 -1
- package/core/odata.js +6 -1
- package/core/ogc-processes.d.ts +182 -25
- package/core/ogc-processes.d.ts.map +1 -1
- package/core/ogc-processes.js +602 -30
- package/core/process-runner.d.ts +8 -1
- package/core/process-runner.d.ts.map +1 -1
- package/core/process-runner.js +1 -0
- package/core/protocol-transport.d.ts +2 -0
- package/core/protocol-transport.d.ts.map +1 -1
- package/core/stac.d.ts +31 -0
- package/core/stac.d.ts.map +1 -1
- package/core/stac.js +136 -19
- package/core/surfaces.d.ts.map +1 -1
- package/core/surfaces.js +273 -68
- package/core/types.d.ts +123 -9
- package/core/types.d.ts.map +1 -1
- package/core/wfs-axis-order.d.ts +18 -0
- package/core/wfs-axis-order.d.ts.map +1 -0
- package/core/wfs-axis-order.js +108 -0
- package/core/wfs-capabilities.d.ts +65 -24
- package/core/wfs-capabilities.d.ts.map +1 -1
- package/core/wfs-capabilities.js +237 -27
- package/core/wfs-filter.d.ts +26 -3
- package/core/wfs-filter.d.ts.map +1 -1
- package/core/wfs-filter.js +48 -53
- package/core/wfs-protocol-error.d.ts +12 -0
- package/core/wfs-protocol-error.d.ts.map +1 -0
- package/core/wfs-protocol-error.js +13 -0
- package/core/wfs.d.ts +53 -1
- package/core/wfs.d.ts.map +1 -1
- package/core/wfs.js +251 -29
- package/core/wms-feature-info.d.ts +92 -0
- package/core/wms-feature-info.d.ts.map +1 -0
- package/core/wms-feature-info.js +285 -0
- package/core/wms.d.ts +9 -0
- package/core/wms.d.ts.map +1 -1
- package/core/wms.js +17 -5
- package/esri-compat/event-bus.d.ts +20 -0
- package/esri-compat/event-bus.d.ts.map +1 -1
- package/esri-compat/feature-filter.d.ts +46 -1
- package/esri-compat/feature-filter.d.ts.map +1 -1
- package/esri-compat/feature-filter.js +49 -0
- package/esri-compat/geojson-layer.d.ts +4 -2
- package/esri-compat/geojson-layer.d.ts.map +1 -1
- package/esri-compat/geojson-layer.js +4 -2
- package/esri-compat/identity-manager.js +2 -2
- package/esri-compat/imagery-layer.d.ts +2 -1
- package/esri-compat/imagery-layer.d.ts.map +1 -1
- package/esri-compat/imagery-layer.js +2 -1
- package/esri-compat/layer-list.d.ts +8 -1
- package/esri-compat/layer-list.d.ts.map +1 -1
- package/esri-compat/layer-list.js +11 -4
- package/esri-compat/legend.d.ts.map +1 -1
- package/esri-compat/legend.js +1 -1
- package/esri-compat/locator.d.ts +297 -0
- package/esri-compat/locator.d.ts.map +1 -0
- package/esri-compat/locator.js +579 -0
- package/esri-compat/map-image-layer.d.ts.map +1 -1
- package/esri-compat/map-view.d.ts.map +1 -1
- package/esri-compat/map-view.js +1 -1
- package/esri-compat/scene-view.d.ts.map +1 -1
- package/esri-compat/time-slider.d.ts +29 -0
- package/esri-compat/time-slider.d.ts.map +1 -1
- package/esri-compat/time-slider.js +117 -0
- package/esri-compat/wfs-layer.d.ts +2 -1
- package/esri-compat/wfs-layer.d.ts.map +1 -1
- package/esri-compat/wfs-layer.js +2 -1
- package/esri-compat/widget-host.d.ts +42 -6
- package/esri-compat/widget-host.d.ts.map +1 -1
- package/esri-compat/widget-host.js +99 -10
- package/generated-app/projection.d.ts.map +1 -1
- package/generated-app/runtime.d.ts.map +1 -1
- package/map/data-to-map-bridge.d.ts +1 -1
- package/map/data-to-map-bridge.d.ts.map +1 -1
- package/map/data-to-map-bridge.js +1 -2
- package/map/honua-map.d.ts.map +1 -1
- package/map/honua-map.js +1 -1
- package/map/index.d.ts +2 -0
- package/map/index.d.ts.map +1 -1
- package/map/index.js +2 -0
- package/map/realtime-reconciliation-adapter.d.ts +98 -0
- package/map/realtime-reconciliation-adapter.d.ts.map +1 -0
- package/map/realtime-reconciliation-adapter.js +84 -0
- package/map/temporal-playback.d.ts +25 -0
- package/map/temporal-playback.d.ts.map +1 -1
- package/map/temporal-playback.js +29 -1
- package/operator/workspace/workspace.d.ts.map +1 -1
- package/operator/workspace/workspace.js +0 -2
- package/package.json +4 -3
- package/query-planner/canonical-filter.d.ts +30 -0
- package/query-planner/canonical-filter.d.ts.map +1 -0
- package/query-planner/canonical-filter.js +30 -0
- package/query-planner/columnar-plan.d.ts +63 -0
- package/query-planner/columnar-plan.d.ts.map +1 -0
- package/query-planner/columnar-plan.js +94 -0
- package/query-planner/diagnostics.d.ts +3 -3
- package/query-planner/diagnostics.d.ts.map +1 -1
- package/query-planner/diagnostics.js +17 -4
- package/query-planner/duckdb.d.ts.map +1 -1
- package/query-planner/duckdb.js +16 -2
- package/query-planner/execution-context.d.ts.map +1 -1
- package/query-planner/execution-context.js +3 -1
- package/query-planner/executor.d.ts.map +1 -1
- package/query-planner/executor.js +7 -1
- package/query-planner/geoservices.d.ts.map +1 -1
- package/query-planner/geoservices.js +24 -5
- package/query-planner/grpc.d.ts.map +1 -1
- package/query-planner/grpc.js +29 -5
- package/query-planner/index.d.ts +20 -5
- package/query-planner/index.d.ts.map +1 -1
- package/query-planner/index.js +16 -3
- package/query-planner/ir.d.ts +6 -1
- package/query-planner/ir.d.ts.map +1 -1
- package/query-planner/ir.js +63 -1
- package/query-planner/odata-protocol-module.d.ts +54 -0
- package/query-planner/odata-protocol-module.d.ts.map +1 -0
- package/query-planner/odata-protocol-module.js +199 -0
- package/query-planner/odata-v1.d.ts +23 -0
- package/query-planner/odata-v1.d.ts.map +1 -0
- package/query-planner/odata-v1.js +202 -0
- package/query-planner/odata.d.ts +0 -3
- package/query-planner/odata.d.ts.map +1 -1
- package/query-planner/odata.js +0 -143
- package/query-planner/ogc-features.d.ts.map +1 -1
- package/query-planner/ogc-features.js +23 -1
- package/query-planner/planner.d.ts +2 -2
- package/query-planner/planner.d.ts.map +1 -1
- package/query-planner/planner.js +146 -27
- package/query-planner/representation.d.ts +59 -0
- package/query-planner/representation.d.ts.map +1 -0
- package/query-planner/representation.js +140 -0
- package/query-planner/types.d.ts +153 -3
- package/query-planner/types.d.ts.map +1 -1
- package/query-planner/wfs-protocol-module.d.ts +57 -0
- package/query-planner/wfs-protocol-module.d.ts.map +1 -0
- package/query-planner/wfs-protocol-module.js +731 -0
- package/query-planner/wfs-v1.d.ts +41 -0
- package/query-planner/wfs-v1.d.ts.map +1 -0
- package/query-planner/wfs-v1.js +366 -0
- package/query-planner/wfs.d.ts +1 -3
- package/query-planner/wfs.d.ts.map +1 -1
- package/query-planner/wfs.js +1 -70
- package/realtime/checkpoint-store.d.ts +206 -0
- package/realtime/checkpoint-store.d.ts.map +1 -0
- package/realtime/checkpoint-store.js +504 -0
- package/realtime/honua-server.d.ts +8 -0
- package/realtime/honua-server.d.ts.map +1 -1
- package/realtime/honua-server.js +123 -14
- package/realtime/index.d.ts +4 -0
- package/realtime/index.d.ts.map +1 -1
- package/realtime/index.js +2 -0
- package/realtime/reconciliation.d.ts +245 -0
- package/realtime/reconciliation.d.ts.map +1 -0
- package/realtime/reconciliation.js +377 -0
- package/realtime/resumable-transport.d.ts.map +1 -1
- package/realtime/sse.d.ts.map +1 -1
- package/realtime/websocket.d.ts.map +1 -1
- package/realtime/wire.d.ts +2 -1
- package/realtime/wire.d.ts.map +1 -1
- package/realtime/wire.js +24 -8
- package/replica-sync/errors.d.ts +19 -1
- package/replica-sync/errors.d.ts.map +1 -1
- package/replica-sync/errors.js +16 -0
- package/replica-sync/geoservices.d.ts +260 -0
- package/replica-sync/geoservices.d.ts.map +1 -0
- package/replica-sync/geoservices.js +1093 -0
- package/replica-sync/index.d.ts +5 -1
- package/replica-sync/index.d.ts.map +1 -1
- package/replica-sync/index.js +3 -1
- package/replica-sync/transport-conformance.d.ts +58 -0
- package/replica-sync/transport-conformance.d.ts.map +1 -0
- package/replica-sync/transport-conformance.js +265 -0
- package/replica-sync/types.d.ts +22 -0
- package/replica-sync/types.d.ts.map +1 -1
- package/runtime/index.d.ts +6 -2
- package/runtime/index.d.ts.map +1 -1
- package/runtime/index.js +3 -1
- package/runtime/load-package.d.ts +24 -0
- package/runtime/load-package.d.ts.map +1 -1
- package/runtime/load-package.js +45 -1
- package/runtime/maplibre-renderer.d.ts +11 -0
- package/runtime/maplibre-renderer.d.ts.map +1 -1
- package/runtime/maplibre-renderer.js +32 -7
- package/runtime/offline-region-protocol.d.ts +73 -0
- package/runtime/offline-region-protocol.d.ts.map +1 -0
- package/runtime/offline-region-protocol.js +63 -0
- package/runtime/offline-region-style.d.ts +78 -0
- package/runtime/offline-region-style.d.ts.map +1 -0
- package/runtime/offline-region-style.js +147 -0
- package/runtime/pmtiles-protocol.d.ts +7 -8
- package/runtime/pmtiles-protocol.d.ts.map +1 -1
- package/runtime/pmtiles-protocol.js +19 -66
- package/runtime/protocol-registry.d.ts +55 -0
- package/runtime/protocol-registry.d.ts.map +1 -0
- package/runtime/protocol-registry.js +103 -0
- package/runtime/runtime.d.ts +2 -0
- package/runtime/runtime.d.ts.map +1 -1
- package/runtime/runtime.js +6 -1
- package/scene-workspace/analysis-widgets.d.ts +4 -2
- package/scene-workspace/analysis-widgets.d.ts.map +1 -1
- package/scene-workspace/analysis-widgets.js +4 -2
- package/scene-workspace/camera-correspondence.d.ts +151 -0
- package/scene-workspace/camera-correspondence.d.ts.map +1 -0
- package/scene-workspace/camera-correspondence.js +220 -0
- package/scene-workspace/cesium-adapter.d.ts +230 -29
- package/scene-workspace/cesium-adapter.d.ts.map +1 -1
- package/scene-workspace/cesium-adapter.js +741 -56
- package/scene-workspace/cesium-mount.d.ts +205 -0
- package/scene-workspace/cesium-mount.d.ts.map +1 -0
- package/scene-workspace/cesium-mount.js +392 -0
- package/scene-workspace/cesium-scene-owner.d.ts +160 -0
- package/scene-workspace/cesium-scene-owner.d.ts.map +1 -0
- package/scene-workspace/cesium-scene-owner.js +205 -0
- package/scene-workspace/cesium-state-sync.d.ts +101 -0
- package/scene-workspace/cesium-state-sync.d.ts.map +1 -0
- package/scene-workspace/cesium-state-sync.js +432 -0
- package/scene-workspace/cesium-time.d.ts +346 -0
- package/scene-workspace/cesium-time.d.ts.map +1 -0
- package/scene-workspace/cesium-time.js +403 -0
- package/scene-workspace/index.d.ts +42 -10
- package/scene-workspace/index.d.ts.map +1 -1
- package/scene-workspace/index.js +32 -7
- package/scene-workspace/maplibre-state-sync.d.ts +112 -0
- package/scene-workspace/maplibre-state-sync.d.ts.map +1 -0
- package/scene-workspace/maplibre-state-sync.js +400 -0
- package/scene-workspace/primitives.d.ts +237 -3
- package/scene-workspace/primitives.d.ts.map +1 -1
- package/scene-workspace/primitives.js +1087 -17
- package/scene-workspace/scene-discovery.d.ts +5 -2
- package/scene-workspace/scene-discovery.d.ts.map +1 -1
- package/scene-workspace/scene-discovery.js +5 -2
- package/scene-workspace/scene-view.d.ts +5 -2
- package/scene-workspace/scene-view.d.ts.map +1 -1
- package/scene-workspace/scene-view.js +5 -2
- package/scene-workspace/source-to-cesium.d.ts +141 -8
- package/scene-workspace/source-to-cesium.d.ts.map +1 -1
- package/scene-workspace/source-to-cesium.js +497 -72
- package/scene-workspace/state-sync-port-kit.d.ts +156 -0
- package/scene-workspace/state-sync-port-kit.d.ts.map +1 -0
- package/scene-workspace/state-sync-port-kit.js +319 -0
- package/scene-workspace/state-sync.d.ts +34 -1
- package/scene-workspace/state-sync.d.ts.map +1 -1
- package/scene-workspace/state-sync.js +48 -0
- package/scene-workspace/types.d.ts +8 -0
- package/scene-workspace/types.d.ts.map +1 -1
- package/scene-workspace/types.js +2 -0
- package/scene-workspace/workspace.d.ts +12 -1
- package/scene-workspace/workspace.d.ts.map +1 -1
- package/scene-workspace/workspace.js +13 -0
- package/studio/index.d.ts +24 -7
- package/studio/index.d.ts.map +1 -1
- package/studio/index.js +21 -7
- package/studio/lifecycle-client.d.ts +164 -0
- package/studio/lifecycle-client.d.ts.map +1 -0
- package/studio/lifecycle-client.js +299 -0
- package/studio/lifecycle-errors.d.ts +91 -0
- package/studio/lifecycle-errors.d.ts.map +1 -0
- package/studio/lifecycle-errors.js +77 -0
- package/studio/lifecycle-types.d.ts +321 -0
- package/studio/lifecycle-types.d.ts.map +1 -0
- package/studio/lifecycle-types.js +22 -0
- package/web-components/controller.d.ts.map +1 -1
- package/web-components/csp-styles.d.ts +6 -0
- package/web-components/csp-styles.d.ts.map +1 -0
- package/web-components/csp-styles.js +53 -0
- package/web-components/elements.d.ts +158 -2
- package/web-components/elements.d.ts.map +1 -1
- package/web-components/elements.js +1302 -215
- package/web-components/export-redaction.d.ts +252 -0
- package/web-components/export-redaction.d.ts.map +1 -0
- package/web-components/export-redaction.js +588 -0
- package/web-components/export.d.ts +417 -0
- package/web-components/export.d.ts.map +1 -0
- package/web-components/export.js +719 -0
- package/web-components/feature-editor-model.d.ts +265 -0
- package/web-components/feature-editor-model.d.ts.map +1 -0
- package/web-components/feature-editor-model.js +438 -0
- package/web-components/feature-editor-sketch.d.ts +125 -0
- package/web-components/feature-editor-sketch.d.ts.map +1 -0
- package/web-components/feature-editor-sketch.js +194 -0
- package/web-components/feature-editor-workflow.d.ts +286 -0
- package/web-components/feature-editor-workflow.d.ts.map +1 -0
- package/web-components/feature-editor-workflow.js +889 -0
- package/web-components/feature-editor.d.ts +83 -0
- package/web-components/feature-editor.d.ts.map +1 -0
- package/web-components/feature-editor.js +829 -0
- package/web-components/feature-table-engine.d.ts +465 -0
- package/web-components/feature-table-engine.d.ts.map +1 -0
- package/web-components/feature-table-engine.js +1360 -0
- package/web-components/feature-table-view.d.ts +95 -0
- package/web-components/feature-table-view.d.ts.map +1 -0
- package/web-components/feature-table-view.js +343 -0
- package/web-components/index.d.ts +34 -3
- package/web-components/index.d.ts.map +1 -1
- package/web-components/index.js +35 -2
- package/web-components/maplibre-renderer.d.ts.map +1 -1
- package/web-components/maplibre-renderer.js +13 -4
- package/web-components/measurement.d.ts +3 -1
- package/web-components/measurement.d.ts.map +1 -1
- package/web-components/measurement.js +62 -18
- package/web-components/time-slider.d.ts +122 -0
- package/web-components/time-slider.d.ts.map +1 -0
- package/web-components/time-slider.js +740 -0
- package/web-components/types.d.ts +310 -0
- package/web-components/types.d.ts.map +1 -1
- package/webmap/convert-label.js +0 -1
- package/webmap/convert-symbol.d.ts.map +1 -1
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { createHonuaWebComponentController } from "./controller.js";
|
|
2
|
+
import { renderCspSafeShadowHtml } from "./csp-styles.js";
|
|
3
|
+
import { redactHonuaExportText } from "./export-redaction.js";
|
|
4
|
+
import { HONUA_EXPORT_KINDS, approximateHonuaScaleLabel, createBrowserPrintExportAdapter, runHonuaExport, } from "./export.js";
|
|
5
|
+
import { HonuaFeatureEditorElement, } from "./feature-editor.js";
|
|
6
|
+
import { featureTableFocusMoveForKey, featureTableGridHtml, featureTableGridStyles, featureTableViewModel, legacyFeatureTableViewModel, } from "./feature-table-view.js";
|
|
2
7
|
import { HonuaMapLibreRenderer } from "./maplibre-renderer.js";
|
|
3
8
|
import { HonuaMeasurementElement } from "./measurement.js";
|
|
9
|
+
import { HonuaTimeSliderElement } from "./time-slider.js";
|
|
4
10
|
const globalDom = globalThis;
|
|
5
11
|
const HTMLElementBase = globalDom.HTMLElement ?? class {
|
|
6
12
|
};
|
|
@@ -8,6 +14,7 @@ class HonuaElementBase extends HTMLElementBase {
|
|
|
8
14
|
#controller;
|
|
9
15
|
#unsubscribe;
|
|
10
16
|
#controllerReadyListener;
|
|
17
|
+
#controllerReadyTarget;
|
|
11
18
|
state;
|
|
12
19
|
get controller() {
|
|
13
20
|
return this.#controller;
|
|
@@ -39,8 +46,9 @@ class HonuaElementBase extends HTMLElementBase {
|
|
|
39
46
|
this.#unsubscribe?.remove();
|
|
40
47
|
this.#unsubscribe = undefined;
|
|
41
48
|
if (this.#controllerReadyListener) {
|
|
42
|
-
this
|
|
49
|
+
this.#controllerReadyTarget?.removeEventListener("honua-controller-ready", this.#controllerReadyListener);
|
|
43
50
|
this.#controllerReadyListener = undefined;
|
|
51
|
+
this.#controllerReadyTarget = undefined;
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
54
|
controllerChanged(_controller) { }
|
|
@@ -54,7 +62,7 @@ class HonuaElementBase extends HTMLElementBase {
|
|
|
54
62
|
this.ensureShadowRoot();
|
|
55
63
|
const root = this.shadowRoot ?? this;
|
|
56
64
|
const focus = captureFocus(root);
|
|
57
|
-
root
|
|
65
|
+
renderCspSafeShadowHtml(root, html);
|
|
58
66
|
restoreFocus(root, focus);
|
|
59
67
|
}
|
|
60
68
|
resolveControllerFromContext() {
|
|
@@ -77,6 +85,7 @@ class HonuaElementBase extends HTMLElementBase {
|
|
|
77
85
|
const target = this.getRootEventTarget();
|
|
78
86
|
if (!target)
|
|
79
87
|
return;
|
|
88
|
+
this.#controllerReadyTarget = target;
|
|
80
89
|
this.#controllerReadyListener = (event) => {
|
|
81
90
|
const detail = event.detail;
|
|
82
91
|
if (!detail?.controller)
|
|
@@ -113,6 +122,14 @@ export class HonuaMapElement extends HonuaElementBase {
|
|
|
113
122
|
#packageLoadToken = 0;
|
|
114
123
|
#packageLoading = false;
|
|
115
124
|
#packageLoadError;
|
|
125
|
+
#messages = {};
|
|
126
|
+
get messages() {
|
|
127
|
+
return this.#messages;
|
|
128
|
+
}
|
|
129
|
+
set messages(messages) {
|
|
130
|
+
this.#messages = messages ?? {};
|
|
131
|
+
this.render();
|
|
132
|
+
}
|
|
116
133
|
get map() {
|
|
117
134
|
return this.#renderer?.map;
|
|
118
135
|
}
|
|
@@ -180,10 +197,11 @@ export class HonuaMapElement extends HonuaElementBase {
|
|
|
180
197
|
}
|
|
181
198
|
catch {
|
|
182
199
|
this.#packageLoading = false;
|
|
183
|
-
this.#
|
|
200
|
+
const message = this.#messages.invalidPackage ?? "Invalid map-package JSON.";
|
|
201
|
+
this.#packageLoadError = message;
|
|
184
202
|
this.dispatchTypedEvent("honua-map-error", {
|
|
185
|
-
error: new Error(
|
|
186
|
-
message
|
|
203
|
+
error: new Error(message),
|
|
204
|
+
message,
|
|
187
205
|
});
|
|
188
206
|
}
|
|
189
207
|
}
|
|
@@ -220,6 +238,7 @@ export class HonuaMapElement extends HonuaElementBase {
|
|
|
220
238
|
const viewport = state?.viewport ?? {};
|
|
221
239
|
const visibleLayers = layers.filter((layer) => layer.visible);
|
|
222
240
|
const label = this.getAttribute("label") ?? state?.mapPackage?.mapPackageId ?? "Honua map";
|
|
241
|
+
const messages = this.#messages;
|
|
223
242
|
const root = this.shadowRoot ?? this;
|
|
224
243
|
if (!root.querySelector?.(".map")) {
|
|
225
244
|
this.setShadowHtml(`
|
|
@@ -227,10 +246,10 @@ export class HonuaMapElement extends HonuaElementBase {
|
|
|
227
246
|
<section part="map" class="map" role="region" aria-label="${escapeHtml(label)}" tabindex="0">
|
|
228
247
|
<div class="map__chrome">
|
|
229
248
|
<div class="map__title"></div>
|
|
230
|
-
<div class="map__controls" aria-label="Map controls">
|
|
231
|
-
<button type="button" class="icon-button" data-zoom="out" aria-label="Zoom out">-</button>
|
|
232
|
-
<output class="zoom" aria-label="Zoom">0</output>
|
|
233
|
-
<button type="button" class="icon-button" data-zoom="in" aria-label="Zoom in">+</button>
|
|
249
|
+
<div class="map__controls" aria-label="${escapeHtml(messages.controlsLabel ?? "Map controls")}">
|
|
250
|
+
<button type="button" class="icon-button" data-zoom="out" aria-label="${escapeHtml(messages.zoomOutLabel ?? "Zoom out")}">-</button>
|
|
251
|
+
<output class="zoom" aria-label="${escapeHtml(messages.zoomLabel ?? "Zoom")}">0</output>
|
|
252
|
+
<button type="button" class="icon-button" data-zoom="in" aria-label="${escapeHtml(messages.zoomInLabel ?? "Zoom in")}">+</button>
|
|
234
253
|
</div>
|
|
235
254
|
</div>
|
|
236
255
|
<div class="map__canvas" part="canvas">
|
|
@@ -238,8 +257,8 @@ export class HonuaMapElement extends HonuaElementBase {
|
|
|
238
257
|
<div class="map__status" aria-live="polite"></div>
|
|
239
258
|
</div>
|
|
240
259
|
<div class="map__footer">
|
|
241
|
-
<span data-visible-layers
|
|
242
|
-
<span data-center
|
|
260
|
+
<span data-visible-layers>${escapeHtml(formatMapVisibleLayers(messages.visibleLayers, 0))}</span>
|
|
261
|
+
<span data-center>${escapeHtml(messages.noCenter ?? "No center")}</span>
|
|
243
262
|
</div>
|
|
244
263
|
</section>
|
|
245
264
|
`);
|
|
@@ -249,8 +268,8 @@ export class HonuaMapElement extends HonuaElementBase {
|
|
|
249
268
|
nextRoot.querySelector(".map")?.setAttribute("aria-label", label);
|
|
250
269
|
setText(nextRoot.querySelector(".map__title"), label);
|
|
251
270
|
setText(nextRoot.querySelector(".zoom"), String(viewport.zoom?.toFixed?.(1) ?? viewport.zoom ?? 0));
|
|
252
|
-
setText(nextRoot.querySelector("[data-visible-layers]"),
|
|
253
|
-
setText(nextRoot.querySelector("[data-center]"), viewport.center ? viewport.center.join(", ") : "No center");
|
|
271
|
+
setText(nextRoot.querySelector("[data-visible-layers]"), formatMapVisibleLayers(messages.visibleLayers, visibleLayers.length));
|
|
272
|
+
setText(nextRoot.querySelector("[data-center]"), viewport.center ? viewport.center.join(", ") : (messages.noCenter ?? "No center"));
|
|
254
273
|
setText(nextRoot.querySelector(".map__status"), this.mapStatusText(state));
|
|
255
274
|
void this.syncRenderer();
|
|
256
275
|
}
|
|
@@ -345,8 +364,8 @@ export class HonuaMapElement extends HonuaElementBase {
|
|
|
345
364
|
if (state?.mapPackage)
|
|
346
365
|
return "";
|
|
347
366
|
if (this.#packageLoading || this.#packageUrl)
|
|
348
|
-
return "Loading map package";
|
|
349
|
-
return "No map package";
|
|
367
|
+
return this.#messages.loadingPackage ?? "Loading map package";
|
|
368
|
+
return this.#messages.noPackage ?? "No map package";
|
|
350
369
|
}
|
|
351
370
|
}
|
|
352
371
|
/**
|
|
@@ -371,6 +390,14 @@ export class HonuaLayerListElement extends HonuaElementBase {
|
|
|
371
390
|
}
|
|
372
391
|
#dragLayerId;
|
|
373
392
|
#layersOverride;
|
|
393
|
+
#messages = {};
|
|
394
|
+
get messages() {
|
|
395
|
+
return this.#messages;
|
|
396
|
+
}
|
|
397
|
+
set messages(messages) {
|
|
398
|
+
this.#messages = messages ?? {};
|
|
399
|
+
this.render();
|
|
400
|
+
}
|
|
374
401
|
/**
|
|
375
402
|
* The layer rows currently rendered. Headless hosts (for example the
|
|
376
403
|
* esri-compat LayerList shim delegating through `HonuaWidgetHost`) can
|
|
@@ -394,7 +421,7 @@ export class HonuaLayerListElement extends HonuaElementBase {
|
|
|
394
421
|
}
|
|
395
422
|
render() {
|
|
396
423
|
const layers = this.layers;
|
|
397
|
-
const label = this.getAttribute("label") ?? "Layers";
|
|
424
|
+
const label = this.#messages.label ?? this.getAttribute("label") ?? "Layers";
|
|
398
425
|
const supportsOpacity = typeof this.controller?.setLayerOpacity === "function";
|
|
399
426
|
const supportsReorder = typeof this.controller?.moveLayer === "function";
|
|
400
427
|
this.setShadowHtml(`
|
|
@@ -403,7 +430,7 @@ export class HonuaLayerListElement extends HonuaElementBase {
|
|
|
403
430
|
<legend>${escapeHtml(label)}</legend>
|
|
404
431
|
<div class="stack" role="list">
|
|
405
432
|
${layers.length === 0
|
|
406
|
-
? `<p class="empty"
|
|
433
|
+
? `<p class="empty">${escapeHtml(this.#messages.noLayers ?? "No layers")}</p>`
|
|
407
434
|
: layers
|
|
408
435
|
.map((layer, index) => this.renderRow(layer, index, layers.length, supportsOpacity, supportsReorder))
|
|
409
436
|
.join("")}
|
|
@@ -428,15 +455,15 @@ export class HonuaLayerListElement extends HonuaElementBase {
|
|
|
428
455
|
max="100"
|
|
429
456
|
step="1"
|
|
430
457
|
value="${opacityPercent}"
|
|
431
|
-
aria-label="Opacity"
|
|
432
|
-
aria-valuetext="${opacityPercent}
|
|
458
|
+
aria-label="${escapeAttribute(this.#messages.opacityLabel ?? "Opacity")}"
|
|
459
|
+
aria-valuetext="${escapeAttribute(formatLayerOpacityValue(this.#messages.opacityValue, opacityPercent))}"
|
|
433
460
|
data-layer-opacity="${id}"
|
|
434
461
|
/>`
|
|
435
462
|
: "";
|
|
436
463
|
const reorder = supportsReorder
|
|
437
464
|
? `
|
|
438
|
-
<button type="button" class="move" part="move-up" aria-label="Move up" data-move="up:${id}" ${index === 0 ? "disabled" : ""}>▲</button>
|
|
439
|
-
<button type="button" class="move" part="move-down" aria-label="Move down" data-move="down:${id}" ${index === count - 1 ? "disabled" : ""}>▼</button>`
|
|
465
|
+
<button type="button" class="move" part="move-up" aria-label="${escapeAttribute(this.#messages.moveUpLabel ?? "Move up")}" data-move="up:${id}" ${index === 0 ? "disabled" : ""}>▲</button>
|
|
466
|
+
<button type="button" class="move" part="move-down" aria-label="${escapeAttribute(this.#messages.moveDownLabel ?? "Move down")}" data-move="down:${id}" ${index === count - 1 ? "disabled" : ""}>▼</button>`
|
|
440
467
|
: "";
|
|
441
468
|
return `
|
|
442
469
|
<div class="layer-row" role="listitem" part="row" data-layer-row="${id}"${supportsReorder ? ` draggable="true"` : ""}>
|
|
@@ -563,6 +590,14 @@ export class HonuaLegendElement extends HonuaElementBase {
|
|
|
563
590
|
return ["for", "label", "include-hidden"];
|
|
564
591
|
}
|
|
565
592
|
#itemsOverride;
|
|
593
|
+
#messages = {};
|
|
594
|
+
get messages() {
|
|
595
|
+
return this.#messages;
|
|
596
|
+
}
|
|
597
|
+
set messages(messages) {
|
|
598
|
+
this.#messages = messages ?? {};
|
|
599
|
+
this.render();
|
|
600
|
+
}
|
|
566
601
|
/**
|
|
567
602
|
* Explicit legend items. When set, these render instead of the controller
|
|
568
603
|
* state's legend (visibility coupling still applies when the controller is
|
|
@@ -598,20 +633,23 @@ export class HonuaLegendElement extends HonuaElementBase {
|
|
|
598
633
|
}
|
|
599
634
|
render() {
|
|
600
635
|
const legend = this.visibleItems;
|
|
601
|
-
const label = this.getAttribute("label") ?? "Legend";
|
|
636
|
+
const label = this.#messages.label ?? this.getAttribute("label") ?? "Legend";
|
|
637
|
+
const swatchStyles = legend
|
|
638
|
+
.map((item, index) => `.swatch-${index} { --swatch: ${escapeAttribute(item.color ?? "#64748b")}; }`)
|
|
639
|
+
.join("\n");
|
|
602
640
|
this.setShadowHtml(`
|
|
603
|
-
<style>${baseStyles()}${listStyles()}</style>
|
|
641
|
+
<style>${baseStyles()}${listStyles()}${legendStyles()}${swatchStyles}</style>
|
|
604
642
|
<section class="panel" part="panel" aria-label="${escapeHtml(label)}">
|
|
605
643
|
<h2>${escapeHtml(label)}</h2>
|
|
606
644
|
<ul class="legend" role="list">
|
|
607
645
|
${legend.length === 0
|
|
608
|
-
? `<li class="empty"
|
|
646
|
+
? `<li class="empty">${escapeHtml(this.#messages.noLegend ?? "No legend")}</li>`
|
|
609
647
|
: legend
|
|
610
|
-
.map((item) => `
|
|
648
|
+
.map((item, index) => `
|
|
611
649
|
<li role="listitem" data-legend-item="${escapeAttribute(item.id)}">
|
|
612
650
|
${item.iconUrl
|
|
613
651
|
? `<img class="swatch" alt="" aria-hidden="true" src="${escapeAttribute(item.iconUrl)}" />`
|
|
614
|
-
: `<span class="swatch" aria-hidden="true"
|
|
652
|
+
: `<span class="swatch swatch-${index}" aria-hidden="true"></span>`}
|
|
615
653
|
<span>${escapeHtml(item.label)}</span>
|
|
616
654
|
</li>
|
|
617
655
|
`)
|
|
@@ -621,12 +659,86 @@ export class HonuaLegendElement extends HonuaElementBase {
|
|
|
621
659
|
`);
|
|
622
660
|
}
|
|
623
661
|
}
|
|
662
|
+
/**
|
|
663
|
+
* `<honua-feature-table>` — the production feature grid (issue #681).
|
|
664
|
+
*
|
|
665
|
+
* Two lanes, both rendering the same accessible WAI-ARIA `grid`:
|
|
666
|
+
*
|
|
667
|
+
* - **Bounded lane** (recommended for operational apps) — assign the `table`
|
|
668
|
+
* property a {@link HonuaFeatureTable} from
|
|
669
|
+
* {@link createHonuaFeatureTable}. The element then renders the engine's
|
|
670
|
+
* virtualized window, drives remote paging from real scroll geometry, pushes
|
|
671
|
+
* header clicks into multi-column sort, exposes total-known / estimated /
|
|
672
|
+
* partial / stale / loading / cancelled / unsupported / error truth, and
|
|
673
|
+
* announces realtime reconciliation conflicts in a polite live region.
|
|
674
|
+
* - **Controller lane** (unchanged, backwards compatible) — with no engine
|
|
675
|
+
* attached the element queries the shared controller for a single bounded
|
|
676
|
+
* page, exactly as before, and renders it through the same grid markup so the
|
|
677
|
+
* keyboard and screen-reader contract does not depend on which lane is in use.
|
|
678
|
+
*
|
|
679
|
+
* Keyboard: arrow keys move the focused cell, `Home`/`End` jump within the row
|
|
680
|
+
* (`Ctrl`/`Cmd` to the grid), `PageUp`/`PageDown` move a window at a time —
|
|
681
|
+
* loading the page the focus lands on in the bounded lane — and
|
|
682
|
+
* `Enter`/`Space` selects the focused row. Exactly one cell is tabbable
|
|
683
|
+
* (roving `tabindex`).
|
|
684
|
+
*/
|
|
624
685
|
export class HonuaFeatureTableElement extends HonuaElementBase {
|
|
625
686
|
static get observedAttributes() {
|
|
626
|
-
return ["for", "source", "fields", "page-size", "filter-text", "label"];
|
|
687
|
+
return ["for", "source", "fields", "page-size", "filter-text", "label", "row-height"];
|
|
627
688
|
}
|
|
628
689
|
#model;
|
|
629
690
|
#refreshToken = 0;
|
|
691
|
+
#table;
|
|
692
|
+
#tableUnsubscribe;
|
|
693
|
+
#tableSnapshot;
|
|
694
|
+
#announcedConflicts = "";
|
|
695
|
+
#tableConnected = false;
|
|
696
|
+
#restoringScroll = false;
|
|
697
|
+
#messages = {};
|
|
698
|
+
get messages() {
|
|
699
|
+
return this.#messages;
|
|
700
|
+
}
|
|
701
|
+
set messages(messages) {
|
|
702
|
+
this.#messages = messages ?? {};
|
|
703
|
+
this.render();
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* The bounded query engine backing this grid. Assigning one switches the
|
|
707
|
+
* element to the bounded lane; assigning `undefined` restores the
|
|
708
|
+
* controller-driven single-page lane.
|
|
709
|
+
*/
|
|
710
|
+
get table() {
|
|
711
|
+
return this.#table;
|
|
712
|
+
}
|
|
713
|
+
set table(table) {
|
|
714
|
+
if (this.#table === table) {
|
|
715
|
+
// Re-assigning the same engine must still be able to re-take a
|
|
716
|
+
// subscription that `disconnectedCallback()` dropped.
|
|
717
|
+
this.#subscribeTable();
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
this.#tableUnsubscribe?.();
|
|
721
|
+
this.#tableUnsubscribe = undefined;
|
|
722
|
+
this.#table = table;
|
|
723
|
+
this.#tableSnapshot = table?.snapshot;
|
|
724
|
+
// Only hold a subscription while connected; `connectedCallback()` re-takes
|
|
725
|
+
// it so a detached-then-reinserted grid resumes updating.
|
|
726
|
+
this.#subscribeTable();
|
|
727
|
+
this.render();
|
|
728
|
+
}
|
|
729
|
+
/** Takes the engine subscription, unless one is already held or we are detached. */
|
|
730
|
+
#subscribeTable() {
|
|
731
|
+
const table = this.#table;
|
|
732
|
+
if (!table || this.#tableUnsubscribe || !this.#tableConnected)
|
|
733
|
+
return;
|
|
734
|
+
// The engine may have moved on while detached; re-read before listening.
|
|
735
|
+
this.#tableSnapshot = table.snapshot;
|
|
736
|
+
this.#tableUnsubscribe = table.subscribe((snapshot) => {
|
|
737
|
+
this.#tableSnapshot = snapshot;
|
|
738
|
+
this.render();
|
|
739
|
+
this.#announceConflicts(snapshot);
|
|
740
|
+
});
|
|
741
|
+
}
|
|
630
742
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
631
743
|
this.resolveControllerFromContext();
|
|
632
744
|
if (name === "filter-text") {
|
|
@@ -640,13 +752,40 @@ export class HonuaFeatureTableElement extends HonuaElementBase {
|
|
|
640
752
|
void this.refresh();
|
|
641
753
|
}
|
|
642
754
|
connectedCallback() {
|
|
755
|
+
this.#tableConnected = true;
|
|
643
756
|
super.connectedCallback();
|
|
757
|
+
this.#subscribeTable();
|
|
758
|
+
if (this.#table) {
|
|
759
|
+
// Reconnect must not re-query: the engine already holds bounded pages, and
|
|
760
|
+
// re-taking the subscription plus a render is enough to resume.
|
|
761
|
+
this.render();
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
644
764
|
void this.refresh();
|
|
645
765
|
}
|
|
766
|
+
disconnectedCallback() {
|
|
767
|
+
super.disconnectedCallback();
|
|
768
|
+
this.#tableConnected = false;
|
|
769
|
+
this.#tableUnsubscribe?.();
|
|
770
|
+
this.#tableUnsubscribe = undefined;
|
|
771
|
+
}
|
|
646
772
|
stateChanged() {
|
|
773
|
+
if (this.#table) {
|
|
774
|
+
// Selection is owned by the engine (and, through it, shared exploration
|
|
775
|
+
// state) in the bounded lane — a controller state change must not trigger
|
|
776
|
+
// an unbounded re-query.
|
|
777
|
+
this.render();
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
647
780
|
void this.refresh();
|
|
648
781
|
}
|
|
649
782
|
async refresh() {
|
|
783
|
+
if (this.#table) {
|
|
784
|
+
const snapshot = await this.#table.refresh();
|
|
785
|
+
this.#tableSnapshot = snapshot;
|
|
786
|
+
this.render();
|
|
787
|
+
return featureTableModelFromSnapshot(snapshot);
|
|
788
|
+
}
|
|
650
789
|
const controller = this.controller;
|
|
651
790
|
if (!controller)
|
|
652
791
|
return undefined;
|
|
@@ -662,53 +801,202 @@ export class HonuaFeatureTableElement extends HonuaElementBase {
|
|
|
662
801
|
return model;
|
|
663
802
|
}
|
|
664
803
|
render() {
|
|
665
|
-
const
|
|
666
|
-
const
|
|
667
|
-
const
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
<table role="grid" aria-rowcount="${escapeHtml(String(model.rows.length))}">
|
|
678
|
-
<thead>
|
|
679
|
-
<tr>${model.fields.map((field) => `<th scope="col">${escapeHtml(field)}</th>`).join("")}</tr>
|
|
680
|
-
</thead>
|
|
681
|
-
<tbody>
|
|
682
|
-
${model.rows.length === 0
|
|
683
|
-
? `<tr><td colspan="${Math.max(1, model.fields.length)}" class="empty">No rows</td></tr>`
|
|
684
|
-
: model.rows
|
|
685
|
-
.map((row) => `
|
|
686
|
-
<tr tabindex="0" data-source-id="${escapeHtml(row.sourceId)}" data-feature-id="${escapeHtml(String(row.id))}" aria-selected="${String(String(selectedId ?? "") === String(row.id))}">
|
|
687
|
-
${model.fields.map((field) => `<td>${escapeHtml(formatCell(row.attributes[field]))}</td>`).join("")}
|
|
688
|
-
</tr>
|
|
689
|
-
`)
|
|
690
|
-
.join("")}
|
|
691
|
-
</tbody>
|
|
692
|
-
</table>
|
|
693
|
-
</div>
|
|
694
|
-
</section>
|
|
695
|
-
`);
|
|
696
|
-
this.shadowRoot?.querySelectorAll("tbody tr[data-feature-id]").forEach((row) => {
|
|
697
|
-
const select = () => this.selectRow(row);
|
|
698
|
-
row.addEventListener("click", select);
|
|
699
|
-
row.addEventListener("keydown", (event) => {
|
|
700
|
-
if (event.key !== "Enter" && event.key !== " ")
|
|
701
|
-
return;
|
|
702
|
-
event.preventDefault();
|
|
703
|
-
select();
|
|
804
|
+
const label = this.#messages.label ?? this.getAttribute("label") ?? "Features";
|
|
805
|
+
const rowHeight = this.rowHeight();
|
|
806
|
+
const snapshot = this.#tableSnapshot;
|
|
807
|
+
const viewModel = snapshot
|
|
808
|
+
? featureTableViewModel(snapshot, { label, rowHeight, messages: this.#messages })
|
|
809
|
+
: legacyFeatureTableViewModel(this.#model ?? tableModelFromState(this.state, this.sourceId(), this.fields(), this.pageSize()), {
|
|
810
|
+
label,
|
|
811
|
+
rowHeight,
|
|
812
|
+
messages: this.#messages,
|
|
813
|
+
...(this.state?.selection?.featureId !== undefined
|
|
814
|
+
? { selectedFeatureId: String(this.state.selection.featureId) }
|
|
815
|
+
: {}),
|
|
704
816
|
});
|
|
817
|
+
// `setShadowHtml` replaces the whole shadow tree, including the scroll
|
|
818
|
+
// container. A fresh container starts at `scrollTop = 0`, which would snap a
|
|
819
|
+
// virtualized grid back to the top (and strand a full-height leading spacer)
|
|
820
|
+
// on every publish — including the synchronous loading snapshot that
|
|
821
|
+
// `setScroll` itself publishes. Carry the offset across the swap.
|
|
822
|
+
const previousScrollTop = this.#scroller()?.scrollTop ?? 0;
|
|
823
|
+
this.setShadowHtml(`<style>${baseStyles()}${tableStyles()}${featureTableGridStyles()}</style>${featureTableGridHtml(viewModel)}`);
|
|
824
|
+
this.#bindGrid();
|
|
825
|
+
this.#restoreScrollTop(previousScrollTop);
|
|
826
|
+
}
|
|
827
|
+
#scroller() {
|
|
828
|
+
return this.shadowRoot?.querySelector("[data-scroller]") ?? null;
|
|
829
|
+
}
|
|
830
|
+
#restoreScrollTop(scrollTop) {
|
|
831
|
+
if (scrollTop <= 0)
|
|
832
|
+
return;
|
|
833
|
+
const scroller = this.#scroller();
|
|
834
|
+
if (!scroller || scroller.scrollTop === scrollTop)
|
|
835
|
+
return;
|
|
836
|
+
// Restoring the offset fires `scroll`; that echo must not be mistaken for a
|
|
837
|
+
// user gesture and fed back into the engine as a new window.
|
|
838
|
+
this.#restoringScroll = true;
|
|
839
|
+
scroller.scrollTop = scrollTop;
|
|
840
|
+
queueMicrotask(() => {
|
|
841
|
+
this.#restoringScroll = false;
|
|
705
842
|
});
|
|
706
843
|
}
|
|
844
|
+
#bindGrid() {
|
|
845
|
+
const root = this.shadowRoot;
|
|
846
|
+
if (!root)
|
|
847
|
+
return;
|
|
848
|
+
for (const button of root.querySelectorAll("[data-sort-field]")) {
|
|
849
|
+
button.addEventListener("click", (event) => {
|
|
850
|
+
const field = button.dataset.sortField;
|
|
851
|
+
if (!field)
|
|
852
|
+
return;
|
|
853
|
+
void this.#table?.toggleSort(field, { additive: event.shiftKey });
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
for (const row of root.querySelectorAll("tbody tr[data-feature-id]")) {
|
|
857
|
+
row.addEventListener("click", () => this.selectRow(row));
|
|
858
|
+
row.addEventListener("keydown", (event) => this.#onRowKeydown(event, row));
|
|
859
|
+
}
|
|
860
|
+
for (const cell of root.querySelectorAll('tbody [role="gridcell"]')) {
|
|
861
|
+
cell.addEventListener("focus", () => this.#focusCell(cell));
|
|
862
|
+
cell.addEventListener("keydown", (event) => this.#onGridKeydown(event, cell));
|
|
863
|
+
}
|
|
864
|
+
const scroller = root.querySelector("[data-scroller]");
|
|
865
|
+
if (scroller && this.#table) {
|
|
866
|
+
scroller.addEventListener("scroll", () => {
|
|
867
|
+
if (this.#restoringScroll)
|
|
868
|
+
return;
|
|
869
|
+
void this.#table?.setScroll({
|
|
870
|
+
scrollTop: scroller.scrollTop,
|
|
871
|
+
rowHeight: this.rowHeight(),
|
|
872
|
+
viewportHeight: scroller.clientHeight || this.rowHeight() * 10,
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Keys pressed while the **row** itself holds focus, rather than one of its
|
|
879
|
+
* cells. Cells handle their own keys and their events bubble through the row,
|
|
880
|
+
* so this bails unless the row is the actual target — no double handling.
|
|
881
|
+
*
|
|
882
|
+
* Enter/Space selects, matching the row-level activation the grid has always
|
|
883
|
+
* offered. Any navigation key hands focus to the row's first cell, which is
|
|
884
|
+
* where the roving-tabindex model takes over.
|
|
885
|
+
*/
|
|
886
|
+
#onRowKeydown(event, row) {
|
|
887
|
+
if (event.target !== row)
|
|
888
|
+
return;
|
|
889
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
890
|
+
event.preventDefault();
|
|
891
|
+
this.selectRow(row);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
if (!featureTableFocusMoveForKey(event.key, { ctrl: event.ctrlKey, meta: event.metaKey }))
|
|
895
|
+
return;
|
|
896
|
+
const cell = row.querySelector('[role="gridcell"]');
|
|
897
|
+
if (!cell)
|
|
898
|
+
return;
|
|
899
|
+
event.preventDefault();
|
|
900
|
+
cell.focus();
|
|
901
|
+
}
|
|
902
|
+
#focusCell(cell) {
|
|
903
|
+
const rowKey = cell.closest("tr")?.dataset.rowKey;
|
|
904
|
+
const field = cell.dataset.field;
|
|
905
|
+
if (!rowKey || !field || !this.#table)
|
|
906
|
+
return;
|
|
907
|
+
if (this.#tableSnapshot?.focus?.rowKey === rowKey && this.#tableSnapshot.focus.field === field)
|
|
908
|
+
return;
|
|
909
|
+
this.#table.setFocus({ rowKey, field });
|
|
910
|
+
}
|
|
911
|
+
#onGridKeydown(event, cell) {
|
|
912
|
+
const row = cell.closest("tr");
|
|
913
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
914
|
+
event.preventDefault();
|
|
915
|
+
if (row)
|
|
916
|
+
this.selectRow(row);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const move = featureTableFocusMoveForKey(event.key, { ctrl: event.ctrlKey, meta: event.metaKey });
|
|
920
|
+
if (!move)
|
|
921
|
+
return;
|
|
922
|
+
event.preventDefault();
|
|
923
|
+
const table = this.#table;
|
|
924
|
+
if (!table) {
|
|
925
|
+
this.#moveDomFocus(move, cell, row);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (!this.#tableSnapshot?.focus)
|
|
929
|
+
this.#focusCell(cell);
|
|
930
|
+
void table.moveFocus(move).then(() => this.#restoreFocusedCell());
|
|
931
|
+
}
|
|
932
|
+
/** Controller lane: move focus within the rendered rows only — no paging. */
|
|
933
|
+
#moveDomFocus(move, cell, row) {
|
|
934
|
+
const root = this.shadowRoot;
|
|
935
|
+
if (!root || !row)
|
|
936
|
+
return;
|
|
937
|
+
const rows = [...root.querySelectorAll("tbody tr")];
|
|
938
|
+
const cells = [...row.querySelectorAll('[role="gridcell"]')];
|
|
939
|
+
const rowIndex = rows.indexOf(row);
|
|
940
|
+
const cellIndex = cells.indexOf(cell);
|
|
941
|
+
const clamp = (value, max) => Math.max(0, Math.min(value, max));
|
|
942
|
+
let nextRow = rowIndex;
|
|
943
|
+
let nextCell = cellIndex;
|
|
944
|
+
if (move === "up")
|
|
945
|
+
nextRow -= 1;
|
|
946
|
+
else if (move === "down")
|
|
947
|
+
nextRow += 1;
|
|
948
|
+
else if (move === "left")
|
|
949
|
+
nextCell -= 1;
|
|
950
|
+
else if (move === "right")
|
|
951
|
+
nextCell += 1;
|
|
952
|
+
else if (move === "row-start")
|
|
953
|
+
nextCell = 0;
|
|
954
|
+
else if (move === "row-end")
|
|
955
|
+
nextCell = cells.length - 1;
|
|
956
|
+
else if (move === "page-up")
|
|
957
|
+
nextRow = 0;
|
|
958
|
+
else if (move === "page-down")
|
|
959
|
+
nextRow = rows.length - 1;
|
|
960
|
+
else if (move === "grid-start") {
|
|
961
|
+
nextRow = 0;
|
|
962
|
+
nextCell = 0;
|
|
963
|
+
}
|
|
964
|
+
else if (move === "grid-end") {
|
|
965
|
+
nextRow = rows.length - 1;
|
|
966
|
+
nextCell = cells.length - 1;
|
|
967
|
+
}
|
|
968
|
+
const target = rows[clamp(nextRow, rows.length - 1)]?.querySelectorAll('[role="gridcell"]')[clamp(nextCell, cells.length - 1)];
|
|
969
|
+
target?.focus();
|
|
970
|
+
}
|
|
971
|
+
#restoreFocusedCell() {
|
|
972
|
+
const focus = this.#tableSnapshot?.focus;
|
|
973
|
+
const root = this.shadowRoot;
|
|
974
|
+
if (!focus || !root)
|
|
975
|
+
return;
|
|
976
|
+
const selector = `tbody tr[data-row-key="${cssEscape(focus.rowKey)}"] [data-field="${cssEscape(focus.field)}"]`;
|
|
977
|
+
root.querySelector(selector)?.focus();
|
|
978
|
+
}
|
|
979
|
+
#announceConflicts(snapshot) {
|
|
980
|
+
const digest = snapshot.conflicts.map((conflict) => `${conflict.code}:${conflict.rowKeys.join(",")}`).join("|");
|
|
981
|
+
if (digest === this.#announcedConflicts)
|
|
982
|
+
return;
|
|
983
|
+
this.#announcedConflicts = digest;
|
|
984
|
+
for (const conflict of snapshot.conflicts) {
|
|
985
|
+
this.dispatchTypedEvent("honua-table-conflict", {
|
|
986
|
+
code: conflict.code,
|
|
987
|
+
message: conflict.message,
|
|
988
|
+
rowKeys: conflict.rowKeys,
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
}
|
|
707
992
|
selectRow(row) {
|
|
708
993
|
const sourceId = row.dataset.sourceId;
|
|
709
994
|
const featureId = row.dataset.featureId;
|
|
710
995
|
if (!sourceId || featureId === undefined)
|
|
711
996
|
return;
|
|
997
|
+
const rowKey = row.dataset.rowKey;
|
|
998
|
+
if (this.#table && rowKey)
|
|
999
|
+
this.#table.select([rowKey]);
|
|
712
1000
|
const feature = (this.#model?.rows ?? []).find((candidate) => String(candidate.id) === featureId);
|
|
713
1001
|
this.controller?.selectFeature({ sourceId, featureId, ...(feature ? { feature } : {}) });
|
|
714
1002
|
this.dispatchTypedEvent("honua-selection-change", {
|
|
@@ -731,6 +1019,53 @@ export class HonuaFeatureTableElement extends HonuaElementBase {
|
|
|
731
1019
|
const parsed = Number(this.getAttribute("page-size"));
|
|
732
1020
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
733
1021
|
}
|
|
1022
|
+
rowHeight() {
|
|
1023
|
+
const parsed = Number(this.getAttribute("row-height"));
|
|
1024
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 32;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Project a bounded snapshot onto the legacy {@link HonuaFeatureTableModel} so
|
|
1029
|
+
* existing `refresh()` callers keep compiling. `totalCount` is only the
|
|
1030
|
+
* engine's count when the engine actually knows one; a partial or unknown count
|
|
1031
|
+
* reports the resident row count, which the `exceededTransferLimit` flag marks
|
|
1032
|
+
* as incomplete rather than presenting it as a total.
|
|
1033
|
+
*/
|
|
1034
|
+
function featureTableModelFromSnapshot(snapshot) {
|
|
1035
|
+
const rows = snapshot.rows.filter((row) => row !== undefined);
|
|
1036
|
+
const known = snapshot.count.kind === "known" || snapshot.count.kind === "estimated";
|
|
1037
|
+
return {
|
|
1038
|
+
sourceId: snapshot.sourceId,
|
|
1039
|
+
status: legacyStatusForState(snapshot.state),
|
|
1040
|
+
fields: snapshot.visibleColumns.map((column) => column.field),
|
|
1041
|
+
rows: rows.map((row) => ({
|
|
1042
|
+
id: row.id,
|
|
1043
|
+
sourceId: row.sourceId,
|
|
1044
|
+
attributes: row.attributes,
|
|
1045
|
+
...(row.geometry !== undefined ? { geometry: row.geometry } : {}),
|
|
1046
|
+
})),
|
|
1047
|
+
totalCount: known ? (snapshot.count.value ?? snapshot.count.loaded) : snapshot.count.loaded,
|
|
1048
|
+
...(known ? {} : { exceededTransferLimit: true }),
|
|
1049
|
+
...(snapshot.error !== undefined ? { error: snapshot.error } : {}),
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
function legacyStatusForState(state) {
|
|
1053
|
+
switch (state) {
|
|
1054
|
+
case "idle":
|
|
1055
|
+
return "idle";
|
|
1056
|
+
case "loading":
|
|
1057
|
+
return "loading";
|
|
1058
|
+
case "error":
|
|
1059
|
+
return "error";
|
|
1060
|
+
case "unsupported":
|
|
1061
|
+
return "unsupported";
|
|
1062
|
+
default:
|
|
1063
|
+
return "ready";
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
/** Minimal `CSS.escape` for the attribute selectors this element builds. */
|
|
1067
|
+
function cssEscape(value) {
|
|
1068
|
+
return value.split("\\").join("\\\\").split('"').join('\\"');
|
|
734
1069
|
}
|
|
735
1070
|
/**
|
|
736
1071
|
* `<honua-search>` — the survival-tier search box (issue #493).
|
|
@@ -752,7 +1087,7 @@ export class HonuaFeatureTableElement extends HonuaElementBase {
|
|
|
752
1087
|
*/
|
|
753
1088
|
export class HonuaSearchElement extends HonuaElementBase {
|
|
754
1089
|
static get observedAttributes() {
|
|
755
|
-
return ["for", "source", "placeholder", "label", "debounce", "zoom"];
|
|
1090
|
+
return ["for", "source", "placeholder", "label", "submit-label", "debounce", "zoom"];
|
|
756
1091
|
}
|
|
757
1092
|
#query = "";
|
|
758
1093
|
#results = [];
|
|
@@ -763,6 +1098,14 @@ export class HonuaSearchElement extends HonuaElementBase {
|
|
|
763
1098
|
#suggestTimer;
|
|
764
1099
|
#suggestToken = 0;
|
|
765
1100
|
#geocodeToken = 0;
|
|
1101
|
+
#messages = {};
|
|
1102
|
+
get messages() {
|
|
1103
|
+
return this.#messages;
|
|
1104
|
+
}
|
|
1105
|
+
set messages(messages) {
|
|
1106
|
+
this.#messages = messages ?? {};
|
|
1107
|
+
this.render();
|
|
1108
|
+
}
|
|
766
1109
|
/** The geocoding provider powering typeahead + geocode-on-submit. */
|
|
767
1110
|
get geocoder() {
|
|
768
1111
|
return this.#geocoder;
|
|
@@ -775,6 +1118,20 @@ export class HonuaSearchElement extends HonuaElementBase {
|
|
|
775
1118
|
this.#status = "";
|
|
776
1119
|
this.render();
|
|
777
1120
|
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Text rendered in the form's submit button. Defaults to `"Search"` for
|
|
1123
|
+
* compatibility with the original markup. The property reflects the
|
|
1124
|
+
* `submit-label` attribute so either caller API can supply localized text.
|
|
1125
|
+
*/
|
|
1126
|
+
get submitLabel() {
|
|
1127
|
+
return this.getAttribute("submit-label") ?? "Search";
|
|
1128
|
+
}
|
|
1129
|
+
set submitLabel(value) {
|
|
1130
|
+
if (value === undefined)
|
|
1131
|
+
this.removeAttribute("submit-label");
|
|
1132
|
+
else
|
|
1133
|
+
this.setAttribute("submit-label", value);
|
|
1134
|
+
}
|
|
778
1135
|
attributeChangedCallback() {
|
|
779
1136
|
this.resolveControllerFromContext();
|
|
780
1137
|
this.render();
|
|
@@ -784,8 +1141,9 @@ export class HonuaSearchElement extends HonuaElementBase {
|
|
|
784
1141
|
super.disconnectedCallback();
|
|
785
1142
|
}
|
|
786
1143
|
render() {
|
|
787
|
-
const label = this.getAttribute("label") ?? "Search";
|
|
788
|
-
const placeholder = this.getAttribute("placeholder") ?? "Search";
|
|
1144
|
+
const label = this.#messages.label ?? this.getAttribute("label") ?? "Search";
|
|
1145
|
+
const placeholder = this.#messages.placeholder ?? this.getAttribute("placeholder") ?? "Search";
|
|
1146
|
+
const submitLabel = this.#messages.submitLabel ?? this.submitLabel;
|
|
789
1147
|
const geocoding = this.#geocoder !== undefined;
|
|
790
1148
|
const expanded = geocoding && this.#suggestions.length > 0;
|
|
791
1149
|
const comboboxAttributes = geocoding
|
|
@@ -797,10 +1155,12 @@ export class HonuaSearchElement extends HonuaElementBase {
|
|
|
797
1155
|
<form>
|
|
798
1156
|
<label class="sr-only" for="honua-search-input">${escapeHtml(label)}</label>
|
|
799
1157
|
<input id="honua-search-input" name="q" value="${escapeAttribute(this.#query)}" placeholder="${escapeAttribute(placeholder)}" autocomplete="off"${comboboxAttributes} />
|
|
800
|
-
<button type="submit"
|
|
1158
|
+
<button type="submit">${escapeHtml(submitLabel)}</button>
|
|
801
1159
|
</form>
|
|
802
1160
|
${geocoding
|
|
803
|
-
? `<ul class="suggestions" id="honua-search-listbox" role="listbox" aria-label="${escapeAttribute(
|
|
1161
|
+
? `<ul class="suggestions" id="honua-search-listbox" role="listbox" aria-label="${escapeAttribute(typeof this.#messages.suggestionsLabel === "function"
|
|
1162
|
+
? this.#messages.suggestionsLabel(label)
|
|
1163
|
+
: (this.#messages.suggestionsLabel ?? `${label} suggestions`))}"${expanded ? "" : " hidden"}>
|
|
804
1164
|
${this.#suggestions
|
|
805
1165
|
.map((suggestion, index) => `<li id="honua-search-option-${index}" role="option" aria-selected="${String(index === this.#activeIndex)}" data-suggestion-index="${index}">${escapeHtml(suggestion.text)}</li>`)
|
|
806
1166
|
.join("")}
|
|
@@ -821,11 +1181,18 @@ export class HonuaSearchElement extends HonuaElementBase {
|
|
|
821
1181
|
else
|
|
822
1182
|
void this.runSearch(query);
|
|
823
1183
|
});
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1184
|
+
// The typed query is tracked unconditionally, not only when a geocoder is
|
|
1185
|
+
// assigned. `render()` re-creates the input from `#query`, so without this
|
|
1186
|
+
// any controller-driven re-render (a viewport change, a layer toggle, a
|
|
1187
|
+
// realtime refresh) silently wiped whatever the user had typed — the
|
|
1188
|
+
// focus-restoration gate this is measured against (issue #683, REQ-004)
|
|
1189
|
+
// found exactly that. Suggestion scheduling stays geocoder-gated.
|
|
1190
|
+
input?.addEventListener("input", () => {
|
|
1191
|
+
this.#query = input.value;
|
|
1192
|
+
if (this.#geocoder)
|
|
827
1193
|
this.scheduleSuggest();
|
|
828
|
-
|
|
1194
|
+
});
|
|
1195
|
+
if (this.#geocoder) {
|
|
829
1196
|
input?.addEventListener("keydown", (event) => this.onComboboxKeydown(event));
|
|
830
1197
|
this.shadowRoot?.querySelectorAll("li[data-suggestion-index]").forEach((option) => {
|
|
831
1198
|
option.addEventListener("click", () => {
|
|
@@ -912,7 +1279,8 @@ export class HonuaSearchElement extends HonuaElementBase {
|
|
|
912
1279
|
return;
|
|
913
1280
|
this.#suggestions = [];
|
|
914
1281
|
this.#activeIndex = -1;
|
|
915
|
-
this.#status =
|
|
1282
|
+
this.#status =
|
|
1283
|
+
error instanceof Error ? error.message : (this.#messages.suggestionsUnavailable ?? "Suggestions unavailable.");
|
|
916
1284
|
this.render();
|
|
917
1285
|
}
|
|
918
1286
|
}
|
|
@@ -949,7 +1317,7 @@ export class HonuaSearchElement extends HonuaElementBase {
|
|
|
949
1317
|
return;
|
|
950
1318
|
const candidate = candidates[0];
|
|
951
1319
|
if (!candidate) {
|
|
952
|
-
this.#status = `No results for "${trimmed}".`;
|
|
1320
|
+
this.#status = this.#messages.noResults?.(trimmed) ?? `No results for "${trimmed}".`;
|
|
953
1321
|
this.render();
|
|
954
1322
|
return;
|
|
955
1323
|
}
|
|
@@ -969,7 +1337,12 @@ export class HonuaSearchElement extends HonuaElementBase {
|
|
|
969
1337
|
catch (error) {
|
|
970
1338
|
if (token !== this.#geocodeToken)
|
|
971
1339
|
return;
|
|
972
|
-
this.#status =
|
|
1340
|
+
this.#status =
|
|
1341
|
+
error instanceof Error
|
|
1342
|
+
? error.message
|
|
1343
|
+
: typeof this.#messages.geocodingFailed === "function"
|
|
1344
|
+
? this.#messages.geocodingFailed(error)
|
|
1345
|
+
: (this.#messages.geocodingFailed ?? "Geocoding failed.");
|
|
973
1346
|
this.render();
|
|
974
1347
|
}
|
|
975
1348
|
}
|
|
@@ -997,9 +1370,20 @@ export class HonuaSearchElement extends HonuaElementBase {
|
|
|
997
1370
|
}
|
|
998
1371
|
export class HonuaEditorElement extends HonuaElementBase {
|
|
999
1372
|
static get observedAttributes() {
|
|
1000
|
-
return [
|
|
1373
|
+
return [
|
|
1374
|
+
"for",
|
|
1375
|
+
"source",
|
|
1376
|
+
"label",
|
|
1377
|
+
"new-label",
|
|
1378
|
+
"save-label",
|
|
1379
|
+
"delete-label",
|
|
1380
|
+
"no-selection-label",
|
|
1381
|
+
"read-only-label",
|
|
1382
|
+
"editable-label",
|
|
1383
|
+
];
|
|
1001
1384
|
}
|
|
1002
1385
|
#model;
|
|
1386
|
+
#messages = {};
|
|
1003
1387
|
get editorModel() {
|
|
1004
1388
|
return this.#model ?? this.state?.editor;
|
|
1005
1389
|
}
|
|
@@ -1007,6 +1391,74 @@ export class HonuaEditorElement extends HonuaElementBase {
|
|
|
1007
1391
|
this.#model = model;
|
|
1008
1392
|
this.render();
|
|
1009
1393
|
}
|
|
1394
|
+
/** Caller-supplied localized status and read-only messages. */
|
|
1395
|
+
get messages() {
|
|
1396
|
+
return this.#messages;
|
|
1397
|
+
}
|
|
1398
|
+
set messages(messages) {
|
|
1399
|
+
this.#messages = messages ?? {};
|
|
1400
|
+
this.render();
|
|
1401
|
+
}
|
|
1402
|
+
/** Text rendered by the create action. Defaults to `"New"`. */
|
|
1403
|
+
get newLabel() {
|
|
1404
|
+
return this.getAttribute("new-label") ?? "New";
|
|
1405
|
+
}
|
|
1406
|
+
set newLabel(value) {
|
|
1407
|
+
if (value === undefined)
|
|
1408
|
+
this.removeAttribute("new-label");
|
|
1409
|
+
else
|
|
1410
|
+
this.setAttribute("new-label", value);
|
|
1411
|
+
}
|
|
1412
|
+
/** Text rendered by the save action. Defaults to `"Save"`. */
|
|
1413
|
+
get saveLabel() {
|
|
1414
|
+
return this.getAttribute("save-label") ?? "Save";
|
|
1415
|
+
}
|
|
1416
|
+
set saveLabel(value) {
|
|
1417
|
+
if (value === undefined)
|
|
1418
|
+
this.removeAttribute("save-label");
|
|
1419
|
+
else
|
|
1420
|
+
this.setAttribute("save-label", value);
|
|
1421
|
+
}
|
|
1422
|
+
/** Text rendered by the delete action. Defaults to `"Delete"`. */
|
|
1423
|
+
get deleteLabel() {
|
|
1424
|
+
return this.getAttribute("delete-label") ?? "Delete";
|
|
1425
|
+
}
|
|
1426
|
+
set deleteLabel(value) {
|
|
1427
|
+
if (value === undefined)
|
|
1428
|
+
this.removeAttribute("delete-label");
|
|
1429
|
+
else
|
|
1430
|
+
this.setAttribute("delete-label", value);
|
|
1431
|
+
}
|
|
1432
|
+
/** Text rendered when no feature is selected. Defaults to `"No selection"`. */
|
|
1433
|
+
get noSelectionLabel() {
|
|
1434
|
+
return this.getAttribute("no-selection-label") ?? "No selection";
|
|
1435
|
+
}
|
|
1436
|
+
set noSelectionLabel(value) {
|
|
1437
|
+
if (value === undefined)
|
|
1438
|
+
this.removeAttribute("no-selection-label");
|
|
1439
|
+
else
|
|
1440
|
+
this.setAttribute("no-selection-label", value);
|
|
1441
|
+
}
|
|
1442
|
+
/** Text rendered for a read-only editor without a model-supplied reason. Defaults to `"Read-only"`. */
|
|
1443
|
+
get readOnlyLabel() {
|
|
1444
|
+
return this.getAttribute("read-only-label") ?? "Read-only";
|
|
1445
|
+
}
|
|
1446
|
+
set readOnlyLabel(value) {
|
|
1447
|
+
if (value === undefined)
|
|
1448
|
+
this.removeAttribute("read-only-label");
|
|
1449
|
+
else
|
|
1450
|
+
this.setAttribute("read-only-label", value);
|
|
1451
|
+
}
|
|
1452
|
+
/** Text rendered for an editable editor. Defaults to `"Editable"`. */
|
|
1453
|
+
get editableLabel() {
|
|
1454
|
+
return this.getAttribute("editable-label") ?? "Editable";
|
|
1455
|
+
}
|
|
1456
|
+
set editableLabel(value) {
|
|
1457
|
+
if (value === undefined)
|
|
1458
|
+
this.removeAttribute("editable-label");
|
|
1459
|
+
else
|
|
1460
|
+
this.setAttribute("editable-label", value);
|
|
1461
|
+
}
|
|
1010
1462
|
attributeChangedCallback() {
|
|
1011
1463
|
this.resolveControllerFromContext();
|
|
1012
1464
|
this.render();
|
|
@@ -1017,19 +1469,30 @@ export class HonuaEditorElement extends HonuaElementBase {
|
|
|
1017
1469
|
const selected = state?.selection?.feature;
|
|
1018
1470
|
const canUpdate = model.capabilities.canUpdate && !model.capabilities.readOnly;
|
|
1019
1471
|
const label = this.getAttribute("label") ?? "Editor";
|
|
1472
|
+
const newLabel = this.newLabel;
|
|
1473
|
+
const saveLabel = this.saveLabel;
|
|
1474
|
+
const deleteLabel = this.deleteLabel;
|
|
1475
|
+
const noSelectionLabel = this.noSelectionLabel;
|
|
1476
|
+
const readOnlyLabel = this.readOnlyLabel;
|
|
1477
|
+
const editableLabel = this.editableLabel;
|
|
1478
|
+
const statusLabel = this.#messages.status?.[model.status] ?? model.status;
|
|
1479
|
+
const readOnlyReason = this.#messages.readOnlyReason;
|
|
1480
|
+
const readOnlyMessage = typeof readOnlyReason === "function"
|
|
1481
|
+
? readOnlyReason(model.capabilities.reason)
|
|
1482
|
+
: (readOnlyReason ?? model.capabilities.reason ?? readOnlyLabel);
|
|
1020
1483
|
this.setShadowHtml(`
|
|
1021
1484
|
<style>${baseStyles()}${editorStyles()}</style>
|
|
1022
1485
|
<section class="editor" part="panel" aria-label="${escapeHtml(label)}">
|
|
1023
1486
|
<div class="editor__bar">
|
|
1024
1487
|
<h2>${escapeHtml(label)}</h2>
|
|
1025
|
-
<span data-status>${escapeHtml(
|
|
1488
|
+
<span data-status>${escapeHtml(statusLabel)}</span>
|
|
1026
1489
|
</div>
|
|
1027
|
-
<p class="selection">${escapeHtml(selected?.title ??
|
|
1028
|
-
<p class="muted">${escapeHtml(model.capabilities.readOnly ?
|
|
1490
|
+
<p class="selection">${escapeHtml(selected?.title ?? noSelectionLabel)}</p>
|
|
1491
|
+
<p class="muted">${escapeHtml(model.capabilities.readOnly ? readOnlyMessage : editableLabel)}</p>
|
|
1029
1492
|
<div class="editor__actions">
|
|
1030
|
-
<button type="button" data-action="new" ${model.capabilities.canCreate && !model.capabilities.readOnly ? "" : "disabled"}
|
|
1031
|
-
<button type="button" data-action="save" ${canUpdate ? "" : "disabled"}
|
|
1032
|
-
<button type="button" data-action="delete" ${model.capabilities.canDelete && !model.capabilities.readOnly ? "" : "disabled"}
|
|
1493
|
+
<button type="button" data-action="new" ${model.capabilities.canCreate && !model.capabilities.readOnly ? "" : "disabled"}>${escapeHtml(newLabel)}</button>
|
|
1494
|
+
<button type="button" data-action="save" ${canUpdate ? "" : "disabled"}>${escapeHtml(saveLabel)}</button>
|
|
1495
|
+
<button type="button" data-action="delete" ${model.capabilities.canDelete && !model.capabilities.readOnly ? "" : "disabled"}>${escapeHtml(deleteLabel)}</button>
|
|
1033
1496
|
</div>
|
|
1034
1497
|
</section>
|
|
1035
1498
|
`);
|
|
@@ -1062,6 +1525,14 @@ export class HonuaChartElement extends HonuaElementBase {
|
|
|
1062
1525
|
return ["for", "label"];
|
|
1063
1526
|
}
|
|
1064
1527
|
#model;
|
|
1528
|
+
#messages = {};
|
|
1529
|
+
get messages() {
|
|
1530
|
+
return this.#messages;
|
|
1531
|
+
}
|
|
1532
|
+
set messages(messages) {
|
|
1533
|
+
this.#messages = messages ?? {};
|
|
1534
|
+
this.render();
|
|
1535
|
+
}
|
|
1065
1536
|
get chartModel() {
|
|
1066
1537
|
return this.#model ?? this.state?.chart;
|
|
1067
1538
|
}
|
|
@@ -1074,24 +1545,28 @@ export class HonuaChartElement extends HonuaElementBase {
|
|
|
1074
1545
|
this.render();
|
|
1075
1546
|
}
|
|
1076
1547
|
render() {
|
|
1077
|
-
const model = this.chartModel ?? defaultChartModel(this.getAttribute("label") ?? "Chart");
|
|
1548
|
+
const model = this.chartModel ?? defaultChartModel(this.#messages.label ?? this.getAttribute("label") ?? "Chart");
|
|
1549
|
+
const title = this.#messages.label ?? model.title;
|
|
1078
1550
|
const max = Math.max(1, ...(model.data ?? []).map((datum) => datum.value));
|
|
1551
|
+
const barStyles = (model.data ?? [])
|
|
1552
|
+
.map((datum, index) => `.bar-${index} { --bar: ${(datum.value / max) * 100}%; --bar-color: ${escapeAttribute(datum.color ?? "#2563eb")}; }`)
|
|
1553
|
+
.join("\n");
|
|
1079
1554
|
this.setShadowHtml(`
|
|
1080
|
-
<style>${baseStyles()}${chartStyles()}</style>
|
|
1081
|
-
<section class="chart" part="panel" aria-label="${escapeHtml(
|
|
1082
|
-
<h2>${escapeHtml(
|
|
1555
|
+
<style>${baseStyles()}${chartStyles()}${barStyles}</style>
|
|
1556
|
+
<section class="chart" part="panel" aria-label="${escapeHtml(title)}">
|
|
1557
|
+
<h2>${escapeHtml(title)}</h2>
|
|
1083
1558
|
<div class="bars">
|
|
1084
1559
|
${model.data?.length
|
|
1085
1560
|
? model.data
|
|
1086
|
-
.map((datum) => `
|
|
1561
|
+
.map((datum, index) => `
|
|
1087
1562
|
<div class="bar-row">
|
|
1088
1563
|
<span>${escapeHtml(datum.label)}</span>
|
|
1089
|
-
<span class="bar
|
|
1564
|
+
<span class="bar bar-${index}"></span>
|
|
1090
1565
|
<strong>${escapeHtml(String(datum.value))}</strong>
|
|
1091
1566
|
</div>
|
|
1092
1567
|
`)
|
|
1093
1568
|
.join("")
|
|
1094
|
-
: `<p class="empty">${escapeHtml(model.message ?? "No chart data")}</p>`}
|
|
1569
|
+
: `<p class="empty">${escapeHtml(this.#messages.noData ?? model.message ?? "No chart data")}</p>`}
|
|
1095
1570
|
</div>
|
|
1096
1571
|
</section>
|
|
1097
1572
|
`);
|
|
@@ -1102,24 +1577,32 @@ export class HonuaBasemapControlElement extends HonuaElementBase {
|
|
|
1102
1577
|
return ["for", "label"];
|
|
1103
1578
|
}
|
|
1104
1579
|
#activeBasemapId;
|
|
1580
|
+
#messages = {};
|
|
1581
|
+
get messages() {
|
|
1582
|
+
return this.#messages;
|
|
1583
|
+
}
|
|
1584
|
+
set messages(messages) {
|
|
1585
|
+
this.#messages = messages ?? {};
|
|
1586
|
+
this.render();
|
|
1587
|
+
}
|
|
1105
1588
|
attributeChangedCallback() {
|
|
1106
1589
|
this.resolveControllerFromContext();
|
|
1107
1590
|
this.render();
|
|
1108
1591
|
}
|
|
1109
1592
|
render() {
|
|
1110
|
-
const label = this.getAttribute("label") ?? "Basemaps";
|
|
1593
|
+
const label = this.#messages.label ?? this.getAttribute("label") ?? "Basemaps";
|
|
1111
1594
|
const basemaps = basemapsFromState(this.state);
|
|
1112
1595
|
const active = this.#activeBasemapId ?? basemaps.find((basemap) => basemap.visible)?.id ?? basemaps[0]?.id;
|
|
1113
1596
|
this.setShadowHtml(`
|
|
1114
|
-
<style>${baseStyles()}${controlPanelStyles()}</style>
|
|
1597
|
+
<style>${baseStyles()}${controlPanelStyles()}${basemapControlStyles()}</style>
|
|
1115
1598
|
<section class="control-panel" part="panel" aria-label="${escapeHtml(label)}">
|
|
1116
1599
|
<div class="control-panel__bar">
|
|
1117
1600
|
<h2>${escapeHtml(label)}</h2>
|
|
1118
|
-
<span>${escapeHtml(basemaps.length >= 2 ? "ready" : "unsupported")}</span>
|
|
1601
|
+
<span>${escapeHtml(this.#messages.status?.[basemaps.length >= 2 ? "ready" : "unsupported"] ?? (basemaps.length >= 2 ? "ready" : "unsupported"))}</span>
|
|
1119
1602
|
</div>
|
|
1120
1603
|
${basemaps.length < 2
|
|
1121
|
-
? `<p class="empty" role="status"
|
|
1122
|
-
: `<div class="segmented" role="group" aria-label="${escapeAttribute(label)}">
|
|
1604
|
+
? `<p class="empty" role="status">${escapeHtml(this.#messages.unavailable ?? this.#messages.unsupported ?? "At least two basemap layers are required.")}</p>`
|
|
1605
|
+
: `<div class="segmented" role="group" aria-label="${escapeAttribute(this.#messages.groupLabel ?? label)}">
|
|
1123
1606
|
${basemaps
|
|
1124
1607
|
.map((basemap) => `
|
|
1125
1608
|
<button type="button" data-basemap-id="${escapeAttribute(basemap.id)}" aria-pressed="${String(basemap.id === active)}">${escapeHtml(basemap.title)}</button>
|
|
@@ -1152,6 +1635,14 @@ export class HonuaBookmarksElement extends HonuaElementBase {
|
|
|
1152
1635
|
static get observedAttributes() {
|
|
1153
1636
|
return ["for", "label", "bookmarks"];
|
|
1154
1637
|
}
|
|
1638
|
+
#messages = {};
|
|
1639
|
+
get messages() {
|
|
1640
|
+
return this.#messages;
|
|
1641
|
+
}
|
|
1642
|
+
set messages(messages) {
|
|
1643
|
+
this.#messages = messages ?? {};
|
|
1644
|
+
this.render();
|
|
1645
|
+
}
|
|
1155
1646
|
attributeChangedCallback() {
|
|
1156
1647
|
this.resolveControllerFromContext();
|
|
1157
1648
|
this.render();
|
|
@@ -1159,15 +1650,17 @@ export class HonuaBookmarksElement extends HonuaElementBase {
|
|
|
1159
1650
|
render() {
|
|
1160
1651
|
const label = this.getAttribute("label") ?? "Bookmarks";
|
|
1161
1652
|
const bookmarks = this.bookmarks();
|
|
1653
|
+
const statusLabel = this.#messages.status?.[bookmarks.length > 0 ? "ready" : "unsupported"] ??
|
|
1654
|
+
(bookmarks.length > 0 ? "ready" : "unsupported");
|
|
1162
1655
|
this.setShadowHtml(`
|
|
1163
1656
|
<style>${baseStyles()}${controlPanelStyles()}</style>
|
|
1164
1657
|
<section class="control-panel" part="panel" aria-label="${escapeHtml(label)}">
|
|
1165
1658
|
<div class="control-panel__bar">
|
|
1166
1659
|
<h2>${escapeHtml(label)}</h2>
|
|
1167
|
-
<span>${escapeHtml(
|
|
1660
|
+
<span>${escapeHtml(statusLabel)}</span>
|
|
1168
1661
|
</div>
|
|
1169
1662
|
${bookmarks.length === 0
|
|
1170
|
-
? `<p class="empty" role="status"
|
|
1663
|
+
? `<p class="empty" role="status">${escapeHtml(this.#messages.empty ?? "No initial view or bookmarks are available.")}</p>`
|
|
1171
1664
|
: `<div class="button-stack">
|
|
1172
1665
|
${bookmarks
|
|
1173
1666
|
.map((bookmark) => `
|
|
@@ -1188,7 +1681,12 @@ export class HonuaBookmarksElement extends HonuaElementBase {
|
|
|
1188
1681
|
bookmarks() {
|
|
1189
1682
|
const configured = parseBookmarks(this.getAttribute("bookmarks"));
|
|
1190
1683
|
const home = this.state?.mapPackage?.initialView;
|
|
1191
|
-
return [
|
|
1684
|
+
return [
|
|
1685
|
+
...(home
|
|
1686
|
+
? [{ id: "home", label: this.#messages.homeLabel ?? "Home", viewport: home }]
|
|
1687
|
+
: []),
|
|
1688
|
+
...configured,
|
|
1689
|
+
];
|
|
1192
1690
|
}
|
|
1193
1691
|
goToBookmark(bookmark) {
|
|
1194
1692
|
this.controller?.setViewport(bookmark.viewport);
|
|
@@ -1205,6 +1703,15 @@ export class HonuaLocateControlElement extends HonuaElementBase {
|
|
|
1205
1703
|
}
|
|
1206
1704
|
#status = "idle";
|
|
1207
1705
|
#message = "";
|
|
1706
|
+
#messages = {};
|
|
1707
|
+
/** Caller-supplied localized labels and state messages. */
|
|
1708
|
+
get messages() {
|
|
1709
|
+
return this.#messages;
|
|
1710
|
+
}
|
|
1711
|
+
set messages(messages) {
|
|
1712
|
+
this.#messages = messages ?? {};
|
|
1713
|
+
this.render();
|
|
1714
|
+
}
|
|
1208
1715
|
attributeChangedCallback() {
|
|
1209
1716
|
this.resolveControllerFromContext();
|
|
1210
1717
|
this.render();
|
|
@@ -1213,15 +1720,20 @@ export class HonuaLocateControlElement extends HonuaElementBase {
|
|
|
1213
1720
|
const label = this.getAttribute("label") ?? "Locate";
|
|
1214
1721
|
const supported = this.hasConfiguredLocation() || Boolean(globalThis.navigator?.geolocation);
|
|
1215
1722
|
const status = supported ? this.#status : "unsupported";
|
|
1723
|
+
const statusLabel = this.#messages.status?.[status] ?? status;
|
|
1724
|
+
const message = this.#message ||
|
|
1725
|
+
(supported
|
|
1726
|
+
? (this.#messages.initial ?? "Centers the shared map controller.")
|
|
1727
|
+
: (this.#messages.unavailable ?? "Geolocation is unavailable."));
|
|
1216
1728
|
this.setShadowHtml(`
|
|
1217
1729
|
<style>${baseStyles()}${controlPanelStyles()}</style>
|
|
1218
1730
|
<section class="control-panel" part="panel" aria-label="${escapeHtml(label)}">
|
|
1219
1731
|
<div class="control-panel__bar">
|
|
1220
1732
|
<h2>${escapeHtml(label)}</h2>
|
|
1221
|
-
<span>${escapeHtml(
|
|
1733
|
+
<span>${escapeHtml(statusLabel)}</span>
|
|
1222
1734
|
</div>
|
|
1223
|
-
<button type="button" data-locate ${supported ? "" : "disabled"}
|
|
1224
|
-
<p class="empty" role="status">${escapeHtml(
|
|
1735
|
+
<button type="button" data-locate="action" ${supported ? "" : "disabled"}>${escapeHtml(this.#messages.actionLabel ?? "Use location")}</button>
|
|
1736
|
+
<p class="empty" role="status">${escapeHtml(message)}</p>
|
|
1225
1737
|
</section>
|
|
1226
1738
|
`);
|
|
1227
1739
|
this.shadowRoot?.querySelector("button[data-locate]")?.addEventListener("click", () => {
|
|
@@ -1237,7 +1749,7 @@ export class HonuaLocateControlElement extends HonuaElementBase {
|
|
|
1237
1749
|
const geolocation = globalThis.navigator?.geolocation;
|
|
1238
1750
|
if (!geolocation) {
|
|
1239
1751
|
this.#status = "unsupported";
|
|
1240
|
-
this.#message = "Geolocation is unavailable.";
|
|
1752
|
+
this.#message = this.#messages.unavailable ?? "Geolocation is unavailable.";
|
|
1241
1753
|
this.dispatchTypedEvent("honua-locate-change", {
|
|
1242
1754
|
status: "unsupported",
|
|
1243
1755
|
message: this.#message,
|
|
@@ -1246,7 +1758,7 @@ export class HonuaLocateControlElement extends HonuaElementBase {
|
|
|
1246
1758
|
return;
|
|
1247
1759
|
}
|
|
1248
1760
|
this.#status = "loading";
|
|
1249
|
-
this.#message = "Requesting location.";
|
|
1761
|
+
this.#message = this.#messages.requesting ?? "Requesting location.";
|
|
1250
1762
|
this.render();
|
|
1251
1763
|
geolocation.getCurrentPosition((position) => {
|
|
1252
1764
|
this.applyLocation({
|
|
@@ -1255,7 +1767,7 @@ export class HonuaLocateControlElement extends HonuaElementBase {
|
|
|
1255
1767
|
});
|
|
1256
1768
|
}, (error) => {
|
|
1257
1769
|
this.#status = "error";
|
|
1258
|
-
this.#message = error.message;
|
|
1770
|
+
this.#message = this.#messages.error?.(error) ?? error.message;
|
|
1259
1771
|
this.dispatchTypedEvent("honua-locate-change", {
|
|
1260
1772
|
status: "error",
|
|
1261
1773
|
error,
|
|
@@ -1266,7 +1778,7 @@ export class HonuaLocateControlElement extends HonuaElementBase {
|
|
|
1266
1778
|
}
|
|
1267
1779
|
applyLocation(viewport) {
|
|
1268
1780
|
this.#status = "ready";
|
|
1269
|
-
this.#message = "Location applied.";
|
|
1781
|
+
this.#message = this.#messages.applied ?? "Location applied.";
|
|
1270
1782
|
this.controller?.setViewport(viewport);
|
|
1271
1783
|
this.dispatchTypedEvent("honua-locate-change", { status: "ready", viewport });
|
|
1272
1784
|
this.dispatchTypedEvent("honua-viewport-change", viewport);
|
|
@@ -1301,6 +1813,15 @@ export class HonuaMeasureControlElement extends HonuaElementBase {
|
|
|
1301
1813
|
return ["for", "label"];
|
|
1302
1814
|
}
|
|
1303
1815
|
#mode = "off";
|
|
1816
|
+
#messages = {};
|
|
1817
|
+
/** Caller-supplied localized status, action, and empty-state messages. */
|
|
1818
|
+
get messages() {
|
|
1819
|
+
return this.#messages;
|
|
1820
|
+
}
|
|
1821
|
+
set messages(messages) {
|
|
1822
|
+
this.#messages = messages ?? {};
|
|
1823
|
+
this.render();
|
|
1824
|
+
}
|
|
1304
1825
|
attributeChangedCallback() {
|
|
1305
1826
|
this.resolveControllerFromContext();
|
|
1306
1827
|
this.render();
|
|
@@ -1311,23 +1832,25 @@ export class HonuaMeasureControlElement extends HonuaElementBase {
|
|
|
1311
1832
|
render() {
|
|
1312
1833
|
const label = this.getAttribute("label") ?? "Measure";
|
|
1313
1834
|
const enabled = this.controller?.canMeasure() ?? false;
|
|
1835
|
+
const status = enabled ? "ready" : "unsupported";
|
|
1314
1836
|
this.setShadowHtml(`
|
|
1315
1837
|
<style>${baseStyles()}${controlPanelStyles()}</style>
|
|
1316
1838
|
<section class="control-panel" part="panel" aria-label="${escapeHtml(label)}">
|
|
1317
1839
|
<div class="control-panel__bar">
|
|
1318
1840
|
<h2>${escapeHtml(label)}</h2>
|
|
1319
|
-
<span>${escapeHtml(
|
|
1841
|
+
<span>${escapeHtml(this.#messages.status?.[status] ?? status)}</span>
|
|
1320
1842
|
</div>
|
|
1321
1843
|
<div class="segmented" role="group" aria-label="${escapeAttribute(label)}">
|
|
1322
1844
|
${["off", "distance", "area"]
|
|
1323
1845
|
.map((mode) => `
|
|
1324
|
-
<button type="button" data-measure-mode="${mode}" aria-pressed="${String(this.#mode === mode)}"${enabled ? "" : ' aria-disabled="true" disabled'}>${escapeHtml(modeLabel(mode))}</button>
|
|
1846
|
+
<button type="button" data-measure-mode="${mode}" aria-pressed="${String(this.#mode === mode)}"${enabled ? "" : ' aria-disabled="true" disabled'}>${escapeHtml(this.#messages.actionLabels?.[mode] ?? modeLabel(mode))}</button>
|
|
1325
1847
|
`)
|
|
1326
1848
|
.join("")}
|
|
1327
1849
|
</div>
|
|
1328
1850
|
${enabled
|
|
1329
1851
|
? ""
|
|
1330
|
-
: `<p class="empty" role="status"
|
|
1852
|
+
: `<p class="empty" role="status">${escapeHtml(this.#messages.empty ??
|
|
1853
|
+
"Measurement is disabled because no geometry provider is configured. Pass a `measurementGeometry` provider to the controller to enable distance and area measuring.")}</p>`}
|
|
1331
1854
|
</section>
|
|
1332
1855
|
`);
|
|
1333
1856
|
if (!enabled)
|
|
@@ -1345,7 +1868,8 @@ export class HonuaMeasureControlElement extends HonuaElementBase {
|
|
|
1345
1868
|
: {
|
|
1346
1869
|
mode,
|
|
1347
1870
|
status: "unsupported",
|
|
1348
|
-
message:
|
|
1871
|
+
message: this.#messages.unsupportedMessage ??
|
|
1872
|
+
"Measurement requires a geometry provider. Configure `measurementGeometry` on the controller to enable measuring.",
|
|
1349
1873
|
};
|
|
1350
1874
|
this.dispatchTypedEvent("honua-measure-change", detail);
|
|
1351
1875
|
this.render();
|
|
@@ -1364,6 +1888,15 @@ export class HonuaSketchControlElement extends HonuaElementBase {
|
|
|
1364
1888
|
return ["for", "label"];
|
|
1365
1889
|
}
|
|
1366
1890
|
#mode = "off";
|
|
1891
|
+
#messages = {};
|
|
1892
|
+
/** Caller-supplied localized status, action, and empty-state messages. */
|
|
1893
|
+
get messages() {
|
|
1894
|
+
return this.#messages;
|
|
1895
|
+
}
|
|
1896
|
+
set messages(messages) {
|
|
1897
|
+
this.#messages = messages ?? {};
|
|
1898
|
+
this.render();
|
|
1899
|
+
}
|
|
1367
1900
|
attributeChangedCallback() {
|
|
1368
1901
|
this.resolveControllerFromContext();
|
|
1369
1902
|
this.render();
|
|
@@ -1374,23 +1907,25 @@ export class HonuaSketchControlElement extends HonuaElementBase {
|
|
|
1374
1907
|
render() {
|
|
1375
1908
|
const label = this.getAttribute("label") ?? "Sketch";
|
|
1376
1909
|
const enabled = this.controller?.canSketch() ?? false;
|
|
1910
|
+
const status = enabled ? "ready" : "unsupported";
|
|
1377
1911
|
this.setShadowHtml(`
|
|
1378
1912
|
<style>${baseStyles()}${controlPanelStyles()}</style>
|
|
1379
1913
|
<section class="control-panel" part="panel" aria-label="${escapeHtml(label)}">
|
|
1380
1914
|
<div class="control-panel__bar">
|
|
1381
1915
|
<h2>${escapeHtml(label)}</h2>
|
|
1382
|
-
<span>${escapeHtml(
|
|
1916
|
+
<span>${escapeHtml(this.#messages.status?.[status] ?? status)}</span>
|
|
1383
1917
|
</div>
|
|
1384
1918
|
<div class="segmented" role="group" aria-label="${escapeAttribute(label)}">
|
|
1385
1919
|
${["off", "point", "line", "polygon"]
|
|
1386
1920
|
.map((mode) => `
|
|
1387
|
-
<button type="button" data-sketch-mode="${mode}" aria-pressed="${String(this.#mode === mode)}"${enabled ? "" : ' aria-disabled="true" disabled'}>${escapeHtml(modeLabel(mode))}</button>
|
|
1921
|
+
<button type="button" data-sketch-mode="${mode}" aria-pressed="${String(this.#mode === mode)}"${enabled ? "" : ' aria-disabled="true" disabled'}>${escapeHtml(this.#messages.actionLabels?.[mode] ?? modeLabel(mode))}</button>
|
|
1388
1922
|
`)
|
|
1389
1923
|
.join("")}
|
|
1390
1924
|
</div>
|
|
1391
1925
|
${enabled
|
|
1392
1926
|
? ""
|
|
1393
|
-
: `<p class="empty" role="status"
|
|
1927
|
+
: `<p class="empty" role="status">${escapeHtml(this.#messages.empty ??
|
|
1928
|
+
"Sketching is disabled because no geometry provider is configured. Pass a `sketchGeometry` provider to the controller to enable point, line, and polygon drawing.")}</p>`}
|
|
1394
1929
|
</section>
|
|
1395
1930
|
`);
|
|
1396
1931
|
if (!enabled)
|
|
@@ -1408,79 +1943,283 @@ export class HonuaSketchControlElement extends HonuaElementBase {
|
|
|
1408
1943
|
: {
|
|
1409
1944
|
mode,
|
|
1410
1945
|
status: "unsupported",
|
|
1411
|
-
message:
|
|
1946
|
+
message: this.#messages.unsupportedMessage ??
|
|
1947
|
+
"Sketching requires a geometry provider. Configure `sketchGeometry` on the controller to enable drawing.",
|
|
1412
1948
|
};
|
|
1413
1949
|
this.dispatchTypedEvent("honua-sketch-change", detail);
|
|
1414
1950
|
this.render();
|
|
1415
1951
|
}
|
|
1416
1952
|
}
|
|
1953
|
+
/** Legacy `honua-export` event `format` label for each export kind. */
|
|
1954
|
+
const EXPORT_FORMAT_BY_KIND = {
|
|
1955
|
+
print: "print",
|
|
1956
|
+
snapshot: "png",
|
|
1957
|
+
state: "json",
|
|
1958
|
+
};
|
|
1959
|
+
const EXPORT_KIND_BY_FORMAT = {
|
|
1960
|
+
print: "print",
|
|
1961
|
+
png: "snapshot",
|
|
1962
|
+
json: "state",
|
|
1963
|
+
};
|
|
1964
|
+
/** Maps an export outcome onto the shared component-status vocabulary. */
|
|
1965
|
+
const EXPORT_COMPONENT_STATUS = {
|
|
1966
|
+
ready: "ready",
|
|
1967
|
+
unsupported: "unsupported",
|
|
1968
|
+
cancelled: "idle",
|
|
1969
|
+
error: "error",
|
|
1970
|
+
};
|
|
1971
|
+
/**
|
|
1972
|
+
* `<honua-print-export>` — print, snapshot, and sanitized state export.
|
|
1973
|
+
*
|
|
1974
|
+
* Assign {@link HonuaPrintExportElement.exportAdapter} to enable snapshot and
|
|
1975
|
+
* state export. Without an adapter both **fail closed** (issue #683): the
|
|
1976
|
+
* buttons render disabled with an explanation, and a programmatic
|
|
1977
|
+
* `requestExport()` resolves to an `unsupported` result carrying a
|
|
1978
|
+
* `HonuaCapabilityNotSupportedError` — never a blank image or a
|
|
1979
|
+
* partially-credentialed JSON document. Browser print keeps working with no
|
|
1980
|
+
* adapter, because `window.print()` reads no pixels and serializes no state.
|
|
1981
|
+
*/
|
|
1417
1982
|
export class HonuaPrintExportElement extends HonuaElementBase {
|
|
1983
|
+
#exportAdapter;
|
|
1984
|
+
#lastResult;
|
|
1985
|
+
#inFlight;
|
|
1986
|
+
#messages = {};
|
|
1987
|
+
/** Monotonic request generation; only the newest export may publish. */
|
|
1988
|
+
#exportGeneration = 0;
|
|
1418
1989
|
static get observedAttributes() {
|
|
1419
1990
|
return ["for", "label", "title"];
|
|
1420
1991
|
}
|
|
1992
|
+
/**
|
|
1993
|
+
* The application-supplied export adapter. Setting it re-renders so the
|
|
1994
|
+
* snapshot/state affordances become enabled; clearing it re-disables them.
|
|
1995
|
+
*/
|
|
1996
|
+
get exportAdapter() {
|
|
1997
|
+
return this.#exportAdapter;
|
|
1998
|
+
}
|
|
1999
|
+
set exportAdapter(adapter) {
|
|
2000
|
+
if (this.#exportAdapter === adapter)
|
|
2001
|
+
return;
|
|
2002
|
+
this.#exportAdapter = adapter;
|
|
2003
|
+
this.render();
|
|
2004
|
+
}
|
|
2005
|
+
/** Caller-supplied localized status, action, and unsupported messages. */
|
|
2006
|
+
get messages() {
|
|
2007
|
+
return this.#messages;
|
|
2008
|
+
}
|
|
2009
|
+
set messages(messages) {
|
|
2010
|
+
this.#messages = messages ?? {};
|
|
2011
|
+
this.render();
|
|
2012
|
+
}
|
|
2013
|
+
/** The most recent export result, for callers that prefer polling to events. */
|
|
2014
|
+
get lastExportResult() {
|
|
2015
|
+
return this.#lastResult;
|
|
2016
|
+
}
|
|
1421
2017
|
attributeChangedCallback() {
|
|
1422
2018
|
this.resolveControllerFromContext();
|
|
1423
2019
|
this.render();
|
|
1424
2020
|
}
|
|
2021
|
+
disconnectedCallback() {
|
|
2022
|
+
// An in-flight export outlives nothing: disconnecting cancels it so a
|
|
2023
|
+
// detached element cannot keep an adapter (or a server-side render job)
|
|
2024
|
+
// alive (REQ-005 deterministic disposal).
|
|
2025
|
+
this.#inFlight?.abort();
|
|
2026
|
+
this.#inFlight = undefined;
|
|
2027
|
+
// Bump the generation too: an abort-ignoring adapter's continuation must
|
|
2028
|
+
// not publish onto a detached element either.
|
|
2029
|
+
this.#exportGeneration += 1;
|
|
2030
|
+
super.disconnectedCallback();
|
|
2031
|
+
}
|
|
2032
|
+
/** Kinds that are actually available right now, given the assigned adapter. */
|
|
2033
|
+
availableExportKinds() {
|
|
2034
|
+
const adapter = this.#resolveAdapter();
|
|
2035
|
+
if (!adapter)
|
|
2036
|
+
return [];
|
|
2037
|
+
try {
|
|
2038
|
+
return adapter.describeCapabilities().kinds.filter((kind) => HONUA_EXPORT_KINDS.includes(kind));
|
|
2039
|
+
}
|
|
2040
|
+
catch {
|
|
2041
|
+
return [];
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
/**
|
|
2045
|
+
* Runs one export. Resolves with the full result envelope (never throws) and
|
|
2046
|
+
* dispatches `honua-export` with a redacted projection of it.
|
|
2047
|
+
*/
|
|
2048
|
+
async requestExport(kind) {
|
|
2049
|
+
this.#inFlight?.abort();
|
|
2050
|
+
const controller = typeof AbortController === "function" ? new AbortController() : undefined;
|
|
2051
|
+
this.#inFlight = controller;
|
|
2052
|
+
// An AbortSignal is advisory: an adapter is free to ignore it and settle
|
|
2053
|
+
// later, so aborting the previous export does not guarantee it stays quiet.
|
|
2054
|
+
// Ownership of the element's visible state therefore belongs to a
|
|
2055
|
+
// generation, not to a controller — a completion that no longer owns the
|
|
2056
|
+
// element neither publishes a result nor dispatches an event, so a slow
|
|
2057
|
+
// first export cannot overwrite a newer one that already finished.
|
|
2058
|
+
this.#exportGeneration += 1;
|
|
2059
|
+
const generation = this.#exportGeneration;
|
|
2060
|
+
const title = this.getAttribute("title") ?? this.state?.packageId ?? undefined;
|
|
2061
|
+
const result = await runHonuaExport({
|
|
2062
|
+
kind,
|
|
2063
|
+
adapter: this.#resolveAdapter(kind),
|
|
2064
|
+
state: this.state,
|
|
2065
|
+
...(title ? { title } : {}),
|
|
2066
|
+
...(controller ? { signal: controller.signal } : {}),
|
|
2067
|
+
});
|
|
2068
|
+
if (this.#inFlight === controller)
|
|
2069
|
+
this.#inFlight = undefined;
|
|
2070
|
+
if (generation !== this.#exportGeneration) {
|
|
2071
|
+
// Superseded. The caller still gets its own result — it asked for this
|
|
2072
|
+
// export and is entitled to the outcome — but the element does not adopt
|
|
2073
|
+
// it, and any resource the adapter handed back is released here, since
|
|
2074
|
+
// nothing else now holds a reference to release it.
|
|
2075
|
+
void result.release();
|
|
2076
|
+
return result;
|
|
2077
|
+
}
|
|
2078
|
+
this.#lastResult = result;
|
|
2079
|
+
this.dispatchTypedEvent("honua-export", exportDetailFromResult(result, title));
|
|
2080
|
+
this.render();
|
|
2081
|
+
return result;
|
|
2082
|
+
}
|
|
2083
|
+
/**
|
|
2084
|
+
* Resolves the adapter for `kind`. The built-in browser-print adapter is used
|
|
2085
|
+
* for `print` only when the application supplied nothing — snapshot and state
|
|
2086
|
+
* have no built-in fallback by design.
|
|
2087
|
+
*/
|
|
2088
|
+
#resolveAdapter(kind) {
|
|
2089
|
+
if (this.#exportAdapter)
|
|
2090
|
+
return this.#exportAdapter;
|
|
2091
|
+
if (kind !== undefined && kind !== "print")
|
|
2092
|
+
return undefined;
|
|
2093
|
+
const printAdapter = createBrowserPrintExportAdapter();
|
|
2094
|
+
return printAdapter.describeCapabilities().kinds.length > 0 ? printAdapter : undefined;
|
|
2095
|
+
}
|
|
1425
2096
|
render() {
|
|
1426
2097
|
const label = this.getAttribute("label") ?? "Print and export";
|
|
2098
|
+
const available = new Set(this.availableExportKinds());
|
|
2099
|
+
const status = available.size === 0 ? "unsupported" : "ready";
|
|
2100
|
+
const last = this.#lastResult;
|
|
2101
|
+
const buttons = ["print", "snapshot", "state"]
|
|
2102
|
+
.map((kind) => {
|
|
2103
|
+
const enabled = available.has(kind);
|
|
2104
|
+
const actionLabel = this.#messages.actionLabels?.[kind] ?? EXPORT_LABELS[kind];
|
|
2105
|
+
const title = enabled
|
|
2106
|
+
? `${actionLabel} export`
|
|
2107
|
+
: (this.#messages.unavailable?.[kind] ??
|
|
2108
|
+
`${actionLabel} export requires an application-supplied export adapter.`);
|
|
2109
|
+
// Deliberately neither `disabled` nor `aria-disabled`, even when no
|
|
2110
|
+
// adapter is assigned. Both suppress activation — assistive technology
|
|
2111
|
+
// skips a disabled control, and Playwright treats either as
|
|
2112
|
+
// non-actionable — which would silence the `honua-export` event that is
|
|
2113
|
+
// how an application *discovers* it must supply an adapter. The
|
|
2114
|
+
// unavailability is conveyed where it can still be read and acted on:
|
|
2115
|
+
// an explanatory `title` on the control and the live-region readout
|
|
2116
|
+
// below. Fail-closed is unaffected — activating it runs the export,
|
|
2117
|
+
// which returns `unsupported` with no bytes.
|
|
2118
|
+
return `<button type="button" data-export-format="${EXPORT_FORMAT_BY_KIND[kind]}" data-export-kind="${kind}"${enabled ? "" : ' data-export-unavailable="true"'} title="${escapeHtml(title)}">${escapeHtml(actionLabel)}</button>`;
|
|
2119
|
+
})
|
|
2120
|
+
.join("");
|
|
1427
2121
|
this.setShadowHtml(`
|
|
1428
2122
|
<style>${baseStyles()}${controlPanelStyles()}</style>
|
|
1429
2123
|
<section class="control-panel" part="panel" aria-label="${escapeHtml(label)}">
|
|
1430
2124
|
<div class="control-panel__bar">
|
|
1431
2125
|
<h2>${escapeHtml(label)}</h2>
|
|
1432
|
-
<span>${escapeHtml(
|
|
1433
|
-
</div>
|
|
1434
|
-
<div class="button-stack">
|
|
1435
|
-
<button type="button" data-export-format="print">Print</button>
|
|
1436
|
-
<button type="button" data-export-format="png">Snapshot</button>
|
|
1437
|
-
<button type="button" data-export-format="json">State JSON</button>
|
|
2126
|
+
<span>${escapeHtml(this.#messages.status?.[status] ?? status)}</span>
|
|
1438
2127
|
</div>
|
|
2128
|
+
<div class="button-stack">${buttons}</div>
|
|
2129
|
+
<p part="status" role="status" aria-live="polite">${escapeHtml(exportStatusText(last, available, this.#messages))}</p>
|
|
1439
2130
|
</section>
|
|
1440
2131
|
`);
|
|
1441
|
-
this.shadowRoot?.querySelectorAll("button[data-export-
|
|
1442
|
-
button.addEventListener("click", () =>
|
|
1443
|
-
|
|
1444
|
-
}
|
|
1445
|
-
export(format) {
|
|
1446
|
-
const title = this.getAttribute("title") ?? this.state?.packageId ?? undefined;
|
|
1447
|
-
if (format === "print" && typeof window !== "undefined" && typeof window.print === "function") {
|
|
1448
|
-
this.dispatchTypedEvent("honua-export", {
|
|
1449
|
-
format,
|
|
1450
|
-
status: "ready",
|
|
1451
|
-
...(title ? { title } : {}),
|
|
2132
|
+
this.shadowRoot?.querySelectorAll("button[data-export-kind]").forEach((button) => {
|
|
2133
|
+
button.addEventListener("click", () => {
|
|
2134
|
+
void this.requestExport(button.dataset.exportKind);
|
|
1452
2135
|
});
|
|
1453
|
-
window.print();
|
|
1454
|
-
return;
|
|
1455
|
-
}
|
|
1456
|
-
const message = format === "print"
|
|
1457
|
-
? "Window printing is unavailable."
|
|
1458
|
-
: "Snapshot export requires an application adapter to avoid leaking private map credentials.";
|
|
1459
|
-
this.dispatchTypedEvent("honua-export", {
|
|
1460
|
-
format,
|
|
1461
|
-
status: "unsupported",
|
|
1462
|
-
...(title ? { title } : {}),
|
|
1463
|
-
message,
|
|
1464
2136
|
});
|
|
1465
2137
|
}
|
|
1466
2138
|
}
|
|
2139
|
+
const EXPORT_LABELS = {
|
|
2140
|
+
print: "Print",
|
|
2141
|
+
snapshot: "Snapshot",
|
|
2142
|
+
state: "State JSON",
|
|
2143
|
+
};
|
|
2144
|
+
/**
|
|
2145
|
+
* Redacted, displayable summary of the last export attempt — or, before any
|
|
2146
|
+
* attempt, an honest statement of what is actually available. The
|
|
2147
|
+
* adapter-required message is keyed on snapshot/state specifically, not on
|
|
2148
|
+
* "nothing is available": browser print is available with no adapter, and
|
|
2149
|
+
* reporting "ready to export" while two of the three buttons are inert would be
|
|
2150
|
+
* exactly the kind of quiet degradation issue #683 exists to remove.
|
|
2151
|
+
*/
|
|
2152
|
+
function exportStatusText(result, available, messages = {}) {
|
|
2153
|
+
if (!result) {
|
|
2154
|
+
if (available.has("snapshot") && available.has("state"))
|
|
2155
|
+
return messages.ready ?? "Ready to export.";
|
|
2156
|
+
return available.size === 0
|
|
2157
|
+
? (messages.adapterRequired ?? "Assign an export adapter to enable export.")
|
|
2158
|
+
: (messages.adapterRequiredForSnapshotState ?? "Assign an export adapter to enable snapshot and state export.");
|
|
2159
|
+
}
|
|
2160
|
+
if (result.status === "ready") {
|
|
2161
|
+
const redacted = result.redactions.length > 0 ? ` ${result.redactions.length} value(s) were withheld as non-exportable.` : "";
|
|
2162
|
+
return result.sideEffectOnly
|
|
2163
|
+
? `Print layout sent to the browser.${redacted}`
|
|
2164
|
+
: `Exported ${result.filename ?? "artifact"}.${redacted}`;
|
|
2165
|
+
}
|
|
2166
|
+
return result.message ?? `Export ${result.status}.`;
|
|
2167
|
+
}
|
|
2168
|
+
/** Projects a result onto the event detail, carrying only redacted fields. */
|
|
2169
|
+
function exportDetailFromResult(result, title) {
|
|
2170
|
+
const detail = {
|
|
2171
|
+
format: EXPORT_FORMAT_BY_KIND[result.kind],
|
|
2172
|
+
kind: result.kind,
|
|
2173
|
+
status: EXPORT_COMPONENT_STATUS[result.status],
|
|
2174
|
+
exportStatus: result.status,
|
|
2175
|
+
redactionCount: result.redactions.length,
|
|
2176
|
+
};
|
|
2177
|
+
const mutable = detail;
|
|
2178
|
+
if (title)
|
|
2179
|
+
mutable.title = redactHonuaExportText(title);
|
|
2180
|
+
if (result.message)
|
|
2181
|
+
mutable.message = result.message;
|
|
2182
|
+
if (result.adapterId)
|
|
2183
|
+
mutable.adapterId = redactHonuaExportText(result.adapterId);
|
|
2184
|
+
if (result.filename)
|
|
2185
|
+
mutable.filename = result.filename;
|
|
2186
|
+
if (result.mediaType)
|
|
2187
|
+
mutable.mediaType = result.mediaType;
|
|
2188
|
+
if (result.bytes)
|
|
2189
|
+
mutable.byteLength = result.bytes.byteLength;
|
|
2190
|
+
if (result.error)
|
|
2191
|
+
mutable.errorCode = result.error.sdkCode;
|
|
2192
|
+
return detail;
|
|
2193
|
+
}
|
|
2194
|
+
/** The `format` → export-kind mapping, exported for callers on the legacy event shape. */
|
|
2195
|
+
export function honuaExportKindFromFormat(format) {
|
|
2196
|
+
return EXPORT_KIND_BY_FORMAT[format];
|
|
2197
|
+
}
|
|
1467
2198
|
export class HonuaMapStatusElement extends HonuaElementBase {
|
|
1468
2199
|
static get observedAttributes() {
|
|
1469
2200
|
return ["for", "label", "attribution"];
|
|
1470
2201
|
}
|
|
2202
|
+
#messages = {};
|
|
2203
|
+
get messages() {
|
|
2204
|
+
return this.#messages;
|
|
2205
|
+
}
|
|
2206
|
+
set messages(messages) {
|
|
2207
|
+
this.#messages = messages ?? {};
|
|
2208
|
+
this.render();
|
|
2209
|
+
}
|
|
1471
2210
|
attributeChangedCallback() {
|
|
1472
2211
|
this.resolveControllerFromContext();
|
|
1473
2212
|
this.render();
|
|
1474
2213
|
}
|
|
1475
2214
|
render() {
|
|
1476
|
-
const label = this.getAttribute("label") ?? "Map status";
|
|
2215
|
+
const label = this.#messages.label ?? this.getAttribute("label") ?? "Map status";
|
|
1477
2216
|
const attribution = this.getAttribute("attribution") ?? this.state?.mapPackage?.mapPackageId ?? "Honua";
|
|
1478
2217
|
this.setShadowHtml(`
|
|
1479
2218
|
<style>${baseStyles()}${mapStatusStyles()}</style>
|
|
1480
2219
|
<section class="map-status" part="panel" aria-label="${escapeHtml(label)}">
|
|
1481
|
-
<span aria-label="Approximate scale">${escapeHtml(approximateScale(this.state?.viewport))}</span>
|
|
1482
|
-
<span aria-label="Attribution">${escapeHtml(attribution)}</span>
|
|
1483
|
-
<button type="button" data-fullscreen
|
|
2220
|
+
<span aria-label="${escapeAttribute(this.#messages.scaleLabel ?? "Approximate scale")}">${escapeHtml(approximateScale(this.state?.viewport))}</span>
|
|
2221
|
+
<span aria-label="${escapeAttribute(this.#messages.attributionLabel ?? "Attribution")}">${escapeHtml(attribution)}</span>
|
|
2222
|
+
<button type="button" data-fullscreen="action">${escapeHtml(this.#messages.fullscreenLabel ?? "Fullscreen")}</button>
|
|
1484
2223
|
</section>
|
|
1485
2224
|
`);
|
|
1486
2225
|
this.shadowRoot?.querySelector("button[data-fullscreen]")?.addEventListener("click", () => {
|
|
@@ -1496,7 +2235,7 @@ export class HonuaMapStatusElement extends HonuaElementBase {
|
|
|
1496
2235
|
this.dispatchTypedEvent("honua-fullscreen-change", {
|
|
1497
2236
|
fullscreen: false,
|
|
1498
2237
|
status: "unsupported",
|
|
1499
|
-
message: "Fullscreen is unavailable.",
|
|
2238
|
+
message: this.#messages.fullscreenUnavailable ?? "Fullscreen is unavailable.",
|
|
1500
2239
|
});
|
|
1501
2240
|
return;
|
|
1502
2241
|
}
|
|
@@ -1511,22 +2250,32 @@ export class HonuaActionPanelElement extends HonuaElementBase {
|
|
|
1511
2250
|
static get observedAttributes() {
|
|
1512
2251
|
return ["for", "label", "actions"];
|
|
1513
2252
|
}
|
|
2253
|
+
#messages = {};
|
|
2254
|
+
/** Caller-supplied localized panel label, status, and empty-state text. */
|
|
2255
|
+
get messages() {
|
|
2256
|
+
return this.#messages;
|
|
2257
|
+
}
|
|
2258
|
+
set messages(messages) {
|
|
2259
|
+
this.#messages = messages ?? {};
|
|
2260
|
+
this.render();
|
|
2261
|
+
}
|
|
1514
2262
|
attributeChangedCallback() {
|
|
1515
2263
|
this.resolveControllerFromContext();
|
|
1516
2264
|
this.render();
|
|
1517
2265
|
}
|
|
1518
2266
|
render() {
|
|
1519
|
-
const label = this.getAttribute("label") ?? "Actions";
|
|
2267
|
+
const label = this.#messages.label ?? this.getAttribute("label") ?? "Actions";
|
|
1520
2268
|
const actions = parseActions(this.getAttribute("actions"));
|
|
2269
|
+
const status = actions.length > 0 ? "ready" : "unsupported";
|
|
1521
2270
|
this.setShadowHtml(`
|
|
1522
|
-
<style>${baseStyles()}${controlPanelStyles()}</style>
|
|
2271
|
+
<style>${baseStyles()}${controlPanelStyles()}${actionPanelStyles()}</style>
|
|
1523
2272
|
<section class="control-panel" part="panel" aria-label="${escapeHtml(label)}">
|
|
1524
2273
|
<div class="control-panel__bar">
|
|
1525
2274
|
<h2>${escapeHtml(label)}</h2>
|
|
1526
|
-
<span>${escapeHtml(
|
|
2275
|
+
<span>${escapeHtml(this.#messages.status?.[status] ?? status)}</span>
|
|
1527
2276
|
</div>
|
|
1528
2277
|
${actions.length === 0
|
|
1529
|
-
? `<p class="empty" role="status"
|
|
2278
|
+
? `<p class="empty" role="status">${escapeHtml(this.#messages.empty ?? "No actions are configured.")}</p>`
|
|
1530
2279
|
: `<div class="button-stack">
|
|
1531
2280
|
${actions
|
|
1532
2281
|
.map((action) => `
|
|
@@ -1549,33 +2298,71 @@ export class HonuaActionPanelElement extends HonuaElementBase {
|
|
|
1549
2298
|
});
|
|
1550
2299
|
}
|
|
1551
2300
|
}
|
|
2301
|
+
/**
|
|
2302
|
+
* Every tag this kit owns, keyed for both the blanket
|
|
2303
|
+
* {@link defineHonuaWebComponents} registration and per-tag
|
|
2304
|
+
* {@link defineHonuaWebComponent} lookups (and, transitively, the catalog id
|
|
2305
|
+
* → constructor resolution `../controls/registry.js` uses for cross-kit
|
|
2306
|
+
* registration). Order here is the kit's auto-registration order.
|
|
2307
|
+
*/
|
|
2308
|
+
const WEB_COMPONENT_ELEMENTS = new Map([
|
|
2309
|
+
["honua-map", HonuaMapElement],
|
|
2310
|
+
["honua-layer-list", HonuaLayerListElement],
|
|
2311
|
+
["honua-legend", HonuaLegendElement],
|
|
2312
|
+
["honua-feature-table", HonuaFeatureTableElement],
|
|
2313
|
+
["honua-search", HonuaSearchElement],
|
|
2314
|
+
["honua-editor", HonuaEditorElement],
|
|
2315
|
+
["honua-feature-editor", HonuaFeatureEditorElement],
|
|
2316
|
+
["honua-chart", HonuaChartElement],
|
|
2317
|
+
["honua-basemap-control", HonuaBasemapControlElement],
|
|
2318
|
+
["honua-bookmarks", HonuaBookmarksElement],
|
|
2319
|
+
["honua-locate-control", HonuaLocateControlElement],
|
|
2320
|
+
["honua-measure-control", HonuaMeasureControlElement],
|
|
2321
|
+
["honua-measurement", HonuaMeasurementElement],
|
|
2322
|
+
["honua-time-slider", HonuaTimeSliderElement],
|
|
2323
|
+
["honua-sketch-control", HonuaSketchControlElement],
|
|
2324
|
+
["honua-print-export", HonuaPrintExportElement],
|
|
2325
|
+
["honua-map-status", HonuaMapStatusElement],
|
|
2326
|
+
["honua-action-panel", HonuaActionPanelElement],
|
|
2327
|
+
]);
|
|
2328
|
+
/**
|
|
2329
|
+
* Registers every custom element this kit owns. Skips tags already defined.
|
|
2330
|
+
* Called automatically on import of `./index.js` (`@honua/sdk-js/web-components`)
|
|
2331
|
+
* — NOT on import of this module, which stays side-effect-free (issue #679).
|
|
2332
|
+
*/
|
|
1552
2333
|
export function defineHonuaWebComponents(registry = globalDom.customElements) {
|
|
1553
2334
|
if (!registry)
|
|
1554
2335
|
return;
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
2336
|
+
for (const [tagName, ctor] of WEB_COMPONENT_ELEMENTS) {
|
|
2337
|
+
defineIfMissing(registry, tagName, ctor);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Registers a single web-components tag by name (e.g. `"honua-feature-table"`).
|
|
2342
|
+
* Unknown tags are a no-op. Skips the registration when the tag is already
|
|
2343
|
+
* defined. This is the primitive the catalog-driven `registerComponent` /
|
|
2344
|
+
* `registerComponents` APIs in `../controls/registry.js` call for
|
|
2345
|
+
* `web-components`-sourced catalog entries, so a consumer can register one
|
|
2346
|
+
* tag from this kit without the blanket {@link defineHonuaWebComponents} call.
|
|
2347
|
+
*/
|
|
2348
|
+
export function defineHonuaWebComponent(tagName, registry = globalDom.customElements) {
|
|
2349
|
+
if (!registry)
|
|
2350
|
+
return;
|
|
2351
|
+
const ctor = WEB_COMPONENT_ELEMENTS.get(tagName);
|
|
2352
|
+
if (ctor)
|
|
2353
|
+
defineIfMissing(registry, tagName, ctor);
|
|
1571
2354
|
}
|
|
1572
2355
|
function defineIfMissing(registry, tagName, ctor) {
|
|
1573
2356
|
if (!registry.get(tagName))
|
|
1574
2357
|
registry.define(tagName, ctor);
|
|
1575
2358
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
2359
|
+
// Deliberately no module-load auto-registration here (issue #679 PR review):
|
|
2360
|
+
// this module must stay side-effect-free on import so `../controls/registry.js`
|
|
2361
|
+
// can dynamically `import()` it for single-tag registration without that
|
|
2362
|
+
// import silently claiming every tag the kit owns. The blanket
|
|
2363
|
+
// auto-registration importing `@honua/sdk-js/web-components` triggers lives
|
|
2364
|
+
// in `./index.js`, which is every existing consumer's actual entry point —
|
|
2365
|
+
// see that module for the `defineHonuaWebComponents()` call.
|
|
1579
2366
|
function tableModelFromState(state, sourceId, fields, pageSize) {
|
|
1580
2367
|
const resolvedSourceId = sourceId ?? Object.keys(state?.featuresBySource ?? {})[0];
|
|
1581
2368
|
const rows = resolvedSourceId ? [...(state?.featuresBySource[resolvedSourceId] ?? [])] : [];
|
|
@@ -1674,24 +2461,16 @@ function getElementById(root, id) {
|
|
|
1674
2461
|
return root.getElementById(id);
|
|
1675
2462
|
return typeof document !== "undefined" ? document.getElementById(id) : null;
|
|
1676
2463
|
}
|
|
1677
|
-
function formatCell(value) {
|
|
1678
|
-
if (value === null || value === undefined)
|
|
1679
|
-
return "";
|
|
1680
|
-
if (typeof value === "object")
|
|
1681
|
-
return JSON.stringify(value);
|
|
1682
|
-
return String(value);
|
|
1683
|
-
}
|
|
1684
2464
|
function modeLabel(mode) {
|
|
1685
2465
|
return mode.charAt(0).toUpperCase() + mode.slice(1);
|
|
1686
2466
|
}
|
|
2467
|
+
/**
|
|
2468
|
+
* `<honua-map-status>`'s scale readout. Shares the single scale implementation
|
|
2469
|
+
* with the export pipeline's provenance block (issue #683) so a printed or
|
|
2470
|
+
* snapshotted map can never disagree with the scale the user saw on screen.
|
|
2471
|
+
*/
|
|
1687
2472
|
function approximateScale(viewport) {
|
|
1688
|
-
|
|
1689
|
-
if (!Number.isFinite(zoom))
|
|
1690
|
-
return "Scale unavailable";
|
|
1691
|
-
const latitude = viewport?.center?.[1] ?? 0;
|
|
1692
|
-
const metersPerPixel = (156543.03392 * Math.cos((latitude * Math.PI) / 180)) / 2 ** Number(zoom);
|
|
1693
|
-
const scale = Math.max(1, Math.round(metersPerPixel * 96 * 39.37));
|
|
1694
|
-
return `1:${scale.toLocaleString("en-US")}`;
|
|
2473
|
+
return approximateHonuaScaleLabel(viewport) ?? "Scale unavailable";
|
|
1695
2474
|
}
|
|
1696
2475
|
function setText(element, value) {
|
|
1697
2476
|
if (element)
|
|
@@ -1765,6 +2544,13 @@ function escapeHtml(value) {
|
|
|
1765
2544
|
function escapeAttribute(value) {
|
|
1766
2545
|
return escapeHtml(value).replace(/'/g, "'");
|
|
1767
2546
|
}
|
|
2547
|
+
function formatLayerOpacityValue(format, percent) {
|
|
2548
|
+
if (typeof format === "function")
|
|
2549
|
+
return format(percent);
|
|
2550
|
+
if (format !== undefined)
|
|
2551
|
+
return format.replace("{percent}", String(percent));
|
|
2552
|
+
return `${percent}%`;
|
|
2553
|
+
}
|
|
1768
2554
|
function baseStyles() {
|
|
1769
2555
|
return `
|
|
1770
2556
|
:host {
|
|
@@ -1806,8 +2592,17 @@ function baseStyles() {
|
|
|
1806
2592
|
}
|
|
1807
2593
|
`;
|
|
1808
2594
|
}
|
|
2595
|
+
function formatMapVisibleLayers(format, count) {
|
|
2596
|
+
return typeof format === "function"
|
|
2597
|
+
? format(count)
|
|
2598
|
+
: format
|
|
2599
|
+
? format.replace("{count}", String(count))
|
|
2600
|
+
: `${count} visible`;
|
|
2601
|
+
}
|
|
1809
2602
|
function mapStyles() {
|
|
1810
2603
|
return `
|
|
2604
|
+
:host { direction: inherit; }
|
|
2605
|
+
:host([dir="rtl"]) { direction: rtl; }
|
|
1811
2606
|
.map {
|
|
1812
2607
|
background: #dbeafe;
|
|
1813
2608
|
border: 1px solid var(--honua-ui-border);
|
|
@@ -1823,7 +2618,8 @@ function mapStyles() {
|
|
|
1823
2618
|
display: flex;
|
|
1824
2619
|
gap: 8px;
|
|
1825
2620
|
justify-content: space-between;
|
|
1826
|
-
padding: 8px
|
|
2621
|
+
padding-block: 8px;
|
|
2622
|
+
padding-inline: 10px;
|
|
1827
2623
|
}
|
|
1828
2624
|
.map__controls { align-items: center; display: flex; gap: 6px; }
|
|
1829
2625
|
.icon-button { min-width: 32px; padding: 0; }
|
|
@@ -1857,18 +2653,19 @@ function mapStyles() {
|
|
|
1857
2653
|
position: absolute;
|
|
1858
2654
|
}
|
|
1859
2655
|
.map__renderer .maplibregl-canvas {
|
|
1860
|
-
|
|
2656
|
+
inset-block-start: 0;
|
|
2657
|
+
inset-inline-start: 0;
|
|
1861
2658
|
position: absolute;
|
|
1862
|
-
top: 0;
|
|
1863
2659
|
}
|
|
1864
2660
|
.map__status {
|
|
1865
2661
|
background: rgba(255,255,255,0.86);
|
|
1866
2662
|
border-radius: 6px;
|
|
1867
2663
|
color: var(--honua-ui-muted);
|
|
1868
|
-
|
|
1869
|
-
|
|
2664
|
+
inset-block-start: 10px;
|
|
2665
|
+
inset-inline-start: 10px;
|
|
2666
|
+
padding-block: 4px;
|
|
2667
|
+
padding-inline: 7px;
|
|
1870
2668
|
position: absolute;
|
|
1871
|
-
top: 10px;
|
|
1872
2669
|
z-index: 1;
|
|
1873
2670
|
}
|
|
1874
2671
|
.map__status:empty { display: none; }
|
|
@@ -1876,21 +2673,52 @@ function mapStyles() {
|
|
|
1876
2673
|
.maplibregl-control-container {
|
|
1877
2674
|
display: none;
|
|
1878
2675
|
}
|
|
2676
|
+
@media (forced-colors: active), (prefers-contrast: more) {
|
|
2677
|
+
.map {
|
|
2678
|
+
background: Canvas;
|
|
2679
|
+
border-color: CanvasText;
|
|
2680
|
+
color: CanvasText;
|
|
2681
|
+
}
|
|
2682
|
+
.map__chrome, .map__footer, .map__status {
|
|
2683
|
+
background: Canvas;
|
|
2684
|
+
color: CanvasText;
|
|
2685
|
+
}
|
|
2686
|
+
.map__canvas { background: Canvas; }
|
|
2687
|
+
.map__controls button { border-color: ButtonText; color: ButtonText; }
|
|
2688
|
+
}
|
|
2689
|
+
@media (max-width: 320px) {
|
|
2690
|
+
.map { min-width: 0; }
|
|
2691
|
+
.map__chrome, .map__footer {
|
|
2692
|
+
align-items: stretch;
|
|
2693
|
+
flex-direction: column;
|
|
2694
|
+
}
|
|
2695
|
+
.map__controls { justify-content: space-between; width: 100%; }
|
|
2696
|
+
}
|
|
1879
2697
|
`;
|
|
1880
2698
|
}
|
|
1881
2699
|
function listStyles() {
|
|
1882
2700
|
return `
|
|
2701
|
+
:host { direction: inherit; }
|
|
2702
|
+
:host([dir="rtl"]) { direction: rtl; }
|
|
1883
2703
|
.panel {
|
|
1884
2704
|
background: var(--honua-ui-bg);
|
|
1885
2705
|
border: 1px solid var(--honua-ui-border);
|
|
1886
2706
|
border-radius: 8px;
|
|
1887
2707
|
margin: 0;
|
|
1888
|
-
padding: 10px;
|
|
2708
|
+
padding-block: 10px;
|
|
2709
|
+
padding-inline: 10px;
|
|
1889
2710
|
}
|
|
1890
|
-
legend { font-weight: 650; padding: 0 4px; }
|
|
2711
|
+
legend { font-weight: 650; padding-block: 0; padding-inline: 4px; }
|
|
1891
2712
|
.stack { display: grid; gap: 8px; }
|
|
1892
2713
|
.check-row { align-items: center; display: flex; gap: 8px; min-height: 28px; }
|
|
1893
|
-
.legend {
|
|
2714
|
+
.legend {
|
|
2715
|
+
display: grid;
|
|
2716
|
+
gap: 8px;
|
|
2717
|
+
list-style: none;
|
|
2718
|
+
margin-block-start: 10px;
|
|
2719
|
+
margin-inline: 0;
|
|
2720
|
+
padding: 0;
|
|
2721
|
+
}
|
|
1894
2722
|
.legend li { align-items: center; display: flex; gap: 8px; min-height: 24px; }
|
|
1895
2723
|
.swatch {
|
|
1896
2724
|
background: var(--swatch);
|
|
@@ -1903,6 +2731,23 @@ function listStyles() {
|
|
|
1903
2731
|
img.swatch { object-fit: cover; }
|
|
1904
2732
|
`;
|
|
1905
2733
|
}
|
|
2734
|
+
function legendStyles() {
|
|
2735
|
+
return `
|
|
2736
|
+
@media (max-width: 320px) {
|
|
2737
|
+
.panel { min-width: 0; }
|
|
2738
|
+
.legend li { align-items: flex-start; min-width: 0; }
|
|
2739
|
+
.legend li > span:last-child { min-width: 0; overflow-wrap: anywhere; }
|
|
2740
|
+
}
|
|
2741
|
+
@media (forced-colors: active), (prefers-contrast: more) {
|
|
2742
|
+
.legend li { color: CanvasText; }
|
|
2743
|
+
.legend .swatch {
|
|
2744
|
+
background: Canvas;
|
|
2745
|
+
border: 2px solid CanvasText;
|
|
2746
|
+
forced-color-adjust: none;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
`;
|
|
2750
|
+
}
|
|
1906
2751
|
function layerListStyles() {
|
|
1907
2752
|
return `
|
|
1908
2753
|
.layer-row {
|
|
@@ -1915,14 +2760,33 @@ function layerListStyles() {
|
|
|
1915
2760
|
align-items: center;
|
|
1916
2761
|
display: flex;
|
|
1917
2762
|
gap: 6px;
|
|
1918
|
-
padding-
|
|
2763
|
+
padding-inline-start: 24px;
|
|
1919
2764
|
}
|
|
1920
2765
|
.opacity { flex: 1; min-width: 60px; }
|
|
1921
2766
|
.move { min-width: 32px; padding: 0; }
|
|
2767
|
+
@media (forced-colors: active), (prefers-contrast: more) {
|
|
2768
|
+
.layer-row {
|
|
2769
|
+
border-top-color: CanvasText;
|
|
2770
|
+
}
|
|
2771
|
+
.layer-row input[type="checkbox"]:checked + span {
|
|
2772
|
+
color: Highlight;
|
|
2773
|
+
}
|
|
2774
|
+
.layer-row__tools button {
|
|
2775
|
+
border-color: ButtonText;
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
@media (max-width: 320px) {
|
|
2779
|
+
.panel { min-width: 0; }
|
|
2780
|
+
.layer-row__tools { flex-wrap: wrap; padding-left: 0; }
|
|
2781
|
+
.opacity { min-width: 0; width: 100%; }
|
|
2782
|
+
.move { flex: 1 1 32px; }
|
|
2783
|
+
}
|
|
1922
2784
|
`;
|
|
1923
2785
|
}
|
|
1924
2786
|
function tableStyles() {
|
|
1925
2787
|
return `
|
|
2788
|
+
:host { direction: inherit; }
|
|
2789
|
+
:host([dir="rtl"]) { direction: rtl; }
|
|
1926
2790
|
.table-panel {
|
|
1927
2791
|
border: 1px solid var(--honua-ui-border);
|
|
1928
2792
|
border-radius: 8px;
|
|
@@ -1933,15 +2797,17 @@ function tableStyles() {
|
|
|
1933
2797
|
background: var(--honua-ui-surface);
|
|
1934
2798
|
display: flex;
|
|
1935
2799
|
justify-content: space-between;
|
|
1936
|
-
padding: 8px
|
|
2800
|
+
padding-block: 8px;
|
|
2801
|
+
padding-inline: 10px;
|
|
1937
2802
|
}
|
|
1938
2803
|
.table-wrap { max-height: 300px; overflow: auto; }
|
|
1939
2804
|
table { border-collapse: collapse; min-width: 100%; table-layout: fixed; }
|
|
1940
2805
|
th, td {
|
|
1941
2806
|
border-top: 1px solid var(--honua-ui-border);
|
|
1942
2807
|
overflow: hidden;
|
|
1943
|
-
padding: 7px
|
|
1944
|
-
|
|
2808
|
+
padding-block: 7px;
|
|
2809
|
+
padding-inline: 9px;
|
|
2810
|
+
text-align: start;
|
|
1945
2811
|
text-overflow: ellipsis;
|
|
1946
2812
|
white-space: nowrap;
|
|
1947
2813
|
}
|
|
@@ -1949,63 +2815,133 @@ function tableStyles() {
|
|
|
1949
2815
|
tr[aria-selected="true"] td { background: #dbeafe; }
|
|
1950
2816
|
tbody tr[data-feature-id] { cursor: pointer; }
|
|
1951
2817
|
tbody tr[data-feature-id]:focus { outline: 2px solid var(--honua-ui-accent); outline-offset: -2px; }
|
|
2818
|
+
@media (forced-colors: active), (prefers-contrast: more) {
|
|
2819
|
+
.table-panel {
|
|
2820
|
+
background: Canvas;
|
|
2821
|
+
border-color: CanvasText;
|
|
2822
|
+
color: CanvasText;
|
|
2823
|
+
}
|
|
2824
|
+
.table-panel__bar, th { background: Canvas; color: CanvasText; }
|
|
2825
|
+
th, td { border-top-color: CanvasText; }
|
|
2826
|
+
tr[aria-selected="true"] td { background: Highlight; color: HighlightText; }
|
|
2827
|
+
tbody tr[data-feature-id]:focus { outline-color: Highlight; }
|
|
2828
|
+
}
|
|
1952
2829
|
`;
|
|
1953
2830
|
}
|
|
1954
2831
|
function searchStyles() {
|
|
1955
2832
|
return `
|
|
2833
|
+
:host { direction: inherit; }
|
|
2834
|
+
:host([dir="rtl"]) { direction: rtl; }
|
|
1956
2835
|
.search {
|
|
1957
2836
|
border: 1px solid var(--honua-ui-border);
|
|
1958
2837
|
border-radius: 8px;
|
|
1959
|
-
|
|
2838
|
+
min-width: 0;
|
|
2839
|
+
padding-block: 10px;
|
|
2840
|
+
padding-inline: 10px;
|
|
2841
|
+
width: 100%;
|
|
1960
2842
|
}
|
|
1961
|
-
form {
|
|
2843
|
+
form button { padding-block: 0; padding-inline: 10px; }
|
|
2844
|
+
form { display: grid; gap: 8px; grid-template-columns: minmax(0, 1fr) auto; min-width: 0; }
|
|
1962
2845
|
input {
|
|
1963
2846
|
border: 1px solid var(--honua-ui-border);
|
|
1964
2847
|
border-radius: 6px;
|
|
1965
2848
|
min-height: 32px;
|
|
1966
2849
|
min-width: 0;
|
|
1967
|
-
padding: 0
|
|
2850
|
+
padding-block: 0;
|
|
2851
|
+
padding-inline: 9px;
|
|
2852
|
+
}
|
|
2853
|
+
.results {
|
|
2854
|
+
display: grid;
|
|
2855
|
+
gap: 6px;
|
|
2856
|
+
list-style: none;
|
|
2857
|
+
margin-block-start: 10px;
|
|
2858
|
+
margin-inline: 0;
|
|
2859
|
+
padding: 0;
|
|
1968
2860
|
}
|
|
1969
|
-
.results {
|
|
1970
|
-
.results button { text-align: left; width: 100%; }
|
|
2861
|
+
.results button { text-align: start; width: 100%; }
|
|
1971
2862
|
.suggestions {
|
|
1972
2863
|
border: 1px solid var(--honua-ui-border);
|
|
1973
2864
|
border-radius: 6px;
|
|
1974
2865
|
display: grid;
|
|
1975
2866
|
list-style: none;
|
|
1976
|
-
margin: 6px
|
|
2867
|
+
margin-block-start: 6px;
|
|
2868
|
+
margin-inline: 0;
|
|
1977
2869
|
overflow: hidden;
|
|
1978
2870
|
padding: 0;
|
|
1979
2871
|
}
|
|
1980
2872
|
.suggestions[hidden] { display: none; }
|
|
1981
|
-
.suggestions [role="option"] {
|
|
2873
|
+
.suggestions [role="option"] {
|
|
2874
|
+
cursor: pointer;
|
|
2875
|
+
padding-block: 6px;
|
|
2876
|
+
padding-inline: 9px;
|
|
2877
|
+
}
|
|
1982
2878
|
.suggestions [role="option"][aria-selected="true"] {
|
|
1983
2879
|
background: var(--honua-ui-accent);
|
|
1984
2880
|
color: var(--honua-ui-accent-fg);
|
|
1985
2881
|
}
|
|
1986
|
-
.status { color: var(--honua-ui-muted); margin: 6px
|
|
2882
|
+
.status { color: var(--honua-ui-muted); margin-block-start: 6px; margin-inline: 0; }
|
|
1987
2883
|
.status:empty { display: none; }
|
|
2884
|
+
@media (max-width: 320px) {
|
|
2885
|
+
form { grid-template-columns: minmax(0, 1fr); }
|
|
2886
|
+
form button { width: 100%; }
|
|
2887
|
+
}
|
|
2888
|
+
@media (forced-colors: active), (prefers-contrast: more) {
|
|
2889
|
+
.search, input, button, .suggestions { border-color: CanvasText; }
|
|
2890
|
+
.suggestions [role="option"][aria-selected="true"] {
|
|
2891
|
+
background: Highlight;
|
|
2892
|
+
color: HighlightText;
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
1988
2895
|
`;
|
|
1989
2896
|
}
|
|
1990
2897
|
function editorStyles() {
|
|
1991
2898
|
return `
|
|
2899
|
+
:host { direction: inherit; }
|
|
2900
|
+
:host([dir="rtl"]) { direction: rtl; }
|
|
1992
2901
|
.editor {
|
|
1993
2902
|
border: 1px solid var(--honua-ui-border);
|
|
1994
2903
|
border-radius: 8px;
|
|
1995
|
-
padding: 10px;
|
|
2904
|
+
padding-block: 10px;
|
|
2905
|
+
padding-inline: 10px;
|
|
1996
2906
|
}
|
|
2907
|
+
button { padding-block: 0; padding-inline: 10px; }
|
|
1997
2908
|
.editor__bar, .editor__actions { align-items: center; display: flex; gap: 8px; justify-content: space-between; }
|
|
1998
2909
|
.editor__actions { justify-content: flex-start; }
|
|
1999
|
-
.selection { margin: 10px
|
|
2000
|
-
.muted { margin: 0
|
|
2910
|
+
.selection { margin-block: 10px 4px; margin-inline: 0; }
|
|
2911
|
+
.muted { margin-block: 0 10px; margin-inline: 0; }
|
|
2912
|
+
@media (forced-colors: active), (prefers-contrast: more) {
|
|
2913
|
+
.editor {
|
|
2914
|
+
background: Canvas;
|
|
2915
|
+
border-color: CanvasText;
|
|
2916
|
+
color: CanvasText;
|
|
2917
|
+
}
|
|
2918
|
+
.editor__actions button {
|
|
2919
|
+
border-color: ButtonText;
|
|
2920
|
+
color: ButtonText;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
@media (max-width: 320px) {
|
|
2924
|
+
.editor { min-width: 0; }
|
|
2925
|
+
.editor__bar { align-items: flex-start; flex-direction: column; }
|
|
2926
|
+
.editor__actions { flex-wrap: wrap; }
|
|
2927
|
+
.editor__actions button {
|
|
2928
|
+
flex: 1 1 120px;
|
|
2929
|
+
min-width: 0;
|
|
2930
|
+
overflow-wrap: anywhere;
|
|
2931
|
+
white-space: normal;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2001
2934
|
`;
|
|
2002
2935
|
}
|
|
2003
2936
|
function chartStyles() {
|
|
2004
2937
|
return `
|
|
2938
|
+
:host { direction: inherit; }
|
|
2939
|
+
:host([dir="rtl"]) { direction: rtl; }
|
|
2005
2940
|
.chart {
|
|
2006
2941
|
border: 1px solid var(--honua-ui-border);
|
|
2007
2942
|
border-radius: 8px;
|
|
2008
|
-
padding: 10px;
|
|
2943
|
+
padding-block: 10px;
|
|
2944
|
+
padding-inline: 10px;
|
|
2009
2945
|
}
|
|
2010
2946
|
.bars { display: grid; gap: 8px; margin-top: 10px; }
|
|
2011
2947
|
.bar-row { align-items: center; display: grid; gap: 8px; grid-template-columns: minmax(70px, 1fr) 3fr auto; }
|
|
@@ -2014,14 +2950,32 @@ function chartStyles() {
|
|
|
2014
2950
|
background: var(--bar-color);
|
|
2015
2951
|
border-radius: inherit;
|
|
2016
2952
|
content: "";
|
|
2017
|
-
inset: 0
|
|
2953
|
+
inset-block: 0;
|
|
2954
|
+
inset-inline-end: auto;
|
|
2955
|
+
inset-inline-start: 0;
|
|
2018
2956
|
position: absolute;
|
|
2019
2957
|
width: var(--bar);
|
|
2020
2958
|
}
|
|
2959
|
+
@media (forced-colors: active), (prefers-contrast: more) {
|
|
2960
|
+
.chart {
|
|
2961
|
+
background: Canvas;
|
|
2962
|
+
border-color: CanvasText;
|
|
2963
|
+
color: CanvasText;
|
|
2964
|
+
}
|
|
2965
|
+
.bar { background: ButtonFace; outline: 1px solid ButtonText; }
|
|
2966
|
+
.bar::before { background: Highlight; }
|
|
2967
|
+
}
|
|
2968
|
+
@media (max-width: 320px) {
|
|
2969
|
+
.chart { min-width: 0; }
|
|
2970
|
+
.bar-row { grid-template-columns: minmax(0, 1fr); }
|
|
2971
|
+
.bar { min-width: 0; width: 100%; }
|
|
2972
|
+
}
|
|
2021
2973
|
`;
|
|
2022
2974
|
}
|
|
2023
2975
|
function controlPanelStyles() {
|
|
2024
2976
|
return `
|
|
2977
|
+
:host { direction: inherit; }
|
|
2978
|
+
:host([dir="rtl"]) { direction: rtl; }
|
|
2025
2979
|
.control-panel {
|
|
2026
2980
|
background: var(--honua-ui-bg);
|
|
2027
2981
|
border: 1px solid var(--honua-ui-border);
|
|
@@ -2029,13 +2983,24 @@ function controlPanelStyles() {
|
|
|
2029
2983
|
display: grid;
|
|
2030
2984
|
gap: 10px;
|
|
2031
2985
|
min-width: 180px;
|
|
2032
|
-
|
|
2986
|
+
max-width: 100%;
|
|
2987
|
+
min-inline-size: 0;
|
|
2988
|
+
padding-block: 10px;
|
|
2989
|
+
padding-inline: 10px;
|
|
2033
2990
|
}
|
|
2991
|
+
.control-panel > * { min-width: 0; max-width: 100%; }
|
|
2992
|
+
button { padding-block: 0; padding-inline: 10px; }
|
|
2034
2993
|
.control-panel__bar {
|
|
2035
2994
|
align-items: center;
|
|
2036
2995
|
display: flex;
|
|
2037
2996
|
gap: 8px;
|
|
2038
2997
|
justify-content: space-between;
|
|
2998
|
+
min-width: 0;
|
|
2999
|
+
}
|
|
3000
|
+
.control-panel__bar h2,
|
|
3001
|
+
.control-panel__bar span {
|
|
3002
|
+
min-width: 0;
|
|
3003
|
+
overflow-wrap: anywhere;
|
|
2039
3004
|
}
|
|
2040
3005
|
.control-panel__bar span {
|
|
2041
3006
|
color: var(--honua-ui-muted);
|
|
@@ -2047,7 +3012,7 @@ function controlPanelStyles() {
|
|
|
2047
3012
|
}
|
|
2048
3013
|
.button-stack button {
|
|
2049
3014
|
justify-content: flex-start;
|
|
2050
|
-
text-align:
|
|
3015
|
+
text-align: start;
|
|
2051
3016
|
}
|
|
2052
3017
|
.segmented {
|
|
2053
3018
|
display: grid;
|
|
@@ -2056,7 +3021,10 @@ function controlPanelStyles() {
|
|
|
2056
3021
|
}
|
|
2057
3022
|
.segmented button {
|
|
2058
3023
|
min-width: 0;
|
|
2059
|
-
|
|
3024
|
+
overflow-wrap: anywhere;
|
|
3025
|
+
padding-block: 0;
|
|
3026
|
+
padding-inline: 8px;
|
|
3027
|
+
white-space: normal;
|
|
2060
3028
|
}
|
|
2061
3029
|
button[aria-pressed="true"] {
|
|
2062
3030
|
background: var(--honua-ui-accent);
|
|
@@ -2065,6 +3033,93 @@ function controlPanelStyles() {
|
|
|
2065
3033
|
}
|
|
2066
3034
|
p {
|
|
2067
3035
|
margin: 0;
|
|
3036
|
+
overflow-wrap: anywhere;
|
|
3037
|
+
}
|
|
3038
|
+
@media (max-width: 240px) {
|
|
3039
|
+
.control-panel {
|
|
3040
|
+
min-width: 0;
|
|
3041
|
+
}
|
|
3042
|
+
.control-panel__bar {
|
|
3043
|
+
align-items: flex-start;
|
|
3044
|
+
flex-direction: column;
|
|
3045
|
+
}
|
|
3046
|
+
.control-panel__bar h2 {
|
|
3047
|
+
min-width: 0;
|
|
3048
|
+
overflow-wrap: anywhere;
|
|
3049
|
+
}
|
|
3050
|
+
.segmented {
|
|
3051
|
+
grid-template-columns: 1fr;
|
|
3052
|
+
}
|
|
3053
|
+
.button-stack button,
|
|
3054
|
+
.segmented button,
|
|
3055
|
+
.control-panel > button {
|
|
3056
|
+
width: 100%;
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
@media (min-width: 241px) and (max-width: 320px) {
|
|
3060
|
+
.segmented { grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); }
|
|
3061
|
+
}
|
|
3062
|
+
@media (forced-colors: active), (prefers-contrast: more) {
|
|
3063
|
+
.control-panel {
|
|
3064
|
+
background: Canvas;
|
|
3065
|
+
border-color: CanvasText;
|
|
3066
|
+
color: CanvasText;
|
|
3067
|
+
}
|
|
3068
|
+
.control-panel__bar span { color: CanvasText; }
|
|
3069
|
+
.button-stack button {
|
|
3070
|
+
background: ButtonFace;
|
|
3071
|
+
border-color: ButtonText;
|
|
3072
|
+
color: ButtonText;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
`;
|
|
3076
|
+
}
|
|
3077
|
+
function basemapControlStyles() {
|
|
3078
|
+
return `
|
|
3079
|
+
@media (forced-colors: active), (prefers-contrast: more) {
|
|
3080
|
+
.control-panel {
|
|
3081
|
+
background: Canvas;
|
|
3082
|
+
border-color: CanvasText;
|
|
3083
|
+
color: CanvasText;
|
|
3084
|
+
}
|
|
3085
|
+
.control-panel__bar span, .empty { color: CanvasText; }
|
|
3086
|
+
.segmented button {
|
|
3087
|
+
background: ButtonFace;
|
|
3088
|
+
border: 2px solid ButtonText;
|
|
3089
|
+
color: ButtonText;
|
|
3090
|
+
forced-color-adjust: none;
|
|
3091
|
+
}
|
|
3092
|
+
.segmented button[aria-pressed="true"] {
|
|
3093
|
+
background: Highlight;
|
|
3094
|
+
border-color: Highlight;
|
|
3095
|
+
color: HighlightText;
|
|
3096
|
+
}
|
|
3097
|
+
.segmented button:focus-visible {
|
|
3098
|
+
outline: 2px solid Highlight;
|
|
3099
|
+
outline-offset: 2px;
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
`;
|
|
3103
|
+
}
|
|
3104
|
+
function actionPanelStyles() {
|
|
3105
|
+
return `
|
|
3106
|
+
@media (forced-colors: active), (prefers-contrast: more) {
|
|
3107
|
+
.control-panel {
|
|
3108
|
+
background: Canvas;
|
|
3109
|
+
border-color: CanvasText;
|
|
3110
|
+
color: CanvasText;
|
|
3111
|
+
}
|
|
3112
|
+
.control-panel__bar span, .empty { color: CanvasText; }
|
|
3113
|
+
.button-stack button {
|
|
3114
|
+
background: ButtonFace;
|
|
3115
|
+
border: 2px solid ButtonText;
|
|
3116
|
+
color: ButtonText;
|
|
3117
|
+
forced-color-adjust: none;
|
|
3118
|
+
}
|
|
3119
|
+
.button-stack button:disabled {
|
|
3120
|
+
border-color: GrayText;
|
|
3121
|
+
color: GrayText;
|
|
3122
|
+
}
|
|
2068
3123
|
}
|
|
2069
3124
|
`;
|
|
2070
3125
|
}
|
|
@@ -2076,15 +3131,47 @@ function mapStatusStyles() {
|
|
|
2076
3131
|
border: 1px solid var(--honua-ui-border);
|
|
2077
3132
|
border-radius: 8px;
|
|
2078
3133
|
display: flex;
|
|
3134
|
+
direction: inherit;
|
|
2079
3135
|
flex-wrap: wrap;
|
|
2080
3136
|
gap: 8px;
|
|
2081
3137
|
justify-content: space-between;
|
|
2082
3138
|
min-height: 42px;
|
|
2083
|
-
padding: 6px
|
|
3139
|
+
padding-block: 6px;
|
|
3140
|
+
padding-inline: 8px;
|
|
2084
3141
|
}
|
|
2085
3142
|
span {
|
|
2086
3143
|
color: var(--honua-ui-muted);
|
|
2087
3144
|
min-width: 0;
|
|
3145
|
+
text-align: start;
|
|
3146
|
+
}
|
|
3147
|
+
.map-status button { text-align: start; }
|
|
3148
|
+
@media (forced-colors: active), (prefers-contrast: more) {
|
|
3149
|
+
.map-status {
|
|
3150
|
+
background: Canvas;
|
|
3151
|
+
border-color: CanvasText;
|
|
3152
|
+
color: CanvasText;
|
|
3153
|
+
}
|
|
3154
|
+
.map-status span { color: CanvasText; }
|
|
3155
|
+
.map-status button {
|
|
3156
|
+
background: ButtonFace;
|
|
3157
|
+
border: 2px solid ButtonText;
|
|
3158
|
+
color: ButtonText;
|
|
3159
|
+
forced-color-adjust: none;
|
|
3160
|
+
}
|
|
3161
|
+
:host([data-status="unsupported"]) .map-status {
|
|
3162
|
+
border-color: GrayText;
|
|
3163
|
+
color: GrayText;
|
|
3164
|
+
}
|
|
3165
|
+
:host([data-status="unsupported"]) .map-status span { color: GrayText; }
|
|
3166
|
+
:host([data-status="error"]) .map-status {
|
|
3167
|
+
border-color: Mark;
|
|
3168
|
+
color: MarkText;
|
|
3169
|
+
}
|
|
3170
|
+
:host([data-status="error"]) .map-status span { color: MarkText; }
|
|
3171
|
+
}
|
|
3172
|
+
@media (max-width: 240px) {
|
|
3173
|
+
.map-status { align-items: flex-start; flex-direction: column; }
|
|
3174
|
+
.map-status button { inline-size: 100%; }
|
|
2088
3175
|
}
|
|
2089
3176
|
`;
|
|
2090
3177
|
}
|