Consistent emojis, reworked bookmarks, and navigation ordering - #1158
Closed
mikebarkmin wants to merge 12 commits into
Closed
Consistent emojis, reworked bookmarks, and navigation ordering#1158mikebarkmin wants to merge 12 commits into
mikebarkmin wants to merge 12 commits into
Conversation
makeUrl only passed URLs through untouched when they contained "://" or started with "data:". Every other scheme was treated as a relative path, so a markdown link like [Write us](mailto:a@b.c) was joined with the base path and rendered as href="/mailto:a@b.c". Links configured in hyperbook.json were unaffected, because those hrefs are used verbatim. Add an isExternalUrl helper to @hyperbook/types that matches any RFC 3986 scheme, and use it in the makeUrl implementations of the builder and the VS Code preview. This covers mailto: and tel: as well as sms:, geo: and friends, while https:// and data: keep working as before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW
Emojis were resolved to plain Unicode characters and drawn by whatever emoji font the reader's operating system provides, so the same book looked different on Windows, macOS, Android and Linux. Add a rehypeEmoji plugin that swaps emojis for Twemoji images when elements.emoji.style is set to "twemoji". It runs after the shell, so icons configured in hyperbook.json are covered along with the content, and it skips pre, code, script, style, textarea and title. Characters that are text by default, like (c) or (tm), are only replaced when the author asked for the emoji presentation, and an emoji without a Twemoji asset is left as text. The full Twemoji set ships in the package assets, but a build copies only the images a book actually uses into its output, both for a full build and for an incremental rebuild in the dev server. The default stays "native", so existing books render exactly as before. Twemoji graphics are CC-BY 4.0 by Twitter, Inc and other contributors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW
A bookmark label was interpolated into the onclick attribute of the bookmark button and rendered with innerHTML. That had three problems: a heading containing a quote or a backslash produced a broken JavaScript string and the button threw, the label was persisted markup that came back through innerHTML, and the label was the markdown text rather than what the page renders, so an emoji drawn as an image was a plain character again in the bookmark list. Labels are now read from the rendered heading when a bookmark is saved and stored as parts, each with its text and, for an emoji, its id. The bookmark list builds its entries from those parts with the DOM, so nothing that was stored is parsed as HTML, emojis keep the look they have on the page, and the id rather than a URL means a bookmark survives a change of the basePath. The button lost its inline onclick and is handled by one delegated listener, which also covers headings that are added later. It carries data-key, a data-label fallback and aria-pressed. The store moves to version 6, which drops the pointless index on the label and migrates existing labels to the new shape. A plain string label is still rendered, so bookmarks from an older version and from an older export keep working. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW
The indicator was the 🔖 character in two places: as the content of the button on a heading, and as the content of a ::before in the bookmark list. Both were drawn with the emoji font of the reader, so the control looked different on every platform. The one in the button also changed into a Twemoji image once a hyperbook set the emoji style, which made a piece of interface follow a content setting. Both are now a mask over the feather bookmark icon, so the control looks the same everywhere and stays a control. It takes the color of its surroundings, which keeps it readable in light and dark mode, and a bookmarked heading shows a filled icon instead of only a less transparent one. The button carries an aria-label now, since it no longer has content to name it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW
The icon inherited its color, which picked up the color of the h1 rather than the color the heading appears in: the heading text is a link, and the button is its sibling, so inheriting missed the brand color the reader actually sees. Take the same color the heading link takes, which also keeps the icon in step with the brand color of the hyperbook and with the dark mode variant of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW
Dogfoods the emoji style on the documentation, so the emojis in the docs, the language flags of the library and the icons of the custom links look the same for every reader. Also fixes a crash the website build turned up: an element that carries no children at all, which a directive can produce for a void element like an img, made the emoji walk throw. It is skipped now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW
A custom element renders its children until it is upgraded, and a side drawer keeps its closed state in its shadow root. The script that defines it is an async module, so it runs after the first paint: until then the search drawer painted over the header and the table of contents drawer over the article, which read as a collapsed layout for a moment. Measured while undefined, the search drawer was 500x98 over the header and the table of contents drawer 500x216 over the article. The nav drawer was not visible on a wide screen, because it sits in the mobile nav, which is hidden there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW
The light and dark stylesheets sit in a noscript element and are written into the page by a script. Nothing can paint before they are there, so loading that script as a file put a round trip in front of every first paint, and a second one in front of the stylesheets it writes. The script is 1.5 kB, so it is part of the page now. On the documentation, over a connection throttled to 1.5 Mbit/s with 100 ms of latency, the first contentful paint went from a median of 1128 ms to 984 ms over seven runs each. The behaviour is unchanged: a mode the reader picked before still wins over the system preference, in both directions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW
Pages were rendered before the sections of the same level, so a page could not sit after or between them. The index of a page and the index of a section are one order now, and the navigation renders a run of pages as a list, broken by every section that falls between them. The reading order follows the same order, so the previous and next buttons, the breadcrumb and pagelist agree with the sidebar. That order lives in one place now, instead of once in the shell and twice in the page list. Without an index nothing moves: a page still comes before a section, and a page wins a tie against a section. A book that indexes its sections but not a page does move that page behind them, which is why the documentation now gives its changelog page an index and shifts the sections after it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW
A page of a section sat one pixel to the right of a page of the level above, so the only thing telling the two levels apart was the hairline on the left of the list. That was harmless while every page came before every section, but a page can sit after a section now, and it read as one more page of that section. The pages of a section are indented by 0.75rem, which puts them 13px in instead of 1px, and a section block keeps a little room below it. Both are scoped to a real section, so a virtual section still renders its pages at the level of its parent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW
Only the top level of the navigation shared one order. Inside a section the subsections were still rendered after every page of that section, so a subsection could not sit between two pages, and the sidebar disagreed with the previous and next buttons, which already followed the index. One function renders a level now and calls itself through the section it renders, which also drops the second copy of the merge that existed for sections shown as a page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012kkjvqifo7fQ5W6woRWiSW
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Member
Author
|
Superseded by #1159. The links, the emojis and the bookmarks from this branch went out with #1153 and shipped as v0.101.0, so what was left is the navigation order and the two load fixes. #1159 carries those five commits on top of the current main. Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A branch that grew from a
mailto:bug into four related changes. Each commit stands on its own, and each is covered by a changeset.Links
makeUrlonly passed a URL through untouched when it contained://or started withdata:, so every other scheme was resolved against the base path and[Write us](mailto:a@b.c)becamehref="/mailto:a@b.c". Links configured inhyperbook.jsonwere unaffected, because those hrefs are used verbatim.An
isExternalUrlhelper in@hyperbook/typesnow matches any RFC 3986 scheme, used by the builder and the VS Code preview. This coversmailto:andtel:as well assms:andgeo:, and keepshttps://anddata:working as before. It also fixes the[get in touch](mailto:…)links on this repository's own website.Emojis
Emojis were resolved to plain characters and drawn with whatever emoji font the reader's operating system provides, so the same book looked different on Windows, macOS, Android and Linux.
elements.emoji.style: "twemoji"replaces them with Twemoji images at build time. It covers emojis in the content as well as icons from the config, leaves code untouched, and only copies the emojis a book actually uses into its output — a full build of the documentation copies 1268 of the 3720 images. The default stays"native", so an existing book renders exactly as before. The website opts in.Characters that are text by default, like © or ™, are only replaced when the author asked for the emoji presentation, and an emoji without a Twemoji asset is left as text.
Bookmarks
A bookmark label used to be interpolated into an
onclickattribute and rendered withinnerHTML. Three problems: a heading containing a quote or a backslash produced a broken JavaScript string and the button threw, the label was persisted markup that came back throughinnerHTML, and the label was the markdown text rather than what the page renders, so an emoji drawn as an image was a plain character again in the bookmark list.Labels are now read from the rendered heading and stored as parts:
The list builds its entries from those parts with the DOM, so nothing that was stored is parsed as HTML, and an emoji keeps the look it has on the page. Emojis are stored by id rather than by URL, so a bookmark survives a change of the
basePath.The bookmark indicator is no longer the 🔖 character in two places — the button on a heading and a
::beforein the list. Both are drawn from the stylesheet with a mask, so the control looks the same everywhere, takes the color of its heading in light and dark mode, and does not follow the emoji setting. A saved bookmark shows a filled icon instead of only a less transparent one.Breaking:
hyperbook.ui.toggleBookmark(key, label)no longer takes a labelonclick; they are handled by a delegated listener and carrydata-key,data-label,aria-labelandaria-pressedNavigation
Pages were rendered before the sections of the same level, so a page could not sit after or between them. The
indexof a page and theindexof a section are one order now, at every level, so a subsection can sit between two pages of its section. The reading order follows the same order, so the previous and next buttons, the breadcrumb and::pagelistagree with the sidebar — that order lives in one place instead of once in the shell and twice in a page list.Without an
indexnothing moves: a page still comes before a section, and a page wins a tie. A book that indexes its sections but leaves it off a page does move that page behind them, which is why the documentation now gives its changelog page anindexand shifts the sections after it. Both website books render the same navigation as before.The pages of a section are also indented now. They sat one pixel to the right of a page of the level above, which was harmless while every page came before every section.
Loading
Two fixes for a moment of collapsed layout on load:
side-drawer.jshad run. Measured while undefined, that was 500x98 over the header and 500x216 over the article.Testing
272 tests pass. New tests cover the link schemes, the emoji substitution and its guards, the bookmark button markup including a heading with a quote, and the navigation order at the top level and inside a section.
Beyond that, everything visual was checked in Chromium against a real build: the bookmark round trip and its store migration from a seeded version 5 database, the emoji styles in light and dark mode, the drawer flash before and after, the first paint measurement, and the navigation with three levels of subsections.
The branch name is left over from the first commit and no longer describes the contents.
Generated by Claude Code