@jackbernnie/hiyf 0.3.5 → 0.3.7

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.
@@ -6,10 +6,17 @@
6
6
  * This wrapper closes it:
7
7
  * - Location is expressed only as decimal latitude/longitude with a
8
8
  * clamped zoom.
9
- * - The embed always uses the approved responsive 16:9 frame, rounded
10
- * corners, muted loading surface, and a keyless OpenStreetMap embed with
11
- * a marker pin — reliable for every coordinate (Google's keyless embed is
12
- * deprecated and renders inconsistently).
9
+ * - `variant="street"` (default) renders the approved responsive 16:9
10
+ * keyless OpenStreetMap embed with a marker pin reliable for every
11
+ * coordinate (Google's keyless embed is deprecated and renders
12
+ * inconsistently).
13
+ * - `variant="aerial"` renders satellite imagery at a FIXED height
14
+ * (256px, full width) so every aerial view is the same size: Esri World
15
+ * Imagery tiles stitched around the coordinate, a centered drop pin, and
16
+ * the required imagery attribution. The frame links out to Google Maps
17
+ * satellite view for full pan/zoom detail. OSM's embed has no imagery
18
+ * layer, and every other satellite embed needs an API key — static tile
19
+ * stitching is the reliable keyless path.
13
20
  * - NO `className`/`style` escape hatch. Need a new map affordance? Add it
14
21
  * here so every map stays on-system.
15
22
  */
@@ -18,10 +25,19 @@ export interface MapEmbedProps {
18
25
  lat: number;
19
26
  /** Longitude in decimal degrees. */
20
27
  lon: number;
21
- /** Zoom level 1-21. Defaults to 14. Controls the size of the map window. */
28
+ /**
29
+ * Zoom level 1-21. Controls the size of the map window. Defaults to 14 for
30
+ * `street`, 17 for `aerial` (close enough to read site infrastructure like
31
+ * substations; Esri imagery is dependable through ~19 in most regions).
32
+ */
22
33
  zoom?: number;
23
34
  /** Accessible title for the embed. Defaults to 'Map'. */
24
35
  title?: string;
36
+ /**
37
+ * 'street' — responsive 16:9 OpenStreetMap embed.
38
+ * 'aerial' — fixed-size satellite imagery with a centered pin.
39
+ */
40
+ variant?: 'street' | 'aerial';
25
41
  }
26
- /** Closed responsive map embed — no className/style. */
27
- export declare function MapEmbed({ lat, lon, zoom, title, }: MapEmbedProps): import("react").JSX.Element | null;
42
+ /** Closed map embed — no className/style. */
43
+ export declare function MapEmbed({ lat, lon, zoom, title, variant, }: MapEmbedProps): import("react").JSX.Element | null;
@@ -1,21 +1,113 @@
1
- import { jsx } from 'react/jsx-runtime';
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
2
  import { AspectRatio } from './ui/aspect-ratio.js';
3
3
 
