Skip to content

feat(sidebar): add compact rail settings - #6567

Merged
rebelchris merged 24 commits into
mainfrom
codex/sidebar-api-settings-frontend
Sep 1, 2026
Merged

feat(sidebar): add compact rail settings#6567
rebelchris merged 24 commits into
mainfrom
codex/sidebar-api-settings-frontend

Conversation

@rebelchris

@rebelchris rebelchris commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add the layout v2 sidebar density picker and compact rail behavior
  • persist sidebar compact mode, shortcuts dock, and pinned/recent section state through server-backed settings flags
  • keep one-time migration paths for earlier local sidebar flag storage and the legacy shortcuts dock store
  • fix extension sidebar links that could resolve to chrome-extension:// paths

Test plan

  • NODE_ENV=test pnpm --filter @dailydotdev/shared exec jest src/contexts/BootProvider.spec.tsx src/components/sidebar/SidebarShortcutsDock.spec.tsx src/components/sidebar/sidebarLinks.spec.ts --runInBand
  • NODE_ENV=test pnpm --filter webapp exec jest tests/SourcePage.tsx tests/TagPage.tsx --runInBand
  • node ./scripts/typecheck-strict-changed.js
  • pnpm --filter @dailydotdev/shared lint

Preview domain

https://codex-sidebar-api-settings-front.preview.app.daily.dev

tsahimatsliah and others added 23 commits August 3, 2026 13:30
The API declares every accepted settings flag in SettingsFlagsPublicInput,
so `sidebarCompact` failed GraphQL validation — and since each settings
write ships the whole flags object, that one unknown key silently broke the
persistence of every setting alongside it. Hence "compact reverts on reload,
and so does whatever you changed next".

Flags the API can't store yet now live behind `clientOnlySettingsFlags`:
kept in local storage, stripped from the remote payload, and pushed up (once,
per user) the moment their field lands — so making them account preferences
is a one-line deletion. See docs/settings-flags-backend.md.

The shortcuts dock moves off IndexedDB into the same settings path for the
same reason, with a one-time lift of any existing device-local dock.

Also in the v2 sidebar:
- appearance page swaps the "Compact sidebar" switch for a picker that
  previews both densities, so the setting explains itself
- rail rhythm tightened: 4px -> 2px row gaps with 26px -> 24px glyphs, from
  one shared source instead of four hardcoded copies
- Presidential briefings moves from the Saved panel to Explore, under
  Happening Now

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- import ClientOnlyFlagKey — the missing type failed the Vercel production
  build; the changed-file strict guard doesn't run tsc over the whole package,
  so nothing local caught it
- run the IndexedDB→settings dock migration from the rail only. It sat in
  useSidebarShortcutItems, which also mounts in every squad row's pin button,
  so one migration fanned out into a burst of identical settings mutations
- clear the legacy dock store only after the settings write resolves, so a
  failed request leaves the pins for the next attempt
- correct the rail's tab-height constant to the measured 58px (was 52). The
  overflow math under-counted, so too few tabs folded into "More"
- treat a non-array shortcuts flag and a non-object flag store as empty; both
  now round-trip through JSON that this client doesn't own

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tion

The flag comment pointed at a `useClientOnlyFlagsMigration` hook that doesn't
exist (the migration is an effect in the provider), and the density picker let
a failed settings write reject unhandled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tions

The line between New post and the shortcuts "•••" measured 22px above and
14px below, despite a comment claiming it sat midway — New post carries its
own my-2, which the separator's my-3 then stacked on top of. Dropping the top
margin lands it at 10px on both sides.

Adds a Storybook gallery of ten directions for the appearance page's density
control, to replace the current pair of cards: they're taller than the setting
warrants, and the mocked feed pane inside each makes the settings sections
below read as part of the previewed sidebar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Round one's cards ignored the page's own idiom. These eight all sit in the
notifications-page row shape — title and description left, control right — and
every preview lives in a fixed-size slot, so toggling can't move the row the
way E did. Verified: title and control edges are identical before and after
toggling in all eight.

Adds an "In page context" story putting the strongest three between real
neighbouring rows, since placement was half the problem.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On the extension the sidebar renders from chrome-extension://<id>, so a
root-relative href lands on the extension origin and 404s. Some rows are
relative on purpose — on the new tab they switch the feed in place — and what
keeps them safe is the section rendering them as BUTTONS (`isNavItemsButton`
→ `isItemsButton`). v2 moved those rows from MainSection into the avatar panel
but rendered it with `isItemsButton={false}`, dropping that contract.

- avatar panel takes `isItemsButton={isNavButtons ?? false}`, as v1's
  MainSection does; its `isForcedLink` rows stay links either way
- shortcuts dock: the Following catalog entry was the one relative path among
  eleven absolute siblings
- pinning a panel row stored its path verbatim, so pinning a relative row
  saved a broken link — to the account, now that the dock syncs
- Recent pages record `router.asPath`; absolute on the extension only, so the
  webapp keeps client-side routing

Adds `sidebarLinks.spec.ts` (re-loaded against a real origin, since the test
env sets the webapp URL to '/') and docs/sidebar-links-extension-audit.md with
the full v1/v2 inventory.

Also adds the R treatments Storybook page for the density control.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Roughly half the comments this branch added were narrating what the line
below already says. Kept the ones carrying a constraint or a gotcha — the
API's flag whitelist, the extension's relative-href trap, the measured rail
constants, the migration's ordering — and deleted the rest. The per-variant
comments in the density stories duplicated the `note` strings those stories
already render.

