feat: sortable workspace tabs, tab context menus, and terminal auto-create removal
- Add drag-and-drop tab reordering with persistent tab order - Add right-click context menu with copy agent ID, copy resume command, close to right - Switch workspace route encoding from percent-encoding to base64url - Remove terminal auto-creation on directory listing (explicit create only) - Add swipe gestures on terminal emulator for mobile sidebar navigation - Highlight active workspace row in sidebar
This commit is contained in:
@@ -0,0 +1,707 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type PropsWithChildren,
|
||||
type ReactElement,
|
||||
} from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
Modal,
|
||||
Platform,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StatusBar,
|
||||
Text,
|
||||
View,
|
||||
type PressableProps,
|
||||
type StyleProp,
|
||||
type ViewStyle,
|
||||
} from "react-native";
|
||||
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { Check, CheckCircle } from "lucide-react-native";
|
||||
|
||||
// Keep parity with dropdown-menu action statuses.
|
||||
export type ActionStatus = "idle" | "pending" | "success";
|
||||
|
||||
type Placement = "top" | "bottom" | "left" | "right";
|
||||
type Alignment = "start" | "center" | "end";
|
||||
|
||||
interface Rect {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
type ContextMenuContextValue = {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
triggerRef: React.RefObject<View | null>;
|
||||
anchorRect: Rect | null;
|
||||
setAnchorRect: (rect: Rect | null) => void;
|
||||
};
|
||||
|
||||
const ContextMenuContext = createContext<ContextMenuContextValue | null>(null);
|
||||
|
||||
function useContextMenuContext(componentName: string): ContextMenuContextValue {
|
||||
const ctx = useContext(ContextMenuContext);
|
||||
if (!ctx) {
|
||||
throw new Error(`${componentName} must be used within <ContextMenu />`);
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
function useControllableOpenState({
|
||||
open,
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}): [boolean, (next: boolean) => void] {
|
||||
const [internalOpen, setInternalOpen] = useState(Boolean(defaultOpen));
|
||||
const isControlled = typeof open === "boolean";
|
||||
const value = isControlled ? Boolean(open) : internalOpen;
|
||||
const setValue = useCallback(
|
||||
(next: boolean) => {
|
||||
if (!isControlled) setInternalOpen(next);
|
||||
onOpenChange?.(next);
|
||||
},
|
||||
[isControlled, onOpenChange]
|
||||
);
|
||||
return [value, setValue];
|
||||
}
|
||||
|
||||
function measureElement(element: View): Promise<Rect> {
|
||||
return new Promise((resolve) => {
|
||||
element.measureInWindow((x, y, width, height) => {
|
||||
resolve({ x, y, width, height });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function computePosition({
|
||||
triggerRect,
|
||||
contentSize,
|
||||
displayArea,
|
||||
placement,
|
||||
alignment,
|
||||
offset,
|
||||
}: {
|
||||
triggerRect: Rect;
|
||||
contentSize: { width: number; height: number };
|
||||
displayArea: Rect;
|
||||
placement: Placement;
|
||||
alignment: Alignment;
|
||||
offset: number;
|
||||
}): { x: number; y: number; actualPlacement: Placement } {
|
||||
const { width: contentWidth, height: contentHeight } = contentSize;
|
||||
|
||||
// Calculate available space
|
||||
const spaceTop = triggerRect.y - displayArea.y;
|
||||
const spaceBottom =
|
||||
displayArea.y + displayArea.height - (triggerRect.y + triggerRect.height);
|
||||
|
||||
// Flip if needed
|
||||
let actualPlacement = placement;
|
||||
if (placement === "bottom" && spaceBottom < contentHeight && spaceTop > spaceBottom) {
|
||||
actualPlacement = "top";
|
||||
} else if (placement === "top" && spaceTop < contentHeight && spaceBottom > spaceTop) {
|
||||
actualPlacement = "bottom";
|
||||
}
|
||||
|
||||
let x: number;
|
||||
let y: number;
|
||||
|
||||
// Position based on placement
|
||||
if (actualPlacement === "bottom") {
|
||||
y = triggerRect.y + triggerRect.height + offset;
|
||||
} else if (actualPlacement === "top") {
|
||||
y = triggerRect.y - contentHeight - offset;
|
||||
} else if (actualPlacement === "left") {
|
||||
x = triggerRect.x - contentWidth - offset;
|
||||
y = triggerRect.y;
|
||||
} else {
|
||||
x = triggerRect.x + triggerRect.width + offset;
|
||||
y = triggerRect.y;
|
||||
}
|
||||
|
||||
// Alignment
|
||||
if (actualPlacement === "top" || actualPlacement === "bottom") {
|
||||
if (alignment === "start") {
|
||||
x = triggerRect.x;
|
||||
} else if (alignment === "end") {
|
||||
x = triggerRect.x + triggerRect.width - contentWidth;
|
||||
} else {
|
||||
x = triggerRect.x + (triggerRect.width - contentWidth) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Constrain to screen
|
||||
const padding = 8;
|
||||
x = Math.max(padding, Math.min(displayArea.width - contentWidth - padding, x!));
|
||||
y = Math.max(
|
||||
displayArea.y + padding,
|
||||
Math.min(displayArea.y + displayArea.height - contentHeight - padding, y!)
|
||||
);
|
||||
|
||||
return { x, y, actualPlacement };
|
||||
}
|
||||
|
||||
function coerceEventPoint(event: unknown): { pageX: number; pageY: number } | null {
|
||||
const nativeEvent: any = (event as any)?.nativeEvent ?? event;
|
||||
const pageX = nativeEvent?.pageX;
|
||||
const pageY = nativeEvent?.pageY;
|
||||
if (typeof pageX === "number" && typeof pageY === "number") {
|
||||
return { pageX, pageY };
|
||||
}
|
||||
const clientX = nativeEvent?.clientX;
|
||||
const clientY = nativeEvent?.clientY;
|
||||
if (typeof clientX === "number" && typeof clientY === "number") {
|
||||
return { pageX: clientX, pageY: clientY };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ContextMenu({
|
||||
open,
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
open?: boolean;
|
||||
defaultOpen?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}>): ReactElement {
|
||||
const triggerRef = useRef<View>(null);
|
||||
const [isOpen, setIsOpen] = useControllableOpenState({
|
||||
open,
|
||||
defaultOpen,
|
||||
onOpenChange,
|
||||
});
|
||||
const [anchorRect, setAnchorRect] = useState<Rect | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setAnchorRect(null);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const value = useMemo<ContextMenuContextValue>(
|
||||
() => ({
|
||||
open: isOpen,
|
||||
setOpen: setIsOpen,
|
||||
triggerRef,
|
||||
anchorRect,
|
||||
setAnchorRect,
|
||||
}),
|
||||
[anchorRect, isOpen, setAnchorRect, setIsOpen]
|
||||
);
|
||||
|
||||
return <ContextMenuContext.Provider value={value}>{children}</ContextMenuContext.Provider>;
|
||||
}
|
||||
|
||||
type TriggerState = { pressed: boolean; hovered: boolean; open: boolean };
|
||||
type TriggerStyleProp = StyleProp<ViewStyle> | ((state: TriggerState) => StyleProp<ViewStyle>);
|
||||
|
||||
export function ContextMenuTrigger({
|
||||
children,
|
||||
disabled,
|
||||
style,
|
||||
enabled = true,
|
||||
enabledOnMobile = false,
|
||||
enabledOnWeb = true,
|
||||
longPressDelayMs,
|
||||
...props
|
||||
}: PropsWithChildren<
|
||||
Omit<PressableProps, "style"> & {
|
||||
style?: TriggerStyleProp;
|
||||
enabled?: boolean;
|
||||
enabledOnMobile?: boolean;
|
||||
enabledOnWeb?: boolean;
|
||||
longPressDelayMs?: number;
|
||||
}
|
||||
>): ReactElement {
|
||||
const ctx = useContextMenuContext("ContextMenuTrigger");
|
||||
|
||||
const shouldEnableOnThisPlatform =
|
||||
enabled &&
|
||||
(Platform.OS === "web" ? enabledOnWeb : enabledOnMobile);
|
||||
|
||||
const openAtEvent = useCallback(
|
||||
(event: unknown) => {
|
||||
if (!shouldEnableOnThisPlatform || disabled) {
|
||||
return;
|
||||
}
|
||||
const point = coerceEventPoint(event);
|
||||
if (!point) {
|
||||
return;
|
||||
}
|
||||
const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0;
|
||||
ctx.setAnchorRect({
|
||||
x: point.pageX,
|
||||
y: point.pageY + statusBarHeight,
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
ctx.setOpen(true);
|
||||
},
|
||||
[ctx, disabled, shouldEnableOnThisPlatform]
|
||||
);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
{...props}
|
||||
ref={ctx.triggerRef}
|
||||
collapsable={false}
|
||||
disabled={disabled}
|
||||
delayLongPress={longPressDelayMs}
|
||||
onLongPress={(event) => {
|
||||
if (Platform.OS === "web") {
|
||||
props.onLongPress?.(event);
|
||||
return;
|
||||
}
|
||||
openAtEvent(event);
|
||||
props.onLongPress?.(event);
|
||||
}}
|
||||
// @ts-ignore - onContextMenu is web-only and not in RN types.
|
||||
onContextMenu={(event: unknown) => {
|
||||
if (Platform.OS !== "web") {
|
||||
return;
|
||||
}
|
||||
const e: any = event;
|
||||
e?.preventDefault?.();
|
||||
e?.stopPropagation?.();
|
||||
openAtEvent(event);
|
||||
}}
|
||||
style={({ pressed, hovered = false }) => {
|
||||
if (typeof style === "function") {
|
||||
return style({ pressed, hovered: Boolean(hovered), open: ctx.open });
|
||||
}
|
||||
return style;
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
export function ContextMenuContent({
|
||||
children,
|
||||
side = "bottom",
|
||||
align = "start",
|
||||
offset = 4,
|
||||
width,
|
||||
minWidth = 180,
|
||||
maxWidth,
|
||||
fullWidth = false,
|
||||
horizontalPadding = 16,
|
||||
testID,
|
||||
}: PropsWithChildren<{
|
||||
side?: Placement;
|
||||
align?: Alignment;
|
||||
offset?: number;
|
||||
width?: number;
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
fullWidth?: boolean;
|
||||
horizontalPadding?: number;
|
||||
testID?: string;
|
||||
}>): ReactElement | null {
|
||||
const { open, setOpen, triggerRef, anchorRect } = useContextMenuContext("ContextMenuContent");
|
||||
const [triggerRect, setTriggerRect] = useState<Rect | null>(null);
|
||||
const [contentSize, setContentSize] = useState<{ width: number; height: number } | null>(null);
|
||||
const [position, setPosition] = useState<{ x: number; y: number } | null>(null);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
// Measure trigger when opening (fallback) and capture point anchors.
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setTriggerRect(null);
|
||||
setContentSize(null);
|
||||
setPosition(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (anchorRect) {
|
||||
setTriggerRect(anchorRect);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!triggerRef.current) {
|
||||
setTriggerRect(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const statusBarHeight = Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0;
|
||||
let cancelled = false;
|
||||
|
||||
measureElement(triggerRef.current).then((rect) => {
|
||||
if (cancelled) return;
|
||||
setTriggerRect({
|
||||
...rect,
|
||||
y: rect.y + statusBarHeight,
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [anchorRect, open, triggerRef]);
|
||||
|
||||
// Calculate position when we have both measurements
|
||||
useEffect(() => {
|
||||
if (!triggerRect || !contentSize) return;
|
||||
|
||||
const { width: screenWidth, height: screenHeight } = Dimensions.get("window");
|
||||
const displayArea = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: screenWidth,
|
||||
height: screenHeight,
|
||||
};
|
||||
|
||||
const result = computePosition({
|
||||
triggerRect,
|
||||
contentSize,
|
||||
displayArea,
|
||||
placement: side,
|
||||
alignment: align,
|
||||
offset,
|
||||
});
|
||||
|
||||
const x = fullWidth ? horizontalPadding : result.x;
|
||||
setPosition({ x, y: result.y });
|
||||
}, [triggerRect, contentSize, side, align, offset, fullWidth, horizontalPadding]);
|
||||
|
||||
const handleContentLayout = useCallback(
|
||||
(event: { nativeEvent: { layout: { width: number; height: number } } }) => {
|
||||
const { width: w, height: h } = event.nativeEvent.layout;
|
||||
setContentSize({ width: w, height: h });
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const { width: screenWidth } = Dimensions.get("window");
|
||||
const resolvedWidthStyle: ViewStyle = fullWidth
|
||||
? { width: screenWidth - horizontalPadding * 2 }
|
||||
: {
|
||||
...(typeof width === "number" ? { width } : null),
|
||||
...(typeof minWidth === "number" ? { minWidth } : null),
|
||||
...(typeof maxWidth === "number" ? { maxWidth } : null),
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={open}
|
||||
transparent
|
||||
animationType="none"
|
||||
statusBarTranslucent={Platform.OS === "android"}
|
||||
onRequestClose={handleClose}
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Menu backdrop"
|
||||
style={styles.backdrop}
|
||||
onPress={handleClose}
|
||||
testID={testID ? `${testID}-backdrop` : undefined}
|
||||
/>
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(100)}
|
||||
exiting={FadeOut.duration(100)}
|
||||
collapsable={false}
|
||||
testID={testID}
|
||||
onLayout={handleContentLayout}
|
||||
style={[
|
||||
styles.content,
|
||||
resolvedWidthStyle,
|
||||
{
|
||||
position: "absolute",
|
||||
top: position?.y ?? -9999,
|
||||
left: position?.x ?? -9999,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ScrollView bounces={false} showsVerticalScrollIndicator contentContainerStyle={{ flexGrow: 1 }}>
|
||||
{children}
|
||||
</ScrollView>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function ContextMenuLabel({
|
||||
children,
|
||||
style,
|
||||
testID,
|
||||
}: PropsWithChildren<{ style?: ViewStyle | ViewStyle[]; testID?: string }>): ReactElement {
|
||||
return (
|
||||
<View style={[styles.labelContainer, style]} testID={testID}>
|
||||
<Text style={styles.labelText}>{children}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function ContextMenuSeparator({ style, testID }: { style?: ViewStyle; testID?: string }): ReactElement {
|
||||
return <View style={[styles.separator, style]} testID={testID} />;
|
||||
}
|
||||
|
||||
export function ContextMenuHint({
|
||||
children,
|
||||
testID,
|
||||
}: PropsWithChildren<{ testID?: string }>): ReactElement {
|
||||
return (
|
||||
<View style={styles.hintContainer} testID={testID}>
|
||||
<Text style={styles.hintText}>{children}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function ContextMenuItem({
|
||||
children,
|
||||
description,
|
||||
onSelect,
|
||||
disabled,
|
||||
destructive,
|
||||
selected,
|
||||
showSelectedCheck = false,
|
||||
selectedVariant = "default",
|
||||
leading,
|
||||
trailing,
|
||||
loading,
|
||||
status,
|
||||
pendingLabel,
|
||||
successLabel,
|
||||
closeOnSelect = true,
|
||||
testID,
|
||||
}: PropsWithChildren<{
|
||||
description?: string;
|
||||
onSelect?: () => void;
|
||||
disabled?: boolean;
|
||||
destructive?: boolean;
|
||||
selected?: boolean;
|
||||
showSelectedCheck?: boolean;
|
||||
selectedVariant?: "default" | "accent";
|
||||
leading?: ReactElement | null;
|
||||
trailing?: ReactElement | null;
|
||||
/** @deprecated Use `status` instead */
|
||||
loading?: boolean;
|
||||
status?: ActionStatus;
|
||||
pendingLabel?: string;
|
||||
successLabel?: string;
|
||||
closeOnSelect?: boolean;
|
||||
testID?: string;
|
||||
}>): ReactElement {
|
||||
const { theme } = useUnistyles();
|
||||
const { setOpen } = useContextMenuContext("ContextMenuItem");
|
||||
|
||||
const isPending = status === "pending" || loading;
|
||||
const isSuccess = status === "success";
|
||||
const isDisabled = disabled || isPending || isSuccess;
|
||||
|
||||
let leadingContent: ReactElement | null = null;
|
||||
if (isPending) {
|
||||
leadingContent = <ActivityIndicator size={16} color={theme.colors.foregroundMuted} />;
|
||||
} else if (isSuccess) {
|
||||
leadingContent = <CheckCircle size={16} color={theme.colors.palette.green[500]} />;
|
||||
} else if (leading) {
|
||||
leadingContent = leading;
|
||||
}
|
||||
|
||||
let label = children;
|
||||
if (isPending && pendingLabel) {
|
||||
label = pendingLabel;
|
||||
} else if (isSuccess && successLabel) {
|
||||
label = successLabel;
|
||||
}
|
||||
|
||||
const trailingContent =
|
||||
trailing ?? (!showSelectedCheck && selected ? <Check size={16} color={theme.colors.foregroundMuted} /> : null);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
testID={testID}
|
||||
accessibilityRole="button"
|
||||
disabled={isDisabled}
|
||||
onPress={() => {
|
||||
if (isDisabled) return;
|
||||
if (closeOnSelect) {
|
||||
setOpen(false);
|
||||
}
|
||||
onSelect?.();
|
||||
}}
|
||||
style={({ pressed, hovered }) => [
|
||||
styles.item,
|
||||
selected ? (selectedVariant === "accent" ? styles.itemSelectedAccent : styles.itemSelected) : null,
|
||||
selected && (hovered || pressed) && selectedVariant !== "accent" ? styles.itemSelectedInteractive : null,
|
||||
isDisabled ? styles.itemDisabled : null,
|
||||
hovered && !pressed && !isDisabled ? styles.itemHovered : null,
|
||||
pressed && !isDisabled ? styles.itemPressed : null,
|
||||
]}
|
||||
>
|
||||
{showSelectedCheck ? (
|
||||
<View style={styles.checkSlot}>
|
||||
{selected ? <Check size={16} color={theme.colors.foreground} /> : null}
|
||||
</View>
|
||||
) : null}
|
||||
{leadingContent ? <View style={styles.leadingSlot}>{leadingContent}</View> : null}
|
||||
<View style={styles.itemContent}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
styles.itemText,
|
||||
destructive && !isSuccess ? styles.itemTextDestructive : null,
|
||||
isSuccess ? styles.itemTextSuccess : null,
|
||||
selected && selectedVariant === "accent" ? styles.itemTextSelectedAccent : null,
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
{description && !isPending && !isSuccess ? (
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[
|
||||
styles.itemDescription,
|
||||
selected && selectedVariant === "accent" ? styles.itemDescriptionSelectedAccent : null,
|
||||
]}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
{trailingContent ? <View style={styles.trailingSlot}>{trailingContent}</View> : null}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
},
|
||||
backdrop: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
content: {
|
||||
backgroundColor: theme.colors.surface0,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 8,
|
||||
elevation: 8,
|
||||
overflow: "hidden",
|
||||
},
|
||||
labelContainer: {
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingTop: theme.spacing[2],
|
||||
paddingBottom: theme.spacing[1],
|
||||
},
|
||||
labelText: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
color: theme.colors.foregroundMuted,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.6,
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
backgroundColor: theme.colors.border,
|
||||
},
|
||||
hintContainer: {
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingBottom: theme.spacing[2],
|
||||
},
|
||||
hintText: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
item: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
minHeight: 36,
|
||||
gap: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[2],
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: "transparent",
|
||||
},
|
||||
itemHovered: {
|
||||
backgroundColor: theme.colors.surface1,
|
||||
},
|
||||
itemPressed: {
|
||||
backgroundColor: theme.colors.surface1,
|
||||
},
|
||||
itemDisabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
itemSelected: {
|
||||
backgroundColor: theme.colors.surface1,
|
||||
},
|
||||
itemSelectedInteractive: {
|
||||
backgroundColor: theme.colors.surface1,
|
||||
},
|
||||
itemSelectedAccent: {
|
||||
backgroundColor: theme.colors.accent,
|
||||
},
|
||||
checkSlot: {
|
||||
width: 16,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
leadingSlot: {
|
||||
width: 16,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
trailingSlot: {
|
||||
marginLeft: "auto",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
itemContent: {
|
||||
flexShrink: 1,
|
||||
},
|
||||
itemText: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
itemTextDestructive: {
|
||||
color: theme.colors.destructive,
|
||||
},
|
||||
itemTextSuccess: {
|
||||
color: theme.colors.palette.green[500],
|
||||
},
|
||||
itemTextSelectedAccent: {
|
||||
color: theme.colors.accentForeground,
|
||||
},
|
||||
itemDescription: {
|
||||
marginTop: 2,
|
||||
fontSize: theme.fontSize.xs,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
itemDescriptionSelectedAccent: {
|
||||
color: theme.colors.accentForeground,
|
||||
opacity: 0.85,
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user