Repository navigation
feat(ui): finish the v3 phone, accessibility and polish pass - #485
Merged
Merged
Conversation
Obsidian updates a plugin in place without reloading its windows, and Svelte only ever appends a component's sheet, so the sheets of the version before an update kept applying until a restart. PodNotes now removes its earlier pn- sheets from the main window and every popout at the start of onload, before registerView mounts the restored views. tests/e2e/update-styles.test.ts installs the 2.21.1 release over this build, plants stand-in stale sheets, then updates back the way Obsidian's updater does, with the view in the main window and in a popout.
…check - .pn-bare in the shell carries the reset of Obsidian's button skin that fifteen rules repeated, together with the shell's own .pn-back, .pn-iconbtn and .pn-navitem. Declarations equal to Obsidian's own button rule (border, cursor, font size) go. - One button:focus-visible rule replaces the list of thirteen selectors. - isPlaying(status) replaces four copies of the same derivation. - tests/tooling/componentStyles.test.ts fails when two components scope the same class, so a component's sheet only styles what it renders. - The queue list drops its unused pn-queue class; the e2e targets select it by its accessible name.
…s and tooltips - A cover without artwork shows the first three lines of its name, at 10 px or more, so a long name keeps its beginning on a row's 40 px cover. - The feed banner keeps monospace for a host only; "Address not on this device" wraps in the UI font instead of truncating at 320 px. - Every disabled button uses Obsidian's 0.7 opacity: the 0.35 and 0.45 overrides go, and an unavailable row dims through its aria-disabled. - The mini player draws its cover with the rows' Cover and tileOf, so an episode without artwork shows its podcast's name as rows do. - No control has both an aria-label, which Obsidian shows as a tooltip, and a title; the tooling test enforces it. - The transcript's Follow chip no longer appears when a resize clamps the list's scroll position, and Find folds a dotted capital I without moving its marks.
…uest - Each settings field is labelled by its row's name through aria-labelledby, which Obsidian does not show as a tooltip. Toggles keep Obsidian's native 30 px size, as every toggle in Obsidian's settings has. - Up next's Edit, and every other panel head control, is at least 44 px wide on a phone, not only 44 px tall. - One shell rule stops every transition and animation in the view when the user prefers reduced motion; the grid card's press had none before. It replaces the play button's and the playing bars' own rules. - styles.test.ts cascades the components' compiled CSS in jsdom: motion under the reduced-motion query, and the video play button's focus ring drawn inside the frame that clips it.
… phones - On a phone the deck keeps the desktop's 12 px gaps and 8 px top padding, so the stage, and the artwork that fills its height, grow by 20 px under Obsidian's header and navbar. A panel already fills the stage. - Hover on the shell's controls, rows and the deck applies only on a device that can hover, so a tapped control does not stay lit on a phone. - A menu's closing Escape is handled, and focus returns to its control, as the menu starts to hide; on a phone the hide callback runs 150 ms later, when the Escape is gone.
…the mini player do The full player kept its own image, failure state and tile. It now uses the rows' Cover and tileOf: a remote episode without artwork, or whose artwork fails, shows its podcast's name as rows and the grid do, and a local file its tile. This saves 111 gzip bytes.
#482 made the banner's detail wrap, so "Address not on this device" no longer truncates at 320 px. The split that set a sentence in the UI font and only a host in monospace cost 52 gzip bytes for a font change, so it goes back to master's single rule; the e2e keeps the no-truncation check.
… transcript note - In a video's fullscreen the view's Escape now leaves fullscreen and keeps the player open; the next Escape closes the player. Obsidian's window does not leave element fullscreen on Escape by itself, and closing the player only ended fullscreen by removing the frame. The fullscreen button stays on the frame in fullscreen as Exit fullscreen. - The transcript panel offers Open note, offline too, when the episode's transcript note already exists at the transcript path, instead of Save as note, which the command refuses for an existing note. It follows a note made, moved or deleted, and a new transcript path. - The podcast menu's Set feed address, and Edit feed URL, are disabled offline, as the feed banner's button is: an address is checked by loading its feed.
…e right sidebar The journeys opened PodNotes wherever the vault's layout left it. After the verify skill's screens.mjs had moved the view to the main area or sized the window, the transcript panel was shorter, the click helper scrolled line 7 into view, and the Follow chip that scroll shows covered it. Each journey now puts PodNotes in the right sidebar 820 px wide, widening the window if it has to.
…odes offline Beside #482's journey for a private feed whose address is missing: one whose address is on this device, opened offline after its feed was cached, lists the cached episodes and shows no feed banner and no missing address.
Svelte writes its scoping class into every selector written at a sheet's
top level, but only once for rules nested in a root-anchored
`.root :global { ... }` block. Each component now keeps its root's own
declarations and its state, width and platform variants (`&.is-wide`,
`.is-phone &`) in that one block.
The flattened rules select the same elements with the same specificity,
except two that rise by one class and change nothing: the wide header's
`.pn-head` and `.pn-head-title` rules, which nothing else styles, and
the queue's row-hover rule for Remove, whose only rival in between is
its own focus rule with the same opacity.
A scoped keyframes rule makes Svelte write its scoping class into every
element of the component. EpisodeRow's `pn-eq` bars are the only
keyframes in the view, and put the class on all 17 of the row's
elements. A global `pn-eq`, inside `:global { }`, leaves it on the root.
ChaptersPanel's sheet held one rule, the weight of a chapter's name. Panel already styles its rows and their time, so it styles the name beside it, and ChaptersPanel no longer ships a sheet of its own.
…sing With a transcript note saved before, the Transcript panel offered Open note only once the feed's transcript loaded. When it did not load (an offline restart) the panel showed only Retry, and when the feed has none (the note came from a paid transcription) it offered to transcribe again. Both now offer Open note, in place of the paid transcription, through the same button as the panel's head, which keeps its label outside the head.
Obsidian 1.14 focuses a toggle's `label.checkbox-container`, and gives its checkbox tabindex -1, so the row's name on the checkbox never reached a screen reader. The label is named after the row too. The unit mock builds Obsidian's toggle markup, so the test can tell. Each Setting has a fresh name element, so it always takes a new id.
A touch screen keeps the last tapped element hovered. Eleven `:hover` rules outside `@media (hover: hover)` stayed lit after a tap, and the panel switch's hover rule outranked its pressed look, so a tapped switch on a phone looked hovered rather than on. Every component's hover rules now sit in that query, and the test walks every component under src/ui instead of a hand-kept list.
The clamped name had a 10cqw padding inside its overflow clip, so the line after the third painted into that padding: on the 93 px card of a 320 px view, four lines had their middle inside the clip. A margin puts the clip edge under the third line and keeps the name where it was. The e2e test now counts those lines.
…on its button A phone hides Obsidian's menu after a 150 ms animation, and Obsidian sends focus from a sidebar to the main area on an Escape nothing handled. The test opens a row's menu in the right drawer, where PodNotes opens on a phone, presses Escape with trusted input, and reads focus in the task after the keydown and 200 ms later: on the row's More button both times.
…d part way The update-styles test swaps PodNotes' artifact symlinks for files and puts them back in its finally. An install that failed after removing main.js made that finally throw ENOENT before relinking it, which left the vault's PodNotes without a bundle. It now removes with force.
Every element the button rules style is a button, and Obsidian's `button` rule already sets the same display, alignment, height, radius, font size, cursor and white-space. No other Obsidian rule that reaches a button in the view sets those properties, so the cascade resolves to the same values. Obsidian also hides a search field's cancel button.
…y state The two states shared their frame, icon and note button, and differ in the icon, the sentence and whether Retry or the paid transcription shows.
…n's flair The counts used Obsidian's `.flair` and undid its tag look with five declarations. A `pn-flair` class needs none of them. What Obsidian's rule still set does not show here: a radius without a background, a relative position without an offset, and vertical-align on a flex item.
…nt offline Offline, a load is not attempted, so the journey that restarts offline records no failure, and showing every recorded failure offline passed it. The journey now fails a refresh online first, and checks that going offline hides that failure behind the Offline strip.
The episode menu said "Remove from Local Files" while the page is "Local files". The stored playlist keeps its name.
Without {{title}} in the download path, episodes resolve to the same file.
The download code then refuses the second one, with "A file already
exists" when another episode's file is there and "A download is already
in progress" while the first is still writing, so nothing is overwritten.
The warning said downloads overwrite each other.
…s narrow The label folded away on the view's compact desktop layout, so in a phone's right drawer, about 330 px wide, "Save as note" ran past the panel's edge. The panel head is now a size container, and Save folds when the head has less than the 340 px the phone's label needs: in a 320 px sidebar and in the drawer, not in a 390 px phone's main area. The CSS mock applies a container query the way it applies a media query.
Following kept the line playing now a third of the way down the list. In a phone's right drawer, where the list is 248 px tall, that left the line before it 7 px above the list and under its faded top edge. The list declares that 12 px edge as scroll padding, and following now puts the line a third down or under the whole line before it and that edge, whichever is lower, but no lower than half way, so a long line before it does not push it out of view. The e2e helpers for the phone layout and the right drawer serve the transcript journeys' three phone tests.
view-polish timed out once waiting for a Latest row. That run reused an instance on which the verify skill's `screens.mjs offline on` had left the renderer offline through CDP: no feed loaded, the Latest page stayed empty, and the next run passed because the suite's offline test turns the network back on as it ends. Reproduced by the same commands, then a run of view-polish: "Timed out waiting for episode First Steps shown". The harness now brings the renderer online before the suite starts, and the reused-vault lifecycle test leaves it offline to check that.
Following put the transcript's line a third of the way down, lower to show the whole line before it, but no lower than half way. A tall line could then end in the faded bottom edge, the half-way cap clipped a line before it that would have fit, and a third of the way down could leave the line above the one before it cut under the top fade, as in a 320 px sidebar. Chapters kept the line a third of the way down and ignored both fades. One reveal beside Panel now serves both panels. It puts the row under as many whole rows as fit in the list's top third, starting where the top fade ends, so no row shows cut there, and under the whole row before it even when that one is taller. The row never reaches into the bottom fade and never starts under the top fade. Each fade's height is one custom property on the scroller, which the mask and the scroll padding use and reveal reads.
view-polish and journey-transcript each pasted the CDP phone metrics and the steps that open PodNotes in a phone's right drawer. asPhone now sits in the journey actions, and placePodNotes takes a "right drawer" place that waits until PodNotes shows there. The Escape focus test removes the global it records focus in.
The test read each field's aria-labelledby and looked the label up itself, so it passed on markup that names nothing. It now turns on diarization, whose rows take focus only then, tabs through PodNotes' settings with trusted key presses, and reads the name Chromium computes for each field that takes focus, the name a screen reader announces, and what Chromium took it from. Each must be its row's name, through aria-labelledby: a focused toggle's label would otherwise take the name of the checkbox inside it.
The note button carried an aria-label equal to its label, so Obsidian showed a tooltip repeating "Open note" in the empty panel, where the label is in sight. The aria-label kept the button named in a narrow head, which folded the label away with display: none. A narrow head now clips the label out of sight and keeps it in the accessibility tree, so the label names the button everywhere and the aria-label is gone.
The rail's add button was the one control whose rule sat outside its component's root, at the specificity of two classes. On a tablet, Obsidian's .is-tablet button:not(.clickable-icon) outranks that and gave it 4 px by 20 px of padding. The rail's styles now nest under its root like every other component's.
The stage's artwork rules matched every cover in the stage, so where a narrow player shows Up next in the stage, its rows' covers took the artwork's drop shadow, and a local file's row tile took the artwork's icon size. The rules now match only the stage's own child.
…sabled icons Disabled buttons took Obsidian's 0.7 for buttons, which reads as enabled, most of all on icon buttons and on an offline row that needs a connection. One rule in the view now dims a disabled or aria-disabled button to 0.4, as Obsidian dims a disabled icon. Hover no longer lights a disabled icon button or call to action, so the icon button's own disabled rule is gone. Nothing inside a disabled button dims again, and the scrubber keeps its own 0.5.
On a phone the clear button is 44 px wide, but the field kept the desktop's 28 px of room on its right, so a pasted link ran into the X. A phone's field now keeps 44 px on its right.
The container rule that folds Save's label gave no reason for 340 px. Measured on a phone, the head's icon, title, Free tag, 44 px Find and labelled Save take 339.8 px.
The one disabled rule dims every disabled control to 0.4, but the pressed icon button's full opacity outranked it. reveal now says "at least the row before" with the position it has already taken instead of reading the DOM again.
… head check reads The settings test clicked the diarization toggle, which turned it off on a vault where it was already on, so the diarization rows never took focus. The transcript head check reports whether Save has an aria-label; it now says so.
Deploying podnotes with
|
| Latest commit: |
6f73df0
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://6bb11e3e.podnotes.pages.dev |
| Branch Preview URL: | https://feat-ui-polish.podnotes.pages.dev |
chhoumann
added a commit
that referenced
this pull request
Oct 15, 2026
Applies #485's Docs at release items: Remove from Local files, Exit fullscreen, Open note in the Transcript panel, Set feed address off while offline, and the Download path warning.
chhoumann
added a commit
that referenced
this pull request
Oct 15, 2026
Name icon buttons by what a reader sees now that #485 gave them per-episode names, split the no-transcript paragraph into a list, scope the iPhone fullscreen sentence, and end phone note crops below the last whole block.
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.
Why
This is the last visual, phone and accessibility pass of the v3 redesign. It also shrinks the bundle, because the parallel units left little room under the 103,000 gzip budget. Each brief item is fixed or has a stated reason below.
What changed
pn-bare), one focus ring, one disabled look, oneisPlayinghelper, onerevealfor both panels, and each component's styles nested under its own root. A tooling test keeps each style owner in one component, puts every:hoverbehind(hover: hover), and forbidsaria-labelwithtitleon one element.Scope
Item by item
removePreviousStylesruns first inonload, before any view mounts.:hoverrule in the view.hide(), inside the Escape, not inonHide, which runs after the 150 ms sheet animation..pn-filetileCover.aria-labelandtitle.pn-bare, onebutton:focus-visiblerule,isPlaying, the tooling test, nopn-queue.Size
Clean builds,
npm run size, gzip level 9.reveal, 0.4 disabled, stage scope, search room)The -200 aim is missed by 53 bytes. Most of it is the transcript follow rule (+144 over two rounds), which a reviewer showed must account for the current row and both fades.
Mutations
Standing order 32. Each mutant fails at least one test.
onunload; cleanup afterregisterView; cleanup after the firstawait; also removing othersvelte-*sheets. The last three fail only the unit test, because Obsidian mounts restored views afteronloadreturns.Verification
Node 22.23.2, Obsidian 1.14.4 (isolated runner, Linux).
npm run lint,format:check,typecheck,build,docs:buildpass;npm run test2,729 tests andnpm run test:tooling35 tests pass on the head.npm run test:e2eonce on a fresh vault: 219 of 219 tests in 30 files (882200e). The round-3 commits after it touch two e2e files, which pass (settings-tab 9 of 9 from both diarization states; journey-transcript and view-polish 12 of 12).tests/e2e/update-styles.test.tsloads the 2.21.1 release bundle (pinned SHA-256), updates in place to this build the way Obsidian's updater does, and checks in the main window and a popout that the earlier sheets are gone, another plugin's sheet stays, and the mini player's Play button matches a fresh load. It fails on master.Each image is master on the left and this PR on the right.
Fullscreen: entered, after the first Escape, after Exit fullscreen. Saved transcript note: Open note offered, note opened. Phone drawer: folded Save and 44 px Edit.
Follow-ups
TranscriptionServiceboth resolve the transcript note path; the service could expose "open the existing note" to both.isPlayinglives insrc/ui;src/player,src/APIand the media session keep their own copies.#heading); that is the note template.screens.mjs setends in phone emulation, and a latertest:e2eon that instance fails at once.Docs at release
faq.md,local_files.md,podcasts.md: "Remove from Local Files" becomes "Remove from Local files".Note
Finish phone layout, accessibility, and style-cleanup pass for
PodcastViewFinal polish pass over the podcast player UI.
Panel.revealhelper that scrolls the current chapter or transcript row into the visible band without scrolling surrounding content; transcript find matching also uses a newfoldCaseutility so dotted capital I matches lowercase queries.PlayerandMiniPlayerto the sharedCovercomponent via the newtileOfhelper, and addsisPlayingso a loading player counts as playing.aria-labelledbynames from their rows, transcript save buttons keep labels when visually collapsed, nativetitletooltips are removed, and Escape in menus restores focus immediately.PodNotes.onloadnow removes stale style sheets from prior builds viaremovePreviousStyles.(hover: hover)devices, reduced-motion now suppresses all view transitions, the unavailable episode row no longer gets 45% opacity,flairclasses are renamed topn-flair, thepn-queueclass is removed fromQueueList, and transcript panels open an existing note instead of saving a duplicate.Macroscope summarized 6f73df0.