Skip to content

v1.1.1

Choose a tag to compare

@jordibrouwer jordibrouwer released this 15 Aug 21:30
· 39 commits to main since this release

Keyboard consistency, and the parts of the app that teach it. Flagged hideFromModal in the index: it counts toward the version number and shows in Config → Overview → Latest update, but does not reopen the What's new modal, which still leads with v1.1.0. DASHBOARD_RELEASE and NEXTDASH_WHATS_NEW_DATA_VERSION are deliberately unchanged — those tokens are what re-show the modal.

Keyboard

  • new — the Shift+letter family is closed: Shift+E inline edit and Shift+V preview join move, tag, delete, checking, pin, share and reveal. ; and [ still work, undocumented, the way 0 still opens the inbox. Brackets mean previous / next sub-tab in config, and one pair of keys meaning two things was the reason to move preview off [.
  • new — share moves to Shift+L, so Shift+S always opens config. It was the only key whose meaning depended on whether a row was selected, which took two cheat-sheet rows and a parenthesis to explain. The capture-phase handler still declines rather than swallows a key it has no row for — that is what keeps Shift+S reaching setupPageShortcuts over a selected row, and tests/shift-key-row-actions.spec.js (renamed from shift-s-two-meanings.spec.js) pins it.
  • new — bare letters act on keydown. C_HOLD_MS and the g-chord hold are gone, and with them the whole keyup handler in keyboard-navigation.js and SearchComponent.addShortcutLetter, which had no other caller. _activateGChordMode now runs on the first g; G_CHORD_MS (3s) is the only timer left.
  • newj/k alias ArrowDown/ArrowUp in the grid, matching config's section rail and bookmark list.
  • newShift+Home focuses the category header via focusCategoryHeader(), which reuses resolveFocusedCategoryEl. Delete on a focused header calls categoryMenu.runAction('delete', …) instead of opening the menu; Shift+F10 opens it.
  • fixhandleContextMenu took clientX/clientY at face value, which a keyboard-raised contextmenu reports as 0/0. menuPointFor() falls back to the row's rectangle; the category menu's own handler does the same.
  • fixresolveFocusedCategoryEl took an options argument: :width keeps the first-category fallback because the palette names what it acts on, Shift+W passes fallbackToFirst: false and falls through.
  • fixsearch.js kept a code-name list (KeyM/KeyD/KeyT/KeyB) that had fallen behind the keys added since; it tests /^Key[A-Z]$/ now.
  • fixsetupPageShortcuts was the only typing guard in the app without an isContentEditable check.

Discoverability

  • newDashboardContextMenu renders a kbd.move-popover-item-key per entry with a matching aria-keyshortcuts, for all eleven actions that have a key. ShortcutFormat.ariaKeys() and .modifierLabel() are shared with the category menu, which had the chips but no aria.
  • newsyncShortcutAriaHints() stamps aria-keyshortcuts on the toolbar, the header buttons and the first nine page tabs. It runs independently of showShortcutTooltips: the tooltips are a desktop hover affordance that can be switched off, this is the only route the keys have to assistive technology. shortcutButtonDefs() replaces the three parallel lists that let the header row end up with tooltips and no aria.
  • fix — the side-rail legend printed the what's-new button's glyph in the same chip as real keys.

Cheat sheet

  • fixbuildPrintSections passed each row's printFallback to the caller's label resolver as a fallback, and generate-cheatsheet.cjs resolves from locales/en.json, where the i18n check guarantees every cheatKey exists. The short wording therefore never reached paper for any of the 23 rows that carry one. printLabel() uses it directly. validate-cheatsheet-registry.cjs gains a check built with the generator's resolver — with the identity resolver the other checks use, a printFallback wins either way and the check would pass against the bug.
  • new — printed rows for k/j, Shift+E and Shift+Home; nextDash-cheatsheet.html, nextDash-cheatsheet.pdf and the static/ copy regenerated.

Config

  • newlogFilterActivity is a fourth value for the log viewer's Show control, matched on Source == "activity" by logEntryMatchesFilter() rather than on severity. Activity lines already flowed into serverLogSink through log.Printf("activity: …") and parseServerLogLine already split the subsystem out; nothing about capture or NEXTDASH_ACTIVITY_LOG changes.
  • newHelp → About carries the wordmark and three addresses: nextdash.cc, GitHub, and jordibrw.nl. The help-signature sign-off is dropped, so each address appears once.

Dashboard

  • fix.category-title centres its flex items instead of aligning them on the baseline. The + button hangs padding and a border below its own baseline, which made every header carrying it 1.44px taller; centring costs nothing, where a line-height tall enough to swallow the button added 3px to every header. .category-title--multiline keeps flex-start.
  • fix* carries the same guards as ! and ., with an exception for its own modal so it still closes the recent list.

Analytics

  • newsettings-snapshot widens from 21 to 45 fields and a new content-snapshot reports install size (bookmarks, pages, categories, tags, finders, collections, monitored, inbox totals). Every value is bucketed client-side; MAX_EVENT_PROPS = 50 is Umami's per-event limit, which is why it is two events. analytics_content.go counts server-side and returns "" when telemetry is off — analytics_content_test.go proves it with a nil store, where any counting would panic.
  • fix — a custom theme reported its own id, which is theme-<base36>-<4 chars> and random per install: not a name leak, but distinctive enough to follow one install across releases. It reports custom.

Docs

  • MANUAL.md and README.md follow the moved keys, the G chord, the category-header keys, the right-click menu chips, the log viewer's Activity only and the About tab.
  • locales/{en,nl,de,fr}.json: the cheat-sheet rows for the moved keys, two new rows (navCategoryHeader, bmCategoryDeleteKey), a rewritten config.helpKeyboardBody, keyboardFixedNoteGridNav without the hold, logLevelActivity, logActivityHint, helpAboutBody, helpSiteProject, helpSiteAuthor.
  • static/nextdash-wordmark.png is keyed out of logo-ascii-on-black-large.png by greenness rather than brightness; the existing logo-ascii-transparent.png clears only pixels darker than 40, which leaves the glow behind the middle of the word as a dark ellipse.
  • New: tests/dashboard-keyboard-uniformity.spec.js, tests/config-help-about.spec.js, TestServerLogSinkActivityFilter. Updated: the c-hold and G-chord cases in tests/create-page-category-from-dashboard.spec.js and tests/dashboard-grid-shortcuts.spec.js, the category-menu chip case in tests/dashboard-nice-to-haves.spec.js.
  • go generate ./... for the changed CSS/JS.

v1.1.0 — 15 August 2026

The first minor since 1.0: a category can be wider than one column. DASHBOARD_RELEASE and NEXTDASH_WHATS_NEW_DATA_VERSION are bumped, so the modal reopens once.

Categories across columns

  • newCategory.Spread (spread in bookmarks-{page}.json) marks a category as allowed to run across grid columns; uncategorized and the smart collections keep theirs in settings.categorySpreads[pageId][categoryId], the shape categorySortModes already uses. dashboard-category-span.js owns both, so no caller has to know which applies.
  • new — the width itself is derived, never stored: spanForCount is min(ceil(count / categoryItemLimit), effectiveColumns, 12). It was a number the user picked in the first draft; it became a switch because the number is implied by two settings that already exist, and a stored number goes stale the moment a bookmark is added.
  • newapplyCategoryItemLimit multiplies the limit by the span, so a spread category shows its limit once per column and stays the height of its neighbours. refreshAllCategorySpans runs from finishIncrementalRefresh, which is what makes an added bookmark bring the next column with it rather than waiting for a reload; settleSpanChange follows that through to the "+ N more" cut and, in packed mode, to a full re-render.
  • new — the two settings rule out each other's extreme: spreadUnavailableReason returns unlimited-items when categoryItemLimit is 0, and the config select disables Unlimited while anySpreadCategory holds. Both say why where they are asked for.
  • new — routes: DashboardCategoryMenu (entry flips to Back to one column, no aria-checked beside a label that already flips), Shift+W in keyboard-navigation.js, :width on|off|all, a data-cat-spread button per row in the categories editor, and Appearance → Layout → Categories across columns for the defaults and the reset.
  • new.category--wide spans with grid-column: span var(--category-span) and repeats the row's track pattern once per column, so subgrid alignment survives. The inner column gap is the grid gap plus 2 × --category-inline-pad: a run of categories pads every column on both sides, a single wide box only its outer edges, and without that the inner columns sat 16px left of the grid. syncWideColumnTracks pins the shortcut and lead tracks, which are intrinsic and were sized per repeat by whichever rows happened to land in them.
  • new — a rule under the header spanning the block, and a ↔N badge in the header, both dropped while the category is collapsed or one column wide.

Packed columns

  • new — packed keeps its round-robin flex columns while nothing spreads, and switches to a grid (packed-masonry) the moment something does: --masonry-span rows per category from dashboard-packed-masonry.js, grid-auto-flow: row dense, a ResizeObserver per category. Bands were tried first and left a hole the height of the tallest column beside the wide block.
  • fixsyncCategoriesFromDom read the columns in document order while the render filled them round-robin. Those are not each other's inverse, so every category drag rewrote the order into one that redistributed differently and scrambled the page. readCategoryElementsInOrder is the single inverse now, keyed on what is in the DOM, and getExistingCategories delegates to it.
  • fixsyncDashboardGridLayout rebuilds className wholesale and dropped packed-masonry, so any settings refresh — including the one behind a window resize — left the categories as bare flex children of a row with no columns in it.

Config

  • fix — the location memory was written only on the exits config knows about; the header's health, inbox and page buttons switch view around it. restoreConfigHash now saves on every move inside config and setActiveView stamps it on the way out. The TTL is 5 minutes (DashboardConfig and DashboardConfigLoader both), counted from leaving rather than from the last click inside.
  • newtype: 'action' panel controls with bindPanelActions, used for Turn spreading off everywhere; NEW_THIS_RELEASE is the one place naming where the twinkle points, drawn on the section, the sub-tab and the panel.
  • new#category-context-menu sizes to its content (width: max-content, capped at 24rem). The shared .move-popover cap is for the move/tag/delete pickers; at 16rem the widest row here needed 274px and got 237, so the Shift+W chip was trimmed away — and the French label is half again as long.

Config → Bookmarks

  • new — the section gets the sub-tab strip five others already have: List and Settings. The nine settings sat after the list — fifty rows down by default and up to five hundred as the infinite scroll loads more, which also rules out jumping to the bottom of a list whose bottom moves as you approach it. Registered in SUB_TABS, SUB_TAB_STATE, SUB_TAB_ATTR and SUB_TAB_SECTION, so the deep link, the remembered location and the arrow-key walk come for free; handleOverviewGo accepts bmTab.

Views

  • new — Health, Inbox and Config → Bookmarks draw one card from feed-row.css instead of three copies. The rule was written out in health-view.css, dashboard-inbox.css and config-view.css and the copies had drifted into being byte-identical — one design, three places to change it, and no way to tell whether a difference was meant. What genuinely differs is a modifier: feed-row--with-select for Health's checkbox column, and the coloured left edge each view uses for its own state, including modern layout's inset redraw of it. Classic layout also gains the row focus ring only modern had.
  • new — the shape the three views disagreed on is settled: Inbox rounded its filter pills and buttons where Health squared them off; rounded won. Config → Bookmarks reads as a view rather than a settings panel — a header with a count, the search box sized like Health's, and tiles matching Health's without the stripe.

Discoverability

  • newspread-notice.js (a NoticeCard) and spread-tutorial.js, a four-step AppModal walkthrough in the shape of the inbox and health tours, reachable afterwards from Help → Pages & categories.
  • fixNoticeCard bound an action with querySelector, and a card naming its × through dismissName puts that attribute on the × as well. The × comes first, so it took the handler and the button sharing its name got none: the side rail's No thanks had been dead since the cards were unified.

Docs

  • MANUAL.md — the spreading section with the columns-per-bookmark-count table, and the config-memory paragraphs at five minutes. Config → Bookmarks is described with its two sub-tabs.
  • README.md — the feature bullet, Shift+W, :width on|off, and the five-minute memory.
  • CHANGELOG.md, static/data/whats-new/v1.1.0.json, index.json, whats-new-stub.js, tests/whats-new-hidden-release.spec.js, the four locale files, and go generate ./... for asset_hashes_gen.go.