Skip to content

Conversation

@jbecke
Copy link
Contributor

@jbecke jbecke commented Dec 20, 2025

Screen.Recording.2025-12-19.at.8.05.44.PM.mov

PR Summary (implementation details)

Focus brackets

  • Updated js/app/packages/app/index.css to restore bracket corner thickness by changing the conic-gradient anchor offsets from 1px → 1.5px in:
    • @utility util-bracket-bg-image
    • @utility bracket-*

Unified list selection highlight overlays split borders

  • In js/app/packages/app/component/UnifiedListView.tsx:

    • Moved the selection highlight layer out of the clipped list content using ScopedPortal scope="split".
    • Added coordinate conversion selectionRectInSplit to translate from unified-list-local coords → split-panel-local coords via getBoundingClientRect().
    • Expanded the rect by 1px on each side so the border can render over the split chrome.
    • Switched the border from outline (draws outside box) to inset box-shadow (prevents 1px overflow).
    • Background fill is a computed 2.5% accent via color-mix.
    • Added subtle diagonal pattern overlay inside the highlight only (pattern-accent pattern-diagonal-4 at opacity-[0.03]).
    • Removed width transitions from the highlight (transition-[transform,height,opacity]) so layout-driven width changes never animate.
    • Added effects to keep highlight aligned:
      • Recompute on split resize + preview toggle via splitContext.panelSize + preview()
      • Recompute after list content changes (entities_()?.length) using requestAnimationFrame to measure post-reflow.
  • In js/app/packages/app/component/split-layout/components/SplitContainer.tsx:

    • Added relative to the split container root so the portaled highlight can be positioned correctly.

Suppress bracket focus on resize gutter

  • In js/app/packages/core/component/Resize/Resize.tsx:
    • Added bracket-never to the focusable gutter separator to avoid bracket focus artifacts on the split divider.

“E to mark done” dopamine swipe

  • In js/app/packages/app/component/SoupContext.tsx:

    • Implemented Superhuman-style behavior for the e hotkey:
      • On each press, immediately advances selection, applies data-done-swipe="true" to the row DOM (.everything-entity via [data-entity-id]), and queues entities for commit.
      • Commits mark_as_done in a debounced batch (DONE_COMMIT_DEBOUNCE_MS = 250ms) so gaps can accumulate while mashing.
      • Prevents double-navigation by suppressing the action handler’s internal navigation when the hotkey already navigated.
    • Added gating so swipe+gaps only happen when the item will actually disappear from the current list:
      • notificationFilter === 'notDone' (Signal/Noise)
      • Email + inbox (uses optimistic exclusion)
      • Otherwise falls back to normal mark-done behavior (no swipe).
  • In js/app/packages/app/index.css:

    • Added row animation styles for .everything-entity[data-done-swipe="true"]:
      • entity-done-swipe: full swipe left off-screen with fade
      • entity-done-sheen: subtle accent sheen sweep
      • Tuned timing to be more visible (~260ms with a small initial hold)

@jbecke jbecke requested a review from a team as a code owner December 20, 2025 01:07
const hasPaid = useHasPaidAccess();

const isSoupActive = createMemo(() => {
const _isSoupActive = createMemo(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this isn't needed just remove it.

Comment on lines +126 to +170
const renderMnemonicLabel = () => {
const mnemonicMap: Partial<Record<ViewId, string>> = {
files: 'd',
people: 'm',
email: 'e',
tasks: 't',
agents: 'a',
folders: 'f',
all: '/',
};
const key = mnemonicMap[value];
if (!key) return <span class="truncate">{label}</span>;

const strong = (ch: string) => (
// Use an inset shadow "underline" so it doesn't get clipped by `truncate` overflow
// AND doesn't change the line box height (unlike border/padding).
<span class="inline-block font-semibold leading-none shadow-[inset_0_-2px_0_0_currentColor]">
{ch}
</span>
);

// If the mnemonic exists inside the label, underline that letter.
const idx = label.toLowerCase().indexOf(key.toLowerCase());
if (idx >= 0) {
return (
<span class="truncate">
{label.slice(0, idx)}
{strong(label[idx]!)}
{label.slice(idx + 1)}
</span>
);
}

// Otherwise, prefix the mnemonic (e.g. "C Msg", "/ All").
return (
<span class="truncate">
{strong(key)}
<span class="opacity-70"> </span>
{label}
</span>
);
};

const isAfterAll = () =>
i() > 0 && props.list[i() - 1]?.value === 'all';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please pull this logic out so it isn't inlined.

Comment on lines +254 to +260
{props.tabAddon?.({
value,
label,
index,
active: isActive(),
triggerEl: ref,
})}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throw this in a show so that props.tabAddon is always truthy

Comment on lines +455 to +510
const _DONE_SWIPE_MS = 200;
const DONE_COMMIT_DEBOUNCE_MS = 250;
let doneCommitTimeout: number | undefined;
let suppressMarkDoneNavUntil = 0;
const pendingDoneEntities = new Map<string, EntityData>();
const pendingDoneRows = new Map<string, HTMLElement>();

const shouldUseDoneSwipe = () => {
// Only do the Superhuman-style swipe+gaps when the current view will actually
// remove the entity from the list. Otherwise it looks broken (swipe out + pop back).
const view = viewData();
if (view?.filters?.notificationFilter === 'notDone') return true;
// Email inbox explicitly opts into optimistic exclusion on `e`
if (selectedView() === 'email' && emailView() === 'inbox') return true;
return false;
};

const triggerDoneSwipe = (entityId: string) => {
try {
const escaped =
typeof CSS !== 'undefined' && 'escape' in CSS
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
(CSS as any).escape(entityId)
: entityId.replaceAll('"', '\\"');
const leaf = document.querySelector<HTMLElement>(
`[data-entity-id="${escaped}"]`
);
const row = leaf?.closest<HTMLElement>('.everything-entity');
if (!row) return;
if (row.dataset.doneSwipe === 'true') return;
row.dataset.doneSwipe = 'true';
pendingDoneRows.set(entityId, row);
} catch {
// noop: animation is best-effort
}
};

const scheduleCommitDone = () => {
if (doneCommitTimeout) window.clearTimeout(doneCommitTimeout);
doneCommitTimeout = window.setTimeout(() => {
const entities = Array.from(pendingDoneEntities.values());
pendingDoneEntities.clear();

// Keep rows invisible briefly while the list updates, then clean up if they remain.
const rows = Array.from(pendingDoneRows.values());
pendingDoneRows.clear();

actionRegistry.execute('mark_as_done', entities);

window.setTimeout(() => {
for (const row of rows) {
if (row.isConnected) delete row.dataset.doneSwipe;
}
}, 400);
}, DONE_COMMIT_DEBOUNCE_MS);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please pull this out

Comment on lines +1 to +6
import type { CombinedRecipientItem } from '@core/user';
import { clamp } from '@core/util/math';
import { Combobox, type ComboboxTriggerMode } from '@kobalte/core/combobox';
import {
type Accessor,
createEffect,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a wrapper around the existing recipient selector.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants