From 161921e5960cbd6c54970156d9c1ab26bbbf6d30 Mon Sep 17 00:00:00 2001 From: AbramovichCp Date: Wed, 29 Apr 2026 19:23:52 +0200 Subject: [PATCH 1/6] docs: update README with store link, planned features, and deployment guide --- README.md | 79 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 9270e42..650af38 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A Chrome extension for inspecting, editing, saving, and replaying URL query parameters on the active tab. Built for developers and QA engineers. +**[Install from the Chrome Web Store](https://chromewebstore.google.com/detail/querycraft/gokkgpehjnmdcnpknmkefbjnhpnnhbih)** + ![QueryCraft popup](public/icons/icon128.png) ## Features @@ -15,6 +17,10 @@ A Chrome extension for inspecting, editing, saving, and replaying URL query para - **Full keyboard navigation** — every interactive element is reachable via Tab, with visible focus rings and sensible shortcuts. - **Screen-reader friendly** — semantic HTML, ARIA live region for status announcements, role-correct toggle/switch/dialog semantics. +## Planned features + +- **Company-specific query filter rules** — support for complex, configurable filters tailored to the needs of individual companies. The configuration delivery mechanism is still being explored (inline config, file import, or remote fetch). Currently available as a **beta** on the [`DLS-beta`](../../tree/DLS-beta) branch — build it yourself to try it out. + ## Keyboard shortcuts | Shortcut | Action | @@ -91,44 +97,43 @@ npm run lint # ESLint with react + jsx-a11y rules npm run test # Vitest (jsdom, with chrome.* mocks preconfigured) ``` -## Project structure +## Deployment -``` -querycraft/ -├── manifest.json -├── vite.config.ts -├── tsconfig.json -├── src/ -│ ├── popup/ # React entry: App.tsx, main.tsx, index.html -│ ├── background/ # Service worker (stateless) -│ ├── components/ # One folder per component (.tsx + .module.css + index.ts) -│ │ ├── Header/ -│ │ ├── UrlPreview/ -│ │ ├── ParamList/ -│ │ ├── ParamRow/ -│ │ ├── ParamKeyInput/ -│ │ ├── ParamValueInput/ -│ │ ├── BooleanToggle/ -│ │ ├── AddParamRow/ -│ │ ├── RemoveParamButton/ -│ │ ├── ActionBar/ -│ │ ├── Button/ -│ │ ├── IconButton/ -│ │ ├── SavedLinksDrawer/ -│ │ ├── GroupSelector/ -│ │ ├── ThemeToggle/ -│ │ ├── LiveRegion/ -│ │ ├── EmptyState/ -│ │ └── icons.tsx # Shared inline SVGs -│ ├── hooks/ # useActiveTabUrl, useClipboard, useTheme, -│ │ # useSavedLinks, useFocusTrap, useKeyboardShortcuts -│ ├── lib/ # Pure logic: urlParser, paramTypes, storage, tabs -│ ├── store/ # Zustand store -│ ├── types/ # Shared TypeScript types -│ ├── styles/ # tokens.css (light + dark), global.css -│ └── test/ # Vitest setup + chrome mocks -└── public/icons/ # 16 / 48 / 128 PNGs -``` +Releases are automated via GitHub Actions and publish directly to the Chrome Web Store. + +### How it works + +| Trigger | What happens | +| --- | --- | +| PR merged into `release` branch | Automatic deploy | +| Manual `workflow_dispatch` | Deploy with a chosen version bump | + +### Version bumping + +The workflow bumps `manifest.json` version automatically before building: + +| Type | Example | +| --- | --- | +| `patch` (default) | `1.0.0` → `1.0.1` | +| `minor` | `1.0.0` → `1.1.0` | +| `major` | `1.0.0` → `2.0.0` | + +### Required secrets + +The following secrets must be set in the repository settings: + +| Secret | Description | +| --- | --- | +| `EXTENSION_ID` | Chrome Web Store extension ID | +| `CLIENT_ID` | Google OAuth client ID | +| `CLIENT_SECRET` | Google OAuth client secret | +| `REFRESH_TOKEN` | Google OAuth refresh token | + +### Release flow + +1. Merge your feature branch into `main`. +2. Open a PR from `main` → `release`. +3. Merge the PR — the workflow bumps the version, builds, zips, uploads to the store, and tags the release automatically. ## Accessibility notes From 785b06923d2f116286ccc27e3108213888ba040d Mon Sep 17 00:00:00 2001 From: AbramovichCp Date: Fri, 22 May 2026 22:48:56 +0200 Subject: [PATCH 2/6] fix: read fresh store state in handleApply to prevent stale navUrl on Apply --- src/popup/App.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/popup/App.tsx b/src/popup/App.tsx index ab35cea..689c393 100644 --- a/src/popup/App.tsx +++ b/src/popup/App.tsx @@ -48,16 +48,22 @@ export function App() { /* ---------- Action handlers ---------- */ const handleApply = useCallback(async () => { - if (tabState.status !== 'ready' || !navUrl) return; + // Read fresh state at call time to avoid stale-closure issues: the user + // may click Apply immediately after adding a param, before React has + // re-rendered and updated the closure-captured navUrl / tabState. + const snap = useAppStore.getState(); + const currentTabState = snap.tabState; + const currentNavUrl = selectNavUrl(snap); + if (currentTabState.status !== 'ready' || !currentNavUrl) return; try { - await tabs.updateUrl(tabState.tabId, navUrl); + await tabs.updateUrl(currentTabState.tabId, currentNavUrl); announce('URL applied to the current tab.'); } catch (err) { announce( `Failed to apply URL: ${err instanceof Error ? err.message : 'unknown error'}`, ); } - }, [tabState, navUrl, announce]); + }, [announce]); const handleReset = useCallback(() => { resetStore(); From 56c90bfb633fdf073d9a5a0da4d5a218cfb632b5 Mon Sep 17 00:00:00 2001 From: AbramovichCp Date: Fri, 22 May 2026 22:56:49 +0200 Subject: [PATCH 3/6] feat: add copy-on-hover button to key and value fields --- src/components/ParamRow/ParamRow.module.css | 71 +++++++++++++++++ src/components/ParamRow/ParamRow.tsx | 86 ++++++++++++++++----- 2 files changed, 138 insertions(+), 19 deletions(-) diff --git a/src/components/ParamRow/ParamRow.module.css b/src/components/ParamRow/ParamRow.module.css index 015a381..a2f6e07 100644 --- a/src/components/ParamRow/ParamRow.module.css +++ b/src/components/ParamRow/ParamRow.module.css @@ -16,6 +16,77 @@ min-width: 0; } +/* ── Copy-on-hover ── */ + +.fieldWrap { + position: relative; +} + +.copyBtn { + position: absolute; + right: 5px; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-surface-raised); + color: var(--color-text-muted); + cursor: pointer; + opacity: 0; + pointer-events: none; + transition: opacity var(--transition-fast), color var(--transition-fast), background var(--transition-fast); +} + +.fieldWrap:hover .copyBtn { + opacity: 1; + pointer-events: auto; +} + +.copyBtn:hover { + color: var(--color-accent); + border-color: var(--color-border-strong); +} + +.copyBtn.copied, +.copyBtnInline.copied { + color: var(--color-success); +} + +/* Structured value: inline copy button (visible on row hover via structuredValue:hover) */ +.copyBtnInline { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + border: 1px solid color-mix(in srgb, var(--color-border) 60%, transparent); + border-radius: var(--radius-sm); + background: transparent; + color: var(--color-text-muted); + cursor: pointer; + opacity: 0; + pointer-events: none; + transition: opacity var(--transition-fast), color var(--transition-fast), background var(--transition-fast); +} + +.structuredValue:hover .copyBtnInline { + opacity: 1; + pointer-events: auto; +} + +.copyBtnInline:hover { + color: var(--color-accent); + border-color: var(--color-border-strong); +} + .removeCell { display: flex; justify-content: flex-end; diff --git a/src/components/ParamRow/ParamRow.tsx b/src/components/ParamRow/ParamRow.tsx index f4f0719..bd816a9 100644 --- a/src/components/ParamRow/ParamRow.tsx +++ b/src/components/ParamRow/ParamRow.tsx @@ -1,10 +1,11 @@ -import { useId } from 'react'; +import { useId, useState } from 'react'; import type { QueryParam } from '@/types'; import { parseStructuredValue, shortPreview } from '@/lib/structuredParam'; import { ParamKeyInput } from '../ParamKeyInput'; import { ParamValueInput } from '../ParamValueInput'; import { BooleanToggle } from '../BooleanToggle'; import { RemoveParamButton } from '../RemoveParamButton'; +import { IconCheck, IconCopy } from '../icons'; import styles from './ParamRow.module.css'; interface ParamRowProps { @@ -26,6 +27,15 @@ export function ParamRow({ }: ParamRowProps) { const keyId = useId(); const valueId = useId(); + const [copiedField, setCopiedField] = useState<'key' | 'value' | null>(null); + + function handleCopy(text: string, field: 'key' | 'value') { + if (!text) return; + void navigator.clipboard.writeText(text).then(() => { + setCopiedField(field); + window.setTimeout(() => setCopiedField(null), 1500); + }); + } const isBool = param.type === 'boolean'; const isStructured = param.type === 'structured'; @@ -45,12 +55,22 @@ export function ParamRow({ - onKeyChange(param.id, next)} - /> +
+ onKeyChange(param.id, next)} + /> + +
@@ -59,12 +79,22 @@ export function ParamRow({ - onToggleBoolean(param.id)} - /> +
+ onToggleBoolean(param.id)} + /> + +
) : isStructured ? (
@@ -85,18 +115,36 @@ export function ParamRow({ /> +
) : ( <> - onValueChange(param.id, next)} - /> +
+ onValueChange(param.id, next)} + /> + +
)}
From 4d5960c805f30be0ab80ab28f2b08faa06527785 Mon Sep 17 00:00:00 2001 From: AbramovichCp Date: Fri, 22 May 2026 23:07:09 +0200 Subject: [PATCH 4/6] feat: replace logo with search icon and add border to bookmark button --- src/components/Header/Header.tsx | 4 ++-- src/components/icons.tsx | 7 +++++++ src/popup/App.module.css | 4 ++++ src/popup/App.tsx | 1 + 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 6c8cb1d..cf6d1ec 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from 'react'; -import { IconLogo } from '../icons'; +import { IconSearch } from '../icons'; import styles from './Header.module.css'; interface HeaderProps { @@ -12,7 +12,7 @@ export function Header({ actions }: HeaderProps) {

QueryCraft

diff --git a/src/components/icons.tsx b/src/components/icons.tsx index ca5214b..7b975c6 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -95,3 +95,10 @@ export const IconLogo = () => ( ); + +export const IconSearch = () => ( + + + + +); diff --git a/src/popup/App.module.css b/src/popup/App.module.css index 0c74b8e..6fde7f0 100644 --- a/src/popup/App.module.css +++ b/src/popup/App.module.css @@ -18,3 +18,7 @@ .previewWrap { padding-top: var(--space-4); } + +.bookmarkBtn { + border-color: var(--color-border); +} diff --git a/src/popup/App.tsx b/src/popup/App.tsx index 689c393..38e75eb 100644 --- a/src/popup/App.tsx +++ b/src/popup/App.tsx @@ -142,6 +142,7 @@ export function App() { icon={} onClick={handleOpenDrawer} disabled={!currentUrl} + className={styles.bookmarkBtn} /> ); From 2c9c8efb61fdd34f50f8111174312bee6efc82bf Mon Sep 17 00:00:00 2001 From: AbramovichCp Date: Fri, 22 May 2026 23:18:12 +0200 Subject: [PATCH 5/6] feat: add saved link editing and unlock bookmark button on empty tabs --- .../SavedLinksDrawer.module.css | 16 ++++ .../SavedLinksDrawer/SavedLinksDrawer.tsx | 80 ++++++++++++++++--- src/components/icons.tsx | 7 ++ src/hooks/useSavedLinks.ts | 24 +++++- src/popup/App.tsx | 13 ++- 5 files changed, 124 insertions(+), 16 deletions(-) diff --git a/src/components/SavedLinksDrawer/SavedLinksDrawer.module.css b/src/components/SavedLinksDrawer/SavedLinksDrawer.module.css index ff46f93..f6aa26e 100644 --- a/src/components/SavedLinksDrawer/SavedLinksDrawer.module.css +++ b/src/components/SavedLinksDrawer/SavedLinksDrawer.module.css @@ -239,6 +239,22 @@ background: var(--color-surface); } +.linkActions { + display: flex; + align-items: center; + gap: var(--space-1); + flex-shrink: 0; + opacity: 0; + pointer-events: none; + transition: opacity var(--transition-fast); +} + +.linkRow:hover .linkActions, +.linkRow:focus-within .linkActions { + opacity: 1; + pointer-events: auto; +} + .linkButton { flex: 1; min-width: 0; diff --git a/src/components/SavedLinksDrawer/SavedLinksDrawer.tsx b/src/components/SavedLinksDrawer/SavedLinksDrawer.tsx index 5b78fb2..32a48fe 100644 --- a/src/components/SavedLinksDrawer/SavedLinksDrawer.tsx +++ b/src/components/SavedLinksDrawer/SavedLinksDrawer.tsx @@ -4,7 +4,7 @@ import { DEFAULT_GROUP_ID } from '@/lib/storage'; import { Button } from '../Button'; import { IconButton } from '../IconButton'; import { GroupSelector } from '../GroupSelector'; -import { IconChevronLeft, IconClose } from '../icons'; +import { IconChevronLeft, IconClose, IconEdit } from '../icons'; import { useFocusTrap } from '@/hooks/useFocusTrap'; import styles from './SavedLinksDrawer.module.css'; @@ -15,6 +15,7 @@ interface SavedLinksDrawerProps { links: SavedLink[]; groups: Group[]; onSave: (input: { url: string; label?: string; groupId: string }) => void; + onUpdateLink: (id: string, label: string | undefined, groupId: string) => void; onDeleteLink: (id: string) => void; onCreateGroup: (name: string) => Promise; onLoadLink: (url: string) => void; @@ -34,13 +35,15 @@ export function SavedLinksDrawer({ links, groups, onSave, + onUpdateLink, onDeleteLink, onCreateGroup, onLoadLink, }: SavedLinksDrawerProps) { - const [mode, setMode] = useState<'list' | 'save'>('list'); + const [mode, setMode] = useState<'list' | 'save' | 'edit'>('list'); const [label, setLabel] = useState(''); const [groupId, setGroupId] = useState(DEFAULT_GROUP_ID); + const [editingLink, setEditingLink] = useState(null); const trapRef = useFocusTrap(open, onClose); @@ -68,6 +71,27 @@ export function SavedLinksDrawer({ setMode('list'); } + function handleStartEdit(link: SavedLink) { + setEditingLink(link); + setLabel(link.label ?? ''); + setGroupId(link.groupId); + setMode('edit'); + } + + function handleUpdate() { + if (!editingLink) return; + onUpdateLink(editingLink.id, label.trim() || undefined, groupId); + setEditingLink(null); + setLabel(''); + setMode('list'); + } + + function handleCancelEdit() { + setEditingLink(null); + setLabel(''); + setMode('list'); + } + if (!open) return null; return ( @@ -82,16 +106,20 @@ export function SavedLinksDrawer({ >
- {mode === 'save' && ( + {(mode === 'save' || mode === 'edit') && ( } - onClick={() => setMode('list')} + onClick={mode === 'edit' ? handleCancelEdit : () => setMode('list')} size="sm" /> )}
} onClick={onClose} /> @@ -110,12 +138,26 @@ export function SavedLinksDrawer({ onCancel={() => setMode('list')} previewUrl={currentUrl} /> + ) : mode === 'edit' && editingLink ? ( + ) : ( setMode('save')} canSave={!!currentUrl} /> @@ -138,6 +180,7 @@ interface SaveFormProps { onSubmit: () => void; onCancel: () => void; previewUrl: string; + submitLabel?: string; } function SaveForm({ @@ -150,6 +193,7 @@ function SaveForm({ onSubmit, onCancel, previewUrl, + submitLabel = 'Save', }: SaveFormProps) { const labelId = 'sl-label'; return ( @@ -185,7 +229,7 @@ function SaveForm({
- } - size="sm" - tone="danger" - onClick={() => onDeleteLink(link.id)} - /> +
+ } + size="sm" + onClick={() => onStartEdit(link)} + /> + } + size="sm" + tone="danger" + onClick={() => onDeleteLink(link.id)} + /> +
))} diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 7b975c6..e5d0401 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -96,6 +96,13 @@ export const IconLogo = () => ( ); +export const IconEdit = () => ( + + + + +); + export const IconSearch = () => ( diff --git a/src/hooks/useSavedLinks.ts b/src/hooks/useSavedLinks.ts index 1198c1f..257a283 100644 --- a/src/hooks/useSavedLinks.ts +++ b/src/hooks/useSavedLinks.ts @@ -12,6 +12,7 @@ export function useSavedLinks(): { groups: Group[]; isLoading: boolean; saveLink: (input: { url: string; label?: string; groupId?: string }) => Promise; + updateLink: (id: string, label: string | undefined, groupId: string) => Promise; deleteLink: (id: string) => Promise; createGroup: (name: string) => Promise; renameGroup: (id: string, name: string) => Promise; @@ -97,5 +98,26 @@ export function useSavedLinks(): { [groups, links], ); - return { links, groups, isLoading, saveLink, deleteLink, createGroup, renameGroup, deleteGroup }; + const updateLink = useCallback( + async (id: string, label: string | undefined, groupId: string) => { + const next = links.map((l) => + l.id === id ? { ...l, label: label || undefined, groupId } : l, + ); + setLinks(next); + await storage.setSavedLinks(next); + }, + [links], + ); + + return { + links, + groups, + isLoading, + saveLink, + updateLink, + deleteLink, + createGroup, + renameGroup, + deleteGroup, + }; } diff --git a/src/popup/App.tsx b/src/popup/App.tsx index 38e75eb..80fd0e6 100644 --- a/src/popup/App.tsx +++ b/src/popup/App.tsx @@ -41,7 +41,7 @@ export function App() { const navUrl = useAppStore(selectNavUrl); // encoded, for Apply / Copy const { copied, copy } = useClipboard(); - const { links, groups, saveLink, deleteLink, createGroup } = useSavedLinks(); + const { links, groups, saveLink, updateLink, deleteLink, createGroup } = useSavedLinks(); const [drawerOpen, setDrawerOpen] = useState(false); @@ -79,6 +79,15 @@ export function App() { setDrawerOpen(true); }, []); + const handleUpdateLink = useCallback( + (id: string, label: string | undefined, groupId: string) => { + void updateLink(id, label, groupId).then(() => { + announce('URL updated.'); + }); + }, + [updateLink, announce], + ); + const handleSaveLink = useCallback( ({ url, label, groupId }: { url: string; label?: string; groupId: string }) => { void saveLink({ url, label, groupId }).then(() => { @@ -141,7 +150,6 @@ export function App() { aria-label="Open saved URLs" icon={} onClick={handleOpenDrawer} - disabled={!currentUrl} className={styles.bookmarkBtn} /> @@ -199,6 +207,7 @@ export function App() { links={links} groups={groups} onSave={handleSaveLink} + onUpdateLink={handleUpdateLink} onDeleteLink={(id) => void deleteLink(id)} onCreateGroup={createGroup} onLoadLink={handleLoadSavedLink} From b890e3dfebfe905af6aea831b72dedd0edbc0694 Mon Sep 17 00:00:00 2001 From: AbramovichCp Date: Fri, 22 May 2026 23:20:15 +0200 Subject: [PATCH 6/6] fix: drop tabs permission to remove browsing history access --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index c6bf53c..73e401d 100644 --- a/manifest.json +++ b/manifest.json @@ -15,7 +15,7 @@ "service_worker": "src/background/service-worker.ts", "type": "module" }, - "permissions": ["tabs", "activeTab", "storage"], + "permissions": ["activeTab", "storage"], "icons": { "16": "icons/icon16.png", "48": "icons/icon48.png",