Skip to content

Commit 97564f9

Browse files
committed
feat: enhance context bar with actionable stats and cleaner UX
- Add Activity icon for active requests (network monitor feel) - Display request count with relative timestamp (event-driven via chrome.storage.onChanged) - Simplify org display (e.g., 'lytics' instead of 'app.lytics.com') - Two-line layout for better readability - Clean empty state: 'No matching requests yet' - Add date-fns for lightweight time formatting (~2-3KB) - Leverage SDK Kit's event-driven architecture (no polling)
1 parent e491b30 commit 97564f9

File tree

9 files changed

+291
-60
lines changed

9 files changed

+291
-60
lines changed

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@
1717
"prepare": "husky",
1818
"lint-staged": "biome check --write --no-errors-on-unmatched"
1919
},
20-
"keywords": ["chrome-extension", "auth", "headers", "sdk-kit", "developer-tools"],
20+
"keywords": [
21+
"chrome-extension",
22+
"auth",
23+
"headers",
24+
"sdk-kit",
25+
"developer-tools"
26+
],
2127
"author": "ProsDevLab",
2228
"license": "MIT",
2329
"packageManager": "[email protected]",
@@ -30,6 +36,7 @@
3036
"@radix-ui/react-switch": "^1.2.6",
3137
"class-variance-authority": "^0.7.1",
3238
"clsx": "^2.1.1",
39+
"date-fns": "^4.1.0",
3340
"lucide-react": "^0.562.0",
3441
"react": "^19.0.0",
3542
"react-dom": "^19.0.0",

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/generate-icons.cjs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
const fs = require('node:fs');
2+
const path = require('node:path');
3+
const zlib = require('node:zlib');
4+
5+
// Simple PNG generator
6+
const sizes = [16, 48, 128];
7+
const iconsDir = path.join(__dirname, '../src/icons');
8+
9+
// Create a simple colored square PNG manually
10+
function createSimplePNG(size, color = [59, 130, 246]) {
11+
const width = size;
12+
const height = size;
13+
14+
// PNG signature
15+
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
16+
17+
// IHDR chunk
18+
const ihdr = Buffer.alloc(25);
19+
ihdr.writeUInt32BE(13, 0); // Length
20+
ihdr.write('IHDR', 4);
21+
ihdr.writeUInt32BE(width, 8);
22+
ihdr.writeUInt32BE(height, 12);
23+
ihdr.writeUInt8(8, 16); // Bit depth
24+
ihdr.writeUInt8(2, 17); // Color type (RGB)
25+
ihdr.writeUInt8(0, 18); // Compression
26+
ihdr.writeUInt8(0, 19); // Filter
27+
ihdr.writeUInt8(0, 20); // Interlace
28+
29+
const crc = zlib.crc32(ihdr.slice(4, 21));
30+
ihdr.writeUInt32BE(crc, 21);
31+
32+
// IDAT chunk - solid blue color
33+
const pixelData = Buffer.alloc(height * (1 + width * 3));
34+
for (let y = 0; y < height; y++) {
35+
pixelData[y * (1 + width * 3)] = 0; // Filter type
36+
for (let x = 0; x < width; x++) {
37+
const offset = y * (1 + width * 3) + 1 + x * 3;
38+
pixelData[offset] = color[0]; // R
39+
pixelData[offset + 1] = color[1]; // G
40+
pixelData[offset + 2] = color[2]; // B
41+
}
42+
}
43+
44+
const compressed = zlib.deflateSync(pixelData);
45+
const idat = Buffer.alloc(12 + compressed.length);
46+
idat.writeUInt32BE(compressed.length, 0);
47+
idat.write('IDAT', 4);
48+
compressed.copy(idat, 8);
49+
const idatCrc = zlib.crc32(idat.slice(4, 8 + compressed.length));
50+
idat.writeUInt32BE(idatCrc, 8 + compressed.length);
51+
52+
// IEND chunk
53+
const iend = Buffer.from([0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]);
54+
55+
return Buffer.concat([signature, ihdr, idat, iend]);
56+
}
57+
58+
// Generate icons
59+
sizes.forEach((size) => {
60+
const png = createSimplePNG(size);
61+
fs.writeFileSync(path.join(iconsDir, `icon-${size}.png`), png);
62+
console.log(`✓ Created icon-${size}.png`);
63+
});
64+
65+
console.log('\n✓ Icons generated successfully!');

