Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "QueryCraft",
"version": "1.0.2",
"version": "1.0.8",
"description": "Inspect, edit, save, and replay URL query parameters on the active tab.",
"action": {
"default_popup": "src/popup/index.html",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "querycraft",
"version": "1.0.0",
"version": "1.0.8",
"description": "Inspect, edit, save, and replay URL query parameters on the active tab.",
"private": true,
"type": "module",
Expand Down
45 changes: 45 additions & 0 deletions src/__tests__/GroupedLinksList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { GroupedLinksList } from '@/components/SavedLinksDrawer/GroupedLinksList';
import type { Group, SavedLink } from '@/types';

const group: Group = { id: 'g1', name: 'Unsorted', createdAt: 0 };
const link: SavedLink = {
id: 'l1',
url: 'https://example.com/?foo=bar',
label: 'Example',
createdAt: 0,
groupId: 'g1',
};

function renderList(onLoadLink = vi.fn()) {
render(
<GroupedLinksList
groups={[group]}
linksByGroup={new Map([['g1', [link]]])}
onLoadLink={onLoadLink}
onDeleteLink={vi.fn()}
onStartEdit={vi.fn()}
onStartSave={vi.fn()}
canSave
/>,
);
return onLoadLink;
}

describe('GroupedLinksList', () => {
it('loads the saved URL when its name is clicked', async () => {
const onLoadLink = renderList();

await userEvent.click(screen.getByText('Example'));

expect(onLoadLink).toHaveBeenCalledWith('https://example.com/?foo=bar');
});

it('exposes the name as a button so it can be reached by keyboard', () => {
renderList();

expect(screen.getByRole('button', { name: /^Example/ })).toBeInTheDocument();
});
});
15 changes: 15 additions & 0 deletions src/__tests__/useActiveTabUrl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ describe('useActiveTabUrl', () => {
});
});

it('keeps the tab id when Chrome hides the URL (e.g. a new tab page)', async () => {
vi.mocked(chrome.tabs.query).mockResolvedValueOnce([
{ id: 3, active: true } as chrome.tabs.Tab,
]);

renderHook(() => useActiveTabUrl());

await waitFor(() => {
expect(useAppStore.getState().tabState.status).toBe('unsupported');
});

// The tab id is what lets a saved link be loaded from this tab.
expect(useAppStore.getState().tabState).toMatchObject({ status: 'unsupported', tabId: 3 });
});

it('sets error status when chrome.tabs.query rejects', async () => {
vi.mocked(chrome.tabs.query).mockRejectedValueOnce(new Error('Permission denied'));

Expand Down
9 changes: 7 additions & 2 deletions src/components/SavedLinksDrawer/GroupedLinksList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,15 @@ export function GroupedLinksList({
const name = link.label || link.url;
return (
<li key={link.id} className={styles.linkRow}>
<span className={styles.linkText}>
<button
type="button"
className={styles.linkText}
title="Load into editor"
onClick={() => onLoadLink(link.url)}
>
<span className={styles.linkLabel}>{name}</span>
{link.label && <span className={styles.linkUrl}>{link.url}</span>}
</span>
</button>
<span className={styles.linkActions}>
<IconButton
aria-label={`Load ${name}`}
Expand Down
14 changes: 14 additions & 0 deletions src/components/SavedLinksDrawer/SavedLinksDrawer.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,13 @@
border-color: var(--color-border-strong);
}

/* Clicking the name loads the link into the editor. */
.linkText {
flex: 1;
min-width: 0;
text-align: left;
line-height: inherit;
border-radius: var(--radius-sm);
}

.linkLabel {
Expand All @@ -170,6 +174,15 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color var(--transition-fast);
}

.linkText:hover .linkLabel {
color: var(--color-text-primary);
}

.linkText:hover .linkUrl {
color: var(--color-text-secondary);
}

.linkUrl {
Expand All @@ -181,6 +194,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color var(--transition-fast);
}

.linkActions {
Expand Down
6 changes: 4 additions & 2 deletions src/lib/tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@

export interface ActiveTab {
id: number;
/** Empty when Chrome hides it (e.g. a new tab page — we have no "tabs" permission). */
url: string;
}

export const tabs = {
async getActive(): Promise<ActiveTab | null> {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab || tab.id === undefined || !tab.url) return null;
return { id: tab.id, url: tab.url };
if (!tab || tab.id === undefined) return null;
// Keep the id even without a URL so a saved link can still be loaded into this tab.
return { id: tab.id, url: tab.url ?? '' };
},

async updateUrl(tabId: number, url: string): Promise<void> {
Expand Down
33 changes: 18 additions & 15 deletions src/styles/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,31 @@
/* Surfaces */
--color-bg: oklch(0.15 0 0); /* popup panel */
--color-bg-outer: oklch(0.1 0 0); /* page behind the panel */
--color-surface-inset: oklch(0.19 0 0); /* inputs, url box, saved items */
--color-surface-header: oklch(0.18 0 0); /* card header strips */
--color-surface-inset: oklch(0.22 0 0); /* inputs, url box, saved items */
--color-surface-header: oklch(0.21 0 0); /* card header strips */

/* Text — descending emphasis */
/*
* Text — descending emphasis. Every step, faint included, stays at or above
* 4.5:1 on bg, surface-inset and surface-header (the README's AA rule).
*/
--color-text-primary: oklch(0.95 0 0);
--color-text-strong: oklch(0.9 0 0); /* param keys, active values */
--color-text-body: oklch(0.94 0 0); /* editable input text */
--color-text-secondary: oklch(0.75 0 0); /* outlined button labels */
--color-text-muted: oklch(0.55 0 0); /* section labels, meta */
--color-text-faint: oklch(0.45 0 0); /* placeholders, idle icons */
--color-text-muted: oklch(0.67 0 0); /* section labels, meta */
--color-text-faint: oklch(0.62 0 0); /* placeholders, idle icons */

/* Borders — white at low alpha */
--color-border-subtle: oklch(1 0 0 / 0.06);
--color-border: oklch(1 0 0 / 0.08);
--color-border-card: oklch(1 0 0 / 0.09);
--color-border-strong: oklch(1 0 0 / 0.12);
--color-border-subtle: oklch(1 0 0 / 0.08);
--color-border: oklch(1 0 0 / 0.1);
--color-border-card: oklch(1 0 0 / 0.12);
--color-border-strong: oklch(1 0 0 / 0.16);
--color-border-focus: oklch(1 0 0 / 0.3);

/* Fills for hover / inset controls */
--color-fill-subtle: oklch(1 0 0 / 0.06);
--color-fill: oklch(1 0 0 / 0.08);
--color-fill-strong: oklch(1 0 0 / 0.1);
--color-fill-subtle: oklch(1 0 0 / 0.08);
--color-fill: oklch(1 0 0 / 0.1);
--color-fill-strong: oklch(1 0 0 / 0.14);

/*
* Primary action — inverted block by default. When an accent color is
Expand Down Expand Up @@ -81,12 +84,12 @@

/* JSON syntax — monochrome, separated by lightness + weight */
--json-key: oklch(0.95 0 0);
--json-index: oklch(0.6 0 0);
--json-index: oklch(0.66 0 0);
--json-string: oklch(0.8 0 0);
--json-number: oklch(0.9 0 0);
--json-boolean: oklch(0.85 0 0);
--json-null: oklch(0.55 0 0);
--json-punct: oklch(0.5 0 0);
--json-null: oklch(0.64 0 0);
--json-punct: oklch(0.62 0 0);
}

/* ── Light ────────────────────────────────────────────────────────── */
Expand Down
Loading