Files
paseo/packages/app/src/components/ui/icon-button-chrome.ts
T
Mohamed Boudra 01f9bc8291 Modernize compact headers and sidebar affordances (#6431)
* feat(app): modernize compact headers and sidebar affordances

Use a shared island header with quick tab creation and keep sheet footers inside their visible layout. Opt sidebar rows into touch sizing without changing global appearance settings.

* test(app): match darker light-theme workspace selection

* test(app): align plugin journeys with compact header
2026-10-10 20:53:09 +01:00

153 lines
4.4 KiB
TypeScript

import { createContext, useCallback, useContext } from "react";
import type { StyleProp, ViewStyle } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import { HEADER_CONTROL_HEIGHT } from "@/components/ui/control-geometry";
import { ICON_SIZE } from "@/styles/theme";
export { extraMutedIconColorMapping, mutedIconColorMapping } from "@/components/ui/icon-color";
export type IconButtonChromeSize = "large" | "small";
const SMALL_ICON_BUTTON_SIZE = 20;
/** Width and height of a large icon button on compact layouts. */
export const COMPACT_LARGE_ICON_BUTTON_SIZE = 32;
const COMPACT_SMALL_ICON_BUTTON_SIZE = 32;
export interface IconButtonChromeState {
hovered?: boolean;
pressed?: boolean;
open?: boolean;
active?: boolean;
}
function resolveIconButtonFrame(size: IconButtonChromeSize, compact: boolean) {
if (size === "large") return styles.large;
return compact ? styles.smallCompact : styles.small;
}
interface IconButtonChromeOptions {
size: IconButtonChromeSize;
state?: IconButtonChromeState;
compact?: boolean;
disabled?: boolean;
/** A circular hitbox and highlight, for buttons inside a pill-shaped container. */
round?: boolean;
style?: StyleProp<ViewStyle>;
}
const IconButtonChromeContext = createContext<"default" | "header-island">("default");
/** A container owns the chrome of its icon actions, including custom menu and badge triggers. */
export const IconButtonChromeProvider = IconButtonChromeContext.Provider;
/** The same state callback works with Pressable, TooltipTrigger, and DropdownMenuTrigger. */
export function useIconButtonChromeStyle({
size,
compact,
disabled,
round,
style,
active,
}: Omit<IconButtonChromeOptions, "state"> & { active?: boolean }) {
const scope = useContext(IconButtonChromeContext);
return useCallback(
(state: IconButtonChromeState) =>
iconButtonChromeStyle({
size: scope === "header-island" ? "large" : size,
compact,
disabled,
round: scope === "header-island" || round,
style,
state: { ...state, active: active || state.active },
}),
[scope, size, compact, disabled, round, style, active],
);
}
/** Shared hitbox and interaction chrome for icon-only header and toolbar controls. */
export function iconButtonChromeStyle({
size,
state,
compact = false,
disabled = false,
round = false,
style,
}: IconButtonChromeOptions): StyleProp<ViewStyle> {
const highlighted = state?.active || state?.hovered || state?.pressed || state?.open;
return [
resolveIconButtonFrame(size, compact),
round ? styles.round : null,
style,
highlighted ? styles.highlighted : null,
disabled ? styles.disabled : null,
];
}
/** Frame-only form for non-interactive placeholders that must preserve header alignment. */
export function iconButtonChromeFrameStyle(
size: IconButtonChromeSize,
compact = false,
): StyleProp<ViewStyle> {
return resolveIconButtonFrame(size, compact);
}
export function iconButtonChromeGlyphSize(size: IconButtonChromeSize, compact = false): number {
if (size === "large") return ICON_SIZE.md;
return compact ? 18 : 14;
}
export function smallIconButtonChromeFrameSize(compact = false): number {
return compact ? COMPACT_SMALL_ICON_BUTTON_SIZE : SMALL_ICON_BUTTON_SIZE;
}
const styles = StyleSheet.create((theme) => ({
large: {
width: {
xs: COMPACT_LARGE_ICON_BUTTON_SIZE,
md: HEADER_CONTROL_HEIGHT,
},
height: {
xs: COMPACT_LARGE_ICON_BUTTON_SIZE,
md: HEADER_CONTROL_HEIGHT,
},
padding: 0,
borderRadius: theme.borderRadius.md,
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
outlineWidth: 0,
outlineColor: "transparent",
},
small: {
width: SMALL_ICON_BUTTON_SIZE,
height: SMALL_ICON_BUTTON_SIZE,
padding: 0,
borderRadius: theme.borderRadius.md,
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
outlineWidth: 0,
outlineColor: "transparent",
},
smallCompact: {
width: COMPACT_SMALL_ICON_BUTTON_SIZE,
height: COMPACT_SMALL_ICON_BUTTON_SIZE,
padding: 0,
borderRadius: theme.borderRadius.md,
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
outlineWidth: 0,
outlineColor: "transparent",
},
round: {
borderRadius: theme.borderRadius.full,
},
highlighted: {
backgroundColor: theme.colors.interactionHighlight,
},
disabled: {
opacity: theme.opacity[50],
},
}));