scripts/generate-icons.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
const fs = require('node:fs');
2+
const path = require('node:path');
3+
const zlib = require('node:zlib');
4+
5+
// Simple PNG generator
6+
const sizes = [16, 48, 128];
7+
const iconsDir = path.join(__dirname, '../src/icons');
8+
9+
// Create a simple colored square PNG manually
10+
function createSimplePNG(size, color = [59, 130, 246]) {
11+
const width = size;
12+
const height = size;
13+
14+
// PNG signature
15+
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
16+
17+
// IHDR chunk
18+
const ihdr = Buffer.alloc(25);
19+
ihdr.writeUInt32BE(13, 0); // Length
20+
ihdr.write('IHDR', 4);
21+
ihdr.writeUInt32BE(width, 8);
22+
ihdr.writeUInt32BE(height, 12);
23+
ihdr.writeUInt8(8, 16); // Bit depth
24+
ihdr.writeUInt8(2, 17); // Color type (RGB)
25+
ihdr.writeUInt8(0, 18); // Compression
26+
ihdr.writeUInt8(0, 19); // Filter
27+
ihdr.writeUInt8(0, 20); // Interlace
28+
29+
const crc = zlib.crc32(ihdr.slice(4, 21));
30+
ihdr.writeUInt32BE(crc, 21);
31+
32+
// IDAT chunk - solid blue color
33+
const pixelData = Buffer.alloc(height * (1 + width * 3));
34+
for (let y = 0; y < height; y++) {
35+
pixelData[y * (1 + width * 3)] = 0; // Filter type
36+
for (let x = 0; x < width; x++) {
37+
const offset = y * (1 + width * 3) + 1 + x * 3;
38+
pixelData[offset] = color[0]; // R
39+
pixelData[offset + 1] = color[1]; // G
40+
pixelData[offset + 2] = color[2]; // B
41+
}
42+
}
43+
44+
const compressed = zlib.deflateSync(pixelData);
45+
const idat = Buffer.alloc(12 + compressed.length);
46+
idat.writeUInt32BE(compressed.length, 0);
47+
idat.write('IDAT', 4);
48+
compressed.copy(idat, 8);
49+
const idatCrc = zlib.crc32(idat.subarray(4, 8 + compressed.length));
50+
idat.writeUInt32BE(idatCrc, 8 + compressed.length);
51+
52+
// IEND chunk
53+
const iend = Buffer.from([0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]);
54+
55+
return Buffer.concat([signature, ihdr, idat, iend]);
56+
}
57+
58+
// Generate icons
59+
for (const size of sizes) {
60+
const png = createSimplePNG(size);
61+
fs.writeFileSync(path.join(iconsDir, `icon-${size}.png`), png);
62+
console.log(`✓ Created icon-${size}.png`);
63+
}
64+
65+
console.log('\n✓ Icons generated successfully!');

scripts/generate-icons.mjs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { dirname, join } from 'node:path';
2+
import { fileURLToPath } from 'node:url';
3+
import sharp from 'sharp';
4+
5+
const __filename = fileURLToPath(import.meta.url);
6+
const __dirname = dirname(__filename);
7+
8+
const iconsDir = join(__dirname, '../src/icons');
9+
const mainIconPath = join(iconsDir, 'icon-main.png');
10+
11+
const sizes = [16, 48, 128];
12+
13+
async function generateIcons() {
14+
for (const size of sizes) {
15+
await sharp(mainIconPath)
16+
.resize(size, size)
17+
.png()
18+
.toFile(join(iconsDir, `icon-${size}.png`));
19+
console.log(`✓ Created icon-${size}.png`);
20+
}
21+
console.log('\n✓ Icons generated successfully!');
22+
}
23+
24+
generateIcons().catch(console.error);

