Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
20 changes: 20 additions & 0 deletions .changeset/open-llamas-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"headertweaker": minor
---

## New Features

- **URL filter bar** — Filter the header list by URL scope to quickly find scoped headers
- **Global tab wizard** — Batch-target multiple headers to a specific URL via a guided flow
- **Drag & drop URL targeting** — Reassign a header's URL target by dragging it to a new scope
- **i18n groundwork** — All UI copy is now externalized to `src/i18n` (English only for now, contributions welcome!)

## Improvements

- Consistently match scoped headers against domains, subdomains, paths, and wildcards
- Group headers on the URL-specific tab by their exact scoped URL
- Reuse previously used URLs via the new `Select` component when targeting a header
- Focus the header key input automatically after creating a new header
- Clearer empty states for the header list
- Prominent alert on the Global tab clarifying that those headers apply everywhere
- New `Toast` component — saving header info now gives instant feedback
33 changes: 33 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
groups:
react:
patterns:
- react
- react-dom
- "@types/react"
- "@types/react-dom"
dev-dependencies:
dependency-type: development
update-types:
- minor
- patch
production-dependencies:
dependency-type: production
update-types:
- minor
- patch

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
actions:
patterns:
- "*"
21 changes: 11 additions & 10 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
git fetch origin ${{ github.base_ref }}:${{ github.base_ref }}

