@zephytiju/prism-geovision-embed 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +141 -47
  2. package/dist/EmbedBody.d.ts +20 -7
  3. package/dist/EmbedBody.d.ts.map +1 -1
  4. package/dist/EmbedBody.js +52 -64
  5. package/dist/GeovisionEmbed.d.ts +9 -7
  6. package/dist/GeovisionEmbed.d.ts.map +1 -1
  7. package/dist/GeovisionEmbed.js +13 -9
  8. package/dist/describeViews.d.ts +11 -16
  9. package/dist/describeViews.d.ts.map +1 -1
  10. package/dist/describeViews.js +19 -24
  11. package/dist/index.d.ts +8 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +5 -1
  14. package/dist/locales/en.json +2 -7
  15. package/dist/locales/index.d.ts +2 -12
  16. package/dist/locales/index.d.ts.map +1 -1
  17. package/dist/locales/zh-CN.json +2 -7
  18. package/dist/map/CesiumMapVariant.d.ts +37 -0
  19. package/dist/map/CesiumMapVariant.d.ts.map +1 -0
  20. package/dist/map/CesiumMapVariant.js +124 -0
  21. package/dist/map/CesiumMarkersLayer.d.ts +16 -0
  22. package/dist/map/CesiumMarkersLayer.d.ts.map +1 -0
  23. package/dist/map/CesiumMarkersLayer.js +22 -0
  24. package/dist/map/MapVariant.d.ts +19 -0
  25. package/dist/map/MapVariant.d.ts.map +1 -0
  26. package/dist/map/MapVariant.js +48 -0
  27. package/dist/map/cesiumEngine.d.ts +62 -0
  28. package/dist/map/cesiumEngine.d.ts.map +1 -0
  29. package/dist/map/cesiumEngine.js +201 -0
  30. package/dist/map/cesiumModel.d.ts +51 -0
  31. package/dist/map/cesiumModel.d.ts.map +1 -0
  32. package/dist/map/cesiumModel.js +65 -0
  33. package/dist/map/railState.d.ts +10 -0
  34. package/dist/map/railState.d.ts.map +1 -0
  35. package/dist/map/railState.js +17 -0
  36. package/dist/resolveView.d.ts +6 -3
  37. package/dist/resolveView.d.ts.map +1 -1
  38. package/dist/resolveView.js +13 -7
  39. package/dist/types.d.ts +37 -8
  40. package/dist/types.d.ts.map +1 -1
  41. package/dist/types.js +6 -1
  42. package/dist/views.d.ts +29 -0
  43. package/dist/views.d.ts.map +1 -0
  44. package/dist/views.js +38 -0
  45. package/locales/en.json +2 -7
  46. package/locales/zh-CN.json +2 -7
  47. package/package.json +13 -5
package/README.md CHANGED
@@ -7,26 +7,44 @@ component of the CLOSED dossier-embed contract (GeoVision ↔ Dossier Editor coo
7
7
  control (compact layer rail, video-like replay, …) stays inside it — nothing an embed does
8
8
  navigates away.
9
9
 
10
+ **The embed is a DISPATCHER**: one invocation renders exactly ONE variant — never a stacked
11
+ dashboard. The saved view's `previewKind` selects it:
12
+
13
+ | `previewKind` | Renders | Chrome |
14
+ | --- | --- | --- |
15
+ | `map` | the **Cesium engine** map (default, `engine="cesium"`): the same zoom/rotate/tilt/pan camera as the globe viewport on an ION-FREE CesiumJS/Resium viewer — an SVG snapshot placeholder crossfading into the lazily-mounted engine with the video-like replay control on top | replay control only — no tools, no rail, no floatboxes, no fusion board, no view-mode cycling, no SAVE |
16
+ | `map` (`engine="svg"`) | the lighter static SVG map (markers / heat / side coloring + video-like replay) | compact layer rail + replay control |
17
+ | `relational` | `@zephytiju/prism-relation-fluxboard` | none — rendered as-is |
18
+ | `timeline` | `@zephytiju/prism-timeline-board` | none — rendered as-is |
19
+ | `stats` | `@zephytiju/prism-stats-panel` | none — rendered as-is |
20
+
21
+ Whatever renders fills the slot's full width/height (a scoped fit rule widens the packages'
22
+ intrinsic standalone widths to the slot); unknown or missing `previewKind` renders the localized
23
+ UNSUPPORTED PREVIEW KIND frame.
24
+
10
25
  ## The contract (implemented exactly)
11
26
 
12
27
  ```tsx
13
- <GeovisionEmbed sourceFileRef viewId mode? interactionMode? locale? width height />
28
+ <GeovisionEmbed sourceFileRef viewId mode? interactionMode? engine? resolutionScale? locale? width height />
14
29
  ```
15
30
 
16
31
  | Prop | Type | Default | Contract |
17
32
  | --- | --- | --- | --- |
18
33
  | `sourceFileRef` | `string` | — | stable ref addressing the saved geovision-view file (`IFileEntry.read`) |
19
- | `viewId` | `string` | — | the saved view to render; a file carries exactly one view, so the viewId is the file ref itself — a mismatch renders the localized UNKNOWN VIEW frame |
34
+ | `viewId` | `string` | — | the saved view to render — `${sourceFileRef}#${previewKind}`, exactly as `describeViews` lists it; a viewId addressing another file renders UNKNOWN VIEW, and one with an unknown/missing previewKind renders UNSUPPORTED PREVIEW KIND |
20
35
  | `mode` | `"static" \| "live"` | `"static"` | v1: `live` is reserved and behaves as `static` (no live fetches) |
21
36
  | `interactionMode` | `"rich" \| "static"` | `"rich"` | `rich` captures pointer events inside the slot for inline interactions; `static` renders a static frame (also forced below 240 px height) |
