critique 0.1.10 → 0.1.12

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/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ # 0.1.12
2
+
3
+ - Web preview:
4
+ - Add client-side mobile detection with redirect to `?v=mobile`
5
+ - Simplify worker: redirect mobile devices instead of content negotiation
6
+ - Remove `Vary` header - URL now determines content, better caching
7
+ - Increase cache max-age to 24h (was 1h)
8
+
9
+ # 0.1.11
10
+
11
+ - All commands:
12
+ - Add support for passing file filters as positional args after `--` (e.g. `critique web -- src/cli.tsx`)
13
+ - Web command:
14
+ - Add `--title` option for custom HTML document title
15
+ - Add scrollbar styling to HTML output (dark/light mode aware)
16
+
1
17
  # 0.1.10
2
18
 
3
19
  - Default command:
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A beautiful terminal UI for reviewing git diffs with syntax highlighting, split view, and word-level diff.
4
4
 
5
- ![Diff Viewer Demo](diff-viewer-demo.png)
5
+ ![Diff Viewer Demo](screenshot.png)
6
6
 
7
7
  ## Installation
8
8
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "critique",
3
3
  "module": "src/diff.tsx",
4
4
  "type": "module",
5
- "version": "0.1.10",
5
+ "version": "0.1.12",
6
6
  "private": false,
7
7
  "bin": "./src/cli.tsx",
