create-honua-app 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +50 -0
- package/bin/create-honua-app.mjs +7 -0
- package/lib/cli.mjs +149 -0
- package/lib/scaffold.mjs +134 -0
- package/lib/templates.mjs +138 -0
- package/package.json +32 -0
- package/templates/react-ts/.stackblitzrc +4 -0
- package/templates/react-ts/README.md +64 -0
- package/templates/react-ts/_gitignore +6 -0
- package/templates/react-ts/fixtures/features.json +46 -0
- package/templates/react-ts/fixtures/layer.json +34 -0
- package/templates/react-ts/index.html +12 -0
- package/templates/react-ts/package.json +31 -0
- package/templates/react-ts/src/App.tsx +153 -0
- package/templates/react-ts/src/fixture-endpoint.ts +9 -0
- package/templates/react-ts/src/main.tsx +18 -0
- package/templates/react-ts/src/styles.css +149 -0
- package/templates/react-ts/tsconfig.json +15 -0
- package/templates/react-ts/vite.config.ts +50 -0
- package/templates/vanilla-ts/.stackblitzrc +4 -0
- package/templates/vanilla-ts/README.md +60 -0
- package/templates/vanilla-ts/_gitignore +6 -0
- package/templates/vanilla-ts/fixtures/features.json +46 -0
- package/templates/vanilla-ts/fixtures/layer.json +34 -0
- package/templates/vanilla-ts/index.html +37 -0
- package/templates/vanilla-ts/package.json +27 -0
- package/templates/vanilla-ts/src/fixture-endpoint.ts +9 -0
- package/templates/vanilla-ts/src/main.ts +113 -0
- package/templates/vanilla-ts/src/styles.css +149 -0
- package/templates/vanilla-ts/tsconfig.json +14 -0
- package/templates/vanilla-ts/vite.config.ts +50 -0
- package/templates.manifest.json +61 -0
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-honua-app",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Scaffold a Honua JavaScript SDK map application: Vite + TypeScript + MapLibre starters that connect and mount a source without hand-assembling peers",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Honua",
|
|
7
|
+
"homepage": "https://honua-io.github.io/honua-sdk-js/",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/honua-io/honua-sdk-js.git",
|
|
11
|
+
"directory": "packages/create-honua-app"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/honua-io/honua-sdk-js/issues"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"provenance": true
|
|
19
|
+
},
|
|
20
|
+
"keywords": ["gis", "geospatial", "maplibre", "typescript", "honua", "scaffold", "starter", "create-app"],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"bin": {
|
|
23
|
+
"create-honua-app": "bin/create-honua-app.mjs"
|
|
24
|
+
},
|
|
25
|
+
"exports": {
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": ["bin", "lib", "templates", "templates.manifest.json", "README.md", "LICENSE"],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20.19.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Honua map app (React + TypeScript)
|
|
2
|
+
|
|
3
|
+
A Vite + React + TypeScript starter for the [Honua JavaScript SDK](https://github.com/honua-io/honua-sdk-js). The app
|
|
4
|
+
owns a plain `maplibre-gl` map and the SDK's React bridge mounts a discovered source onto it:
|
|
5
|
+
|
|
6
|
+
```text
|
|
7
|
+
connect → source → useMountedSource
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Run it
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install
|
|
14
|
+
npm run dev
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The default lane serves a committed GeoServices fixture from the Vite dev server, so the first map needs no account,
|
|
18
|
+
no API key, and no third-party network call. The same fixture is served by `npm run preview` after `npm run build`.
|
|
19
|
+
|
|
20
|
+
## Point it at live data
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
VITE_HONUA_ENDPOINT=https://your-public-service.example/rest/services/example/FeatureServer/0 npm run dev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- `VITE_HONUA_ENDPOINT` — an anonymous, CORS-enabled GeoServices FeatureServer layer or OGC API Features landing page.
|
|
27
|
+
- `VITE_HONUA_PROTOCOL` — `auto`, `geoservices-feature-service`, or `ogc-features` (default:
|
|
28
|
+
`geoservices-feature-service`, which matches the fixture).
|
|
29
|
+
|
|
30
|
+
Do not put durable credentials in Vite environment variables: Vite embeds them in public JavaScript. Use an anonymous
|
|
31
|
+
endpoint or a server-side proxy.
|
|
32
|
+
|
|
33
|
+
## What to edit
|
|
34
|
+
|
|
35
|
+
| File | Why |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `src/App.tsx` | Connection, query, and the mounted-source hook. Change the query here. |
|
|
38
|
+
| `src/main.tsx` | React root. `StrictMode` stays on: the SDK hooks are StrictMode-safe. |
|
|
39
|
+
| `src/fixture-endpoint.ts` | The same-origin path the offline fixture is served on. |
|
|
40
|
+
| `vite.config.ts` | The fixture service. Delete it once you point the app at a real endpoint. |
|
|
41
|
+
|
|
42
|
+
`HonuaMapProvider` publishes an app-owned map to descendants when the map is created deeper in the tree; this starter
|
|
43
|
+
passes the map to `useMountedSource` directly. Add `@vitejs/plugin-react` if you want React Fast Refresh — Vite
|
|
44
|
+
compiles the JSX without it.
|
|
45
|
+
|
|
46
|
+
## Dependencies
|
|
47
|
+
|
|
48
|
+
`@honua/sdk-js`, `maplibre-gl`, and React are the packages this app calls directly. `@bufbuild/protobuf` and the two
|
|
49
|
+
`@connectrpc` packages are the SDK's optional transport peers: they are installed and pinned here so the bundle builds
|
|
50
|
+
without any peer-dependency assembly, and they are what a Honua-server connection uses.
|
|
51
|
+
|
|
52
|
+
This starter pins **MapLibre GL JS 5.24.0**, the major the published `@honua/sdk-js` release accepts as an optional
|
|
53
|
+
peer. MapLibre 6 is supported by the SDK source (`^5.0.0 || ^6.0.0`) and ships in the next release; upgrading to 6.x
|
|
54
|
+
also means configuring MapLibre's ESM worker before the first map is created — under Vite, `setWorkerUrl` with
|
|
55
|
+
`import maplibreWorkerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url"`.
|
|
56
|
+
|
|
57
|
+
## Checks
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm run typecheck
|
|
61
|
+
npm run build
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The fixture data is synthetic demonstration data from the Honua SDK sample fixtures (Apache-2.0).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"objectIdFieldName": "OBJECTID",
|
|
3
|
+
"geometryType": "esriGeometryPolygon",
|
|
4
|
+
"spatialReference": { "wkid": 4326 },
|
|
5
|
+
"fields": [
|
|
6
|
+
{ "name": "OBJECTID", "type": "esriFieldTypeOID", "alias": "OBJECTID" },
|
|
7
|
+
{ "name": "NAME", "type": "esriFieldTypeString", "alias": "Name" },
|
|
8
|
+
{ "name": "STATUS", "type": "esriFieldTypeString", "alias": "Status" },
|
|
9
|
+
{ "name": "CATEGORY", "type": "esriFieldTypeString", "alias": "Category" }
|
|
10
|
+
],
|
|
11
|
+
"features": [
|
|
12
|
+
{
|
|
13
|
+
"attributes": {
|
|
14
|
+
"OBJECTID": 1,
|
|
15
|
+
"NAME": "Civic center service zone",
|
|
16
|
+
"STATUS": "Monitoring",
|
|
17
|
+
"CATEGORY": "Operations"
|
|
18
|
+
},
|
|
19
|
+
"geometry": {
|
|
20
|
+
"rings": [[[-157.866, 21.311], [-157.856, 21.311], [-157.856, 21.302], [-157.866, 21.302], [-157.866, 21.311]]]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"attributes": {
|
|
25
|
+
"OBJECTID": 2,
|
|
26
|
+
"NAME": "Harbor response district",
|
|
27
|
+
"STATUS": "Field review",
|
|
28
|
+
"CATEGORY": "Maritime"
|
|
29
|
+
},
|
|
30
|
+
"geometry": {
|
|
31
|
+
"rings": [[[-157.891, 21.304], [-157.879, 21.304], [-157.879, 21.294], [-157.891, 21.294], [-157.891, 21.304]]]
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"attributes": {
|
|
36
|
+
"OBJECTID": 3,
|
|
37
|
+
"NAME": "Kakaako utility corridor",
|
|
38
|
+
"STATUS": "Ready",
|
|
39
|
+
"CATEGORY": "Infrastructure"
|
|
40
|
+
},
|
|
41
|
+
"geometry": {
|
|
42
|
+
"rings": [[[-157.87, 21.296], [-157.856, 21.296], [-157.856, 21.286], [-157.87, 21.286], [-157.87, 21.296]]]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"currentVersion": 11.3,
|
|
3
|
+
"id": 0,
|
|
4
|
+
"name": "Honolulu operations areas",
|
|
5
|
+
"type": "Feature Layer",
|
|
6
|
+
"description": "The same three synthetic First Map polygons exposed by the OGC API Features projection.",
|
|
7
|
+
"copyrightText": "Copyright Honua contributors; synthetic demonstration data.",
|
|
8
|
+
"provenance": {
|
|
9
|
+
"source": "honua-sdk-js test/fixtures/honua-quickstart-demo",
|
|
10
|
+
"retrievedAt": "2026-07-13T00:00:00.000Z"
|
|
11
|
+
},
|
|
12
|
+
"geometryType": "esriGeometryPolygon",
|
|
13
|
+
"objectIdField": "OBJECTID",
|
|
14
|
+
"maxRecordCount": 25,
|
|
15
|
+
"advancedQueryCapabilities": {
|
|
16
|
+
"supportsPagination": true,
|
|
17
|
+
"supportsReturningQueryExtent": false,
|
|
18
|
+
"supportsStatistics": false
|
|
19
|
+
},
|
|
20
|
+
"extent": {
|
|
21
|
+
"xmin": -157.891,
|
|
22
|
+
"ymin": 21.286,
|
|
23
|
+
"xmax": -157.856,
|
|
24
|
+
"ymax": 21.311,
|
|
25
|
+
"spatialReference": { "wkid": 4326 }
|
|
26
|
+
},
|
|
27
|
+
"fields": [
|
|
28
|
+
{ "name": "OBJECTID", "type": "esriFieldTypeOID", "alias": "OBJECTID" },
|
|
29
|
+
{ "name": "NAME", "type": "esriFieldTypeString", "alias": "Name" },
|
|
30
|
+
{ "name": "STATUS", "type": "esriFieldTypeString", "alias": "Status" },
|
|
31
|
+
{ "name": "CATEGORY", "type": "esriFieldTypeString", "alias": "Category" }
|
|
32
|
+
],
|
|
33
|
+
"capabilities": "Query"
|
|
34
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Honua map app (React)</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "honua-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20.19.0"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "vite",
|
|
11
|
+
"build": "vite build",
|
|
12
|
+
"preview": "vite preview",
|
|
13
|
+
"typecheck": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@bufbuild/protobuf": "2.13.0",
|
|
17
|
+
"@connectrpc/connect": "2.1.2",
|
|
18
|
+
"@connectrpc/connect-web": "2.1.2",
|
|
19
|
+
"@honua/sdk-js": "0.1.2-beta.0",
|
|
20
|
+
"maplibre-gl": "5.24.0",
|
|
21
|
+
"react": "19.2.8",
|
|
22
|
+
"react-dom": "19.2.8"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "24.13.3",
|
|
26
|
+
"@types/react": "19.2.18",
|
|
27
|
+
"@types/react-dom": "19.2.4",
|
|
28
|
+
"typescript": "5.9.3",
|
|
29
|
+
"vite": "8.2.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { createHonua } from "@honua/sdk-js";
|
|
2
|
+
import type { ConnectProtocolHint, Source } from "@honua/sdk-js";
|
|
3
|
+
import { useMountedSource } from "@honua/sdk-js/react";
|
|
4
|
+
import * as maplibregl from "maplibre-gl";
|
|
5
|
+
import type { RefObject } from "react";
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
|
+
|
|
8
|
+
import { FIXTURE_LAYER_PATH } from "./fixture-endpoint.js";
|
|
9
|
+
|
|
10
|
+
/** Offline basemap: the starter never fetches third-party tiles on its default lane. */
|
|
11
|
+
const BASEMAP_STYLE = {
|
|
12
|
+
version: 8 as const,
|
|
13
|
+
sources: {},
|
|
14
|
+
layers: [{ id: "background", type: "background" as const, paint: { "background-color": "#0e1a17" } }],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const INITIAL_VIEW = { center: [-157.873, 21.2985] as [number, number], zoom: 11.4 };
|
|
18
|
+
|
|
19
|
+
/** Bounded row budget. The bridge reports truncation instead of hiding it. */
|
|
20
|
+
const MAX_FEATURES = 250;
|
|
21
|
+
|
|
22
|
+
const QUERY = { returnGeometry: true, pagination: { limit: MAX_FEATURES } };
|
|
23
|
+
|
|
24
|
+
const configuredEndpoint = import.meta.env.VITE_HONUA_ENDPOINT?.trim();
|
|
25
|
+
const configuredProtocol = import.meta.env.VITE_HONUA_PROTOCOL?.trim();
|
|
26
|
+
|
|
27
|
+
const ENDPOINT =
|
|
28
|
+
configuredEndpoint && configuredEndpoint.length > 0
|
|
29
|
+
? configuredEndpoint
|
|
30
|
+
: new URL(FIXTURE_LAYER_PATH, window.location.origin).toString();
|
|
31
|
+
const PROTOCOL = (
|
|
32
|
+
configuredProtocol && configuredProtocol.length > 0 ? configuredProtocol : "geoservices-feature-service"
|
|
33
|
+
) as ConnectProtocolHint;
|
|
34
|
+
const DATA_LANE = configuredEndpoint && configuredEndpoint.length > 0 ? "Live endpoint" : "Committed fixture";
|
|
35
|
+
|
|
36
|
+
interface ConnectionState {
|
|
37
|
+
readonly source: Source | null;
|
|
38
|
+
readonly sourceId?: string;
|
|
39
|
+
readonly protocol?: string;
|
|
40
|
+
readonly error?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The app owns its MapLibre map, exactly like a `@vis.gl/react-maplibre` `<Map>` would. */
|
|
44
|
+
function useExternalMap(): { containerRef: RefObject<HTMLDivElement | null>; map: maplibregl.Map | null } {
|
|
45
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
46
|
+
const [map, setMap] = useState<maplibregl.Map | null>(null);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const container = containerRef.current;
|
|
50
|
+
if (!container) return;
|
|
51
|
+
const created = new maplibregl.Map({
|
|
52
|
+
container,
|
|
53
|
+
style: BASEMAP_STYLE,
|
|
54
|
+
center: INITIAL_VIEW.center,
|
|
55
|
+
zoom: INITIAL_VIEW.zoom,
|
|
56
|
+
attributionControl: false,
|
|
57
|
+
});
|
|
58
|
+
setMap(created);
|
|
59
|
+
return () => {
|
|
60
|
+
setMap(null);
|
|
61
|
+
created.remove();
|
|
62
|
+
};
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
return { containerRef, map };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Connect once per effect run and publish the discovered source. */
|
|
69
|
+
function useHonuaSource(): ConnectionState {
|
|
70
|
+
const [state, setState] = useState<ConnectionState>({ source: null });
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const honua = createHonua();
|
|
74
|
+
let cancelled = false;
|
|
75
|
+
void (async () => {
|
|
76
|
+
try {
|
|
77
|
+
const connection = await honua.connect(
|
|
78
|
+
{ url: ENDPOINT, protocol: PROTOCOL },
|
|
79
|
+
{ authorizationScopeFingerprint: "anonymous-public" },
|
|
80
|
+
);
|
|
81
|
+
const inspection = await connection.inspect();
|
|
82
|
+
const sourceId = inspection.defaultSourceId ?? inspection.sources[0]?.descriptor.id;
|
|
83
|
+
if (!sourceId) throw new Error("The endpoint advertised no queryable source.");
|
|
84
|
+
if (cancelled) return;
|
|
85
|
+
setState({ source: connection.source(sourceId), sourceId, protocol: inspection.protocol });
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (!cancelled) setState({ source: null, error: error instanceof Error ? error.message : String(error) });
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
return () => {
|
|
91
|
+
cancelled = true;
|
|
92
|
+
void honua.dispose();
|
|
93
|
+
};
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
return state;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function App() {
|
|
100
|
+
const { containerRef, map } = useExternalMap();
|
|
101
|
+
const connection = useHonuaSource();
|
|
102
|
+
const mounted = useMountedSource(map, connection.source, { query: QUERY, fitBounds: true });
|
|
103
|
+
|
|
104
|
+
const failure = connection.error ?? (mounted.error instanceof Error ? mounted.error.message : undefined);
|
|
105
|
+
const status = failure
|
|
106
|
+
? `The workflow stopped: ${failure}`
|
|
107
|
+
: mounted.handle
|
|
108
|
+
? "The queried source is mounted through the React bridge."
|
|
109
|
+
: "Connecting, discovering, and mounting…";
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<main className="app">
|
|
113
|
+
<section className="panel" aria-label="Workflow">
|
|
114
|
+
<p className="eyebrow">Honua JavaScript SDK · React</p>
|
|
115
|
+
<h1 className="title">connect → source → useMountedSource</h1>
|
|
116
|
+
<output className="status" aria-live="polite">
|
|
117
|
+
{status}
|
|
118
|
+
</output>
|
|
119
|
+
<dl className="facts">
|
|
120
|
+
<div>
|
|
121
|
+
<dt>Data lane</dt>
|
|
122
|
+
<dd>{DATA_LANE}</dd>
|
|
123
|
+
</div>
|
|
124
|
+
<div>
|
|
125
|
+
<dt>Endpoint</dt>
|
|
126
|
+
<dd>{ENDPOINT}</dd>
|
|
127
|
+
</div>
|
|
128
|
+
<div>
|
|
129
|
+
<dt>Protocol</dt>
|
|
130
|
+
<dd>{connection.protocol ?? "—"}</dd>
|
|
131
|
+
</div>
|
|
132
|
+
<div>
|
|
133
|
+
<dt>Source</dt>
|
|
134
|
+
<dd>{connection.sourceId ?? "—"}</dd>
|
|
135
|
+
</div>
|
|
136
|
+
<div>
|
|
137
|
+
<dt>Strategy</dt>
|
|
138
|
+
<dd>{mounted.diagnostics?.strategy ?? "—"}</dd>
|
|
139
|
+
</div>
|
|
140
|
+
<div>
|
|
141
|
+
<dt>Features</dt>
|
|
142
|
+
<dd id="fact-features">{mounted.diagnostics?.featureCount ?? "—"}</dd>
|
|
143
|
+
</div>
|
|
144
|
+
</dl>
|
|
145
|
+
<p className="hint">
|
|
146
|
+
Edit <code>src/App.tsx</code> to change the query, or set <code>VITE_HONUA_ENDPOINT</code> to run the same
|
|
147
|
+
components against a live public service.
|
|
148
|
+
</p>
|
|
149
|
+
</section>
|
|
150
|
+
<div className="map" ref={containerRef} role="application" aria-label="Mounted map" />
|
|
151
|
+
</main>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-origin path the committed fixture service answers on.
|
|
3
|
+
*
|
|
4
|
+
* `vite.config.ts` serves the fixture here in both `npm run dev` and
|
|
5
|
+
* `npm run preview`, and `src/main.ts` connects here whenever no live endpoint
|
|
6
|
+
* is configured. Keeping the path in one module is what makes the default lane
|
|
7
|
+
* work with no account, no key, and no third-party network call.
|
|
8
|
+
*/
|
|
9
|
+
export const FIXTURE_LAYER_PATH = "/fixture/rest/services/honua-demo/FeatureServer/0";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import "maplibre-gl/dist/maplibre-gl.css";
|
|
2
|
+
|
|
3
|
+
import { StrictMode } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
|
|
6
|
+
import { App } from "./App.js";
|
|
7
|
+
import "./styles.css";
|
|
8
|
+
|
|
9
|
+
const container = document.getElementById("root");
|
|
10
|
+
if (!container) throw new Error("Missing #root element");
|
|
11
|
+
|
|
12
|
+
// StrictMode double-invokes effects in development. The SDK's React hooks are
|
|
13
|
+
// StrictMode-safe, so the mount/unmount/mount cycle never leaks a map source.
|
|
14
|
+
createRoot(container).render(
|
|
15
|
+
<StrictMode>
|
|
16
|
+
<App />
|
|
17
|
+
</StrictMode>,
|
|
18
|
+
);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--surface: #f6f7f5;
|
|
3
|
+
--panel: #ffffff;
|
|
4
|
+
--ink: #10201b;
|
|
5
|
+
--muted: #5c6b66;
|
|
6
|
+
--line: #d9e0dc;
|
|
7
|
+
--accent: #0f766e;
|
|
8
|
+
color-scheme: light dark;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@media (prefers-color-scheme: dark) {
|
|
12
|
+
:root {
|
|
13
|
+
--surface: #0b1210;
|
|
14
|
+
--panel: #111a17;
|
|
15
|
+
--ink: #e8f0ec;
|
|
16
|
+
--muted: #93a49e;
|
|
17
|
+
--line: #23322c;
|
|
18
|
+
--accent: #5eead4;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
* {
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
body {
|
|
27
|
+
margin: 0;
|
|
28
|
+
background: var(--surface);
|
|
29
|
+
color: var(--ink);
|
|
30
|
+
font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.app {
|
|
34
|
+
display: grid;
|
|
35
|
+
grid-template-columns: minmax(280px, 360px) 1fr;
|
|
36
|
+
gap: 16px;
|
|
37
|
+
padding: 16px;
|
|
38
|
+
height: 100vh;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@media (max-width: 820px) {
|
|
42
|
+
.app {
|
|
43
|
+
grid-template-columns: 1fr;
|
|
44
|
+
grid-template-rows: auto minmax(320px, 1fr);
|
|
45
|
+
height: auto;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.panel {
|
|
50
|
+
background: var(--panel);
|
|
51
|
+
border: 1px solid var(--line);
|
|
52
|
+
border-radius: 12px;
|
|
53
|
+
padding: 20px;
|
|
54
|
+
overflow-y: auto;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.eyebrow {
|
|
58
|
+
margin: 0;
|
|
59
|
+
color: var(--accent);
|
|
60
|
+
font-size: 12px;
|
|
61
|
+
font-weight: 600;
|
|
62
|
+
letter-spacing: 0.08em;
|
|
63
|
+
text-transform: uppercase;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.title {
|
|
67
|
+
margin: 6px 0 12px;
|
|
68
|
+
font-size: 18px;
|
|
69
|
+
line-height: 1.3;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.status {
|
|
73
|
+
margin: 0 0 16px;
|
|
74
|
+
color: var(--muted);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.stages {
|
|
78
|
+
list-style: none;
|
|
79
|
+
margin: 0 0 20px;
|
|
80
|
+
padding: 0;
|
|
81
|
+
display: grid;
|
|
82
|
+
gap: 6px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.stages li {
|
|
86
|
+
display: flex;
|
|
87
|
+
justify-content: space-between;
|
|
88
|
+
gap: 12px;
|
|
89
|
+
padding: 8px 10px;
|
|
90
|
+
border: 1px solid var(--line);
|
|
91
|
+
border-radius: 8px;
|
|
92
|
+
font-size: 13px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.stages li[data-state="running"] {
|
|
96
|
+
border-color: var(--accent);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.stages li[data-state="done"] .stage-state {
|
|
100
|
+
color: var(--accent);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.stages li[data-state="failed"] .stage-state {
|
|
104
|
+
color: #dc2626;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.stage-state {
|
|
108
|
+
color: var(--muted);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.facts {
|
|
112
|
+
display: grid;
|
|
113
|
+
gap: 8px;
|
|
114
|
+
margin: 0 0 16px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.facts div {
|
|
118
|
+
display: grid;
|
|
119
|
+
grid-template-columns: 96px 1fr;
|
|
120
|
+
gap: 10px;
|
|
121
|
+
font-size: 13px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.facts dt {
|
|
125
|
+
color: var(--muted);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.facts dd {
|
|
129
|
+
margin: 0;
|
|
130
|
+
overflow-wrap: anywhere;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.hint {
|
|
134
|
+
margin: 0;
|
|
135
|
+
color: var(--muted);
|
|
136
|
+
font-size: 13px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
code {
|
|
140
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
|
|
141
|
+
font-size: 12px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.map {
|
|
145
|
+
min-height: 360px;
|
|
146
|
+
border: 1px solid var(--line);
|
|
147
|
+
border-radius: 12px;
|
|
148
|
+
overflow: hidden;
|
|
149
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"verbatimModuleSyntax": true,
|
|
10
|
+
"allowSyntheticDefaultImports": true,
|
|
11
|
+
"jsx": "react-jsx",
|
|
12
|
+
"types": ["vite/client", "node"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { type Plugin, defineConfig } from "vite";
|
|
6
|
+
|
|
7
|
+
import { FIXTURE_LAYER_PATH } from "./src/fixture-endpoint.js";
|
|
8
|
+
|
|
9
|
+
function fixtureDocument(name: string): string {
|
|
10
|
+
return readFileSync(fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url)), "utf8");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Deterministic, offline GeoServices FeatureServer fixture.
|
|
15
|
+
*
|
|
16
|
+
* The starter's green path must never depend on a third-party endpoint, so the
|
|
17
|
+
* dev and preview servers answer the two routes the SDK's GeoServices adapter
|
|
18
|
+
* needs: the layer description and its bounded query. Point
|
|
19
|
+
* `VITE_HONUA_ENDPOINT` at a real service to run the same code against live data.
|
|
20
|
+
*/
|
|
21
|
+
function honuaFixtureService(): Plugin {
|
|
22
|
+
const layer = fixtureDocument("layer.json");
|
|
23
|
+
const features = fixtureDocument("features.json");
|
|
24
|
+
const middleware = (request: IncomingMessage, response: ServerResponse, next: () => void): void => {
|
|
25
|
+
const pathname = new URL(request.url ?? "/", "http://localhost").pathname;
|
|
26
|
+
let body: string | undefined;
|
|
27
|
+
if (pathname === FIXTURE_LAYER_PATH) body = layer;
|
|
28
|
+
else if (pathname === `${FIXTURE_LAYER_PATH}/query`) body = features;
|
|
29
|
+
if (body === undefined) {
|
|
30
|
+
next();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
34
|
+
response.setHeader("cache-control", "no-store");
|
|
35
|
+
response.end(body);
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
name: "honua-fixture-service",
|
|
39
|
+
configureServer(server) {
|
|
40
|
+
server.middlewares.use(middleware);
|
|
41
|
+
},
|
|
42
|
+
configurePreviewServer(server) {
|
|
43
|
+
server.middlewares.use(middleware);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default defineConfig({
|
|
49
|
+
plugins: [honuaFixtureService()],
|
|
50
|
+
});
|