37
+ | `engine` | `"cesium" \| "svg"` | `"cesium"` | the MAP variant's render engine: the CesiumJS engine embed (same zoom/rotate as the globe) or the lighter static SVG variant. Ignored by the other preview kinds; the degradation paths always fall back to the SVG snapshot |
38
+ | `resolutionScale` | `number` | `0.75` | the Cesium engine's render resolution scale — the embed's GPU/retina footprint cap. Ignored by the `"svg"` engine |
22
39
  | `locale` | `"en" \| "zh-CN"` | `"en"` | the embed renders in the editor's active locale (unknown locales fall back to en) |
23
40
  | `width` / `height` | `number \| string` | `"100%"` / `"100%"` | slot size; below 240 px height the embed degrades to a static frame with no pointer capture |
24
41
 
25
42
  `GeovisionEmbed.describeViews(sourceFileRef)` feeds the editor's VIEW ▾ dropdown: it reads +
26
- validates the file and returns `{ viewId, name, previewKind }[]` (one saved view per file; the name
27
- is derived from the file's recorded provenance — `savedBy · save time` — because the format carries
28
- no name field; `previewKind` summarizes the painted map variant: `normal` / `heat` /
29
- `side-color`). Fails closed to an empty list for unreadable or invalid files.
43
+ validates the file and returns `{ viewId, name, previewKind }[]` — ONE view per previewKind
44
+ (`map` / `relational` / `timeline` / `stats`), `viewId = ${fileRef}#${previewKind}` (the file
45
+ format is closed and carries no extension fields, so the per-kind views are addressed by
46
+ convention over the stable file ref; the name derives from the recorded provenance because the
47
+ format carries no name field). Fails closed to an empty list for unreadable or invalid files.
30
48
 
31
49
  Agreed coordination record: task list
32
50
  https://applink.feishu.cn/client/todo/task_list?guid=33ec590f-7c59-4ace-b069-53c649fc55ab and the
@@ -42,60 +60,136 @@ authoritative designs:
42
60
  no live fetches occur beyond the host's own routes.
43
61
  2. **Validate** fail-closed through the authoritative format
44
62
  (`@zephytiju/lattice-common-bundle/geovisionView`): structure AND digest. Failures render
45
- localized frames (`not-found` / `invalid` / `unknown-view`).
63
+ localized frames (`not-found` / `invalid` / `unknown-view` / `unknown-preview`).
46
64
  3. **Seed** an isolated `<ChannelScope>` — `query-box.entity-results` carries the snapshot
47
65
  projection, `search-results.selected-entity` the workspace's selected entity — so every embed
