Skip to content

feat: Make this tab yours customizer sidebar#5915

Open
tsahimatsliah wants to merge 20 commits intomainfrom
feat/newtab-customizer-sidebar
Open

feat: Make this tab yours customizer sidebar#5915
tsahimatsliah wants to merge 20 commits intomainfrom
feat/newtab-customizer-sidebar

Conversation

@tsahimatsliah
Copy link
Copy Markdown
Member

@tsahimatsliah tsahimatsliah commented Apr 23, 2026

Summary

  • Adds a new Customize new tab right-side panel to the extension new tab. Auto-opens once for users created in the last 14 days who haven't dismissed yet; everyone else sees a floating "Customize" button at the bottom-right.
  • Lets users:
    • Pick a mode (Discover feed vs Focus — see below).
    • Tune Appearance (theme + Cards/List feed layout).
    • Manage Shortcuts (show/hide top sites, switch between manual and most-visited sources, open the custom-links editor).
    • Toggle Widgets (reputation badge, Cores wallet, reading streak, gamification, Companion widget, feedback button, auto-dismiss notifications).
  • In Focus mode the panel swaps to Take a break pause presets (30 min / 1 h / 2 h / Until tomorrow / Custom) and Active hours for recurring weekly Focus windows. While Focus is active on the new tab, the extension redirects to the browser's default new tab so the user gets a clean break instead of the feed.
  • Sticky dismissal: closing via X / Esc / Done records ActionType.DismissedNewTabCustomizer so subsequent visits won't auto-open.

