krackedmaps 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # KrackedMaps · interactive map of Malaysia
2
+
3
+ A self-contained, themeable SVG map of Malaysia — **16 admin-1 regions** (13 states +
4
+ Kuala Lumpur, Putrajaya, Labuan) and **159 admin-2 *daerah*** — with **zero runtime
5
+ dependencies**. Use it as a live component, a CLI, an MCP server for Claude, an embeddable
6
+ iframe, or raw SVG / GeoJSON.
7
+
8
+ **Customize & export:** https://map.themasterofnone.xyz · **Live demo:** https://map.themasterofnone.xyz/demo.html
9
+
10
+ Two aligned layers from one shared projection: click a state to **drill in** (carve its
11
+ districts + info panel); hover a district for its name; press Esc to step back out. East
12
+ Malaysia is slid west to close the South China Sea gap for a compact frame.
13
+
14
+ ---
15
+
16
+ ## Three ways in
17
+
18
+ ```bash
19
+ npm i krackedmaps # library + CLI + MCP server, one package
20
+ ```
21
+
22
+ ### 1. Library (browser / SSR)
23
+
24
+ ```js
25
+ import { createMalaysiaMap } from "krackedmaps";
26
+ import "krackedmaps/css";
27
+
28
+ const map = createMalaysiaMap("#map", { theme: "blueprint" });
29
+ map.on("select", (slug) => console.log("selected", slug));
30
+ ```
31
+
32
+ ```js
33
+ // server-side string builders (no DOM)
34
+ import { renderSVG, project } from "krackedmaps";
35
+ const svg = renderSVG({ theme: "blueprint", withDistricts: true });
36
+ project(101.7117, 3.1578); // -> { x, y } in the SVG viewBox
37
+ ```
38
+
39
+ ### 2. CLI — grab assets from the terminal
40
+
41
+ ```bash
42
+ npx krackedmaps svg --theme blueprint --districts > malaysia.svg
43
+ npx krackedmaps geojson --districts > daerah.geojson
44
+ npx krackedmaps react --theme batik > MalaysiaMap.jsx
45
+ npx krackedmaps project 101.7117 3.1578 # -> {"x":…,"y":…}
46
+ npx krackedmaps states # list states
47
+ npx krackedmaps --help
48
+ ```
49
+
50
+ | command | output |
51
+ |---|---|
52
+ | `svg [--theme] [--districts] [--out]` | themed, self-contained SVG |
53
+ | `geojson [--districts] [--out]` | GeoJSON (original lng/lat) |
54
+ | `json [--out]` | projected paths + projection params |
55
+ | `react [--theme] [--out]` | a `<MalaysiaMap>` component |
56
+ | `embed [--theme] [--districts] [--labels] [--static]` | iframe snippet |
57
+ | `project <lng> <lat>` | project real coords → SVG `{x,y}` |
58
+ | `states` · `districts [--state <slug>]` · `themes` | listings |
59
+
60
+ ### 3. MCP server — use it inside Claude
61
+
62
+ Exposes tools (`krackedmaps_svg`, `krackedmaps_geojson`, `krackedmaps_project`,
63
+ `krackedmaps_states`, `krackedmaps_districts`, `krackedmaps_react`, `krackedmaps_embed`,
64
+ `krackedmaps_paths`) so any Claude conversation can pull the map + data.
65
+
66
+ **Claude Code:**
67
+ ```bash
68
+ claude mcp add krackedmaps -- npx -y krackedmaps mcp
69
+ ```
70
+
71
+ **Claude Desktop** (`claude_desktop_config.json`):
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "krackedmaps": { "command": "npx", "args": ["-y", "krackedmaps", "mcp"] }
76
+ }
77
+ }
78
+ ```
79
+
80
+ Then ask Claude things like *"give me the Malaysia map as a blueprint SVG with districts"* or
81
+ *"what SVG coordinate is KLCC?"*.
82
+
83
+ ---
84
+
85
+ ## `createMalaysiaMap(container, options)`
86
+
87
+ `container` is an `HTMLElement` or selector. Returns an **instance** — make as many as you
88
+ like; `destroy()` tears each one down cleanly.
89
+
90
+ | option | default | description |
91
+ |---|---|---|
92
+ | `theme` | `"blueprint"` | preset (`mono`, `flat-dark`, `light`, `blueprint`, `batik`) or `{cssVar: value}` |
93
+ | `panel` / `tooltip` | `true` | built-in info panel / hover tooltip |
94
+ | `showDistricts` | `true` | district drill-down layer |
95
+ | `interactive` | `true` | pointer / click / keyboard handlers |
96
+ | `zoom` | `true` | zoom-to-state on select (`false` keeps the full map framed) |
97
+ | `keyboard` | `true` | Escape steps back out (scoped, never global) |
98
+ | `labels` | `false` | start with state labels shown |
99
+ | `initialState` | `null` | slug to select after build |
100
+ | `meta` / `colors` / `renderPanel` | — | per-state info / choropleth ramp / custom panel |
101
+
102
+ ```js
103
+ map.select("penang"); map.focus("sabah"); map.drillInto("sarawak");
104
+ map.addPin({ lng: 101.7117, lat: 3.1578, label: "KLCC" });
105
+ map.setData({ penang: 1.8, sabah: 3.4 }); // choropleth by slug
106
+ map.setTheme("batik"); map.toggleLabels(true);
107
+ map.on("select" | "hover" | "drill", cb); map.off(name, cb);
108
+ map.destroy();
109
+ map.STATES; map.DISTRICTS; map.project(lng, lat);
110
+ ```
111
+
112
+ ## Theming
113
+
114
+ Everything is CSS variables on the `.mmap` container — pass a `theme`, call `setTheme(...)`,
115
+ or override in your own CSS. Key vars: `--sea`, `--land`, `--land-hover`, `--land-active`,
116
+ `--stroke`, `--district`, `--carve`, `--district-hi`, `--label`, `--pin`, `--accent`.
117
+
118
+ ## Data only (no engine)
119
+
120
+ ```js
121
+ import { STATES, DISTRICTS, project } from "krackedmaps/data";
122
+ ```
123
+
124
+ Pre-generated files also live in [`exports/`](exports/) (served at `/exports/`): themed SVGs,
125
+ normalized GeoJSON, projected-paths JSON. See [`exports/README.md`](exports/README.md).
126
+
127
+ ---
128
+
129
+ ## Develop
130
+
131
+ ```bash
132
+ npm run dev # python3 -m http.server 4173 — open http://localhost:4173
133
+ npm run build # bundle dist/ for npm (esbuild) + copy GeoJSON
134
+ npm run bake # re-run data/bake.py + data/export.py after changing source data
135
+ ```
136
+
137
+ | file | role |
138
+ |------|------|
139
+ | `src/engine.js` | `createMalaysiaMap` — render + interaction engine |
140
+ | `src/render.js` | pure SVG / snippet builders (shared by browser, CLI, MCP) |
141
+ | `src/data.js` / `src/themes.js` | baked geometry re-export / theme presets |
142
+ | `bin/cli.js` / `bin/mcp.js` | the CLI and the MCP server |
143
+ | `data/states.js` | **baked** paths + `project()` (auto-generated — don't hand-edit) |
144
+ | `index.html` / `playground.js` | the customize & export page (landing) |
145
+ | `demo.html` / `demo.js` | the live demo (dogfoods the engine) |
146
+
147
+ Provenance: states from codeforamerica/click_that_hood; districts (ADM2) + Labuan from
148
+ geoBoundaries gbOpen (MYS). Check each source's license before commercial redistribution.
149
+ MIT licensed.
package/bin/cli.js ADDED
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ // KrackedMaps CLI — grab Malaysia map assets straight from the terminal.
3
+ import { readFileSync, writeFileSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import {
6
+ renderSVG, reactComponent, embedSnippet, project,
7
+ STATES, DISTRICTS, PROJECTION, PRESETS,
8
+ } from "../dist/krackedmaps.esm.js";
9
+
10
+ const VERSION = "0.1.0";
11
+ const readGeo = (f) => readFileSync(fileURLToPath(new URL("../dist/" + f, import.meta.url)), "utf8");
12
+
13
+ // crude flag parser → { _: [positional], flag: value | true }
14
+ function parseArgs(argv) {
15
+ const out = { _: [] };
16
+ for (let i = 0; i < argv.length; i++) {
17
+ const a = argv[i];
18
+ if (a.startsWith("--")) {
19
+ const next = argv[i + 1];
20
+ if (next !== undefined && !next.startsWith("--")) { out[a.slice(2)] = next; i++; }
21
+ else out[a.slice(2)] = true;
22
+ } else out._.push(a);
23
+ }
24
+ return out;
25
+ }
26
+
27
+ function emit(text, args) {
28
+ if (typeof args.out === "string") {
29
+ writeFileSync(args.out, text.endsWith("\n") ? text : text + "\n");
30
+ process.stderr.write(`wrote ${args.out}\n`);
31
+ } else {
32
+ process.stdout.write(text.endsWith("\n") ? text : text + "\n");
33
+ }
34
+ }
35
+
36
+ function fail(msg) { process.stderr.write("krackedmaps: " + msg + "\n"); process.exit(1); }
37
+
38
+ const HELP = `KrackedMaps ${VERSION} — interactive SVG map of Malaysia (16 states + 159 districts)
39
+
40
+ Usage: krackedmaps <command> [options]
41
+
42
+ Commands:
43
+ svg [--theme <name>] [--districts] [--out <file>] themed SVG
44
+ geojson [--districts] [--out <file>] GeoJSON (lng/lat)
45
+ json [--out <file>] projected paths JSON
46
+ react [--theme <name>] [--out <file>] React component
47
+ embed [--theme <name>] [--districts] [--labels] [--static] iframe snippet
48
+ project <lng> <lat> project coords -> {x,y}
49
+ states list states
50
+ districts [--state <slug>] list districts
51
+ themes list theme presets
52
+ mcp start the MCP server (stdio)
53
+
54
+ Themes: ${Object.keys(PRESETS).join(", ")}
55
+ Docs: https://map.themasterofnone.xyz`;
56
+
57
+ async function main() {
58
+ const [cmd, ...rest] = process.argv.slice(2);
59
+ const args = parseArgs(rest);
60
+
61
+ switch (cmd) {
62
+ case undefined:
63
+ case "help": case "-h": case "--help":
64
+ console.log(HELP); break;
65
+ case "version": case "-v": case "--version":
66
+ console.log(VERSION); break;
67
+
68
+ case "svg":
69
+ emit(renderSVG({ withDistricts: !!args.districts, theme: args.theme || "blueprint" }), args); break;
70
+
71
+ case "geojson":
72
+ emit(readGeo(args.districts ? "krackedmaps-districts.geojson" : "krackedmaps.geojson"), args); break;
73
+
74
+ case "json":
75
+ emit(JSON.stringify({
76
+ projection: PROJECTION,
77
+ viewBox: `0 0 ${PROJECTION.viewW} ${PROJECTION.viewH}`,
78
+ states: STATES, districts: DISTRICTS,
79
+ }, null, args.out ? 0 : 2), args); break;
80
+
81
+ case "react":
82
+ emit(reactComponent(args.theme || "blueprint"), args); break;
83
+
84
+ case "embed":
85
+ emit(embedSnippet({
86
+ theme: args.theme || "blueprint",
87
+ districts: !!args.districts, labels: !!args.labels, static: !!args.static,
88
+ }), args); break;
89
+
90
+ case "project": {
91
+ const [lng, lat] = args._.map(Number);
92
+ if (!Number.isFinite(lng) || !Number.isFinite(lat))
93
+ fail("project needs <lng> <lat>, e.g. krackedmaps project 101.7117 3.1578");
94
+ console.log(JSON.stringify(project(lng, lat))); break;
95
+ }
96
+
97
+ case "states":
98
+ for (const s of STATES) console.log(`${s.slug}\t${s.name}\t${s.type}`); break;
99
+
100
+ case "districts": {
101
+ const list = args.state ? DISTRICTS.filter((d) => d.state === args.state) : DISTRICTS;
102
+ if (!list.length) fail(`no districts for state "${args.state}"`);
103
+ for (const d of list) console.log(`${d.state}/${d.slug}\t${d.name}`); break;
104
+ }
105
+
106
+ case "themes":
107
+ for (const name of Object.keys(PRESETS)) console.log(name); break;
108
+
109
+ case "mcp":
110
+ await import("./mcp.js"); break;
111
+
112
+ default:
113
+ fail(`unknown command "${cmd}". Run: krackedmaps --help`);
114
+ }
115
+ }
116
+
117
+ main().catch((e) => fail(e.message));
package/bin/mcp.js ADDED
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ // KrackedMaps MCP server — exposes the Malaysia map to Claude over stdio.
3
+ // Zero-dependency, newline-delimited JSON-RPC 2.0 (MCP stdio transport).
4
+ import { readFileSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import {
7
+ renderSVG, reactComponent, embedSnippet, project,
8
+ STATES, DISTRICTS, PROJECTION, PRESETS,
9
+ } from "../dist/krackedmaps.esm.js";
10
+
11
+ const VERSION = "0.1.0";
12
+ const readGeo = (f) => readFileSync(fileURLToPath(new URL("../dist/" + f, import.meta.url)), "utf8");
13
+ const themeEnum = Object.keys(PRESETS);
14
+
15
+ const TOOLS = [
16
+ { name: "krackedmaps_svg",
17
+ description: "Themed, self-contained SVG of Malaysia (16 states; optional 159 districts).",
18
+ inputSchema: { type: "object", properties: { theme: { type: "string", enum: themeEnum }, districts: { type: "boolean" } } },
19
+ run: (a) => renderSVG({ theme: a.theme || "blueprint", withDistricts: !!a.districts }) },
20
+ { name: "krackedmaps_geojson",
21
+ description: "GeoJSON FeatureCollection (original lng/lat) of states or districts, with normalized props.",
22
+ inputSchema: { type: "object", properties: { level: { type: "string", enum: ["states", "districts"] } } },
23
+ run: (a) => readGeo(a.level === "districts" ? "krackedmaps-districts.geojson" : "krackedmaps.geojson") },
24
+ { name: "krackedmaps_paths",
25
+ description: "Projected SVG paths + projection params (JSON) for custom rendering.",
26
+ inputSchema: { type: "object", properties: {} },
27
+ run: () => JSON.stringify({ projection: PROJECTION, viewBox: `0 0 ${PROJECTION.viewW} ${PROJECTION.viewH}`, states: STATES, districts: DISTRICTS }) },
28
+ { name: "krackedmaps_project",
29
+ description: "Project a real-world lng/lat to the map's SVG coordinates (pins land exactly).",
30
+ inputSchema: { type: "object", properties: { lng: { type: "number" }, lat: { type: "number" } }, required: ["lng", "lat"] },
31
+ run: (a) => JSON.stringify(project(Number(a.lng), Number(a.lat))) },
32
+ { name: "krackedmaps_states",
33
+ description: "List the 16 states/territories (slug, name, type).",
34
+ inputSchema: { type: "object", properties: {} },
35
+ run: () => JSON.stringify(STATES.map((s) => ({ slug: s.slug, name: s.name, type: s.type }))) },
36
+ { name: "krackedmaps_districts",
37
+ description: "List the 159 districts (optionally filtered by parent state slug).",
38
+ inputSchema: { type: "object", properties: { state: { type: "string" } } },
39
+ run: (a) => JSON.stringify((a.state ? DISTRICTS.filter((d) => d.state === a.state) : DISTRICTS).map((d) => ({ slug: d.slug, name: d.name, state: d.state }))) },
40
+ { name: "krackedmaps_react",
41
+ description: "A ready-to-paste React component using the krackedmaps npm package.",
42
+ inputSchema: { type: "object", properties: { theme: { type: "string", enum: themeEnum } } },
43
+ run: (a) => reactComponent(a.theme || "blueprint") },
44
+ { name: "krackedmaps_embed",
45
+ description: "An iframe embed snippet for the hosted, interactive map.",
46
+ inputSchema: { type: "object", properties: { theme: { type: "string", enum: themeEnum }, districts: { type: "boolean" } } },
47
+ run: (a) => embedSnippet({ theme: a.theme || "blueprint", districts: !!a.districts }) },
48
+ ];
49
+
50
+ const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
51
+ const reply = (id, result) => send({ jsonrpc: "2.0", id, result });
52
+ const errReply = (id, code, message) => send({ jsonrpc: "2.0", id, error: { code, message } });
53
+
54
+ function handle(msg) {
55
+ const { id, method, params } = msg;
56
+ switch (method) {
57
+ case "initialize":
58
+ return reply(id, {
59
+ protocolVersion: params?.protocolVersion || "2025-06-18",
60
+ capabilities: { tools: {} },
61
+ serverInfo: { name: "krackedmaps", version: VERSION },
62
+ });
63
+ case "notifications/initialized":
64
+ case "notifications/cancelled":
65
+ return; // notifications: no reply
66
+ case "ping":
67
+ return reply(id, {});
68
+ case "tools/list":
69
+ return reply(id, { tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) });
70
+ case "tools/call": {
71
+ const tool = TOOLS.find((t) => t.name === params?.name);
72
+ if (!tool) return errReply(id, -32602, `unknown tool: ${params?.name}`);
73
+ try {
74
+ return reply(id, { content: [{ type: "text", text: tool.run(params.arguments || {}) }] });
75
+ } catch (e) {
76
+ return reply(id, { content: [{ type: "text", text: "Error: " + e.message }], isError: true });
77
+ }
78
+ }
79
+ default:
80
+ if (id !== undefined) errReply(id, -32601, `method not found: ${method}`);
81
+ }
82
+ }
83
+
84
+ let buf = "";
85
+ process.stdin.setEncoding("utf8");
86
+ process.stdin.on("data", (chunk) => {
87
+ buf += chunk;
88
+ let nl;
89
+ while ((nl = buf.indexOf("\n")) >= 0) {
90
+ const line = buf.slice(0, nl).trim();
91
+ buf = buf.slice(nl + 1);
92
+ if (!line) continue;
93
+ let msg;
94
+ try { msg = JSON.parse(line); } catch { continue; }
95
+ try { handle(msg); } catch (e) { if (msg && msg.id !== undefined) errReply(msg.id, -32603, e.message); }
96
+ }
97
+ });
98
+ process.stdin.on("end", () => process.exit(0));
99
+ process.stderr.write(`krackedmaps MCP server ${VERSION} ready (stdio)\n`);
package/dist/data.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ export interface Point { x: number; y: number; }
2
+
3
+ export interface StateFeature {
4
+ slug: string;
5
+ name: string;
6
+ type: "state" | "ft";
7
+ d: string;
8
+ centroid: Point;
9
+ }
10
+
11
+ export interface DistrictFeature {
12
+ slug: string;
13
+ name: string;
14
+ state: string;
15
+ d: string;
16
+ centroid: Point;
17
+ }
18
+
19
+ export interface Projection {
20
+ minx: number; maxy: number; scale: number; pad: number; eastLng: number;
21
+ shift: number; offX: number; offY: number; viewW: number; viewH: number;
22
+ }
23
+
24
+ export const STATES: StateFeature[];
25
+ export const DISTRICTS: DistrictFeature[];
26
+ export const PROJECTION: Projection;
27
+ export function project(lng: number, lat: number): Point;