diff --git a/plans/clickablebox3.md b/plans/clickablebox3.md
index 04ee699989ff..0fcef11e62f4 100644
--- a/plans/clickablebox3.md
+++ b/plans/clickablebox3.md
@@ -24,7 +24,7 @@
## ✅ Done: ClickableBox3 implemented and devices/ migrated
Committed in `3ac6c14b82`. Key points for future reference:
-- `ClickableBox3Props = Box2Props & {onClick?, onLongPress?, hitSlop?}` — `direction` required
+- `ClickableBox3Props = Box2Props & {onClick?, onLongPress?, hitSlop?}` — `direction` required; desktop mouse events (`onMouseDown/Up/Leave/Move/Over/Enter`, `onContextMenu`) are in `Box2Props` and passed through to the `
`
- `box2ClassNames()` extracted from Box2 and shared; `box2SharedProps` exported from `box.tsx`
- `devices/` pilot complete: 3 CB2 usages → CB3, inner Box2 wrappers eliminated, `mobileAddHeader` style simplified
@@ -38,19 +38,19 @@ Use `migrate-clickable-box` skill for each chunk. Run `yarn lint && yarn tsc` an
- [x] `shared/devices/` (6 total)
### Round 1 — small
-- [ ] `shared/git/` (3)
-- [ ] `shared/incoming-share/` (2)
-- [ ] `shared/signup/` (2)
-- [ ] `shared/provision/` (4)
-- [ ] `shared/people/` (2)
-- [ ] `shared/settings/` (4)
-- [ ] `shared/profile/` (10)
+- [x] `shared/git/` (3)
+- [x] `shared/incoming-share/` (2)
+- [x] `shared/signup/` (2)
+- [x] `shared/provision/` (4)
+- [x] `shared/people/` (2)
+- [x] `shared/settings/` (4)
+- [x] `shared/profile/` (10)
### Round 2 — medium
-- [ ] `shared/tracker/` (4)
-- [ ] `shared/menubar/` (3)
-- [ ] `shared/app/` (4)
-- [ ] `shared/router-v2/` (9)
+- [x] `shared/tracker/` (4)
+- [x] `shared/menubar/` (3)
+- [x] `shared/app/` (4)
+- [x] `shared/router-v2/` (9)
- [ ] `shared/teams/` (25+)
- [ ] `shared/team-building/` (9+)
diff --git a/plans/todo.md b/plans/todo.md
index 2376ce52ba86..f998c24bf379 100644
--- a/plans/todo.md
+++ b/plans/todo.md
@@ -1,6 +1,7 @@
go screen by screen and find cleanup
legends to more desktop
move to clickablebox3
+crypto screens button doesn't stick to keyboard well
automated testing of all screens
any leftover zustand store
legend list for chat thread native
diff --git a/shared/.maestro/e2e/flows/crypto-subtabs.yaml b/shared/.maestro/e2e/flows/crypto-subtabs.yaml
index 40c99295ad34..4cd6e2ff53e5 100644
--- a/shared/.maestro/e2e/flows/crypto-subtabs.yaml
+++ b/shared/.maestro/e2e/flows/crypto-subtabs.yaml
@@ -17,11 +17,6 @@ appId: keybase.ios
visible:
id: "crypto-encrypt-input"
timeout: 3000
-- takeScreenshot: tests/results/ios-debug/crypto-subtabs-encrypt-keyboard
-- tapOn:
- point: "5%,5%"
-- waitForAnimationToEnd:
- timeout: 500
- takeScreenshot: tests/results/ios-debug/crypto-subtabs-encrypt
- tapOn:
id: "backButton"
@@ -32,11 +27,6 @@ appId: keybase.ios
visible:
id: "crypto-decrypt-input"
timeout: 3000
-- takeScreenshot: tests/results/ios-debug/crypto-subtabs-decrypt-keyboard
-- tapOn:
- point: "5%,5%"
-- waitForAnimationToEnd:
- timeout: 500
- takeScreenshot: tests/results/ios-debug/crypto-subtabs-decrypt
- tapOn:
id: "backButton"
@@ -47,11 +37,6 @@ appId: keybase.ios
visible:
id: "crypto-sign-input"
timeout: 3000
-- takeScreenshot: tests/results/ios-debug/crypto-subtabs-sign-keyboard
-- tapOn:
- point: "5%,5%"
-- waitForAnimationToEnd:
- timeout: 500
- takeScreenshot: tests/results/ios-debug/crypto-subtabs-sign
- tapOn:
id: "backButton"
@@ -62,11 +47,6 @@ appId: keybase.ios
visible:
id: "crypto-verify-input"
timeout: 3000
-- takeScreenshot: tests/results/ios-debug/crypto-subtabs-verify-keyboard
-- tapOn:
- point: "5%,5%"
-- waitForAnimationToEnd:
- timeout: 500
- takeScreenshot: tests/results/ios-debug/crypto-subtabs-verify
- tapOn:
id: "backButton"
diff --git a/shared/.maestro/e2e/flows/devices-view.yaml b/shared/.maestro/e2e/flows/devices-view.yaml
index e36a1a8bab65..fdf8a5a46394 100644
--- a/shared/.maestro/e2e/flows/devices-view.yaml
+++ b/shared/.maestro/e2e/flows/devices-view.yaml
@@ -14,3 +14,4 @@ appId: keybase.ios
visible:
id: "devices-row"
timeout: 3000
+- takeScreenshot: tests/results/ios-debug/devices-view
diff --git a/shared/.maestro/e2e/flows/files-browse.yaml b/shared/.maestro/e2e/flows/files-browse.yaml
index 601527c7b3d2..a31a0a1d7ee7 100644
--- a/shared/.maestro/e2e/flows/files-browse.yaml
+++ b/shared/.maestro/e2e/flows/files-browse.yaml
@@ -12,3 +12,4 @@ appId: keybase.ios
visible:
id: "files-browser"
timeout: 3000
+- takeScreenshot: tests/results/ios-debug/files-browse
diff --git a/shared/.maestro/e2e/flows/settings-navigation.yaml b/shared/.maestro/e2e/flows/settings-navigation.yaml
index 1e8ed1f32bab..46bfe8e38937 100644
--- a/shared/.maestro/e2e/flows/settings-navigation.yaml
+++ b/shared/.maestro/e2e/flows/settings-navigation.yaml
@@ -13,3 +13,4 @@ appId: keybase.ios
visible:
text: "Email & phone"
timeout: 3000
+- takeScreenshot: tests/results/ios-debug/settings-navigation
diff --git a/shared/.maestro/e2e/flows/teams-browse.yaml b/shared/.maestro/e2e/flows/teams-browse.yaml
index 1a5635bb3412..96397835863e 100644
--- a/shared/.maestro/e2e/flows/teams-browse.yaml
+++ b/shared/.maestro/e2e/flows/teams-browse.yaml
@@ -7,6 +7,7 @@ appId: keybase.ios
visible:
id: "teams-list"
timeout: 3000
+- takeScreenshot: tests/results/ios-debug/teams-browse
- runFlow:
when:
visible:
diff --git a/shared/app/global-errors.tsx b/shared/app/global-errors.tsx
index d5cfc7e672ae..d1257d310d98 100644
--- a/shared/app/global-errors.tsx
+++ b/shared/app/global-errors.tsx
@@ -213,7 +213,7 @@ const GlobalError = () => {
}
return (
-
+
{summary}
@@ -239,7 +239,7 @@ const GlobalError = () => {
{details}
-
+
)
}
diff --git a/shared/app/runtime-stats.tsx b/shared/app/runtime-stats.tsx
index bad8651248a6..4214b75d8e5c 100644
--- a/shared/app/runtime-stats.tsx
+++ b/shared/app/runtime-stats.tsx
@@ -226,8 +226,7 @@ const RuntimeStatsDesktop = ({stats}: Props) => {
return (
<>
- setMoreLogs(m => !m)}>
-
+ setMoreLogs(m => !m)} direction="vertical" style={styles.container} gap="xxtiny" fullWidth={true}>
{!moreLogs &&
stats.processStats?.map((stat, i) => {
return (
@@ -303,8 +302,7 @@ const RuntimeStatsDesktop = ({stats}: Props) => {
)*/}
-
-
+
>
)
@@ -333,9 +331,9 @@ const RuntimeStatsMobile = ({stats}: Props) => {
style={showLogs ? styles.modalLogStats : styles.modalLogStatsHidden}
gap="xtiny"
>
- setShowLogs(s => !s)}>
+ setShowLogs(s => !s)} direction="vertical">
-
+
{processStat && (
diff --git a/shared/common-adapters/box.tsx b/shared/common-adapters/box.tsx
index 64a7c9c8be10..d377163dc69a 100644
--- a/shared/common-adapters/box.tsx
+++ b/shared/common-adapters/box.tsx
@@ -24,6 +24,7 @@ export type Box2Props = {
onDrop?: (syntheticDragEvent: React.DragEvent) => void
onLayout?: (evt: LayoutEvent) => void
onMouseDown?: (syntheticEvent: React.MouseEvent) => void
+ onMouseEnter?: (syntheticEvent: React.MouseEvent) => void
onMouseMove?: (syntheticEvent: React.MouseEvent) => void
onMouseLeave?: (syntheticEvent: React.MouseEvent) => void
onMouseUp?: (syntheticEvent: React.MouseEvent) => void
@@ -318,31 +319,42 @@ export const ClickableBox3 = (p: ClickableBox3Props & {ref?: React.Ref}
className={cn}
+ data-testid={testID}
+ data-tooltip={tooltip}
onClick={onClick}
+ onContextMenu={onContextMenu}
+ onMouseDown={onMouseDown}
+ onMouseEnter={onMouseEnter}
+ onMouseLeave={onMouseLeave}
+ onMouseMove={onMouseMove}
onMouseOver={onMouseOver}
+ onMouseUp={onMouseUp}
style={s}
- data-testid={testID}
+ title={title}
>
{children}
)
}
- const {style: s, children: c} = box2SharedProps(box2p)
+ const {style: s, children: c, onLayout, collapsable, pointerEvents} = box2SharedProps(box2p)
return (
}
- onPress={onClick ? () => { onClick() } : undefined}
+ collapsable={collapsable}
+ hitSlop={hitSlop}
+ onLayout={onLayout}
onLongPress={onLongPress}
+ onPress={onClick ? () => { onClick() } : undefined}
+ pointerEvents={pointerEvents}
style={s}
- hitSlop={hitSlop}
testID={box2p.testID}
>
{c}
diff --git a/shared/git/index.tsx b/shared/git/index.tsx
index 9d704e4f58ee..265c2595910a 100644
--- a/shared/git/index.tsx
+++ b/shared/git/index.tsx
@@ -194,7 +194,7 @@ const loading = C.Waiting.useAnyWaiting(C.waitingKeyGitLoading)
{!!error && {error.message}}
{isMobile && (
-
+
New encrypted git repository...
-
+
)}
({
header: {
- ...Kb.Styles.globalStyles.flexBoxCenter,
- ...Kb.Styles.globalStyles.flexBoxRow,
flexShrink: 0,
height: 48,
},
diff --git a/shared/git/row.tsx b/shared/git/row.tsx
index a2cda4686ac2..b978267b4a57 100644
--- a/shared/git/row.tsx
+++ b/shared/git/row.tsx
@@ -119,13 +119,16 @@ function ConnectedRow(ownProps: OwnProps) {
expanded && {backgroundColor: Kb.Styles.globalColors.white},
])}
>
-
-
)}
-
-
+
{expanded && (
? undefined
: {
content: (
-
+
Save in Files
-
+
),
}
}
@@ -335,11 +335,6 @@ const IncomingShareMain = (props: IncomingShareMainProps) => {
}
const styles = Kb.Styles.styleSheetCreate(() => ({
- footer: {
- ...Kb.Styles.globalStyles.flexBoxRow,
- ...Kb.Styles.centered(),
- width: '100%',
- },
footerIcon: {
marginRight: Kb.Styles.globalMargins.tiny,
},
diff --git a/shared/menubar/index.desktop.tsx b/shared/menubar/index.desktop.tsx
index 7c9640febddb..3d70402e3829 100644
--- a/shared/menubar/index.desktop.tsx
+++ b/shared/menubar/index.desktop.tsx
@@ -118,53 +118,55 @@ const ChatRow = (p: {conv: Conversation; httpSrvAddress: string; httpSrvToken: s
const timestamp = conv.timestamp ? TimestampUtil.formatTimeForConversationList(conv.timestamp) : ''
return (
- R.remoteDispatch(RemoteGen.createOpenChatFromWidget({conversationIDKey: conv.conversationIDKey}))}
- style={styles.chatRow}
+ direction="horizontal"
+ fullWidth={true}
+ alignItems="center"
+ gap="tiny"
+ style={styles.chatRowInner}
>
-
-
-
-
-
-
- {isTeam && conv.channelname ? `${name}#${conv.channelname}` : name}
-
- {conv.hasBadge && }
-
- {!!timestamp && (
-
- {timestamp}
-
- )}
+
+
+
+
+
+ {isTeam && conv.channelname ? `${name}#${conv.channelname}` : name}
+
+ {conv.hasBadge && }
- {!!conv.snippetDecorated && (
+ {!!timestamp && (
- {conv.snippetDecorated}
+ {timestamp}
)}
+ {!!conv.snippetDecorated && (
+
+ {conv.snippetDecorated}
+
+ )}
-
+
)
}
@@ -191,17 +193,22 @@ const ChatPreview = (p: {conversationsToSend: ReadonlyArray; convL
// Inline file updates (replaces FilesContainer + files.desktop.tsx with store-connected components)
const FileUpdate = (p: {path: T.FS.Path; uploading: boolean; onClick: () => void}) => (
-
-
-
- {p.uploading && (
-
-
-
- )}
-
-
-
+
+
+ {p.uploading && (
+
+
+
+ )}
+
+
)
const defaultNumFileOptionsShown = 3
@@ -642,11 +649,6 @@ const styles = Kb.Styles.styleSheetCreate(() => ({
backgroundColor: Kb.Styles.globalColors.white,
color: Kb.Styles.globalColors.black,
},
- chatRow: Kb.Styles.platformStyles({
- isElectron: {
- ...Kb.Styles.desktopStyles.clickable,
- },
- }),
chatRowInner: Kb.Styles.padding(Kb.Styles.globalMargins.xtiny, Kb.Styles.globalMargins.xsmall),
chatRowName: {flexShrink: 1},
@@ -655,7 +657,6 @@ const styles = Kb.Styles.styleSheetCreate(() => ({
chatSnippet: {color: Kb.Styles.globalColors.black_50},
chatSnippetUnread: {color: Kb.Styles.globalColors.black},
chatTimestamp: {color: Kb.Styles.globalColors.black_50, flexShrink: 0, marginLeft: Kb.Styles.globalMargins.tiny},
- fileFullWidth: {width: '100%'},
fileIcon: {
flexShrink: 0,
...Kb.Styles.size(16),
diff --git a/shared/people/follow-notification.tsx b/shared/people/follow-notification.tsx
index 297c88e8b598..4d7e6f1d7717 100644
--- a/shared/people/follow-notification.tsx
+++ b/shared/people/follow-notification.tsx
@@ -49,7 +49,7 @@ const FollowNotification = (props: Props) => {
}
return (
-
+
{
)}
-
+
)
}
diff --git a/shared/profile/add-to-team.tsx b/shared/profile/add-to-team.tsx
index 0448b9516d1c..207d5d2888a0 100644
--- a/shared/profile/add-to-team.tsx
+++ b/shared/profile/add-to-team.tsx
@@ -341,7 +341,7 @@ type RowProps = {
const TeamRow = (props: RowProps) => {
return (
- props.onCheck(!props.checked) : undefined}>
+ props.onCheck(!props.checked) : undefined}>
@@ -372,7 +372,7 @@ const TeamRow = (props: RowProps) => {
{!isMobile && }
-
+
)
}
diff --git a/shared/profile/edit-avatar/index.tsx b/shared/profile/edit-avatar/index.tsx
index 71c6f733bb02..47e7f1b99850 100644
--- a/shared/profile/edit-avatar/index.tsx
+++ b/shared/profile/edit-avatar/index.tsx
@@ -159,7 +159,8 @@ const DesktopEditAvatar = (_p: Props) => {
{' '}
for one.
- {
type="iconfont-camera"
/>
)}
-
+
{loading === 'loaded' ? Click to select. Scroll to zoom. : null}
@@ -311,12 +312,14 @@ const NativeAvatarUploadWrapper = (p: Props) => {
const renderImageZoomer = () => {
if (type === 'team' && !selectedImage) {
return (
-
-
+
)
}
return selectedImage ? (
diff --git a/shared/profile/generic/proofs-list.tsx b/shared/profile/generic/proofs-list.tsx
index 3a4986f101a8..4ca32f5db9c5 100644
--- a/shared/profile/generic/proofs-list.tsx
+++ b/shared/profile/generic/proofs-list.tsx
@@ -620,7 +620,10 @@ const ProviderPicker = ({
renderItem={(_: unknown, provider: Provider) => (
- onSelect(provider.key)}
style={styles.containerBox}
@@ -647,7 +650,7 @@ const ProviderPicker = ({
style={styles.iconArrow}
type="iconfont-arrow-right"
/>
-
+
)}
/>
@@ -1314,11 +1317,7 @@ const styles = Kb.Styles.styleSheetCreate(
},
}),
containerBox: {
- alignItems: 'center',
- display: 'flex',
- flexDirection: 'row',
height: isMobile ? 56 : 48,
- justifyContent: 'flex-start',
},
description: {...rightColumnStyle},
error: {
diff --git a/shared/profile/user/actions/index.tsx b/shared/profile/user/actions/index.tsx
index 22c7c9af3c6b..b7b37964f035 100644
--- a/shared/profile/user/actions/index.tsx
+++ b/shared/profile/user/actions/index.tsx
@@ -208,14 +208,12 @@ const DropdownButton = (p: DropdownProps) => {
const {showPopup, popup, popupAnchor} = Kb.usePopup2(makePopup)
return (
-
-
-
-
-
-
+
+
+
+
{popup}
-
+
)
}
diff --git a/shared/profile/user/friend.tsx b/shared/profile/user/friend.tsx
index d4366fc0cd78..72844c74b2cf 100644
--- a/shared/profile/user/friend.tsx
+++ b/shared/profile/user/friend.tsx
@@ -25,12 +25,12 @@ const Container = (ownProps: OwnProps) => {
: followsYou ? ('icon-follow-me-21' as const) : ('icon-following-21' as const)
return (
-
-
+
{!!followIconType && }
@@ -45,8 +45,7 @@ const Container = (ownProps: OwnProps) => {
{fullname}
-
-
+
)
}
diff --git a/shared/profile/user/index.tsx b/shared/profile/user/index.tsx
index d70bc05c2d7a..8c0914460892 100644
--- a/shared/profile/user/index.tsx
+++ b/shared/profile/user/index.tsx
@@ -227,24 +227,24 @@ const Tabs = (p: TabsProps) => {
const onClickFollowing = () => p.onSelectTab('following')
const onClickFollowers = () => p.onSelectTab('followers')
const tab = (tab: Tab) => (
-
-
-
- {tab === 'following'
- ? `Following${!p.loadingFollowing ? ` (${p.numFollowing || 0})` : ''}`
- : `Followers${!p.loadingFollowers ? ` (${p.numFollowers || 0})` : ''}`}
-
- {((tab === 'following' && p.loadingFollowing) || p.loadingFollowers) && (
-
- )}
-
-
+
+ {tab === 'following'
+ ? `Following${!p.loadingFollowing ? ` (${p.numFollowing || 0})` : ''}`
+ : `Followers${!p.loadingFollowers ? ` (${p.numFollowers || 0})` : ''}`}
+
+ {((tab === 'following' && p.loadingFollowing) || p.loadingFollowers) && (
+
+ )}
+
)
return (
diff --git a/shared/profile/user/teams/index.tsx b/shared/profile/user/teams/index.tsx
index bc5b8bdb9a05..46ad3d7ffc7d 100644
--- a/shared/profile/user/teams/index.tsx
+++ b/shared/profile/user/teams/index.tsx
@@ -73,14 +73,12 @@ const TeamShowcase = (props: TeamShowcaseProps) => {
const ShowcaseTeamsOffer = (p: {onEdit: () => void}) => (
-
-
-
-
- {"Feature the teams you're in"}
-
-
-
+
+
+
+ {"Feature the teams you're in"}
+
+
)
diff --git a/shared/profile/user/teams/team-row.tsx b/shared/profile/user/teams/team-row.tsx
index 4751ff7217a1..4f6a1152cdb6 100644
--- a/shared/profile/user/teams/team-row.tsx
+++ b/shared/profile/user/teams/team-row.tsx
@@ -12,19 +12,17 @@ type Props = {
}
const TeamRow = ({isOpen, loading = false, name, onClick, popup, popupAnchor}: Props) => (
-
-
- <>
- {popup}
-
- >
-
- {name}
-
- {typeof isOpen === 'boolean' && }
- {loading && }
-
-
+
+ <>
+ {popup}
+
+ >
+
+ {name}
+
+ {typeof isOpen === 'boolean' && }
+ {loading && }
+
)
const styles = Kb.Styles.styleSheetCreate(() => ({
diff --git a/shared/provision/code-page/container.tsx b/shared/provision/code-page/container.tsx
index 3f5a252f10b2..8e557c366c69 100644
--- a/shared/provision/code-page/container.tsx
+++ b/shared/provision/code-page/container.tsx
@@ -216,7 +216,7 @@ const CodePageContainer = () => {
}
const heyWaitBanner = () => (
- setTroubleshooting(true)}>
+ setTroubleshooting(true)} direction="vertical" fullWidth={true}>
{
]}
/>
-
+
)
const troubleshootingContent = () => (
diff --git a/shared/provision/code-page/qr-scan/scanner.tsx b/shared/provision/code-page/qr-scan/scanner.tsx
index e22a9b95f37c..8de1aee64108 100644
--- a/shared/provision/code-page/qr-scan/scanner.tsx
+++ b/shared/provision/code-page/qr-scan/scanner.tsx
@@ -1,6 +1,5 @@
import * as React from 'react'
import * as Kb from '@/common-adapters'
-import {CameraView, useCameraPermissions} from 'expo-camera'
type Props = {
onBarCodeRead: (code: string) => void
@@ -9,6 +8,9 @@ type Props = {
}
const QRScannerMobile = (p: Props): React.ReactElement | null => {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
+ const {CameraView, useCameraPermissions} = require('expo-camera') as typeof import('expo-camera')
const [scanned, setScanned] = React.useState(false)
const [permission, requestPermission] = useCameraPermissions()
diff --git a/shared/provision/troubleshooting.tsx b/shared/provision/troubleshooting.tsx
index 9cb68753923c..297223df6c92 100644
--- a/shared/provision/troubleshooting.tsx
+++ b/shared/provision/troubleshooting.tsx
@@ -17,12 +17,12 @@ type BigButtonProps = {
}
const BigButton = ({onClick, icon, mainText, subText, waiting}: BigButtonProps) => (
-
-
+
)}
-
-
+
)
const Troubleshooting = (props: Props) => {
diff --git a/shared/router-v2/header/index.desktop.tsx b/shared/router-v2/header/index.desktop.tsx
index 6275f6242fa6..8083ba381b8c 100644
--- a/shared/router-v2/header/index.desktop.tsx
+++ b/shared/router-v2/header/index.desktop.tsx
@@ -79,17 +79,19 @@ const SystemButtons = ({isMaximized}: {isMaximized: boolean}) => {
}
return (
-
-
-
+
{
style={styles.appIcon}
type={isMaximized ? 'iconfont-app-un-maximize' : 'iconfont-app-maximize'}
/>
-
-
+
-
+
)
}
@@ -192,21 +195,20 @@ function DesktopHeader(p: Props) {
>
{/* TODO have headerLeft be the back button */}
{headerLeft !== null && (
-
-
-
-
-
+
+
)}
{
const menuHeader = (
-
+
{
}
/>
-
+
{
return (
<>
-
-
-
+
+ <>
+
+ Hi {username}!
+
+
- <>
-
- Hi {username}!
-
-
- >
-
-
+ >
+
{popup}
>
)
@@ -267,37 +266,33 @@ function Tab(props: TabProps) {
}
return (
-
-
-
-
-
- {tab === Tabs.fsTab && }
-
-
- {label}
-
-
+
+
+
+ {tab === Tabs.fsTab && }
-
+
+ {label}
+
+
+
)
}
diff --git a/shared/settings/account/email-phone-row.tsx b/shared/settings/account/email-phone-row.tsx
index 7232edfef755..b5ce8c071c59 100644
--- a/shared/settings/account/email-phone-row.tsx
+++ b/shared/settings/account/email-phone-row.tsx
@@ -147,15 +147,16 @@ const EmailPhoneRow = (p: {contactKey: string; onEmailVerificationSuccess: (emai
{!!menuItems.length && (
<>
-
{gearIconBadge}
-
+
{popup}
>
)}
diff --git a/shared/settings/feedback/index.tsx b/shared/settings/feedback/index.tsx
index df2f0ccbc028..0b3497691f9e 100644
--- a/shared/settings/feedback/index.tsx
+++ b/shared/settings/feedback/index.tsx
@@ -96,14 +96,14 @@ const Feedback = (props: Props) => {
)}
-
+
-
+
{props.loggedOut && (
@@ -143,9 +143,6 @@ export default Feedback
const styles = Kb.Styles.styleSheetCreate(
() =>
({
- includeLogs: {
- ...Kb.Styles.globalStyles.fullWidth,
- },
input: Kb.Styles.platformStyles({
isElectron: {padding: Kb.Styles.globalMargins.tiny},
isMobile: {...Kb.Styles.padding(Kb.Styles.globalMargins.tiny, Kb.Styles.globalMargins.small)},
diff --git a/shared/settings/files/index.tsx b/shared/settings/files/index.tsx
index a0f0271c425a..85c6341d7788 100644
--- a/shared/settings/files/index.tsx
+++ b/shared/settings/files/index.tsx
@@ -79,11 +79,11 @@ const FinderIntegration = () => {
{Platform.fileUIName} integration
{isPending && }
{driverStatus.type === T.FS.DriverStatusType.Disabled && driverStatus.kextPermissionError && (
-
+
Action needed!
-
+
)}
{driverStatus.type === T.FS.DriverStatusType.Enabled ? (
diff --git a/shared/settings/logout.tsx b/shared/settings/logout.tsx
index 66f77c4fe313..68ff45edbc93 100644
--- a/shared/settings/logout.tsx
+++ b/shared/settings/logout.tsx
@@ -123,8 +123,10 @@ const LogoutContainer = () => {
{loggingOut ? (
) : (
-
@@ -132,7 +134,7 @@ const LogoutContainer = () => {
Just sign out
-
+
)}
@@ -175,13 +177,8 @@ const styles = Kb.Styles.styleSheetCreate(
logout: {paddingLeft: Kb.Styles.globalMargins.xtiny},
logoutContainer: Kb.Styles.platformStyles({
common: {
- ...Kb.Styles.globalStyles.flexBoxRow,
- justifyContent: 'center',
paddingTop: Kb.Styles.globalMargins.tiny,
},
- isElectron: {
- ...Kb.Styles.desktopStyles.clickable,
- },
}),
modalFooter: Kb.Styles.platformStyles({
common: {
diff --git a/shared/settings/routes.tsx b/shared/settings/routes.tsx
index 4366fd26efb6..1736bdf966e8 100644
--- a/shared/settings/routes.tsx
+++ b/shared/settings/routes.tsx
@@ -19,16 +19,17 @@ const PushPromptSkipButton = () => {
const rejectPermissions = usePushState(s => s.dispatch.rejectPermissions)
const clearModals = C.Router2.clearModals
return (
- {
rejectPermissions()
clearModals()
}}
+ direction="vertical"
>
Skip
-
+
)
}
diff --git a/shared/settings/sub-nav/settings-item.tsx b/shared/settings/sub-nav/settings-item.tsx
index 4c26d690ed7c..bfb85c4897cf 100644
--- a/shared/settings/sub-nav/settings-item.tsx
+++ b/shared/settings/sub-nav/settings-item.tsx
@@ -21,8 +21,12 @@ function SettingsItem(props: SettingsItemProps) {
_onClick(type)
}
return (
-
{props.iconComponent ? (
@@ -53,7 +57,7 @@ function SettingsItem(props: SettingsItemProps) {
{!!props.badgeNumber && props.badgeNumber > 0 && (
)}
-
+
)
}
export default SettingsItem
@@ -64,14 +68,10 @@ const styles = Kb.Styles.styleSheetCreate(() => ({
},
item: Kb.Styles.platformStyles({
common: {
- ...Kb.Styles.globalStyles.flexBoxRow,
- alignItems: 'center',
...Kb.Styles.paddingH(Kb.Styles.globalMargins.small),
- position: 'relative',
},
isElectron: {
height: 32,
- width: '100%',
},
isMobile: {
borderBottomColor: Kb.Styles.globalColors.black_10,
diff --git a/shared/signup/common.tsx b/shared/signup/common.tsx
index aae72810070d..af92c6bbc008 100644
--- a/shared/signup/common.tsx
+++ b/shared/signup/common.tsx
@@ -79,8 +79,7 @@ const Header = (props: HeaderProps) => (
)}
{props.onBack && (
-
-
+
(
>
Back
-
-
+
)}
{props.titleComponent || {props.title}}
{props.onRightAction && !!props.rightActionLabel && (
diff --git a/shared/signup/phone-number/verify-body.tsx b/shared/signup/phone-number/verify-body.tsx
index d5a8c30a7c37..e383c263f48e 100644
--- a/shared/signup/phone-number/verify-body.tsx
+++ b/shared/signup/phone-number/verify-body.tsx
@@ -36,9 +36,10 @@ const VerifyBody = (props: BodyProps) => {
containerStyle={styles.inputContainer2}
inputStyle={styles.inputText2}
/>
-
{
)}
-
+
)
}
@@ -108,7 +109,6 @@ const styles = Kb.Styles.styleSheetCreate(
},
}),
opacity30: {opacity: 0.3},
- positionRelative: {position: 'relative'},
progressContainer: Kb.Styles.platformStyles({
common: {...Kb.Styles.globalStyles.fillAbsolute},
isElectron: {paddingTop: Kb.Styles.globalMargins.tiny},
diff --git a/shared/tests/e2e/generate-electron-report.mts b/shared/tests/e2e/generate-electron-report.mts
index 2fef624f5fd8..82bece2800a7 100644
--- a/shared/tests/e2e/generate-electron-report.mts
+++ b/shared/tests/e2e/generate-electron-report.mts
@@ -8,6 +8,7 @@ const require = createRequire(import.meta.url)
const {PNG} = require('pngjs') as {PNG: {sync: {read: (buf: Buffer) => {data: Buffer; width: number; height: number}}}}
const resultsPath = 'tests/results/report/results.json'
+const debugDir = 'tests/results/electron-debug'
const prevDir = 'tests/results/electron-prev'
const outputPath = 'tests/results/electron-report.html'
@@ -25,7 +26,7 @@ type TestCase = {
label: string
passed: boolean
durationMs: number
- screenshotB64: string | null
+ screenshotPath: string | null
prevScreenshotPath: string | null
diff: DiffResult | null
errorMessage: string | null
@@ -43,9 +44,9 @@ function flattenSpecs(suite: PlaywrightSuite): Array<{suiteName: string; spec: P
return out
}
-function computeDiff(bufA: Buffer, pathB: string): DiffResult | null {
+function computeDiff(pathA: string, pathB: string): DiffResult | null {
try {
- const a = PNG.sync.read(bufA)
+ const a = PNG.sync.read(fs.readFileSync(pathA))
const b = PNG.sync.read(fs.readFileSync(pathB))
if (a.width !== b.width || a.height !== b.height) return null
const total = a.width * a.height
@@ -76,23 +77,29 @@ function parseReport(report: Report): TestCase[] {
: null
const screenshotAtt = result.attachments.find(a => a.name === 'screenshot' && a.contentType === 'image/png')
- const screenshotB64 = screenshotAtt?.body
- ?? (screenshotAtt?.path ? fs.readFileSync(screenshotAtt.path).toString('base64') : null)
+ let screenshotPath: string | null = null
+ if (screenshotAtt) {
+ const buf = screenshotAtt.body
+ ? Buffer.from(screenshotAtt.body, 'base64')
+ : screenshotAtt.path ? fs.readFileSync(screenshotAtt.path) : null
+ if (buf) {
+ fs.mkdirSync(debugDir, {recursive: true})
+ screenshotPath = path.join(debugDir, `${key}.png`)
+ fs.writeFileSync(screenshotPath, buf)
+ }
+ }
const prevPath = path.join(prevDir, `${key}.png`)
const prevScreenshotPath = fs.existsSync(prevPath) ? prevPath : null
- let diff: DiffResult | null = null
- if (screenshotB64 && prevScreenshotPath) {
- diff = computeDiff(Buffer.from(screenshotB64, 'base64'), prevScreenshotPath)
- }
+ const diff = screenshotPath && prevScreenshotPath ? computeDiff(screenshotPath, prevScreenshotPath) : null
cases.push({
key,
label: `${suiteName} · ${spec.title}`,
passed,
durationMs,
- screenshotB64,
+ screenshotPath,
prevScreenshotPath,
diff,
errorMessage,
@@ -123,19 +130,18 @@ function buildHtml(cases: TestCase[], timestamp: string): string {
? `Δ ${c.diff.pct.toFixed(1)}%`
: ''
+ const rel = (p: string) => path.relative(path.dirname(outputPath), p)
let visual: string
- if (c.screenshotB64 && c.prevScreenshotPath) {
- const curUrl = `data:image/png;base64,${c.screenshotB64}`
- const prevUrl = `data:image/png;base64,${fs.readFileSync(c.prevScreenshotPath).toString('base64')}`
+ if (c.screenshotPath && c.prevScreenshotPath) {
visual = ``
- } else if (c.screenshotB64) {
- visual = ``
+ } else if (c.screenshotPath) {
+ visual = ``
} else {
visual = `No screenshot
`
}
@@ -149,13 +155,12 @@ function buildHtml(cases: TestCase[], timestamp: string): string {
return buildPage('Keybase Electron E2E Tests', allPassed, totalPassed, totalFailed, cases.length, hasDiff, timestamp, cards)
}
-// Save baseline: write current screenshots as PNGs to prevDir
function saveBaseline(cases: TestCase[]) {
fs.mkdirSync(prevDir, {recursive: true})
let saved = 0
for (const c of cases) {
- if (!c.screenshotB64) continue
- fs.writeFileSync(path.join(prevDir, `${c.key}.png`), Buffer.from(c.screenshotB64, 'base64'))
+ if (!c.screenshotPath || !fs.existsSync(c.screenshotPath)) continue
+ fs.copyFileSync(c.screenshotPath, path.join(prevDir, `${c.key}.png`))
saved++
}
console.log(`Baseline saved: ${saved} screenshots to ${prevDir}/`)
diff --git a/shared/tests/e2e/generate-ios-report.mts b/shared/tests/e2e/generate-ios-report.mts
index 27a9ac8fddbc..cb31786dc9b1 100644
--- a/shared/tests/e2e/generate-ios-report.mts
+++ b/shared/tests/e2e/generate-ios-report.mts
@@ -68,10 +68,6 @@ function computeDiff(pathA: string, pathB: string): DiffResult | null {
}
}
-function imageToDataUrl(filePath: string): string {
- return `data:image/png;base64,${fs.readFileSync(filePath).toString('base64')}`
-}
-
function parseFlow(name: string): ScreenshotResult[] {
const commands = readCommandsFile(name)
const failed = commands?.find(c => c.metadata.status === 'FAILED')
@@ -161,19 +157,20 @@ function buildHtml(results: ScreenshotResult[], timestamp: string, title: string
: ''
const durStr = r.durationMs > 0 ? formatDuration(r.durationMs) : ''
+ const rel = (p: string) => path.relative(path.dirname(outputPath), p)
let visual: string
if (r.screenshotPath && r.prevScreenshotPath) {
visual = ``
} else if (r.screenshotPath) {
- visual = ``
+ visual = ``
} else if (r.failureScreenshotPath) {
- visual = ``
+ visual = ``
} else {
visual = `No screenshot
`
}
diff --git a/shared/tracker/assertion.tsx b/shared/tracker/assertion.tsx
index af8ed8898903..dd09f151610e 100644
--- a/shared/tracker/assertion.tsx
+++ b/shared/tracker/assertion.tsx
@@ -222,24 +222,29 @@ const Container = (ownProps: OwnProps) => {
)}
-
-
-
- {items ? (
- <>
-
- {popup}
- >
- ) : (
-
- )}
-
-
+
+
+ {items ? (
+ <>
+
+ {popup}
+ >
+ ) : (
+
+ )}
+
{!!metas.length && (
@@ -482,9 +487,9 @@ const AssertionSiteIcon = (p: SIProps) => {
child = {child}
}
return (
-
+
{child}
-
+
)
}
diff --git a/skill/migrate-clickable-box/SKILL.md b/skill/migrate-clickable-box/SKILL.md
index 4f1925297054..3a340d9ad9e0 100644
--- a/skill/migrate-clickable-box/SKILL.md
+++ b/skill/migrate-clickable-box/SKILL.md
@@ -11,10 +11,11 @@ See `plans/clickablebox3.md` for the full migration plan, directory checklist, a
`ClickableBox3` = `ClickableBox2` + all `Box2` layout props (direction optional). On desktop it renders a `` with the Box2 CSS class system plus `clickable-box2` cursor. On mobile it uses `Pressable` + `box2SharedProps` for layout.
-Type: `Omit
& {direction?: ..., onClick?, onLongPress?, onMouseOver?, hitSlop?}`
+Type: `Box2Props & {onClick?, onLongPress?, hitSlop?}` — **`direction` is required.**
-When `direction` is omitted: no flex layout — behaves like CB2 (plain clickable wrapper).
-When `direction` is provided: Box2 layout is applied — **eliminates the inner Box2**.
+`Box2Props` includes desktop mouse events: `onMouseDown`, `onMouseUp`, `onMouseLeave`, `onMouseMove`, `onMouseOver`, `onMouseEnter`, `onContextMenu`. CB3 passes all of these through to the desktop ``. They are **not** forwarded on mobile (Pressable doesn't support them).
+
+`direction` is always required. For Pattern B swaps (plain clickable wrapper, no layout needed), pass the direction that matches how children are stacked — usually `"vertical"` for a single child or vertically-stacked children.
## Process
@@ -79,14 +80,16 @@ Read each matched file fully before classifying.
```
-**Action:** Simple swap to CB3 with no layout props.
+**Action:** Swap to CB3. Always provide `direction`. Add `fullWidth={true}` if the original CB was filling its parent's width (e.g., list rows, banners, full-width wrappers).
```tsx
-
+
```
+**⚠️ `box2_centered` / `alignSelf: center` gotcha:** CB3 applies `align-self: center` (desktop: `box2_centered` CSS class; mobile: `nativeStyles.centered = {alignSelf: 'center'}`) whenever neither `fullWidth` nor `fullHeight` is set. CB1 did NOT do this. This affects **both platforms**. If the original CB was expected to fill its parent (e.g. list rows, banners, full-width wrappers), omitting `fullWidth={true}` will shrink it to content-width. Always check the parent layout context — when in doubt, add `fullWidth={true}`.
+
### Pattern C — CB with style that encodes flex layout
```tsx
@@ -109,11 +112,12 @@ These CB1 props have no CB3 equivalent:
- `hoverColor`, `underlayColor` → add `hover_background_color_*` CSS className to CB3 instead
- `feedback={false}` → drop (Pressable doesn't have this)
- `activeOpacity` → drop
-- `onMouseEnter` / `onMouseLeave` → rare; use a wrapper div if truly needed
- `onPressIn` / `onPressOut` → not in CB3; leave as CB1 and note it
- `tooltip` → wrap with `` outside CB3
- `onLongPress={(e) => ...}` → remove the `e` param (CB3 signature is `() => void`)
+Note: `onMouseDown`, `onMouseUp`, `onMouseLeave`, `onMouseMove`, `onMouseOver`, `onMouseEnter` are **fully supported** in CB3 via `Box2Props` — these are NOT Pattern D cases.
+
## Step 3: Present Changes Before Touching Anything
For each usage show: file, line, pattern (A/B/C/D), proposed change.
@@ -132,7 +136,7 @@ From `shared/`:
```
yarn lint && yarn tsc
```
-Fix errors before reporting done.
+**Both must pass with zero errors before reporting done.** Fix any failures — including lint errors in files we touched or that were broken in a prior session. Do not skip lint or treat failures as pre-existing without verifying via `git stash`.
## Step 6: Update Checklist
@@ -165,9 +169,9 @@ Keep in the style object: fixed `height`, `width` values, `padding`, `margin`, `
CB3 on RN does NOT add `borderRadius: 3` (CB1's old default). Remove it from styles only if it was purely inherited from CB1, not a design intent.
-### Note: CB3 does NOT have a `direction` default
+### Note: `direction` is required in CB3
-When `direction` is omitted, CB3 is a block-level clickable wrapper (no flex). If you need flex, always pass `direction`.
+Always pass `direction`. For plain wrapper cases (Pattern B) with no layout need, use `"vertical"` as the default. Remember to add `fullWidth={true}` if the element should fill its parent.
## What NOT to Do
diff --git a/skill/simplify-ui-section/SKILL.md b/skill/simplify-ui-section/SKILL.md
index d85aa4a40238..e51d18c43994 100644
--- a/skill/simplify-ui-section/SKILL.md
+++ b/skill/simplify-ui-section/SKILL.md
@@ -23,15 +23,20 @@ digraph simplify {
"Ask: proceed, skip, or adjust?" [shape=box];
"User approves?" [shape=diamond];
"Implement changes" [shape=box];
+ "Any meaningful findings?" [shape=diamond];
"Read all files in scope" -> "Analyze across categories";
"Analyze across categories" -> "Ask clarifying questions";
"Ask clarifying questions" -> "Present findings by category";
"Present findings by category" -> "Show flat numbered list of ALL changes";
"Show flat numbered list of ALL changes" -> "Ask: proceed, skip, or adjust?";
- "Ask: proceed, skip, or adjust?" -> "User approves?" ;
+ "Ask: proceed, skip, or adjust?" -> "User approves?";
"User approves?" -> "Implement changes" [label="yes"];
"User approves?" -> "Present findings by category" [label="revise"];
+ "Implement changes" -> "Read all files in scope" [label="re-read simplified files"];
+ "Analyze across categories" -> "Any meaningful findings?";
+ "Any meaningful findings?" -> "Ask clarifying questions" [label="yes"];
+ "Any meaningful findings?" -> "Done" [label="no"];
}
```
@@ -61,6 +66,17 @@ Read **every file** in scope before forming opinions. Patterns only become visib
- Components split across files for no structural reason — candidate for collocating
- Deeply nested JSX that could flatten via composition
+### Nested Boxes (high-signal smell)
+
+Two or more `Box2` (or `Kb.Box2`) components nested directly inside each other is a reliable sign something can be simplified. When you see this pattern, investigate:
+
+- **Redundant wrapper**: the outer box exists only to pass a `style` or `direction` that the inner box could absorb — collapse them into one
+- **Props can merge**: both boxes carry layout props (`alignItems`, `gap`, `fullWidth`, etc.) with no conflicting values — merge onto a single box
+- **Composition opportunity**: the nesting reflects a structural concern (header + body) that could be expressed as named sub-components instead of anonymous nested boxes
+- **Unnecessary intermediate container**: an outer box wraps a single child box with no additional siblings or layout purpose — remove the outer layer
+
+A long chain of `` almost always means something went wrong at the design level. Trace back to why each layer exists before proposing a fix; the root cause is usually one of the above.
+
### Props and Styles
- Components with large prop lists where many props just pass through — consider composition or context
- Repeated style patterns across components that could become a shared style helper or `Kb.Styles` utility call
@@ -178,6 +194,16 @@ Wait for the user's response. Do not begin any edits until they reply.
Make all approved changes. Remove unused imports, styles, and variables left behind. Run lint and tsc after.
+## Step 6: Iterate
+
+**Simplification is not a single pass.** After implementing changes, the simplified code often reveals new opportunities that were hidden by the original clutter. Always do at least one more pass.
+
+Go back to **Step 1** and re-read all files in scope. Then repeat Steps 2–5 with fresh eyes.
+
+**Stop iterating when:** a full pass produces no meaningful findings — every category comes up empty or yields only borderline cases the user opts to skip.
+
+**Never stop after the first pass.** The first pass removes the obvious problems. The second pass finds what those problems were hiding. The third pass is usually final.
+
## The Hard Line: No Unilateral Visual Changes
**This skill is structural by default. Zero UX or behavior changes without explicit user sign-off.**