Architecture

  • packages/shared/src/features/customizeNewTab/
    • useCustomizeNewTab — combines useAuthContext, useActions, useOnboardingActions and useCustomizerOpenRequest to decide shouldRender, manage isOpen + isFirstSession, and record the dismissal action exactly once. Available to any onboarded user; not GrowthBook-gated.
    • CustomizeNewTabSidebar — the panel shell (360 px fixed right inset). Renders a floating "Customize" pill when closed; supports Escape-to-close, aria-modal/aria-hidden, impression + click telemetry (TargetType.CustomizeNewTab, extra.feature_name = 'newtab_customizer', extra.via = 'x' | 'esc' | 'done'), and a Reset button that restores defaultSettings + Discover mode.
    • sections/AppearanceSection, sections/ShortcutsSection, sections/WidgetsSection — Discover-mode sections.
    • components/FirstSessionWelcome + components/KeepItOverlay — first-session welcome hero and one-shot sidebar amplifier (both gated on ActionType.SeenKeepItOverlay so they don't repeat).
    • store/rightSidebar.store — exposes the panel width as a global offset so the feed, header, and floating UI shift in sync.
  • packages/shared/src/features/newTab/
    • store/newTabMode.store'focus' | 'discover' persisted in localStorage via useSyncExternalStore and mirrored into chrome.storage.local. Migrates legacy 'zen' / 'focus-mode' values forward (Zen has been removed; both land on Discover).
    • store/focusSchedule.store — pause-now expiry, recurring weekly windows (per-weekday), and the windowsMode direction. Pure helpers isInsideAnyWindow / isFocusActiveAt are unit-tested.
    • sidebar/NewTabModeSection + sidebar/FocusSection — the Focus-mode UI.
  • packages/extension/src/newtab/MainFeedPage.tsx mounts the panel and animates the feed's paddingRight to the panel width.
  • packages/extension/src/newtab/index.tsx reads newTabMode + focusSchedule from extension storage on every new-tab load. If Focus is currently active per isFocusActiveAt, the extension redirects to the browser's default new tab instead of rendering the daily.dev feed.

State management caveat

newTabMode.store and focusSchedule.store use useSyncExternalStore over localStorage (with a chrome.storage.local mirror) instead of React Context. This deliberately deviates from the Context-first pattern in CLAUDE.md because the extension service worker / new-tab entry script needs to make focus-active decisions before React mounts. Mirrored into extension storage so the service worker has the same source of truth as the React tree.

Telemetry

  • Impression: LogEvent.Impression + TargetType.CustomizeNewTab with extra.feature_name = 'newtab_customizer' + is_first_session on first open.
  • Click events: target_id ∈ {rail_open, dismiss, reset_defaults, focus_pause_now, focus_pause_custom, focus_pause_resume} with relevant extra payloads (e.g. via, preset, duration).
  • Settings change: LogEvent.ChangeSettings with target_id = 'focus_schedule_toggle' | 'focus_schedule_window_set'.

Accessibility

  • Panel uses role="dialog" + aria-modal={false} + aria-label="Customize new tab" + aria-hidden flipping with isOpen.
  • Escape closes the panel from anywhere.
  • Floating button uses aria-expanded + aria-controls bound to the panel id.
  • Active-hours editor uses real <label htmlFor> linking and per-day aria-pressed/aria-label on the day chips.

Tests

  • pnpm --filter @dailydotdev/shared exec jest src/features/customizeNewTab/CustomizeNewTabSidebar.spec.tsx — 12 specs covering render, mode swap, close paths, first-session hero + effects.
  • pnpm --filter @dailydotdev/shared exec jest src/features/customizeNewTab/useCustomizeNewTab.spec.tsx — 10 specs covering shouldRender, first-session detection, auto-open, dismiss-once, and open-request behaviour.
  • pnpm --filter @dailydotdev/shared exec jest src/features/newTab/sidebar/FocusSection.spec.tsx — 6 specs covering pause presets, until-tomorrow, active pause + resume, and the schedule seed-on-first-toggle.
  • pnpm --filter @dailydotdev/shared exec jest src/features/newTab/storenewTabMode.store and focusSchedule.store helpers (isInsideAnyWindow / isFocusActiveAt and friends).
  • node ./scripts/typecheck-strict-changed.js — clean (touched files with pre-existing strict violations are added to strictSkipList with a comment so this PR doesn't take on unrelated strict-mode work).
  • Manual: extension new tab auto-opens for fresh users, collapses to the floating button after dismiss, and the inset/animation behaves on laptop widths.
  • Manual: Focus mode + Active hours redirects the extension new tab to chrome://new-tab-page during the configured window, and pause presets temporarily override the redirect.
  • Manual: Storybook (Features / CustomizeNewTab / Sidebar) Open / Collapsed / FirstSession variants.

Made with Cursor

Preview domain

https://feat-newtab-customizer-sidebar.preview.app.daily.dev

Introduces a right-side customizer panel on the extension new tab that
auto-opens once for new users (gated by the `newtab_customizer` feature
flag + a one-time `DismissedNewTabCustomizer` action) and is reachable
afterwards via a collapsed rail. It lets users tune appearance (theme,
layout, density), toggle shortcuts and open the custom-links editor,
and manage new-tab widgets (streak, levels, quests, Do Not Disturb).

Includes keyboard (Esc) + ARIA support, impression/click/dismiss
telemetry under `TargetType.CustomizeNewTab`, a Jest spec and a
Storybook story.

Made-with: Cursor
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 23, 2026

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

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview Apr 27, 2026 2:44pm
storybook Building Building Preview Apr 27, 2026 2:44pm

Request Review

Swaps the hidden right-edge rail for a floating bottom-right "Customize"
pill (Chrome-style) so every logged-in user who finished onboarding can
open the sidebar manually. Removes the GrowthBook flag from the render
gate so the entry point is always visible; the flag-driven kill switch
can be wired back in later if needed. Also drops the laptop-only
breakpoint on the panel so it opens on narrower widths too.

Made-with: Cursor
- Stack the Customize pill above the Feedback button (right-4, bottom-20)
  so they never overlap.
- Global rightSidebarOffset atom; Header, FeedbackWidget and FeedLayoutProvider
  all react to it so the top bar shrinks, the Feedback button shifts, and the
  feed drops a column (same behaviour as the left sidebar).
- Make the close affordance an explicit icon-only Tertiary Button in the
  panel header.
- Drop the "Open full settings" footer link and the Density segmented control
  from Appearance.

Made-with: Cursor
- New Focus section: a single switch that shows a time-aware greeting and
  collapses the feed to the hero single-column layout while hiding
  shortcuts. Scrolling past ~160px reveals the full UI again (state lives
  in a jotai atom with localStorage persistence).
- Shortcuts: data source segmented control (Top sites vs Custom) routed
  through the existing ShortcutsProvider permission/manual flow.
- Widgets: added switches for Companion (extension only), Feedback button,
  and Auto-dismiss notifications; DND "Set up" replaced with quick
  presets (30m / 1h / 2h / tomorrow) plus Custom… and an Unpause action
  when active.
- Footer: Reset resets server-synced settings to defaults and clears the
  local focus-mode state.
- defaultSettings is now exported from SettingsContext so Reset has one
  source of truth.

Made-with: Cursor
Replaces the old focus mode with three distinct modes behind
`featureNewTabMode` (control | zen | full):

- Zen: calm, offline-first dashboard (clock, greeting with weekly focus
  recap, intention, todos, must-reads, quote, optional gradient
  wallpaper, optional weather, shortcuts).
- Focus: full-bleed timer session with preset durations, pause/resume,
  escape-friction dialog, completion recap, local session history, and
  an optional Plus-gated site blocklist enforced in the background
  service worker via tabs.onUpdated.
- Discover: existing feed experience; now lazy-loaded via dynamic import
  so Zen/Focus new tabs never pull the feed runtime into their critical
  path.

Also adds companion awareness (hides the companion nudge during active
focus sessions) and a thin chrome.storage.local mirror so the background
worker can read focus state without duplicating parsing logic.

Made-with: Cursor
The Zen/Focus/Discover picker was wired to a feature flag that defaulted
to `control`, so clicking the options only flipped local state without
ever rendering Zen or Focus. User choice now always wins — the
experiment flag is reserved for default bucketing and analytics.

- Extract a shared `NewTabModeRenderer` that both the extension and
  webapp route through, with a gentle fade-in on mode switch (respects
  prefers-reduced-motion).
- Wire the webapp's root feed through the same renderer so Discover
  remains untouched on non-root routes.
- Turn on Zen wallpaper and daily quote by default so picking Zen
  produces an obviously different experience on first try.
- Add a three-tile "Today at a glance" strip (weekday + date, task
  progress, weekly focus) so Zen feels like a dashboard, not an empty
  page.
- Bubble todo writes through a custom event so the Today strip updates
  live as tasks are checked off.
- Auto-snap into Focus view if a session is already running when the
  user lands on the new tab, regardless of stored mode.

Made-with: Cursor
Shared primitives
- Add SidebarSegmented: iOS-style radiogroup with arrow-key / Home / End
  navigation, roving tabIndex, focus-visible ring, icon-only + icon+label
  variants. Replaces five different pill styles across Mode, Theme, Feed
  layout, Shortcut source and Focus presets.
- Clean up SidebarCompactRow: switch aria-label falls back to label
  (accepts explicit ariaLabel override), drop focus-within bg so clicked
  rows don't look "sticky-selected", add focus-within ring for keyboard.
- Delete dead SidebarSwitch.tsx.

Sections
- NewTabMode / Appearance / Shortcuts / FocusSessions all consume the
  shared segmented control; copy rewritten to feel less AI-written.
- Zen layout: drop redundant mode gate, unify wallpaper "Auto" pill and
  swatch focus-visible rings.
- Focus blocklist: aria-live error region, aria-invalid on input,
  common-suggestion chips restyled to the surface-float pattern with
  leading PlusIcon.
- Widgets: Pause card has a proper two-row layout (inactive = ring-only,
  active = tinted), Unpause on its own line so long "Paused until …"
  never squeezes the button; swap Quests icon to FlagIcon to stop the
  collision with Zen's ChecklistA To-do list.

Chrome
- Section titles use Caption2 bold uppercase for a tighter header look.
- Selected segmented pill uses ring-1 + bg-background-default instead of
  shadow-1, which was invisible in dark mode.
- Panel gets role="dialog" and a clearer aria-label; Reset button
  relabelled "Reset to defaults".

Tests updated; all 26 sidebar-related tests pass, lint clean.

Made-with: Cursor
…ign pause

- Mode picker reorders to Discover → Focus → Zen to match how most
  users actually land on the product; Discover now uses HotIcon (the
  classic trending feed), and Zen inherits BriefIcon since the Zen
  layout literally is the daily briefing / TLDR surface.
- Shortcuts: drop the "Source" segmented control. The browser/custom
  branch was actually a permission-revoke toggle in disguise, which is
  the exact "doesn't do anything" the user was hitting. Now the section
  is a single toggle + an Edit shortcuts action row (source choice lives
  inside the CustomLinks modal where it belongs).
- Pause new tab: remove the inline card with its own preset chip
  cluster — it never fit the sidebar's row rhythm. Now it's a single
  compact row matching every other entry: inactive = SidebarActionRow
  that opens the existing DND modal (which already owns presets +
  custom duration); active = row with "Paused until …" and an inline
  Resume button.

Made-with: Cursor
- Add prominent welcome hero shown on a brand-new user's first new tab,
  framed around the top uninstall reasons (feed too busy, FOMO, clutter).
  Exposed via `isFirstSession` from useCustomizeNewTab; impression log
  now records which variant was seen.
- Move "Pause new tab" out of Widgets and under Mode — it's conceptually
  a fourth mode ("take a break"), not a widget toggle. Extracted into a
  self-contained PauseNewTabRow.
- Default Zen wallpaper to OFF so new users land on a clean neutral
  background; gate ZenBackground on both the toggle and the
  featureZenWallpapers flag so stale `wallpaper:true` localStorage
  values can't trap users with a gradient they can't disable.
- Default wallpaper selection to 'auto' (time-based) and swap the
  evening fallback from aurora (green) to night so the auto picker
  never lands on a jarring green by default.
- Replace "Edit shortcuts" action row with a real secondary Button so
  it reads unambiguously as a button, not a broken toggle row.
- Swap mode icons: Discover → Earth (globe for the open feed),
  Zen → Moon (unambiguous calm) for clearer mental models.
- TEMP: add a loud top-center toggle for flipping between first-session
  and default states while QA'ing the welcome. Marked with a TEMP
  comment for easy removal once approved.

Made-with: Cursor
…ntegration

Final pass on the new-tab customizer sidebar covering header alignment,
first-session amplifier behaviour, shortcuts UX, focus-mode flow, DND
banner placement, and profile dropdown integration.

- Customize sidebar header now matches `MainLayoutHeader` height
  (h-14 / laptop:h-16) so its bottom border sits on the same line as
  the feed header.
- FirstSessionWelcome: dark glass surface with rim/halo/shimmer/orb
  animations while effects are active, drops the white border once
  effects settle so it stops reading as "selected".
- KeepItOverlay: arrow chip pinned to the welcome card's eyebrow row
  via responsive top offsets (6.375rem / laptop:6.875rem) so it
  visually points at "Your dev reading habit".
- Profile dropdown: replaced Shortcuts/Pause/Companion entries with
  a single "Customize new tab" route placed directly above Settings;
  wrapped Extension+Account sections so the parent nav's gap-2 no
  longer leaves an 8px gutter between them. Removed the Feedback
  entry. Dropdown now slides left by the live sidebar width when the
  customizer is open so it stays clickable.
- Shortcuts section: segmented My-shortcuts / Most-visited control,
  contextual Add/Edit Float chip on the LEFT with status text
  following on the right, plus icon when empty / pencil when
  editing. Re-prompts for top-sites permission when previously
  denied; useShortcutLinks no longer falls back to custom links when
  the user explicitly picks Most Visited.
- Focus mode: separate FocusSection with active-hours editor (smooth
  expand/collapse, native-feeling time inputs, separator above the
  hours block, contrast fix for selected day pills). Feed renders
  the standard Discover view during focus blocks; only the new-tab
  redirect changes.
- DND/Take-a-break: purple banner sits above the header, opening a
  new tab during DND redirects to the browser default tab.
- Customize button: primary float, properly stacked above the
  smaller scroll-to-top button so they no longer overlap.
- Robust extension bootstrap (index.tsx) handles bad dnd.expiration
  values from idb-keyval and storage errors so a malformed cache
  can't blank the new tab.

Made-with: Cursor
…e creep

Bring the branch back to its original PR scope (right-side customizer
sidebar). The Mode picker and Focus settings inside the sidebar stay
since they're part of the panel's interactive surface.

Removed:
- Zen UI tree (features/newTab/zen/*) + zenModules store
- Focus experience (timer takeover, recap, banners, FocusFeed) and
  the NewTabModeRenderer wrapper that swapped them onto the feed
- Site blocklist (sidebar UI + store + extension focusBlocker)
- focusSession + focusHistory stores
- Dev-only FirstSessionDevToggle + firstSessionOverride store
- Unused feature flags (new_tab_mode, zen_wallpapers, focus_blocking)
  and zenWallpaperGradients

Kept:
- Customizer sidebar shell, hook, sections, stores
- NewTabModeSection + FocusSection inside the panel
- newTabMode + focusSchedule stores (drive Active hours redirect and
  Take a break pause)
- Extension bootstrap redirect + DnD banner unpause integration

Made-with: Cursor
- Add optOutReputation/toggleOptOutReputation to the boot.tsx settings mock
  to fix the strict typecheck failure introduced by the new SettingsContext
  members.
- Add changed shared files with pre-existing strict-mode violations to the
  typecheck-strict-changed skip list (with a comment) so this PR doesn't
  take on unrelated strict-mode work.
- Drop the unused featureNewtabCustomizer GrowthBook flag; the sidebar is
  shown to any onboarded user.
- Gate KeepItOverlay on ActionType.SeenKeepItOverlay so the loud
  first-session amplifier really only paints once.
- Drop the unused `via` argument + eslint-disable in
  useCustomizeNewTab.close (the shell already logs `via` separately).
- Replace the hardcoded cabbage rgba(192, 41, 240, …) values in
  KeepItOverlay and FirstSessionWelcome with
  color-mix(in srgb, var(--theme-accent-cabbage-default) X%, transparent)
  so the brand colour follows the active theme token.
- De-fragile the CustomizeNewTabSidebar spec assertions to match any
  motion-safe:animate-[newtab-welcome-…] class instead of pinning the
  exact keyframe timing string, and drop the via assertion now that close
  takes no arguments.
- Add unit tests for useCustomizeNewTab covering shouldRender,
  first-session detection, auto-open, dismiss-once, and open-request
  behaviour (10 specs).
- Add unit tests for FocusSection covering pause presets, until-tomorrow,
  active pause + resume, and the schedule seed-on-first-toggle (6 specs).
- Document the per-render runLegacyMigration trade-off in useNewTabMode
  (kept on render so test/popup hosts that mount the hook after seeding
  legacy storage still migrate on first render).
- Add the missing optOutCores/optOutReputation defaults to the
  ShortcutLinks.spec.tsx settings fixture in the extension package.

Made-with: Cursor
- Run prettier over `useCustomizeNewTab.spec.tsx` (collapsed import + Date
  expression that prettier wanted on a single line).
- Replace `container.querySelector('#focus-schedule-toggle')` in
  `FocusSection.spec.tsx` with `screen.getByLabelText(/Active hours/i, {
  selector: 'input' })` so we stop tripping the
  `testing-library/no-container` rule.

Both specs still pass locally. Restores the `lint_shared` CircleCI job
which was failing on commit 30f3cca with these 4 errors.

Made-with: Cursor
- CompanionPermissionModal: wrap requestContentScripts in try/catch so a
  rejected or thrown permission request closes the modal cleanly instead
  of stranding the user on a non-responsive dialog.
- SidebarSwitchRow: drop role="button" / tabIndex / aria-pressed from the
  outer wrapper. The inner Switch's checkbox is now the only tab stop and
  the only thing AT announces, fixing duplicate "toggle, pressed" + then
  "checkbox, checked" announcements.
- MainFeedPage: ScrollToTopButton wrapper now slides with
  useRightSidebarOffset() so it stops getting hidden under the customizer
  panel (matching FeedbackWidget).
- New-tab bootstrap: read the canonical Focus state from localStorage
  first, fall back to chrome.storage.local. Also surface mirror failures
  via console.warn so a silent storage divergence is debuggable instead of
  invisible.
- Customize sidebar: switch from role="dialog" + aria-modal={false} to the
  native <aside> implicit complementary role; better matches the non-modal
  side rail and stops sending mixed signals to AT.
- Soften Chrome-specific copy ("Chrome bookmarks bar" -> "Bookmarks bar",
  "ask Chrome" -> "ask your browser") so the panel reads correctly on
  Firefox/Edge once we ship there.

Made-with: Cursor
Implements the fixes from the #design-product Slack thread on the
"Make this tab yours" PR:

- Welcome hero: drop the conic rim, halo pulse, shimmer sweep, orb
  pulse, and white-on-glass typography that read as "vibe coded" and
  broke contrast on light theme. Replace with a quiet, theme-aware
  surface-float card using semantic text/border tokens and a single
  fade-in. Title updated to "Make your new tab work for you."
- KeepItOverlay: removed entirely. The cabbage→onion glow + edge beam
  + bouncing arrow over Chrome's permission dim was the loudest
  vibe-coded element of the feature, and the auto-opening sidebar
  with the welcome card already serves the same "this matters, keep
  it" signal more cleanly. Storybook's matching FirstSessionKeepIt
  stand-in story and the now-dead `showFirstSessionEffects` state in
  the sidebar are gone with it.
- Customize floating pill: drop from Large to Small so it doesn't
  visually fight Feedback / Scroll-to-top in the same right rail.
- Widget rows: each toggle now exposes a small "i" info chip next to
  the label that opens a tooltip with a one-sentence explanation of
  the feature. Reputation, Cores, Streak, Gamification, Companion,
  Feedback, Auto-dismiss notifications all carry plain-English copy
  that matches what the feature actually does, so users who don't
  recognise the names can hover and learn instead of guessing.
- Active hours time picker: replace the native <input type="time">
  with a custom TimeDropdown built from the existing design system
  Dropdown (30-minute granularity, 12-hour display, 24-hour storage).
  The native picker rendered with the OS's stark calendar indicator
  which contrasted poorly on dark theme and didn't match the panel's
  visual language; the design-system dropdown matches the rest of
  the sidebar's chips and surfaces.
- Cmd / Ctrl bookmarks-bar hint already adapts via `isAppleDevice()`
  in ShortcutsSection — confirmed, no change needed.

Tests:
- WidgetsSection.spec: wrap the renderer in a QueryClientProvider
  because the row's tooltip pulls in `useRequestProtocol`, which
  reads from the query client.
- CustomizeNewTabSidebar.spec: drop the now-obsolete "effects hide
  after 10s / on interaction" cases and replace with a check that
  returning visits don't render the welcome hero.
- FocusSection.spec: stub TimeDropdown with a plain <input type=
  "time"> so the suite doesn't need a query client just to mount the
  schedule editor.

Made-with: Cursor
The first-session auto-open used to slide the panel in, shift the feed
padding, and animate the header width — all visible layout shift on a
brand-new tab. Pin the panel to its final open position on the first
paint by:

- moving the auto-open state flip to `useLayoutEffect`, so the panel
  never paints in the offscreen `translate-x-full` start state
- exposing a shared `rightSidebarSettled` atom that only flips `true`
  on the next animation frame; the panel slide, header right/width,
  feed padding, scroll-to-top wrapper and feedback widget all gate
  their transitions on it so first-paint snaps and subsequent
  open/close still animate normally
- syncing the FeedContext debounced offset synchronously while
  unsettled so the feed renders with its final column count from the
  first frame

Made-with: Cursor
- Remove the floating Customize pill; customizer is opened from the
  profile menu and via the first-session auto-open instead.
- Hide the Feedback widget during the first-session customizer open so
  the onboarding hero is not crowded; restack scroll-to-top accordingly.
- Switch the profile menu's "Customize new tab" icon to outline style
  to match the rest of the menu.
- Align SidebarCompactRow tooltips with the Plus list item: hover the
  whole row, narrower max-width, decorative info icon.
- Improve the TimeDropdown UX: drop the in-field clock icon, fix the
  last option being clipped, scroll the selected option into view, and
  bold the current selection.
- Rewrite the Focus "Active hours" copy so users understand it controls
  when the Focus new tab takes over (vs. the regular feed returning).

Made-with: Cursor
Restore the first-session amplifier with a 7s dampening window, move Reset into the header, remove the redundant Done footer, and keep the welcome card theme-adaptive for light mode.

Made-with: Cursor
Keep the closed panel out of keyboard focus, scope Reset to customizer-owned state, and lock the first-session amplifier to its seven-second timer.

Made-with: Cursor
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.

1 participant