- name: Check for changeset
if: github.actor != 'github-actions[bot]'
if: github.actor != 'github-actions[bot]' && github.actor != 'dependabot[bot]'
shell: bash
run: |
CHANGES=$(git diff --name-status origin/${{ github.base_ref }}...HEAD \
Expand All @@ -33,19 +33,20 @@ jobs:
echo "$CHANGES"
fi

- name: Use Node.js
- name: Install pnpm
uses: pnpm/action-setup@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install pnpm
run: |
corepack enable
corepack prepare pnpm@latest --activate
pnpm --version
node-version-file: .nvmrc
cache: pnpm

- name: Install dependencies
run: pnpm install
run: pnpm install --frozen-lockfile

- name: Audit
run: pnpm audit --audit-level=high --prod

- name: Lint
run: pnpm lint
Expand Down
51 changes: 47 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,66 @@ pnpm format-n-lint:fix # Auto-fix lint and format issues
- **`src/background.ts`** — Extension background script (service worker)
- **`src/headertweaker.tsx`** — Main UI entry point
- **`src/components/`** — Feature components, each co-located with its `.module.scss`
- **`src/helpers/`** — Pure utility functions
- **`src/helpers/`** — Pure utility functions; every function must live in its own dedicated helper file
- **`src/contexts/`** — React context providers
- **`src/interfaces/`** — Shared TypeScript types (`Header`, `Status`)
- **`src/i18n/`** — Localization: `config.ts` initializes i18next, `locales/en-US.json` holds the nested translation keys
- **`public/manifest.json`** — Firefox manifest (base); `manifests/chrome.json` for Chrome overrides
- The Vite `sync-manifest` plugin writes the current `package.json` version into the built manifests at bundle time

## Conventions

- Use **path aliases** for cross-directory imports (never relative `../../`):
`@components/*`, `@helpers/*`, `@contexts/*`, `@interfaces/*`, `@constants/*`, `@styles/*`
- CSS: **SCSS modules** (`.module.scss`) co-located with each component
`@components/*`, `@helpers/*`, `@contexts/*`, `@interfaces/*`, `@constants/*`, `@styles/*`, `@i18n/*`
- **Localization**: never hardcode user-facing text (labels, placeholders, aria-labels, messages). Add a key to the matching group in `src/i18n/locales/en-US.json` and render it with `const { t } = useTranslation()` from `react-i18next`. Interpolate values with `t('group.key', { name })` matching `{{name}}` in the translation, and use i18next `_one` / `_other` suffixed keys with `{ count }` for plurals. Non-component code (helpers, constants) returns a `TranslationKey` instead of translated text, so the component can translate it.
- **SCSS styling**: Use SCSS modules (`.module.scss`) co-located with each component. Import design tokens using `@use '@styles/variables' as vars;` and reference tokens via `vars.$colors-*`, `vars.$spacing-core-*`, `vars.$border-radius-primary`, etc. Never hardcode colors or spacing values — always use design tokens from `src/styles/variables.scss`.
- Components use named arrow-function exports typed as `FC<Props>`.
- Always render text through the `Text` component (`@components/text/text`); never place raw strings in bare DOM elements such as `<span>` or `<p>`.
- Declare all TypeScript types with `type`; do not use `interface`. Export a component prop type when it is shared.
- Constants belong in `src/constants/`, never inline in components. Define sets of options as a `SCREAMING_SNAKE_CASE` object with `as const` and derive the type from it:

```ts
export const SCOPES = {
ALL: 'all',
NO_SCOPE: 'no-scope',
} as const;

export type Scope = (typeof SCOPES)[keyof typeof SCOPES];
```

- Type DOM-wrapping components with `ComponentPropsWithoutRef<'element'>` and wrappers with `PropsWithChildren<Props>`.
- Do not use `as any` or `as unknown as T` to silence TypeScript errors; narrow values to the required type instead.
- Import `clsx` as `classnames`: `import classnames from 'clsx'`. Compose class names as `classnames(css.header, className)`.
- Keep compound-component subcomponents in the parent component's `elements/` folder. The root component module must re-export each element directly; do not add an `elements/index.ts` barrel. For example, `modal.tsx` should contain `export { ModalHeader } from './elements/modal-header';`.
- **Composition over configuration**: pass content as `children` rather than through content props (`label`, `icon`, `text`). `Button`, `IconButton`, and `Text` follow this.
- **Compound components**: Build multi-part components using composition, not props. Keep subcomponents in an `elements/` folder and re-export them from the parent module (no `elements/index.ts` barrel). The parent manages state and shares it with its subcomponents through a context. This pattern is used for `Modal` and `Steps`.

```tsx
// src/components/steps/steps.tsx
<StepsProvider value={contextValue}>
<Steps.StepIndicators />
<Steps.Step title="...">Content</Steps.Step>
<Steps.StepNavigation />
</StepsProvider>
```

- **Contexts**: Create all contexts in `src/contexts/` with the naming convention `*.context.tsx`. A context file exports the context value type, the context, a dedicated provider component, and a `use*Context` consumer hook that throws when used outside its provider. Always consume a context through its hook — never `useContext` directly.

```tsx
// src/contexts/steps.context.tsx
export type StepsContextValue = { /* ... */ };
export const StepsContext = createContext<StepsContextValue | undefined>(undefined);
export const StepsProvider: FC<PropsWithChildren<{ value: StepsContextValue }>> = ({ value, children }) => (
<StepsContext.Provider value={value}>{children}</StepsContext.Provider>
);
export const useStepsContext = (): StepsContextValue => {
const context = useContext(StepsContext);

if (!context) throw new Error('useStepsContext must be used within a StepsProvider');

return context;
};
```

- Linting/formatting: **Biome** for JS/TS/JSON, **Stylelint** for SCSS — both run in CI

## Testing
Expand Down
18 changes: 11 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@
},
"packageManager": "pnpm@10.17.1",
"engines": {
"node": ">=20",
"node": ">=24",
"pnpm": ">=10"
},
"dependencies": {
"@heroicons/react": "2.2.0",
"clsx": "2.1.1",
"i18next": "26.3.6",
"react": "18.2.0",
"react-compiler-runtime": "^1.0.0",
"react-compiler-runtime": "1.0.0",
"react-dom": "18.2.0",
"react-i18next": "17.0.11",
"tldts": "7.4.10",
"uuid": "11.1.0"
},
"devDependencies": {
Expand All @@ -34,7 +37,9 @@
"@types/react-dom": "19.1.9",
"@vitejs/plugin-react": "5.0.2",
"babel-plugin-react-compiler": "^1.0.0",
"concurrently": "9.2.1",
"husky": "9.1.7",
"jsdom": "24.1.3",
"rimraf": "6.0.1",
"sass": "1.70.0",
"stylelint": "16.23.1",
Expand All @@ -44,15 +49,14 @@
"typescript": "5.9.2",
"vite": "5.0.0",
"vitest": "1.6.1",
"jsdom": "24.1.3",
"web-ext": "8.9.0"
},
"scripts": {
"dev:firefox": "BROWSER=firefox vite build && web-ext run --source-dir=dist/firefox --watch-files=dist/firefox/*",
"dev:firefox:console": "BROWSER=firefox vite build && web-ext run --source-dir=dist/firefox --watch-files=dist/firefox/* --browser-console",
"dev:firefox": "BROWSER=firefox vite build && concurrently --kill-others-on-fail \"BROWSER=firefox vite build --watch\" \"web-ext run --source-dir=dist/firefox\"",
"dev:firefox:console": "BROWSER=firefox vite build && concurrently --kill-others-on-fail \"BROWSER=firefox vite build --watch\" \"web-ext run --source-dir=dist/firefox --browser-console\"",
"build:firefox": "BROWSER=firefox vite build",
"dev:chrome": "BROWSER=chrome vite build && web-ext run --target=chromium --source-dir=dist/chrome --watch-files=dist/chrome/*",
"dev:chrome:console": "BROWSER=chrome vite build && web-ext run --target=chromium --source-dir=dist/chrome --watch-files=dist/chrome/* --browser-console",
"dev:chrome": "BROWSER=chrome vite build && concurrently --kill-others-on-fail \"BROWSER=chrome vite build --watch\" \"web-ext run --target=chromium --source-dir=dist/chrome\"",
"dev:chrome:console": "BROWSER=chrome vite build && concurrently --kill-others-on-fail \"BROWSER=chrome vite build --watch\" \"web-ext run --target=chromium --source-dir=dist/chrome --browser-console\"",
"build:chrome": "BROWSER=chrome vite build",
"build:all": "pnpm run build:firefox && pnpm run build:chrome",
"change": "changeset",
Expand Down
Loading