48
- renders its own file and nothing leaks across embeds on the same page.
49
- 4. **Mount the REAL packages inside the scope**: `@zephytiju/prism-relation-fluxboard`,
50
- `@zephytiju/prism-timeline-board` and `@zephytiju/prism-stats-panel` (their internal fetches
51
- flow through the host transport; in the demo the mock executor answers from the file's
52
- snapshot), plus the NEW simplified map:
53
- - a static equirectangular SVG mini-map over the snapshot's located entities;
54
- - layers from the file — normal markers (overlay), kernel-density blobs per enabled heat layer
55
- (simplified variant of the globe's heat kernel), side-color mapping applied to marker
56
- fills/outlines (`solid` / `stripe` / `accent`). Tag matchers apply to the item's stream
57
- `type` (the kinds≡tags convention the recorded behavior uses for heat selections) because
58
- the format's `EntityResultItem` declares only `id/type/label/lat/lon`;
59
- - the compact layer rail — the same click/long-press contract as the globe's rail at compact
60
- density (click toggles, long-press/right-click opens the anchored opacity options; changes
61
- are local to the surface, persisted only via the editor's block config);
62
- - the VIDEO-LIKE replay control — play/pause + progress scrubbing entity positions across the
63
- snapshot's time-windowed timeline events (tracks interpolate linearly by time between
64
- deterministic waypoints derived from the events; display-only).
66
+ renders its own file and nothing leaks across embeds on the same page (the 2×2 demo grid runs
67
+ four of them side by side).
68
+ 4. **Dispatch** on the view's `previewKind` — exactly ONE of:
69
+ - **map, Cesium engine (default)** — the SAME engine/zoom/rotate as the globe viewport
70
+ (`cesium` 1.143.0 + `resium` 1.24.0, identical to `@zephytiju/prism-globe-viewport`),
71
+ wrapped in an EMBED-BUDGET architecture:
72
+ - **snapshot placeholder** — the existing SVG mini-map renders from the same snapshot data
73
+ the moment the embed mounts (zero Cesium cost — the instant visual; see the performance
74
+ strategy below for the lazy handover);
75
+ - **lazy mount** — an `IntersectionObserver` on the embed container boots the Cesium
76
+ `Viewer` only while the slot is on screen (deferred off the observer's own path, so the
77
+ WebGL boot never blocks the interaction that revealed it); once the viewer element
78
+ exists the layers crossfade (SVG out, canvas in). Scrolling the slot out of view — or
79
+ unmounting — unmounts the Resium `<Viewer>`, whose teardown **destroys** the WebGL
80
+ context/workers; re-entry re-observes and boots again;
81
+ - **idle GPU** — `requestRenderMode: true` + `maximumRenderTimeChange: Infinity` keep the
82
+ render loop asleep when nothing changes (Cesium's camera input handlers request renders
83
+ themselves on zoom/rotate/tilt/pan, which stay fully enabled — the
84
+ `ScreenSpaceCameraController` is untouched); `resolutionScale` 0.75 (the embed prop) caps
85
+ the raster footprint, `scene.globe.tileCacheSize` 10 bounds the tile memory, every UI
86
+ widget is disabled (animation/timeline/baseLayerPicker/geocoder/homeButton/
87
+ sceneModePicker/navigationHelpButton/fullscreenButton/infoBox/selectionIndicator) and the
88
+ sky is off (`skyBox: false`, `skyAtmosphere: false`). CesiumJS 1.143 carries no
89
+ `creditsDisplay` constructor option, so the credit strip (the only chrome Cesium still
90
+ paints with everything else disabled) is suppressed by a rule scoped to the embed's
91
+ stage; the imagery credit stays attached to the provider;
92
+ - **ion-free imagery + camera restore** — the saved view's `baseLayer.urlTemplate` (with its
93
+ credit) feeds a `UrlTemplateImageryProvider`, falling back to the
94
+ `OpenStreetMapImageryProvider` default — never a Cesium ion asset; terrain is the default
95
+ ellipsoid. The camera is restored from the saved view's `camera` state
96
+ `{lon, lat, height}` — the same `IGeoView` serialization the globe viewport saves;
97
+ - **markers + replay** — entity markers derive from the snapshot (the same side-color
98
+ derivation the SVG mini-map paints: token-colored points + mono labels, the workspace's
99
+ selected entity emphasized). The video-like replay control overlays the canvas; each
100
+ scrub tick re-derives the marker positions with the SAME track math as the SVG variant
101
+ (linear interpolation between deterministic waypoints) and explicitly calls
102
+ `scene.requestRender()` — under `requestRenderMode` a scrub tick renders exactly one
103
+ frame;
104
+ - **no other chrome** — no tools, no layer rail, no floatboxes, no fusion board, no
105
+ view-mode cycling, no SAVE: just basemap + markers + replay;
106
+ - **map, SVG engine (`engine="svg"`)** — the lighter static variant hosts opt into: the
107
+ static equirectangular SVG mini-map filling the slot with
108
+ - layers from the file — normal markers (overlay), kernel-density blobs per enabled heat
109
+ layer (simplified variant of the globe's heat kernel), side-color mapping applied to marker
110
+ fills/outlines (`solid` / `stripe` / `accent`). Tag matchers apply to the item's stream
111
+ `type` (the kinds≡tags convention the recorded behavior uses for heat selections) because
112
+ the format's `EntityResultItem` declares only `id/type/label/lat/lon`;
113
+ - the compact layer rail — the same click/long-press contract as the globe's rail at compact
114
+ density (click toggles, long-press/right-click opens the anchored opacity options; changes
115
+ are local to the surface, persisted only via the editor's block config);
116
+ - the VIDEO-LIKE replay control — play/pause + progress scrubbing entity positions across the
117
+ snapshot's time-windowed timeline events (tracks interpolate linearly by time between
118
+ deterministic waypoints derived from the events; display-only);
119
+ - **relational / timeline / stats** — the REAL packages mounted inside the scope, rendered
120
+ as-is (their internal fetches flow through the host transport; in the demo the mock executor
121
+ answers from the file's snapshot).
122
+
123
+ ### Degradation path
124
+
125
+ `engine="svg"`, `interactionMode="static"` and slots below `MIN_RICH_HEIGHT` (240 px) NEVER
126
+ mount Cesium: the map variant renders the SVG snapshot only (the SVG mini-map plus, on the
127
+ rich-SVG path, its rail/replay chrome), and static frames additionally capture no pointer events.
128
+ Off-screen rich Cesium embeds hold no GPU resources either — the observer gate is the second
129
+ half of the degradation story (visible ⇒ engine, invisible ⇒ destroyed).
130
+
131
+ ### Performance strategy (the Cesium engine in one paragraph)
132
+
133
+ Instant SVG from the snapshot (no engine cost at first paint) → the engine boots only when the
134
+ slot is actually visible (IntersectionObserver, boot deferred to an idle callback) → once booted
135
+ the render loop sleeps (`requestRenderMode` + `maximumRenderTimeChange: Infinity`; Cesium's own
136
+ camera handlers wake it for zoom/rotate/tilt/pan) → replay ticks and marker moves render exactly
137
+ one explicit `scene.requestRender()` frame each → the raster is capped (`resolutionScale` 0.75,
138
+ configurable) and tile memory bounded (`tileCacheSize` 10) → leaving the viewport destroys the
139
+ viewer outright (WebGL context + workers freed), and re-entry rebuilds it.
65
140
 
66
141
  ## Peer dependencies / registry status
67
142
 
68
- The three mounted component packages are REGISTRY PEERS (`>=0.1.0`). They are not yet on npm —
69
- their tag-triggered trusted-publishing runs returned 404 (the npm-side trusted-publisher
70
- registration for the new package names appears pending; `v0.1.0` tags are already pushed), and
71
- the ambient npm auth is expired, so they could not be published from this work. Until the first
72
- release the dev surface installs them from **vendored tarballs** produced by `npm pack` in the
73
- sibling repos (`vendor/*.tgz`, referenced as `file:` devDependencies):
143
+ The three mounted component packages are REGISTRY PEERS (`>=0.1.0`) and are published on npm;
144
+ the dev surface pins the same registry specifiers as devDependencies (no vendored tarballs
145
+ since 0.2.0):
74
146
 
75
- - `.npmrc` sets `legacy-peer-deps=true` so npm never auto-installs the peers;
147
+ - `.npmrc` keeps `legacy-peer-deps=true` so npm never auto-installs the peers on top of the
148
+ explicit dev pins;
76
149
  - typecheck/tests/demo run against the REAL packaged output (dist) of the siblings, exactly what
77
- the registry will serve once published — the jsdom run inlines them and stubs the
78
- `@xyflow/react` stylesheet (`vitest.config.ts`);
79
- - the EMITTED package keeps the registry specifiers (`import … from
80
- "@zephytiju/prism-relation-fluxboard"`), so hosts resolve them normally once published;
81
- - when the siblings land on npm, drop `vendor/`, pin registry versions in devDependencies and
82
- remove `legacy-peer-deps`.
150
+ the registry serves — the jsdom run inlines them and stubs the `@xyflow/react` stylesheet
151
+ (`vitest.config.ts`).
83
152
 
84
- Per the owner gate this package is NOT published or tagged from this work.
153
+ The Cesium engine comes from the REGISTRY as regular dependencies — `cesium` 1.143.0 +
154
+ `resium` 1.24.0 (the same versions and `@cesium/*` overrides as
155
+ `@zephytiju/prism-globe-viewport`), so hosts install the identical engine the globe runs. The
156
+ demo's vite config serves Cesium's static asset tree (Workers/Assets/ThirdParty/Widgets)
157
+ straight from `node_modules` via `publicDir` with `CESIUM_BASE_URL="/"` — nothing is bundled or
158
+ vendored, and no ion endpoint is configured anywhere.
159
+
160
+ Per the owner gate this package is NOT published or tagged from this work (the npm `0.1.0` is the
161
+ superseded stacked-dashboard build; `0.2.0` is the dispatcher fix, published only after owner
162
+ confirmation).
85
163
 
86
164
  ## Development
87
165
 
88
166
  ```bash
89
- npm install # legacy-peer-deps (see .npmrc); peers come from vendor/*.tgz
90
- npm run typecheck # tsc
91
- npm test # vitest (jsdom) — 26 tests
92
- npm run dev # vite demo (two embeds, two files, EN/中文, dark × light)
167
+ npm install # registry peers pinned as devDependencies (see .npmrc) + cesium/resium
168
+ npm run typecheck # tsc (against the REAL cesium/resium types)
169
+ npm test # vitest (jsdom) — 56 tests; cesium/resium stubbed at the resolver level
170
+ npm run dev # vite demo (2×2 grid: map on the Cesium engine + relational/timeline/stats,
171
+ # plus the engine="svg" fallback panel; EN/中文, dark × light)
93
172
  npm run build # emits dist/ with registry peer imports
173
+ npm run shot-demo # captures /tmp/guanlan-review/demo-geovision-embed-v3.png
94
174
  ```
95
175
 
176
+ **Tests** follow the globe viewport's mocking strategy: jsdom cannot host the ~30MB WebGL
177
+ engine, so `vitest.config.ts` aliases `cesium`/`resium` to `test/stubs/` — plain surface stubs
178
+ that record the Viewer constructor props, `camera.setView` calls, `scene.requestRender` counts,
179
+ the `globe.tileCacheSize` assignment and viewer destroys. The library build (tsc) still
180
+ typechecks against the real packages. `test/setup.ts` additionally installs an always-visible
181
+ IntersectionObserver default; the cesium-engine suite installs its own controllable mock to
182
+ drive enter/exit visibility transitions.
183
+
96
184
  Source layout (each module ≤250 lines): `src/GeovisionEmbed.tsx` (contract + failure frames),
97
- `src/resolveView.ts` (IFileEntry.read + fail-closed validation), `src/seed.ts` (ChannelScope
98
- seeding), `src/EmbedBody.tsx` (scoped composition of the real packages + map + rail + replay),
99
- `src/describeViews.ts`, `src/map/` (projection, replay math, heat blobs, side coloring, tokens,
100
- MiniMap, ReplayControl), `src/rail/LayerRailCompact.tsx`, `src/locales/` (en + zh-CN with exact
101
- key parity; `stringsForLocale` falls back to en).
185
+ `src/resolveView.ts` (IFileEntry.read + fail-closed validation + previewKind resolution),
186
+ `src/views.ts` (view-id addressing: `viewIdFor` / `previewKindOf` / `resolveViewKind`),
187
+ `src/seed.ts` (ChannelScope seeding), `src/EmbedBody.tsx` (the DISPATCHER — one variant per
188
+ invocation), `src/map/CesiumMapVariant.tsx` (the map variant's engine dispatch: SVG-only
189
+ degradation paths + the Cesium engine surface — observer gate, ready-gate crossfade, replay
190
+ wiring), `src/map/CesiumMarkersLayer.tsx` (the Resium entity layer),
191
+ `src/map/cesiumModel.ts` (pure marker/camera/imagery derivations),
192
+ `src/map/MapVariant.tsx` (the SVG variant + its chrome), `src/map/railState.ts` (shared
193
+ initial-rail derivation), `src/describeViews.ts`, `src/map/` (projection, replay math, heat
194
+ blobs, side coloring, tokens, MiniMap, ReplayControl), `src/rail/LayerRailCompact.tsx`,
195
+ `src/locales/` (en + zh-CN with exact key parity; `stringsForLocale` falls back to en).
@@ -1,17 +1,30 @@
1
1
  import type { GeovisionViewFile } from "@zephytiju/lattice-common-bundle/geovisionView";
2
+ import type { GeovisionPreviewKind, GeovisionMapEngine } from "./types.js";
2
3
  import type { GeovisionEmbedStrings } from "./locales/index.js";
3
4
  /**
4
- * The seeded render surface: an ISOLATED ChannelScope carrying the file's
5
- * snapshot channels wraps the REAL packages (relation-fluxboard,
6
- * timeline-board, stats-panel) and the simplified mini-map. Every internal
7
- * control — the compact layer rail and the video-like replay — stays inside
8
- * the slot; nothing an embed does navigates away. Static frames wrap the
9
- * whole body with pointerEvents none (no pointer capture at all).
5
+ * The DISPATCHER: ONE invocation renders exactly ONE variant, selected by
6
+ * the saved view's previewKind —
7
+ * - "map" → the Cesium engine map (default) — an SVG snapshot
8
+ * placeholder crossfading into the lazily-mounted ION-FREE
9
+ * Cesium viewer with the video-like replay control on top;
10
+ * engine="svg" opts into the lighter static SVG map (the
11
+ * variant carrying the compact layer rail);
12
+ * - "relational" → the real prism-relation-fluxboard, rendered as-is;
13
+ * - "timeline" → the real prism-timeline-board, rendered as-is;
14
+ * - "stats" → the real prism-stats-panel, rendered as-is.
15
+ * Whatever renders fills the slot's full width/height — no stacking, no
16
+ * sections, no tabs. An ISOLATED ChannelScope seeded from the view's
17
+ * snapshot wraps the variant, so every embed renders its own file and
18
+ * nothing leaks across embeds. Static frames wrap the body with
19
+ * pointerEvents none (no pointer capture at all).
10
20
  */
11
- export declare function EmbedBody({ file, locale, staticFrame, strings, }: {
21
+ export declare function EmbedBody({ file, previewKind, locale, staticFrame, engine, resolutionScale, strings, }: {
12
22
  readonly file: GeovisionViewFile;
23
+ readonly previewKind: GeovisionPreviewKind;
13
24
  readonly locale: "en" | "zh-CN";
14
25
  readonly staticFrame: boolean;
26
+ readonly engine: GeovisionMapEngine;
27
+ readonly resolutionScale?: number;
15
28
  readonly strings: GeovisionEmbedStrings;
16
29
  }): React.ReactElement;
17
30
  //# sourceMappingURL=EmbedBody.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"EmbedBody.d.ts","sourceRoot":"","sources":["../src/EmbedBody.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gDAAgD,CAAC;AAUxF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAEhE;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,EACxB,IAAI,EACJ,MAAM,EACN,WAAW,EACX,OAAO,GACR,EAAE;IACD,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;CACzC,GAAG,KAAK,CAAC,YAAY,CAiJrB"}
1
+ {"version":3,"file":"EmbedBody.d.ts","sourceRoot":"","sources":["../src/EmbedBody.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gDAAgD,CAAC;AAMxF,OAAO,KAAK,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC3E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAyBhE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,SAAS,CAAC,EACxB,IAAI,EACJ,WAAW,EACX,MAAM,EACN,WAAW,EACX,MAAM,EACN,eAAe,EACf,OAAO,GACR,EAAE;IACD,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,oBAAoB,CAAC;IAC3C,QAAQ,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;CACzC,GAAG,KAAK,CAAC,YAAY,CA6DrB"}
package/dist/EmbedBody.js CHANGED
@@ -1,81 +1,69 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useMemo, useRef, useState } from "react";
2
+ import { useMemo } from "react";
3
3
  import { ChannelScope } from "@zephytiju/prism-react";
4
4
  import { RelationFluxboard } from "@zephytiju/prism-relation-fluxboard";
5
5
  import { TimelineBoard } from "@zephytiju/prism-timeline-board";
6
6
  import { StatsPanel } from "@zephytiju/prism-stats-panel";
7
7
  import { seedFromFile } from "./seed.js";
8
- import { MiniMap } from "./map/MiniMap.js";
9
- import { ReplayControl } from "./map/ReplayControl.js";
10
- import { hasMotion, buildTracks } from "./map/replay.js";
11
- import { LayerRailCompact } from "./rail/LayerRailCompact.js";
8
+ import { CesiumMapVariant } from "./map/CesiumMapVariant.js";
12
9
  /**
13
- * The seeded render surface: an ISOLATED ChannelScope carrying the file's
14
- * snapshot channels wraps the REAL packages (relation-fluxboard,
15
- * timeline-board, stats-panel) and the simplified mini-map. Every internal
16
- * control — the compact layer rail and the video-like replay — stays inside
17
- * the slot; nothing an embed does navigates away. Static frames wrap the
18
- * whole body with pointerEvents none (no pointer capture at all).
10
+ * Scoped fit rules (unique to this component's slots): the mounted packages
11
+ * carry their own INTRINSIC inline root widths (460/540/360 px — their
12
+ * standalone panel sizes). Inside an embed the ONE dispatched variant must
13
+ * fill the slot's full width/height, so the direct package root yields to
14
+ * the slot contract (`!important` overrides their inline widths; the
15
+ * packages themselves render as-is, unmodified and un-wrapped in chrome).
19
16
  */
20
- export function EmbedBody({ file, locale, staticFrame, strings, }) {
17
+ const FIT_CSS = [
18
+ ".geovision-embed-fit{width:100%;height:100%;min-width:0;min-height:0;overflow:hidden;}",
19
+ '.geovision-embed-fit>[data-testid="relation-fluxboard"],',
20
+ '.geovision-embed-fit>[data-testid="timeline-board"],',
21
+ '.geovision-embed-fit>[data-testid="stats-panel"]',
22
+ "{width:100% !important;height:100% !important;}",
23
+ ].join("");
24
+ /** Full-slot wrapper — the ONE rendered variant fills the embed's width/height. */
25
+ function variantSlot(children) {
26
+ return (_jsx("div", { className: "geovision-embed-fit", children: children }));
27
+ }
28
+ /**
29
+ * The DISPATCHER: ONE invocation renders exactly ONE variant, selected by
30
+ * the saved view's previewKind —
31
+ * - "map" → the Cesium engine map (default) — an SVG snapshot
32
+ * placeholder crossfading into the lazily-mounted ION-FREE
33
+ * Cesium viewer with the video-like replay control on top;
34
+ * engine="svg" opts into the lighter static SVG map (the
35
+ * variant carrying the compact layer rail);
36
+ * - "relational" → the real prism-relation-fluxboard, rendered as-is;
37
+ * - "timeline" → the real prism-timeline-board, rendered as-is;
38
+ * - "stats" → the real prism-stats-panel, rendered as-is.
39
+ * Whatever renders fills the slot's full width/height — no stacking, no
40
+ * sections, no tabs. An ISOLATED ChannelScope seeded from the view's
41
+ * snapshot wraps the variant, so every embed renders its own file and
42
+ * nothing leaks across embeds. Static frames wrap the body with
43
+ * pointerEvents none (no pointer capture at all).
44
+ */
45
+ export function EmbedBody({ file, previewKind, locale, staticFrame, engine, resolutionScale, strings, }) {
21
46
  const seeded = useMemo(() => seedFromFile(file), [file]);
22
- const containerRef = useRef(null);
23
47
  // The mounted packages accept only en | "zh-CN" — unknown locales resolve
24
48
  // onto the en fallback before crossing the seam.
25
49
  const innerLocale = locale === "zh-CN" ? "zh-CN" : "en";
26
- const initialRail = useMemo(() => file.view.layers.map((layer, index) => {
27
- if (layer.kind === "overlay") {
28
- return { id: `overlay-${String(index)}`, kind: "overlay", name: "markers", enabled: true, opacity: layer.opacity };
29
- }
30
- if (layer.kind === "heat") {
31
- return { id: `heat-${String(index)}`, kind: "heat", name: layer.selection.name, enabled: true, opacity: layer.intensity };
32
- }
33
- return { id: `side-color-${String(index)}`, kind: "side-color", name: "sides", enabled: true, opacity: 1 };
34
- }), [file]);
35
- const [rail, setRail] = useState(initialRail);
36
- const railForFile = useMemo(() => (rail.length === initialRail.length ? rail : initialRail), [rail, initialRail]);
37
- const replayWindow = useMemo(() => {
38
- const start = Date.parse(seeded.window.start);
39
- const end = Date.parse(seeded.window.end);
40
- return Number.isFinite(start) && Number.isFinite(end) && end > start
41
- ? { startMs: start, endMs: end }
42
- : { startMs: 0, endMs: 3600_000 };
43
- }, [seeded]);
44
- const [fraction, setFraction] = useState(0);
45
- const [playing, setPlaying] = useState(false);
46
- const onFraction = useCallback((next) => {
47
- const clamped = Math.min(1, Math.max(0, next));
48
- setFraction(clamped);
49
- if (clamped >= 1)
50
- setPlaying(false);
51
- }, []);
52
- const located = useMemo(() => (file.snapshot.entityResults?.items ?? []).filter((item) => item.lat !== undefined && item.lon !== undefined), [file]);
53
- const motion = useMemo(() => hasMotion(buildTracks(located, file.snapshot.timelineEvents, replayWindow)), [located, file, replayWindow]);
54
- const toggleLayer = useCallback((id) => {
55
- setRail((previous) => previous.map((layer) => (layer.id === id ? { ...layer, enabled: !layer.enabled } : layer)));
56
- }, []);
57
- const setOpacity = useCallback((id, opacity) => {
58
- setRail((previous) => previous.map((layer) => (layer.id === id ? { ...layer, opacity } : layer)));
59
- }, []);
60
- return (_jsx(ChannelScope, { seed: seeded.seed, children: _jsxs("div", { ref: containerRef, "data-testid": "geovision-embed-body", style: {
50
+ return (_jsx(ChannelScope, { seed: seeded.seed, children: _jsxs("div", { "data-testid": "geovision-embed-body", "data-preview-kind": previewKind, style: {
61
51
  width: "100%",
62
52
  height: "100%",
63
- display: "flex",
64
- flexDirection: "column",
65
53
  overflow: "hidden",
54
+ position: "relative",
66
55
  ...(staticFrame ? { pointerEvents: "none" } : {}),
67
- }, children: [_jsxs("div", { style: { position: "relative", flex: "1 1 46%", minHeight: 120 }, children: [_jsx(MiniMap, { file: file, container: containerRef.current, rail: railForFile, replayFraction: fraction, replayWindow: replayWindow, strings: strings }), _jsx(LayerRailCompact, { layers: railForFile, disabled: staticFrame, strings: strings, onToggle: toggleLayer, onOpacity: setOpacity }), staticFrame ? (_jsx("div", { "data-testid": "geovision-embed-static-chip", style: {
68
- position: "absolute",
69
- right: 6,
70
- top: 6,
71
- fontSize: 8,
72
- padding: "1px 6px",
73
- borderRadius: 3,
74
- fontFamily: "var(--mantine-font-family-monospace)",
75
- background: "var(--mantine-color-input-filled)",
76
- color: "var(--mantine-color-muted-filled)",
77
- border: "1px solid var(--mantine-color-border-filled)",
78
- }, children: strings.staticFrame })) : null] }), _jsx(ReplayControl, { fraction: fraction, playing: playing, disabled: staticFrame || !motion, timeWindow: replayWindow, strings: strings, onFraction: onFraction, onTogglePlay: () => {
79
- setPlaying((value) => !value);
80
- } }), _jsxs("div", { style: { display: "flex", gap: 4, flex: "1 1 30%", minHeight: 96, padding: 4 }, children: [_jsx("div", { style: { flex: 1, minWidth: 0, overflow: "hidden" }, children: _jsx(RelationFluxboard, { locale: innerLocale, title: strings.fluxTitle, subtitle: strings.fluxSubtitle }) }), _jsx("div", { style: { width: 210, flexShrink: 0, overflow: "hidden" }, children: _jsx(StatsPanel, { locale: innerLocale, window: seeded.window, title: strings.statsTitle, subtitle: strings.statsSubtitle }) })] }), _jsx("div", { style: { height: 108, flexShrink: 0, padding: 4, overflow: "hidden" }, children: _jsx(TimelineBoard, { locale: innerLocale, defaultWindow: seeded.window, live: false, title: strings.timelineTitle, subtitle: strings.timelineSubtitle }) })] }) }));
56
+ }, children: [_jsx("style", { children: FIT_CSS }), previewKind === "map" ? (variantSlot(_jsx(CesiumMapVariant, { file: file, seed: seeded, staticFrame: staticFrame, engine: engine, resolutionScale: resolutionScale, strings: strings }))) : previewKind === "relational" ? (variantSlot(_jsx(RelationFluxboard, { locale: innerLocale }))) : previewKind === "timeline" ? (variantSlot(_jsx(TimelineBoard, { locale: innerLocale, defaultWindow: seeded.window, live: false }))) : (variantSlot(_jsx(StatsPanel, { locale: innerLocale, window: seeded.window }))), staticFrame ? (_jsx("div", { "data-testid": "geovision-embed-static-chip", style: {
57
+ position: "absolute",
58
+ right: 6,
59
+ top: 6,
60
+ fontSize: 8,
61
+ padding: "1px 6px",
62
+ borderRadius: 3,
63
+ fontFamily: "var(--mantine-font-family-monospace)",
64
+ background: "var(--mantine-color-input-filled)",
65
+ color: "var(--mantine-color-muted-filled)",
66
+ border: "1px solid var(--mantine-color-border-filled)",
67
+ zIndex: 5,
68
+ }, children: strings.staticFrame })) : null] }) }));
81
69
  }
@@ -3,18 +3,20 @@ import type { GeovisionEmbedProps } from "./types.js";
3
3
  export declare const MIN_RICH_HEIGHT = 240;
4
4
  /**
5
5
  * Platform Prism geovision-embed micro-UI (component id "geovision-embed")
6
- * — the agreed dossier-embed render component:
6
+ * — the agreed dossier-embed render component, as a DISPATCHER:
7
7
  * `<GeovisionEmbed sourceFileRef viewId mode? interactionMode? locale? width height />`.
8
8
  *
9
9
  * The component resolves { sourceFileRef, viewId } through the HOST-supplied
10
10
  * Lattice transport (IFileEntry.read — the embed installs NO executor of
11
11
  * its own, so in static mode no live fetches occur beyond the host's own
12
12
  * routes), validates the file against the authoritative geovision-view
13
- * format (fail-closed: structure + digest), then seeds an isolated
14
- * ChannelScope and mounts the real packages + the simplified SVG map.
15
- * "live" mode is reserved and behaves as static in v1. Below 240px height
16
- * or with interactionMode="static" the slot degrades to a static frame
17
- * (pointer-events captured nowhere).
13
+ * format (fail-closed: structure + digest), then reads the view's
14
+ * previewKind and renders exactly ONE variant — the simplified SVG map
15
+ * (with layers, heat, side coloring and replay) OR the real
16
+ * relation-fluxboard OR timeline-board OR stats-panel — each filling the
17
+ * slot's full width/height. "live" mode is reserved and behaves as static
18
+ * in v1. Below 240px height or with interactionMode="static" the slot
19
+ * degrades to a static frame (pointer-events captured nowhere).
18
20
  */
19
- export declare function GeovisionEmbed({ sourceFileRef, viewId, mode, interactionMode, locale, width, height, }: GeovisionEmbedProps): React.ReactElement;
21
+ export declare function GeovisionEmbed({ sourceFileRef, viewId, mode, interactionMode, engine, resolutionScale, locale, width, height, }: GeovisionEmbedProps): React.ReactElement;
20
22
  //# sourceMappingURL=GeovisionEmbed.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"GeovisionEmbed.d.ts","sourceRoot":"","sources":["../src/GeovisionEmbed.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAItD,yFAAyF;AACzF,eAAO,MAAM,eAAe,MAAM,CAAC;AAEnC;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,EAC7B,aAAa,EACb,MAAM,EACN,IAAe,EACf,eAAwB,EACxB,MAAa,EACb,KAAc,EACd,MAAM,GACP,EAAE,mBAAmB,GAAG,KAAK,CAAC,YAAY,CAwD1C"}
1
+ {"version":3,"file":"GeovisionEmbed.d.ts","sourceRoot":"","sources":["../src/GeovisionEmbed.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAItD,yFAAyF;AACzF,eAAO,MAAM,eAAe,MAAM,CAAC;AAEnC;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,EAC7B,aAAa,EACb,MAAM,EACN,IAAe,EACf,eAAwB,EACxB,MAAiB,EACjB,eAAe,EACf,MAAa,EACb,KAAc,EACd,MAAM,GACP,EAAE,mBAAmB,GAAG,KAAK,CAAC,YAAY,CAoE1C"}
@@ -8,20 +8,22 @@ import { EmbedBody } from "./EmbedBody.js";
8
8
  export const MIN_RICH_HEIGHT = 240;
9
9
  /**
10
10
  * Platform Prism geovision-embed micro-UI (component id "geovision-embed")
11
- * — the agreed dossier-embed render component:
11
+ * — the agreed dossier-embed render component, as a DISPATCHER:
12
12
  * `<GeovisionEmbed sourceFileRef viewId mode? interactionMode? locale? width height />`.
13
13
  *
14
14
  * The component resolves { sourceFileRef, viewId } through the HOST-supplied
15
15
  * Lattice transport (IFileEntry.read — the embed installs NO executor of
16
16
  * its own, so in static mode no live fetches occur beyond the host's own
17
17
  * routes), validates the file against the authoritative geovision-view
18
- * format (fail-closed: structure + digest), then seeds an isolated
19
- * ChannelScope and mounts the real packages + the simplified SVG map.
20
- * "live" mode is reserved and behaves as static in v1. Below 240px height
21
- * or with interactionMode="static" the slot degrades to a static frame
22
- * (pointer-events captured nowhere).
18
+ * format (fail-closed: structure + digest), then reads the view's
19
+ * previewKind and renders exactly ONE variant — the simplified SVG map
20
+ * (with layers, heat, side coloring and replay) OR the real
21
+ * relation-fluxboard OR timeline-board OR stats-panel — each filling the
22
+ * slot's full width/height. "live" mode is reserved and behaves as static
23
+ * in v1. Below 240px height or with interactionMode="static" the slot
24
+ * degrades to a static frame (pointer-events captured nowhere).
23
25
  */
24
- export function GeovisionEmbed({ sourceFileRef, viewId, mode = "static", interactionMode = "rich", locale = "en", width = "100%", height, }) {
26
+ export function GeovisionEmbed({ sourceFileRef, viewId, mode = "static", interactionMode = "rich", engine = "cesium", resolutionScale, locale = "en", width = "100%", height, }) {
25
27
  const strings = stringsForLocale(locale);
26
28
  const transport = useLatticeTransport();
27
29
  const resolved = useResolvedView(transport, sourceFileRef, viewId);
@@ -29,7 +31,7 @@ export function GeovisionEmbed({ sourceFileRef, viewId, mode = "static", interac
29
31
  const numericHeight = typeof height === "number" ? height : undefined;
30
32
  const richHeight = numericHeight === undefined || numericHeight >= MIN_RICH_HEIGHT;
31
33
  const staticFrame = interactionMode === "static" || !richHeight;
32
- return (_jsxs("div", { ref: containerRef, "data-testid": "geovision-embed", "data-embed-mode": mode, "data-interaction-mode": staticFrame ? "static" : "rich", "data-file-ref": sourceFileRef, "aria-label": strings.slotAria, style: {
34
+ return (_jsxs("div", { ref: containerRef, "data-testid": "geovision-embed", "data-embed-mode": mode, "data-interaction-mode": staticFrame ? "static" : "rich", "data-file-ref": sourceFileRef, "data-view-id": viewId, "data-preview-kind": resolved.previewKind ?? "", "aria-label": strings.slotAria, style: {
33
35
  width,
34
36
  height: height ?? "100%",
35
37
  position: "relative",
@@ -40,5 +42,7 @@ export function GeovisionEmbed({ sourceFileRef, viewId, mode = "static", interac
40
42
  ? strings.errNotFound
41
43
  : resolved.phase.code === "invalid"
42
44
  ? strings.errInvalid
43
- : strings.errUnknownView }), _jsx("span", { style: { fontSize: 9, color: "var(--mantine-color-muted-filled)" }, children: resolved.phase.message })] })) : null, resolved.phase.phase === "ready" && resolved.file !== null ? (_jsx(EmbedBody, { file: resolved.file, locale: locale, staticFrame: staticFrame, strings: strings })) : null] }));
45
+ : resolved.phase.code === "unknown-preview"
46
+ ? strings.errUnknownPreview
47
+ : strings.errUnknownView }), _jsx("span", { style: { fontSize: 9, color: "var(--mantine-color-muted-filled)" }, children: resolved.phase.message })] })) : null, resolved.phase.phase === "ready" && resolved.file !== null && resolved.previewKind !== null ? (_jsx(EmbedBody, { file: resolved.file, previewKind: resolved.previewKind, locale: locale, staticFrame: staticFrame, engine: engine, resolutionScale: resolutionScale, strings: strings })) : null] }));
44
48
  }
