mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-04 23:44:41 +00:00
* 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
153 lines
4.4 KiB
TypeScript
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],
|
|
},
|
|
}));
|