mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-09 01:45:00 +00:00
fix(app): show the archive undo toast in the sidebar, with only Undo pressable
The archive button sat 2px from the kebab, so a press aimed at the menu could archive the workspace. The toast card was a Pressable used only to pause on hover, which made the whole card look clickable. Toasts raised by the workspace list now land above the sidebar footer, next to the row that raised them. A collapsed sidebar stays mounted and its rows still handle the archive shortcut, so while it is hidden those toasts go to the app host. A toast leaves when the sidebar hides because its dismissal is what ends an archive's undo window.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import { useEffect, useMemo, useRef, type ReactNode } from "react";
|
||||
import { View } from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { ToastViewport, useToastHost, type ToastApi } from "@/components/toast-host";
|
||||
import { ToastApiProvider, useToast } from "@/contexts/toast-context";
|
||||
|
||||
/**
|
||||
* Shows toasts raised by the sidebar's list over its bottom edge, just above the footer.
|
||||
*
|
||||
* A collapsed sidebar stays mounted and its rows still handle shortcuts, so while it is hidden
|
||||
* their toasts go to the app's host instead. The sidebar's API stays stable across open and close
|
||||
* so that toggling the panel doesn't re-render every row.
|
||||
*/
|
||||
export function SidebarToastRegion({ active, children }: { active: boolean; children: ReactNode }) {
|
||||
const appToast = useToast();
|
||||
const { api: sidebarToast, toast, dismiss } = useToastHost();
|
||||
const activeRef = useRef(active);
|
||||
|
||||
useEffect(() => {
|
||||
activeRef.current = active;
|
||||
// A toast must not outlive its visibility: its dismissal is what ends an archive's undo.
|
||||
return active ? dismiss : undefined;
|
||||
}, [active, dismiss]);
|
||||
|
||||
const api = useMemo<ToastApi>(() => {
|
||||
const visibleHost = () => (activeRef.current ? sidebarToast : appToast);
|
||||
return {
|
||||
show: (content, options) => visibleHost().show(content, options),
|
||||
copied: (label) => visibleHost().copied(label),
|
||||
error: (message) => visibleHost().error(message),
|
||||
};
|
||||
}, [appToast, sidebarToast]);
|
||||
|
||||
return (
|
||||
<View style={styles.region}>
|
||||
<ToastApiProvider api={api}>{children}</ToastApiProvider>
|
||||
<ToastViewport toast={toast} onDismiss={dismiss} placement="sidebar" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
region: {
|
||||
position: "relative",
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
},
|
||||
});
|
||||
@@ -74,7 +74,11 @@ const pinLeadingIcon = <ThemedPin size={14} uniProps={foregroundMutedColorMappin
|
||||
const unpinLeadingIcon = <ThemedPinOff size={14} uniProps={foregroundMutedColorMapping} />;
|
||||
|
||||
/** Pointer-only, so it is always the kebab's pointer size. */
|
||||
export const ARCHIVE_BUTTON_SIZE = 18;
|
||||
const ARCHIVE_BUTTON_SIZE = 18;
|
||||
/** Keeps a press aimed at the kebab from landing on the one-click archive. */
|
||||
const ARCHIVE_BUTTON_KEBAB_GAP = 8;
|
||||
/** The width the archive button and its gap take up left of the kebab. */
|
||||
export const ARCHIVE_BUTTON_FOOTPRINT = ARCHIVE_BUTTON_SIZE + ARCHIVE_BUTTON_KEBAB_GAP;
|
||||
|
||||
const triggerIconProps = densityIconProps("sm", foregroundMutedColorMapping);
|
||||
const triggerIconHoveredProps = densityIconProps("sm", foregroundColorMapping);
|
||||
@@ -482,7 +486,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 4,
|
||||
marginLeft: 2,
|
||||
// Center the touch box on the 20px title line it is anchored to.
|
||||
marginTop: bySizing({ touch: -6, pointer: 0 }),
|
||||
// MoreVertical paints only around the center of its SVG. Keep the padded hit box, but
|
||||
@@ -493,10 +496,11 @@ const styles = StyleSheet.create((theme) => ({
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
// The kebab's box without its offsets: the archive glyph fills its view box, so it needs
|
||||
// none of the nudging the three dots do. The kebab's marginLeft is the gap between them.
|
||||
// none of the nudging the three dots do.
|
||||
archiveButton: {
|
||||
width: ARCHIVE_BUTTON_SIZE,
|
||||
height: ARCHIVE_BUTTON_SIZE,
|
||||
marginRight: ARCHIVE_BUTTON_KEBAB_GAP,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 4,
|
||||
|
||||
@@ -31,7 +31,7 @@ import { resolveSidebarWorkspacePrimaryLabel } from "@/components/sidebar/sideba
|
||||
import { DensityIcon } from "@/components/ui/density-icon";
|
||||
import { SCRIM_WIDTH, TrailingActionScrim } from "@/components/ui/trailing-action-scrim";
|
||||
import {
|
||||
ARCHIVE_BUTTON_SIZE,
|
||||
ARCHIVE_BUTTON_FOOTPRINT,
|
||||
SidebarWorkspaceArchiveButton,
|
||||
} from "@/components/sidebar/sidebar-workspace-menu";
|
||||
import { useWorkspaceLabelDefinitions } from "@/workspace-labels";
|
||||
@@ -446,9 +446,6 @@ export function SidebarWorkspaceTrailingActionBase({
|
||||
);
|
||||
}
|
||||
|
||||
// The archive button plus the kebab's marginLeft, which is the gap between them.
|
||||
const ARCHIVE_BUTTON_SCRIM_WIDTH = ARCHIVE_BUTTON_SIZE + 2;
|
||||
|
||||
export function SidebarWorkspaceTrailingActionOverlay({
|
||||
visible,
|
||||
scrimBackdrop,
|
||||
@@ -473,7 +470,7 @@ export function SidebarWorkspaceTrailingActionOverlay({
|
||||
{scrimBackdrop ? (
|
||||
<TrailingActionScrim
|
||||
backdrop={scrimBackdrop}
|
||||
width={onArchiveButtonPress ? SCRIM_WIDTH + ARCHIVE_BUTTON_SCRIM_WIDTH : SCRIM_WIDTH}
|
||||
width={onArchiveButtonPress ? SCRIM_WIDTH + ARCHIVE_BUTTON_FOOTPRINT : SCRIM_WIDTH}
|
||||
testID="sidebar-workspace-trailing-scrim"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user