@tanishqkancharla/diffmap 0.1.3 → 0.1.5

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 (50) hide show
  1. package/README.md +25 -14
  2. package/package.json +3 -3
  3. package/src/cli.test.ts +50 -0
  4. package/src/cli.ts +126 -51
  5. package/src/contentPlugin.ts +5 -3
  6. package/src/errors.ts +6 -1
  7. package/src/gist/GistApp.tsx +34 -21
  8. package/src/gist/GistStatus.tsx +8 -43
  9. package/src/gist/main.tsx +2 -2
  10. package/src/gist/pin.ts +14 -0
  11. package/src/github/GitHubApp.tsx +235 -0
  12. package/src/github/fetchRepo.test.ts +17 -0
  13. package/src/github/fetchRepo.ts +300 -0
  14. package/src/github/patch.test.ts +30 -0
  15. package/src/github/patch.ts +40 -0
  16. package/src/github/pin.ts +14 -0
  17. package/src/github/route.test.ts +68 -0
  18. package/src/github/route.ts +175 -0
  19. package/src/host/HostApp.tsx +22 -0
  20. package/src/host/HostHome.tsx +73 -0
  21. package/src/host/files.ts +10 -0
  22. package/src/host/home.md +194 -0
  23. package/src/host/home.test.ts +82 -0
  24. package/src/index.ts +1 -0
  25. package/src/mermaid.ts +29 -1
  26. package/src/packageVersion.ts +7 -0
  27. package/src/parseViewer.test.ts +185 -0
  28. package/src/parseViewer.ts +27 -3
  29. package/src/serve.test.ts +306 -0
  30. package/src/serve.ts +23 -5
  31. package/src/share.ts +5 -1
  32. package/src/update.test.ts +19 -0
  33. package/src/update.ts +285 -0
  34. package/src/viewer/CallStackDiff.tsx +26 -6
  35. package/src/viewer/CloseServerButton.tsx +16 -0
  36. package/src/viewer/FileExcerpt.tsx +52 -3
  37. package/src/viewer/HtmlPlaceholder.tsx +6 -1
  38. package/src/viewer/SourceDiffPanel.tsx +155 -59
  39. package/src/viewer/TableOfContents.tsx +230 -39
  40. package/src/viewer/TocButton.tsx +27 -0
  41. package/src/viewer/ViewerApp.tsx +182 -47
  42. package/src/viewer/main.tsx +10 -3
  43. package/src/viewer/sourceHeader.test.ts +131 -0
  44. package/src/viewer/sourceHeader.ts +103 -0
  45. package/src/viewer/sourceNavigation.ts +81 -3
  46. package/src/viewer/viewerMode.ts +1 -1
  47. package/src/viewer/vite-env.d.ts +2 -1
  48. package/vite.config.ts +1 -1
  49. package/src/viewer/Check.tsx +0 -24
  50. package/src/viewer/DoneButton.tsx +0 -32
package/README.md CHANGED
@@ -5,34 +5,33 @@
5
5
  diffmap is a set of personal skills as well as a web-viewer for Markdown files. It has a unique form of showing diffs using a mixture of callstack diffs and mermaid diagram links, which I've found to be personally extremely helpful in understanding massive diffs.
6
6
 
7
7
  ```sh
8
- npx diffmap path/to/file.md
8
+ npx @tanishqkancharla/diffmap serve path/to/file.md
9
9
  ```
10
10
 
