From 0c0c54c52c0de6b3eb3a528a85f5fcbc401214a9 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 29 May 2026 12:00:01 -0400 Subject: [PATCH 1/8] update ui skill --- skill/simplify-ui-section/SKILL.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/skill/simplify-ui-section/SKILL.md b/skill/simplify-ui-section/SKILL.md index d85aa4a40238..9c73b6114ced 100644 --- a/skill/simplify-ui-section/SKILL.md +++ b/skill/simplify-ui-section/SKILL.md @@ -61,6 +61,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 From d47235afc33810f05facc5f07a808d5fcd47ee43 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 29 May 2026 12:10:30 -0400 Subject: [PATCH 2/8] simplify-ui-section skill: add iteration loop after implementation Adds Step 6 directing the model to re-read and re-analyze files after each implementation pass, stopping only when a full pass yields no meaningful findings. Also updates the flowchart to show the loop. --- skill/simplify-ui-section/SKILL.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/skill/simplify-ui-section/SKILL.md b/skill/simplify-ui-section/SKILL.md index 9c73b6114ced..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"]; } ``` @@ -189,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.** From 1edc945550d2fa817a21d822845655c82780a0fe Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 29 May 2026 12:17:54 -0400 Subject: [PATCH 3/8] provision: lazy-require expo-camera to fix Electron crash expo-camera uses native code that crashes on Electron at import time. Move the require inside the mobile-only component body so it never evaluates on desktop. --- shared/provision/code-page/qr-scan/scanner.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shared/provision/code-page/qr-scan/scanner.tsx b/shared/provision/code-page/qr-scan/scanner.tsx index e22a9b95f37c..55a2ad9d524a 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,8 @@ type Props = { } const QRScannerMobile = (p: Props): React.ReactElement | null => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const {CameraView, useCameraPermissions} = require('expo-camera') as typeof import('expo-camera') const [scanned, setScanned] = React.useState(false) const [permission, requestPermission] = useCameraPermissions() From f570901e6e2f00b626a405973afbdc41c361a2c7 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 29 May 2026 12:18:49 -0400 Subject: [PATCH 4/8] =?UTF-8?q?migrate=20CB/CB2=20=E2=86=92=20CB3:=20git,?= =?UTF-8?q?=20incoming-share,=20signup,=20provision,=20people,=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/git/index.tsx | 6 ++---- shared/git/row.tsx | 20 +++++++++----------- shared/incoming-share/index.tsx | 9 ++------- shared/people/follow-notification.tsx | 4 ++-- shared/provision/code-page/container.tsx | 4 ++-- shared/provision/troubleshooting.tsx | 15 +++++++-------- shared/settings/account/email-phone-row.tsx | 5 +++-- shared/settings/feedback/index.tsx | 7 ++----- shared/settings/files/index.tsx | 4 ++-- shared/settings/logout.tsx | 11 ++++------- shared/settings/routes.tsx | 5 +++-- shared/settings/sub-nav/settings-item.tsx | 10 +++++----- shared/signup/common.tsx | 6 ++---- shared/signup/phone-number/verify-body.tsx | 8 ++++---- skill/migrate-clickable-box/SKILL.md | 15 ++++++++------- 15 files changed, 57 insertions(+), 72 deletions(-) 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/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/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/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/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..fb719f284f6d 100644 --- a/shared/settings/sub-nav/settings-item.tsx +++ b/shared/settings/sub-nav/settings-item.tsx @@ -21,8 +21,11 @@ function SettingsItem(props: SettingsItemProps) { _onClick(type) } return ( - {props.iconComponent ? ( @@ -53,7 +56,7 @@ function SettingsItem(props: SettingsItemProps) { {!!props.badgeNumber && props.badgeNumber > 0 && ( )} - + ) } export default SettingsItem @@ -64,10 +67,7 @@ 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, 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/skill/migrate-clickable-box/SKILL.md b/skill/migrate-clickable-box/SKILL.md index 4f1925297054..209fc4062511 100644 --- a/skill/migrate-clickable-box/SKILL.md +++ b/skill/migrate-clickable-box/SKILL.md @@ -11,10 +11,9 @@ 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?, onMouseOver?, 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**. +`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 +78,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` gotcha:** CB3 applies the CSS class `box2_centered` (i.e. `align-self: center`) whenever neither `fullWidth` nor `fullHeight` is set. CB1 did NOT do this. If the original CB was expected to fill its parent, omitting `fullWidth={true}` will shrink it to content-width. Always check the parent layout context before omitting `fullWidth`. + ### Pattern C — CB with style that encodes flex layout ```tsx @@ -165,9 +166,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 From 07ddd03e2ae3d7ad7d0109095bc9070a90e698cb Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 29 May 2026 12:20:40 -0400 Subject: [PATCH 5/8] fix settings-item CB3: add fullWidth to prevent alignSelf:center shrinking list rows --- shared/settings/sub-nav/settings-item.tsx | 2 +- skill/migrate-clickable-box/SKILL.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/settings/sub-nav/settings-item.tsx b/shared/settings/sub-nav/settings-item.tsx index fb719f284f6d..bfb85c4897cf 100644 --- a/shared/settings/sub-nav/settings-item.tsx +++ b/shared/settings/sub-nav/settings-item.tsx @@ -26,6 +26,7 @@ function SettingsItem(props: SettingsItemProps) { direction="horizontal" alignItems="center" relative={true} + fullWidth={true} style={Kb.Styles.collapseStyles([styles.item, selected && styles.selected] as const)} > {props.iconComponent ? ( @@ -71,7 +72,6 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ }, isElectron: { height: 32, - width: '100%', }, isMobile: { borderBottomColor: Kb.Styles.globalColors.black_10, diff --git a/skill/migrate-clickable-box/SKILL.md b/skill/migrate-clickable-box/SKILL.md index 209fc4062511..f473702a08ce 100644 --- a/skill/migrate-clickable-box/SKILL.md +++ b/skill/migrate-clickable-box/SKILL.md @@ -86,7 +86,7 @@ Read each matched file fully before classifying. ``` -**⚠️ `box2_centered` gotcha:** CB3 applies the CSS class `box2_centered` (i.e. `align-self: center`) whenever neither `fullWidth` nor `fullHeight` is set. CB1 did NOT do this. If the original CB was expected to fill its parent, omitting `fullWidth={true}` will shrink it to content-width. Always check the parent layout context before omitting `fullWidth`. +**⚠️ `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 From 8aaa92a88385b213b66c02159566026367f88626 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 29 May 2026 12:44:06 -0400 Subject: [PATCH 6/8] fix ClickableBox3: forward missing props to both desktop and mobile Desktop: onMouseDown/Leave/Move/Up, onContextMenu, title, tooltip/data-tooltip. Mobile: onLayout, collapsable, pointerEvents. --- shared/common-adapters/box.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/shared/common-adapters/box.tsx b/shared/common-adapters/box.tsx index 64a7c9c8be10..fc9171f51534 100644 --- a/shared/common-adapters/box.tsx +++ b/shared/common-adapters/box.tsx @@ -318,31 +318,41 @@ export const ClickableBox3 = (p: ClickableBox3Props & {ref?: React.Ref} className={cn} + data-testid={testID} + data-tooltip={tooltip} onClick={onClick} + onContextMenu={onContextMenu} + onMouseDown={onMouseDown} + 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} From 3d3d219073767e03f731ae27410966aa77974070 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 29 May 2026 13:00:26 -0400 Subject: [PATCH 7/8] WIP --- plans/todo.md | 1 + shared/.maestro/e2e/flows/crypto-subtabs.yaml | 20 --------- shared/.maestro/e2e/flows/devices-view.yaml | 1 + shared/.maestro/e2e/flows/files-browse.yaml | 1 + .../e2e/flows/settings-navigation.yaml | 1 + shared/.maestro/e2e/flows/teams-browse.yaml | 1 + shared/tests/e2e/generate-electron-report.mts | 45 ++++++++++--------- shared/tests/e2e/generate-ios-report.mts | 13 +++--- 8 files changed, 35 insertions(+), 48 deletions(-) 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/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 = `
- current - baseline + current + baseline
BASELINE
NOW
` - } else if (c.screenshotB64) { - visual = `
${c.label}
` + } else if (c.screenshotPath) { + visual = `
${c.label}
` } 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 = `
- current - baseline + current + baseline
BASELINE
NOW
` } else if (r.screenshotPath) { - visual = `
${r.name}
` + visual = `
${r.name}
` } else if (r.failureScreenshotPath) { - visual = `
failure
` + visual = `
failure
` } else { visual = `
No screenshot
` } From 3b78e5a1dd3392cbb47d8d677f704d37a84abd89 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 29 May 2026 16:15:12 -0400 Subject: [PATCH 8/8] cb3 2 (#29263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * migrate CB/CB2 → CB3: profile * migrate CB/CB2 → CB3: tracker, menubar, app, router-v2 * clean up iconContainer: remove redundant clickable/flexBoxColumn, fold in icon size * update skill * add onMouseEnter to Box2Props and forward it in ClickableBox3 desktop * fix lint: suppress consistent-type-imports for expo-camera require cast * skill --- plans/clickablebox3.md | 24 ++-- shared/app/global-errors.tsx | 4 +- shared/app/runtime-stats.tsx | 10 +- shared/common-adapters/box.tsx | 4 +- shared/menubar/index.desktop.tsx | 105 +++++++++--------- shared/profile/add-to-team.tsx | 4 +- shared/profile/edit-avatar/index.tsx | 11 +- shared/profile/generic/proofs-list.tsx | 11 +- shared/profile/user/actions/index.tsx | 12 +- shared/profile/user/friend.tsx | 15 ++- shared/profile/user/index.tsx | 30 ++--- shared/profile/user/teams/index.tsx | 14 +-- shared/profile/user/teams/team-row.tsx | 24 ++-- .../provision/code-page/qr-scan/scanner.tsx | 1 + shared/router-v2/header/index.desktop.tsx | 42 ++++--- shared/router-v2/tab-bar.desktop.tsx | 101 ++++++++--------- shared/tracker/assertion.tsx | 45 ++++---- skill/migrate-clickable-box/SKILL.md | 9 +- 18 files changed, 231 insertions(+), 235 deletions(-) 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/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 fc9171f51534..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,7 +319,7 @@ export const ClickableBox3 = (p: ClickableBox3Props & {ref?: React.Ref 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/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/qr-scan/scanner.tsx b/shared/provision/code-page/qr-scan/scanner.tsx index 55a2ad9d524a..8de1aee64108 100644 --- a/shared/provision/code-page/qr-scan/scanner.tsx +++ b/shared/provision/code-page/qr-scan/scanner.tsx @@ -9,6 +9,7 @@ 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/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/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 f473702a08ce..3a340d9ad9e0 100644 --- a/skill/migrate-clickable-box/SKILL.md +++ b/skill/migrate-clickable-box/SKILL.md @@ -11,7 +11,9 @@ 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: `Box2Props & {onClick?, onLongPress?, onMouseOver?, hitSlop?}` — **`direction` is required.** +Type: `Box2Props & {onClick?, onLongPress?, hitSlop?}` — **`direction` is required.** + +`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. @@ -110,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. @@ -133,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