#300: route icon-only controls through IconButton/IconLink #308
10
CLAUDE.md
10
CLAUDE.md
@ -27,13 +27,3 @@ AA) — nicht nachträglich. Kurzfassung; Details und Begründung in
|
||||
machen — betroffene Specs mit anpassen (scopen), nicht das Label opfern.
|
||||
|
||||
Verstöße gelten in Review und Abnahme als Funktionsfehler.
|
||||
|
||||
## graphify
|
||||
|
||||
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
|
||||
|
||||
Rules:
|
||||
- For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
|
||||
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
|
||||
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
|
||||
- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
|
||||
|
||||
@ -16,6 +16,7 @@ import { ApiError, apiGet } from '../lib/api';
|
||||
import { usePondLabels } from '../labels/use-pond-labels';
|
||||
import { usePondMembers } from '../members/use-pond-members';
|
||||
import { useAccessRules, useAccessRuleMutations } from './use-access-rules';
|
||||
import { IconButton } from '../components/IconButton';
|
||||
|
||||
type ScopedType = 'label' | 'page';
|
||||
|
||||
@ -238,15 +239,14 @@ export function AccessRulesManager({ pondId }: { pondId: string }): React.JSX.El
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
className="icon-button rule-add__submit"
|
||||
<IconButton
|
||||
className="rule-add__submit"
|
||||
type="submit"
|
||||
disabled={!canSubmit}
|
||||
aria-label={t('add.submit')}
|
||||
title={t('add.submit')}
|
||||
label={t('add.submit')}
|
||||
>
|
||||
<Plus aria-hidden />
|
||||
</button>
|
||||
</IconButton>
|
||||
</form>
|
||||
|
||||
{rules.length === 0 ? (
|
||||
@ -260,15 +260,13 @@ export function AccessRulesManager({ pondId }: { pondId: string }): React.JSX.El
|
||||
{group.rules.map((rule) => (
|
||||
<li key={rule.id} className="rule-item">
|
||||
<span className="rule-sentence">{ruleSentence(rule, t)}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button rule-remove"
|
||||
aria-label={t('remove')}
|
||||
title={t('remove')}
|
||||
<IconButton
|
||||
className="rule-remove"
|
||||
label={t('remove')}
|
||||
onClick={() => void mutations.remove(rule.id)}
|
||||
>
|
||||
<Trash2 aria-hidden />
|
||||
</button>
|
||||
</IconButton>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@ -1,4 +1,14 @@
|
||||
import type { ButtonHTMLAttributes } from 'react';
|
||||
import { Link, type LinkProps } from 'react-router-dom';
|
||||
|
||||
/** The shared class list behind both controls (issue #300): one place decides
|
||||
* what an icon-only control looks like, so the box, the icon size, the hover
|
||||
* and the focus ring cannot drift apart between a button and a link. */
|
||||
function iconClasses(active: boolean | undefined, className: string | undefined): string {
|
||||
return ['icon-button', active ? 'icon-button--active' : '', className ?? '']
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
/** Localized accessible name; also shown as the hover tooltip. */
|
||||
@ -15,12 +25,42 @@ export function IconButton({
|
||||
children,
|
||||
...rest
|
||||
}: IconButtonProps): React.JSX.Element {
|
||||
const classes = ['icon-button', active ? 'icon-button--active' : '', className ?? '']
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
return (
|
||||
<button type="button" className={classes} aria-label={label} title={label} {...rest}>
|
||||
<button
|
||||
type="button"
|
||||
className={iconClasses(active, className)}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
interface IconLinkProps extends LinkProps {
|
||||
/** Localized accessible name; also shown as the hover tooltip. */
|
||||
label: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The navigating twin of {@link IconButton} (issue #300). An icon-only control
|
||||
* that goes somewhere is a link, not a button — but it has to look and focus
|
||||
* exactly like one, which is why both share {@link iconClasses}. Without it the
|
||||
* three navigating icons (pond settings, graph, trash) stayed the one group
|
||||
* that had to glue the class on by hand.
|
||||
*/
|
||||
export function IconLink({
|
||||
label,
|
||||
active,
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
}: IconLinkProps): React.JSX.Element {
|
||||
return (
|
||||
<Link className={iconClasses(active, className)} aria-label={label} title={label} {...rest}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import { FormError } from '../components/forms';
|
||||
import { apiDelete, apiGet } from '../lib/api';
|
||||
|
||||
import { fileGlyph, formatBytes, mediaUrl } from './file-format';
|
||||
import { IconButton } from '../components/IconButton';
|
||||
|
||||
/**
|
||||
* Pond-wide file manager (issue #61), Pond-Admin-gated in the api. Lists every
|
||||
@ -69,15 +70,13 @@ export function PondFileManager({ pondId }: { pondId: string }): React.JSX.Eleme
|
||||
{formatBytes(item.sizeBytes)} · {item.uploaderName} ·{' '}
|
||||
{item.pageTitle ?? <span className="attachments-item__orphan">{t('orphan')}</span>}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button attachments-item__delete"
|
||||
aria-label={t('delete')}
|
||||
title={t('delete')}
|
||||
<IconButton
|
||||
className="attachments-item__delete"
|
||||
label={t('delete')}
|
||||
onClick={() => void remove(item.id)}
|
||||
>
|
||||
<Trash2 aria-hidden />
|
||||
</button>
|
||||
</IconButton>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@ -4,6 +4,7 @@ import { useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useImport } from './use-import';
|
||||
import { IconButton } from '../components/IconButton';
|
||||
|
||||
interface ImportControlProps {
|
||||
pondId: string;
|
||||
@ -27,15 +28,13 @@ export function ImportControl({ pondId, pondSlug }: ImportControlProps): React.J
|
||||
|
||||
return (
|
||||
<div className="sidebar__import">
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button sidebar__import-action"
|
||||
title={t('action')}
|
||||
aria-label={t('action')}
|
||||
<IconButton
|
||||
className="sidebar__import-action"
|
||||
label={t('action')}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>
|
||||
<Import aria-hidden />
|
||||
</button>
|
||||
</IconButton>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
|
||||
@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ApiError } from '../lib/api';
|
||||
import { useLabelMutations, usePondLabels } from './use-pond-labels';
|
||||
import { IconButton } from '../components/IconButton';
|
||||
|
||||
/** Turns an ApiError code into a translated message; other errors are generic. */
|
||||
function useErrorText(): (error: unknown) => string {
|
||||
@ -72,15 +73,9 @@ export function LabelManager({ pondId }: { pondId: string }): React.JSX.Element
|
||||
placeholder={t('settings.newRootPlaceholder')}
|
||||
aria-label={t('settings.newRootPlaceholder')}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="icon-button"
|
||||
disabled={!newRoot.trim()}
|
||||
aria-label={t('settings.add')}
|
||||
title={t('settings.add')}
|
||||
>
|
||||
<IconButton type="submit" disabled={!newRoot.trim()} label={t('settings.add')}>
|
||||
<Plus aria-hidden />
|
||||
</button>
|
||||
</IconButton>
|
||||
</form>
|
||||
|
||||
{error && (
|
||||
@ -250,15 +245,13 @@ function LabelNode({
|
||||
>
|
||||
{t('settings.addChild')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button label-node__delete"
|
||||
aria-label={t('settings.delete')}
|
||||
title={t('settings.delete')}
|
||||
<IconButton
|
||||
className="label-node__delete"
|
||||
label={t('settings.delete')}
|
||||
onClick={() => void remove()}
|
||||
>
|
||||
<Trash2 aria-hidden />
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -278,15 +271,9 @@ function LabelNode({
|
||||
aria-label={t('settings.addChild')}
|
||||
onChange={(event) => setChildName(event.target.value)}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="icon-button"
|
||||
disabled={!childName.trim()}
|
||||
aria-label={t('settings.add')}
|
||||
title={t('settings.add')}
|
||||
>
|
||||
<IconButton type="submit" disabled={!childName.trim()} label={t('settings.add')}>
|
||||
<Plus aria-hidden />
|
||||
</button>
|
||||
</IconButton>
|
||||
<button type="button" className="linklike" onClick={() => setAddingChild(false)}>
|
||||
{t('settings.cancel')}
|
||||
</button>
|
||||
|
||||
@ -9,6 +9,7 @@ import { Link } from 'react-router-dom';
|
||||
import { useAuth } from '../auth/auth-context';
|
||||
import { ApiError, apiDelete, apiGet, apiPost } from '../lib/api';
|
||||
import { useLabelMutations, usePondLabels } from './use-pond-labels';
|
||||
import { IconButton } from '../components/IconButton';
|
||||
|
||||
/**
|
||||
* Page label picker (issue #44): a searchable, hierarchy-aware multi-select of
|
||||
@ -161,15 +162,14 @@ export function LabelPicker({
|
||||
aria-label={t('settings.newRootPlaceholder')}
|
||||
onChange={(event) => setNewName(event.target.value)}
|
||||
/>
|
||||
<button
|
||||
<IconButton
|
||||
className="label-picker__create-submit"
|
||||
type="submit"
|
||||
className="icon-button label-picker__create-submit"
|
||||
disabled={!newName.trim()}
|
||||
aria-label={t('settings.add')}
|
||||
title={t('settings.add')}
|
||||
label={t('settings.add')}
|
||||
>
|
||||
<Plus aria-hidden />
|
||||
</button>
|
||||
</IconButton>
|
||||
</form>
|
||||
{createError && (
|
||||
<p className="label-picker__error" role="alert">
|
||||
|
||||
@ -24,6 +24,7 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '../auth/auth-context';
|
||||
import { FormError } from '../components/forms';
|
||||
import { IconButton, IconLink } from '../components/IconButton';
|
||||
import { usePageFavorites } from '../favorites/use-favorites';
|
||||
import { ImportControl } from '../import/ImportControl';
|
||||
import { LabelChips } from '../labels/LabelChips';
|
||||
@ -395,34 +396,30 @@ function SidebarContent({
|
||||
bottom (#124): graph, new page, import, trash — hover hints via
|
||||
title. The graph is for every member (#112); trash is owner-only. */}
|
||||
<div className="sidebar__footer">
|
||||
<Link
|
||||
<IconLink
|
||||
to={`/p/${pondSlug}/graph`}
|
||||
className="icon-button sidebar__graph-link"
|
||||
title={t('graph:link')}
|
||||
aria-label={t('graph:link')}
|
||||
className="sidebar__graph-link"
|
||||
label={t('graph:link')}
|
||||
>
|
||||
<Waypoints aria-hidden />
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button sidebar__new-page"
|
||||
title={t('layout.sidebar.newPageHint')}
|
||||
aria-label={t('layout.sidebar.newPageHint')}
|
||||
</IconLink>
|
||||
<IconButton
|
||||
className="sidebar__new-page"
|
||||
label={t('layout.sidebar.newPageHint')}
|
||||
aria-expanded={creating}
|
||||
onClick={() => setCreating(!creating)}
|
||||
>
|
||||
<FilePlus aria-hidden />
|
||||
</button>
|
||||
</IconButton>
|
||||
<ImportControl pondId={pond.id} pondSlug={pondSlug} />
|
||||
{isOwner && (
|
||||
<Link
|
||||
<IconLink
|
||||
to={`/p/${pondSlug}/trash`}
|
||||
className="icon-button sidebar__trash-link"
|
||||
title={t('editor:trash.showLink')}
|
||||
aria-label={t('editor:trash.showLink')}
|
||||
className="sidebar__trash-link"
|
||||
label={t('editor:trash.showLink')}
|
||||
>
|
||||
<Trash2 aria-hidden />
|
||||
</Link>
|
||||
</IconLink>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '../auth/auth-context';
|
||||
import { IconButton } from '../components/IconButton';
|
||||
import { IconButton, IconLink } from '../components/IconButton';
|
||||
import { apiGet } from '../lib/api';
|
||||
import { isTypingTarget } from '../lib/keyboard';
|
||||
import { useDismissable } from '../lib/use-dismissable';
|
||||
@ -94,28 +94,25 @@ export function TopBar({ sidebarCollapsed, onToggleSidebar }: TopBarProps): Reac
|
||||
|
||||
return (
|
||||
<header className="topbar">
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
<IconButton
|
||||
onClick={onToggleSidebar}
|
||||
aria-expanded={!sidebarCollapsed}
|
||||
aria-label={sidebarCollapsed ? t('layout.sidebar.expand') : t('layout.sidebar.collapse')}
|
||||
label={sidebarCollapsed ? t('layout.sidebar.expand') : t('layout.sidebar.collapse')}
|
||||
>
|
||||
<Menu aria-hidden />
|
||||
</button>
|
||||
</IconButton>
|
||||
<Link to="/" className="topbar__brand">
|
||||
Dorfteich
|
||||
</Link>
|
||||
{user && <PondSwitcher />}
|
||||
{isPondOwner && pondSlug && (
|
||||
<Link
|
||||
<IconLink
|
||||
to={`/p/${pondSlug}/settings`}
|
||||
className="icon-button topbar__pond-settings"
|
||||
aria-label={t('labels:link')}
|
||||
title={t('labels:link')}
|
||||
className="topbar__pond-settings"
|
||||
label={t('labels:link')}
|
||||
>
|
||||
<Settings aria-hidden />
|
||||
</Link>
|
||||
</IconLink>
|
||||
)}
|
||||
<span className="topbar__spacer" />
|
||||
{/* Page-scoped slots, rendered only for signed-in users: live presence
|
||||
|
||||
@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { ApiError } from '../lib/api';
|
||||
import { useMemberMutations, usePondMembers } from './use-pond-members';
|
||||
import { IconButton } from '../components/IconButton';
|
||||
|
||||
/** Turns an ApiError code into a translated message; other errors are generic. */
|
||||
function useErrorText(): (error: unknown) => string {
|
||||
@ -140,15 +141,14 @@ export function MemberManager({ pondId }: { pondId: string }): React.JSX.Element
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<button
|
||||
className="icon-button member-add__submit"
|
||||
<IconButton
|
||||
className="member-add__submit"
|
||||
type="submit"
|
||||
disabled={addBlockedByQuota}
|
||||
aria-label={t('add.submit')}
|
||||
title={t('add.submit')}
|
||||
label={t('add.submit')}
|
||||
>
|
||||
<UserPlus aria-hidden />
|
||||
</button>
|
||||
</IconButton>
|
||||
{addBlockedByQuota && (
|
||||
<p className="member-add__quota-full" role="note">
|
||||
{t('add.quotaFull', { role: roleLabel(addRole) })}
|
||||
@ -238,15 +238,9 @@ function MemberRow({
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button member-row__remove"
|
||||
aria-label={t('actions.remove')}
|
||||
title={t('actions.remove')}
|
||||
onClick={onRemove}
|
||||
>
|
||||
<IconButton className="member-row__remove" label={t('actions.remove')} onClick={onRemove}>
|
||||
<UserMinus aria-hidden />
|
||||
</button>
|
||||
</IconButton>
|
||||
</span>
|
||||
) : (
|
||||
<span className="member-row__role-label">{roleLabel(member.role)}</span>
|
||||
|
||||
@ -5,6 +5,7 @@ import { useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { IconButton } from '../components/IconButton';
|
||||
import { apiGet, apiPost } from '../lib/api';
|
||||
import { useDismissable } from '../lib/use-dismissable';
|
||||
|
||||
@ -48,18 +49,22 @@ export function NotificationsBell(): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="notifications-bell" ref={bellRef}>
|
||||
<button
|
||||
type="button"
|
||||
className="notifications-bell__button"
|
||||
<IconButton
|
||||
// The unread count belongs in the accessible name (issue #300): the
|
||||
// badge sits inside the control, so aria-label would otherwise hide it
|
||||
// and a screen reader announced only "Notifications", never how many.
|
||||
label={unread > 0 ? t('titleUnread', { count: unread }) : t('title')}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={open}
|
||||
aria-label={t('title')}
|
||||
title={t('title')}
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
>
|
||||
<Bell aria-hidden />
|
||||
{unread > 0 && <span className="notifications-bell__badge">{unread}</span>}
|
||||
</button>
|
||||
{unread > 0 && (
|
||||
<span className="notifications-bell__badge" aria-hidden>
|
||||
{unread}
|
||||
</span>
|
||||
)}
|
||||
</IconButton>
|
||||
{open && (
|
||||
<div className="notifications-bell__dropdown" role="menu">
|
||||
<div className="notifications-bell__header">
|
||||
|
||||
@ -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 {
|
||||
<Link className="plugin-manager__preview" to={`/admin/plugins/${plugin.id}/preview`}>
|
||||
{t('admin.preview')}
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button plugin-manager__uninstall"
|
||||
<IconButton
|
||||
className="plugin-manager__uninstall"
|
||||
disabled={plugin.mode === 'required'}
|
||||
aria-label={t('admin.uninstall')}
|
||||
label={t('admin.uninstall')}
|
||||
// A required plugin explains in the tooltip why it cannot be
|
||||
// removed, so the title deliberately differs from the name;
|
||||
// IconButton spreads rest last, which lets it through.
|
||||
title={
|
||||
plugin.mode === 'required' ? t('admin.requiredLocked') : t('admin.uninstall')
|
||||
}
|
||||
onClick={() => uninstall.mutate(plugin.id)}
|
||||
>
|
||||
<Trash2 aria-hidden />
|
||||
</button>
|
||||
</IconButton>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}
|
||||
</Link>
|
||||
<span className="watches-list__type">{t(`settings.types.${watch.targetType}`)}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button watches-list__unwatch"
|
||||
aria-label={t('settings.unwatch')}
|
||||
title={t('settings.unwatch')}
|
||||
<IconButton
|
||||
className="watches-list__unwatch"
|
||||
label={t('settings.unwatch')}
|
||||
onClick={() => void unwatch(watch.targetType, watch.targetId)}
|
||||
>
|
||||
<EyeOff aria-hidden />
|
||||
</button>
|
||||
</IconButton>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@ -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 <IconButton> (or <IconLink> 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 <IconButton> (or <IconLink> 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.
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"title": "Benachrichtigungen",
|
||||
"titleUnread": "Benachrichtigungen, {{count}} ungelesen",
|
||||
"markAllRead": "Alle als gelesen markieren",
|
||||
"empty": "Noch keine Benachrichtigungen.",
|
||||
"someone": "Jemand",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"title": "Notifications",
|
||||
"titleUnread": "Notifications, {{count}} unread",
|
||||
"markAllRead": "Mark all read",
|
||||
"empty": "No notifications yet.",
|
||||
"someone": "Someone",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user