Skip to content

v6.5.0

Choose a tag to compare

@58bits 58bits released this 20 Sep 01:43
· 83 commits to develop since this release

Highlights

  • @byline/richtext-lexical — An inline image can now carry a click-through link. The image dialog gained a Link tab offering a Byline document or an arbitrary URL, using the same discriminated union the link extension already uses, so an editor picks a target the same way in both places. The target is a second, independent relation: the flat DocumentRelation on the node still addresses the source media document, while the new nested link addresses where a reader goes, which is why it is nested rather than flattened. An internal target is refreshed on every save and read by a new inlineImageLinkVisitor, exported from @byline/richtext-lexical/server alongside its two siblings for hosts that compose their own visitor list; runLexicalPopulate hydrates both relations in one batched pass, one read per collection. When a target is deleted the walker marks it unresolved and the renderer drops the anchor while keeping the image, matching how a text link degrades. link rides through exportJSON/importJSON, so copying an inline image with its caption and pasting it elsewhere keeps the link — that is the path a clipboard actually takes between two editors sharing a namespace. The HTML round-trip carries the relation as data-link-* attributes on the <img> rather than an <a> wrapper, because an anchor there would make Lexical's own link conversion produce a link node wrapping an inline-image node on re-import. Markdown export emits [![alt](src)](href). The field is additive and optional: the serialized node version is unchanged and existing images import untouched.

  • @byline/richtext-lexical — The link and inline-image target pickers are now one shared sub-form. Both modals previously carried their own copy of the "document or URL" controls. The decision layer moved into a React-free link-target-state module — empty/from/to/validate — with a single JSX shell above it, which is what lets the inline image offer a "No link" choice the link node cannot. The picker itself was rebuilt to match the admin relation field: a chosen document is a bordered tile with edit and remove icon buttons rather than a text button whose label changed with the state, and the frame uses the shared --field-border token, darkened on dark surfaces. The inline-image dialog is now tabbed (Image, Link) with a reserved panel height, so choosing a link type no longer shifts the dialog under the cursor; a validation error switches to the tab that owns it rather than letting Save appear to do nothing.

  • @byline/ui, @byline/admin, @byline/i18n — The password reveal toggle is a real button, and every password field has one. It was a bare <div> with an onClick wrapping an aria-hidden icon: unreachable by keyboard and silent to screen readers, so the only way to reveal a password was a mouse. It is now a <button type="button"> with a translated accessible name and an aria-pressed state, swapping only the inner icon so its DOM node stays stable across re-renders, and preventing default on mousedown so activating it does not blur the field or disturb the caret. Sign-in and admin user creation used a plain input with no toggle at all and now use InputPassword; a repository-wide check finds no remaining unmanaged password inputs. common.actions.showPassword and common.actions.hidePassword were added to all eight bundled locales for the button's accessible name — @byline/ui itself stays i18n-agnostic and accepts the strings as optional props with English defaults.

Bug Fixes

  • @byline/ui — InputPassword silently ignored its own type. Caller props were spread after the computed type, so a caller passing the obvious type="password" won: the eye icon swapped on click while the field stayed masked forever. Props now spread first. type and endAdornment are additionally removed from the public props type, so the mistake is a compile error rather than a bug someone has to notice — see Breaking Changes. This was not hypothetical: both password fields converted in this cycle were passing type="password", and converting them without this fix would have shipped exactly that symptom.

  • @byline/cli (public renderer templates) — A linked image inside a text link no longer emits nested anchors. <a> inside <a> is un-nested by the HTML parser, which silently restructures the document and drags the surrounding layout with it. The shape is reachable in production rather than theoretical: markdown [![alt](img)](url) imports as a link node containing an inline-image node, and the image dialog can then give that image a link of its own. The serializer now tracks whether an anchor is actually being rendered — a link node whose target is unresolved emits no anchor, and suppressing the inner link in that case would cost a working link for nothing — and a link node reached while already inside an anchor renders its children plain. The anchor context travels into a caption's nested serialize pass too, since a caption can hold its own links.

  • @byline/cli (public renderer templates) — An inline image's "Open in new tab" setting is now honoured. The shared text-link helper forces target="_blank" for any external href regardless of the node's own newTab, which suits a text link — they also get a visible external-link affordance — but made the image dialog's checkbox a no-op for exactly the URLs an editor is most likely to tick it for. The image now derives target from its own setting and states rel="noopener" alongside it. The equivalent inconsistency in text links is unchanged and deliberate; altering it would silently change every existing external link.

  • @byline/richtext-lexical — Removing an inline image's link now works. InlineImageNode.update() guards every field with != null, a convention that cannot express "remove this". link is keyed on the presence of the property instead, so clearing a click target is a real operation rather than a silent no-op.

Chores

  • monorepo — Dependencies updated across the workspace, including the Lexical toolchain to 0.51 — see Breaking Changes.
  • @byline/richtext-lexical — The internal-link hydration rules (title lookup, buildDocumentPath with its generic fallback, the unresolved-target branch) are shared by the link and inline-image visitors through one createInternalLinkHydration factory rather than duplicated per node type.
  • monorepo — SerializedLexicalNode and inlineImageLinkVisitor are recorded in the public-export baseline as deliberate API: a host writing its own rich-text renderer needs the node type, and a host composing a custom visitor list cannot do so without the visitor export.

Breaking Changes

  • @byline/richtext-lexical — Lexical moved to 0.51, and two of its types changed shape. SerializedEditorState no longer takes a type parameter, so SerializedEditorState<SerializedLexicalNode> no longer compiles — drop the argument. A nested editor's children (an inline image's caption, for instance) are now typed SerializedPartialNode[], whose version and $slots relax because a compact export omits what parsing restores on its own; a renderer whose node type intersects Lexical's strict SerializedLexicalNode will reject them, and the fix is to stop intersecting rather than to cast at each call site. Separately, @lexical/code is now a thin facade over @lexical/code-core and no longer re-exports the Prism tokenizer or language tables: registerCodeHighlighting, CODE_LANGUAGE_FRIENDLY_NAME_MAP, CODE_LANGUAGE_MAP and getLanguageFriendlyName come from @lexical/code-prism, which is now a direct dependency of this package. Any application carrying its own rich-text serializer should expect to make these edits when upgrading.

  • @byline/ui — InputPassword no longer accepts type or endAdornment. Both are omitted from InputPasswordProps, because both are what the component exists to control and passing either previously broke the reveal toggle without any error. A caller passing them will now fail to compile; remove the prop. Two new optional props, showPasswordLabel and hidePasswordLabel, supply the toggle's accessible name and default to English.

All other @byline/* packages bumped to 6.5.0 in lockstep with no behavioural changes this cycle.