-
Notifications
You must be signed in to change notification settings - Fork 3
feat(list): dopamine like Superhuman when j k e
#735
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
| const hasPaid = useHasPaidAccess(); | ||
|
|
||
| const isSoupActive = createMemo(() => { | ||
| const _isSoupActive = createMemo(() => { |
There was a problem hiding this comment.
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.
| 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'; |
There was a problem hiding this comment.
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.
| {props.tabAddon?.({ | ||
| value, | ||
| label, | ||
| index, | ||
| active: isActive(), | ||
| triggerEl: ref, | ||
| })} |
There was a problem hiding this comment.
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
| 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); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please pull this out
| import type { CombinedRecipientItem } from '@core/user'; | ||
| import { clamp } from '@core/util/math'; | ||
| import { Combobox, type ComboboxTriggerMode } from '@kobalte/core/combobox'; | ||
| import { | ||
| type Accessor, | ||
| createEffect, |
There was a problem hiding this comment.
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.
Screen.Recording.2025-12-19.at.8.05.44.PM.mov
PR Summary (implementation details)
Focus brackets
js/app/packages/app/index.cssto 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:ScopedPortal scope="split".selectionRectInSplitto translate from unified-list-local coords → split-panel-local coords viagetBoundingClientRect().outline(draws outside box) to insetbox-shadow(prevents 1px overflow).color-mix.pattern-accent pattern-diagonal-4atopacity-[0.03]).transition-[transform,height,opacity]) so layout-driven width changes never animate.splitContext.panelSize+preview()entities_()?.length) usingrequestAnimationFrameto measure post-reflow.In
js/app/packages/app/component/split-layout/components/SplitContainer.tsx:relativeto the split container root so the portaled highlight can be positioned correctly.Suppress bracket focus on resize gutter
js/app/packages/core/component/Resize/Resize.tsx:bracket-neverto 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:ehotkey:data-done-swipe="true"to the row DOM (.everything-entityvia[data-entity-id]), and queues entities for commit.mark_as_donein a debounced batch (DONE_COMMIT_DEBOUNCE_MS = 250ms) so gaps can accumulate while mashing.notificationFilter === 'notDone'(Signal/Noise)In
js/app/packages/app/index.css:.everything-entity[data-done-swipe="true"]:entity-done-swipe: full swipe left off-screen with fadeentity-done-sheen: subtle accent sheen sweep~260mswith a small initial hold)