From 58c19abfddadd6d180a91e8c745b74f110e5a26f Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Tue, 21 Jul 2026 14:35:16 +0200 Subject: [PATCH] =?UTF-8?q?#169:=20Nicht-Text-Inhalte=20=E2=80=94=20Task-C?= =?UTF-8?q?heckboxen,=20Wissensgraph?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task-Checkboxen tragen in beiden Renderpfaden einen Namen: docToHtml setzt aria-label aus dem Aufgabentext, die Editor-NodeView ebenso. Die NodeView rendert ihr Host-Element jetzt selbst als li (ReactNodeView- Renderer as/attrs) — TipTaps zusätzliches div-Host-Element zwischen ul und li brach die Listensemantik; der Wrapper flacht per display:contents ab, die #137-Pixel-Abstimmung bleibt erhalten (Selektor auf die neue Tiefe nachgeführt, Ausrichtung nachgemessen: 1px-Versatz unverändert). Der Wissensgraph-SVG bekommt ein beschreibendes aria-label inklusive Verweis auf die Backlinks als gleichwertige Listenform. Der Bild-Alt-Editor existierte bereits (Bild-Controls bei Auswahl) — kein Änderungsbedarf. Hinweis: gecachte Seiten übernehmen das Checkbox-Label wie bei jeder docToHtml-Änderung erst mit dem nächsten Persist ihrer Inhalte. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AGM8jo3hwoV9wsCVGfy8iq --- apps/web/e2e/content.spec.ts | 5 ++++- apps/web/e2e/sidebar.spec.ts | 5 ++++- apps/web/src/editor/nodes/task-item.tsx | 20 ++++++++++++++++--- apps/web/src/graph/ForceGraph.tsx | 3 +++ apps/web/src/styles/base.css | 10 +++++++--- packages/shared/i18n/de/graph.json | 3 ++- packages/shared/i18n/en/graph.json | 3 ++- .../shared/src/editor-schema/html.test.ts | 4 +++- packages/shared/src/editor-schema/html.ts | 4 +++- 9 files changed, 45 insertions(+), 12 deletions(-) diff --git a/apps/web/e2e/content.spec.ts b/apps/web/e2e/content.spec.ts index 2c59712..b2dfd93 100644 --- a/apps/web/e2e/content.spec.ts +++ b/apps/web/e2e/content.spec.ts @@ -62,7 +62,10 @@ test('page lifecycle: create via the sidebar, rename, appears in the sidebar', a const page = await context.newPage(); await page.goto(`/p/${pond.slug}`); await page.getByRole('button', { name: /new page|neue seite/i }).click(); - await page.locator('.sidebar').getByLabel(/title|titel/i).fill(title); + await page + .locator('.sidebar') + .getByLabel(/title|titel/i) + .fill(title); await page.getByRole('button', { name: /create|erstellen/i }).click(); await expect(page.locator('.sidebar__page--active')).toHaveText(title); diff --git a/apps/web/e2e/sidebar.spec.ts b/apps/web/e2e/sidebar.spec.ts index 2d4a571..9cae1a2 100644 --- a/apps/web/e2e/sidebar.spec.ts +++ b/apps/web/e2e/sidebar.spec.ts @@ -110,7 +110,10 @@ test('new-page flow: button opens a title prompt and the editor opens on create' await page.goto(`/p/${pond.slug}`); await page.getByRole('button', { name: /new page|neue seite/i }).click(); - await page.locator('.sidebar').getByLabel(/title|titel/i).fill(title); + await page + .locator('.sidebar') + .getByLabel(/title|titel/i) + .fill(title); await page.getByRole('button', { name: /create|erstellen/i }).click(); await expect(page).toHaveURL(new RegExp(`/p/${pond.slug}/.+`)); diff --git a/apps/web/src/editor/nodes/task-item.tsx b/apps/web/src/editor/nodes/task-item.tsx index 64d8886..75535c3 100644 --- a/apps/web/src/editor/nodes/task-item.tsx +++ b/apps/web/src/editor/nodes/task-item.tsx @@ -1,21 +1,29 @@ import { Node } from '@tiptap/core'; import { NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react'; import type { NodeViewProps } from '@tiptap/react'; +import { useTranslation } from 'react-i18next'; import { attributesFromSpec, nodeSpec } from '../spec-utils'; /** `packages/shared`'s task_item.parseDOM does not read `data-checked` back * (issue #24) — checked state only ever comes from the node's own attrs, set - * here via the checkbox, never re-parsed from HTML. */ + * here via the checkbox, never re-parsed from HTML. + * + * DOM shape (#169): the render host itself is the `
  • ` (see the renderer + * options below) so the `
      ` has only list items as direct children — + * TipTap's default extra `
      ` host broke the list semantics for + * screen readers. The wrapper flattens away via display:contents. */ function TaskItemView({ node, updateAttributes, editor }: NodeViewProps): React.JSX.Element { + const { t } = useTranslation('tasks'); const checked = Boolean(node.attrs.checked); return ( - + @@ -34,7 +42,13 @@ export const TaskItem = Node.create({ parseHTML: () => taskItemSpec.parseDOM, renderHTML: ({ node }) => taskItemSpec.toDOM!(node), addNodeView() { - return ReactNodeViewRenderer(TaskItemView); + return ReactNodeViewRenderer(TaskItemView, { + as: 'li', + attrs: ({ node }) => ({ + 'data-type': 'task_item', + 'data-checked': String(node.attrs.checked === true), + }), + }); }, addKeyboardShortcuts() { return { diff --git a/apps/web/src/graph/ForceGraph.tsx b/apps/web/src/graph/ForceGraph.tsx index e464dfc..925eaa2 100644 --- a/apps/web/src/graph/ForceGraph.tsx +++ b/apps/web/src/graph/ForceGraph.tsx @@ -14,6 +14,7 @@ import { type SimulationNodeDatum, } from 'd3-force'; import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; /** * Self-contained SVG force graph (issue #112). Only `d3-force` is bundled — @@ -92,6 +93,7 @@ export function ForceGraph({ onNodeClick?: (id: string) => void; settings?: ForceGraphSettings; }): React.JSX.Element { + const { t } = useTranslation('graph'); const [view, setView] = useState({ k: 1, tx: 0, ty: 0 }); /** Last known positions — read by React renders, written by sim ticks. */ const positionsRef = useRef(new Map()); @@ -272,6 +274,7 @@ export function ForceGraph({ className="force-graph" viewBox={`${-width / 2} ${-height / 2} ${width} ${height}`} role="img" + aria-label={t('svgLabel', { nodes: nodes.length, edges: edges.length })} onWheel={onWheel} onPointerDown={onPointerDown} onPointerMove={onPointerMove} diff --git a/apps/web/src/styles/base.css b/apps/web/src/styles/base.css index 4d0024a..fbfad2d 100644 --- a/apps/web/src/styles/base.css +++ b/apps/web/src/styles/base.css @@ -1701,15 +1701,19 @@ ul[data-type='task_list'] li { robust form. `:first-of-type`/`:last-of-type` (NOT `:first-child`) because the ``/`