src/panel/App.tsx

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import { Button } from '@/components/ui/button';
21
import { Label } from '@/components/ui/label';
32
import { Switch } from '@/components/ui/switch';
43
import type { AuthRule } from '@/shared/types';
5-
import { Settings } from 'lucide-react';
64
import { useState } from 'react';
75
import { ContextBar } from './components/ContextBar';
86
import { RuleForm } from './components/RuleForm';
97
import { RulesList } from './components/RulesList';
10-
import { SettingsDialog } from './components/SettingsDialog';
118
import { useAuthRules } from './hooks/useAuthRules';
129
import { useCurrentTab } from './hooks/useCurrentTab';
1310
import { useExtensionEnabled } from './hooks/useExtensionEnabled';
@@ -17,11 +14,15 @@ export function App() {
1714
const { rules, loading: rulesLoading, addRule, updateRule, deleteRule } = useAuthRules();
1815
const { isEnabled, loading: enabledLoading, setEnabled } = useExtensionEnabled();
1916
const { tab, loading: tabLoading } = useCurrentTab();
20-
const { getActiveRuleIds, getCountForRules, loading: statsLoading } = useRequestStats();
17+
const {
18+
getActiveRuleIds,
19+
getCountForRules,
20+
getLastSeenForRules,
21+
loading: statsLoading,
22+
} = useRequestStats();
2123

2224
const [isFormOpen, setIsFormOpen] = useState(false);
2325
const [editingRule, setEditingRule] = useState<AuthRule | null>(null);
24-
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
2526

2627
const loading = rulesLoading || enabledLoading || tabLoading || statsLoading;
2728

@@ -62,9 +63,10 @@ export function App() {
6263
const activeRuleIds = getActiveRuleIds();
6364
const activeDisplayRules = relevantRules.filter((rule) => activeRuleIds.has(rule.id));
6465

65-
// Get request count for relevant rules only
66+
// Get request count and timestamp for relevant rules
6667
const relevantPatterns = relevantRules.map((r) => r.pattern);
6768
const pageRequestCount = getCountForRules(relevantPatterns);
69+
const lastSeenTimestamp = getLastSeenForRules(relevantPatterns);
6870

6971
// Handlers
7072
const handleAddRule = async (rule: Omit<AuthRule, 'id' | 'createdAt' | 'updatedAt'>) => {
@@ -104,17 +106,16 @@ export function App() {
104106
disabled={loading}
105107
/>
106108
</div>
107-
<Button variant="ghost" size="icon" onClick={() => setIsSettingsOpen(true)}>
108-
<Settings className="h-5 w-5" />
109-
</Button>
110109
</div>
111110
</div>
112111

113-
{/* Context Bar (Sticky) - Shows active rules for current page's domain */}
112+
{/* Context Bar (Sticky) - Shows active rules for current page */}
114113
<ContextBar
115114
tab={tab}
116-
matchCount={activeDisplayRules.length}
115+
matchCount={relevantRules.length}
117116
requestCount={pageRequestCount}
117+
lastSeen={lastSeenTimestamp}
118+
isEnabled={isEnabled}
118119
loading={loading}
119120
/>
120121

@@ -158,9 +159,6 @@ export function App() {
158159
title="Edit Auth Rule"
159160
/>
160161
)}
161-
162-
{/* Settings Dialog */}
163-
<SettingsDialog open={isSettingsOpen} onOpenChange={setIsSettingsOpen} />
164162
</div>
165163
);
166164
}

src/panel/components/ContextBar.tsx

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,78 @@
1-
import { Globe } from 'lucide-react';
1+
import { formatDistanceToNow } from 'date-fns';
2+
import { Activity, Globe } from 'lucide-react';
23
import type { CurrentTab } from '../hooks/useCurrentTab';
34

45
interface ContextBarProps {
56
tab: CurrentTab;
6-
matchCount: number; // Number of rules actively intercepting requests (based on stats)
7-
requestCount: number; // Total requests intercepted
7+
matchCount: number; // Number of rules matching this page
8+
requestCount: number; // Total requests intercepted for these rules
9+
lastSeen: number | null; // Most recent request timestamp
10+
isEnabled: boolean; // Whether extension is enabled
811
loading?: boolean;
912
}
1013

1114
/**
12-
* Context bar showing current page and active interceptors
13-
* "Active" means the rule has actually intercepted API calls (not URL pattern matching)
15+
* Compact context bar showing current page and actionable status
16+
* Uses existing metadata to show if rules are working
1417
*/
15-
export function ContextBar({ tab, matchCount, requestCount, loading }: ContextBarProps) {
16-
// Extract hostname from URL
17-
const getHostname = (url: string | null) => {
18+
export function ContextBar({
19+
tab,
20+
matchCount,
21+
requestCount,
22+
lastSeen,
23+
isEnabled,
24+
loading,
25+
}: ContextBarProps) {
26+
// Extract org name from hostname (e.g., "lytics" from "app.lytics.com")
27+
const getOrgName = (url: string | null) => {
1828
if (!url) return null;
1929
try {
20-
return new URL(url).hostname;
30+
const hostname = new URL(url).hostname;
31+
const parts = hostname.split('.');
32+
// Get second-to-last part (e.g., "lytics" from "app.lytics.com")
33+
return parts.length >= 2 ? parts[parts.length - 2] : hostname;
2134
} catch {
2235
return url;
2336
}
2437
};
2538

26-
const hostname = getHostname(tab.url);
39+
const orgName = getOrgName(tab.url);
2740

2841
return (
2942
<div className="sticky top-[60px] z-10 border-b border-border bg-muted/30 backdrop-blur-sm p-3">
30-
<div className="flex items-center justify-between px-4 py-3">
31-
<div className="flex items-center gap-2 flex-1 min-w-0">
43+
<div className="px-4 py-3 space-y-2">
44+
{/* Page/Org Name */}
45+
<div className="flex items-center gap-2">
3246
<Globe className="h-4 w-4 text-muted-foreground flex-shrink-0" />
3347
{loading ? (
3448
<span className="text-sm text-muted-foreground">Loading...</span>
3549
) : tab.isRestricted ? (
3650
<span className="text-sm text-destructive">Not available on this page</span>
3751
) : (
3852
<span className="text-sm font-medium text-foreground truncate">
39-
{hostname || 'No page'}
53+
{orgName || 'No page'}
4054
</span>
4155
)}
4256
</div>
4357

44-
{!loading && !tab.isRestricted && (
45-
<div className="flex items-center gap-2 flex-shrink-0">
46-
<div
47-
className={`h-2 w-2 rounded-full ${matchCount > 0 ? 'bg-primary' : 'bg-muted-foreground'}`}
48-
/>
49-
<span
50-
className={`text-xs font-medium ${matchCount > 0 ? 'text-primary' : 'text-muted-foreground'}`}
51-
>
52-
{requestCount > 0
53-
? `${requestCount} request${requestCount === 1 ? '' : 's'}${matchCount} rule${matchCount === 1 ? '' : 's'}`
54-
: matchCount > 0
55-
? `${matchCount} rule${matchCount === 1 ? '' : 's'} active`
56-
: 'No active rules'}
57-
</span>
58+
{/* Activity Status */}
59+
{!loading && !tab.isRestricted && isEnabled && matchCount > 0 && (
60+
<div className="flex items-center gap-2 pl-6">
61+
{requestCount > 0 ? (
62+
<>
63+
<Activity className="h-3.5 w-3.5 text-primary" />
64+
<span className="text-xs font-medium text-primary">
65+
{requestCount} request{requestCount === 1 ? '' : 's'}
66+
{lastSeen && (
67+
<span className="text-muted-foreground ml-1">
68+
{formatDistanceToNow(lastSeen, { addSuffix: true })}
69+
</span>
70+
)}
71+
</span>
72+
</>
73+
) : (
74+
<span className="text-xs text-muted-foreground">No matching requests yet</span>
75+
)}
5876
</div>
5977
)}
6078
</div>

0 commit comments

Comments
 (0)