8
8
  "scripts": {
package/screenshot.png ADDED
Binary file
package/src/ansi-html.ts CHANGED
@@ -15,6 +15,8 @@ export interface AnsiToHtmlOptions {
15
15
  trimEmptyLines?: boolean
16
16
  /** Enable auto light/dark mode based on system preference */
17
17
  autoTheme?: boolean
18
+ /** HTML document title */
19
+ title?: string
18
20
  }
19
21
 
20
22
  /**
@@ -129,6 +131,7 @@ export function ansiToHtmlDocument(input: string | Buffer, options: AnsiToHtmlOp
129
131
  textColor = "#1a1a1a",
130
132
  fontFamily = "Monaco, Menlo, 'Ubuntu Mono', Consolas, monospace",
131
133
  fontSize = "14px",
134
+ title = "Critique Diff",
132
135
  } = options
133
136
 
134
137
  const content = ansiToHtml(input, options)
@@ -143,7 +146,7 @@ export function ansiToHtmlDocument(input: string | Buffer, options: AnsiToHtmlOp
143
146
  <head>
144
147
  <meta charset="utf-8">
145
148
  <meta name="viewport" content="width=device-width, initial-scale=1">
146
- <title>Critique Diff</title>
149
+ <title>${escapeHtml(title)}</title>
147
150
  <style>
148
151
  * { margin: 0; padding: 0; box-sizing: border-box; }
149
152
  html {
@@ -189,10 +192,7 @@ ${options.autoTheme ? `@media (prefers-color-scheme: light) {
189
192
  html {
190
193
  filter: invert(1) hue-rotate(180deg);
191
194
  }
192
- }` : ''}
193
- </style>
194
- </head>
195
- <body>
195
+ }` : ''}\nhtml{scrollbar-width:thin;scrollbar-color:#6b7280 #2d3748;}@media(prefers-color-scheme:light){html{scrollbar-color:#a0aec0 #edf2f7;}}::-webkit-scrollbar{width:12px;}::-webkit-scrollbar-track{background:#2d3748;}::-webkit-scrollbar-thumb{background:#6b7280;border-radius:6px;}::-webkit-scrollbar-thumb:hover{background:#a0aec0;}@media(prefers-color-scheme:light){::-webkit-scrollbar-track{background:#edf2f7;}::-webkit-scrollbar-thumb{background:#a0aec0;}::-webkit-scrollbar-thumb:hover{background:#cbd5e1;}}::-webkit-scrollbar {\n width: 12px;\n}\n::-webkit-scrollbar-track {\n background: #2d3748;\n}\n::-webkit-scrollbar-thumb {\n background: #6b7280;\n border-radius: 6px;\n}\n::-webkit-scrollbar-thumb:hover {\n background: #a0aec0;\n}\n@media (prefers-color-scheme: light) {\n ::-webkit-scrollbar-track {\n background: #edf2f7;\n }\n ::-webkit-scrollbar-thumb {\n background: #a0aec0;\n }\n ::-webkit-scrollbar-thumb:hover {\n background: #cbd5e1;\n }\n}\n</style>\n</head>\n<body>
196
196
  <div id="content">
197
197
  ${content}
198
198
  </div>
@@ -204,6 +204,17 @@ ${content}
204
204
  const minFontSize = 4;
205
205
  const maxFontSize = 16;
206
206
 
207
+ // Redirect mobile devices to ?v=mobile for optimized view
208
+ // Only redirect if not already on a forced version
209
+ const params = new URLSearchParams(window.location.search);
210
+ if (!params.has('v')) {
211
+ const isMobile = /Mobile|iP(hone|od|ad)|Android|BlackBerry|IEMobile|Kindle|Opera M(obi|ini)|Windows Phone|webOS/i.test(navigator.userAgent);
212
+ if (isMobile) {
213
+ params.set('v', 'mobile');
214
+ window.location.replace(window.location.pathname + '?' + params.toString());
215
+ }
216
+ }
217
+
207
218
  function adjustFontSize() {
208
219
  const viewportWidth = window.innerWidth;
209
220
  const calculatedSize = (viewportWidth - padding) / (cols * charRatio);
package/src/cli.tsx CHANGED
@@ -566,7 +566,10 @@ cli
566
566
  .action(async (base, head, options) => {
567
567
  try {
568
568
  const contextArg = options.context ? `-U${options.context}` : "";
569
- const filters = options.filter ? (Array.isArray(options.filter) ? options.filter : [options.filter]) : [];
569
+ // Combine --filter options with positional args after --
570
+ const filterOptions = options.filter ? (Array.isArray(options.filter) ? options.filter : [options.filter]) : [];
571
+ const positionalFilters = options['--'] || [];
572
+ const filters = [...filterOptions, ...positionalFilters];
570
573
  const filterArg = filters.length > 0 ? `-- ${filters.map((f: string) => `"${f}"`).join(" ")}` : "";
571
574
  const gitCommand = (() => {
572
575
  if (options.staged)
@@ -1031,12 +1034,16 @@ cli
1031
1034
  .option("--context <lines>", "Number of context lines (default: 3)")
1032
1035
  .option("--theme <name>", "Theme to use for rendering")
1033
1036
  .option("--filter <pattern>", "Filter files by glob pattern (can be used multiple times)")
1037
+ .option("--title <title>", "HTML document title")
1034
1038
  .action(async (base, head, options) => {
1035
1039
  const pty = await import("@xmorse/bun-pty");
1036
1040
  const { ansiToHtmlDocument } = await import("./ansi-html.ts");
1037
1041
 
1038
1042
  const contextArg = options.context ? `-U${options.context}` : "";
1039
- const filters = options.filter ? (Array.isArray(options.filter) ? options.filter : [options.filter]) : [];
1043
+ // Combine --filter options with positional args after --
1044
+ const filterOptions = options.filter ? (Array.isArray(options.filter) ? options.filter : [options.filter]) : [];
1045
+ const positionalFilters = options['--'] || [];
1046
+ const filters = [...filterOptions, ...positionalFilters];
1040
1047
  const filterArg = filters.length > 0 ? `-- ${filters.map((f: string) => `"${f}"`).join(" ")}` : "";
1041
1048
  const gitCommand = (() => {
1042
1049
  if (options.staged)
@@ -1133,7 +1140,7 @@ cli
1133
1140
  const backgroundColor = rgbaToHex(theme.background);
1134
1141
  const textColor = rgbaToHex(theme.text);
1135
1142
 
1136
- return ansiToHtmlDocument(ansiOutput, { cols, rows: renderRows, backgroundColor, textColor, autoTheme: !customTheme });
1143
+ return ansiToHtmlDocument(ansiOutput, { cols, rows: renderRows, backgroundColor, textColor, autoTheme: !customTheme, title: options.title });
1137
1144
  }
1138
1145
 
1139
1146
  // Generate desktop and mobile versions in parallel
@@ -1376,5 +1383,4 @@ cli
1376
1383
 
1377
1384
  cli.help();
1378
1385
  cli.version("1.0.0");
1379
- // comment
1380
1386
  cli.parse();
package/src/worker.ts CHANGED
@@ -90,7 +90,8 @@ app.post("/upload", async (c) => {
90
90
 
91
91
  // View HTML content with streaming
92
92
  // GET /view/:id
93
- // Query params: ?v=desktop or ?v=mobile to force a version
93
+ // Query params: ?v=desktop or ?v=mobile to select version
94
+ // Server redirects mobile devices to ?v=mobile, client JS also handles redirect
94
95
  app.get("/view/:id", async (c) => {
95
96
  const id = c.req.param("id")
96
97
 
@@ -98,12 +99,21 @@ app.get("/view/:id", async (c) => {
98
99
  return c.text("Invalid ID", 400)
99
100
  }
100
101
 
101
- // Check for forced version via query param
102
- const forcedVersion = c.req.query("v")
103
- const isMobile = forcedVersion === "mobile" || (forcedVersion !== "desktop" && isMobileDevice(c))
102
+ // Check for version query param
103
+ const version = c.req.query("v")
104
+
105
+ // If no version specified and mobile device detected, redirect to ?v=mobile
106
+ // This is a fallback - client JS also handles this redirect
107
+ if (!version && isMobileDevice(c)) {
108
+ const url = new URL(c.req.url)
109
+ url.searchParams.set("v", "mobile")
110
+ return c.redirect(url.toString(), 302)
111
+ }
104
112
 
105
- // Try to get the appropriate version
113
+ // Serve the appropriate version based on query param
114
+ const isMobile = version === "mobile"
106
115
  let html: string | null = null
116
+
107
117
  if (isMobile) {
108
118
  // Try mobile version first, fall back to desktop
109
119
  html = await c.env.CRITIQUE_KV.get(`${id}-mobile`)
@@ -120,11 +130,9 @@ app.get("/view/:id", async (c) => {
120
130
 
121
131
  // Stream the HTML content for faster initial load
122
132
  return stream(c, async (s) => {
123
- // Set content type header
124
133
  c.header("Content-Type", "text/html; charset=utf-8")
125
- // Vary by User-Agent and Sec-CH-UA-Mobile for proper caching
126
- c.header("Vary", "User-Agent, Sec-CH-UA-Mobile")
127
- c.header("Cache-Control", "public, max-age=3600")
134
+ // Cache is now safe - URL determines content, no Vary needed
135
+ c.header("Cache-Control", "public, max-age=86400")
128
136
 
129
137
  // Stream in chunks for better performance
130
138
  const chunkSize = 16 * 1024 // 16KB chunks
Binary file