@@ -1,26 +1,21 @@
1
1
  /**
2
2
  * describeViews(sourceFileRef) — the editor's VIEW ▾ dropdown source: reads
3
3
  * the file through the host Lattice transport, validates it against the
4
- * authoritative format, and lists what it contains. A geovision-view file
5
- * carries exactly ONE saved view, so the descriptor's viewId IS the file
6
- * ref; the display name is derived from the file's recorded provenance
7
- * (savedBy + save time — the format carries no name field), and previewKind
8
- * summarizes the painted map variant.
4
+ * authoritative format, and lists the views it can embed. A validated file
5
+ * carries one saved view PER previewKind (map / relational / timeline /
6
+ * stats — the dispatcher's variants), addressed `${fileRef}#${previewKind}`;
7
+ * the descriptor's viewId is exactly what the editor passes back to
8
+ * <GeovisionEmbed viewId=…> and previewKind labels the dropdown row.
9
9
  */
10
10
  import type { LatticeTransport } from "@zephytiju/prism-react";
11
11
  import type { GeovisionViewDescriptor } from "./types.js";
12
- /** Summarizes the painted map variant from the file's layer set. */
13
- export declare function previewKindFor(layers: readonly {
14
- readonly kind: string;
15
- }[]): GeovisionViewDescriptor["previewKind"];
16
- /** Descriptor for a validated file (one saved view per file). */
17
- export declare function describeFile(sourceFileRef: string, savedBy: string, savedAt: string, layers: readonly {
18
- readonly kind: string;
19
- }[]): GeovisionViewDescriptor;
12
+ /** One descriptor per previewKind — the file's complete embeddable view list. */
13
+ export declare function describeFile(sourceFileRef: string, savedBy: string, savedAt: string): readonly GeovisionViewDescriptor[];
20
14
  /**
21
- * Lists the file's saved views for the editor dropdown. Fails closed: an
22
- * unreadable or invalid file yields an empty list (the dropdown simply has
23
- * nothing to offer) — the EMBED surface renders the structured failure.
15
+ * Lists the file's saved views for the editor dropdown (one per
16
+ * previewKind). Fails closed: an unreadable or invalid file yields an empty
17
+ * list (the dropdown simply has nothing to offer) — the EMBED surface
18
+ * renders the structured failure.
24
19
  */
25
20
  export declare function describeViews(sourceFileRef: string, transport?: LatticeTransport): Promise<readonly GeovisionViewDescriptor[]>;
26
21
  //# sourceMappingURL=describeViews.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"describeViews.d.ts","sourceRoot":"","sources":["../src/describeViews.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG/D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAe1D,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,uBAAuB,CAAC,aAAa,CAAC,CAInH;AAED,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,uBAAuB,CAM3J;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,aAAa,EAAE,MAAM,EACrB,SAAS,GAAE,gBAA2C,GACrD,OAAO,CAAC,SAAS,uBAAuB,EAAE,CAAC,CAU7C"}
1
+ {"version":3,"file":"describeViews.d.ts","sourceRoot":"","sources":["../src/describeViews.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAK/D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAe1D,iFAAiF;AACjF,wBAAgB,YAAY,CAC1B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,GACd,SAAS,uBAAuB,EAAE,CAMpC;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,aAAa,EAAE,MAAM,EACrB,SAAS,GAAE,gBAA2C,GACrD,OAAO,CAAC,SAAS,uBAAuB,EAAE,CAAC,CAU7C"}