{"id":48559691,"url":"https://github.com/gvellut/mapvibe","last_synced_at":"2026-04-08T12:07:55.462Z","repository":{"id":306958544,"uuid":"1023817601","full_name":"gvellut/mapvibe","owner":"gvellut","description":"Google My Maps replacement for my blog","archived":false,"fork":false,"pushed_at":"2026-03-14T11:18:59.000Z","size":1192,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-03-14T22:17:09.820Z","etag":null,"topics":["maplibre","mapping","vibecoded"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gvellut.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-07-21T18:34:01.000Z","updated_at":"2026-03-14T11:19:02.000Z","dependencies_parsed_at":"2025-07-28T18:09:31.245Z","dependency_job_id":"cc96cf48-1501-4b27-9e6a-df43ee90dba4","html_url":"https://github.com/gvellut/mapvibe","commit_stats":null,"previous_names":["gvellut/mapvibe"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/gvellut/mapvibe","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gvellut%2Fmapvibe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gvellut%2Fmapvibe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gvellut%2Fmapvibe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gvellut%2Fmapvibe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gvellut","download_url":"https://codeload.github.com/gvellut/mapvibe/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gvellut%2Fmapvibe/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31554235,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-08T10:21:54.569Z","status":"ssl_error","status_checked_at":"2026-04-08T10:21:38.171Z","response_time":54,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["maplibre","mapping","vibecoded"],"created_at":"2026-04-08T12:07:54.547Z","updated_at":"2026-04-08T12:07:55.314Z","avatar_url":"https://github.com/gvellut.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# MapVibe\n\nA static map interface for embedding in blog posts or websites, as a Google My Maps replacement. Runs fully client-side, loads configuration from a single JSON file.\n\nMade with [MapLibre GL JS](https://maplibre.org/maplibre-gl-js/docs/) and React in TypeScript.\n\n## Configuration\n\n- Uses the [MapLibre Style Spec](https://maplibre.org/maplibre-style-spec/) for sources, layers, and styling.\n- The config file must include a `customUi` object:\n  - `customUi.imports`: Optional list of imported style fragments using Mapbox-style `{ id, url }` entries. Imported styles are fetched asynchronously and inserted below non-background top-style overlay/data layers. It is similar to the *Imports* in the Mapbox SDK v2 (after the Maplibre fork).\n  - `customUi.backgroundLayers`: List of selectable backgrounds shown in the layer chooser.\n    - Each entry uses its own `id` / `name` plus a required `layerIds` array.\n    - `backgroundLayers[].id` is a `customUi` id, separate from top-style layer ids and import ids.\n    - `backgroundLayers[].layerIds` references top-style layer ids and/or import ids in bottom-to-top order.\n    - Set `visible: true` on one entry to choose the initial GUI selection. If multiple entries set it, MapVibe warns and uses the first. If none set it, MapVibe selects the first background entry.\n    - Background entries default to not selected unless `visible: true` is set.\n  - `customUi.dataLayers`: List of data layers (lines, points, polygons) for toggling visibility.\n    - `dataLayers[].layerIds` references top-style layer ids.\n    - Data layers default to visible. Set `visible: false` to start hidden.\n    - Set `interactive: true` to make a data layer clickable.\n    - Add `clusterInteractive: true` on an interactive clustered data layer to make generated cluster features zoom to their expansion level instead of opening the info panel. When omitted or `false`, cluster clicks are ignored and only non-cluster features remain actionable.\n    - Add `openUrl: true` on an interactive data layer to open each clicked feature's `url` property in a new tab instead of showing the info panel.\n  - `customUi.panel`: Panel color and width.\n    - Optional `imageSizeIsMax: true` uses each feature's `imageSize` as the maximum image box size in the info panel. Default is `false`, which keeps the current full-width behavior.\n    - Info panel features can also provide `imageBackgroundColor` as a hex color string such as `#000000` to fill the image wrapper behind the image.\n  - `customUi.controls`: Which UI controls to show (zoom, scale, layer chooser, fullscreen, attribution).\n  - `customUi.globalMinZoom` / `globalMaxZoom`: Clamp zoom range for all backgrounds.\n- On startup, MapVibe hides all top-style layers first, then applies visibility from `customUi.backgroundLayers` and `customUi.dataLayers`. Raw top-style `layout.visibility` does not control initial visibility.\n- The layer chooser still uses a single selected background at a time. Selecting a background entry hides the other managed backgrounds and restores that entry's top-style/import members as one virtual background.\n\n### Notes\n\nIn `\"customUi\" \u003e \"panel\"`,  to recenter marker when it would be covered by info panel, add:\n\n```js\n\"marginRecenterOnOpen\": 10,\n\"recenterOnOpen\": true\n```\n\nTo enable cooperative gestures (`ctrl + scroll` to zoom on desktop + 2 finger pan on mobile) in the standalone `/mapvibe` app, add `mgc=y` to the URL. `mgc=0`, `mgc=n`, or `mgc=no` disables it. That parameter is forced to `mgc=n` when opening the map in a new tab from the fullscreen button.\n\nTo remember the last map position, use `rlp=page` or `rlp=domain` in the standalone `/mapvibe` URL:\n- `rlp=page` remembers pan/zoom per host + path\n- `rlp=domain` remembers pan/zoom per host across paths\n- `rlp=1` is the same as `rlp=page`\n- `rlp=0` disables the feature and ignores any saved position\n\nTo override the fullscreen button in the standalone `/mapvibe` app, use `fs=y` to force-enable it or `fs=0`, `fs=n`, or `fs=no` to force-disable it. When `fs` is absent, the app defers to `customUi.controls.fullscreen` in the config JSON. The fullscreen button opens the new tab with `fs=no` so that view does not show another fullscreen button.\n\nWhen enabled, the remembered position takes precedence over `center`, `zoom`, `bounds`, and auto-fit-to-data on reload.\n\nWhen an imported background is selected, MapVibe may switch the map `glyphs` URL to the imported style's `glyphs` URL. For background entries with multiple imported styles declaring different `glyphs` URLs, MapVibe warns and uses the last imported member in the entry's order.\n\n## Usage\n\n- Host the `dist` output (see the doc on [Build](#build)) in folder `/mapvibe`.\n  - You will have to create a config JSON file and host it on the same server (see the `samples` folder for examples for `config.json` files).\n  - Your config JSON file can refer to icons other than the included ones: Host them on your server.\n  - The config can refer to your own GeoJSON data layers: Also host them on your server.\n- Embed in your page with `\u003ciframe src=\"/mapvibe/?config=.../config.json\" ...\u003e`.\n\n### Deployment in Hugo\n\nThe `dist` output can be added under `static/mapvibe` in the Hugo folder, for later deployment on your server.\n\nCreate a new `mapvibe` shortcode in `layouts/shortcodes/mapvibe.html`. For example:\n\n```html\n\u003ciframe loading=\"lazy\" frameborder=\"0\" scrolling=\"no\" marginheight=\"0\" marginwidth=\"0\" \n    src=\"/mapvibe/?config={{ .Page.RelPermalink }}config.json\" \n    width=\"100%\" height=\"{{ .Get 1 }}\"\u003e\u003c/iframe\u003e\n```\n\nThis assumes the post is a page bundle with the `config.json` hosted inside (so `RelPermalink` is actually a folder in Hugo).\n\nThe map can be embedded in a Hugo content using:\n\n```html\n{{\u003c mapvibe \"640\" \"480\" \u003e}}\n```\n\n### Example\n\nHugo blog:\n\nhttps://blog.vellut.com/2025/07/hike-to-pointe-noire-de-pormenaz/ (scroll a little to see the map)\n\n`MapVibe` was meant to replace something like:\n\nhttps://blog.vellut.com/2025/06/hike-to-pointe-des-aravis-aiguille-de-borderan/ (Google My Maps)\n\n## Development\n\n```bash\nnpm install\nnpm run dev\n```\n\nSome simple `config.json` samples can be found in folder `samples`. To load one of them, use something like\n\n`http://localhost:5173/mapvibe/?config=samples/sample1/config.json`\n\nas the URL for testing. For imports and multi-layer backgrounds, file `samples/sample4/config.json` has a sample of use. For clustered interactive points with click-to-zoom clusters, file `samples/sample5/config.json` shows `clusterInteractive: true`.\n\nOr, since the project will be used inside an iframe (with limited width and height), use :\n\n`http://localhost:5173/mapvibe/iframe.html?config=samples/sample2/config.json`\n\n## Build\n\n### Building the Website\n\n```bash\nnpm run build\n```\n\nThis will output static files to `dist` for hosting as a standalone website.\n\nBefore building:\n- The hosting path can be customized in `vite.config.ts` (the CSS will load some assets using that path so it should correspond to where it will be hosted).\n- The favicon can also be changed.\n- New icons can be added in `public/assets/markers` (but they can be loaded from any place so not really needed except convenience).\n\n### Building the Library\n\nTo build MapVibe as an npm library:\n\n```bash\nnpm run build:lib\n```\n\nThis will generate:\n- `dist/mapvibe.mjs` - ES module build\n- `dist/mapvibe.cjs` - CommonJS build\n- `dist/mapvibe.css` - Compiled CSS styles\n- `dist/lib.d.ts` - TypeScript declarations for autocomplete and type checking\n\n## Publishing to NPM\n\nTo publish the library to npm:\n\n1. Update the version in `package.json`\n2. Ensure you're logged in to npm: `npm login`\n3. Run: `npm publish`\n\nThe `prepublishOnly` script will automatically run `npm run build:lib` before publishing.\n\n## Using MapVibe as a Library\n\n### Installation\n\n```bash\nnpm install mapvibe\n```\n\n### Peer Dependencies\n\nMapVibe requires the following peer dependencies:\n- `react` (^18.0.0 || ^19.0.0)\n- `react-dom` (^18.0.0 || ^19.0.0)\n- `maplibre-gl` (^4.0.0 || ^5.0.0)\n\nMake sure to install them if not already present:\n\n```bash\nnpm install react react-dom maplibre-gl\n```\n\n### Basic Usage\n\n```tsx\nimport { MapVibeMap, type AppConfig } from 'mapvibe';\nimport 'mapvibe/style.css';\n\nfunction App({ config }: { config: AppConfig }) {\n  return \u003cMapVibeMap config={config} rememberLastPosition=\"page\" /\u003e;\n}\n```\n\n`MapVibeMap` is the embeddable component for host applications. If you want the standalone app that reads a `config` URL parameter, use the built website output described earlier in this README.\n\n`rememberLastPosition` accepts `false | 0 | true | 1 | \"page\" | \"domain\"`.\n- `false` / `0`: disabled, never load saved pan/zoom even if one exists\n- `true` / `1` / `\"page\"`: remember pan/zoom per host + path\n- `\"domain\"`: remember pan/zoom per host across paths\n\nWhen a remembered position exists, it overrides `config.center`, `config.zoom`, `config.bounds`, and the automatic fit-to-sources fallback.\n\n### Accessing the MapLibre Instance\n\n`MapVibeMap` exposes the underlying `maplibregl.Map` instance through a React ref so the embedding app can wire custom globals such as `window.goto`.\n\nThe handle also exposes:\n- `getLayerIdsForBackgroundLayer(id)` to resolve a `customUi.backgroundLayers[].id` to the concrete runtime layer ids currently associated with it\n- `getImportInfo(id)` to inspect a loaded import's original URL and namespaced layer/source/sprite ids\n\n```tsx\nimport { createRef } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { MapVibeMap, type AppConfig, type MapVibeMapHandle } from 'mapvibe';\nimport 'mapvibe/style.css';\n\nconst mapRef = createRef\u003cMapVibeMapHandle\u003e();\nconst root = createRoot(document.getElementById('map')!);\n\ndeclare global {\n  interface Window {\n    goto?: (lat: number, lon: number) =\u003e void;\n  }\n}\n\nwindow.goto = (lat: number, lon: number) =\u003e {\n  mapRef.current?.getMap()?.flyTo({ center: [lon, lat] });\n};\n\nroot.render(\n  \u003cMapVibeMap\n    ref={mapRef}\n    config={config}\n  /\u003e\n);\n```\n\n### Importing CSS and Assets\n\nWhen impoorting the library published on NPM (TBD):\n\n**CSS**: The library exports a compiled CSS file that must be imported in your application:\n\n```tsx\nimport 'mapvibe/style.css';\n```\n\nAlternatively, you can import it in your main CSS file:\n \n```css\n@import 'mapvibe/style.css';\n```\n\n**Icons and Assets**: The library includes UI icons (layers, close, fullscreen) in the compiled CSS (by default Vite inline the referenced icons smaller than 4kB: These are embedded as data URIs). When using the library:\n\n1. If you're using custom marker icons, host them on your server and reference them in your config.json\n2. The default UI icons (layer chooser, close button, fullscreen) are bundled with the CSS\n\n### TypeScript Support\n\nMapVibe is written in TypeScript and includes full type definitions. When using the library in a TypeScript project, you'll get:\n\n- Full autocomplete for the `MapVibeMap` component\n- Type definitions for configuration interfaces:\n  - `AppConfig`\n  - `BackgroundLayerConfig`\n  - `StyleImportConfig`\n  - `DataLayerConfig`\n  - `CustomUiConfig`\n  - `InfoPanelData`\n  - `MapVibeImportInfo`\n  - `MapVibeMapHandle`\n\nExample with types:\n\n```tsx\nimport { MapVibeMap, AppConfig } from 'mapvibe';\nimport 'mapvibe/style.css';\n\n// TypeScript will provide autocomplete for config structure\nconst config: AppConfig = {\n  title: \"My Map\",\n  center: [6.8665, 45.9237],\n  zoom: 12,\n  // ... rest of config with full type checking\n};\n```\n\nFor `openUrl` layers, each clicked feature is expected to expose a string `url` property in its GeoJSON `properties`. If `url` is missing, MapVibe logs a warning in the console and does not open a popup.\n\nFor clustered GeoJSON sources, generated cluster features should be grouped into the same `dataLayers[].layerIds` entry as the leaf layer if they share one visibility toggle. `clusterInteractive: true` changes only click behavior for those generated cluster features; leaf features in the same data layer still use `openUrl` or the info panel.\n\n\n## Notes\n\n### Upgrade dev deps\n\n`npx npm-check-updates -u --dep dev`\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgvellut%2Fmapvibe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgvellut%2Fmapvibe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgvellut%2Fmapvibe/lists"}