mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-01 06:02:56 +00:00
feat(app): polish compact header, composer, and settings menu (#7129)
Header islands lose their filled background and shadow; the round tap highlight stays. The floating header's fade now carries the icon contrast the island fill used to give, so it stays solid through the header and runs a longer tail into the content. The fade lives inside the overlay because Android clips children that overhang their parent, and keeps three gradient stops because Android rendered a four-stop version far more transparent than specified. Single-line header titles read one step larger. The compact composer gets a soft drop shadow. The compact settings root groups App and Host into islands of tiles, driven by breakpoint style values so desktop keeps its plain list.
This commit is contained in:
@@ -50,11 +50,13 @@ async function openNamedWorkspace(page: Page, names: typeof labels.long) {
|
||||
}
|
||||
|
||||
async function expectHeaderContentsFit(page: Page, info: TestInfo, long: boolean) {
|
||||
const menu = page.getByTestId("menu-button").filter({ visible: true });
|
||||
const title = page.getByTestId("workspace-header-title");
|
||||
const project = page.getByTestId("workspace-header-subtitle");
|
||||
const badge = page.getByTestId(`host-badge-${getServerId()}`).filter({ visible: true });
|
||||
const trigger = page.getByRole("button", { name: /Switch tabs/ });
|
||||
const measure = async () => ({
|
||||
menu: await menu.boundingBox(),
|
||||
title: await title.boundingBox(),
|
||||
project: await project.boundingBox(),
|
||||
badge: await badge.boundingBox(),
|
||||
@@ -66,23 +68,21 @@ async function expectHeaderContentsFit(page: Page, info: TestInfo, long: boolean
|
||||
await page.screenshot({ path: info.outputPath("header.png") });
|
||||
await expect(async () => {
|
||||
const bounds = await measure();
|
||||
expect(bounds.menu).not.toBeNull();
|
||||
expect(bounds.title).not.toBeNull();
|
||||
expect(bounds.project).not.toBeNull();
|
||||
expect(bounds.badge).not.toBeNull();
|
||||
expect(bounds.icon).not.toBeNull();
|
||||
expect(bounds.trigger).not.toBeNull();
|
||||
// Approved geometry: 12px outside margin, 46px leading island, 8px title gap.
|
||||
expect(bounds.title!.x).toBe(66);
|
||||
// The labels sit between the menu button and the actions without overlapping either.
|
||||
expect(bounds.title!.x).toBeGreaterThan(bounds.menu!.x + bounds.menu!.width);
|
||||
expect(bounds.project!.x).toBe(bounds.title!.x);
|
||||
expect(bounds.trigger!.width).toBe(32);
|
||||
expect(bounds.trigger!.height).toBe(32);
|
||||
// The action frame starts 7px inside its island; preserve the 8px gap before it.
|
||||
const contentRight = bounds.trigger!.x - 15;
|
||||
for (const box of [bounds.title!, bounds.project!, bounds.badge!, bounds.icon!]) {
|
||||
expect(box.x + box.width).toBeLessThanOrEqual(contentRight + 1);
|
||||
expect(box.x + box.width).toBeLessThan(bounds.trigger!.x);
|
||||
}
|
||||
expect(bounds.project!.width).toBeGreaterThan(20);
|
||||
expect(bounds.icon!.width).toBe(12);
|
||||
// Long labels truncate instead of squeezing the host icon.
|
||||
expect(bounds.icon!.width).toBe(bounds.icon!.height);
|
||||
expect(bounds.badge!.x).toBeGreaterThan(bounds.project!.x + bounds.project!.width);
|
||||
expect(bounds.titleTruncated).toBe(long);
|
||||
expect(bounds.projectTruncated).toBe(long);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useCallback, useId, useState, type ReactNode } from "react";
|
||||
import { useCallback, useState, type ReactNode } from "react";
|
||||
import { View, type LayoutChangeEvent } from "react-native";
|
||||
import Svg, { Defs, LinearGradient as SvgLinearGradient, Rect, Stop } from "react-native-svg";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { EdgeFade } from "@/components/ui/edge-fade";
|
||||
|
||||
/** How far the fade runs past the header's bottom edge, so content dissolves instead of clipping. */
|
||||
const FADE_OVERHANG = 24;
|
||||
const FADE_OVERHANG = 48;
|
||||
/** Share of the fade that stays opaque, so the header's title and icons keep contrast. */
|
||||
const FADE_SOLID_FRACTION = 0.57;
|
||||
|
||||
interface FloatingHeaderLayoutProps {
|
||||
header: ReactNode;
|
||||
@@ -29,51 +30,22 @@ export function FloatingHeaderLayout({ header, floating, children }: FloatingHea
|
||||
{floating ? null : header}
|
||||
<View style={styles.content}>{children(floating ? headerHeight : 0)}</View>
|
||||
{floating ? (
|
||||
<View style={styles.overlay} pointerEvents="box-none" onLayout={handleHeaderLayout}>
|
||||
<FloatingHeaderFade />
|
||||
{header}
|
||||
<View style={styles.overlay} pointerEvents="box-none">
|
||||
<EdgeFade
|
||||
edge="top"
|
||||
solidFraction={FADE_SOLID_FRACTION}
|
||||
backdrop="surfaceWorkspace"
|
||||
style={styles.fade}
|
||||
/>
|
||||
<View pointerEvents="box-none" onLayout={handleHeaderLayout}>
|
||||
{header}
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function FloatingHeaderFadeSvg({ gradientId, color }: { gradientId: string; color: string }) {
|
||||
return (
|
||||
<Svg width="100%" height="100%" preserveAspectRatio="none">
|
||||
<Defs>
|
||||
<SvgLinearGradient id={gradientId} x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
{/* Vary opacity rather than interpolating toward `transparent`, which crosses black in
|
||||
some engines and leaves a grey fringe. */}
|
||||
<Stop offset="0%" stopColor={color} stopOpacity={1} />
|
||||
<Stop offset="50%" stopColor={color} stopOpacity={0.97} />
|
||||
<Stop offset="75%" stopColor={color} stopOpacity={0.8} />
|
||||
<Stop offset="100%" stopColor={color} stopOpacity={0} />
|
||||
</SvgLinearGradient>
|
||||
</Defs>
|
||||
<Rect x="0" y="0" width="100%" height="100%" fill={`url(#${gradientId})`} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
const ThemedFloatingHeaderFadeSvg = withUnistyles(FloatingHeaderFadeSvg);
|
||||
const workspaceSurfaceColorMapping = (theme: Theme) => ({
|
||||
color: theme.colors.surfaceWorkspace,
|
||||
});
|
||||
|
||||
function FloatingHeaderFade() {
|
||||
// React-generated ids contain characters that are invalid inside SVG fragment references.
|
||||
const gradientId = `floating-header-fade-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
||||
return (
|
||||
<View style={styles.fade} pointerEvents="none">
|
||||
<ThemedFloatingHeaderFadeSvg
|
||||
gradientId={gradientId}
|
||||
uniProps={workspaceSurfaceColorMapping}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
flex: 1,
|
||||
@@ -88,12 +60,14 @@ const styles = StyleSheet.create({
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
// The fade's tail lives inside the overlay so Android does not clip it.
|
||||
paddingBottom: FADE_OVERHANG,
|
||||
},
|
||||
fade: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: -FADE_OVERHANG,
|
||||
bottom: 0,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@ import type { ShortcutKey } from "@/utils/format-shortcut";
|
||||
|
||||
const ThemedArrowLeft = withUnistyles(ArrowLeft);
|
||||
|
||||
const ISLAND_PADDING = 7;
|
||||
const NO_SHORTCUT: ShortcutKey[] = [];
|
||||
|
||||
interface CompactHeaderProps {
|
||||
@@ -37,8 +36,9 @@ interface CompactHeaderProps {
|
||||
|
||||
/**
|
||||
* The phone header: the navigation button and the screen's actions sit in islands, with the title
|
||||
* left-aligned between them. It has no background of its own, so it reads the same in flow and floating
|
||||
* over content (see `FloatingHeaderLayout`). Its height matches the old flat header, so anything
|
||||
* left-aligned between them. Neither the header nor its islands draw a background, so it reads the
|
||||
* same in flow and floating over content (see `FloatingHeaderLayout`, whose fade gives the icons
|
||||
* contrast). Its height matches the old flat header, so anything
|
||||
* offset by the header height still lines up.
|
||||
*/
|
||||
export function CompactHeader({
|
||||
@@ -90,7 +90,11 @@ function CompactHeaderTitle({
|
||||
return (
|
||||
<>
|
||||
{title ? (
|
||||
<Text style={styles.title} numberOfLines={1} testID={titleTestID}>
|
||||
<Text
|
||||
style={subtitle ? styles.title : styles.singleLineTitle}
|
||||
numberOfLines={1}
|
||||
testID={titleTestID}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
) : null}
|
||||
@@ -146,10 +150,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
island: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
padding: ISLAND_PADDING,
|
||||
borderRadius: theme.borderRadius.full,
|
||||
backgroundColor: theme.colors.surface2,
|
||||
...theme.shadow.sm,
|
||||
},
|
||||
title: {
|
||||
fontSize: theme.fontSize.base,
|
||||
@@ -157,6 +157,13 @@ const styles = StyleSheet.create((theme) => ({
|
||||
color: theme.colors.foreground,
|
||||
textAlign: "left",
|
||||
},
|
||||
// A title without a subtitle has the header to itself, so it reads one step larger.
|
||||
singleLineTitle: {
|
||||
fontSize: theme.fontSize.lg,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
color: theme.colors.foreground,
|
||||
textAlign: "left",
|
||||
},
|
||||
titleSkeleton: {
|
||||
width: 140,
|
||||
maxWidth: "100%",
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useId } from "react";
|
||||
import { View, type StyleProp, type ViewStyle } from "react-native";
|
||||
import Svg, { Defs, LinearGradient as SvgLinearGradient, Rect, Stop } from "react-native-svg";
|
||||
import { withUnistyles } from "react-native-unistyles";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
|
||||
type EdgeFadeBackdrop = "surface0" | "surfaceWorkspace";
|
||||
|
||||
const backdropColorMappings: Record<EdgeFadeBackdrop, (theme: Theme) => { color: string }> = {
|
||||
surface0: (theme) => ({ color: theme.colors.surface0 }),
|
||||
surfaceWorkspace: (theme) => ({ color: theme.colors.surfaceWorkspace }),
|
||||
};
|
||||
|
||||
interface EdgeFadeSvgProps {
|
||||
gradientId: string;
|
||||
color: string;
|
||||
edge: "top" | "bottom";
|
||||
solidFraction: number;
|
||||
}
|
||||
|
||||
function EdgeFadeSvg({ gradientId, color, edge, solidFraction }: EdgeFadeSvgProps) {
|
||||
const fromTop = edge === "top";
|
||||
return (
|
||||
<Svg width="100%" height="100%" preserveAspectRatio="none">
|
||||
<Defs>
|
||||
<SvgLinearGradient
|
||||
id={gradientId}
|
||||
x1="0%"
|
||||
y1={fromTop ? "0%" : "100%"}
|
||||
x2="0%"
|
||||
y2={fromTop ? "100%" : "0%"}
|
||||
>
|
||||
{/* Vary opacity rather than interpolating toward `transparent`, which crosses black in
|
||||
some engines and leaves a grey fringe. Keep it to three stops: Android rendered a
|
||||
four-stop version far more transparent than its stops specify. */}
|
||||
<Stop offset={0} stopColor={color} stopOpacity={1} />
|
||||
<Stop offset={solidFraction} stopColor={color} stopOpacity={1} />
|
||||
<Stop offset={1} stopColor={color} stopOpacity={0} />
|
||||
</SvgLinearGradient>
|
||||
</Defs>
|
||||
<Rect x="0" y="0" width="100%" height="100%" fill={`url(#${gradientId})`} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
const ThemedEdgeFadeSvg = withUnistyles(EdgeFadeSvg);
|
||||
|
||||
interface EdgeFadeProps {
|
||||
/** The solid edge; content dissolves moving away from it. */
|
||||
edge: "top" | "bottom";
|
||||
/** Share of the fade, measured from the solid edge, that stays fully opaque. */
|
||||
solidFraction: number;
|
||||
backdrop: EdgeFadeBackdrop;
|
||||
/** Position and size. Keep the fade inside its parent: Android clips overhanging children. */
|
||||
style: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
/** Dissolves content into the surface behind chrome that floats over it. */
|
||||
export function EdgeFade({ edge, solidFraction, backdrop, style }: EdgeFadeProps) {
|
||||
// React-generated ids contain characters that are invalid inside SVG fragment references.
|
||||
const gradientId = `edge-fade-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
||||
return (
|
||||
<View style={style} pointerEvents="none">
|
||||
<ThemedEdgeFadeSvg
|
||||
gradientId={gradientId}
|
||||
edge={edge}
|
||||
solidFraction={solidFraction}
|
||||
uniProps={backdropColorMappings[backdrop]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1862,11 +1862,12 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
const inputWrapperCombinedStyle = useMemo(
|
||||
() => [
|
||||
styles.inputWrapper,
|
||||
isCompact && !isFullscreen && styles.inputWrapperCompact,
|
||||
readOnly && styles.inputWrapperReadOnly,
|
||||
inputWrapperStyle,
|
||||
{ opacity: surfacePresentation.input.opacity },
|
||||
],
|
||||
[inputWrapperStyle, readOnly, surfacePresentation.input.opacity],
|
||||
[inputWrapperStyle, isCompact, isFullscreen, readOnly, surfacePresentation.input.opacity],
|
||||
);
|
||||
// `withUnistyles` maps this component's `style` into a `.hash > *` child
|
||||
// rule, which ties on specificity with react-native-web's own
|
||||
@@ -2223,6 +2224,10 @@ const styles = StyleSheet.create((theme: Theme) => ({
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
// Lifts the composer off the chat it sits under on phones.
|
||||
inputWrapperCompact: {
|
||||
boxShadow: "0px 4px 16px rgba(0, 0, 0, 0.2)",
|
||||
},
|
||||
// Dotted says "this surface is the same box, but there is nothing to type
|
||||
// into it" without swapping the border colour, which reads as an error state.
|
||||
inputWrapperReadOnly: {
|
||||
|
||||
@@ -120,6 +120,7 @@ import { MetadataGenerationPage } from "@/screens/settings/metadata-generation-p
|
||||
import ProjectsScreen from "@/screens/projects-screen";
|
||||
import ProjectSettingsScreen from "@/screens/project-settings-screen";
|
||||
import { SETTINGS_DESKTOP_SIDEBAR_WIDTH, useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { densityIconBox, densityIconSize } from "@/styles/density";
|
||||
import { useLocalDaemonServerId } from "@/hooks/use-is-local-daemon";
|
||||
import {
|
||||
type EnableBuiltInDaemonOption,
|
||||
@@ -758,7 +759,7 @@ function SidebarSectionButton({
|
||||
isSelected,
|
||||
onSelect,
|
||||
}: SidebarSectionButtonProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const { theme, rt } = useUnistyles();
|
||||
const handlePress = useCallback(() => {
|
||||
onSelect(itemId);
|
||||
}, [onSelect, itemId]);
|
||||
@@ -775,7 +776,7 @@ function SidebarSectionButton({
|
||||
style={isSelected ? selectedSidebarItemStyle : sidebarItemStyle}
|
||||
>
|
||||
<IconComponent
|
||||
size={theme.iconSize.md}
|
||||
size={densityIconSize(rt, "md")}
|
||||
color={isSelected ? theme.colors.foreground : theme.colors.foregroundMuted}
|
||||
strokeWidth={SIDEBAR_ICON_STROKE_WIDTH}
|
||||
/>
|
||||
@@ -801,7 +802,7 @@ function SidebarHostSectionButton({
|
||||
isSelected,
|
||||
onSelect,
|
||||
}: SidebarHostSectionButtonProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const { theme, rt } = useUnistyles();
|
||||
const handlePress = useCallback(() => {
|
||||
onSelect(itemId);
|
||||
}, [onSelect, itemId]);
|
||||
@@ -819,7 +820,7 @@ function SidebarHostSectionButton({
|
||||
style={isSelected ? selectedSidebarItemStyle : sidebarItemStyle}
|
||||
>
|
||||
<IconComponent
|
||||
size={theme.iconSize.md}
|
||||
size={densityIconSize(rt, "md")}
|
||||
color={isSelected ? theme.colors.foreground : theme.colors.foregroundMuted}
|
||||
strokeWidth={SIDEBAR_ICON_STROKE_WIDTH}
|
||||
/>
|
||||
@@ -912,6 +913,16 @@ function HostPicker({
|
||||
);
|
||||
}
|
||||
|
||||
/** A labelled group of sidebar rows. On compact layouts the rows sit together in one island. */
|
||||
function SidebarGroup({ label, children }: { label?: string; children: ReactNode }) {
|
||||
return (
|
||||
<View style={sidebarStyles.group}>
|
||||
{label ? <Text style={sidebarStyles.groupLabel}>{label}</Text> : null}
|
||||
<View style={sidebarStyles.island}>{children}</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
interface SettingsSidebarProps {
|
||||
view: SettingsView;
|
||||
onSelectSection: (section: SettingsSectionSlug) => void;
|
||||
@@ -933,7 +944,7 @@ function SettingsSidebar({
|
||||
activeHostServerId,
|
||||
layout,
|
||||
}: SettingsSidebarProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const { theme, rt } = useUnistyles();
|
||||
const { t } = useTranslation();
|
||||
const hosts = useHosts();
|
||||
const localServerId = useLocalDaemonServerId();
|
||||
@@ -960,8 +971,7 @@ function SettingsSidebar({
|
||||
|
||||
const sidebarBody = (
|
||||
<>
|
||||
<View style={sidebarStyles.list}>
|
||||
<Text style={sidebarStyles.groupLabel}>{t("settings.groups.app")}</Text>
|
||||
<SidebarGroup label={t("settings.groups.app")}>
|
||||
{items.map((item) => (
|
||||
<SidebarSectionButton
|
||||
key={item.id}
|
||||
@@ -972,10 +982,9 @@ function SettingsSidebar({
|
||||
onSelect={onSelectSection}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</SidebarGroup>
|
||||
{hasHosts ? (
|
||||
<View style={sidebarStyles.list}>
|
||||
<Text style={sidebarStyles.groupLabel}>{t("settings.groups.host")}</Text>
|
||||
<SidebarGroup label={t("settings.groups.host")}>
|
||||
<HostPicker
|
||||
activeServerId={activeHostServerId}
|
||||
sortedHosts={sortedHosts}
|
||||
@@ -993,9 +1002,9 @@ function SettingsSidebar({
|
||||
onSelect={onSelectHostSection}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</SidebarGroup>
|
||||
) : (
|
||||
<View style={sidebarStyles.list}>
|
||||
<SidebarGroup>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t("settings.addHost")}
|
||||
@@ -1004,7 +1013,7 @@ function SettingsSidebar({
|
||||
style={sidebarItemStyle}
|
||||
>
|
||||
<Plus
|
||||
size={theme.iconSize.md}
|
||||
size={densityIconSize(rt, "md")}
|
||||
color={theme.colors.foregroundMuted}
|
||||
strokeWidth={SIDEBAR_ICON_STROKE_WIDTH}
|
||||
/>
|
||||
@@ -1021,7 +1030,7 @@ function SettingsSidebar({
|
||||
style={sidebarItemStyle}
|
||||
>
|
||||
<Server
|
||||
size={theme.iconSize.md}
|
||||
size={densityIconSize(rt, "md")}
|
||||
color={theme.colors.foregroundMuted}
|
||||
strokeWidth={SIDEBAR_ICON_STROKE_WIDTH}
|
||||
/>
|
||||
@@ -1030,7 +1039,7 @@ function SettingsSidebar({
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
</SidebarGroup>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@@ -1572,26 +1581,33 @@ const sidebarStyles = StyleSheet.create((theme) => ({
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
},
|
||||
list: {
|
||||
// Compact values draw each group as an island of tiles; desktop keeps a plain list.
|
||||
group: {
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
gap: theme.spacing[0.5],
|
||||
gap: { xs: theme.spacing[1], md: theme.spacing[0.5] },
|
||||
},
|
||||
groupLabel: {
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
color: theme.colors.foregroundExtraMuted,
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
paddingHorizontal: { xs: theme.spacing[4], md: theme.spacing[2] },
|
||||
paddingVertical: theme.spacing[1],
|
||||
},
|
||||
island: {
|
||||
gap: { xs: 2, md: theme.spacing[0.5] },
|
||||
borderRadius: { xs: theme.borderRadius["2xl"], md: 0 },
|
||||
overflow: "hidden",
|
||||
},
|
||||
item: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
minHeight: 28,
|
||||
gap: { xs: theme.spacing[3], md: theme.spacing[2] },
|
||||
minHeight: { xs: 52, md: 28 },
|
||||
paddingVertical: theme.spacing[1],
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
paddingHorizontal: { xs: theme.spacing[4], md: theme.spacing[2] },
|
||||
borderRadius: { xs: 0, md: theme.borderRadius.lg },
|
||||
backgroundColor: { xs: theme.colors.surface1, md: "transparent" },
|
||||
},
|
||||
itemHovered: {
|
||||
backgroundColor: theme.colors.surfaceSidebarHover,
|
||||
@@ -1600,7 +1616,7 @@ const sidebarStyles = StyleSheet.create((theme) => ({
|
||||
backgroundColor: theme.colors.surfaceSidebarHover,
|
||||
},
|
||||
label: {
|
||||
fontSize: theme.fontSize.base,
|
||||
fontSize: { xs: theme.fontSize.lg, md: theme.fontSize.base },
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
flex: 1,
|
||||
@@ -1608,11 +1624,12 @@ const sidebarStyles = StyleSheet.create((theme) => ({
|
||||
pickerTrigger: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
minHeight: 28,
|
||||
gap: { xs: theme.spacing[3], md: theme.spacing[2] },
|
||||
minHeight: { xs: 52, md: 28 },
|
||||
paddingVertical: theme.spacing[1],
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
paddingHorizontal: { xs: theme.spacing[4], md: theme.spacing[2] },
|
||||
borderRadius: { xs: 0, md: theme.borderRadius.lg },
|
||||
backgroundColor: { xs: theme.colors.surface1, md: "transparent" },
|
||||
},
|
||||
pickerTriggerHovered: {
|
||||
backgroundColor: theme.colors.surfaceSidebarHover,
|
||||
@@ -1620,14 +1637,14 @@ const sidebarStyles = StyleSheet.create((theme) => ({
|
||||
pickerTriggerLabel: {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
fontSize: theme.fontSize.base,
|
||||
fontSize: { xs: theme.fontSize.lg, md: theme.fontSize.base },
|
||||
color: theme.colors.foreground,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
},
|
||||
// Match the setting items' icon footprint so the host label aligns with them.
|
||||
pickerTriggerDot: {
|
||||
width: theme.iconSize.md,
|
||||
height: theme.iconSize.md,
|
||||
width: densityIconBox("md"),
|
||||
height: densityIconBox("md"),
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user