11
+ Bare `npx @tanishqkancharla/diffmap path/to/file.md` is the same as `serve`.
12
+
11
13
  Share a secret gist and print a `diffmap.dev` link (requires [`gh`](https://cli.github.com/) logged in). Does not start a local server:
12
14
 
13
15
  ```sh
14
- npx diffmap share path/to.md
16
+ npx @tanishqkancharla/diffmap share path/to.md
15
17
  ```
16
18
 
17
19
  Hosted viewer: `https://diffmap.dev/g/<gistId>` (optional `/<file.md>`; `#heading` is the table of contents). The page fetches the gist in the browser from `api.github.com`. Gists are unlisted, not private.
18
20
 
19
21
  ## Agent skills
20
22
 
21
- Install skills from this repository:
22
-
23
23
  ```sh
24
- npx skills add tanishqkancharla/diffmap --skill code-walkthrough
25
24
  npx skills add tanishqkancharla/diffmap --skill generate-spec
26
25
  ```
27
26
 
28
- `code-walkthrough` explains landed changes. `generate-spec` writes a phased spec for work that has not happened yet, then serves `specs/<name>.md` with diffmap.
27
+ Keeps a spec in `specs/` that stays true to what's planned versus already in the tree. Serve it with diffmap. Share from a GitHub PR (`https://diffmap.dev/<owner>/<repo>/pull/<n>/specs/<name>.md`) or `npx @tanishqkancharla/diffmap share`.
29
28
 
30
29
  Options:
31
30
 
32
- - `--port <n>` — listen port (default `4177`)
31
+ - `--port <n>` — pin a listen port (default: a free port)
33
32
  - `--root <dir>` — workspace root for file excerpts (default the directory you ran the command from)
34
33
 
35
- **Done** in the top right stops the server.
34
+ **Close server** in the top right stops the server. Hosted gist and GitHub viewers have no local server, so they omit this control.
36
35
 
37
36
  The server also stops after 24 hours without a page or source request.
38
37
  Loading or refreshing the page resets the timer; background health checks and
@@ -41,23 +40,35 @@ Vite connections do not keep it alive.
41
40
  List every running viewer, including viewers using custom ports:
42
41
 
43
42
  ```sh
44
- npx diffmap list
43
+ npx @tanishqkancharla/diffmap list
45
44
  ```
46
45
 
46
+ Update a global install to the latest `@tanishqkancharla/diffmap` on npm. `diffmap --update` does the same thing:
47
+
48
+ ```sh
49
+ diffmap update
50
+ ```
51
+
52
+ If you are already on the latest version, it says so and exits. Otherwise it runs the global install for the package manager that installed diffmap (`npm install --global`, `pnpm add --global`, or `bun add --global`) and prints the old and new versions. Under `npx`, or when it cannot tell how diffmap was installed, it prints the command to run instead of installing anything.
53
+
47
54
  Request the page with `Accept: text/markdown` to read the current source file
48
- instead of the rendered HTML:
55
+ instead of the rendered HTML. Use the URL printed by `serve`:
49
56
 
50
57
  ```sh
51
- curl -H 'Accept: text/markdown' http://127.0.0.1:4177/
58
+ curl -H 'Accept: text/markdown' http://127.0.0.1:<port>/
52
59
  ```
53
60
 
54
61
  ## Library
55
62
 
56
63
  ```ts
57
- import { startServer, parseFence, parseViewerDocument } from "diffmap";
64
+ import {
65
+ startServer,
66
+ parseFence,
67
+ parseViewerDocument,
68
+ } from "@tanishqkancharla/diffmap";
58
69
  ```
59
70
 
60
- `parseViewerDocument` turns markdown into the page document with [md4x](https://github.com/unjs/md4x). `startServer` listens. The generate-spec and code-walkthrough skills own spec vs walkthrough section order; the viewer does not.
71
+ `parseViewerDocument` turns markdown into the page document with [md4x](https://github.com/unjs/md4x). `startServer` listens. The generate-spec skill owns document shape; the viewer does not.
61
72
 
62
73
  ## Fences
63
74
 
@@ -176,7 +187,7 @@ Resolution uses the workspace's TypeScript configuration and current files.
176
187
  Deleted files and lines that no longer match the workspace show a message;
177
188
  their call stack references still highlight the embedded old-side diff.
178
189
 
179
- Run `npx diffmap fixtures/annotations.md` for an example
190
+ Run `npx @tanishqkancharla/diffmap fixtures/annotations.md` for an example
180
191
  with old/new references, multiple files, and unchanged context.
181
192
 
182
193
  ## Link Mermaid nodes and edges
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanishqkancharla/diffmap",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Serve a spec or code walkthrough as a local page",
5
5
  "license": "MIT",
6
6
  "author": "Tanishq Kancharla",
@@ -33,7 +33,7 @@
33
33
  "lint:fix": "oxlint --fix src vite.config.ts vite.gist.config.ts bin.js",
34
34
  "prepublishOnly": "npm run typecheck && npm run lint && npm run format:check",
35
35
  "release": "npm publish",
36
- "test": "tsx --test src/gist/route.test.ts",
36
+ "test": "tsx --test src/cli.test.ts src/update.test.ts src/parseViewer.test.ts src/serve.test.ts src/gist/route.test.ts src/github/route.test.ts src/github/patch.test.ts src/github/fetchRepo.test.ts src/viewer/sourceHeader.test.ts src/host/home.test.ts",
37
37
  "typecheck": "tsc --noEmit",
38
38
  "build": "vite build",
39
39
  "build:gist": "vite build",
@@ -45,7 +45,7 @@
45
45
  "beautiful-mermaid": "^1.1.3",
46
46
  "errore": "^0.14.1",
47
47
  "incur": "^0.4.20",
48
- "maui": "npm:@tanishqkancharla/maui@0.0.16",
48
+ "maui": "npm:@tanishqkancharla/maui@0.0.30",
49
49
  "md4x": "^0.0.29",
50
50
  "purse-styles": "^0.2.3",
51
51
  "react": "^19.2.8",
@@ -0,0 +1,50 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFile } from "node:child_process";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { promisify } from "node:util";
7
+ import test from "node:test";
8
+
9
+ const execFileAsync = promisify(execFile);
10
+ const srcDir = fileURLToPath(new URL(".", import.meta.url));
11
+ const packageRoot = path.resolve(srcDir, "..");
12
+ const cliPath = path.join(srcDir, "cli.ts");
13
+
14
+ async function runCli(args: string[]) {
15
+ try {
16
+ const result = await execFileAsync(
17
+ process.execPath,
18
+ ["--import", "tsx", cliPath, ...args],
19
+ {
20
+ cwd: packageRoot,
21
+ timeout: 20_000,
22
+ maxBuffer: 1_000_000,
23
+ },
24
+ );
25
+ return { exitCode: 0, stdout: result.stdout, stderr: result.stderr };
26
+ } catch (cause) {
27
+ if (typeof cause !== "object" || !cause) throw cause;
28
+ const exitCode =
29
+ "code" in cause && typeof cause.code === "number"
30
+ ? cause.code
31
+ : undefined;
32
+ const stdout =
33
+ "stdout" in cause && typeof cause.stdout === "string" ? cause.stdout : "";
34
+ const stderr =
35
+ "stderr" in cause && typeof cause.stderr === "string" ? cause.stderr : "";
36
+ return { exitCode, stdout, stderr };
37
+ }
38
+ }
39
+
40
+ test("diffmap --version matches package.json", async () => {
41
+ const pkg = JSON.parse(
42
+ await fs.readFile(path.join(packageRoot, "package.json"), "utf8"),
43
+ ) as { version: string };
44
+ const source = await fs.readFile(cliPath, "utf8");
45
+ assert.doesNotMatch(source, /version:\s*["'`]\d+\.\d+\.\d+/);
46
+ const result = await runCli(["--version"]);
47
+ assert.equal(result.exitCode, 0, result.stderr);
48
+ const printed = `${result.stdout}${result.stderr}`.trim();
49
+ assert.equal(printed, pkg.version);
50
+ });
package/src/cli.ts CHANGED
@@ -2,44 +2,120 @@
2
2
 
3
3
  import { Cli, z } from "incur";
4
4
  import { resolveFromInvokeCwd } from "./invokeCwd.js";
5
+ import { packageVersion } from "./packageVersion.js";
5
6
  import { listRunningDiffmaps } from "./registry.js";
6
7
  import { startServer } from "./serve.js";
7
8
  import { shareMarkdownFile } from "./share.js";
9
+ import {
10
+ detectInstallMethod,
11
+ fetchLatestVersion,
12
+ packageName,
13
+ resolveUpdateArgv,
14
+ runCommand,
15
+ runUpdate,
16
+ } from "./update.js";
8
17
 
9
- const cli = Cli.create("diffmap", {
10
- description: "Serve a spec or code walkthrough as a local page",
11
- version: "0.1.3",
12
- args: z.object({
13
- file: z.string().describe("Path to the markdown file"),
14
- }),
15
- options: z.object({
16
- port: z.coerce.number().optional().describe("Port (default 4177)"),
17
- root: z.string().optional().describe("Workspace root for file excerpts"),
18
- }),
19
- output: z.object({
20
- url: z.string(),
21
- file: z.string(),
22
- }),
23
- async *run(c) {
24
- const started = await startServer({
25
- filePath: resolveFromInvokeCwd(c.args.file),
26
- workspaceRoot:
27
- c.options.root === undefined
28
- ? resolveFromInvokeCwd(".")
29
- : resolveFromInvokeCwd(c.options.root),
30
- port: c.options.port === undefined ? 4177 : c.options.port,
18
+ const serveArgs = z.object({
19
+ file: z.string().describe("Path to the markdown file"),
20
+ });
21
+
22
+ const serveOptions = z.object({
23
+ port: z.coerce.number().optional().describe("Port (default: a free port)"),
24
+ root: z.string().optional().describe("Workspace root for file excerpts"),
25
+ });
26
+
27
+ const serveOutput = z.object({
28
+ url: z.string(),
29
+ file: z.string(),
30
+ });
31
+
32
+ type ServeContext = {
33
+ args: { file: string };
34
+ options: { port?: number | undefined; root?: string | undefined };
35
+ error: (input: { code: string; message: string }) => never;
36
+ };
37
+
38
+ async function* runServe(c: ServeContext) {
39
+ const started = await startServer({
40
+ filePath: resolveFromInvokeCwd(c.args.file),
41
+ workspaceRoot:
42
+ c.options.root === undefined
43
+ ? resolveFromInvokeCwd(".")
44
+ : resolveFromInvokeCwd(c.options.root),
45
+ port: c.options.port,
46
+ });
47
+ if (started instanceof Error) {
48
+ return c.error({
49
+ code: "DIFFMAP",
50
+ message: started.message,
31
51
  });
32
- if (started instanceof Error) {
33
- return c.error({
34
- code: "DIFFMAP",
35
- message: started.message,
36
- });
37
- }
38
- yield { url: started.url, file: started.filePath };
39
- await started.closed;
40
- return { url: started.url, file: started.filePath };
41
- },
52
+ }
53
+ yield { url: started.url, file: started.filePath };
54
+ await started.closed;
55
+ return { url: started.url, file: started.filePath };
56
+ }
57
+
58
+ const cli = Cli.create("diffmap", {
59
+ description: "Serve a spec locally, or share it as a gist",
60
+ version: packageVersion,
61
+ args: serveArgs,
62
+ options: serveOptions,
63
+ output: serveOutput,
64
+ hint: "`serve <spec.md>` runs the local viewer. `share <spec.md>` publishes a secret gist. `update` (or `--update`) installs the latest diffmap.",
65
+ examples: [
66
+ {
67
+ args: { file: "spec.md" },
68
+ description: "Same as `diffmap serve spec.md`",
69
+ },
70
+ ],
71
+ run: runServe,
42
72
  })
73
+ .command("serve", {
74
+ description: "Run the local viewer server",
75
+ args: serveArgs,
76
+ options: serveOptions,
77
+ output: serveOutput,
78
+ examples: [
79
+ {
80
+ args: { file: "spec.md" },
81
+ description: "Open the local viewer",
82
+ },
83
+ ],
84
+ run: runServe,
85
+ })
86
+ .command("share", {
87
+ description: "Publish a secret gist and print https://diffmap.dev/g/<id>",
88
+ args: z.object({
89
+ file: z.string().describe("Path to the markdown file"),
90
+ }),
91
+ options: z.object({
92
+ public: z.boolean().optional().describe("Create a public gist"),
93
+ }),
94
+ output: z.object({
95
+ gistId: z.string(),
96
+ gistUrl: z.string(),
97
+ viewerUrl: z.string(),
98
+ }),
99
+ examples: [
100
+ {
101
+ args: { file: "spec.md" },
102
+ description: "Secret gist → https://diffmap.dev/g/<id>",
103
+ },
104
+ ],
105
+ async run(c) {
106
+ const shared = await shareMarkdownFile({
107
+ filePath: resolveFromInvokeCwd(c.args.file),
108
+ isPublic: c.options.public === true,
109
+ });
110
+ if (shared instanceof Error) {
111
+ return c.error({
112
+ code: "DIFFMAP",
113
+ message: shared.message,
114
+ });
115
+ }
116
+ return c.ok(shared);
117
+ },
118
+ })
43
119
  .command("list", {
44
120
  description: "List running diffmap viewers",
45
121
  output: z.object({
@@ -63,32 +139,31 @@ const cli = Cli.create("diffmap", {
63
139
  return c.ok({ instances });
64
140
  },
65
141
  })
66
- .command("share", {
67
- description: "Upload a walkthrough gist and print the diffmap.dev URL",
68
- args: z.object({
69
- file: z.string().describe("Path to the markdown file"),
70
- }),
71
- options: z.object({
72
- public: z.boolean().optional().describe("Create a public gist"),
73
- }),
142
+ .command("update", {
143
+ description: `Update diffmap to the latest ${packageName} on npm (same as --update)`,
74
144
  output: z.object({
75
- gistId: z.string(),
76
- gistUrl: z.string(),
77
- viewerUrl: z.string(),
145
+ status: z.enum(["up-to-date", "updated", "manual"]),
146
+ message: z.string(),
147
+ current: z.string(),
148
+ previous: z.string().optional(),
149
+ latest: z.string().optional(),
150
+ command: z.string().optional(),
78
151
  }),
79
152
  async run(c) {
80
- const shared = await shareMarkdownFile({
81
- filePath: resolveFromInvokeCwd(c.args.file),
82
- isPublic: c.options.public === true,
153
+ const result = await runUpdate({
154
+ currentVersion: packageVersion,
155
+ fetchLatestVersion,
156
+ detectInstallMethod,
157
+ runCommand,
83
158
  });
84
- if (shared instanceof Error) {
159
+ if (result instanceof Error) {
85
160
  return c.error({
86
- code: "DIFFMAP",
87
- message: shared.message,
161
+ code: "UPDATE_FAILED",
162
+ message: result.message,
88
163
  });
89
164
  }
90
- return c.ok(shared);
165
+ return c.ok(result);
91
166
  },
92
167
  });
93
168
 
94
- await cli.serve();
169
+ await cli.serve(resolveUpdateArgv(process.argv.slice(2)));
@@ -15,9 +15,11 @@ export function diffmapContentPlugin(input: {
15
15
  async load(id) {
16
16
  if (id !== virtualId) return;
17
17
  const source = await fs.readFile(input.filePath, "utf8");
18
- const document = parseViewerDocument(source);
19
- if (document instanceof Error) throw document;
20
- return `export const viewerDocument = ${JSON.stringify(document)};`;
18
+ const document = parseViewerDocument(source, input.filePath);
19
+ if (document instanceof Error) {
20
+ return `export const viewerDocument = null; export const parseError = ${JSON.stringify(document.message)};`;
21
+ }
22
+ return `export const viewerDocument = ${JSON.stringify(document)}; export const parseError = null;`;
21
23
  },
22
24
  transformIndexHtml(html) {
23
25
  return html.replaceAll(
package/src/errors.ts CHANGED
@@ -17,7 +17,7 @@ export class DiffmapRegistryError extends errore.createTaggedError({
17
17
 
18
18
  export class DiffmapMermaidError extends errore.createTaggedError({
19
19
  name: "DiffmapMermaidError",
20
- message: "diffmap could not render mermaid",
20
+ message: "diffmap could not parse mermaid in $path ($where): $reason",
21
21
  }) {}
22
22
 
23
23
  export class DiffmapParseError extends errore.createTaggedError({
@@ -39,3 +39,8 @@ export class DiffmapShareError extends errore.createTaggedError({
39
39
  name: "DiffmapShareError",
40
40
  message: "diffmap share failed: $reason",
41
41
  }) {}
42
+
43
+ export class DiffmapUpdateError extends errore.createTaggedError({
44
+ name: "DiffmapUpdateError",
45
+ message: "diffmap update failed: $reason",
46
+ }) {}
@@ -6,7 +6,9 @@ import { parseViewerDocument } from "../parseViewer.ts";
6
6
  import type { ViewerDocument } from "../parseViewer.ts";
7
7
  import { ViewerApp } from "../viewer/ViewerApp.tsx";
8
8
  import { loadGistMarkdown, type GistDocument } from "./fetchGist.ts";
9
- import { GistLanding, GistLoading, GistStatus } from "./GistStatus.tsx";
9
+ import { GistPinContext } from "./pin.ts";
10
+ import { HostHome } from "../host/HostHome.tsx";
11
+ import { GistLoading, GistStatus } from "./GistStatus.tsx";
10
12
  import {
11
13
  gistPath,
12
14
  parseGistPath,
@@ -82,7 +84,7 @@ export function GistApp() {
82
84
  return () => abort.abort();
83
85
  }, [route]);
84
86
 
85
- if (route.kind === "home") return <GistLanding />;
87
+ if (route.kind === "home") return <HostHome />;
86
88
  if (route.kind === "invalid") {
87
89
  return (
88
90
  <GistStatus title="Not a gist id">
@@ -151,24 +153,32 @@ export function GistApp() {
151
153
  }
152
154
 
153
155
  return (
154
- <ViewerApp
155
- key={`${view.gist.id}:${view.file}`}
156
- document={view.document}
157
- mode="gist"
158
- headerActions={
159
- <GistHeader
160
- gistId={view.gist.id}
161
- htmlUrl={view.gist.htmlUrl}
162
- files={view.gist.files}
163
- current={view.file}
164
- onSelect={(file) => {
165
- const next = gistPath(view.gist.id, file);
166
- window.history.pushState({}, "", next);
167
- setRoute(parseGistPath(next));
168
- }}
169
- />
170
- }
171
- />
156
+ <GistPinContext.Provider
157
+ value={{
158
+ gistId: view.gist.id,
159
+ file: view.file,
160
+ htmlUrl: view.gist.htmlUrl,
161
+ }}
162
+ >
163
+ <ViewerApp
164
+ key={`${view.gist.id}:${view.file}`}
165
+ document={view.document}
166
+ mode="gist"
167
+ headerActions={
168
+ <GistHeader
169
+ gistId={view.gist.id}
170
+ htmlUrl={view.gist.htmlUrl}
171
+ files={view.gist.files}
172
+ current={view.file}
173
+ onSelect={(file) => {
174
+ const next = gistPath(view.gist.id, file);
175
+ window.history.pushState({}, "", next);
176
+ setRoute(parseGistPath(next));
177
+ }}
178
+ />
179
+ }
180
+ />
181
+ </GistPinContext.Provider>
172
182
  );
173
183
  }
174
184
 
@@ -196,7 +206,10 @@ async function loadView(
196
206
  file: file ?? "",
197
207
  };
198
208
  }
199
- const parsed = parseViewerDocument(loaded.gist.chosen.content);
209
+ const parsed = parseViewerDocument(
210
+ loaded.gist.chosen.content,
211
+ loaded.gist.chosen.name,
212
+ );
200
213
  if (parsed instanceof Error) {
201
214
  return {
202
215
  kind: "parse-error",
@@ -19,6 +19,7 @@ export function GistStatus(props: {
19
19
  children: ReactNode;
20
20
  gistId?: string;
21
21
  gistUrl?: string;
22
+ linkLabel?: string;
22
23
  }) {
23
24
  const shell = useStyles(styles.shell);
24
25
  const header = useStyles(styles.header);
@@ -39,7 +40,9 @@ export function GistStatus(props: {
39
40
  {props.children}
40
41
  {gistUrl !== undefined && (
41
42
  <P>
42
- <Link href={gistUrl}>Open gist on GitHub</Link>
43
+ <Link href={gistUrl}>
44
+ {props.linkLabel ?? "Open gist on GitHub"}
45
+ </Link>
43
46
  </P>
44
47
  )}
45
48
  </main>
@@ -47,10 +50,10 @@ export function GistStatus(props: {
47
50
  );
48
51
  }
49
52
 
50
- export function GistLoading() {
53
+ export function GistLoading(props: { title?: string }) {
51
54
  const row = useStyles(styles.loading);
52
55
  return (
53
- <GistStatus title="Loading gist…">
56
+ <GistStatus title={props.title ?? "Loading gist…"}>
54
57
  <div className={row}>
55
58
  <Thinking />
56
59
  </div>
@@ -58,41 +61,6 @@ export function GistLoading() {
58
61
  );
59
62
  }
60
63
 
61
- export function GistLanding() {
62
- const code = useStyles(styles.code);
63
- return (
64
- <GistStatus title="diffmap">
65
- <P>
66
- A hosted viewer for diffmap walkthroughs. This page fetches a GitHub
67
- gist in your browser. There is no server holding the markdown.
68
- </P>
69
- <P>
70
- Canonical URL:{" "}
71
- <code className={code}>https://diffmap.dev/g/&lt;gistId&gt;</code>.
72
- Optional file:{" "}
73
- <code className={code}>/g/&lt;gistId&gt;/&lt;file.md&gt;</code>. Heading
74
- hashes stay in the fragment, same as the local viewer.
75
- </P>
76
- <P>
77
- Share a secret gist (requires <code className={code}>gh</code>
78
- ):
79
- </P>
80
- <P>
81
- <code className={code}>npx diffmap share path/to.md</code>
82
- </P>
83
- <P>
84
- Secret gists are unlisted, not private. Anyone with the id can read
85
- them. Unauthenticated GitHub API is 60 requests per hour per IP.
86
- </P>
87
- <P>
88
- <Link href="https://github.com/tanishqkancharla/diffmap">
89
- tanishqkancharla/diffmap
90
- </Link>
91
- </P>
92
- </GistStatus>
93
- );
94
- }
95
-
96
64
  const styles = {
97
65
  shell: style(flex({ direction: "column" }), {
98
66
  width: "100%",
@@ -103,7 +71,7 @@ const styles = {
103
71
  backgroundColor: colors.gray[4],
104
72
  }),
105
73
  header: style(
106
- flex({ direction: "row", align: "center", justify: "between" }),
74
+ flex({ direction: "row", alignItems: "center", justifyContent: "between" }),
107
75
  spacing.padding({ x: 6, y: 3 }),
108
76
  flexItem({ size: "hug" }),
109
77
  border(["bottom"], "border"),
@@ -128,10 +96,7 @@ const styles = {
128
96
  title: style(text({ size: "xl", fontWeight: 600, color: "highContrast" }), {
129
97
  marginBottom: spacing.value(4),
130
98
  }),
131
- loading: style(flex({ direction: "row", align: "center" }), {
99
+ loading: style(flex({ direction: "row", alignItems: "center" }), {
132
100
  minHeight: spacing.value(12),
133
101
  }),
134
- code: style(text({ size: "sm", color: "highContrast" }), {
135
- fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
136
- }),
137
102
  };
package/src/gist/main.tsx CHANGED
@@ -2,7 +2,7 @@ import { StrictMode } from "react";
2
2
  import { createRoot } from "react-dom/client";
3
3
  import { MauiProvider } from "maui";
4
4
  import { rewritePiGistUrl } from "./route.ts";
5
- import { GistApp } from "./GistApp.tsx";
5
+ import { HostApp } from "../host/HostApp.tsx";
6
6
  import "../viewer/styles.css";
7
7
 
8
8
  const rewritten = rewritePiGistUrl(
@@ -19,7 +19,7 @@ if (root === null) throw new Error("diffmap root element is missing");
19
19
  createRoot(root).render(
20
20
  <StrictMode>
21
21
  <MauiProvider>
22
- <GistApp />
22
+ <HostApp />
23
23
  </MauiProvider>
24
24
  </StrictMode>,
25
25
  );
@@ -0,0 +1,14 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ /** Gist snapshot: identify the gist file, never a fake owner/repo. */
4
+ export type GistPin = {
5
+ gistId: string;
6
+ file: string;
7
+ htmlUrl: string;
8
+ };
9
+
10
+ export const GistPinContext = createContext<GistPin | undefined>(undefined);
11
+
12
+ export function useGistPin() {
13
+ return useContext(GistPinContext);
14
+ }