Two mobile problems in the record table: tapping a chip in the first column takes two taps, and chips in every other column open a side panel where a full page is wanted. #23422 is stacked on this branch. ## Two taps to open a record The table's interactive layer lives in a hover portal mounted from `onMouseMove` on the table wrapper. Touch has no hover, so the browser fakes one, and the synthesised `mousemove` arrives *before* the `mousedown`. React commits the portal in the microtask between them, so the whole tap is hit-tested against a subtree that did not exist when the user aimed. Confirmed in Chromium with real touch input (`page.tap`, Pixel 5 emulation), mounting an overlay from the mousemove handler: ``` mousemove target=chip >>> overlay mounted <- the hover portal mousedown target=portalChip <- a node that did not exist when the finger went down mouseup target=portalChip click target=portalChip ``` The same test also ruled out `preventDefault` on the compat `mousedown` as a cause, and showed a `setTimeout`-deferred mount does *not* retarget — it is specifically React's sync flush timing that does. So hover state is now only tracked on hover-capable pointers. `useMoveHoverToCurrentCell` becomes the single writer and absorbs the deduplication `RecordTableContent` was duplicating inline. The interaction/layout split matters here: `useIsMobile` is a 768px width query, which answers "how much room is there to lay out", not "how does this person point". The new `useIsTouchDevice` uses `(hover: none) and (pointer: coarse)`. Layout keeps using width; interaction uses capability. ## Side panel on mobile "Where does a record open" was computed independently in six places and only `useOpenRecordFromIndexView` knew about mobile. `RecordChip` — every chip outside the first column, plus board cards and relation fields — had its own copy without that check. On mobile the side panel animates to `fullScreen`, so it is a full-page view with no URL and no back button. That decision now lives in one `resolveOpenRecordIn`: the view setting is an intent, and the side panel is only a real destination when there is room for it and the object supports it. Also here: `MOUSE_DOWN` navigation downgrades to `CLICK` on touch. It only buys a frame on a real pointer, since a tap synthesises its mouse events after the finger is already gone. ## Hover styling Separate layer, same root cause. A tap leaves CSS `:hover` applied until the next tap lands elsewhere, so a row you came back from keeps reading as selected. Nine `:hover` blocks across the record table, `Chip` and `Avatar` are now fenced behind `(hover: hover)` — the same media feature `useIsTouchDevice` branches on, via a new `hover-capable` SCSS mixin on the twenty-ui side and inline media queries in the Linaria components. Desktop rendering is unchanged, since Chrome matches `hover: hover`. Verified the built CSS emits the wrapper correctly, and checked the nested form through stylis directly for the Linaria side. ## Testing - New unit tests for `resolveOpenRecordIn` and for hover not being tracked on touch devices. - Full frontend suite: 951 suites, 5598 tests passing. Typecheck and lint clean. - Not observed end to end in a running app: no database in this environment, and the `RecordIndexPage` story renders an empty table under its msw mocks. The browser-level mechanism is verified and the fix removes the mid-gesture DOM change, but it is worth one pass on a real device before merge. ## Follow-ups not in this PR - The whole first cell navigates but only the chip-sized part of it gives tap feedback, and `isRecordTableRowActive` is only set on the side panel path — setting it on the navigate path too would keep the row lit while the page loads. - Rows are 32px against a 44px minimum touch target. - Giving the side panel a URL would make "panel vs page" a rendering decision on the same location, rather than something each call site has to branch on. --- _Generated by [Claude Code](https://claude.ai/code/session_019cDWPgWESbdRUhGxGb66j8)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23424?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
The #1 Open-Source CRM
Website ·
Documentation ·
Roadmap ·
Discord ·
Figma
Why Twenty
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
Learn more about why we built Twenty
Installation
Cloud
The fastest way to get started. Sign up at twenty.com and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.
Build an app
Scaffold a new app with the Twenty CLI:
npx create-twenty-app my-app
Define objects, fields, and views as code:
import { defineObject, FieldType } from 'twenty-sdk/define';
export default defineObject({
nameSingular: 'deal',
namePlural: 'deals',
labelSingular: 'Deal',
labelPlural: 'Deals',
fields: [
{ name: 'name', label: 'Name', type: FieldType.TEXT },
{ name: 'amount', label: 'Amount', type: FieldType.CURRENCY },
{ name: 'closeDate', label: 'Close Date', type: FieldType.DATE_TIME },
],
});
Then ship it to your workspace:
npx twenty app:publish --private
See the app development guide for objects, views, agents, and logic functions.
Self-hosting
Run Twenty on your own infrastructure with Docker Compose, or contribute locally via the local setup guide.
Everything you need
Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.
Want to go deeper? Read the User Guide for product walkthroughs, or the
Documentation for developer reference.
|
|
|
|
|
|
Stack
TypeScript
Nx
NestJS, with BullMQ,
PostgreSQL,
Redis
React, with Jotai, Linaria and Lingui
Thanks
Thanks to these amazing services that we use and recommend for code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
Join the Community
Star the repo ·
Discord ·
Feature requests ·
Releases ·
X ·
LinkedIn ·
Crowdin ·
Contribute





