diff --git a/apps/mobile/src/Stack.tsx b/apps/mobile/src/Stack.tsx
index 4dfa3f3a9856..8a16c17bc2ba 100644
--- a/apps/mobile/src/Stack.tsx
+++ b/apps/mobile/src/Stack.tsx
@@ -1,3 +1,4 @@
+import { ComponentCatalogScreen } from "./features/development/ComponentCatalogScreen";
import { PullRequestGuideStopScreen } from "./features/pull-requests/guide/PullRequestGuideScreen";
import { PullRequestInlineCommentScreen } from "./features/pull-requests/PullRequestInlineCommentScreen";
import { PullRequestDiscussionScreen } from "./features/pull-requests/PullRequestDiscussionScreen";
@@ -716,6 +717,15 @@ const RootStackConfig = createWorkspaceStackNavigator({
headerShown: false,
},
screens: {
+ ...(__DEV__
+ ? {
+ ComponentCatalog: createNativeStackScreen({
+ screen: ComponentCatalogScreen,
+ linking: "dev/components",
+ options: { title: "UI components" },
+ }),
+ }
+ : {}),
Home: createNativeStackScreen({
screen: WorkspaceRootScreen,
linking: "",
diff --git a/apps/mobile/src/components/Button.android.tsx b/apps/mobile/src/components/Button.android.tsx
index 1d8919034219..7c0fcf45bcbc 100644
--- a/apps/mobile/src/components/Button.android.tsx
+++ b/apps/mobile/src/components/Button.android.tsx
@@ -13,13 +13,13 @@ import {
import { defaultMinSize, fillMaxWidth, size } from "@expo/ui/jetpack-compose/modifiers";
import { View } from "react-native";
-import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
+import { useControlTheme } from "./useControlTheme";
import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole";
import type { ButtonProps } from "./Button";
import { useAndroidControlSizing } from "./useAndroidControlSizing";
export function Button(props: ButtonProps) {
- const { themeAppearance, themeVariables: colors } = useAppearancePreferences();
+ const { colorScheme, colors } = useControlTheme();
const typography = useScaledTextRole("footnote");
const { scale, mediumIconSize } = useAndroidControlSizing();
const tone = props.tone ?? "secondary";
@@ -64,7 +64,7 @@ export function Button(props: ButtonProps) {
diff --git a/apps/mobile/src/components/Chip.tsx b/apps/mobile/src/components/Chip.tsx
new file mode 100644
index 000000000000..9f46e7ffd3b1
--- /dev/null
+++ b/apps/mobile/src/components/Chip.tsx
@@ -0,0 +1,90 @@
+import { GlassView } from "expo-glass-effect";
+import { Pressable, View } from "react-native";
+import { withUniwind } from "uniwind";
+
+import { cn } from "../lib/cn";
+import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../native/native-glass";
+import { AppText } from "./AppText";
+import { SymbolView, type AppSymbolName } from "./AppSymbol";
+
+const ChipGlass = withUniwind(GlassView, {
+ style: { fromClassName: "className" },
+ tintColor: { fromClassName: "tintColorClassName", styleProperty: "accentColor" },
+});
+
+/** Compact labels and filter triggers. Menu owners can wrap the non-pressable form. */
+export function Chip(props: {
+ readonly label: string;
+ readonly selected?: boolean;
+ readonly icon?: AppSymbolName;
+ readonly trailingIcon?: AppSymbolName;
+ readonly size?: "small" | "default";
+ readonly glass?: boolean;
+ readonly colorScheme?: "light" | "dark";
+ readonly onPress?: () => void;
+ readonly accessibilityLabel?: string;
+ readonly disabled?: boolean;
+}) {
+ const small = props.size === "small";
+ const layout = cn(
+ "flex-row items-center gap-1.5 rounded-2xl px-3",
+ small ? "py-1" : "min-h-8 py-1.5",
+ );
+ const iconColor = props.selected ? "accent-primary-foreground" : "accent-icon-muted";
+ const content = (
+ <>
+ {props.icon ? (
+
+ ) : null}
+
+ {props.label}
+
+ {props.trailingIcon ? (
+
+ ) : null}
+ >
+ );
+ const body =
+ props.glass && NATIVE_LIQUID_GLASS_SUPPORTED ? (
+
+ {content}
+
+ ) : (
+
+ {content}
+
+ );
+ return props.onPress ? (
+
+ {body}
+
+ ) : (
+ body
+ );
+}
diff --git a/apps/mobile/src/components/ErrorBanner.tsx b/apps/mobile/src/components/ErrorBanner.tsx
index 38f85de195b5..c049fccc4213 100644
--- a/apps/mobile/src/components/ErrorBanner.tsx
+++ b/apps/mobile/src/components/ErrorBanner.tsx
@@ -1,10 +1,5 @@
-import { View } from "react-native";
+import { Notice } from "./Notice";
-import { AppText as Text } from "./AppText";
export function ErrorBanner(props: { readonly message: string }) {
- return (
-
- {props.message}
-
- );
+ return ;
}
diff --git a/apps/mobile/src/components/MaterialIconButton.android.tsx b/apps/mobile/src/components/MaterialIconButton.android.tsx
index 64387e3a4f48..0c7e6daa5e88 100644
--- a/apps/mobile/src/components/MaterialIconButton.android.tsx
+++ b/apps/mobile/src/components/MaterialIconButton.android.tsx
@@ -7,7 +7,7 @@ import {
import { size } from "@expo/ui/jetpack-compose/modifiers";
import { View } from "react-native";
-import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
+import { useControlTheme } from "./useControlTheme";
import { useAndroidControlSizing } from "./useAndroidControlSizing";
import { SymbolView, type AppSymbolName } from "./AppSymbol";
@@ -20,7 +20,7 @@ export function MaterialIconButton(props: {
readonly variant?: "standard" | "primary" | "tonal" | "danger";
readonly tintColorClassName?: string;
}) {
- const { themeAppearance, themeVariables: colors } = useAppearancePreferences();
+ const { colorScheme, colors } = useControlTheme();
const { iconSize, buttonSize } = useAndroidControlSizing();
const variant = props.variant ?? "standard";
const Component =
@@ -58,7 +58,7 @@ export function MaterialIconButton(props: {
>
diff --git a/apps/mobile/src/components/MaterialSearchField.tsx b/apps/mobile/src/components/MaterialSearchField.tsx
index 639249313bfd..f32ac5e86d59 100644
--- a/apps/mobile/src/components/MaterialSearchField.tsx
+++ b/apps/mobile/src/components/MaterialSearchField.tsx
@@ -6,12 +6,14 @@ import { useAndroidControlSizing } from "./useAndroidControlSizing";
export function MaterialSearchField({
inputRef,
+ autoFocus = true,
accessibilityLabel,
clearAccessibilityLabel,
placeholder,
value,
onChangeText,
}: {
+ readonly autoFocus?: boolean;
readonly inputRef: RefObject;
readonly accessibilityLabel: string;
readonly clearAccessibilityLabel: string;
@@ -37,7 +39,7 @@ export function MaterialSearchField({
{
if (!props.disabled) props.onValueChange?.(!props.value);
};
@@ -25,7 +25,7 @@ export function MaterialSwitch(props: ThemedSwitchProps) {
>
diff --git a/apps/mobile/src/components/Notice.tsx b/apps/mobile/src/components/Notice.tsx
new file mode 100644
index 000000000000..d6cf26d40c36
--- /dev/null
+++ b/apps/mobile/src/components/Notice.tsx
@@ -0,0 +1,85 @@
+import { useEffect } from "react";
+import { AccessibilityInfo, Platform, Pressable, View } from "react-native";
+
+import { cn } from "../lib/cn";
+import { AppText } from "./AppText";
+import { SymbolView, type AppSymbolName } from "./AppSymbol";
+import { GlassSurface } from "./GlassSurface";
+
+/** Inline feedback or a dismissible notice positioned by its owning screen. */
+export function Notice(props: {
+ readonly message: string;
+ readonly tone?: "default" | "danger";
+ readonly icon?: AppSymbolName;
+ readonly iconTone?: "default" | "danger";
+ readonly floating?: boolean;
+ readonly onDismiss?: () => void;
+ readonly dismissLabel?: string;
+}) {
+ const danger = props.tone === "danger";
+ useEffect(() => {
+ // Live regions reach TalkBack; VoiceOver needs an explicit announcement.
+ if (Platform.OS === "ios") AccessibilityInfo.announceForAccessibility(props.message);
+ }, [props.message]);
+
+ const content = (
+
+ {props.icon ? (
+
+
+
+ ) : null}
+
+ {props.message}
+
+ {props.onDismiss ? (
+
+
+
+ ) : null}
+
+ );
+ if (props.floating && Platform.OS === "ios")
+ return (
+
+ {content}
+
+ );
+ return (
+
+ {content}
+
+ );
+}
diff --git a/apps/mobile/src/components/NumberField.android.tsx b/apps/mobile/src/components/NumberField.android.tsx
new file mode 100644
index 000000000000..23095ee862fb
--- /dev/null
+++ b/apps/mobile/src/components/NumberField.android.tsx
@@ -0,0 +1,44 @@
+import { View } from "react-native";
+
+import { AppText } from "./AppText";
+import { MaterialIconButton } from "./MaterialIconButton";
+import type { NumberFieldProps } from "./NumberField";
+
+export function NumberField(props: NumberFieldProps) {
+ const adjust = (amount: number) => {
+ if (props.disabled) return;
+ const next = Math.max(props.min, Math.min(props.max, props.value + amount));
+ if (next !== props.value) props.onValueChange(next);
+ };
+
+ return (
+
+ {/* Match the 32dp switch track while retaining 48dp button touch targets. */}
+
+ adjust(-1)}
+ />
+
+ {props.value}
+
+ = props.max}
+ onPress={() => adjust(1)}
+ />
+
+ );
+}
diff --git a/apps/mobile/src/features/settings/components/AutoSettleDaysField.ios.tsx b/apps/mobile/src/components/NumberField.ios.tsx
similarity index 70%
rename from apps/mobile/src/features/settings/components/AutoSettleDaysField.ios.tsx
rename to apps/mobile/src/components/NumberField.ios.tsx
index d7734f5a923c..958698b65a99 100644
--- a/apps/mobile/src/features/settings/components/AutoSettleDaysField.ios.tsx
+++ b/apps/mobile/src/components/NumberField.ios.tsx
@@ -11,26 +11,18 @@ import {
presentationBackground,
tag,
} from "@expo/ui/swift-ui/modifiers";
-import {
- MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
- MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
-} from "@t3tools/contracts";
import { useState } from "react";
-import { useAppearancePreferences } from "../appearance/AppearancePreferencesProvider";
-import type { AutoSettleDaysFieldProps } from "./AutoSettleDaysField";
-
-const days = Array.from(
- { length: MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS - MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS + 1 },
- (_, index) => MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS + index,
-);
+import { useControlTheme } from "./useControlTheme";
+import type { NumberFieldProps } from "./NumberField";
-export function AutoSettleDaysField(props: AutoSettleDaysFieldProps) {
- const { themeAppearance, themeVariables: colors, appearance } = useAppearancePreferences();
+export function NumberField(props: NumberFieldProps) {
+ const { colorScheme, colors, fontSize } = useControlTheme();
+ const values = Array.from({ length: props.max - props.min + 1 }, (_, index) => props.min + index);
const [open, setOpen] = useState(false);
const [draft, setDraft] = useState(props.value);
return (
-
+
-
+ {props.maximumIcon ? (
+
+ ) : null}
);
diff --git a/apps/mobile/src/components/useControlTheme.ts b/apps/mobile/src/components/useControlTheme.ts
new file mode 100644
index 000000000000..de686e62f9ed
--- /dev/null
+++ b/apps/mobile/src/components/useControlTheme.ts
@@ -0,0 +1,13 @@
+import { useAppearancePreferences } from "../features/settings/appearance/AppearancePreferencesProvider";
+
+/** Palette and type size for native controls hosted outside the RN style tree. */
+export function useControlTheme() {
+ const { themeAppearance, themeVariables, systemColorsActive, appearance } =
+ useAppearancePreferences();
+ return {
+ colorScheme: themeAppearance,
+ colors: themeVariables,
+ systemColorsActive,
+ fontSize: appearance.baseFontSize,
+ };
+}
diff --git a/apps/mobile/src/dependency-graph.test.ts b/apps/mobile/src/dependency-graph.test.ts
index 7f2b0abdfc52..5b148384f7b2 100644
--- a/apps/mobile/src/dependency-graph.test.ts
+++ b/apps/mobile/src/dependency-graph.test.ts
@@ -269,7 +269,7 @@ describe("mobile dependency graph", () => {
["lib", "features", 7, "lib must not add imports from features"],
// components -> features: mostly the appearance preferences provider
// and the layout toolbar bridges.
- ["components", "features", 33, "components must not add imports from features"],
+ ["components", "features", 31, "components must not add imports from features"],
// native -> features: native glue reading appearance/keyboard/review features.
["native", "features", 8, "native must not add imports from features"],
// lib -> state: attachment/session plumbing that predates the cycle
diff --git a/apps/mobile/src/features/development/ComponentCatalogScreen.tsx b/apps/mobile/src/features/development/ComponentCatalogScreen.tsx
new file mode 100644
index 000000000000..8069ffe70687
--- /dev/null
+++ b/apps/mobile/src/features/development/ComponentCatalogScreen.tsx
@@ -0,0 +1,272 @@
+import { useNavigation } from "@react-navigation/native";
+import { useRef, useState } from "react";
+import { Alert, Platform, View, type TextInputInstance } from "react-native";
+
+import { AppText, AppTextInput } from "../../components/AppText";
+import { AppSheet } from "../../components/AppSheet";
+import { Chip } from "../../components/Chip";
+import { ControlPillMenu } from "../../components/ControlPillMenu";
+import { ListActionRow } from "../../components/ListActionRow";
+import { ListChoiceRow } from "../../components/ListChoiceRow";
+import { ListRow } from "../../components/ListRow";
+import { ListSection } from "../../components/ListSection";
+import { ListToggleRow } from "../../components/ListToggleRow";
+import { Button } from "../../components/Button";
+import { MaterialIconButton } from "../../components/MaterialIconButton";
+import { MaterialFloatingActionButton } from "../../components/MaterialFloatingActionButton";
+import { MaterialSearchField } from "../../components/MaterialSearchField";
+import { LoadingStrip } from "../../components/LoadingStrip";
+import { Notice } from "../../components/Notice";
+import { NumberField } from "../../components/NumberField";
+import { ScreenHeader } from "../../components/ScreenHeader";
+import { ScreenScrollView } from "../../components/ScreenScrollView";
+import { SegmentedControl } from "../../components/SegmentedControl";
+import { SliderRow } from "../../components/SliderRow";
+import { showConfirmDialog } from "../../components/ConfirmDialogHost";
+import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
+import { MOBILE_THEME_IDS } from "../../lib/mobileTheme";
+
+const CHOICES = [
+ { value: "overview", label: "Overview" },
+ { value: "activity", label: "Activity" },
+];
+
+/** Development-only, real controls with local state and no environment commands. */
+export function ComponentCatalogScreen() {
+ const navigation = useNavigation();
+ const appearance = useAppearancePreferences();
+ const [page, setPage] = useState("controls");
+ const [tab, setTab] = useState("overview");
+ const [enabled, setEnabled] = useState(true);
+ const [value, setValue] = useState(3);
+ const [text, setText] = useState("");
+ const [search, setSearch] = useState("");
+ const [sheet, setSheet] = useState(false);
+ const [notice, setNotice] = useState(true);
+ const searchRef = useRef(null);
+ const announceAction = () => setNotice(true);
+ const confirm = () => {
+ if (Platform.OS === "ios")
+ Alert.alert("Discard changes?", "This example only changes local catalog state.", [
+ { text: "Cancel", style: "cancel" },
+ { text: "Discard", style: "destructive", onPress: () => setText("") },
+ ]);
+ else
+ showConfirmDialog({
+ title: "Discard changes?",
+ message: "This example only changes local catalog state.",
+ confirmText: "Discard",
+ destructive: true,
+ onConfirm: () => setText(""),
+ });
+ };
+ return (
+
+ navigation.goBack()} sidebar={false} />
+
+
+ Theme preview
+
+ {MOBILE_THEME_IDS.filter(
+ (id) => id !== "material-you" || appearance.systemColorsAvailable,
+ ).map((id) => (
+ appearance.setThemeIdForBothAppearances(id)}
+ />
+ ))}
+
+
+
+
+ {page === "controls" ? (
+ <>
+
+ Lists and selection
+
+ setSheet(true)} />
+
+
+
+
+
+
+ setEnabled(!enabled)}
+ />
+
+
+
+
+
+ Buttons
+
+
+
+
+
+
+
+
+ setSheet(true)}
+ />
+
+
+ >
+ ) : null}
+ {page === "forms" ? (
+
+ Forms
+
+
+
+
+
+ Count
+
+
+
+
+
+
+ ) : null}
+ {page === "feedback" ? (
+
+ Feedback and presentation
+
+ {notice ? (
+ setNotice(false)}
+ />
+ ) : (
+
+ )}
+
+
+
+
+
+
+ setSheet(true)}
+ />
+
+
+ ) : null}
+
+ {sheet ? (
+ setSheet(false)}>
+
+
+ setTab("overview")}
+ />
+ setTab("activity")}
+ />
+
+
+
+ ) : null}
+
+ );
+}
diff --git a/apps/mobile/src/features/pull-requests/PullRequestOverviewLoading.tsx b/apps/mobile/src/features/pull-requests/PullRequestOverviewLoading.tsx
index 7e6e4f490ae9..7bb8631e1b9b 100644
--- a/apps/mobile/src/features/pull-requests/PullRequestOverviewLoading.tsx
+++ b/apps/mobile/src/features/pull-requests/PullRequestOverviewLoading.tsx
@@ -1,7 +1,7 @@
+import { Chip } from "../../components/Chip";
import { PullRequestSectionHeading } from "./PullRequestSectionHeading";
import type { PullRequestListEntry } from "@t3tools/contracts";
import { View } from "react-native";
-import { AppText as Text } from "../../components/AppText";
import { PullRequestCardGroup, PullRequestCardRow } from "./PullRequestControls";
import { PullRequestStackCard } from "./PullRequestStackCard";
import type { PullRequestParams } from "./usePullRequestDiff";
@@ -91,9 +91,7 @@ export function PullRequestOverviewLoading({
{summary.labels.length ? (
{summary.labels.map((label) => (
-
- {label.name}
-
+
))}
) : null}
diff --git a/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx b/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx
index 5afd705aae8c..5459cb77c619 100644
--- a/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx
+++ b/apps/mobile/src/features/pull-requests/PullRequestRouteScreen.tsx
@@ -1,3 +1,4 @@
+import { Chip } from "../../components/Chip";
import { pullRequestTabOffset } from "./pullRequestTabScroll";
import { guideFocusedRows, type GuideDiffFocus } from "./guide/guideDiffFocus";
import { PullRequestGuideContent } from "./guide/PullRequestGuideScreen";
@@ -259,9 +260,7 @@ function PullRequestOverview(props: {
{pr.labels.length ? (
{pr.labels.map((label) => (
-
- {label.name}
-
+
))}
) : null}
diff --git a/apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx b/apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx
index 76f18645c0bd..dbde61fae840 100644
--- a/apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx
+++ b/apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx
@@ -1 +1,14 @@
-export { FontSizeSliderRow } from "./FontSizeSliderRow.shared";
+import type { ComponentProps } from "react";
+import { SliderRow } from "../../../../components/SliderRow";
+
+export function FontSizeSliderRow(
+ props: Omit, "minimumIcon" | "maximumIcon">,
+) {
+ return (
+
+ );
+}
diff --git a/apps/mobile/src/features/settings/components/AutoSettleDaysField.android.tsx b/apps/mobile/src/features/settings/components/AutoSettleDaysField.android.tsx
deleted file mode 100644
index 572812880ada..000000000000
--- a/apps/mobile/src/features/settings/components/AutoSettleDaysField.android.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import {
- MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
- MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
-} from "@t3tools/contracts";
-import { View } from "react-native";
-
-import { AppText } from "../../../components/AppText";
-import { MaterialIconButton } from "../../../components/MaterialIconButton";
-import type { AutoSettleDaysFieldProps } from "./AutoSettleDaysField";
-
-export function AutoSettleDaysField(props: AutoSettleDaysFieldProps) {
- const adjust = (amount: number) => {
- if (props.disabled) return;
- const next = Math.max(
- MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
- Math.min(MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS, props.value + amount),
- );
- if (next !== props.value) props.onValueChange(next);
- };
-
- return (
-
- {/* Match the 32dp switch track while retaining 48dp button touch targets. */}
-
- adjust(-1)}
- />
-
- {props.value}
-
- = MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS}
- onPress={() => adjust(1)}
- />
-
- );
-}
diff --git a/apps/mobile/src/features/settings/components/AutoSettleDaysField.tsx b/apps/mobile/src/features/settings/components/AutoSettleDaysField.tsx
index ddbca55b6433..20603c7f91f2 100644
--- a/apps/mobile/src/features/settings/components/AutoSettleDaysField.tsx
+++ b/apps/mobile/src/features/settings/components/AutoSettleDaysField.tsx
@@ -1,48 +1,23 @@
-import { useState } from "react";
import {
MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS,
} from "@t3tools/contracts";
+import type { ComponentProps } from "react";
+import { NumberField } from "../../../components/NumberField";
-import { AppTextInput } from "../../../components/AppText";
-
-export interface AutoSettleDaysFieldProps {
- readonly value: number;
- readonly onValueChange: (value: number) => void;
- readonly disabled?: boolean;
-}
+export type AutoSettleDaysFieldProps = Pick<
+ ComponentProps,
+ "value" | "onValueChange" | "disabled"
+>;
export function AutoSettleDaysField(props: AutoSettleDaysFieldProps) {
- const [draft, setDraft] = useState(null);
- const commit = () => {
- if (props.disabled) {
- setDraft(null);
- return;
- }
- const text = (draft ?? "").trim();
- setDraft(null);
- // Validate the whole input; decimals and trailing text must not become whole days.
- const parsed = /^\d+$/.test(text) ? Number(text) : Number.NaN;
- if (
- Number.isInteger(parsed) &&
- parsed >= MIN_SIDEBAR_AUTO_SETTLE_AFTER_DAYS &&
- parsed <= MAX_SIDEBAR_AUTO_SETTLE_AFTER_DAYS &&
- parsed !== props.value
- ) {
- props.onValueChange(parsed);
- }
- };
return (
- `${value} ${value === 1 ? "day" : "days"}`}
/>
);
}
diff --git a/apps/mobile/src/features/threads/ComposerErrorNotice.tsx b/apps/mobile/src/features/threads/ComposerErrorNotice.tsx
index 37ea6078f2ab..020b23ffca5f 100644
--- a/apps/mobile/src/features/threads/ComposerErrorNotice.tsx
+++ b/apps/mobile/src/features/threads/ComposerErrorNotice.tsx
@@ -1,55 +1,21 @@
-import { useEffect } from "react";
-import { AccessibilityInfo, Platform, Pressable, View } from "react-native";
-
-import { AppText as Text } from "../../components/AppText";
-import { SymbolView } from "../../components/AppSymbol";
+import { View } from "react-native";
+import { Notice } from "../../components/Notice";
/** Why the thread's last message did not send, above the composer until dismissed. */
-export function ComposerErrorNotice({
- message,
- onDismiss,
-}: {
+export function ComposerErrorNotice(props: {
readonly message: string;
readonly onDismiss: () => void;
}) {
- // accessibilityLiveRegion below only reaches TalkBack; VoiceOver needs an
- // explicit announcement.
- useEffect(() => {
- if (Platform.OS === "ios") {
- AccessibilityInfo.announceForAccessibility(message);
- }
- }, [message]);
return (
-
-
-
- {message}
-
-
-
-
-
+
);
}
diff --git a/apps/mobile/src/features/workspace/ListFilterMenu.tsx b/apps/mobile/src/features/workspace/ListFilterMenu.tsx
index b645722f53dd..ad08ee45a761 100644
--- a/apps/mobile/src/features/workspace/ListFilterMenu.tsx
+++ b/apps/mobile/src/features/workspace/ListFilterMenu.tsx
@@ -1,21 +1,9 @@
-import { GlassView } from "expo-glass-effect";
-import { Pressable, View } from "react-native";
-import { withUniwind } from "uniwind";
-import { AppText as Text } from "../../components/AppText";
-import { SymbolView } from "../../components/AppSymbol";
+import { Pressable } from "react-native";
+import { Chip } from "../../components/Chip";
import { ControlPillMenu } from "../../components/ControlPillMenu";
import type { ControlPillMenuProps } from "../../components/ControlPillMenu.types";
-import { cn } from "../../lib/cn";
-import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
-const FilterGlass = withUniwind(GlassView, {
- style: { fromClassName: "className" },
- tintColor: { fromClassName: "tintColorClassName", styleProperty: "accentColor" },
-});
-
-const PILL = "h-8 flex-row items-center gap-1.5 rounded-full px-3";
-
/** A visible filter with a native menu containing only that filter's choices. */
export function ListFilterMenu(props: {
readonly label: string;
@@ -26,33 +14,6 @@ export function ListFilterMenu(props: {
readonly onSelect: (id: string) => void;
}) {
const { themeAppearance } = useAppearancePreferences();
- const content = (
- <>
- {props.icon ? (
-
- ) : null}
-
- {props.label}
-
- {!props.icon ? (
-
- ) : null}
- >
- );
return (
- {NATIVE_LIQUID_GLASS_SUPPORTED ? (
-
- {content}
-
- ) : (
- {content}
- )}
+
);