diff --git a/apps/web/src/pages/PluginManager.tsx b/apps/web/src/pages/PluginManager.tsx
index 5c25bcb..0cdd2fc 100644
--- a/apps/web/src/pages/PluginManager.tsx
+++ b/apps/web/src/pages/PluginManager.tsx
@@ -8,6 +8,7 @@ import { PLUGIN_INSTANCE_MODES, type PluginInstanceMode, type PluginView } from
import { FormError } from '../components/forms';
import { apiDelete, apiGet, apiPatch, apiUploadFile } from '../lib/api';
+import { IconButton } from '../components/IconButton';
/**
* Site Admin plugin administration (issue #72): the installed-plugin list with
@@ -190,18 +191,20 @@ export function PluginManager(): React.JSX.Element {
{t('admin.preview')}
- uninstall.mutate(plugin.id)}
>
-
+
))}
diff --git a/apps/web/src/styles/base.css b/apps/web/src/styles/base.css
index 35b6990..19e0641 100644
--- a/apps/web/src/styles/base.css
+++ b/apps/web/src/styles/base.css
@@ -3811,19 +3811,15 @@ ul[data-type='task_list'] li p:last-of-type {
position: relative;
}
-.notifications-bell__button {
- background: none;
- border: none;
- cursor: pointer;
- font-size: 1.1rem;
- position: relative;
- padding: var(--space-1);
-}
-
+/* The bell is an `.icon-button` like search and the theme toggle (issue
+ #300) — it used to carry its own rules, which lacked the flex centring
+ and the icon size, so the glyph sat on the text baseline and rendered at
+ lucide's 24px default. The badge stays positioned against that button;
+ `.icon-button` is `position: relative` for exactly this. */
.notifications-bell__badge {
position: absolute;
- top: -2px;
- right: -4px;
+ top: -1px;
+ right: -3px;
background: var(--color-danger);
color: var(--color-danger-contrast);
border-radius: 999px;
diff --git a/apps/web/src/watches/WatchesSection.tsx b/apps/web/src/watches/WatchesSection.tsx
index 04565b1..fe56061 100644
--- a/apps/web/src/watches/WatchesSection.tsx
+++ b/apps/web/src/watches/WatchesSection.tsx
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { apiDelete, apiGet } from '../lib/api';
+import { IconButton } from '../components/IconButton';
/**
* The account's watch list (issue #93): everything the user follows, with
@@ -42,15 +43,13 @@ export function WatchesSection(): React.JSX.Element {
{watch.name}
{t(`settings.types.${watch.targetType}`)}
-
void unwatch(watch.targetType, watch.targetId)}
>
-
+
))}
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 77c0a0f..4779c85 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -52,6 +52,31 @@ export default tseslint.config(
},
},
},
+ {
+ // Icon-only controls go through the shared components (issue #300).
+ // Gluing `icon-button` onto a raw element copies the looks but skips the
+ // contract that guarantees an accessible name — that is how the
+ // notification bell drifted into its own size and focus ring.
+ files: ['apps/web/src/**/*.tsx'],
+ ignores: ['apps/web/src/components/IconButton.tsx'],
+ rules: {
+ 'no-restricted-syntax': [
+ 'error',
+ {
+ selector:
+ 'JSXOpeningElement[name.name=/^(button|a|Link)$/] > JSXAttribute[name.name="className"] > Literal[value=/(^|\\s)icon-button(\\s|$)/]',
+ message:
+ 'Use
(or for navigation) from components/IconButton instead of putting the icon-button class on a raw element — the component enforces the accessible name.',
+ },
+ {
+ selector:
+ 'JSXOpeningElement[name.name=/^(button|a|Link)$/] > JSXAttribute[name.name="className"] > JSXExpressionContainer > TemplateLiteral > TemplateElement[value.raw=/(^|\\s)icon-button(\\s|$)/]',
+ message:
+ 'Use (or for navigation) from components/IconButton instead of putting the icon-button class on a raw element — the component enforces the accessible name.',
+ },
+ ],
+ },
+ },
{
rules: {
// Unused values are usually bugs; underscore-prefix marks intentional ones.
diff --git a/packages/shared/i18n/de/notifications.json b/packages/shared/i18n/de/notifications.json
index ef9614c..e2e68b3 100644
--- a/packages/shared/i18n/de/notifications.json
+++ b/packages/shared/i18n/de/notifications.json
@@ -1,5 +1,6 @@
{
"title": "Benachrichtigungen",
+ "titleUnread": "Benachrichtigungen, {{count}} ungelesen",
"markAllRead": "Alle als gelesen markieren",
"empty": "Noch keine Benachrichtigungen.",
"someone": "Jemand",
diff --git a/packages/shared/i18n/en/notifications.json b/packages/shared/i18n/en/notifications.json
index 88799e9..3d874b8 100644
--- a/packages/shared/i18n/en/notifications.json
+++ b/packages/shared/i18n/en/notifications.json
@@ -1,5 +1,6 @@
{
"title": "Notifications",
+ "titleUnread": "Notifications, {{count}} unread",
"markAllRead": "Mark all read",
"empty": "No notifications yet.",
"someone": "Someone",