4
+ const TILE_SIZE = 256;
5
+ const AERIAL_HEIGHT = 256;
6
+ const AERIAL_TILES_X = 3;
7
+ const AERIAL_TILES_Y = 1;
8
+ function tileCoords(lat, lon, zoom) {
9
+ const n = 2 ** zoom;
10
+ const latRad = lat * Math.PI / 180;
11
+ const x = (lon + 180) / 360 * n;
12
+ const y = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n;
13
+ return { x, y };
14
+ }
15
+ function AerialEmbed({
16
+ lat,
17
+ lon,
18
+ zoom,
19
+ title
20
+ }) {
21
+ const n = 2 ** zoom;
22
+ const { x, y } = tileCoords(lat, lon, zoom);
23
+ const px = x * TILE_SIZE;
24
+ const py = y * TILE_SIZE;
25
+ const centerTileX = Math.floor(x);
26
+ const centerTileY = Math.floor(y);
27
+ const tiles = [];
28
+ for (let dx = -AERIAL_TILES_X; dx <= AERIAL_TILES_X; dx++) {
29
+ for (let dy = -AERIAL_TILES_Y; dy <= AERIAL_TILES_Y; dy++) {
30
+ const tx = centerTileX + dx;
31
+ const ty = centerTileY + dy;
32
+ if (ty < 0 || ty >= n) continue;
33
+ const wrappedTx = (tx % n + n) % n;
34
+ tiles.push({
35
+ key: `${tx}:${ty}`,
36
+ left: tx * TILE_SIZE - px,
37
+ top: ty * TILE_SIZE - py,
38
+ src: `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/${zoom}/${ty}/${wrappedTx}`
39
+ });
40
+ }
41
+ }
42
+ const externalHref = `https://www.google.com/maps/@?api=1&map_action=map&center=${lat}%2C${lon}&zoom=${zoom}&basemap=satellite`;
43
+ return /* @__PURE__ */ jsxs(
44
+ "a",
45
+ {
46
+ href: externalHref,
47
+ target: "_blank",
48
+ rel: "noreferrer",
49
+ title,
50
+ "aria-label": `${title} \u2014 open satellite view`,
51
+ className: "relative block w-full shrink-0 overflow-hidden rounded-lg bg-muted",
52
+ style: { height: AERIAL_HEIGHT },
53
+ children: [
54
+ /* @__PURE__ */ jsx("div", { className: "absolute left-1/2 top-1/2", children: tiles.map((tile) => /* @__PURE__ */ jsx(
55
+ "img",
56
+ {
57
+ src: tile.src,
58
+ alt: "",
59
+ loading: "lazy",
60
+ draggable: false,
61
+ className: "absolute max-w-none select-none",
62
+ style: { left: tile.left, top: tile.top, width: TILE_SIZE, height: TILE_SIZE }
63
+ },
64
+ tile.key
65
+ )) }),
66
+ /* @__PURE__ */ jsxs(
67
+ "svg",
68
+ {
69
+ viewBox: "0 0 24 24",
70
+ "aria-hidden": true,
71
+ className: "absolute left-1/2 top-1/2 h-8 w-8 -translate-x-1/2 -translate-y-full drop-shadow-md",
72
+ children: [
73
+ /* @__PURE__ */ jsx(
74
+ "path",
75
+ {
76
+ d: "M12 2c-3.9 0-7 3.1-7 7 0 5.2 7 13 7 13s7-7.8 7-13c0-3.9-3.1-7-7-7z",
77
+ fill: "#e11d48",
78
+ stroke: "#fff",
79
+ strokeWidth: "1.5"
80
+ }
81
+ ),
82
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "9", r: "2.5", fill: "#fff" })
83
+ ]
84
+ }
85
+ ),
86
+ /* @__PURE__ */ jsx("span", { className: "absolute bottom-0 right-0 rounded-tl bg-black/50 px-1.5 py-0.5 text-[10px] leading-tight text-white/90", children: "Imagery \xA9 Esri, Maxar, Earthstar Geographics" })
87
+ ]
88
+ }
89
+ );
90
+ }
4
91
  function MapEmbed({
5
92
  lat,
6
93
  lon,
7
- zoom = 14,
8
- title = "Map"
94
+ zoom,
95
+ title = "Map",
96
+ variant = "street"
9
97
  }) {
10
98
  if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
11
99
  return null;
12
100
  }
13
- const zoomValue = Number.isNaN(zoom) ? 14 : zoom;
14
- const clampedZoom = Math.min(21, Math.max(1, zoomValue));
101
+ const defaultZoom = variant === "aerial" ? 17 : 14;
102
+ const zoomValue = zoom == null || Number.isNaN(zoom) ? defaultZoom : zoom;
103
+ const clampedZoom = Math.min(21, Math.max(1, Math.round(zoomValue)));
104
+ if (variant === "aerial") {
105
+ return /* @__PURE__ */ jsx(AerialEmbed, { lat, lon, zoom: clampedZoom, title });
106
+ }
15
107
  const half = Math.min(20, Math.max(4e-3, 40 / 2 ** clampedZoom));
16
108
  const bbox = `${lon - half},${lat - half},${lon + half},${lat + half}`;
17
109
  const src = `https://www.openstreetmap.org/export/embed.html?bbox=${bbox}&layer=mapnik&marker=${lat},${lon}`;
18
- return /* @__PURE__ */ jsx("div", { className: "w-full overflow-hidden rounded-lg bg-muted", children: /* @__PURE__ */ jsx(AspectRatio, { ratio: 16 / 9, children: /* @__PURE__ */ jsx(
110
+ return /* @__PURE__ */ jsx("div", { className: "w-full shrink-0 overflow-hidden rounded-lg bg-muted", children: /* @__PURE__ */ jsx(AspectRatio, { ratio: 16 / 9, children: /* @__PURE__ */ jsx(
19
111
  "iframe",
20
112
  {
21
113
  src,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jackbernnie/hiyf",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "human-in-your-face — an AI design protocol (a locked, LLM-safe design system). Forked from Polar's Orbit (Apache-2.0).",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",