Repository navigation
Richer internal-link chips, and a kind icon beside the pad link #174
Copy link
Copy link
Open
Labels
EditorTiptap & ProsemirrorTiptap & ProsemirrorFeatureUIenhancementNew feature or requestNew feature or request
Description
Activity
- addedenhancementNew feature or requestNew feature or requestEditorTiptap & ProsemirrorTiptap & Prosemirror
on Aug 21, 2026 Build notes for the kind icon (checked against
main, 2026-09-28)These notes cover the pad-text half of this issue: the leading kind icon. Each code claim below was read in the repo. Runtime claims that need a browser say so.
Scope. Build the kind icon only, with no background wash. This issue asks for the icon, not a wash. A wash needs a maintainer ruling on this issue first. If a wash is ruled in, draw it with CSS on the anchor, not with inline decorations. Inline decorations draw one pill per text node on a link with mixed marks.
Which links to scan
- Scan text with an internal-classifying href on both
hyperlinkandlink. Stored pads, Markdown import and connected-app writes still hold the oldlinkmark. The pad keeps it registered but inert (apps/webapp/src/components/TipTap/TipTap.tsx:113-121). Store links from Markdown import and connected-app writes on the hyperlink mark #335 moves new Markdown links ontohyperlink; old pads keeplink. - Some stored links show the wrong kind. A heading link built in a Filter view classifies as
filter, and one built with?chatroom=classifies aschat. Build stored heading and message links from the pad path, not the address bar #331 fixes new links. Stored links keep their kind, and the preview already shows that kind today. - A
wwwlink does not classify, because the origin must match exactly (TipTap/hyperlinkPopovers/internalDocumentLink.ts:29). - Put the plugin in the webapp, next to
classifyInternalDocumentLink, not in@docs.plus/extension-hyperlink. The classifier reads the pad route.
Widget placement
- Draw the icon with
Decoration.widget(from, icon, { side: 1, key: kind + href })and nomarksoption. - Reason: the webapp resolves
prosemirror-view1.42.3 through@tiptap/pm. In itsupdateChildren(prosemirror-view/src/viewdesc.ts:787-790), a widget withside >= 0and nomarkssyncs to the live marks of the next child. So the icon joins the live<a>. - A widget that carries
marks: [mark]keeps a copy of the mark. After an href edit on a mapped set, it can draw a second, stale<a>. Readers would then meet two links. Confirm the single<a>in a browser. - With the icon inside the
<a>, a tap on the icon opens the same preview. The click path usestarget.closest('a')(extensions/extension-hyperlink/src/interactions/clickHandler.ts:18). - Never toggle attributes on the
<a>from JS. A mark has acontentDOM, so a toggle dirties its range. - A link with mixed marks has several text nodes. Give only the first one the icon.
- Build the icon from
apps/webapp/src/utils/lucideSvgString.ts, so no user text goes intoinnerHTML. Mark itaria-hidden, so readers meet one link.
Rebuild rule and cost
- Rebuild only when a transaction touches link marks or carries
y-syncmeta. Otherwise map the set. - Cost trap:
@tiptap/y-tiptap3.0.9 applies each remote change as onereplaceover the whole document (_typeChangedin@tiptap/y-tiptap/dist/y-tiptap.js:794-798). Mapping then drops every decoration. So "rebuild ony-sync" is a full walk on every remote keystroke. - A cheaper path has three parts. Give each widget a stable
key. Cache the classify result by href and pad slug. Rescan only betweenoldDoc.content.findDiffStart(newDoc.content)andfindDiffEnd. y-tiptap reuses unchanged nodes, so that diff is cheap. This path is code-read, not run. - Before merge, measure the rebuild on a large pad while a second client types. Record the per-transaction time in a comment on this issue.
Checks before merge
- Walk typing at a link start in Chrome, Safari and Firefox on desktop. Also walk it on iOS Safari, where
IOSCaretFixruns (TipTap.tsx:234), and Android Gboard composition. A widget iscontentEditable=false. - Stale clients show plain links. No schema change, so there is no two-release rollout.
- Print: the icon prints in PDF export and shows in History. Decide on print when you build it. If it must not print, hide it with one rule in
apps/webapp/src/styles/_print.scss. - Add one row to
.cursor/docs/design-system.mdfor the in-text kind icon. The doc has no chip-in-text row today. - Run
bun run --filter @docs.plus/webapp typecheck. Walk the icon in light and dark, on desktop and phone.
Rules that apply
apps/webapp/src/components/TipTap/CLAUDE.md§Editor Performance: map the decoration set; do not rebuild on every keystroke.AGENTS.md§UI And Theme and thedesign-systemskill for the icon size and color.AGENTS.md§Test Policy: no test is required. If the rebuild rule grows dense branching, a unit test may pin it. Prove it by sabotage.
- Scan text with an internal-classifying href on both
Metadata
Metadata
Assignees
Labels
EditorTiptap & ProsemirrorTiptap & ProsemirrorFeatureUIenhancementNew feature or requestNew feature or request
Goal
Make same-document hyperlinks easier to read and more useful than a generic URL.
Two surfaces, one classifier (
classifyInternalDocumentLink):Facts (2026-08-21)
document,heading,chat,filter,history. Labels come fromdescribeInternalDocumentLink. Filter copy isFiltered view · N term(s)plusMatch any/Match all. The terms themselves never appear in the chip.<a href>. No kind icon. No extra schema attr.info/--info-ink). Do not merge them with this hyperlink chip.renderHTMLis['a', attrs, 0]. Icons must not be written into Yjs text. Do not addtoDOMon the storage hyperlink stub..ProseMirrorfrom JS (DOMObserver recreates media node-views). Widget decorations and CSS on the existing<a>are the safe paths. Live regions inside the editor stay forbidden.Icons. Do not put emoji,Fa*, orSi*into pad text or the chip.What to build
Preview chip — name the destination
+N; the full set stays in the popover.Version N/History.info/--info-inklanguage so they look like the same filters. The icon tile on the chip stays the internal-link primary wash.Pad text — leading kind icon
Interaction
Internal links may do more in the preview than an external unfurl (named destination, Go in place, terms). They must not do more in the document click than today’s preview/open split. Keyboard and screen readers still meet one link, not “icon + link”.
Out of scope
https://github.com/…links (later, separate).Acceptance criteria
+N, not only a count.Blocked by
None. Visual density of many-term chips (how many names before
+N) is HITL on this issue if the first walk feels crowded.