Adds R7 to the density treatments: R4's purple tint with R5's check badge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dock migration read `flags.sidebarShortcuts` before settings had loaded,
so an absent value could mean "still in flight" rather than "nothing stored".
That is harmless today (the flag is device-local) and a data-loss bug the day
the API stores it: a dock synced from another device would be overwritten by
this device's IndexedDB leftovers. Gate on `loadedSettings`.

The client-only flag migration didn't catch its own write, so a failed request
surfaced as an unhandled rejection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…coped

The store behind the flags the API cannot hold yet had several problems that
only bite once one of those flags graduates, at which point they are data loss
with no server copy to fall back on.

- Writes replaced the whole store with whatever the caller's `flags` happened
  to carry, so any owner writing before the others had loaded silently dropped
  them (and a second tab's write lost the race). Writes merge now; the one
  place that must remove keys — retiring a graduated flag — is its own call.
- `loadedSettings` is cache presence, not settings freshness, so "the server
  has no value for this flag" was being read off last session's cache. The
  boot provider now exposes `isRemoteSettingsLoaded`, set where the response is
  written into the cached data (`isBootReady` flips a commit earlier, before
  the settings it belongs to have landed), and the migration waits for it.
- The store was device-wide, so a second login on the same machine inherited
  the first account's rail density and pinned dock. It is keyed per account
  now, with the pre-existing entry handed to the first signed-in account that
  loads and then deleted. A signed-out reader's copy is not carried over.
- The flags were read in an effect, which landed a render after the identity
  and settings they belong to: the rail painted labels for a frame before
  snapping narrow. They are derived during render instead.
- Every dock pin, unpin and reorder fired a full `updateUserSettings` whose
  only changed key was stripped from the payload, so the request was identical
  to the last one and a reorder drag sent a burst of them. A write whose
  changed keys are all client-only skips the remote call; it starts going out
  again by itself the day the field graduates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The hook's own comment explains why it must not run before the account's dock
has arrived: a dock synced from another device would be read as absent and
overwritten by this device's leftovers. `loadedSettings` never provided that —
it only says a cached boot exists — so it waits on `isRemoteSettingsLoaded`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two `aria-pressed` buttons describe two independent toggles, so a screen reader
announced "Comfortable, pressed" and "Compact, not pressed" as unrelated
controls with nothing tying either to the "Sidebar" heading beside them. This
is one setting with two values: a radiogroup labelled by that heading, with
native radios so arrow-key navigation and roving focus come for free.

Also swaps the check badge's raw `text-white` for the semantic token the same
badge uses elsewhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`compact` on the section family means "rendered inside a v2 rail panel", fixed
true from SidebarDesktopV2. Now that `sidebarCompact` is a user-facing density,
reading `!compact && briefUIFeatureValue` one concept away from "not in compact
density" is a trap. The prop has no consumers outside this folder, so it is
renamed rather than explained.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…bar-compact-c9b70f

# Conflicts:
#	packages/shared/src/components/sidebar/sections/RecentSection.tsx
The context panel animates its own width from 0 to 240px, and its
children stretch to that animating width, so everything inside is being
laid out again on every frame of the transition. List panels survive it
because their rows are left-aligned and simply get clipped, but the
streak panel is built from a 10-column calendar grid and justify-between
hero rows: the dot columns redistribute and the rows slide apart for the
length of the animation.

Pinning the content to the panel's open width leaves the layout settled
and lets overflow-hidden reveal it, which is what the other panels
already looked like they were doing. Settings keeps stretching, since
that panel legitimately fills the whole sidebar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rail's label-free density is the one we want accounts to land on, so
an unset `sidebarCompact` now reads as compact.

The flag stays three-valued rather than being flipped: unset means the
user never chose and gets the new default, while an explicit `false`
still means they picked Comfortable and keeps their labels.

All three readers go through one hook, because they have to agree
without looking related. The rail sizes itself from the flag, MainLayout
pads the page content to match that width, and the appearance settings
page renders the selected density. Two of them disagreeing puts the
content over the rail or leaves a gap beside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…debar-compact-c9b70f

Brings in the panel expand-reflow fix and the compact-by-default rail (#6497).

Two resolutions on top of the automatic merge:

- appearance.tsx: this branch already moved the compact switch into
  SidebarDensitySection, so #6497's hook swap on the old switch is dropped
  rather than reinstated.
- SidebarDensitySection now reads useSidebarCompact() instead of the raw
  flag, so the picker and the rail can't report different densities for an
  account that never touched the setting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Combines main's jobs feature gate (#6519) with this branch's move of the
shortcuts dock from device storage to settings flags: the dock and Recent
keep the jobs filter while reading and writing through the settings flag.
@vercel

vercel Bot commented Sep 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
daily-webapp Ready Ready Preview Sep 1, 2026 12:33pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
storybook Ignored Ignored Sep 1, 2026 12:33pm UTC

Request Review

@rebelchris
rebelchris merged commit 5509891 into main Sep 1, 2026
12 checks passed
@rebelchris
rebelchris deleted the codex/sidebar-api-settings-frontend branch September 1, 2026 13:23
tsahimatsliah added a commit that referenced this pull request Sep 1, 2026
Main moved the shortcuts dock onto settings flags (#6567) and gave the
rail its own Home button (#6572). The dock's "storage has answered"
signal the pin coach waits on is now isRemoteSettingsLoaded, and the
rail's shared row gap wraps the dock container the tour passes props to.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants