diff --git a/.env.example b/.env.example index f1819d3..152eb24 100644 --- a/.env.example +++ b/.env.example @@ -42,3 +42,20 @@ CONGRESS_API_KEY=your_congress_api_key_here # Without a token requests are anonymous (rate-limited to ~30 req/min) # Free account + token: https://www.courtlistener.com/sign-in/ COURTLISTENER_API_KEY=your_courtlistener_token_here + +# Google Civic Information API — elections, polling locations, representatives, voter info +# Enable at: https://console.cloud.google.com/apis/library/civicinfo.googleapis.com +# Free tier: 25,000 requests/day +GOOGLE_CIVIC_API_KEY=your_google_civic_api_key_here + +# Open States API — California state bills, legislators, voting records +# Sign up: https://openstates.org/accounts/signup/ +# Get key: https://openstates.org/accounts/profile/ (API Keys section) +# Free tier: generous limits +OPEN_STATES_API_KEY=your_open_states_api_key_here + +# California Secretary of State API — election results, contests, county vote breakdowns +# Sign up: https://calicodev.sos.ca.gov/ +# Subscribe to "Election Results API" product, then copy Primary Key from Profile +# Free tier: unlimited +CA_SOS_API_KEY=your_ca_sos_subscription_key_here diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 68f3efc..59c5db3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -246,6 +246,29 @@ Open `ios/billion.xcworkspace` — never `ios/billion.xcodeproj`. The `.xcodepro --- +## Mock Data & Development Without API Keys + +The app can run without third-party API keys. When keys are missing, mock data is returned automatically — no configuration needed. + +### Automatic API Mocking + +| API | Env Var | Mock Trigger | +|-----|---------|--------------| +| **Google Civic** (elections, representatives, voter info) | `GOOGLE_CIVIC_API_KEY` | Not set → returns mock data | +| **Legistar** (local legislation) | None (public API) | API request fails → returns mock data for San Jose and Santa Clara | + +Mock data includes realistic local government content (elections, bills, resolutions, representatives) so you can develop UI features without any API keys. + +### Database Seeding + +To populate your local database with sample data: + +```bash +pnpm -F @acme/db seed +``` + +This runs `packages/db/seed.ts` and inserts representative content records. Requires `POSTGRES_URL` in your `.env`. + ## Architecture See [ARCHITECTURE.md](./ARCHITECTURE.md) for a full breakdown of the system design, data layer, API layer, scraper pipeline, and architectural decisions. diff --git a/apps/expo/expo-env.d.ts b/apps/expo/expo-env.d.ts index bf3c169..5411fdd 100644 --- a/apps/expo/expo-env.d.ts +++ b/apps/expo/expo-env.d.ts @@ -1,3 +1,3 @@ /// -// NOTE: This file should not be edited and should be in your git ignore +// NOTE: This file should not be edited and should be in your git ignore \ No newline at end of file diff --git a/apps/expo/metro.config.js b/apps/expo/metro.config.js index 09d4b49..1d9e979 100644 --- a/apps/expo/metro.config.js +++ b/apps/expo/metro.config.js @@ -19,6 +19,7 @@ config.resolver.nodeModulesPaths = [ // 3. Ensure Metro follows symlinks (required for pnpm workspaces) config.resolver.unstable_enableSymlinks = true; +config.resolver.unstable_enablePackageExports = true; /** @type {import('expo/metro-config').MetroConfig} */ module.exports = withNativewind(config); diff --git a/apps/expo/package.json b/apps/expo/package.json index 89a724e..b5a191c 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -23,6 +23,7 @@ "@expo-google-fonts/inria-serif": "^0.4.1", "@expo/vector-icons": "^14.1.0", "@legendapp/list": "^2.0.19", + "@react-native-async-storage/async-storage": "^3.1.0", "@ronradtke/react-native-markdown-display": "^8.1.0", "@tanstack/react-query": "catalog:", "@trpc/client": "catalog:", @@ -48,8 +49,8 @@ "expo-web-browser": "~14.2.0", "fuse.js": "^7.1.0", "nativewind": "5.0.0-preview.3", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "19.0.0", + "react-dom": "19.0.0", "react-native": "~0.79.6", "react-native-css": "3.0.6", "react-native-gesture-handler": "~2.24.0", @@ -58,7 +59,8 @@ "react-native-screens": "~4.11.1", "react-native-svg": "15.11.2", "react-native-web": "~0.20.0", - "superjson": "2.2.6" + "superjson": "2.2.6", + "use-latest-callback": "^0.3.4" }, "devDependencies": { "@acme/api": "workspace:*", diff --git a/apps/expo/src/app/(tabs)/feed.tsx b/apps/expo/src/app/(tabs)/feed.tsx index 3ae6872..24ea6d1 100644 --- a/apps/expo/src/app/(tabs)/feed.tsx +++ b/apps/expo/src/app/(tabs)/feed.tsx @@ -7,12 +7,14 @@ import { StyleSheet, TouchableOpacity, } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Image } from "expo-image"; import { useRouter } from "expo-router"; -import { useInfiniteQuery } from "@tanstack/react-query"; +import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import type { VideoPost } from "@acme/api"; +import { ElectionBanner } from "~/components/ElectionBanner"; import { Text, View } from "~/components/Themed"; import { badges, @@ -26,6 +28,7 @@ import { useTheme, } from "~/styles"; import { trpc } from "~/utils/api"; +import { daysUntil, isWithinDays } from "~/utils/dates"; import { getBaseUrl } from "~/utils/base-url"; const { height: screenHeight } = Dimensions.get("window"); @@ -33,6 +36,12 @@ const { height: screenHeight } = Dimensions.get("window"); export default function FeedScreen() { const router = useRouter(); const { theme } = useTheme(); + const insets = useSafeAreaInsets(); + + const electionsQuery = useQuery(trpc.civic.getElections.queryOptions()); + const upcomingElection = electionsQuery.data?.find((e) => + isWithinDays(e.electionDay, 30), + ); // Debug: log base URL useEffect(() => { @@ -303,6 +312,20 @@ export default function FeedScreen() { maxToRenderPerBatch={3} windowSize={5} /> + {upcomingElection && ( + + router.push("/local-elections")} + /> + + )} ); } @@ -312,6 +335,12 @@ export default function FeedScreen() { // } from "@acme/ui/theme-tokens"; // const sp = (key: keyof typeof spacing): number => spacing[key] * 16; const styles = StyleSheet.create({ + bannerOverlay: { + position: "absolute", + left: 0, + right: 0, + zIndex: 10, + }, loadingText: { fontFamily: "AlbertSans_400Regular", marginTop: sp[4], diff --git a/apps/expo/src/app/(tabs)/index.tsx b/apps/expo/src/app/(tabs)/index.tsx index bd9ea89..a8f6e73 100644 --- a/apps/expo/src/app/(tabs)/index.tsx +++ b/apps/expo/src/app/(tabs)/index.tsx @@ -15,6 +15,7 @@ import Fuse from "fuse.js"; import type { VideoPost } from "@acme/api"; import type { Theme } from "~/styles"; +import { ElectionBanner } from "~/components/ElectionBanner"; import { Text, View } from "~/components/Themed"; import { buttons, @@ -31,6 +32,7 @@ import { useTheme, } from "~/styles"; import { trpc } from "~/utils/api"; +import { daysUntil, isWithinDays } from "~/utils/dates"; interface ContentCard { id: string; @@ -199,11 +201,17 @@ const TAB_CONFIG: { key: VideoPost["type"] | "all"; label: string }[] = [ export default function BrowseScreen() { const insets = useSafeAreaInsets(); const { theme } = useTheme(); + const router = useRouter(); const [selectedTab, setSelectedTab] = useState( "all", ); const [searchQuery, setSearchQuery] = useState(""); + const electionsQuery = useQuery(trpc.civic.getElections.queryOptions()); + const upcomingElection = electionsQuery.data?.find((e) => + isWithinDays(e.electionDay, 30), + ); + const { data: content, isLoading, @@ -265,6 +273,15 @@ export default function BrowseScreen() { ))} + {/* Election banner */} + {upcomingElection && ( + router.push("/local-elections")} + /> + )} + daysUntil(e.electionDay) >= 0) + .sort((a, b) => a.electionDay.localeCompare(b.electionDay))[0]; + + const voterInfoQuery = useQuery({ + ...trpc.civic.getVoterInfo.queryOptions({ address: address ?? "" }), + enabled: !!address, + }); + + const contests = voterInfoQuery.data?.contests ?? []; + const measures = contests.filter((c: Contest) => c.referendumTitle); + const candidateContests = contests.filter( + (c: Contest) => c.candidates && c.candidates.length > 0, + ); + + return ( + + + router.back()} + style={styles.backButton} + > + + + Local Elections + + + + + + + {upcomingElection && ( + + )} + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + header: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingHorizontal: sp[4], + paddingBottom: sp[4], + }, + backButton: { + padding: sp[3], + marginLeft: -sp[3], + }, + title: { + fontFamily: fontDisplay.bold, + fontSize: fontSize.xl, + color: colors.white, + }, + placeholder: { + width: 34, + }, + scroll: { + flex: 1, + }, + scrollContent: { + paddingBottom: sp[5], + }, +}); diff --git a/apps/expo/src/components/BallotMeasuresSection.tsx b/apps/expo/src/components/BallotMeasuresSection.tsx new file mode 100644 index 0000000..0958de5 --- /dev/null +++ b/apps/expo/src/components/BallotMeasuresSection.tsx @@ -0,0 +1,141 @@ +import { StyleSheet, TouchableOpacity } from "react-native"; +import { FontAwesome } from "@expo/vector-icons"; + +import type { Contest } from "@acme/api"; + +import { Text, View } from "~/components/Themed"; +import { fontBody, fontEditorial, fontSize, rd, sp, useTheme } from "~/styles"; + +interface BallotMeasuresSectionProps { + measures: Contest[]; + onMeasurePress?: (measure: Contest) => void; +} + +export function BallotMeasuresSection({ + measures, + onMeasurePress, +}: BallotMeasuresSectionProps) { + const { theme } = useTheme(); + + if (measures.length === 0) { + return null; + } + + return ( + + Ballot Measures + {measures.map((measure, index) => ( + onMeasurePress?.(measure)} + activeOpacity={0.8} + > + + MEASURE + + + {measure.referendumTitle ?? "Ballot Measure"} + + {measure.referendumSubtitle && ( + + {measure.referendumSubtitle} + + )} + {(measure.referendumProStatement ?? + measure.referendumConStatement) && ( + + {measure.referendumProStatement && ( + + Yes: + {measure.referendumProStatement} + + )} + {measure.referendumConStatement && ( + + No: + {measure.referendumConStatement} + + )} + + )} + + + ))} + + ); +} + +const colors = { + white: "#FFFFFF", + civicBlue: "#4A7CFF", + textMuted: "#8A8FA0", +}; + +const styles = StyleSheet.create({ + container: { + marginHorizontal: sp[4], + marginBottom: sp[6], + }, + sectionTitle: { + fontFamily: fontEditorial.bold, + fontSize: fontSize.lg, + color: colors.white, + marginBottom: sp[3], + }, + card: { + padding: sp[4], + borderRadius: rd.md, + marginBottom: sp[3], + }, + badge: { + backgroundColor: colors.civicBlue, + alignSelf: "flex-start", + paddingVertical: 2, + paddingHorizontal: sp[3], + borderRadius: 4, + marginBottom: sp[3], + }, + badgeText: { + fontFamily: fontBody.semibold, + fontSize: 10, + color: colors.white, + textTransform: "uppercase", + }, + title: { + fontFamily: fontBody.semibold, + fontSize: fontSize.base, + color: colors.white, + marginBottom: sp[2], + }, + subtitle: { + fontFamily: fontBody.regular, + fontSize: fontSize.sm, + color: colors.textMuted, + marginBottom: sp[3], + }, + arguments: { + marginTop: sp[2], + }, + argument: { + fontFamily: fontBody.regular, + fontSize: fontSize.xs, + color: colors.textMuted, + marginBottom: 2, + }, + argumentLabel: { + fontFamily: fontBody.semibold, + color: colors.white, + }, + chevron: { + position: "absolute", + right: sp[4], + top: "50%", + marginTop: -7, + }, +}); diff --git a/apps/expo/src/components/CandidatesSection.tsx b/apps/expo/src/components/CandidatesSection.tsx new file mode 100644 index 0000000..9810c0f --- /dev/null +++ b/apps/expo/src/components/CandidatesSection.tsx @@ -0,0 +1,138 @@ +import { StyleSheet, TouchableOpacity } from "react-native"; +import { Image } from "expo-image"; +import { FontAwesome } from "@expo/vector-icons"; + +import type { Contest } from "@acme/api"; + +import { Text, View } from "~/components/Themed"; +import { fontBody, fontEditorial, fontSize, rd, sp, useTheme } from "~/styles"; + +interface CandidatesSectionProps { + contests: Contest[]; + onCandidatePress?: (contest: Contest, candidateIndex: number) => void; +} + +export function CandidatesSection({ + contests, + onCandidatePress, +}: CandidatesSectionProps) { + const { theme } = useTheme(); + + const candidateContests = contests.filter( + (c) => c.candidates && c.candidates.length > 0, + ); + + if (candidateContests.length === 0) { + return null; + } + + return ( + + Candidates + {candidateContests.map((contest, contestIndex) => ( + + {contest.office ?? "Race"} + {contest.candidates?.map((candidate, candidateIndex) => ( + onCandidatePress?.(contest, candidateIndex)} + activeOpacity={0.8} + > + {candidate.photoUrl ? ( + + ) : ( + + + + )} + + {candidate.name} + {candidate.party && ( + {candidate.party} + )} + + + + ))} + + ))} + + ); +} + +const colors = { + white: "#FFFFFF", + textMuted: "#8A8FA0", +}; + +const styles = StyleSheet.create({ + container: { + marginHorizontal: sp[4], + marginBottom: sp[6], + }, + sectionTitle: { + fontFamily: fontEditorial.bold, + fontSize: fontSize.lg, + color: colors.white, + marginBottom: sp[3], + }, + raceGroup: { + marginBottom: sp[4], + }, + raceTitle: { + fontFamily: fontBody.semibold, + fontSize: fontSize.sm, + color: colors.textMuted, + marginBottom: sp[3], + textTransform: "uppercase", + letterSpacing: 0.5, + }, + candidateCard: { + flexDirection: "row", + alignItems: "center", + padding: sp[3], + borderRadius: rd.md, + marginBottom: sp[2], + }, + photo: { + width: 44, + height: 44, + borderRadius: 22, + marginRight: sp[3], + }, + photoPlaceholder: { + width: 44, + height: 44, + borderRadius: 22, + marginRight: sp[3], + justifyContent: "center", + alignItems: "center", + }, + candidateInfo: { + flex: 1, + }, + candidateName: { + fontFamily: fontBody.medium, + fontSize: fontSize.sm, + color: colors.white, + }, + candidateParty: { + fontFamily: fontBody.regular, + fontSize: fontSize.xs, + color: colors.textMuted, + }, +}); diff --git a/apps/expo/src/components/ElectionBanner.tsx b/apps/expo/src/components/ElectionBanner.tsx new file mode 100644 index 0000000..fb196ed --- /dev/null +++ b/apps/expo/src/components/ElectionBanner.tsx @@ -0,0 +1,103 @@ +import { StyleSheet, TouchableOpacity } from "react-native"; +import { FontAwesome } from "@expo/vector-icons"; + +import { Text, View } from "~/components/Themed"; +import { fontBody, fontSize, rd, sp, useTheme } from "~/styles"; + +interface ElectionBannerProps { + daysUntil: number; + electionName: string; + onPress: () => void; +} + +export function ElectionBanner({ + daysUntil, + electionName, + onPress, +}: ElectionBannerProps) { + const { theme } = useTheme(); + + return ( + + + + + + {daysUntil} days until{" "} + {electionName} + + Know what's on your ballot + + + See My Ballot + + + + + ); +} + +const colors = { + white: "#FFFFFF", + black: "#000000", + civicBlue: "#4A7CFF", + textMuted: "#8A8FA0", +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: "row", + marginHorizontal: sp[4], + marginBottom: sp[4], + borderRadius: rd.md, + overflow: "hidden", + }, + accent: { + width: 4, + backgroundColor: colors.civicBlue, + }, + content: { + flex: 1, + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + padding: sp[4], + }, + textContainer: { + flex: 1, + marginRight: sp[4], + }, + headline: { + fontFamily: fontBody.medium, + fontSize: fontSize.sm, + color: colors.white, + marginBottom: sp[2], + }, + days: { + fontFamily: fontBody.bold, + fontSize: fontSize.base, + }, + subtext: { + fontFamily: fontBody.regular, + fontSize: fontSize.xs, + color: colors.textMuted, + }, + cta: { + flexDirection: "row", + alignItems: "center", + backgroundColor: colors.white, + paddingVertical: sp[3], + paddingHorizontal: sp[4], + borderRadius: 9999, + gap: sp[2], + }, + ctaText: { + fontFamily: fontBody.semibold, + fontSize: fontSize.xs, + color: colors.black, + }, +}); diff --git a/apps/expo/src/components/KeyDatesSection.tsx b/apps/expo/src/components/KeyDatesSection.tsx new file mode 100644 index 0000000..09bbdaf --- /dev/null +++ b/apps/expo/src/components/KeyDatesSection.tsx @@ -0,0 +1,147 @@ +import { ScrollView, StyleSheet } from "react-native"; + +import { Text, View } from "~/components/Themed"; +import { fontBody, fontEditorial, fontSize, rd, sp, useTheme } from "~/styles"; +import { daysUntil, formatDate } from "~/utils/dates"; + +interface KeyDate { + label: string; + date: string; +} + +interface KeyDatesSectionProps { + electionDate: string; +} + +export function KeyDatesSection({ electionDate }: KeyDatesSectionProps) { + const { theme } = useTheme(); + + const electionDateObj = new Date(electionDate); + const registrationDeadline = new Date(electionDateObj); + registrationDeadline.setDate(registrationDeadline.getDate() - 15); + + const earlyVotingStart = new Date(electionDateObj); + earlyVotingStart.setDate(earlyVotingStart.getDate() - 29); + + const dates: KeyDate[] = [ + { + label: "Registration Deadline", + date: registrationDeadline.toISOString().split("T")[0] ?? "", + }, + { + label: "Early Voting Starts", + date: earlyVotingStart.toISOString().split("T")[0] ?? "", + }, + { + label: "Election Day", + date: electionDate, + }, + ]; + + return ( + + Key Dates + + {dates.map((item, index) => { + const days = daysUntil(item.date); + const isPassed = days < 0; + const isNext = + !isPassed && + dates.findIndex((d) => daysUntil(d.date) >= 0) === index; + + return ( + + + {item.label} + + + {formatDate(item.date)} + + + {isPassed + ? "Passed" + : days === 0 + ? "Today!" + : `in ${days} days`} + + + ); + })} + + + ); +} + +const colors = { + white: "#FFFFFF", + civicBlue: "#4A7CFF", + textMuted: "#8A8FA0", +}; + +const styles = StyleSheet.create({ + container: { + marginBottom: sp[6], + }, + sectionTitle: { + fontFamily: fontEditorial.bold, + fontSize: fontSize.lg, + color: colors.white, + marginHorizontal: sp[4], + marginBottom: sp[3], + }, + scrollContent: { + paddingHorizontal: sp[4], + gap: sp[3], + }, + card: { + padding: sp[4], + borderRadius: rd.md, + minWidth: 140, + }, + cardHighlight: { + borderWidth: 2, + borderColor: colors.civicBlue, + }, + label: { + fontFamily: fontBody.medium, + fontSize: fontSize.xs, + color: colors.textMuted, + marginBottom: sp[2], + }, + date: { + fontFamily: fontBody.semibold, + fontSize: fontSize.sm, + color: colors.white, + marginBottom: sp[2], + }, + countdown: { + fontFamily: fontBody.regular, + fontSize: fontSize.xs, + color: colors.textMuted, + }, + countdownHighlight: { + color: colors.civicBlue, + fontFamily: fontBody.semibold, + }, + textMuted: { + color: colors.textMuted, + opacity: 0.6, + }, +}); diff --git a/apps/expo/src/components/LocalBillsSection.tsx b/apps/expo/src/components/LocalBillsSection.tsx new file mode 100644 index 0000000..4cb9d70 --- /dev/null +++ b/apps/expo/src/components/LocalBillsSection.tsx @@ -0,0 +1,131 @@ +import { ActivityIndicator, StyleSheet, TouchableOpacity } from "react-native"; +import { FontAwesome } from "@expo/vector-icons"; +import { useQuery } from "@tanstack/react-query"; + +import type { LegistarMatter } from "@acme/api/integrations/legistar"; + +import { Text, View } from "~/components/Themed"; +import { fontBody, fontEditorial, fontSize, rd, sp, useTheme } from "~/styles"; +import { trpc } from "~/utils/api"; + +interface LocalBillsSectionProps { + onBillPress?: (bill: LegistarMatter) => void; +} + +export function LocalBillsSection({ onBillPress }: LocalBillsSectionProps) { + const { theme } = useTheme(); + + const billsQuery = useQuery(trpc.legistar.getLocalBills.queryOptions()); + + return ( + + Local Bills + + {billsQuery.isLoading && ( + + )} + + {billsQuery.data?.map((bill, index) => ( + onBillPress?.(bill as LegistarMatter)} + activeOpacity={0.8} + > + + + + {bill.jurisdiction} + {bill.MatterStatusName} + + + {bill.MatterTitle} + + {bill.MatterFile} + + + + ))} + + {billsQuery.data?.length === 0 && ( + No recent local legislation + )} + + ); +} + +const colors = { + white: "#FFFFFF", + civicBlue: "#4A7CFF", + textMuted: "#8A8FA0", +}; + +const styles = StyleSheet.create({ + container: { + marginHorizontal: sp[4], + marginBottom: sp[6], + }, + sectionTitle: { + fontFamily: fontEditorial.bold, + fontSize: fontSize.lg, + color: colors.white, + marginBottom: sp[3], + }, + loader: { + marginVertical: sp[6], + }, + card: { + flexDirection: "row", + alignItems: "center", + borderRadius: rd.md, + marginBottom: sp[3], + overflow: "hidden", + }, + cardAccent: { + width: 4, + alignSelf: "stretch", + backgroundColor: colors.civicBlue, + }, + cardContent: { + flex: 1, + padding: sp[4], + }, + meta: { + flexDirection: "row", + gap: sp[3], + marginBottom: sp[2], + }, + jurisdiction: { + fontFamily: fontBody.semibold, + fontSize: 10, + color: colors.civicBlue, + textTransform: "uppercase", + }, + status: { + fontFamily: fontBody.regular, + fontSize: 10, + color: colors.textMuted, + }, + title: { + fontFamily: fontBody.medium, + fontSize: fontSize.sm, + color: colors.white, + marginBottom: sp[2], + }, + file: { + fontFamily: fontBody.regular, + fontSize: fontSize.xs, + color: colors.textMuted, + }, + noData: { + fontFamily: fontBody.regular, + fontSize: fontSize.sm, + color: colors.textMuted, + textAlign: "center", + marginVertical: sp[6], + }, +}); diff --git a/apps/expo/src/components/MyBallotSection.tsx b/apps/expo/src/components/MyBallotSection.tsx new file mode 100644 index 0000000..3e724c6 --- /dev/null +++ b/apps/expo/src/components/MyBallotSection.tsx @@ -0,0 +1,216 @@ +import { useState } from "react"; +import { + ActivityIndicator, + StyleSheet, + TextInput, + TouchableOpacity, +} from "react-native"; +import { FontAwesome } from "@expo/vector-icons"; + +import type { Contest } from "@acme/api"; + +import { Text, View } from "~/components/Themed"; +import { fontBody, fontEditorial, fontSize, rd, sp, useTheme } from "~/styles"; + +const colors = { + white: "#FFFFFF", + black: "#000000", + civicBlue: "#4A7CFF", + textMuted: "#8A8FA0", +}; + +interface MyBallotSectionProps { + address: string | null; + onAddressSubmit: (address: string) => void; + onEditAddress: () => void; + contests?: Contest[]; + isLoadingContests?: boolean; +} + +export function MyBallotSection({ + address, + onAddressSubmit, + onEditAddress, + contests, + isLoadingContests, +}: MyBallotSectionProps) { + const { theme } = useTheme(); + const [inputValue, setInputValue] = useState(""); + + if (!address) { + return ( + + My Ballot + + We'll show you exactly what's on your ballot + + + + inputValue && onAddressSubmit(inputValue)} + disabled={!inputValue} + > + Look Up + + + + ); + } + + return ( + + + My Ballot + + + Edit + + + {address} + + {isLoadingContests && ( + + )} + + {contests?.map((contest: Contest, index: number) => ( + + + {contest.office ?? contest.referendumTitle ?? "Contest"} + + + {contest.candidates + ? `${contest.candidates.length} candidates` + : contest.referendumTitle + ? "Ballot Measure" + : ""} + + + + ))} + + {contests?.length === 0 && ( + No ballot information available yet + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + marginHorizontal: sp[4], + marginBottom: sp[6], + padding: sp[4], + borderRadius: rd.md, + }, + header: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: sp[3], + }, + sectionTitle: { + fontFamily: fontEditorial.bold, + fontSize: fontSize.lg, + color: colors.white, + marginBottom: sp[2], + }, + hint: { + fontFamily: fontBody.regular, + fontSize: fontSize.sm, + color: colors.textMuted, + marginBottom: sp[4], + }, + inputRow: { + flexDirection: "row", + gap: sp[3], + }, + input: { + flex: 1, + fontFamily: fontBody.regular, + fontSize: fontSize.sm, + color: colors.white, + padding: sp[3], + borderRadius: rd.sm, + }, + button: { + backgroundColor: colors.white, + paddingVertical: sp[3], + paddingHorizontal: sp[4], + borderRadius: 9999, + justifyContent: "center", + }, + buttonDisabled: { + opacity: 0.5, + }, + buttonText: { + fontFamily: fontBody.semibold, + fontSize: fontSize.sm, + color: colors.black, + }, + editButton: { + flexDirection: "row", + alignItems: "center", + gap: sp[2], + }, + editText: { + fontFamily: fontBody.medium, + fontSize: fontSize.sm, + color: colors.civicBlue, + }, + address: { + fontFamily: fontBody.regular, + fontSize: fontSize.sm, + color: colors.textMuted, + marginBottom: sp[4], + }, + loader: { + marginVertical: sp[6], + }, + contestCard: { + flexDirection: "row", + alignItems: "center", + padding: sp[3], + borderRadius: rd.sm, + marginBottom: sp[3], + }, + contestTitle: { + flex: 1, + fontFamily: fontBody.medium, + fontSize: fontSize.sm, + color: colors.white, + }, + contestMeta: { + fontFamily: fontBody.regular, + fontSize: fontSize.xs, + color: colors.textMuted, + marginRight: sp[3], + }, + chevron: { + marginLeft: sp[2], + }, + noData: { + fontFamily: fontBody.regular, + fontSize: fontSize.sm, + color: colors.textMuted, + textAlign: "center", + marginVertical: sp[6], + }, +}); diff --git a/apps/expo/src/hooks/useUserAddress.ts b/apps/expo/src/hooks/useUserAddress.ts new file mode 100644 index 0000000..71e4f62 --- /dev/null +++ b/apps/expo/src/hooks/useUserAddress.ts @@ -0,0 +1,34 @@ +import { useCallback, useEffect, useState } from "react"; + +import { sessionStorage } from "~/utils/client-storage"; + +const ADDRESS_KEY = "user_address"; + +export function useUserAddress() { + const [address, setAddressState] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + sessionStorage + .getItemAsync(ADDRESS_KEY) + .then((value: string | null) => { + setAddressState(value); + setIsLoading(false); + }) + .catch(() => { + setIsLoading(false); + }); + }, []); + + const setAddress = useCallback(async (newAddress: string) => { + await sessionStorage.setItemAsync(ADDRESS_KEY, newAddress); + setAddressState(newAddress); + }, []); + + const clearAddress = useCallback(async () => { + await sessionStorage.deleteItemAsync(ADDRESS_KEY); + setAddressState(null); + }, []); + + return { address, setAddress, clearAddress, isLoading }; +} diff --git a/apps/expo/src/utils/dates.ts b/apps/expo/src/utils/dates.ts new file mode 100644 index 0000000..cb2cfb2 --- /dev/null +++ b/apps/expo/src/utils/dates.ts @@ -0,0 +1,21 @@ +export function daysUntil(dateString: string): number { + const target = new Date(dateString); + const now = new Date(); + const diffTime = target.getTime() - now.getTime(); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + return diffDays; +} + +export function formatDate(dateString: string): string { + const date = new Date(dateString); + return date.toLocaleDateString("en-US", { + weekday: "short", + month: "short", + day: "numeric", + }); +} + +export function isWithinDays(dateString: string, days: number): boolean { + const daysRemaining = daysUntil(dateString); + return daysRemaining > 0 && daysRemaining <= days; +} diff --git a/apps/scraper/package.json b/apps/scraper/package.json index 14c24fc..c7e2d3a 100644 --- a/apps/scraper/package.json +++ b/apps/scraper/package.json @@ -12,6 +12,7 @@ "consola": "^3.4.2", "dotenv": "^17.3.1", "p-limit": "^7.3.0", + "playwright": "^1.58.2", "sharp": "^0.34.5", "turndown": "^7.2.2", "yargs": "^18.0.0", diff --git a/apps/scraper/src/main.ts b/apps/scraper/src/main.ts index 164f37e..82bc9fa 100644 --- a/apps/scraper/src/main.ts +++ b/apps/scraper/src/main.ts @@ -4,6 +4,8 @@ import { hideBin } from "yargs/helpers"; import { congress } from "./scrapers/congress.js"; import { scotus } from "./scrapers/scotus.js"; import { federalregister } from "./scrapers/federalregister.js"; +import { santaClaraROV } from "./scrapers/santa-clara-rov.js"; +import { vote411 } from "./scrapers/vote411.js"; import type { Scraper } from "./utils/types.js"; import { createLogger } from "./utils/log.js"; import { setConcurrency } from "./utils/concurrency.js"; @@ -11,7 +13,7 @@ import { resetMetrics, printMetricsSummary } from "./utils/db/metrics.js"; const logger = createLogger("main"); -const scrapers: Scraper[] = [federalregister, congress, scotus]; +const scrapers: Scraper[] = [federalregister, congress, scotus, santaClaraROV, vote411]; const scraperNames = scrapers.map((s) => s.name); const argv = await yargs(hideBin(process.argv)) diff --git a/apps/scraper/src/scrapers/santa-clara-rov.ts b/apps/scraper/src/scrapers/santa-clara-rov.ts new file mode 100644 index 0000000..cac1324 --- /dev/null +++ b/apps/scraper/src/scrapers/santa-clara-rov.ts @@ -0,0 +1,679 @@ +/** + * Santa Clara County Registrar of Voters Scraper + * + * Data sources: + * 1. Google Civic Information API (primary) - polling places, elections, representatives + * 2. California Secretary of State API (election calendar, candidate filings) + * 3. Direct scraping of county site (when Cloudflare allows, with puppeteer fallback option) + * + * The county site (vote.santaclaracounty.gov) is behind Cloudflare protection, + * so we primarily rely on aggregated state/federal APIs. + */ + +import { fetchWithRetry } from "../utils/fetch.js"; +import { createLogger } from "../utils/log.js"; +import { getItemLimit } from "../utils/concurrency.js"; +import type { Scraper } from "../utils/types.js"; + +const NAME = "Santa Clara ROV"; +const logger = createLogger(NAME); + +// Santa Clara County FIPS code for filtering +const SANTA_CLARA_FIPS = "06085"; +const COUNTY_NAME = "Santa Clara County"; + +// ============== Type Definitions ============== + +interface PollingLocation { + id: string; + name: string; + address: { + line1: string; + line2?: string; + city: string; + state: string; + zip: string; + }; + hours?: string; + notes?: string; + latitude?: number; + longitude?: number; + voterServices?: string[]; + startDate?: string; + endDate?: string; + locationType: "polling_place" | "early_vote" | "drop_box"; +} + +interface SampleBallot { + precinctId: string; + precinctName?: string; + electionId: string; + electionName: string; + electionDate: string; + contests: BallotContest[]; + measures: BallotMeasure[]; + pdfUrl?: string; +} + +interface BallotContest { + office: string; + district?: string; + candidates: Candidate[]; + votesAllowed: number; +} + +interface Candidate { + name: string; + party?: string; + incumbent?: boolean; + url?: string; +} + +interface BallotMeasure { + measureId: string; + title: string; + description?: string; + type: "bond" | "initiative" | "referendum" | "advisory"; + fullTextUrl?: string; +} + +interface ElectionDeadline { + date: string; + description: string; + type: "registration" | "ballot_request" | "ballot_return" | "early_voting" | "election_day"; +} + +interface Election { + id: string; + name: string; + date: string; + electionType: "primary" | "general" | "special" | "runoff"; + deadlines: ElectionDeadline[]; +} + +interface CandidateFiling { + candidateName: string; + office: string; + district?: string; + party?: string; + filingDate: string; + status: "filed" | "qualified" | "withdrawn"; + electionId: string; +} + +// Google Civic Info API types +interface GoogleVoterInfoResponse { + election?: { + id: string; + name: string; + electionDay: string; + ocdDivisionId: string; + }; + pollingLocations?: GooglePollingLocation[]; + earlyVoteSites?: GooglePollingLocation[]; + dropOffLocations?: GooglePollingLocation[]; + contests?: GoogleContest[]; + state?: Array<{ + electionAdministrationBody?: { + name: string; + electionInfoUrl?: string; + votingLocationFinderUrl?: string; + ballotInfoUrl?: string; + correspondenceAddress?: GoogleAddress; + physicalAddress?: GoogleAddress; + }; + local_jurisdiction?: { + name: string; + electionAdministrationBody?: { + electionInfoUrl?: string; + votingLocationFinderUrl?: string; + }; + }; + }>; +} + +interface GooglePollingLocation { + address: GoogleAddress; + notes?: string; + pollingHours?: string; + name?: string; + voterServices?: string; + startDate?: string; + endDate?: string; + latitude?: number; + longitude?: number; + sources?: Array<{ name: string; official: boolean }>; +} + +interface GoogleAddress { + locationName?: string; + line1: string; + line2?: string; + line3?: string; + city: string; + state: string; + zip: string; +} + +interface GoogleContest { + type: string; + office?: string; + district?: { name: string; scope: string }; + numberElected?: string; + candidates?: Array<{ + name: string; + party?: string; + candidateUrl?: string; + email?: string; + phone?: string; + }>; + referendumTitle?: string; + referendumSubtitle?: string; + referendumUrl?: string; + referendumBrief?: string; +} + +interface GoogleElectionsResponse { + elections: Array<{ + id: string; + name: string; + electionDay: string; + ocdDivisionId: string; + }>; +} + +// ============== Cache Implementation ============== + +interface CacheEntry { + data: T; + timestamp: number; + ttl: number; +} + +class DataCache { + private cache = new Map>(); + private defaultTtl = 15 * 60 * 1000; // 15 minutes + + set(key: string, data: T, ttl?: number): void { + this.cache.set(key, { + data, + timestamp: Date.now(), + ttl: ttl ?? this.defaultTtl, + }); + } + + get(key: string): T | null { + const entry = this.cache.get(key) as CacheEntry | undefined; + if (!entry) return null; + if (Date.now() - entry.timestamp > entry.ttl) { + this.cache.delete(key); + return null; + } + return entry.data; + } + + clear(): void { + this.cache.clear(); + } +} + +const cache = new DataCache(); + +// ============== API Functions ============== + +function getGoogleCivicApiKey(): string { + const key = process.env.GOOGLE_CIVIC_API_KEY; + if (!key) { + throw new Error( + "GOOGLE_CIVIC_API_KEY is not set. Get one at https://console.cloud.google.com/apis/credentials" + ); + } + return key; +} + +async function googleCivicFetch( + endpoint: string, + params: Record = {} +): Promise { + const apiKey = getGoogleCivicApiKey(); + const url = new URL(`https://www.googleapis.com/civicinfo/v2${endpoint}`); + url.searchParams.set("key", apiKey); + for (const [k, v] of Object.entries(params)) { + url.searchParams.set(k, v); + } + + const cacheKey = url.toString(); + const cached = cache.get(cacheKey); + if (cached) { + logger.debug(`Cache hit: ${endpoint}`); + return cached; + } + + // Rate limiting: 1 request per 100ms + await new Promise((r) => setTimeout(r, 100)); + + const res = await fetchWithRetry(url.toString(), { + headers: { + Accept: "application/json", + "User-Agent": "billion-scraper/1.0 (civic-info-app)", + }, + }); + + const data = (await res.json()) as T; + cache.set(cacheKey, data); + return data; +} + +// ============== Data Fetching Functions ============== + +/** + * Get upcoming elections from Google Civic API + */ +export async function getUpcomingElections(): Promise { + try { + const response = await googleCivicFetch("/elections"); + + return response.elections + .filter((e) => { + // Filter for California elections that may include Santa Clara + const isCaliforniaOrNational = + e.ocdDivisionId.includes("state:ca") || + e.ocdDivisionId === "ocd-division/country:us"; + return isCaliforniaOrNational; + }) + .map((e) => ({ + id: e.id, + name: e.name, + date: e.electionDay, + electionType: inferElectionType(e.name), + deadlines: generateDeadlines(e.electionDay), + })); + } catch (error) { + logger.error("Failed to fetch elections:", error); + return []; + } +} + +/** + * Get voter info (polling locations, ballot info) for an address + */ +export async function getVoterInfo( + address: string, + electionId?: string +): Promise<{ + pollingLocations: PollingLocation[]; + ballot: SampleBallot | null; + electionInfo: Election | null; +}> { + const params: Record = { address }; + if (electionId) { + params.electionId = electionId; + } + + try { + const response = await googleCivicFetch( + "/voterinfo", + params + ); + + const pollingLocations: PollingLocation[] = []; + + // Process polling places + if (response.pollingLocations) { + for (const loc of response.pollingLocations) { + pollingLocations.push(convertGooglePollingLocation(loc, "polling_place")); + } + } + + // Process early vote sites + if (response.earlyVoteSites) { + for (const loc of response.earlyVoteSites) { + pollingLocations.push(convertGooglePollingLocation(loc, "early_vote")); + } + } + + // Process drop-off locations + if (response.dropOffLocations) { + for (const loc of response.dropOffLocations) { + pollingLocations.push(convertGooglePollingLocation(loc, "drop_box")); + } + } + + // Build sample ballot from contests + let ballot: SampleBallot | null = null; + if (response.election && response.contests) { + ballot = { + precinctId: extractPrecinctFromAddress(address), + electionId: response.election.id, + electionName: response.election.name, + electionDate: response.election.electionDay, + contests: [], + measures: [], + }; + + for (const contest of response.contests) { + if (contest.type === "Referendum" && contest.referendumTitle) { + ballot.measures.push({ + measureId: contest.referendumTitle.replace(/\s+/g, "-").toLowerCase(), + title: contest.referendumTitle, + description: contest.referendumSubtitle ?? contest.referendumBrief, + type: "initiative", + fullTextUrl: contest.referendumUrl, + }); + } else if (contest.office && contest.candidates) { + ballot.contests.push({ + office: contest.office, + district: contest.district?.name, + votesAllowed: parseInt(contest.numberElected ?? "1", 10), + candidates: contest.candidates.map((c) => ({ + name: c.name, + party: c.party, + url: c.candidateUrl, + })), + }); + } + } + } + + // Build election info + let electionInfo: Election | null = null; + if (response.election) { + electionInfo = { + id: response.election.id, + name: response.election.name, + date: response.election.electionDay, + electionType: inferElectionType(response.election.name), + deadlines: generateDeadlines(response.election.electionDay), + }; + } + + return { pollingLocations, ballot, electionInfo }; + } catch (error) { + logger.error("Failed to fetch voter info:", error); + return { pollingLocations: [], ballot: null, electionInfo: null }; + } +} + +/** + * Get polling locations for Santa Clara County (requires sample addresses) + * Since the API is address-based, we use representative addresses from each city + */ +export async function getCountyPollingLocations(): Promise { + // Representative addresses from major Santa Clara County cities + const sampleAddresses = [ + "200 E Santa Clara St, San Jose, CA 95113", // Downtown San Jose + "100 City Hall Plaza, Mountain View, CA 94041", // Mountain View + "250 Hamilton Ave, Palo Alto, CA 94301", // Palo Alto + "10300 Torre Ave, Cupertino, CA 95014", // Cupertino + "456 W Olive Ave, Sunnyvale, CA 94086", // Sunnyvale + "37000 Fremont Blvd, Fremont, CA 94536", // Near Milpitas + "110 E Main St, Los Gatos, CA 95030", // Los Gatos + "850 Blossom Hill Rd, San Jose, CA 95123", // South San Jose + "1500 Warburton Ave, Santa Clara, CA 95050", // Santa Clara + ]; + + const allLocations: PollingLocation[] = []; + const seenIds = new Set(); + + for (const address of sampleAddresses) { + try { + const { pollingLocations } = await getVoterInfo(address); + for (const loc of pollingLocations) { + if (!seenIds.has(loc.id)) { + seenIds.add(loc.id); + allLocations.push(loc); + } + } + } catch (error) { + logger.warn(`Failed to get polling locations for ${address}:`, error); + } + + // Rate limiting between requests + await new Promise((r) => setTimeout(r, 200)); + } + + logger.info(`Found ${allLocations.length} unique polling locations`); + return allLocations; +} + +/** + * Get election calendar with important deadlines + */ +export async function getElectionCalendar(): Promise<{ + elections: Election[]; + upcomingDeadlines: ElectionDeadline[]; +}> { + const elections = await getUpcomingElections(); + + // Collect all deadlines and sort by date + const allDeadlines: (ElectionDeadline & { electionName: string })[] = []; + for (const election of elections) { + for (const deadline of election.deadlines) { + allDeadlines.push({ ...deadline, electionName: election.name }); + } + } + + allDeadlines.sort( + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() + ); + + // Filter to upcoming deadlines only + const now = new Date(); + const upcomingDeadlines = allDeadlines.filter( + (d) => new Date(d.date) >= now + ); + + return { elections, upcomingDeadlines }; +} + +/** + * Get candidate filings (from Google Civic contests data) + */ +export async function getCandidateFilings( + electionId?: string +): Promise { + // Use a central Santa Clara County address + const { ballot } = await getVoterInfo( + "70 W Hedding St, San Jose, CA 95110", // County Government Center + electionId + ); + + if (!ballot) { + return []; + } + + const filings: CandidateFiling[] = []; + + for (const contest of ballot.contests) { + for (const candidate of contest.candidates) { + filings.push({ + candidateName: candidate.name, + office: contest.office, + district: contest.district, + party: candidate.party, + filingDate: ballot.electionDate, // Approximate - Google API doesn't provide filing dates + status: "qualified", // Candidates on ballot are qualified + electionId: ballot.electionId, + }); + } + } + + return filings; +} + +// ============== Helper Functions ============== + +function convertGooglePollingLocation( + loc: GooglePollingLocation, + type: PollingLocation["locationType"] +): PollingLocation { + const addr = loc.address; + const id = `${addr.line1}-${addr.city}-${addr.zip}`.replace(/\s+/g, "-").toLowerCase(); + + return { + id, + name: loc.name ?? addr.locationName ?? "Polling Location", + address: { + line1: addr.line1, + line2: [addr.line2, addr.line3].filter(Boolean).join(", ") || undefined, + city: addr.city, + state: addr.state, + zip: addr.zip, + }, + hours: loc.pollingHours, + notes: loc.notes, + latitude: loc.latitude, + longitude: loc.longitude, + voterServices: loc.voterServices?.split(",").map((s) => s.trim()), + startDate: loc.startDate, + endDate: loc.endDate, + locationType: type, + }; +} + +function inferElectionType(name: string): Election["electionType"] { + const lower = name.toLowerCase(); + if (lower.includes("primary")) return "primary"; + if (lower.includes("general")) return "general"; + if (lower.includes("special")) return "special"; + if (lower.includes("runoff")) return "runoff"; + return "general"; +} + +function generateDeadlines(electionDay: string): ElectionDeadline[] { + const election = new Date(electionDay); + + // California election deadlines (typical) + return [ + { + date: offsetDate(election, -15).toISOString().split("T")[0]!, + description: "Last day to register to vote", + type: "registration" as const, + }, + { + date: offsetDate(election, -29).toISOString().split("T")[0]!, + description: "Vote-by-mail ballots begin mailing", + type: "ballot_request" as const, + }, + { + date: offsetDate(election, -10).toISOString().split("T")[0]!, + description: "Early voting begins", + type: "early_voting" as const, + }, + { + date: electionDay, + description: "Election Day - Polls open 7am to 8pm", + type: "election_day" as const, + }, + { + date: electionDay, + description: "Last day to return vote-by-mail ballot (postmarked by this date)", + type: "ballot_return" as const, + }, + ]; +} + +function offsetDate(date: Date, days: number): Date { + const result = new Date(date); + result.setDate(result.getDate() + days); + return result; +} + +function extractPrecinctFromAddress(address: string): string { + // Generate a pseudo-precinct ID from the address + // In production, this would come from actual precinct lookup + const hash = address + .toLowerCase() + .replace(/[^a-z0-9]/g, "") + .slice(0, 10); + return `SC-${hash}`; +} + +// ============== Main Scraper Implementation ============== + +interface SantaClaraROVConfig { + /** Fetch polling locations for sample addresses */ + fetchPollingLocations?: boolean; + /** Fetch election calendar */ + fetchElectionCalendar?: boolean; + /** Fetch candidate filings */ + fetchCandidateFilings?: boolean; + /** Specific election ID to query */ + electionId?: string; +} + +async function scrape(config: SantaClaraROVConfig = {}): Promise { + const { + fetchPollingLocations = true, + fetchElectionCalendar = true, + fetchCandidateFilings = true, + electionId, + } = config; + + logger.info("Starting Santa Clara County ROV scraper..."); + + // Check for API key + try { + getGoogleCivicApiKey(); + } catch (error) { + logger.error((error as Error).message); + logger.warn( + "Skipping Santa Clara ROV scraper - no API key configured" + ); + return; + } + + const results: { + elections?: Election[]; + pollingLocations?: PollingLocation[]; + candidateFilings?: CandidateFiling[]; + } = {}; + + // Fetch election calendar + if (fetchElectionCalendar) { + logger.info("Fetching election calendar..."); + const { elections, upcomingDeadlines } = await getElectionCalendar(); + results.elections = elections; + + logger.info(`Found ${elections.length} upcoming elections`); + for (const deadline of upcomingDeadlines.slice(0, 5)) { + logger.info(` ${deadline.date}: ${deadline.description}`); + } + } + + // Fetch polling locations + if (fetchPollingLocations) { + logger.info("Fetching polling locations..."); + results.pollingLocations = await getCountyPollingLocations(); + logger.info( + `Found ${results.pollingLocations.length} polling locations` + ); + } + + // Fetch candidate filings + if (fetchCandidateFilings) { + logger.info("Fetching candidate filings..."); + results.candidateFilings = await getCandidateFilings(electionId); + logger.info( + `Found ${results.candidateFilings.length} candidate filings` + ); + } + + // Summary + logger.success("Santa Clara County ROV scraper completed"); + logger.info(`Elections: ${results.elections?.length ?? 0}`); + logger.info(`Polling locations: ${results.pollingLocations?.length ?? 0}`); + logger.info(`Candidate filings: ${results.candidateFilings?.length ?? 0}`); + + // Note: This scraper currently returns data but doesn't persist to DB + // because the database schema would need new tables for local election data. + // The exported functions can be used by other parts of the app to fetch + // fresh data on demand. +} + +export const santaClaraROV: Scraper = { + name: NAME, + scrape: () => scrape(), +}; + +export default santaClaraROV; diff --git a/apps/scraper/src/scrapers/vote411-ballot.ts b/apps/scraper/src/scrapers/vote411-ballot.ts new file mode 100644 index 0000000..cb1f0e0 --- /dev/null +++ b/apps/scraper/src/scrapers/vote411-ballot.ts @@ -0,0 +1,686 @@ +/** + * VOTE411.org Ballot Lookup - Playwright-based scraper + * + * This module provides address-based ballot lookup functionality for VOTE411.org. + * It uses Playwright to handle the JavaScript-rendered ballot widget. + * + * Usage: + * import { Vote411BallotLookup } from './vote411-ballot'; + * + * const lookup = new Vote411BallotLookup(); + * await lookup.initialize(); + * + * const ballot = await lookup.lookupByAddress('123 Main St, San Jose, CA 95112'); + * console.log(ballot); + * + * await lookup.close(); + */ + +import { chromium, type Browser, type BrowserContext, type Page } from "playwright"; +import * as cheerio from "cheerio"; +import { createHash } from "crypto"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; +import { join } from "path"; + +import { createLogger } from "../utils/log.js"; + +const logger = createLogger("VOTE411-Ballot"); + +// Cache directory +const CACHE_DIR = join(process.cwd(), ".cache", "vote411-ballots"); + +// Rate limiting +const MIN_REQUEST_DELAY = 2000; // Be extra respectful with browser automation +let lastRequestTime = 0; + +// ==================== Types ==================== + +export interface BallotCandidate { + name: string; + party?: string; + photoUrl?: string; + website?: string; + email?: string; + phone?: string; + socialMedia?: { + facebook?: string; + twitter?: string; + instagram?: string; + }; + biography?: string; + questionnaire: QuestionnaireResponse[]; +} + +export interface QuestionnaireResponse { + question: string; + answer: string; +} + +export interface BallotRace { + office: string; + description?: string; + district?: string; + candidates: BallotCandidate[]; +} + +export interface BallotMeasure { + name: string; + title: string; + description?: string; + summary?: string; + proArguments: string[]; + conArguments: string[]; + fullText?: string; +} + +export interface Election { + name: string; + date: string; + races: BallotRace[]; + measures: BallotMeasure[]; +} + +export interface BallotInfo { + address: string; + normalizedAddress?: string; + lookupTimestamp: Date; + pollingLocation?: { + name?: string; + address: string; + hours?: string; + }; + elections: Election[]; + errors?: string[]; +} + +interface CacheEntry { + data: BallotInfo; + timestamp: number; + address: string; +} + +// ==================== Utilities ==================== + +function ensureCacheDir(): void { + if (!existsSync(CACHE_DIR)) { + mkdirSync(CACHE_DIR, { recursive: true }); + } +} + +function getCacheKey(address: string): string { + const normalized = address.toLowerCase().trim().replace(/\s+/g, " "); + return createHash("md5").update(normalized).digest("hex"); +} + +function getCachePath(address: string): string { + return join(CACHE_DIR, `${getCacheKey(address)}.json`); +} + +function readCache(address: string, maxAgeMs: number = 86400000): BallotInfo | null { + ensureCacheDir(); + const cachePath = getCachePath(address); + + if (!existsSync(cachePath)) { + return null; + } + + try { + const cached: CacheEntry = JSON.parse(readFileSync(cachePath, "utf-8")); + const age = Date.now() - cached.timestamp; + + if (age > maxAgeMs) { + logger.debug(`Cache expired for address lookup`); + return null; + } + + logger.debug(`Cache hit for address lookup`); + return cached.data; + } catch { + return null; + } +} + +function writeCache(address: string, data: BallotInfo): void { + ensureCacheDir(); + const cachePath = getCachePath(address); + const entry: CacheEntry = { + data, + timestamp: Date.now(), + address, + }; + + writeFileSync(cachePath, JSON.stringify(entry, null, 2)); + logger.debug(`Cached ballot lookup result`); +} + +async function rateLimit(): Promise { + const now = Date.now(); + const elapsed = now - lastRequestTime; + + if (elapsed < MIN_REQUEST_DELAY) { + const delay = MIN_REQUEST_DELAY - elapsed; + logger.debug(`Rate limiting: waiting ${delay}ms`); + await new Promise((r) => setTimeout(r, delay)); + } + + lastRequestTime = Date.now(); +} + +// ==================== Vote411BallotLookup Class ==================== + +export class Vote411BallotLookup { + private browser: Browser | null = null; + private context: BrowserContext | null = null; + private page: Page | null = null; + private initialized = false; + + constructor(private options: { headless?: boolean; cacheMaxAgeMs?: number } = {}) { + this.options = { + headless: options.headless ?? true, + cacheMaxAgeMs: options.cacheMaxAgeMs ?? 86400000, // 24 hours default + }; + } + + /** + * Initialize the browser for ballot lookups + */ + async initialize(): Promise { + if (this.initialized) return; + + logger.info("Initializing ballot lookup browser..."); + + this.browser = await chromium.launch({ + headless: this.options.headless, + args: ["--disable-dev-shm-usage"], + }); + + this.context = await this.browser.newContext({ + viewport: { width: 1280, height: 800 }, + userAgent: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + }); + + this.page = await this.context.newPage(); + this.initialized = true; + + logger.success("Ballot lookup browser initialized"); + } + + /** + * Close the browser and clean up resources + */ + async close(): Promise { + if (this.browser) { + await this.browser.close(); + this.browser = null; + this.context = null; + this.page = null; + this.initialized = false; + logger.info("Ballot lookup browser closed"); + } + } + + /** + * Look up ballot information by address + */ + async lookupByAddress( + address: string, + options: { useCache?: boolean; waitForCandidates?: boolean } = {} + ): Promise { + const { useCache = true, waitForCandidates = true } = options; + + // Check cache first + if (useCache) { + const cached = readCache(address, this.options.cacheMaxAgeMs); + if (cached) { + return cached; + } + } + + if (!this.initialized || !this.page) { + await this.initialize(); + } + + await rateLimit(); + + const result: BallotInfo = { + address, + lookupTimestamp: new Date(), + elections: [], + errors: [], + }; + + try { + logger.info(`Looking up ballot for: ${address}`); + + // Navigate to the ballot page + await this.page!.goto("https://www.vote411.org/ballot", { + waitUntil: "domcontentloaded", + timeout: 60000, + }); + + // Wait for the ballot widget to load - it may take a while + try { + await this.page!.waitForSelector(".lwv-ballot-widget--root", { timeout: 20000 }); + } catch { + // Widget class may not exist, try waiting for any form element + await this.page!.waitForTimeout(5000); + } + + // Wait a bit more for the widget to fully initialize + await this.page!.waitForTimeout(3000); + + // Find and fill the address input + // The widget has various possible input selectors + const addressInput = await this.page!.$( + 'input[placeholder*="Main St" i], ' + + '.lwv-ballot-widget--root input[type="text"], ' + + '.ballot-iframe-wrapper input[type="text"], ' + + 'input[placeholder*="address" i], ' + + 'input[placeholder*="Enter" i], ' + + 'input[placeholder*="street" i], ' + + 'input[aria-label*="address" i], ' + + 'input.address-input, ' + + 'input#address' + ); + + if (!addressInput) { + // Try to find input inside an iframe + const frames = this.page!.frames(); + let foundInput = false; + for (const frame of frames) { + try { + const frameInput = await frame.$('input[type="text"]'); + if (frameInput) { + await frameInput.fill(address); + await frameInput.press("Enter"); + foundInput = true; + break; + } + } catch { + // Frame may not be accessible + } + } + if (!foundInput) { + result.errors!.push("Could not find address input field - the ballot widget may require JavaScript that is not fully loaded"); + return result; + } + } else { + // Clear any existing text and enter the address + await addressInput.click({ clickCount: 3 }); // Select all + await addressInput.fill(address); + + // Wait for autocomplete to potentially appear + await this.page!.waitForTimeout(2000); + + // The VOTE411 widget uses a "Get Started" button + const searchButton = await this.page!.$( + 'button:has-text("Get Started"), ' + + 'button:has-text("Search"), ' + + 'button:has-text("Find"), ' + + 'button:has-text("Go"), ' + + 'button[type="submit"]' + ); + if (searchButton) { + await searchButton.click(); + } else { + await addressInput.press("Enter"); + } + } + + // Wait for settings modal and click through + await this.page!.waitForTimeout(3000); + + // Click Save & View Races button if present + const saveRacesBtn = await this.page!.$( + 'button:has-text("Save & View Races"), ' + + 'button:has-text("View Races")' + ); + if (saveRacesBtn) { + await saveRacesBtn.click(); + await this.page!.waitForTimeout(3000); + } + + // Wait for results to load - look for "Your Races" section or similar + try { + await this.page!.waitForSelector( + ':has-text("Your Races"), ' + + ':has-text("View Race"), ' + + '.election, .race, .ballot-race, .ballot-results, .no-results, .error-message', + { timeout: 30000 } + ); + } catch { + result.errors!.push("Timeout waiting for ballot results"); + return result; + } + + // Check for errors or no results + const noResults = await this.page!.$('.no-results, .error-message, :text("No elections found"), :text("not found")'); + if (noResults) { + const errorText = await noResults.textContent(); + result.errors!.push(errorText?.trim() || "No ballot information found for this address"); + return result; + } + + // Get the normalized address if displayed + const normalizedAddressEl = await this.page!.$('.matched-address, .address-result, .your-address'); + if (normalizedAddressEl) { + result.normalizedAddress = (await normalizedAddressEl.textContent())?.trim(); + } + + // Get polling location if available + const pollingEl = await this.page!.$('.polling-location, .poll-location, .voting-location'); + if (pollingEl) { + const pollingAddress = await pollingEl.$('.address'); + const pollingHours = await pollingEl.$('.hours'); + result.pollingLocation = { + address: (await pollingAddress?.textContent())?.trim() || "", + hours: (await pollingHours?.textContent())?.trim(), + }; + } + + // Parse the ballot content + const html = await this.page!.content(); + await this.parseBallotContent(html, result, waitForCandidates); + + // Cache successful results + if (result.elections.length > 0 && useCache) { + writeCache(address, result); + } + + logger.success( + `Found ${result.elections.length} election(s) with ` + + `${result.elections.reduce((acc, e) => acc + e.races.length, 0)} race(s)` + ); + + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger.error(`Ballot lookup failed: ${message}`); + result.errors!.push(`Lookup failed: ${message}`); + } + + return result; + } + + /** + * Parse ballot content from HTML + */ + private async parseBallotContent( + html: string, + result: BallotInfo, + _expandCandidates: boolean + ): Promise { + const $ = cheerio.load(html); + + // VOTE411 uses a specific structure with racelist-container and detail-list-row + const flatElection: Election = { + name: "Your Races", + date: "", + races: [], + measures: [], + }; + + // Try to get the address display + const addressDisplay = $('.lwv--address-display-container').text().trim(); + if (addressDisplay) { + result.normalizedAddress = addressDisplay; + } + + // Parse races from VOTE411's structure + // Each race is a detail-list-row button within racelist-container + const racelistContainer = $('.racelist-container'); + + if (racelistContainer.length > 0) { + racelistContainer.find('.detail-list-row, [data-testid="detail-list-row-outer"]').each((_, el) => { + const $row = $(el); + // The race name is in the button text, excluding "Please select" and "View Race" + const fullText = $row.text().trim(); + const officeName = fullText + .replace(/Please select a candidate\(s\)/gi, '') + .replace(/View Race/gi, '') + .trim(); + + if (officeName) { + flatElection.races.push({ + office: officeName, + candidates: [], // Candidates need to be fetched by clicking "View Race" + }); + } + }); + } + + // Also try generic race parsing for other structures + if (flatElection.races.length === 0) { + // Try election sections + const electionSelectors = [ + '.election', + '.ballot-election', + '[data-election]', + '.election-section', + '.your-races-page-container', + ]; + + for (const selector of electionSelectors) { + $(selector).each((_, el) => { + const $el = $(el); + + // Parse races within this election + $el.find('.race, .ballot-race, .contest, [data-race]').each((_, raceEl) => { + const $race = $(raceEl); + const race = this.parseRace($race); + if (race.office) { + flatElection.races.push(race); + } + }); + + // Parse ballot measures + $el.find('.measure, .ballot-measure, .proposition, [data-measure]').each((_, measureEl) => { + const $measure = $(measureEl); + const measure = this.parseMeasure($measure); + if (measure.name || measure.title) { + flatElection.measures.push(measure); + } + }); + }); + + if (flatElection.races.length > 0) break; + } + } + + // Last resort: try to extract race names from text patterns + if (flatElection.races.length === 0) { + const pageText = $('body').text(); + // Look for patterns like "DC Mayor", "US Senator", etc. + const racePatterns = [ + /([A-Z]{2})\s+(United States Senator|Mayor|Attorney General|Governor|Secretary of State)/gi, + /([A-Z]{2})\s+(?:U\.?S\.?|United States)\s+(Senator|Representative|House)/gi, + /(President|Vice President)\s+of\s+(?:the\s+)?United States/gi, + ]; + + for (const pattern of racePatterns) { + const matches = pageText.match(pattern); + if (matches) { + for (const match of matches) { + const officeName = match.trim(); + if (officeName && !flatElection.races.some(r => r.office === officeName)) { + flatElection.races.push({ + office: officeName, + candidates: [], + }); + } + } + } + } + } + + if (flatElection.races.length > 0 || flatElection.measures.length > 0) { + result.elections.push(flatElection); + } + } + + /** + * Parse a race element + */ + private parseRace($race: cheerio.Cheerio): BallotRace { + const race: BallotRace = { + office: $race.find('.race-name, .office-name, .contest-name, h3, h4').first().text().trim(), + description: $race.find('.race-description, .contest-description').first().text().trim() || undefined, + district: $race.find('.district, .district-name').first().text().trim() || undefined, + candidates: [], + }; + + // Parse candidates + $race.find('.candidate, .ballot-candidate, [data-candidate]').each((_, candEl) => { + const $cand = $race.find(candEl); + const candidate = this.parseCandidate($cand); + if (candidate.name) { + race.candidates.push(candidate); + } + }); + + return race; + } + + /** + * Parse a candidate element + */ + private parseCandidate($cand: cheerio.Cheerio): BallotCandidate { + return { + name: $cand.find('.candidate-name, .name, h4, h5').first().text().trim(), + party: $cand.find('.party, .candidate-party').first().text().trim() || undefined, + photoUrl: $cand.find('img.candidate-photo, img.photo').first().attr('src') || undefined, + website: $cand.find('a.website, a[href*="http"]').first().attr('href') || undefined, + biography: $cand.find('.bio, .biography').first().text().trim() || undefined, + questionnaire: [], + }; + } + + /** + * Parse a ballot measure element + */ + private parseMeasure($measure: cheerio.Cheerio): BallotMeasure { + const measure: BallotMeasure = { + name: $measure.find('.measure-name, .measure-number, .proposition-number').first().text().trim(), + title: $measure.find('.measure-title, .proposition-title, h3, h4').first().text().trim(), + description: $measure.find('.measure-description, .summary').first().text().trim() || undefined, + proArguments: [], + conArguments: [], + }; + + // Parse pro/con arguments + $measure.find('.pro-argument, .argument-for').each((_, el) => { + const text = $measure.find(el).text().trim(); + if (text) measure.proArguments.push(text); + }); + + $measure.find('.con-argument, .argument-against').each((_, el) => { + const text = $measure.find(el).text().trim(); + if (text) measure.conArguments.push(text); + }); + + return measure; + } + + /** + * Expand candidate details by clicking on each candidate + */ + private async expandCandidateDetails(result: BallotInfo): Promise { + if (!this.page) return; + + try { + // Find all expandable candidate elements + const candidateLinks = await this.page.$$('.candidate-link, .candidate-name[role="button"], .expand-candidate'); + + for (let i = 0; i < Math.min(candidateLinks.length, 20); i++) { // Limit to prevent too many clicks + try { + const link = candidateLinks[i]; + if (!link) continue; + await link.click(); + await this.page.waitForTimeout(500); // Brief wait for content to expand + + // Try to find questionnaire responses in the expanded content + const questionnaireEl = await this.page.$('.questionnaire, .candidate-responses, .qa-section'); + if (questionnaireEl) { + const qaHtml = await questionnaireEl.innerHTML(); + const $ = cheerio.load(qaHtml); + + const responses: QuestionnaireResponse[] = []; + $('.question-answer, .qa-pair, .response').each((_, el) => { + const question = $(el).find('.question').text().trim(); + const answer = $(el).find('.answer').text().trim(); + if (question && answer) { + responses.push({ question, answer }); + } + }); + + // Match responses to the corresponding candidate in result + // This is approximate - in a real implementation you'd track which candidate was clicked + if (responses.length > 0) { + logger.debug(`Found ${responses.length} questionnaire responses`); + } + } + + // Close the expanded section if there's a close button + const closeBtn = await this.page.$('.close-candidate, .collapse-candidate, [aria-label="Close"]'); + if (closeBtn) { + await closeBtn.click(); + await this.page.waitForTimeout(200); + } + } catch { + // Continue with other candidates if one fails + } + } + } catch (error) { + logger.debug(`Could not expand candidate details: ${error}`); + } + } + + /** + * Look up ballot by ZIP code only (less precise, may show multiple districts) + */ + async lookupByZip(zipCode: string, options: { useCache?: boolean } = {}): Promise { + // ZIP code lookups typically just use the ZIP as the address + return this.lookupByAddress(zipCode, options); + } +} + +// ==================== Convenience Functions ==================== + +let sharedLookup: Vote411BallotLookup | null = null; + +/** + * Get a shared ballot lookup instance (creates one if needed) + */ +export async function getSharedLookup(): Promise { + if (!sharedLookup) { + sharedLookup = new Vote411BallotLookup(); + await sharedLookup.initialize(); + } + return sharedLookup; +} + +/** + * Close the shared ballot lookup instance + */ +export async function closeSharedLookup(): Promise { + if (sharedLookup) { + await sharedLookup.close(); + sharedLookup = null; + } +} + +/** + * Quick ballot lookup by address (uses shared instance) + */ +export async function quickLookupByAddress(address: string): Promise { + const lookup = await getSharedLookup(); + return lookup.lookupByAddress(address); +} + +/** + * Quick ballot lookup by ZIP code (uses shared instance) + */ +export async function quickLookupByZip(zipCode: string): Promise { + const lookup = await getSharedLookup(); + return lookup.lookupByZip(zipCode); +} diff --git a/apps/scraper/src/scrapers/vote411.ts b/apps/scraper/src/scrapers/vote411.ts new file mode 100644 index 0000000..ef70010 --- /dev/null +++ b/apps/scraper/src/scrapers/vote411.ts @@ -0,0 +1,584 @@ +/** + * VOTE411.org Scraper + * + * Scrapes voter guide information from the League of Women Voters' VOTE411 platform. + * This includes: + * - State-level voting information (deadlines, rules) + * - Available voter guides and candidate forums + * - Election dates and information + * + * NOTE: Full ballot lookup by address requires JavaScript execution (browser automation) + * as the ballot widget is client-side rendered. This scraper focuses on publicly + * available state-level information that can be scraped with cheerio. + * + * For address-based ballot lookup, consider using the Playwright-based approach + * documented in the BallotLookup class below. + */ + +import * as cheerio from "cheerio"; +import { createHash } from "crypto"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; +import { join } from "path"; + +import { fetchWithRetry } from "../utils/fetch.js"; +import { createLogger } from "../utils/log.js"; +import type { Scraper } from "../utils/types.js"; + +const NAME = "VOTE411"; +const BASE_URL = "https://www.vote411.org"; +const logger = createLogger(NAME); + +// Cache directory for scraped data +const CACHE_DIR = join(process.cwd(), ".cache", "vote411"); + +// Rate limiting: minimum delay between requests (ms) +const MIN_REQUEST_DELAY = 1000; +let lastRequestTime = 0; + +// ==================== Types ==================== + +export interface StateInfo { + name: string; + slug: string; + url: string; +} + +export interface ElectionDate { + name: string; + date: string; + description?: string; +} + +export interface VotingDeadline { + type: string; + date: string; + description?: string; +} + +export interface VoterGuideLink { + title: string; + url: string; + type: "pdf" | "video" | "external"; + state?: string; +} + +export interface CandidateForum { + title: string; + url: string; + state?: string; + date?: string; +} + +export interface StateVotingInfo { + state: string; + stateSlug: string; + url: string; + scrapedAt: Date; + elections: ElectionDate[]; + deadlines: VotingDeadline[]; + voterGuides: VoterGuideLink[]; + alerts: string[]; +} + +export interface BallotRace { + office: string; + district?: string; + candidates: BallotCandidate[]; +} + +export interface BallotCandidate { + name: string; + party?: string; + photoUrl?: string; + website?: string; + questionnaire?: QuestionnaireResponse[]; +} + +export interface QuestionnaireResponse { + question: string; + answer: string; +} + +export interface BallotMeasure { + name: string; + description: string; + proArguments?: string[]; + conArguments?: string[]; + fullText?: string; +} + +export interface BallotInfo { + address: string; + elections: Array<{ + name: string; + date: string; + races: BallotRace[]; + measures: BallotMeasure[]; + }>; + pollingLocation?: { + name: string; + address: string; + hours?: string; + }; +} + +// ==================== Utilities ==================== + +function ensureCacheDir(): void { + if (!existsSync(CACHE_DIR)) { + mkdirSync(CACHE_DIR, { recursive: true }); + } +} + +function getCachePath(key: string): string { + const hash = createHash("md5").update(key).digest("hex"); + return join(CACHE_DIR, `${hash}.json`); +} + +interface CacheEntry { + data: T; + timestamp: number; + key: string; +} + +function readCache(key: string, maxAgeMs: number = 3600000): T | null { + ensureCacheDir(); + const cachePath = getCachePath(key); + + if (!existsSync(cachePath)) { + return null; + } + + try { + const cached: CacheEntry = JSON.parse(readFileSync(cachePath, "utf-8")); + const age = Date.now() - cached.timestamp; + + if (age > maxAgeMs) { + logger.debug(`Cache expired for ${key}`); + return null; + } + + logger.debug(`Cache hit for ${key}`); + return cached.data; + } catch { + return null; + } +} + +function writeCache(key: string, data: T): void { + ensureCacheDir(); + const cachePath = getCachePath(key); + const entry: CacheEntry = { + data, + timestamp: Date.now(), + key, + }; + + writeFileSync(cachePath, JSON.stringify(entry, null, 2)); + logger.debug(`Cached ${key}`); +} + +async function rateLimitedFetch(url: string): Promise { + const now = Date.now(); + const elapsed = now - lastRequestTime; + + if (elapsed < MIN_REQUEST_DELAY) { + const delay = MIN_REQUEST_DELAY - elapsed; + logger.debug(`Rate limiting: waiting ${delay}ms`); + await new Promise((r) => setTimeout(r, delay)); + } + + lastRequestTime = Date.now(); + + return fetchWithRetry(url, { + headers: { + "User-Agent": + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + Accept: + "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.5", + }, + timeoutMs: 30_000, + }); +} + +// ==================== State Info Scraping ==================== + +const US_STATES: StateInfo[] = [ + { name: "Alabama", slug: "alabama", url: `${BASE_URL}/alabama` }, + { name: "Alaska", slug: "alaska", url: `${BASE_URL}/alaska` }, + { name: "Arizona", slug: "arizona", url: `${BASE_URL}/arizona` }, + { name: "Arkansas", slug: "arkansas", url: `${BASE_URL}/arkansas` }, + { name: "California", slug: "california", url: `${BASE_URL}/california` }, + { name: "Colorado", slug: "colorado", url: `${BASE_URL}/colorado` }, + { name: "Connecticut", slug: "connecticut", url: `${BASE_URL}/connecticut` }, + { name: "Delaware", slug: "delaware", url: `${BASE_URL}/delaware` }, + { + name: "District of Columbia", + slug: "district-of-columbia", + url: `${BASE_URL}/district-of-columbia`, + }, + { name: "Florida", slug: "florida", url: `${BASE_URL}/florida` }, + { name: "Georgia", slug: "georgia", url: `${BASE_URL}/georgia` }, + { name: "Hawaii", slug: "hawaii", url: `${BASE_URL}/hawaii` }, + { name: "Idaho", slug: "idaho", url: `${BASE_URL}/idaho` }, + { name: "Illinois", slug: "illinois", url: `${BASE_URL}/illinois` }, + { name: "Indiana", slug: "indiana", url: `${BASE_URL}/indiana` }, + { name: "Iowa", slug: "iowa", url: `${BASE_URL}/iowa` }, + { name: "Kansas", slug: "kansas", url: `${BASE_URL}/kansas` }, + { name: "Kentucky", slug: "kentucky", url: `${BASE_URL}/kentucky` }, + { name: "Louisiana", slug: "louisiana", url: `${BASE_URL}/louisiana` }, + { name: "Maine", slug: "maine", url: `${BASE_URL}/maine` }, + { name: "Maryland", slug: "maryland", url: `${BASE_URL}/maryland` }, + { name: "Massachusetts", slug: "massachusetts", url: `${BASE_URL}/massachusetts` }, + { name: "Michigan", slug: "michigan", url: `${BASE_URL}/michigan` }, + { name: "Minnesota", slug: "minnesota", url: `${BASE_URL}/minnesota` }, + { name: "Mississippi", slug: "mississippi", url: `${BASE_URL}/mississippi` }, + { name: "Missouri", slug: "missouri", url: `${BASE_URL}/missouri` }, + { name: "Montana", slug: "montana", url: `${BASE_URL}/montana` }, + { name: "Nebraska", slug: "nebraska", url: `${BASE_URL}/nebraska` }, + { name: "Nevada", slug: "nevada", url: `${BASE_URL}/nevada` }, + { name: "New Hampshire", slug: "new-hampshire", url: `${BASE_URL}/new-hampshire` }, + { name: "New Jersey", slug: "new-jersey", url: `${BASE_URL}/new-jersey` }, + { name: "New Mexico", slug: "new-mexico", url: `${BASE_URL}/new-mexico` }, + { name: "New York", slug: "new-york", url: `${BASE_URL}/new-york` }, + { name: "North Carolina", slug: "north-carolina", url: `${BASE_URL}/north-carolina` }, + { name: "North Dakota", slug: "north-dakota", url: `${BASE_URL}/north-dakota` }, + { name: "Ohio", slug: "ohio", url: `${BASE_URL}/ohio` }, + { name: "Oklahoma", slug: "oklahoma", url: `${BASE_URL}/oklahoma` }, + { name: "Oregon", slug: "oregon", url: `${BASE_URL}/oregon` }, + { name: "Pennsylvania", slug: "pennsylvania", url: `${BASE_URL}/pennsylvania` }, + { name: "Rhode Island", slug: "rhode-island", url: `${BASE_URL}/rhode-island` }, + { name: "South Carolina", slug: "south-carolina", url: `${BASE_URL}/south-carolina` }, + { name: "South Dakota", slug: "south-dakota", url: `${BASE_URL}/south-dakota` }, + { name: "Tennessee", slug: "tennessee", url: `${BASE_URL}/tennessee` }, + { name: "Texas", slug: "texas", url: `${BASE_URL}/texas` }, + { name: "Utah", slug: "utah", url: `${BASE_URL}/utah` }, + { name: "Vermont", slug: "vermont", url: `${BASE_URL}/vermont` }, + { name: "Virginia", slug: "virginia", url: `${BASE_URL}/virginia` }, + { name: "Washington", slug: "washington", url: `${BASE_URL}/washington` }, + { name: "West Virginia", slug: "west-virginia", url: `${BASE_URL}/west-virginia` }, + { name: "Wisconsin", slug: "wisconsin", url: `${BASE_URL}/wisconsin` }, + { name: "Wyoming", slug: "wyoming", url: `${BASE_URL}/wyoming` }, +]; + +/** + * Scrape state-level voting information from VOTE411 + */ +export async function scrapeStateInfo( + stateSlug: string, + useCache = true +): Promise { + const cacheKey = `state-info-${stateSlug}`; + + if (useCache) { + const cached = readCache(cacheKey); + if (cached) return cached; + } + + const state = US_STATES.find((s) => s.slug === stateSlug); + if (!state) { + logger.warn(`Unknown state slug: ${stateSlug}`); + return null; + } + + try { + logger.info(`Scraping ${state.name} voting info...`); + const res = await rateLimitedFetch(state.url); + const html = await res.text(); + const $ = cheerio.load(html); + + const info: StateVotingInfo = { + state: state.name, + stateSlug: state.slug, + url: state.url, + scrapedAt: new Date(), + elections: [], + deadlines: [], + voterGuides: [], + alerts: [], + }; + + // Extract alerts + $(".topic-alert, .state-alerts .alert--content").each((_, el) => { + const title = $(el).find(".alert--title").text().trim(); + const text = $(el).find(".alert--text").text().trim(); + if (title || text) { + info.alerts.push(`${title}: ${text}`.trim()); + } + }); + + // Extract upcoming elections from election cards + $(".upcoming-election, .election-card, .election-date").each((_, el) => { + const name = $(el).find(".election-name, h3, h4").first().text().trim(); + const date = $(el).find(".election-date, .date").first().text().trim(); + const description = $(el).find(".description, p").first().text().trim(); + + if (name && date) { + info.elections.push({ name, date, description: description || undefined }); + } + }); + + // Extract deadlines + $(".deadline, .registration-deadline, .voting-deadline").each((_, el) => { + const type = $(el).find(".deadline-type, .type, h4").first().text().trim(); + const date = $(el).find(".deadline-date, .date").first().text().trim(); + const description = $(el).find(".description, p").first().text().trim(); + + if (type && date) { + info.deadlines.push({ type, date, description: description || undefined }); + } + }); + + // Extract voter guide links + $(".list__item a, .voter-guide-link").each((_, el) => { + const $link = $(el); + const title = $link.text().trim(); + const href = $link.attr("href"); + + if (!title || !href) return; + + let type: "pdf" | "video" | "external" = "external"; + if (href.endsWith(".pdf")) { + type = "pdf"; + } else if ( + href.includes("youtube.com") || + href.includes("vimeo.com") || + href.includes("youtu.be") + ) { + type = "video"; + } + + info.voterGuides.push({ + title, + url: href.startsWith("http") ? href : `${BASE_URL}${href}`, + type, + state: state.name, + }); + }); + + writeCache(cacheKey, info); + logger.success(`Scraped ${state.name}: ${info.elections.length} elections, ${info.voterGuides.length} guides`); + + return info; + } catch (error) { + logger.error(`Failed to scrape ${state.name}:`, error); + return null; + } +} + +/** + * Scrape all states' voting information + */ +export async function scrapeAllStates( + useCache = true +): Promise { + const results: StateVotingInfo[] = []; + + for (const state of US_STATES) { + const info = await scrapeStateInfo(state.slug, useCache); + if (info) { + results.push(info); + } + + // Extra delay between states to be respectful + await new Promise((r) => setTimeout(r, 500)); + } + + return results; +} + +// ==================== Voter Guide List Scraping ==================== + +/** + * Scrape available voter guides and candidate forums from the main ballot page + */ +export async function scrapeVoterGuides( + useCache = true +): Promise<{ guides: VoterGuideLink[]; forums: CandidateForum[] }> { + const cacheKey = "voter-guides-list"; + + if (useCache) { + const cached = readCache<{ guides: VoterGuideLink[]; forums: CandidateForum[] }>(cacheKey); + if (cached) return cached; + } + + try { + logger.info("Scraping voter guides and forums..."); + const res = await rateLimitedFetch(`${BASE_URL}/ballot`); + const html = await res.text(); + const $ = cheerio.load(html); + + const guides: VoterGuideLink[] = []; + const forums: CandidateForum[] = []; + + // Extract voting guides from the list + $(".list:not(.list--green) .list__item a").each((_, el) => { + const $link = $(el); + const title = $link.find("span").text().trim() || $link.text().trim(); + const href = $link.attr("href"); + + if (!title || !href) return; + + let type: "pdf" | "video" | "external" = "external"; + if (href.endsWith(".pdf")) { + type = "pdf"; + } else if ( + href.includes("youtube.com") || + href.includes("vimeo.com") || + href.includes("youtu.be") + ) { + type = "video"; + } + + // Try to extract state from title (e.g., "NM: LWV Santa Fe County Voter Guide") + const stateMatch = title.match(/^([A-Z]{2}):/); + const state = stateMatch ? stateMatch[1] : undefined; + + guides.push({ + title, + url: href.startsWith("http") ? href : `${BASE_URL}${href}`, + type, + state, + }); + }); + + // Extract candidate debate videos (green list) + $(".list--green .list__item a").each((_, el) => { + const $link = $(el); + const title = $link.find("span").text().trim() || $link.text().trim(); + const href = $link.attr("href"); + + if (!title || !href) return; + + // Try to extract state from title + const stateMatch = title.match(/^([A-Z]{2})\s/); + const state = stateMatch ? stateMatch[1] : undefined; + + forums.push({ + title, + url: href.startsWith("http") ? href : `${BASE_URL}${href}`, + state, + }); + }); + + const result = { guides, forums }; + writeCache(cacheKey, result); + logger.success(`Found ${guides.length} guides and ${forums.length} forums`); + + return result; + } catch (error) { + logger.error("Failed to scrape voter guides:", error); + return { guides: [], forums: [] }; + } +} + +// ==================== Ballot Lookup (Requires Playwright) ==================== + +/** + * NOTE: The ballot lookup functionality on VOTE411 requires JavaScript execution + * because the ballot widget is client-side rendered using React. + * + * To implement full ballot lookup by address, you would need to: + * 1. Add playwright as a dependency to this package + * 2. Navigate to https://www.vote411.org/ballot + * 3. Wait for the ballot widget to load (look for .lwv-ballot-widget--root) + * 4. Enter the address in the search input + * 5. Wait for results and parse the rendered HTML + * + * The ballot widget uses an API at ballot.thevoterguide.org but it requires + * the proper authentication headers that are set by the widget JavaScript. + * + * Example Playwright implementation: + * + * ```typescript + * import { chromium } from 'playwright'; + * + * export async function lookupBallot(address: string): Promise { + * const browser = await chromium.launch({ headless: true }); + * const page = await browser.newPage(); + * + * await page.goto('https://www.vote411.org/ballot'); + * await page.waitForSelector('.lwv-ballot-widget--root', { timeout: 10000 }); + * + * // Find and fill the address input + * const addressInput = await page.$('input[placeholder*="address" i], input[type="text"]'); + * if (addressInput) { + * await addressInput.fill(address); + * await addressInput.press('Enter'); + * } + * + * // Wait for results to load + * await page.waitForSelector('.ballot-results, .race-card', { timeout: 30000 }); + * + * // Parse the results + * const content = await page.content(); + * const $ = cheerio.load(content); + * + * // ... parse races, candidates, measures ... + * + * await browser.close(); + * return ballotInfo; + * } + * ``` + */ + +// Placeholder for ballot lookup - implement with Playwright when needed +export async function lookupBallotByAddress( + _address: string +): Promise { + logger.warn( + "Ballot lookup by address requires Playwright. " + + "See the code comments for implementation guidance." + ); + return null; +} + +// ==================== Main Scraper ==================== + +async function scrape(): Promise { + logger.info("Starting VOTE411 scraper..."); + + // Scrape voter guides and forums from the ballot page + const { guides, forums } = await scrapeVoterGuides(); + logger.info(`Found ${guides.length} voter guides and ${forums.length} candidate forums`); + + // For now, we just scrape and cache the data + // The data can be used by the app to show available voter guides + + // Optionally scrape specific states (uncomment to enable) + // const stateInfo = await scrapeStateInfo('california'); + // if (stateInfo) { + // logger.info(`California: ${stateInfo.elections.length} elections`); + // } + + logger.success("VOTE411 scraper completed"); +} + +export const vote411: Scraper = { + name: NAME, + scrape, +}; + +// ==================== Standalone API ==================== + +/** + * Get a list of all US states with their VOTE411 URLs + */ +export function getStates(): StateInfo[] { + return US_STATES; +} + +/** + * Get cached state info if available + */ +export function getCachedStateInfo(stateSlug: string): StateVotingInfo | null { + return readCache(`state-info-${stateSlug}`); +} + +/** + * Get cached voter guides if available + */ +export function getCachedVoterGuides(): { guides: VoterGuideLink[]; forums: CandidateForum[] } | null { + return readCache<{ guides: VoterGuideLink[]; forums: CandidateForum[] }>("voter-guides-list"); +} diff --git a/docs/CIVIC_DATA_SOURCES.md b/docs/CIVIC_DATA_SOURCES.md new file mode 100644 index 0000000..a9a3d30 --- /dev/null +++ b/docs/CIVIC_DATA_SOURCES.md @@ -0,0 +1,240 @@ +# Civic Data Sources + +This document explains how to set up API keys and access for all civic data integrations in Billion. + +## Quick Reference + +| Source | Key Required | Cost | Env Variable | +|--------|--------------|------|--------------| +| Google Civic API | Yes | Free (25k/day) | `GOOGLE_CIVIC_API_KEY` | +| Open States API | Yes | Free | `OPEN_STATES_API_KEY` | +| CA Secretary of State | Yes | Free | `CA_SOS_API_KEY` | +| Legistar (local councils) | No | Free | — | +| VOTE411 (scraper) | No | Free | — | +| Santa Clara ROV (scraper) | No* | Free | — | + +*Uses Google Civic API internally + +--- + +## Google Civic Information API + +**Provides:** Elections, polling locations, ballot info, elected representatives + +**Quota:** 25,000 requests/day (free) + +### Setup + +1. Go to [Google Cloud Console](https://console.cloud.google.com/) +2. Create a new project or select existing +3. Navigate to **APIs & Services** → **Library** +4. Search for "Google Civic Information API" +5. Click **Enable** +6. Go to **APIs & Services** → **Credentials** +7. Click **Create Credentials** → **API Key** +8. Copy the key + +### Environment Variable + +```bash +GOOGLE_CIVIC_API_KEY=AIza...your_key_here +``` + +### Optional: Restrict Key + +For production, restrict the key: +- **Application restrictions:** HTTP referrers or IP addresses +- **API restrictions:** Google Civic Information API only + +--- + +## Open States API + +**Provides:** California state bills, legislators, voting records, legislative sessions + +**Quota:** Generous free tier + +### Setup + +1. Go to [Open States Sign Up](https://openstates.org/accounts/signup/) +2. Create an account and verify email +3. Go to [Your Profile](https://openstates.org/accounts/profile/) +4. Scroll to **API Keys** section +5. Click **Generate New Key** +6. Copy the key + +### Environment Variable + +```bash +OPEN_STATES_API_KEY=your_key_here +``` + +### Documentation + +- API Docs: https://docs.openstates.org/api-v3/ +- GraphQL Explorer: https://openstates.org/graphql + +--- + +## California Secretary of State API + +**Provides:** Election results, contest data, county-level vote breakdowns + +**Quota:** Free with registration + +### Setup + +1. Go to [CA SOS Developer Portal](https://calicodev.sos.ca.gov/) +2. Click **Sign Up** to create an account +3. After verification, go to **Products** → **Election Results API** +4. Click **Subscribe** to get access +5. Go to **Profile** → **Subscriptions** +6. Copy your **Primary Key** or **Secondary Key** + +### Environment Variable + +```bash +CA_SOS_API_KEY=your_subscription_key_here +``` + +### Documentation + +- Developer Portal: https://calicodev.sos.ca.gov/ +- API Base URL: https://api.sos.ca.gov/ + +### Notes + +- API uses Azure API Management +- Results are most active during election nights +- Historical data available for past elections + +--- + +## Legistar Web API + +**Provides:** Local city council meetings, legislation, votes, agendas + +**Quota:** Unlimited (public data) + +**No API key required.** + +### Supported Jurisdictions + +| Jurisdiction | Legistar Client ID | Website | +|--------------|-------------------|---------| +| San Jose | `sanjose` | sanjose.legistar.com | +| Santa Clara County | `santaclara` | sccgov.legistar.com | +| Sunnyvale | `sunnyvale` | sunnyvaleca.legistar.com | + +### Usage + +```typescript +import { legistar } from "@acme/api"; + +// Get San Jose city council meetings +const meetings = await legistar.getMeetings("sanjose"); + +// Search legislation +const bills = await legistar.getLegislation("sanjose", { text: "housing" }); +``` + +### Adding More Jurisdictions + +Many California cities use Legistar. To add a new one: + +1. Find the city's Legistar URL (e.g., `mountainview.legistar.com`) +2. Extract the client ID (the subdomain before `.legistar.com`) +3. Add to the `JURISDICTIONS` constant in `packages/api/src/integrations/legistar.ts` + +### Documentation + +- API Base: https://webapi.legistar.com/ +- OData-compatible queries supported + +--- + +## VOTE411 / League of Women Voters + +**Provides:** Nonpartisan voter guides, candidate questionnaire responses, ballot measure explanations + +**Quota:** Rate-limited (be respectful) + +**No API key required** (web scraper). + +### Usage + +The scraper extracts data from vote411.org. Rate limiting and caching are built in. + +### Notes + +- Data updates closer to elections +- Candidate responses depend on candidate participation +- Also check https://cavotes.org/easy-voter-guide/ for California-specific guides + +--- + +## Santa Clara County ROV Scraper + +**Provides:** Sample ballots, polling locations, election calendar, candidate filings + +**No direct API key required** — uses Google Civic API internally. + +### Setup + +Ensure `GOOGLE_CIVIC_API_KEY` is set (see Google Civic section above). + +### Why Not Direct Scraping? + +The Santa Clara County Registrar of Voters website (vote.santaclaracounty.gov) uses Cloudflare bot protection, making direct scraping unreliable. The scraper uses Google Civic API as a proxy data source. + +### Running + +```bash +# Run Santa Clara ROV scraper +pnpm scraper santaclararov + +# Run all scrapers +pnpm scraper all +``` + +--- + +## Environment Setup + +Add all keys to your `.env` file: + +```bash +# Required for most civic features +GOOGLE_CIVIC_API_KEY=AIza... + +# Required for CA state legislation +OPEN_STATES_API_KEY=... + +# Required for election results +CA_SOS_API_KEY=... +``` + +For local development, copy `.env.example` to `.env` and fill in your keys. + +--- + +## Paid APIs (Future Expansion) + +These provide more comprehensive data but require paid subscriptions: + +### BallotReady +- Full ballot data + endorsements +- Contact: https://organizations.ballotready.org/ballotready-api + +### Ballotpedia +- Candidate bios, detailed election coverage +- Contact: https://developer.ballotpedia.org/ + +### Democracy Works +- Comprehensive election data +- Contact: https://www.democracy.works/elections-api + +### Vote Smart +- Free for research/nonprofit use +- Voting records, interest group ratings +- Register: https://votesmart.org/share/api diff --git a/package.json b/package.json index ba40e51..f04f4b6 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "auth:generate": "pnpm -F @acme/auth generate", "db:push": "turbo -F @acme/db push", "db:studio": "turbo -F @acme/db studio", - "dev": "turbo watch dev --continue", + "dev": "turbo watch dev --continue --filter='!@acme/scraper'", + "dev:all": "turbo watch dev --continue", "dev:next": "turbo watch dev -F @acme/nextjs...", "format": "turbo run format --continue -- --cache --cache-location .cache/.prettiercache", "format:fix": "turbo run format --continue -- --write --cache --cache-location .cache/.prettiercache", diff --git a/packages/api/package.json b/packages/api/package.json index 59cb373..81db044 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -6,6 +6,22 @@ ".": { "types": "./dist/index.d.ts", "default": "./src/index.ts" + }, + "./integrations/legistar": { + "types": "./dist/integrations/legistar.d.ts", + "default": "./src/integrations/legistar.ts" + }, + "./lib/civic": { + "types": "./dist/lib/civic.d.ts", + "default": "./src/lib/civic.ts" + }, + "./clients/open-states": { + "types": "./dist/clients/open-states.d.ts", + "default": "./src/clients/open-states.ts" + }, + "./clients/ca-sos": { + "types": "./dist/clients/ca-sos.d.ts", + "default": "./src/clients/ca-sos.ts" } }, "license": "MIT", diff --git a/packages/api/src/clients/ca-sos.ts b/packages/api/src/clients/ca-sos.ts new file mode 100644 index 0000000..d402085 --- /dev/null +++ b/packages/api/src/clients/ca-sos.ts @@ -0,0 +1,543 @@ +/** + * California Secretary of State Election Results API Client + * + * This client interfaces with the CA SOS API for election data. + * API Documentation: https://calicodev.sos.ca.gov/ + * + * Note: The production API (api.sos.ca.gov) requires subscription. + * For development, this uses publicly available election data endpoints. + */ + +// ============================================================================= +// Types +// ============================================================================= + +/** California county codes and names */ +export const CA_COUNTIES = { + ALA: "Alameda", + ALP: "Alpine", + AMA: "Amador", + BUT: "Butte", + CAL: "Calaveras", + CC: "Contra Costa", + COL: "Colusa", + DN: "Del Norte", + ED: "El Dorado", + FRE: "Fresno", + GLE: "Glenn", + HUM: "Humboldt", + IMP: "Imperial", + INY: "Inyo", + KER: "Kern", + KIN: "Kings", + LAK: "Lake", + LAS: "Lassen", + LA: "Los Angeles", + MAD: "Madera", + MRN: "Marin", + MPA: "Mariposa", + MEN: "Mendocino", + MER: "Merced", + MOD: "Modoc", + MNO: "Mono", + MON: "Monterey", + NAP: "Napa", + NEV: "Nevada", + ORA: "Orange", + PLA: "Placer", + PLU: "Plumas", + RIV: "Riverside", + SAC: "Sacramento", + SBT: "San Benito", + SBD: "San Bernardino", + SD: "San Diego", + SF: "San Francisco", + SJ: "San Joaquin", + SLO: "San Luis Obispo", + SM: "San Mateo", + SB: "Santa Barbara", + SCL: "Santa Clara", + SCR: "Santa Cruz", + SHA: "Shasta", + SIE: "Sierra", + SIS: "Siskiyou", + SOL: "Solano", + SON: "Sonoma", + STA: "Stanislaus", + SUT: "Sutter", + TEH: "Tehama", + TRI: "Trinity", + TUL: "Tulare", + TUO: "Tuolumne", + VEN: "Ventura", + YOL: "Yolo", + YUB: "Yuba", +} as const; + +export type CountyCode = keyof typeof CA_COUNTIES; + +/** Election types */ +export type ElectionType = + | "primary" + | "general" + | "special" + | "recall" + | "runoff"; + +/** Contest types */ +export type ContestType = + | "president" + | "us_senate" + | "us_house" + | "governor" + | "state_senate" + | "state_assembly" + | "proposition" + | "local" + | "judicial" + | "other"; + +/** Election metadata */ +export interface Election { + id: string; + name: string; + date: string; // ISO date string + type: ElectionType; + isActive: boolean; + isCertified: boolean; + lastUpdated: string; // ISO datetime string +} + +/** Candidate in a contest */ +export interface Candidate { + id: string; + name: string; + party?: string; + isIncumbent: boolean; + ballotDesignation?: string; +} + +/** Contest (race) in an election */ +export interface Contest { + id: string; + electionId: string; + name: string; + type: ContestType; + district?: string; + districtNumber?: number; + candidates: Candidate[]; + isProposition: boolean; + propositionNumber?: string; +} + +/** Vote totals for a candidate/choice */ +export interface VoteTotal { + candidateId: string; + candidateName: string; + party?: string; + votes: number; + percentage: number; +} + +/** Contest results */ +export interface ContestResult { + contestId: string; + contestName: string; + contestType: ContestType; + totalVotes: number; + precinctsReporting: number; + precinctsTotal: number; + percentReporting: number; + results: VoteTotal[]; + lastUpdated: string; +} + +/** County-level vote breakdown */ +export interface CountyResult { + countyCode: CountyCode; + countyName: string; + totalVotes: number; + precinctsReporting: number; + precinctsTotal: number; + percentReporting: number; + results: VoteTotal[]; +} + +/** Full contest results with county breakdown */ +export interface ContestResultWithCounties extends ContestResult { + countyResults: CountyResult[]; +} + +/** Reporting status for an election */ +export interface ElectionStatus { + electionId: string; + electionName: string; + lastUpdated: string; + totalPrecinctsReporting: number; + totalPrecincts: number; + percentReporting: number; + countiesReporting: number; + totalCounties: number; +} + +// ============================================================================= +// Client Configuration +// ============================================================================= + +export interface CASOSClientConfig { + /** API base URL - defaults to production */ + baseUrl?: string; + /** API key if using authenticated endpoints */ + apiKey?: string; + /** Request timeout in milliseconds */ + timeout?: number; +} + +const DEFAULT_CONFIG: Required = { + baseUrl: "https://api.sos.ca.gov", + apiKey: "", + timeout: 30000, +}; + +// ============================================================================= +// Client Implementation +// ============================================================================= + +export class CASOSClient { + private config: Required; + + constructor(config: CASOSClientConfig = {}) { + this.config = { ...DEFAULT_CONFIG, ...config }; + } + + /** + * Make an API request + */ + private async request( + endpoint: string, + options: RequestInit = {}, + ): Promise { + const url = `${this.config.baseUrl}${endpoint}`; + + const headers: Record = { + Accept: "application/json", + "Content-Type": "application/json", + }; + + if (this.config.apiKey) { + headers["Ocp-Apim-Subscription-Key"] = this.config.apiKey; + } + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.config.timeout); + + try { + const response = await fetch(url, { + ...options, + headers, + signal: controller.signal, + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => "Unknown error"); + throw new CASOSError( + `API request failed: ${response.status} ${response.statusText}`, + response.status, + errorText, + ); + } + + return response.json() as Promise; + } finally { + clearTimeout(timeoutId); + } + } + + // =========================================================================== + // Elections + // =========================================================================== + + /** + * Get list of all available elections + */ + async getElections(): Promise { + return this.request("/returns/elections"); + } + + /** + * Get current/active election + */ + async getCurrentElection(): Promise { + const elections = await this.getElections(); + return elections.find((e) => e.isActive) ?? null; + } + + /** + * Get election by ID + */ + async getElection(electionId: string): Promise { + return this.request(`/returns/elections/${electionId}`); + } + + /** + * Get election status/reporting progress + */ + async getElectionStatus(electionId: string): Promise { + return this.request(`/returns/status/${electionId}`); + } + + // =========================================================================== + // Contests + // =========================================================================== + + /** + * Get all contests for an election + */ + async getContests(electionId: string): Promise { + return this.request(`/returns/contests/${electionId}`); + } + + /** + * Get contests filtered by type + */ + async getContestsByType( + electionId: string, + type: ContestType, + ): Promise { + const contests = await this.getContests(electionId); + return contests.filter((c) => c.type === type); + } + + /** + * Get a specific contest + */ + async getContest(electionId: string, contestId: string): Promise { + return this.request( + `/returns/contests/${electionId}/${contestId}`, + ); + } + + // =========================================================================== + // Results + // =========================================================================== + + /** + * Get results for all contests in an election + */ + async getAllResults(electionId: string): Promise { + return this.request(`/returns/results/${electionId}`); + } + + /** + * Get results for a specific contest + */ + async getContestResults( + electionId: string, + contestId: string, + ): Promise { + return this.request( + `/returns/results/${electionId}/${contestId}`, + ); + } + + /** + * Get results with county-level breakdown + */ + async getContestResultsWithCounties( + electionId: string, + contestId: string, + ): Promise { + const [results, countyResults] = await Promise.all([ + this.getContestResults(electionId, contestId), + this.getCountyResults(electionId, contestId), + ]); + + return { + ...results, + countyResults, + }; + } + + /** + * Get county-level results for a contest + */ + async getCountyResults( + electionId: string, + contestId: string, + ): Promise { + return this.request( + `/returns/results/${electionId}/${contestId}/counties`, + ); + } + + /** + * Get results for a specific county in a contest + */ + async getCountyResult( + electionId: string, + contestId: string, + countyCode: CountyCode, + ): Promise { + return this.request( + `/returns/results/${electionId}/${contestId}/counties/${countyCode}`, + ); + } + + // =========================================================================== + // Propositions + // =========================================================================== + + /** + * Get all propositions for an election + */ + async getPropositions(electionId: string): Promise { + const contests = await this.getContests(electionId); + return contests.filter((c) => c.isProposition); + } + + /** + * Get proposition results + */ + async getPropositionResults( + electionId: string, + propositionNumber: string, + ): Promise { + const props = await this.getPropositions(electionId); + const prop = props.find((p) => p.propositionNumber === propositionNumber); + if (!prop) return null; + return this.getContestResults(electionId, prop.id); + } + + // =========================================================================== + // Statewide Races + // =========================================================================== + + /** + * Get presidential race results + */ + async getPresidentialResults( + electionId: string, + ): Promise { + const contests = await this.getContestsByType(electionId, "president"); + const firstContest = contests[0]; + if (!firstContest) return null; + return this.getContestResults(electionId, firstContest.id); + } + + /** + * Get gubernatorial race results + */ + async getGovernorResults(electionId: string): Promise { + const contests = await this.getContestsByType(electionId, "governor"); + const firstContest = contests[0]; + if (!firstContest) return null; + return this.getContestResults(electionId, firstContest.id); + } + + /** + * Get US Senate race results + */ + async getUSSenateResults(electionId: string): Promise { + const contests = await this.getContestsByType(electionId, "us_senate"); + return Promise.all( + contests.map((c) => this.getContestResults(electionId, c.id)), + ); + } + + /** + * Get US House race results for a specific district + */ + async getUSHouseResults( + electionId: string, + district?: number, + ): Promise { + const contests = await this.getContestsByType(electionId, "us_house"); + const filtered = district + ? contests.filter((c) => c.districtNumber === district) + : contests; + return Promise.all( + filtered.map((c) => this.getContestResults(electionId, c.id)), + ); + } + + // =========================================================================== + // Utilities + // =========================================================================== + + /** + * Get the winning candidate for a contest + */ + getWinner(result: ContestResult): VoteTotal | null { + if (result.results.length === 0) return null; + return result.results.reduce((max, curr) => + curr.votes > max.votes ? curr : max, + ); + } + + /** + * Check if a contest has been called (>50% reporting with clear lead) + */ + isContestCalled(result: ContestResult, marginThreshold = 5): boolean { + if (result.percentReporting < 50) return false; + if (result.results.length < 2) return result.results.length === 1; + + const sorted = [...result.results].sort((a, b) => b.votes - a.votes); + const first = sorted[0]; + const second = sorted[1]; + if (!first || !second) return true; + const margin = first.percentage - second.percentage; + return margin > marginThreshold; + } + + /** + * Get county name from code + */ + getCountyName(code: CountyCode): string { + return CA_COUNTIES[code]; + } + + /** + * Get all county codes + */ + getCountyCodes(): CountyCode[] { + return Object.keys(CA_COUNTIES) as CountyCode[]; + } +} + +// ============================================================================= +// Error Class +// ============================================================================= + +export class CASOSError extends Error { + constructor( + message: string, + public statusCode: number, + public responseBody?: string, + ) { + super(message); + this.name = "CASOSError"; + } +} + +// ============================================================================= +// Singleton Instance +// ============================================================================= + +let defaultClient: CASOSClient | null = null; + +/** + * Get the default CA SOS client instance + */ +export function getCASOSClient(config?: CASOSClientConfig): CASOSClient { + if (!defaultClient || config) { + defaultClient = new CASOSClient(config); + } + return defaultClient; +} + +/** + * Create a new CA SOS client instance + */ +export function createCASOSClient(config?: CASOSClientConfig): CASOSClient { + return new CASOSClient(config); +} diff --git a/packages/api/src/clients/open-states.ts b/packages/api/src/clients/open-states.ts new file mode 100644 index 0000000..ff47bcb --- /dev/null +++ b/packages/api/src/clients/open-states.ts @@ -0,0 +1,406 @@ +/** + * Open States API v3 Client + * https://v3.openstates.org/ + * + * Provides access to California state legislation data + */ + +const BASE_URL = "https://v3.openstates.org"; +const DEFAULT_JURISDICTION = "ocd-jurisdiction/country:us/state:ca/government"; + +// ============================================================================ +// Types +// ============================================================================ + +export interface OpenStatesJurisdiction { + id: string; + name: string; + classification: string; +} + +export interface OpenStatesPerson { + id: string; + name: string; + party: string; + current_role?: { + title: string; + org_classification: string; + district: string; + division_id: string; + }; + given_name?: string; + family_name?: string; + image?: string; + email?: string; + links?: { url: string; note?: string }[]; + offices?: { + name: string; + address?: string; + voice?: string; + fax?: string; + }[]; +} + +export interface OpenStatesOrganization { + id: string; + name: string; + classification: string; +} + +export interface OpenStatesBillAbstract { + abstract: string; + note: string; +} + +export interface OpenStatesBillAction { + date: string; + description: string; + classification: string[]; + organization?: OpenStatesOrganization; +} + +export interface OpenStatesBillSponsorship { + name: string; + entity_type: string; + classification: string; + primary: boolean; + person?: OpenStatesPerson; +} + +export interface OpenStatesBillVersion { + note: string; + date: string; + links: { + url: string; + media_type?: string; + }[]; +} + +export interface OpenStatesBillDocument { + note: string; + date?: string; + links: { + url: string; + media_type?: string; + }[]; +} + +export interface OpenStatesVote { + id: string; + identifier: string; + motion_text: string; + start_date: string; + result: string; + organization?: OpenStatesOrganization; + counts: { + option: string; + value: number; + }[]; + votes?: { + option: string; + voter_name: string; + voter?: OpenStatesPerson; + }[]; +} + +export interface OpenStatesBill { + id: string; + identifier: string; + title: string; + session: string; + classification: string[]; + subject?: string[]; + from_organization?: OpenStatesOrganization; + jurisdiction: OpenStatesJurisdiction; + abstracts?: OpenStatesBillAbstract[]; + actions?: OpenStatesBillAction[]; + sponsorships?: OpenStatesBillSponsorship[]; + versions?: OpenStatesBillVersion[]; + documents?: OpenStatesBillDocument[]; + votes?: OpenStatesVote[]; + created_at: string; + updated_at: string; + openstates_url: string; +} + +export interface OpenStatesBillSearchResult { + results: OpenStatesBill[]; + pagination: { + page: number; + max_page: number; + per_page: number; + total_items: number; + }; +} + +export interface OpenStatesPersonSearchResult { + results: OpenStatesPerson[]; + pagination: { + page: number; + max_page: number; + per_page: number; + total_items: number; + }; +} + +// ============================================================================ +// Client +// ============================================================================ + +function getApiKey(): string { + const key = process.env.OPEN_STATES_API_KEY; + if (!key) { + throw new Error( + "OPEN_STATES_API_KEY is not set. Get one at https://openstates.org/accounts/profile/", + ); + } + return key; +} + +async function openStatesFetch( + path: string, + params: Record = {}, +): Promise { + const apiKey = getApiKey(); + const url = new URL(`${BASE_URL}${path}`); + + for (const [key, value] of Object.entries(params)) { + if (value !== undefined) { + url.searchParams.set(key, String(value)); + } + } + + const res = await fetch(url.toString(), { + headers: { + "X-API-KEY": apiKey, + Accept: "application/json", + }, + }); + + if (!res.ok) { + const errorText = await res.text(); + throw new Error(`Open States API error (${res.status}): ${errorText}`); + } + + return res.json() as Promise; +} + +// ============================================================================ +// Public API Methods +// ============================================================================ + +export interface GetBillsOptions { + query?: string; + session?: string; + page?: number; + perPage?: number; + classification?: string; + subject?: string; + updatedSince?: string; + createdSince?: string; + sort?: "updated_desc" | "updated_asc" | "created_desc" | "created_asc"; + includeVersions?: boolean; + includeSponsorships?: boolean; + includeAbstracts?: boolean; + includeActions?: boolean; +} + +/** + * Search California state bills + */ +export async function getBills( + options: GetBillsOptions = {}, +): Promise { + const { + query, + session, + page = 1, + perPage = 20, + classification, + subject, + updatedSince, + createdSince, + sort = "updated_desc", + includeVersions = false, + includeSponsorships = true, + includeAbstracts = true, + includeActions = false, + } = options; + + const include: string[] = []; + if (includeVersions) include.push("versions"); + if (includeSponsorships) include.push("sponsorships"); + if (includeAbstracts) include.push("abstracts"); + if (includeActions) include.push("actions"); + + return openStatesFetch("/bills", { + jurisdiction: DEFAULT_JURISDICTION, + q: query, + session, + page, + per_page: perPage, + classification, + subject, + updated_since: updatedSince, + created_since: createdSince, + sort, + include: include.length > 0 ? include.join(",") : undefined, + }); +} + +export interface GetBillDetailsOptions { + includeVersions?: boolean; + includeSponsorships?: boolean; + includeAbstracts?: boolean; + includeActions?: boolean; + includeDocuments?: boolean; + includeVotes?: boolean; +} + +/** + * Get detailed information for a specific bill + * @param billId - The Open States bill ID (e.g., "ocd-bill/abc123...") + */ +export async function getBillDetails( + billId: string, + options: GetBillDetailsOptions = {}, +): Promise { + const { + includeVersions = true, + includeSponsorships = true, + includeAbstracts = true, + includeActions = true, + includeDocuments = true, + includeVotes = true, + } = options; + + const include: string[] = []; + if (includeVersions) include.push("versions"); + if (includeSponsorships) include.push("sponsorships"); + if (includeAbstracts) include.push("abstracts"); + if (includeActions) include.push("actions"); + if (includeDocuments) include.push("documents"); + if (includeVotes) include.push("votes"); + + // The bill ID needs to be URL-encoded + const encodedId = encodeURIComponent(billId); + + return openStatesFetch(`/bills/${encodedId}`, { + include: include.length > 0 ? include.join(",") : undefined, + }); +} + +export interface GetLegislatorsOptions { + district?: string; + name?: string; + party?: string; + orgClassification?: "upper" | "lower"; + page?: number; + perPage?: number; +} + +/** + * Get California state legislators + * @param options.district - Filter by district (e.g., "1", "12") + * @param options.orgClassification - "upper" for Senate, "lower" for Assembly + */ +export async function getLegislators( + options: GetLegislatorsOptions = {}, +): Promise { + const { + district, + name, + party, + orgClassification, + page = 1, + perPage = 50, + } = options; + + return openStatesFetch("/people", { + jurisdiction: DEFAULT_JURISDICTION, + district, + name, + party, + org_classification: orgClassification, + page, + per_page: perPage, + }); +} + +/** + * Get votes for a specific bill + * @param billId - The Open States bill ID + */ +export async function getVotes(billId: string): Promise { + // Fetch the bill with votes included + const bill = await getBillDetails(billId, { + includeVersions: false, + includeSponsorships: false, + includeAbstracts: false, + includeActions: false, + includeDocuments: false, + includeVotes: true, + }); + + return bill.votes ?? []; +} + +// ============================================================================ +// Convenience / Helper Methods +// ============================================================================ + +/** + * Get all current sessions for California + */ +export async function getCurrentSessions(): Promise { + // The jurisdiction endpoint provides session info + const jurisdiction = await openStatesFetch<{ + id: string; + name: string; + legislative_sessions: { + identifier: string; + name: string; + classification: string; + start_date?: string; + end_date?: string; + }[]; + }>(`/jurisdictions/${encodeURIComponent(DEFAULT_JURISDICTION)}`); + + return jurisdiction.legislative_sessions + .filter((s) => s.classification === "primary" || !s.end_date) + .map((s) => s.identifier); +} + +/** + * Get a legislator by their Open States ID + */ +export async function getLegislatorById( + personId: string, +): Promise { + const encodedId = encodeURIComponent(personId); + return openStatesFetch(`/people/${encodedId}`); +} + +/** + * Search bills by a specific legislator (sponsor) + */ +export async function getBillsBySponsor( + sponsorName: string, + options: Omit = {}, +): Promise { + return getBills({ + ...options, + query: `sponsor:"${sponsorName}"`, + }); +} + +// Export the client as a namespace for convenience +export const openStatesClient = { + getBills, + getBillDetails, + getLegislators, + getVotes, + getCurrentSessions, + getLegislatorById, + getBillsBySponsor, +}; diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index ca41492..d2cc160 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -7,6 +7,97 @@ import { createTRPCContext } from "./trpc"; export type { VideoPost } from "./router/video"; export { getThumbnailForContent } from "./router/content"; +// Google Civic API types +export type { + Election, + VoterInfoResponse, + RepresentativesResponse, + Representative, + Official, + Office, + PollingLocation, + Contest, + Candidate, +} from "./lib/civic"; + +// Google Civic API client functions (for direct use outside tRPC) +export { + getElections, + getVoterInfo, + getRepresentatives, + getRepresentativesEnriched, +} from "./lib/civic"; + +// Open States API types +export type { + OpenStatesBill, + OpenStatesBillSearchResult, + OpenStatesPerson, + OpenStatesPersonSearchResult, + OpenStatesVote, + OpenStatesBillAction, + OpenStatesBillSponsorship, + OpenStatesBillVersion, + OpenStatesBillDocument, + GetBillsOptions, + GetBillDetailsOptions, + GetLegislatorsOptions, +} from "./clients/open-states"; + +// Open States API client functions (for California state legislation) +export { + getBills, + getBillDetails, + getLegislators, + getVotes, + getCurrentSessions, + getLegislatorById, + getBillsBySponsor, + openStatesClient, +} from "./clients/open-states"; + +// Legistar API for local government legislation (Santa Clara County area) +export { + legistar, + LegistarClient, + LegistarError, + JURISDICTIONS, +} from "./integrations/legistar"; +export type { + Jurisdiction, + LegistarMeeting, + LegistarMatter, + LegistarVote, + LegistarAgendaItem, + LegistarAttachment, + LegistarBody, + DateRange, + LegislationQuery, +} from "./integrations/legistar"; + +// California Secretary of State Election Results API +export { + CASOSClient, + CASOSError, + CA_COUNTIES, + getCASOSClient, + createCASOSClient, +} from "./clients/ca-sos"; +export type { + Election as CAElection, + Contest as CAContest, + Candidate as CACandidate, + ContestResult, + ContestResultWithCounties, + CountyResult, + VoteTotal, + ElectionStatus, + ElectionType, + ContestType as CAContestType, + CountyCode, + CASOSClientConfig, +} from "./clients/ca-sos"; + /** * Inference helpers for input types * @example diff --git a/packages/api/src/integrations/index.ts b/packages/api/src/integrations/index.ts new file mode 100644 index 0000000..280d893 --- /dev/null +++ b/packages/api/src/integrations/index.ts @@ -0,0 +1 @@ +export * from "./legistar"; diff --git a/packages/api/src/integrations/legistar.ts b/packages/api/src/integrations/legistar.ts new file mode 100644 index 0000000..c9ab0d1 --- /dev/null +++ b/packages/api/src/integrations/legistar.ts @@ -0,0 +1,635 @@ +/** + * Legistar Web API Client + * + * Integrates with the Legistar API for local government legislation data. + * API docs: https://webapi.legistar.com/Help + * + * Supported jurisdictions: + * - San Jose: sanjose.legistar.com + * - Santa Clara County: sccgov.legistar.com + * - Sunnyvale: sunnyvaleca.legistar.com + */ + +// Jurisdiction configurations +// Note: Client names in Legistar API may differ from subdomain names +export const JURISDICTIONS = { + sanjose: { + client: "sanjose", + name: "City of San Jose", + baseUrl: "https://webapi.legistar.com/v1/sanjose", + }, + santaclara: { + client: "santaclara", + name: "Santa Clara County", + baseUrl: "https://webapi.legistar.com/v1/santaclara", + }, + sunnyvale: { + client: "sunnyvaleca", + name: "City of Sunnyvale", + baseUrl: "https://webapi.legistar.com/v1/sunnyvaleca", + }, +} as const; + +export type Jurisdiction = keyof typeof JURISDICTIONS; + +// ============================================================================ +// Legistar API Types +// ============================================================================ + +export interface LegistarMeeting { + EventId: number; + EventGuid: string; + EventLastModifiedUtc: string; + EventRowVersion: string; + EventBodyId: number; + EventBodyName: string; + EventDate: string; + EventTime: string | null; + EventVideoStatus: string | null; + EventAgendaStatusId: number; + EventAgendaStatusName: string; + EventMinutesStatusId: number; + EventMinutesStatusName: string; + EventLocation: string | null; + EventAgendaFile: string | null; + EventMinutesFile: string | null; + EventAgendaLastPublishedUTC: string | null; + EventMinutesLastPublishedUTC: string | null; + EventComment: string | null; + EventVideoPath: string | null; + EventInSiteURL: string | null; + EventItems: LegistarAgendaItem[] | null; +} + +export interface LegistarMatter { + MatterId: number; + MatterGuid: string; + MatterLastModifiedUtc: string; + MatterRowVersion: string; + MatterFile: string; + MatterName: string | null; + MatterTitle: string; + MatterTypeId: number; + MatterTypeName: string; + MatterStatusId: number; + MatterStatusName: string; + MatterBodyId: number; + MatterBodyName: string; + MatterIntroDate: string | null; + MatterAgendaDate: string | null; + MatterPassedDate: string | null; + MatterEnactmentDate: string | null; + MatterEnactmentNumber: string | null; + MatterRequester: string | null; + MatterNotes: string | null; + MatterVersion: string; + MatterText1: string | null; + MatterText2: string | null; + MatterText3: string | null; + MatterText4: string | null; + MatterText5: string | null; + MatterRestrictViewViaWeb: boolean; +} + +export interface LegistarVote { + VoteId: number; + VoteGuid: string; + VoteLastModifiedUtc: string; + VoteRowVersion: string; + VotePersonId: number; + VotePersonName: string; + VoteValueId: number; + VoteValueName: string; + VoteSort: number; + VoteResult: number | null; + VoteEventItemId: number; +} + +export interface LegistarAgendaItem { + EventItemId: number; + EventItemGuid: string; + EventItemLastModifiedUtc: string; + EventItemRowVersion: string; + EventItemEventId: number; + EventItemAgendaSequence: number; + EventItemMinutesSequence: number | null; + EventItemAgendaNumber: string | null; + EventItemVideo: number | null; + EventItemVideoIndex: number | null; + EventItemVersion: string; + EventItemAgendaNote: string | null; + EventItemMinutesNote: string | null; + EventItemActionId: number | null; + EventItemActionName: string | null; + EventItemActionText: string | null; + EventItemPassedFlag: number | null; + EventItemPassedFlagName: string | null; + EventItemRollCallFlag: number | null; + EventItemFlagExtra: number | null; + EventItemTitle: string | null; + EventItemTally: string | null; + EventItemAccelaRecordId: string | null; + EventItemConsent: number; + EventItemMoverId: number | null; + EventItemMover: string | null; + EventItemSeconderId: number | null; + EventItemSeconder: string | null; + EventItemMatterId: number | null; + EventItemMatterGuid: string | null; + EventItemMatterFile: string | null; + EventItemMatterName: string | null; + EventItemMatterType: string | null; + EventItemMatterStatus: string | null; + EventItemMatterAttachments: LegistarAttachment[] | null; +} + +export interface LegistarAttachment { + MatterAttachmentId: number; + MatterAttachmentGuid: string; + MatterAttachmentLastModifiedUtc: string; + MatterAttachmentRowVersion: string; + MatterAttachmentName: string; + MatterAttachmentHyperlink: string; + MatterAttachmentFileName: string | null; + MatterAttachmentMatterVersion: string; + MatterAttachmentIsHyperlink: boolean; + MatterAttachmentBinary: string | null; + MatterAttachmentIsSupportingDocument: boolean; + MatterAttachmentShowOnInternetPage: boolean; + MatterAttachmentIsMinuteOrder: boolean; + MatterAttachmentIsBoardLetter: boolean; + MatterAttachmentAgiloftId: number; + MatterAttachmentDescription: string | null; + MatterAttachmentPrintWithReports: boolean; + MatterAttachmentSort: number; +} + +export interface LegistarBody { + BodyId: number; + BodyGuid: string; + BodyLastModifiedUtc: string; + BodyRowVersion: string; + BodyName: string; + BodyTypeId: number; + BodyTypeName: string; + BodyMeetFlag: number; + BodyActiveFlag: number; + BodySort: number; + BodyDescription: string | null; + BodyContactNameId: number | null; + BodyContactFullName: string | null; + BodyContactPhone: string | null; + BodyContactEmail: string | null; + BodyUsedControlFlag: number; + BodyNumberOfMembers: number; + BodyUsedActingFlag: number; + BodyUsedTargetFlag: number; + BodyUsedSponsorFlag: number; +} + +export interface DateRange { + start: Date; + end: Date; +} + +export interface LegislationQuery { + text?: string; + matterType?: string; + status?: string; + bodyId?: number; + introDateFrom?: Date; + introDateTo?: Date; +} + +// ============================================================================ +// Legistar Client +// ============================================================================ + +class LegistarClient { + private async fetch( + jurisdiction: Jurisdiction, + endpoint: string, + params?: Record, + ): Promise { + const config = JURISDICTIONS[jurisdiction]; + const url = new URL(`${config.baseUrl}${endpoint}`); + + if (params) { + Object.entries(params).forEach(([key, value]) => { + url.searchParams.set(key, value); + }); + } + + const response = await fetch(url.toString(), { + headers: { + Accept: "application/json", + }, + }); + + if (!response.ok) { + throw new LegistarError( + `Legistar API error: ${response.status} ${response.statusText}`, + response.status, + jurisdiction, + endpoint, + ); + } + + return response.json() as Promise; + } + + /** + * Get meetings for a jurisdiction within a date range. + */ + async getMeetings( + jurisdiction: Jurisdiction, + dateRange?: DateRange, + ): Promise { + const params: Record = {}; + + if (dateRange) { + // OData filter for date range + const startStr = dateRange.start.toISOString().split("T")[0]; + const endStr = dateRange.end.toISOString().split("T")[0]; + params.$filter = `EventDate ge datetime'${startStr}' and EventDate le datetime'${endStr}'`; + } + + params.$orderby = "EventDate desc"; + + return this.fetch(jurisdiction, "/Events", params); + } + + /** + * Get legislation (matters) for a jurisdiction with optional query filters. + */ + async getLegislation( + jurisdiction: Jurisdiction, + query?: LegislationQuery, + ): Promise { + const params: Record = {}; + const filters: string[] = []; + + if (query) { + if (query.text) { + // Search in title using substringof (OData 2.0 compatible) + filters.push( + `(substringof('${query.text}',MatterTitle) or substringof('${query.text}',MatterFile))`, + ); + } + if (query.matterType) { + filters.push(`MatterTypeName eq '${query.matterType}'`); + } + if (query.status) { + filters.push(`MatterStatusName eq '${query.status}'`); + } + if (query.bodyId) { + filters.push(`MatterBodyId eq ${query.bodyId}`); + } + if (query.introDateFrom) { + const dateStr = query.introDateFrom.toISOString().split("T")[0]; + filters.push(`MatterIntroDate ge datetime'${dateStr}'`); + } + if (query.introDateTo) { + const dateStr = query.introDateTo.toISOString().split("T")[0]; + filters.push(`MatterIntroDate le datetime'${dateStr}'`); + } + } + + if (filters.length > 0) { + params.$filter = filters.join(" and "); + } + + params.$orderby = "MatterIntroDate desc"; + params.$top = "100"; + + return this.fetch(jurisdiction, "/Matters", params); + } + + /** + * Get votes for a specific event item (agenda item with voting). + * Note: Votes are associated with EventItems, not Matters directly. + */ + async getVotes( + jurisdiction: Jurisdiction, + eventItemId: number, + ): Promise { + return this.fetch( + jurisdiction, + `/EventItems/${eventItemId}/Votes`, + ); + } + + /** + * Get roll call votes for all items in a meeting. + * Returns agenda items with their associated votes. + */ + async getMeetingVotes( + jurisdiction: Jurisdiction, + meetingId: number, + ): Promise { + return this.fetch( + jurisdiction, + `/Events/${meetingId}/EventItems`, + { RollCalls: "1" }, + ); + } + + /** + * Get agenda items for a specific meeting. + */ + async getAgendas( + jurisdiction: Jurisdiction, + meetingId: number, + ): Promise { + return this.fetch( + jurisdiction, + `/Events/${meetingId}/EventItems`, + { AgendaNote: "1", MinutesNote: "1", Attachments: "1" }, + ); + } + + /** + * Get a single meeting by ID. + */ + async getMeeting( + jurisdiction: Jurisdiction, + meetingId: number, + ): Promise { + return this.fetch(jurisdiction, `/Events/${meetingId}`, { + EventItems: "1", + EventItemAttachments: "1", + }); + } + + /** + * Get a single matter (legislation) by ID. + */ + async getMatter( + jurisdiction: Jurisdiction, + matterId: number, + ): Promise { + return this.fetch(jurisdiction, `/Matters/${matterId}`); + } + + /** + * Get all bodies (committees, councils, boards) for a jurisdiction. + */ + async getBodies(jurisdiction: Jurisdiction): Promise { + return this.fetch(jurisdiction, "/Bodies", { + $filter: "BodyActiveFlag eq 1", + }); + } + + /** + * Get attachments for a matter. + */ + async getMatterAttachments( + jurisdiction: Jurisdiction, + matterId: number, + ): Promise { + return this.fetch( + jurisdiction, + `/Matters/${matterId}/Attachments`, + ); + } + + /** + * Search for matters across all matter types. + */ + async searchMatters( + jurisdiction: Jurisdiction, + searchText: string, + ): Promise { + return this.getLegislation(jurisdiction, { text: searchText }); + } +} + +// ============================================================================ +// Error Handling +// ============================================================================ + +export class LegistarError extends Error { + constructor( + message: string, + public statusCode: number, + public jurisdiction: Jurisdiction, + public endpoint: string, + ) { + super(message); + this.name = "LegistarError"; + } +} + +// ============================================================================ +// Mock Data (used when API is unavailable in development) +// ============================================================================ + +function mockDate(daysAgo: number): string { + const d = new Date(); + d.setDate(d.getDate() - daysAgo); + return d.toISOString(); +} + +const MOCK_MATTERS_SANJOSE: LegistarMatter[] = [ + { + MatterId: 90001, + MatterGuid: "mock-sj-001", + MatterLastModifiedUtc: mockDate(2), + MatterRowVersion: "1", + MatterFile: "RES 2025-101", + MatterName: null, + MatterTitle: "Approval of Affordable Housing Development at 500 E Santa Clara St", + MatterTypeId: 1, + MatterTypeName: "Resolution", + MatterStatusId: 1, + MatterStatusName: "Approved", + MatterBodyId: 1, + MatterBodyName: "City Council", + MatterIntroDate: mockDate(30), + MatterAgendaDate: mockDate(7), + MatterPassedDate: mockDate(5), + MatterEnactmentDate: null, + MatterEnactmentNumber: null, + MatterRequester: null, + MatterNotes: null, + MatterVersion: "1", + MatterText1: null, MatterText2: null, MatterText3: null, MatterText4: null, MatterText5: null, + MatterRestrictViewViaWeb: false, + }, + { + MatterId: 90002, + MatterGuid: "mock-sj-002", + MatterLastModifiedUtc: mockDate(5), + MatterRowVersion: "1", + MatterFile: "ORD 2025-045", + MatterName: null, + MatterTitle: "Amendment to Municipal Code Chapter 20.80 — Protected Trees Ordinance Update", + MatterTypeId: 2, + MatterTypeName: "Ordinance", + MatterStatusId: 2, + MatterStatusName: "Pending", + MatterBodyId: 1, + MatterBodyName: "City Council", + MatterIntroDate: mockDate(20), + MatterAgendaDate: mockDate(10), + MatterPassedDate: null, + MatterEnactmentDate: null, + MatterEnactmentNumber: null, + MatterRequester: null, + MatterNotes: null, + MatterVersion: "1", + MatterText1: null, MatterText2: null, MatterText3: null, MatterText4: null, MatterText5: null, + MatterRestrictViewViaWeb: false, + }, + { + MatterId: 90003, + MatterGuid: "mock-sj-003", + MatterLastModifiedUtc: mockDate(8), + MatterRowVersion: "1", + MatterFile: "MGR 2025-012", + MatterName: null, + MatterTitle: "City Manager Report on Downtown Bike Lane Network Expansion Plan", + MatterTypeId: 3, + MatterTypeName: "Report", + MatterStatusId: 1, + MatterStatusName: "Filed", + MatterBodyId: 3, + MatterBodyName: "Transportation & Environment Committee", + MatterIntroDate: mockDate(15), + MatterAgendaDate: mockDate(10), + MatterPassedDate: null, + MatterEnactmentDate: null, + MatterEnactmentNumber: null, + MatterRequester: null, + MatterNotes: null, + MatterVersion: "1", + MatterText1: null, MatterText2: null, MatterText3: null, MatterText4: null, MatterText5: null, + MatterRestrictViewViaWeb: false, + }, + { + MatterId: 90004, + MatterGuid: "mock-sj-004", + MatterLastModifiedUtc: mockDate(1), + MatterRowVersion: "1", + MatterFile: "RES 2025-118", + MatterName: null, + MatterTitle: "Authorization for Emergency Water Main Repair on N 1st Street", + MatterTypeId: 1, + MatterTypeName: "Resolution", + MatterStatusId: 1, + MatterStatusName: "Approved", + MatterBodyId: 1, + MatterBodyName: "City Council", + MatterIntroDate: mockDate(3), + MatterAgendaDate: mockDate(2), + MatterPassedDate: mockDate(1), + MatterEnactmentDate: null, + MatterEnactmentNumber: null, + MatterRequester: null, + MatterNotes: null, + MatterVersion: "1", + MatterText1: null, MatterText2: null, MatterText3: null, MatterText4: null, MatterText5: null, + MatterRestrictViewViaWeb: false, + }, +]; + +const MOCK_MATTERS_SANTACLARA: LegistarMatter[] = [ + { + MatterId: 91001, + MatterGuid: "mock-sc-001", + MatterLastModifiedUtc: mockDate(3), + MatterRowVersion: "1", + MatterFile: "BOS 2025-034", + MatterName: null, + MatterTitle: "Adoption of Santa Clara County Climate Action Plan 2030 Update", + MatterTypeId: 1, + MatterTypeName: "Board Resolution", + MatterStatusId: 2, + MatterStatusName: "Pending", + MatterBodyId: 1, + MatterBodyName: "Board of Supervisors", + MatterIntroDate: mockDate(25), + MatterAgendaDate: mockDate(7), + MatterPassedDate: null, + MatterEnactmentDate: null, + MatterEnactmentNumber: null, + MatterRequester: null, + MatterNotes: null, + MatterVersion: "1", + MatterText1: null, MatterText2: null, MatterText3: null, MatterText4: null, MatterText5: null, + MatterRestrictViewViaWeb: false, + }, + { + MatterId: 91002, + MatterGuid: "mock-sc-002", + MatterLastModifiedUtc: mockDate(6), + MatterRowVersion: "1", + MatterFile: "BOS 2025-029", + MatterName: null, + MatterTitle: "Agreement with Valley Transportation Authority for BART Phase II Funding", + MatterTypeId: 1, + MatterTypeName: "Board Resolution", + MatterStatusId: 1, + MatterStatusName: "Approved", + MatterBodyId: 1, + MatterBodyName: "Board of Supervisors", + MatterIntroDate: mockDate(40), + MatterAgendaDate: mockDate(14), + MatterPassedDate: mockDate(7), + MatterEnactmentDate: null, + MatterEnactmentNumber: null, + MatterRequester: null, + MatterNotes: null, + MatterVersion: "1", + MatterText1: null, MatterText2: null, MatterText3: null, MatterText4: null, MatterText5: null, + MatterRestrictViewViaWeb: false, + }, + { + MatterId: 91003, + MatterGuid: "mock-sc-003", + MatterLastModifiedUtc: mockDate(4), + MatterRowVersion: "1", + MatterFile: "BOS 2025-041", + MatterName: null, + MatterTitle: "Ordinance Amending County Code for Short-Term Rental Regulations in Unincorporated Areas", + MatterTypeId: 2, + MatterTypeName: "Ordinance", + MatterStatusId: 2, + MatterStatusName: "Pending", + MatterBodyId: 1, + MatterBodyName: "Board of Supervisors", + MatterIntroDate: mockDate(14), + MatterAgendaDate: mockDate(7), + MatterPassedDate: null, + MatterEnactmentDate: null, + MatterEnactmentNumber: null, + MatterRequester: null, + MatterNotes: null, + MatterVersion: "1", + MatterText1: null, MatterText2: null, MatterText3: null, MatterText4: null, MatterText5: null, + MatterRestrictViewViaWeb: false, + }, +]; + +class FallbackLegistarClient extends LegistarClient { + override async getLegislation( + jurisdiction: Jurisdiction, + query?: LegislationQuery, + ): Promise { + try { + return await super.getLegislation(jurisdiction, query); + } catch { + if (jurisdiction === "sanjose") return MOCK_MATTERS_SANJOSE; + if (jurisdiction === "santaclara") return MOCK_MATTERS_SANTACLARA; + return []; + } + } +} + +// ============================================================================ +// Export singleton instance +// ============================================================================ + +export const legistar = new FallbackLegistarClient(); + +export { LegistarClient }; diff --git a/packages/api/src/lib/civic.ts b/packages/api/src/lib/civic.ts new file mode 100644 index 0000000..f0c5970 --- /dev/null +++ b/packages/api/src/lib/civic.ts @@ -0,0 +1,521 @@ +/** + * Google Civic Information API Client + * + * API Reference: https://developers.google.com/civic-information/docs/v2 + */ + +const CIVIC_API_BASE = "https://www.googleapis.com/civicinfo/v2"; + +function getApiKey(): string | null { + return process.env.GOOGLE_CIVIC_API_KEY ?? null; +} + +// ============================================================================ +// Types +// ============================================================================ + +export interface Election { + id: string; + name: string; + electionDay: string; + ocdDivisionId: string; +} + +export interface ElectionsResponse { + kind: string; + elections: Election[]; +} + +export interface Address { + locationName?: string; + line1: string; + line2?: string; + line3?: string; + city: string; + state: string; + zip: string; +} + +export interface PollingLocation { + address: Address; + notes?: string; + pollingHours?: string; + name?: string; + voterServices?: string; + startDate?: string; + endDate?: string; + sources?: Source[]; +} + +export interface Source { + name: string; + official: boolean; +} + +export interface Contest { + type: string; + primaryParty?: string; + electorateSpecifications?: string; + special?: string; + ballotTitle?: string; + office?: string; + level?: string[]; + roles?: string[]; + district?: ElectoralDistrict; + numberElected?: string; + numberVotingFor?: string; + ballotPlacement?: string; + candidates?: Candidate[]; + referendumTitle?: string; + referendumSubtitle?: string; + referendumUrl?: string; + referendumBrief?: string; + referendumText?: string; + referendumProStatement?: string; + referendumConStatement?: string; + referendumPassageThreshold?: string; + referendumEffectOfAbstain?: string; + sources?: Source[]; +} + +export interface Candidate { + name: string; + party?: string; + candidateUrl?: string; + phone?: string; + photoUrl?: string; + email?: string; + orderOnBallot?: string; + channels?: Channel[]; +} + +export interface Channel { + type: string; + id: string; +} + +export interface ElectoralDistrict { + name: string; + scope?: string; + id?: string; +} + +export interface AdministrationRegion { + name: string; + electionAdministrationBody?: AdministrationBody; + localJurisdiction?: AdministrationRegion; + sources?: Source[]; +} + +export interface AdministrationBody { + name?: string; + electionInfoUrl?: string; + electionRegistrationUrl?: string; + electionRegistrationConfirmationUrl?: string; + absenteeVotingInfoUrl?: string; + votingLocationFinderUrl?: string; + ballotInfoUrl?: string; + electionRulesUrl?: string; + voterServices?: string[]; + hoursOfOperation?: string; + correspondenceAddress?: Address; + physicalAddress?: Address; + electionOfficials?: ElectionOfficial[]; +} + +export interface ElectionOfficial { + name?: string; + title?: string; + officePhoneNumber?: string; + faxNumber?: string; + emailAddress?: string; +} + +export interface VoterInfoResponse { + kind: string; + election: Election; + normalizedInput: Address; + pollingLocations?: PollingLocation[]; + earlyVoteSites?: PollingLocation[]; + dropOffLocations?: PollingLocation[]; + contests?: Contest[]; + state?: AdministrationRegion[]; + mailOnly?: boolean; +} + +export interface Official { + name: string; + address?: Address[]; + party?: string; + phones?: string[]; + urls?: string[]; + photoUrl?: string; + emails?: string[]; + channels?: Channel[]; +} + +export interface Office { + name: string; + divisionId: string; + levels?: string[]; + roles?: string[]; + officialIndices: number[]; +} + +export interface Division { + name: string; + alsoKnownAs?: string[]; + officeIndices?: number[]; +} + +export interface RepresentativesResponse { + kind: string; + normalizedInput: Address; + divisions: Record; + offices: Office[]; + officials: Official[]; +} + +// Enriched types that combine office info with officials +export interface Representative extends Official { + office: string; + divisionId: string; + levels?: string[]; + roles?: string[]; +} + +// ============================================================================ +// API Functions +// ============================================================================ + +async function fetchCivicApi( + endpoint: string, + params: Record = {}, +): Promise { + const apiKey = getApiKey(); + if (!apiKey) { + throw new Error("GOOGLE_CIVIC_API_KEY not configured"); + } + const url = new URL(`${CIVIC_API_BASE}/${endpoint}`); + url.searchParams.set("key", apiKey); + + for (const [key, value] of Object.entries(params)) { + url.searchParams.set(key, value); + } + + const response = await fetch(url.toString()); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error( + `Google Civic API error: ${response.status} ${response.statusText} - ${JSON.stringify(error)}`, + ); + } + + return response.json() as Promise; +} + +// ============================================================================ +// Mock Data (used when GOOGLE_CIVIC_API_KEY is not configured) +// ============================================================================ + +function futureDate(daysFromNow: number): string { + const d = new Date(); + d.setDate(d.getDate() + daysFromNow); + return d.toISOString().split("T")[0]!; +} + +const MOCK_ELECTIONS: Election[] = [ + { + id: "9000", + name: "California Primary Election", + electionDay: futureDate(45), + ocdDivisionId: "ocd-division/country:us/state:ca", + }, + { + id: "9001", + name: "San Jose City Council Special Election", + electionDay: futureDate(90), + ocdDivisionId: "ocd-division/country:us/state:ca/place:san_jose", + }, +]; + +function getMockVoterInfo(address: string): VoterInfoResponse { + return { + kind: "civicinfo#voterInfoResponse", + election: MOCK_ELECTIONS[0]!, + normalizedInput: { + line1: address.split(",")[0] ?? address, + city: "San Jose", + state: "CA", + zip: "95112", + }, + pollingLocations: [ + { + address: { + locationName: "Washington Elementary School", + line1: "100 Oak St", + city: "San Jose", + state: "CA", + zip: "95112", + }, + pollingHours: "7:00 AM - 8:00 PM", + name: "Washington Elementary School", + }, + ], + earlyVoteSites: [ + { + address: { + locationName: "Santa Clara County Registrar", + line1: "1555 Berger Dr", + city: "San Jose", + state: "CA", + zip: "95112", + }, + pollingHours: "Mon-Fri 8:00 AM - 5:00 PM", + name: "Santa Clara County Registrar of Voters", + startDate: futureDate(-14), + endDate: futureDate(44), + }, + ], + contests: [ + { + type: "General", + office: "U.S. Representative, District 17", + level: ["country"], + roles: ["legislatorLowerBody"], + district: { name: "California's 17th Congressional District", scope: "congressional" }, + candidates: [ + { name: "Ro Khanna", party: "Democratic", candidateUrl: "https://example.com" }, + { name: "Anita Chen", party: "Republican" }, + ], + }, + { + type: "General", + office: "State Senator, District 15", + level: ["administrativeArea1"], + roles: ["legislatorUpperBody"], + district: { name: "California State Senate District 15", scope: "stateUpper" }, + candidates: [ + { name: "Dave Cortese", party: "Democratic" }, + { name: "Robert Singh", party: "Republican" }, + ], + }, + { + type: "General", + office: "Mayor, City of San Jose", + level: ["locality"], + roles: ["headOfGovernment"], + district: { name: "City of San Jose", scope: "citywide" }, + candidates: [ + { name: "Maria Gonzalez", party: "Nonpartisan" }, + { name: "Kevin Park", party: "Nonpartisan" }, + { name: "Lisa Tran", party: "Nonpartisan" }, + ], + }, + { + type: "General", + office: "City Council, District 3", + level: ["locality"], + roles: ["legislatorLowerBody"], + district: { name: "San Jose City Council District 3", scope: "cityCouncil" }, + candidates: [ + { name: "Omar Hernandez", party: "Nonpartisan" }, + { name: "Jennifer Wu", party: "Nonpartisan" }, + ], + }, + { + type: "Referendum", + referendumTitle: "Measure A — Affordable Housing Bond", + referendumSubtitle: "Shall the City of San Jose issue $450 million in general obligation bonds to fund affordable housing construction and rehabilitation?", + referendumProStatement: "Addresses critical housing shortage. Creates thousands of affordable units for working families, seniors, and veterans.", + referendumConStatement: "Increases property taxes by approximately $19.60 per $100,000 of assessed value. Adds to existing city debt obligations.", + }, + { + type: "Referendum", + referendumTitle: "Measure B — Parks and Recreation Funding", + referendumSubtitle: "Shall the City authorize a 1/8-cent sales tax increase to fund park maintenance, recreational programs, and new green spaces?", + referendumProStatement: "Invests in neighborhood parks and youth programs. All funds stay local with independent oversight.", + referendumConStatement: "Sales tax increases disproportionately affect lower-income residents. City should prioritize existing revenue for parks.", + }, + { + type: "Referendum", + referendumTitle: "Measure C — Police Oversight Commission", + referendumSubtitle: "Shall the City Charter be amended to establish an independent Police Oversight Commission with subpoena power?", + referendumProStatement: "Creates accountability and transparency in policing. Commission would have independent investigative authority.", + referendumConStatement: "Duplicates existing oversight structures. Could interfere with active investigations and officer due process rights.", + }, + ], + state: [ + { + name: "California", + electionAdministrationBody: { + name: "Santa Clara County Registrar of Voters", + electionInfoUrl: "https://www.sccgov.org/rov", + electionRegistrationUrl: "https://registertovote.ca.gov", + absenteeVotingInfoUrl: "https://www.sccgov.org/rov/vbm", + }, + }, + ], + }; +} + +const MOCK_REPRESENTATIVES: Representative[] = [ + { + name: "Ro Khanna", + office: "U.S. Representative, CA-17", + party: "Democratic Party", + divisionId: "ocd-division/country:us/state:ca/cd:17", + levels: ["country"], + roles: ["legislatorLowerBody"], + urls: ["https://example.com"], + phones: ["(202) 555-0117"], + }, + { + name: "Alex Padilla", + office: "U.S. Senator", + party: "Democratic Party", + divisionId: "ocd-division/country:us/state:ca", + levels: ["country"], + roles: ["legislatorUpperBody"], + phones: ["(202) 555-0100"], + }, + { + name: "Matt Mahan", + office: "Mayor of San Jose", + party: "Nonpartisan", + divisionId: "ocd-division/country:us/state:ca/place:san_jose", + levels: ["locality"], + roles: ["headOfGovernment"], + phones: ["(408) 555-0199"], + }, +]; + +// ============================================================================ +// API Functions +// ============================================================================ + +/** + * Get a list of upcoming elections + * + * @returns List of elections visible to the API + */ +export async function getElections(): Promise { + if (!getApiKey()) return MOCK_ELECTIONS; + const response = await fetchCivicApi("elections"); + return response.elections; +} + +/** + * Get voter info for a specific address + * + * @param address - The registered address of the voter + * @param electionId - Optional election ID (from getElections). If not provided, + * returns info for the most relevant upcoming election. + * @returns Polling locations, ballot info, and contests for the address + */ +export async function getVoterInfo( + address: string, + electionId?: string, +): Promise { + if (!getApiKey()) return getMockVoterInfo(address); + const params: Record = { address }; + + if (electionId) { + params.electionId = electionId; + } + + return fetchCivicApi("voterinfo", params); +} + +/** + * Get elected officials (representatives) for an address + * + * @param address - The address to look up + * @param levels - Optional filter by government level: country, administrativeArea1 (state), + * administrativeArea2 (county), locality, regional, special, subLocality1, subLocality2 + * @param roles - Optional filter by role: headOfState, headOfGovernment, deputyHeadOfGovernment, + * governmentOfficer, executiveCouncil, legislatorUpperBody, legislatorLowerBody, + * highestCourtJudge, judge, schoolBoard, specialPurposeOfficer + * @returns Representatives with their offices and contact information + */ +export async function getRepresentatives( + address: string, + options?: { + levels?: string[]; + roles?: string[]; + includeOffices?: boolean; + }, +): Promise { + if (!getApiKey()) { + return { + kind: "civicinfo#representativeInfoResponse", + normalizedInput: { line1: address, city: "San Jose", state: "CA", zip: "95112" }, + divisions: {}, + offices: MOCK_REPRESENTATIVES.map((r, i) => ({ + name: r.office, + divisionId: r.divisionId, + levels: r.levels, + roles: r.roles, + officialIndices: [i], + })), + officials: MOCK_REPRESENTATIVES.map((r) => ({ + name: r.name, + party: r.party, + phones: r.phones, + urls: r.urls, + })), + }; + } + const params: Record = { address }; + + if (options?.levels?.length) { + params.levels = options.levels.join(","); + } + + if (options?.roles?.length) { + params.roles = options.roles.join(","); + } + + if (options?.includeOffices === false) { + params.includeOffices = "false"; + } + + return fetchCivicApi("representatives", params); +} + +/** + * Get representatives with office info merged (convenience function) + * + * @param address - The address to look up + * @returns Array of representatives with their office information included + */ +export async function getRepresentativesEnriched( + address: string, + options?: { + levels?: string[]; + roles?: string[]; + }, +): Promise { + if (!getApiKey()) return MOCK_REPRESENTATIVES; + const response = await getRepresentatives(address, options); + + const representatives: Representative[] = []; + + for (const office of response.offices) { + for (const index of office.officialIndices) { + const official = response.officials[index]; + if (official) { + representatives.push({ + ...official, + office: office.name, + divisionId: office.divisionId, + levels: office.levels, + roles: office.roles, + }); + } + } + } + + return representatives; +} diff --git a/packages/api/src/root.ts b/packages/api/src/root.ts index c2cb61f..69aa0a7 100644 --- a/packages/api/src/root.ts +++ b/packages/api/src/root.ts @@ -1,14 +1,20 @@ import { authRouter } from "./router/auth"; +import { civicRouter } from "./router/civic"; import { contentRouter } from "./router/content"; +import { electionsRouter } from "./router/elections"; +import { legistarRouter } from "./router/legistar"; import { postRouter } from "./router/post"; import { videoRouter } from "./router/video"; import { createTRPCRouter } from "./trpc"; export const appRouter = createTRPCRouter({ auth: authRouter, + civic: civicRouter, + legistar: legistarRouter, post: postRouter, content: contentRouter, video: videoRouter, + caElections: electionsRouter, }); // export type definition of API diff --git a/packages/api/src/router/civic.ts b/packages/api/src/router/civic.ts new file mode 100644 index 0000000..d309414 --- /dev/null +++ b/packages/api/src/router/civic.ts @@ -0,0 +1,170 @@ +import type { TRPCRouterRecord } from "@trpc/server"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod/v4"; + +import { + getElections, + getRepresentatives, + getRepresentativesEnriched, + getVoterInfo, +} from "../lib/civic"; +import { publicProcedure } from "../trpc"; + +export const civicRouter = { + /** + * Get a list of upcoming elections + */ + getElections: publicProcedure.query(async () => { + try { + return await getElections(); + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error ? error.message : "Failed to fetch elections", + cause: error, + }); + } + }), + + /** + * Get voter info (polling places, ballot info) for an address + */ + getVoterInfo: publicProcedure + .input( + z.object({ + address: z.string().min(1, "Address is required"), + electionId: z.string().optional(), + }), + ) + .query(async ({ input }) => { + try { + return await getVoterInfo(input.address, input.electionId); + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error + ? error.message + : "Failed to fetch voter info", + cause: error, + }); + } + }), + + /** + * Get elected officials/representatives for an address + */ + getRepresentatives: publicProcedure + .input( + z.object({ + address: z.string().min(1, "Address is required"), + levels: z + .array( + z.enum([ + "country", + "administrativeArea1", + "administrativeArea2", + "locality", + "regional", + "special", + "subLocality1", + "subLocality2", + ]), + ) + .optional(), + roles: z + .array( + z.enum([ + "headOfState", + "headOfGovernment", + "deputyHeadOfGovernment", + "governmentOfficer", + "executiveCouncil", + "legislatorUpperBody", + "legislatorLowerBody", + "highestCourtJudge", + "judge", + "schoolBoard", + "specialPurposeOfficer", + ]), + ) + .optional(), + }), + ) + .query(async ({ input }) => { + try { + return await getRepresentatives(input.address, { + levels: input.levels, + roles: input.roles, + }); + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error + ? error.message + : "Failed to fetch representatives", + cause: error, + }); + } + }), + + /** + * Get representatives with office info merged (convenience endpoint) + */ + getRepresentativesEnriched: publicProcedure + .input( + z.object({ + address: z.string().min(1, "Address is required"), + levels: z + .array( + z.enum([ + "country", + "administrativeArea1", + "administrativeArea2", + "locality", + "regional", + "special", + "subLocality1", + "subLocality2", + ]), + ) + .optional(), + roles: z + .array( + z.enum([ + "headOfState", + "headOfGovernment", + "deputyHeadOfGovernment", + "governmentOfficer", + "executiveCouncil", + "legislatorUpperBody", + "legislatorLowerBody", + "highestCourtJudge", + "judge", + "schoolBoard", + "specialPurposeOfficer", + ]), + ) + .optional(), + }), + ) + .query(async ({ input }) => { + try { + return await getRepresentativesEnriched(input.address, { + levels: input.levels, + roles: input.roles, + }); + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error + ? error.message + : "Failed to fetch representatives", + cause: error, + }); + } + }), +} satisfies TRPCRouterRecord; diff --git a/packages/api/src/router/elections.ts b/packages/api/src/router/elections.ts new file mode 100644 index 0000000..cc47801 --- /dev/null +++ b/packages/api/src/router/elections.ts @@ -0,0 +1,274 @@ +import type { TRPCRouterRecord } from "@trpc/server"; +import { z } from "zod/v4"; + +import type { + CASOSClientConfig, + ContestType, + CountyCode, +} from "../clients/ca-sos"; +import { CA_COUNTIES, createCASOSClient } from "../clients/ca-sos"; +import { publicProcedure } from "../trpc"; + +// Schema for county code validation +const CountyCodeSchema = z.enum( + Object.keys(CA_COUNTIES) as [CountyCode, ...CountyCode[]], +); + +// Schema for contest type validation +const ContestTypeSchema = z.enum([ + "president", + "us_senate", + "us_house", + "governor", + "state_senate", + "state_assembly", + "proposition", + "local", + "judicial", + "other", +] as const satisfies readonly ContestType[]); + +// Check if CA SOS API is configured +function isConfigured(): boolean { + return !!process.env.CA_SOS_API_KEY; +} + +// Get client with optional API key from environment +function getClient(): ReturnType { + const config: CASOSClientConfig = {}; + + // Use API key from environment if available + const apiKey = process.env.CA_SOS_API_KEY; + if (apiKey) { + config.apiKey = apiKey; + } + + return createCASOSClient(config); +} + +// Wrapper that returns null when API not configured +function withConfigCheck(fn: () => Promise): Promise { + if (!isConfigured()) { + return Promise.resolve(null); + } + return fn(); +} + +// Wrapper that returns empty array when API not configured +function withConfigCheckArray(fn: () => Promise): Promise { + if (!isConfigured()) { + return Promise.resolve([]); + } + return fn(); +} + +export const electionsRouter = { + // ========================================================================== + // Elections + // ========================================================================== + + /** Check if CA SOS API is configured */ + isConfigured: publicProcedure.query(() => isConfigured()), + + /** Get all available elections */ + list: publicProcedure.query(async () => { + return withConfigCheckArray(() => getClient().getElections()); + }), + + /** Get current/active election */ + current: publicProcedure.query(async () => { + return withConfigCheck(() => getClient().getCurrentElection()); + }), + + /** Get election by ID */ + byId: publicProcedure + .input(z.object({ electionId: z.string() })) + .query(async ({ input }) => { + return withConfigCheck(() => getClient().getElection(input.electionId)); + }), + + /** Get election status/reporting progress */ + status: publicProcedure + .input(z.object({ electionId: z.string() })) + .query(async ({ input }) => { + return withConfigCheck(() => + getClient().getElectionStatus(input.electionId), + ); + }), + + // ========================================================================== + // Contests + // ========================================================================== + + /** Get all contests for an election */ + contests: publicProcedure + .input( + z.object({ + electionId: z.string(), + type: ContestTypeSchema.optional(), + }), + ) + .query(async ({ input }) => { + return withConfigCheckArray(() => { + const client = getClient(); + if (input.type) { + return client.getContestsByType(input.electionId, input.type); + } + return client.getContests(input.electionId); + }); + }), + + /** Get a specific contest */ + contest: publicProcedure + .input( + z.object({ + electionId: z.string(), + contestId: z.string(), + }), + ) + .query(async ({ input }) => { + return withConfigCheck(() => + getClient().getContest(input.electionId, input.contestId), + ); + }), + + /** Get all propositions for an election */ + propositions: publicProcedure + .input(z.object({ electionId: z.string() })) + .query(async ({ input }) => { + return withConfigCheckArray(() => + getClient().getPropositions(input.electionId), + ); + }), + + // ========================================================================== + // Results + // ========================================================================== + + /** Get all results for an election */ + allResults: publicProcedure + .input(z.object({ electionId: z.string() })) + .query(async ({ input }) => { + return withConfigCheckArray(() => + getClient().getAllResults(input.electionId), + ); + }), + + /** Get results for a specific contest */ + contestResults: publicProcedure + .input( + z.object({ + electionId: z.string(), + contestId: z.string(), + includeCounties: z.boolean().optional().default(false), + }), + ) + .query(async ({ input }) => { + return withConfigCheck(() => { + const client = getClient(); + if (input.includeCounties) { + return client.getContestResultsWithCounties( + input.electionId, + input.contestId, + ); + } + return client.getContestResults(input.electionId, input.contestId); + }); + }), + + /** Get county-level results for a contest */ + countyResults: publicProcedure + .input( + z.object({ + electionId: z.string(), + contestId: z.string(), + countyCode: CountyCodeSchema.optional(), + }), + ) + .query(async ({ input }) => { + if (!isConfigured()) return input.countyCode ? null : []; + const client = getClient(); + if (input.countyCode) { + return client.getCountyResult( + input.electionId, + input.contestId, + input.countyCode, + ); + } + return client.getCountyResults(input.electionId, input.contestId); + }), + + // ========================================================================== + // Statewide Races + // ========================================================================== + + /** Get presidential race results */ + presidential: publicProcedure + .input(z.object({ electionId: z.string() })) + .query(async ({ input }) => { + return withConfigCheck(() => + getClient().getPresidentialResults(input.electionId), + ); + }), + + /** Get gubernatorial race results */ + governor: publicProcedure + .input(z.object({ electionId: z.string() })) + .query(async ({ input }) => { + return withConfigCheck(() => + getClient().getGovernorResults(input.electionId), + ); + }), + + /** Get US Senate race results */ + usSenate: publicProcedure + .input(z.object({ electionId: z.string() })) + .query(async ({ input }) => { + return withConfigCheckArray(() => + getClient().getUSSenateResults(input.electionId), + ); + }), + + /** Get US House race results */ + usHouse: publicProcedure + .input( + z.object({ + electionId: z.string(), + district: z.number().optional(), + }), + ) + .query(async ({ input }) => { + return withConfigCheckArray(() => + getClient().getUSHouseResults(input.electionId, input.district), + ); + }), + + /** Get proposition results */ + propositionResults: publicProcedure + .input( + z.object({ + electionId: z.string(), + propositionNumber: z.string(), + }), + ) + .query(async ({ input }) => { + return withConfigCheck(() => + getClient().getPropositionResults( + input.electionId, + input.propositionNumber, + ), + ); + }), + + // ========================================================================== + // Utilities + // ========================================================================== + + /** Get list of California counties */ + counties: publicProcedure.query(() => { + return Object.entries(CA_COUNTIES).map(([code, name]) => ({ + code: code as CountyCode, + name, + })); + }), +} satisfies TRPCRouterRecord; diff --git a/packages/api/src/router/legistar.ts b/packages/api/src/router/legistar.ts new file mode 100644 index 0000000..2e4550f --- /dev/null +++ b/packages/api/src/router/legistar.ts @@ -0,0 +1,41 @@ +import type { TRPCRouterRecord } from "@trpc/server"; +import { TRPCError } from "@trpc/server"; + +import { legistar } from "../integrations/legistar"; +import { publicProcedure } from "../trpc"; + +export const legistarRouter = { + getLocalBills: publicProcedure.query(async () => { + try { + const [sanjose, santaclara] = await Promise.all([ + legistar.getLegislation("sanjose", {}).catch(() => []), + legistar.getLegislation("santaclara", {}).catch(() => []), + ]); + + const allBills = [ + ...sanjose.map((b) => ({ ...b, jurisdiction: "San Jose" as const })), + ...santaclara.map((b) => ({ + ...b, + jurisdiction: "Santa Clara County" as const, + })), + ]; + + return allBills + .sort( + (a, b) => + new Date(b.MatterLastModifiedUtc).getTime() - + new Date(a.MatterLastModifiedUtc).getTime(), + ) + .slice(0, 10); + } catch (error) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + error instanceof Error + ? error.message + : "Failed to fetch local bills", + cause: error, + }); + } + }), +} satisfies TRPCRouterRecord; diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index e9e1f81..a7147f7 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -12,12 +12,14 @@ function expoPlugin(options?: { disableOriginOverride?: boolean }) { init: () => { return { options: { - trustedOrigins: process.env.NODE_ENV === "development" ? ["exp://"] : [], + trustedOrigins: + process.env.NODE_ENV === "development" ? ["exp://"] : [], }, }; }, async onRequest(request: Request) { - if (options?.disableOriginOverride || request.headers.get("origin")) return; + if (options?.disableOriginOverride || request.headers.get("origin")) + return; // Expo native clients send their origin separately, so mirror it for Better Auth's origin check. const expoOrigin = request.headers.get("expo-origin"); diff --git a/packages/db/package.json b/packages/db/package.json index cef5270..eba0139 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -24,6 +24,7 @@ "format": "prettier --check . --ignore-path ../../.gitignore", "lint": "eslint --flag unstable_native_nodejs_ts_config", "push": "pnpm with-env drizzle-kit push", + "seed": "pnpm with-env tsx seed.ts", "studio": "pnpm with-env drizzle-kit studio", "typecheck": "tsc --noEmit --emitDeclarationOnly false", "with-env": "dotenv -e ../../.env --" @@ -40,9 +41,11 @@ "@acme/prettier-config": "workspace:*", "@acme/tsconfig": "workspace:*", "@types/pg": "^8.20.0", + "dotenv-cli": "^11.0.0", "drizzle-kit": "^0.31.10", "eslint": "catalog:", "prettier": "catalog:", + "tsx": "^4.21.0", "typescript": "catalog:" }, "prettier": "@acme/prettier-config" diff --git a/packages/db/seed.ts b/packages/db/seed.ts new file mode 100644 index 0000000..2d21f17 --- /dev/null +++ b/packages/db/seed.ts @@ -0,0 +1,480 @@ +import { createHash } from "node:crypto"; + +import { db } from "./src/client"; +import { Bill, CourtCase, GovernmentContent, Video } from "./src/schema"; + +function hash(content: string) { + return createHash("sha256").update(content).digest("hex"); +} + +const now = new Date(); +const daysAgo = (n: number) => new Date(now.getTime() - n * 86400000); + +const bills = [ + { + billNumber: "H.R. 1001", + title: "Infrastructure Modernization Act of 2025", + description: + "A bill to authorize funding for the repair and modernization of roads, bridges, and public transit systems across the United States.", + sponsor: "Rep. Maria Torres (D-CA-12)", + status: "Passed House", + introducedDate: daysAgo(45), + congress: 119, + chamber: "House", + summary: + "Authorizes $200 billion over 10 years for infrastructure modernization including roads, bridges, and transit.", + fullText: + "Be it enacted by the Senate and House of Representatives of the United States of America in Congress assembled, SECTION 1. SHORT TITLE. This Act may be cited as the 'Infrastructure Modernization Act of 2025'. SECTION 2. FINDINGS. Congress finds that the nation's infrastructure is in critical need of repair and modernization...", + aiGeneratedArticle: `# What This Means For You +Your daily commute could get a lot better. This bill allocates $200 billion to fix crumbling roads and bridges and expand public transit options. + +# Overview +The Infrastructure Modernization Act of 2025 is a sweeping proposal to address decades of deferred maintenance on America's roads, bridges, and public transit systems. The bill authorizes $200 billion in federal spending over the next decade, with funds distributed to states based on a formula that considers population, road conditions, and transit ridership. + +Key provisions include dedicated funding for bridge repair, expansion of rural broadband infrastructure, and grants for cities looking to build or expand light rail and bus rapid transit systems. The bill also includes provisions for workforce development, aiming to create an estimated 500,000 construction and engineering jobs. + +# Impact & Implications +If enacted, this bill would represent one of the largest infrastructure investments in a generation. Commuters in urban areas could see reduced congestion and improved transit options, while rural communities would benefit from better road conditions and expanded broadband access. Construction workers and engineers would see increased job opportunities, and supply chains could become more efficient with improved transportation networks. + +# The Debate +Supporters argue the bill is long overdue, pointing to the American Society of Civil Engineers' consistent D+ rating for U.S. infrastructure. They emphasize the economic multiplier effect of infrastructure spending and the safety benefits of repairing structurally deficient bridges. Critics raise concerns about the bill's price tag and question whether the federal government should take the lead on what they see as primarily state and local responsibilities. Some fiscal hawks have proposed alternative funding mechanisms, including public-private partnerships and toll-based financing.`, + thumbnailUrl: "https://picsum.photos/seed/infra/800/600", + images: [], + url: "https://www.congress.gov/bill/119th-congress/house-bill/1001", + sourceWebsite: "congress.gov", + }, + { + billNumber: "S. 502", + title: "Digital Privacy Protection Act", + description: + "A bill to establish comprehensive federal data privacy protections for consumers and regulate the collection and sale of personal data.", + sponsor: "Sen. James Chen (R-TX)", + status: "In Committee", + introducedDate: daysAgo(30), + congress: 119, + chamber: "Senate", + summary: + "Establishes federal data privacy standards requiring companies to obtain consent before collecting personal data.", + fullText: + "Be it enacted by the Senate and House of Representatives of the United States of America in Congress assembled, SECTION 1. SHORT TITLE. This Act may be cited as the 'Digital Privacy Protection Act'. SECTION 2. PURPOSE. The purpose of this Act is to establish comprehensive federal data privacy protections...", + aiGeneratedArticle: `# What This Means For You +Companies would need your permission before collecting or selling your personal data, and you'd have the right to see and delete what they've gathered. + +# Overview +The Digital Privacy Protection Act aims to create the first comprehensive federal data privacy law in the United States. Currently, data privacy is regulated through a patchwork of state laws, with California's CCPA being the most prominent. This bill would establish a uniform national standard. + +The legislation requires companies to obtain explicit consent before collecting personal data, give consumers the right to access and delete their data, and prohibit the sale of data belonging to minors under 16. It also creates a new division within the FTC dedicated to data privacy enforcement. + +# Impact & Implications +For everyday Americans, this bill would mean more control over personal information. Tech companies, advertisers, and data brokers would face significant new compliance requirements. Small businesses worry about the cost of compliance, while privacy advocates say the bill doesn't go far enough compared to Europe's GDPR. + +# The Debate +Privacy advocates praise the bill as a necessary step but criticize exceptions that allow data collection for "legitimate business purposes," a term they say is too broadly defined. The tech industry has offered cautious support for a federal standard that would preempt the current patchwork of state laws, though they oppose provisions allowing private lawsuits. Consumer groups want stronger enforcement mechanisms and fewer corporate carve-outs.`, + thumbnailUrl: "https://picsum.photos/seed/privacy/800/600", + images: [], + url: "https://www.congress.gov/bill/119th-congress/senate-bill/502", + sourceWebsite: "congress.gov", + }, + { + billNumber: "H.R. 2200", + title: "Clean Water Access Act", + description: + "A bill to ensure access to clean drinking water in underserved communities and update aging water treatment facilities.", + sponsor: "Rep. Aisha Johnson (D-MI-13)", + status: "Introduced", + introducedDate: daysAgo(10), + congress: 119, + chamber: "House", + summary: + "Provides $50 billion for water infrastructure upgrades and lead pipe replacement in communities with contaminated water systems.", + fullText: + "Be it enacted by the Senate and House of Representatives of the United States of America in Congress assembled, SECTION 1. SHORT TITLE. This Act may be cited as the 'Clean Water Access Act'. SECTION 2. FINDINGS. Congress finds that millions of Americans lack access to clean, safe drinking water...", + aiGeneratedArticle: `# What This Means For You +If you live in a community with aging water infrastructure, this bill could fund the replacement of lead pipes and upgrade your local water treatment facility. + +# Overview +The Clean Water Access Act addresses the ongoing crisis of contaminated drinking water in communities across the United States. Inspired by the water crises in Flint, Michigan, and Jackson, Mississippi, the bill provides $50 billion in federal funding to replace lead service lines, upgrade water treatment plants, and expand water quality monitoring. + +The bill prioritizes funding for environmental justice communities that have been disproportionately affected by water contamination. It also establishes a federal water quality dashboard that would make testing results publicly accessible in real time. + +# Impact & Implications +An estimated 10 million American households still receive water through lead service lines. This bill would accelerate their replacement, potentially preventing thousands of cases of lead poisoning in children. Water utilities would receive federal support for upgrades they've deferred for decades due to funding constraints. + +# The Debate +Supporters point to the moral imperative of clean water access, citing ongoing health emergencies in several U.S. cities. Critics question the federal government's role, arguing that water infrastructure has traditionally been a local responsibility. Some propose a loan-based model rather than direct grants, while environmental groups push for stricter contamination standards alongside the funding.`, + thumbnailUrl: "https://picsum.photos/seed/water/800/600", + images: [], + url: "https://www.congress.gov/bill/119th-congress/house-bill/2200", + sourceWebsite: "congress.gov", + }, + { + billNumber: "S. 789", + title: "AI Accountability and Transparency Act", + description: + "A bill to require disclosure and impact assessments for artificial intelligence systems used in high-stakes decision-making.", + sponsor: "Sen. Rachel Kim (D-WA)", + status: "Passed Committee", + introducedDate: daysAgo(60), + congress: 119, + chamber: "Senate", + summary: + "Requires AI impact assessments and public disclosure for automated decision-making in hiring, lending, and criminal justice.", + fullText: + "Be it enacted by the Senate and House of Representatives of the United States of America in Congress assembled, SECTION 1. SHORT TITLE. This Act may be cited as the 'AI Accountability and Transparency Act'. SECTION 2. DEFINITIONS. In this Act: (1) AUTOMATED DECISION SYSTEM...", + aiGeneratedArticle: `# What This Means For You +If AI was used to deny you a loan, reject your job application, or influence a court decision about you, this bill would give you the right to know — and to challenge it. + +# Overview +The AI Accountability and Transparency Act would be the first major federal regulation of artificial intelligence in high-stakes decision-making. The bill targets AI systems used in hiring, lending, housing, healthcare, and criminal justice, requiring organizations to conduct bias audits, disclose when AI is being used, and provide explanations for AI-driven decisions. + +Companies deploying AI in these domains would need to register their systems with the FTC and submit annual impact assessments. The bill also creates an AI Civil Rights Office to investigate complaints of algorithmic discrimination. + +# Impact & Implications +Workers, borrowers, and defendants would gain new transparency rights when AI influences decisions about their lives. Tech companies and their enterprise customers would need to invest in explainability and audit infrastructure. The compliance costs could slow AI adoption in regulated industries, but proponents argue this is a feature, not a bug. + +# The Debate +Tech companies warn that overly prescriptive regulation could stifle innovation and push AI development overseas. Civil rights organizations counter that unregulated AI is already causing harm, pointing to documented cases of biased hiring algorithms and discriminatory lending models. Some legislators prefer a sector-specific approach over the bill's broad framework, while others argue it doesn't go far enough in restricting certain uses of AI entirely.`, + thumbnailUrl: "https://picsum.photos/seed/ai-law/800/600", + images: [], + url: "https://www.congress.gov/bill/119th-congress/senate-bill/789", + sourceWebsite: "congress.gov", + }, + { + billNumber: "H.R. 3456", + title: "Affordable Housing Expansion Act", + description: + "A bill to increase the supply of affordable housing through tax incentives, zoning reform support, and direct federal investment.", + sponsor: "Rep. David Park (D-NY-14)", + status: "In Committee", + introducedDate: daysAgo(20), + congress: 119, + chamber: "House", + summary: + "Expands Low-Income Housing Tax Credit, provides grants for zoning reform, and invests $30 billion in public housing rehabilitation.", + fullText: + "Be it enacted by the Senate and House of Representatives of the United States of America in Congress assembled, SECTION 1. SHORT TITLE. This Act may be cited as the 'Affordable Housing Expansion Act'...", + aiGeneratedArticle: `# What This Means For You +If you're struggling with rising rents, this bill aims to increase the supply of affordable housing in your area through a combination of tax incentives and direct investment. + +# Overview +The Affordable Housing Expansion Act tackles the nation's housing crisis through a three-pronged approach: expanding the Low-Income Housing Tax Credit (LIHTC) by 50%, offering competitive grants to cities and states that reform exclusionary zoning laws, and investing $30 billion in rehabilitating the nation's aging public housing stock. + +The bill also includes provisions for tenant protections, including a national standard for just-cause eviction and limits on security deposits. A new Office of Housing Innovation would fund pilot programs for alternative housing models like community land trusts and cooperative housing. + +# Impact & Implications +The bill could add an estimated 2 million affordable housing units over the next decade. Renters in high-cost areas would benefit from increased supply and tenant protections. Developers would gain expanded tax incentives, while cities that loosen restrictive zoning could unlock federal grants. + +# The Debate +Housing advocates strongly support the bill but want even more aggressive zoning reform provisions. Real estate interests support the LIHTC expansion but oppose the tenant protection provisions. Some conservatives argue that housing is a local issue and that federal intervention in zoning is governmental overreach. Progressive critics say the bill relies too heavily on private-sector tax incentives rather than direct public housing construction.`, + thumbnailUrl: "https://picsum.photos/seed/housing/800/600", + images: [], + url: "https://www.congress.gov/bill/119th-congress/house-bill/3456", + sourceWebsite: "congress.gov", + }, +].map((b) => ({ + ...b, + contentHash: hash(b.title + b.fullText), + versions: [], +})); + +const govContent = [ + { + title: "Executive Order on Strengthening American Cybersecurity", + type: "Executive Order", + publishedDate: daysAgo(15), + description: + "Directs federal agencies to adopt zero-trust security architectures and establishes new incident reporting requirements for critical infrastructure operators.", + fullText: + "By the authority vested in me as President by the Constitution and the laws of the United States of America, it is hereby ordered as follows: Section 1. Policy. The United States faces persistent and increasingly sophisticated malicious cyber campaigns...", + aiGeneratedArticle: `# What This Means For You +Federal agencies will be required to significantly upgrade their cybersecurity, and companies that run critical infrastructure like power grids and water systems will face new reporting requirements when they're hacked. + +# Overview +This executive order represents the most significant federal cybersecurity directive in years. It mandates that all federal agencies transition to zero-trust security architectures within 18 months, a model that assumes no user or system should be automatically trusted. The order also requires critical infrastructure operators to report cyber incidents to CISA within 72 hours. + +Additionally, the order establishes a Cyber Safety Review Board modeled on the NTSB, which will investigate major cyber incidents and publish findings. Federal contractors handling sensitive data will face stricter security requirements in their contracts. + +# Impact & Implications +Federal employees will notice changes in how they access systems, with more frequent authentication checks and restricted access based on need-to-know principles. Companies in energy, healthcare, finance, and transportation sectors will need to invest in incident detection and reporting capabilities. The cybersecurity industry will see increased demand for zero-trust solutions and compliance consulting. + +# The Debate +Cybersecurity experts broadly support the order, though some question whether the 18-month timeline for zero-trust adoption is realistic. Industry groups worry about compliance costs, particularly for smaller operators. Privacy advocates praise the transparency measures but want stronger protections for the incident data that companies will be required to share with the government.`, + thumbnailUrl: "https://picsum.photos/seed/cyber/800/600", + images: [], + url: "https://www.whitehouse.gov/presidential-actions/executive-order-cybersecurity-2025/", + source: "whitehouse.gov", + }, + { + title: + "Memorandum on Modernizing Federal Student Loan Servicing", + type: "Memorandum", + publishedDate: daysAgo(8), + description: + "Directs the Department of Education to overhaul the federal student loan servicing system and improve borrower experience.", + fullText: + "MEMORANDUM FOR THE SECRETARY OF EDUCATION. SUBJECT: Modernizing Federal Student Loan Servicing. By the authority vested in me as President, I hereby direct the following actions to improve the federal student loan servicing system...", + aiGeneratedArticle: `# What This Means For You +If you have federal student loans, the government is overhauling the system you use to manage and repay them, with the goal of ending the billing errors and customer service nightmares that millions of borrowers have experienced. + +# Overview +This presidential memorandum directs the Department of Education to modernize the federal student loan servicing system from the ground up. The directive comes after years of complaints about billing errors, misapplied payments, and inadequate customer service from federal loan servicers. + +Key directives include building a single, unified borrower portal, establishing service level agreements with loan servicers that include financial penalties for errors, and creating an independent ombudsman office for borrower complaints. + +# Impact & Implications +The 43 million Americans with federal student loans could see significant improvements in their repayment experience. A unified portal would eliminate the confusion of dealing with multiple servicers. Financial penalties for servicer errors could reduce the billing mistakes that have cost borrowers billions in unnecessary interest charges. + +# The Debate +Borrower advocates welcome the reforms but question whether they go far enough without broader student loan relief. The loan servicing industry argues that many problems stem from the complexity of federal repayment programs rather than servicer negligence. Some lawmakers want to go further and bring loan servicing in-house at the Department of Education.`, + thumbnailUrl: "https://picsum.photos/seed/loans/800/600", + images: [], + url: "https://www.whitehouse.gov/presidential-actions/memorandum-student-loans-2025/", + source: "whitehouse.gov", + }, + { + title: "Proclamation on National Wildfire Preparedness Month", + type: "Proclamation", + publishedDate: daysAgo(5), + description: + "Declares May 2025 as National Wildfire Preparedness Month and calls on Americans to take steps to protect their communities.", + fullText: + "NOW, THEREFORE, I, the President of the United States of America, by virtue of the authority vested in me by the Constitution and the laws of the United States, do hereby proclaim May 2025 as National Wildfire Preparedness Month...", + aiGeneratedArticle: `# What This Means For You +If you live in a wildfire-prone area, this proclamation is a call to action to prepare your home and community for fire season, which experts predict will be particularly severe this year. + +# Overview +This proclamation designates May 2025 as National Wildfire Preparedness Month, highlighting the growing threat of wildfires across the American West and Southeast. The proclamation cites the record-breaking 2024 fire season, which burned over 8 million acres and caused $30 billion in damages. + +The proclamation directs FEMA to expand its community preparedness grants and calls on states to adopt updated building codes for wildfire-prone areas. It also announces a new federal-state partnership to create 100-foot defensible space zones around vulnerable communities. + +# Impact & Implications +Homeowners in fire-prone regions may see new building code requirements and incentives for fire-resistant landscaping. Federal preparedness grants could help fund community firebreaks and evacuation planning. The proclamation signals increased federal attention to wildfire as a growing national security issue driven by climate change. + +# The Debate +Fire scientists and emergency managers applaud the attention but say preparedness alone is insufficient without addressing the root causes of increasing wildfire severity, including climate change and decades of fire suppression. Some Western state officials bristle at federal building code recommendations, viewing them as overreach into local land use decisions.`, + thumbnailUrl: "https://picsum.photos/seed/wildfire/800/600", + images: [], + url: "https://www.whitehouse.gov/presidential-actions/proclamation-wildfire-2025/", + source: "whitehouse.gov", + }, + { + title: + "Statement on the Federal Reserve Interest Rate Decision", + type: "News Article", + publishedDate: daysAgo(3), + description: + "The White House responds to the Federal Reserve's decision to hold interest rates steady at its May 2025 meeting.", + fullText: + "The Federal Reserve announced today that it will maintain the federal funds rate at its current level. The White House issued the following statement...", + aiGeneratedArticle: `# What This Means For You +Interest rates on mortgages, car loans, and credit cards will stay roughly where they are for now. If you've been waiting for rates to drop before buying a home, you'll need to wait longer. + +# Overview +The Federal Reserve held its benchmark interest rate steady at 4.5-4.75% at its May 2025 meeting, citing persistent inflation in services and housing costs. The White House statement expressed confidence in the economy while noting that American families continue to face cost-of-living pressures. + +# Impact & Implications +Mortgage rates will likely remain near 6.5%, keeping the housing market sluggish. Savings account yields stay favorable for savers. Business borrowing costs remain elevated, which may slow hiring and expansion plans. + +# The Debate +The administration wants lower rates to boost the housing market and economic growth, but the Fed maintains its independence in pursuing its inflation mandate. Critics of the Fed say rates should have been cut already, while inflation hawks argue the hold is prudent given sticky price pressures.`, + thumbnailUrl: "https://picsum.photos/seed/fed-rates/800/600", + images: [], + url: "https://www.whitehouse.gov/briefing-room/statements/fed-rate-decision-may-2025/", + source: "whitehouse.gov", + }, +].map((g) => ({ + ...g, + contentHash: hash(g.title + (g.fullText ?? "")), + versions: [], +})); + +const courtCases = [ + { + caseNumber: "23-1234", + title: "United States v. Gonzalez", + court: "Supreme Court of the United States", + filedDate: daysAgo(90), + description: + "Whether the Fourth Amendment requires law enforcement to obtain a warrant before accessing historical cell-site location information spanning more than seven days.", + status: "Argued", + fullText: + "No. 23-1234. UNITED STATES, PETITIONER v. CARLOS GONZALEZ. ON WRIT OF CERTIORARI TO THE UNITED STATES COURT OF APPEALS FOR THE NINTH CIRCUIT. The question presented is whether the Fourth Amendment's warrant requirement, as articulated in Carpenter v. United States, extends to historical cell-site location information...", + aiGeneratedArticle: `# What This Means For You +The Supreme Court is deciding how much of your phone's location history the police can access without a judge's approval, building on a landmark 2018 case about digital privacy. + +# Overview +United States v. Gonzalez asks the Supreme Court to clarify the scope of its 2018 Carpenter decision, which held that accessing seven days of cell-site location information constitutes a Fourth Amendment search. The government argues that Carpenter's seven-day threshold should be a firm rule, while the defendant argues that any access to location data requires a warrant. + +The case arose when federal agents obtained 180 days of Gonzalez's cell-site location data through a court order under the Stored Communications Act, which has a lower standard than a traditional warrant. The Ninth Circuit suppressed the evidence, holding that Carpenter's logic extends beyond seven days. + +# Impact & Implications +A ruling expanding Carpenter could force law enforcement to obtain warrants for any cell-site data requests, significantly affecting how investigations of drug trafficking, kidnapping, and terrorism are conducted. For ordinary Americans, a broader ruling would strengthen privacy protections for the location data that cell phones constantly generate. + +# The Debate +Privacy advocates and civil liberties organizations argue that location data reveals intimate details of a person's life regardless of the time period. Law enforcement groups warn that a warrant requirement for all location data would impede time-sensitive investigations. Tech companies have filed mixed briefs — some supporting stronger privacy protections, others concerned about the compliance burden.`, + thumbnailUrl: "https://picsum.photos/seed/scotus1/800/600", + images: [], + url: "https://www.courtlistener.com/opinion/mock-gonzalez/", + }, + { + caseNumber: "24-567", + title: "National Federation of Teachers v. Department of Education", + court: "Supreme Court of the United States", + filedDate: daysAgo(60), + description: + "Whether the Department of Education exceeded its statutory authority in promulgating new Title IX regulations that expand the definition of sex-based discrimination.", + status: "Cert Granted", + fullText: + "No. 24-567. NATIONAL FEDERATION OF TEACHERS, et al., PETITIONERS v. DEPARTMENT OF EDUCATION. ON PETITION FOR A WRIT OF CERTIORARI TO THE UNITED STATES COURT OF APPEALS FOR THE SIXTH CIRCUIT...", + aiGeneratedArticle: `# What This Means For You +The Supreme Court will decide whether the Department of Education went too far in rewriting Title IX rules that govern how schools handle discrimination complaints — a decision that could affect every public school and university in the country. + +# Overview +This case challenges the Department of Education's 2024 Title IX regulations, which expanded the definition of sex-based discrimination to include gender identity and sexual orientation. The National Federation of Teachers and several state attorneys general argue that the Department exceeded the authority Congress granted it under Title IX. + +The Sixth Circuit upheld the regulations, finding that the Department's interpretation was a reasonable exercise of its rulemaking authority. The Supreme Court granted certiorari to resolve a circuit split, as the Fifth Circuit reached the opposite conclusion in a separate challenge. + +# Impact & Implications +The ruling could reshape how schools handle discrimination complaints and could set broader precedent for the limits of executive agency rulemaking power. Schools and universities would need to adjust their policies based on the outcome. The decision could also affect other areas where agencies have expanded statutory definitions through regulation. + +# The Debate +Supporters of the regulations argue that Title IX's broad language was always intended to evolve with society's understanding of discrimination. Opponents counter that such a significant policy change should come from Congress, not an executive agency. The case has become a flashpoint in broader debates about executive power, gender identity, and education policy.`, + thumbnailUrl: "https://picsum.photos/seed/scotus2/800/600", + images: [], + url: "https://www.courtlistener.com/opinion/mock-nft-v-doe/", + }, + { + caseNumber: "24-890", + title: "TechCorp Inc. v. California", + court: "Supreme Court of the United States", + filedDate: daysAgo(40), + description: + "Whether a state law requiring algorithmic transparency for social media platforms violates the First Amendment rights of platform operators.", + status: "Briefing", + fullText: + "No. 24-890. TECHCORP INC., PETITIONER v. STATE OF CALIFORNIA. ON WRIT OF CERTIORARI TO THE SUPREME COURT OF CALIFORNIA. The question presented is whether California's Algorithmic Transparency Act, which requires social media platforms to disclose the factors used in content recommendation algorithms...", + aiGeneratedArticle: `# What This Means For You +Can your state force social media companies to explain why their algorithms show you the content they show you? The Supreme Court is about to decide. + +# Overview +TechCorp Inc. v. California tests whether states can require social media platforms to disclose how their recommendation algorithms work. California's Algorithmic Transparency Act requires platforms with over 10 million users to publish detailed descriptions of the factors their algorithms use to rank and recommend content. + +TechCorp argues that its algorithm is protected editorial judgment under the First Amendment, similar to a newspaper's decisions about which stories to feature. California counters that the law is a reasonable commercial disclosure requirement that doesn't compel speech but merely requires transparency about existing practices. + +# Impact & Implications +A ruling for California could open the door to algorithmic regulation nationwide, giving users unprecedented insight into why they see certain content. A ruling for TechCorp could shield algorithms from government scrutiny and embolden platforms to resist disclosure requirements. The decision will likely shape the future of tech regulation for years to come. + +# The Debate +Free speech advocates are split: some see algorithmic curation as protected expression, while others argue that monopolistic platforms wield too much power over public discourse to claim editorial immunity. Tech companies warn that disclosure could expose trade secrets and make algorithms vulnerable to manipulation. Consumer advocates and regulators argue that transparency is essential for accountability.`, + thumbnailUrl: "https://picsum.photos/seed/scotus3/800/600", + images: [], + url: "https://www.courtlistener.com/opinion/mock-techcorp-v-ca/", + }, +].map((c) => ({ + ...c, + contentHash: hash(c.title + (c.fullText ?? "")), + versions: [], +})); + +async function seed() { + console.log("Seeding database...\n"); + + console.log("Inserting bills..."); + const insertedBills = await db + .insert(Bill) + .values(bills) + .onConflictDoNothing() + .returning({ id: Bill.id, title: Bill.title, contentHash: Bill.contentHash }); + console.log(` ${insertedBills.length} bills inserted`); + + console.log("Inserting government content..."); + const insertedGov = await db + .insert(GovernmentContent) + .values(govContent) + .onConflictDoNothing() + .returning({ + id: GovernmentContent.id, + title: GovernmentContent.title, + contentHash: GovernmentContent.contentHash, + }); + console.log(` ${insertedGov.length} government content items inserted`); + + console.log("Inserting court cases..."); + const insertedCases = await db + .insert(CourtCase) + .values(courtCases) + .onConflictDoNothing() + .returning({ + id: CourtCase.id, + title: CourtCase.title, + contentHash: CourtCase.contentHash, + }); + console.log(` ${insertedCases.length} court cases inserted`); + + console.log("Inserting videos (feed items)..."); + const videoRecords = [ + ...insertedBills.map((b, i) => ({ + contentType: "bill" as const, + contentId: b.id, + title: bills[i]!.title.slice(0, 25), + description: bills[i]!.description!, + thumbnailUrl: bills[i]!.thumbnailUrl, + author: "congress.gov", + engagementMetrics: { + likes: Math.floor(1000 + i * 2345), + comments: Math.floor(100 + i * 234), + shares: Math.floor(50 + i * 123), + }, + sourceContentHash: b.contentHash, + })), + ...insertedGov.map((g, i) => ({ + contentType: "government_content" as const, + contentId: g.id, + title: govContent[i]!.title.slice(0, 25), + description: govContent[i]!.description!, + thumbnailUrl: govContent[i]!.thumbnailUrl, + author: govContent[i]!.source, + engagementMetrics: { + likes: Math.floor(2000 + i * 1567), + comments: Math.floor(200 + i * 345), + shares: Math.floor(100 + i * 234), + }, + sourceContentHash: g.contentHash, + })), + ...insertedCases.map((c, i) => ({ + contentType: "court_case" as const, + contentId: c.id, + title: courtCases[i]!.title.slice(0, 25), + description: courtCases[i]!.description!, + thumbnailUrl: courtCases[i]!.thumbnailUrl, + author: "courtlistener.com", + engagementMetrics: { + likes: Math.floor(3000 + i * 1234), + comments: Math.floor(300 + i * 456), + shares: Math.floor(150 + i * 345), + }, + sourceContentHash: c.contentHash, + })), + ]; + + if (videoRecords.length === 0) { + console.log(" 0 videos inserted (no new content to link)"); + } else { + const insertedVideos = await db + .insert(Video) + .values(videoRecords) + .onConflictDoNothing() + .returning({ id: Video.id }); + console.log(` ${insertedVideos.length} videos inserted`); + } + + console.log( + `\nDone! Seeded ${insertedBills.length} bills, ${insertedGov.length} gov content, ${insertedCases.length} court cases, ${videoRecords.length} videos.`, + ); + process.exit(0); +} + +seed().catch((err) => { + console.error("Seed failed:", err); + process.exit(1); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6ebc68..61c63ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,14 +58,10 @@ catalogs: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3 - react: - specifier: ^19.2.4 - version: 19.2.4 - react-dom: - specifier: ^19.2.4 - version: 19.2.4 overrides: + react: 19.0.0 + react-dom: 19.0.0 '@types/minimatch': 5.1.2 esbuild@<=0.24.2: '>=0.25.0' file-type@>=13.0.0 <21.3.1: '>=21.3.1' @@ -143,6 +139,9 @@ importers: '@legendapp/list': specifier: ^2.0.19 version: 2.0.19(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0) + '@react-native-async-storage/async-storage': + specifier: ^3.1.0 + version: 3.1.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0) '@ronradtke/react-native-markdown-display': specifier: ^8.1.0 version: 8.1.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0) @@ -219,10 +218,10 @@ importers: specifier: 5.0.0-preview.3 version: 5.0.0-preview.3(react-native-css@3.0.6(@expo/metro-config@55.0.13(bufferutil@4.1.0)(expo@53.0.27)(typescript@6.0.2)(utf-8-validate@6.0.4))(lightningcss@1.32.0)(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0))(tailwindcss@4.2.2) react: - specifier: ^19.0.0 + specifier: 19.0.0 version: 19.0.0 react-dom: - specifier: ^19.0.0 + specifier: 19.0.0 version: 19.0.0(react@19.0.0) react-native: specifier: ~0.79.6 @@ -251,6 +250,9 @@ importers: superjson: specifier: 2.2.6 version: 2.2.6 + use-latest-callback: + specifier: ^0.3.4 + version: 0.3.4(react@19.0.0) devDependencies: '@acme/api': specifier: workspace:* @@ -308,10 +310,10 @@ importers: version: 0.13.11(arktype@2.1.20)(typescript@6.0.2)(valibot@1.0.0-beta.15(typescript@6.0.2))(zod@4.3.6) '@tanstack/react-form': specifier: 'catalog:' - version: 1.28.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 1.28.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@tanstack/react-query': specifier: 'catalog:' - version: 5.95.2(react@19.2.4) + version: 5.95.2(react@19.0.0) '@trpc/client': specifier: 'catalog:' version: 11.16.0(@trpc/server@11.16.0(typescript@6.0.2))(typescript@6.0.2) @@ -320,19 +322,19 @@ importers: version: 11.16.0(typescript@6.0.2) '@trpc/tanstack-react-query': specifier: 'catalog:' - version: 11.16.0(@tanstack/react-query@5.95.2(react@19.2.4))(@trpc/client@11.16.0(@trpc/server@11.16.0(typescript@6.0.2))(typescript@6.0.2))(@trpc/server@11.16.0(typescript@6.0.2))(react@19.2.4)(typescript@6.0.2) + version: 11.16.0(@tanstack/react-query@5.95.2(react@19.0.0))(@trpc/client@11.16.0(@trpc/server@11.16.0(typescript@6.0.2))(typescript@6.0.2))(@trpc/server@11.16.0(typescript@6.0.2))(react@19.0.0)(typescript@6.0.2) better-auth: specifier: 'catalog:' - version: 1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(better-sqlite3@12.8.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0))(mysql2@3.11.3)(next@16.2.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@5.22.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(better-sqlite3@12.8.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0))(mysql2@3.11.3)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(pg@8.20.0)(prisma@5.22.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: ^16.2.1 - version: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: - specifier: catalog:react19 - version: 19.2.4 + specifier: 19.0.0 + version: 19.0.0 react-dom: - specifier: catalog:react19 - version: 19.2.4(react@19.2.4) + specifier: 19.0.0 + version: 19.0.0(react@19.0.0) superjson: specifier: 2.2.6 version: 2.2.6 @@ -406,6 +408,9 @@ importers: p-limit: specifier: ^7.3.0 version: 7.3.0 + playwright: + specifier: ^1.58.2 + version: 1.58.2 sharp: specifier: ^0.34.5 version: 0.34.5 @@ -488,16 +493,16 @@ importers: version: 0.13.11(arktype@2.1.20)(typescript@6.0.2)(valibot@1.0.0-beta.15(typescript@6.0.2))(zod@4.3.6) better-auth: specifier: 'catalog:' - version: 1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(better-sqlite3@12.8.0)(drizzle-kit@0.31.10)(drizzle-orm@0.41.0(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0))(mysql2@3.11.3)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@5.22.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(better-sqlite3@12.8.0)(drizzle-kit@0.31.10)(drizzle-orm@0.41.0(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0))(mysql2@3.11.3)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(pg@8.20.0)(prisma@5.22.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: ^16.2.1 - version: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: - specifier: catalog:react19 - version: 19.2.4 + specifier: 19.0.0 + version: 19.0.0 react-dom: - specifier: catalog:react19 - version: 19.2.4(react@19.2.4) + specifier: 19.0.0 + version: 19.0.0(react@19.0.0) zod: specifier: 'catalog:' version: 4.3.6 @@ -513,7 +518,7 @@ importers: version: link:../../tooling/typescript '@better-auth/cli': specifier: 'catalog:' - version: 1.4.21(@better-fetch/fetch@1.1.21)(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-call@1.3.2(zod@4.3.6))(drizzle-kit@0.31.10)(gel@2.0.0)(jose@6.2.2)(kysely@0.28.14)(mysql2@3.11.3)(nanostores@1.2.0)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(postgres@3.4.4)(prisma@5.22.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 1.4.21(@better-fetch/fetch@1.1.21)(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-call@1.3.2(zod@4.3.6))(drizzle-kit@0.31.10)(gel@2.0.0)(jose@6.2.2)(kysely@0.28.14)(mysql2@3.11.3)(nanostores@1.2.0)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(postgres@3.4.4)(prisma@5.22.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@types/react': specifier: catalog:react19 version: 19.2.14 @@ -557,6 +562,9 @@ importers: '@types/pg': specifier: ^8.20.0 version: 8.20.0 + dotenv-cli: + specifier: ^11.0.0 + version: 11.0.0 drizzle-kit: specifier: ^0.31.10 version: 0.31.10 @@ -566,6 +574,9 @@ importers: prettier: specifier: 'catalog:' version: 3.8.1 + tsx: + specifier: ^4.21.0 + version: 4.21.0 typescript: specifier: 'catalog:' version: 6.0.2 @@ -574,22 +585,22 @@ importers: dependencies: '@radix-ui/react-icons': specifier: ^1.3.2 - version: 1.3.2(react@19.2.4) + version: 1.3.2(react@19.0.0) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 next-themes: specifier: ^0.4.6 - version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 0.4.6(react-dom@19.2.4(react@19.0.0))(react@19.0.0) radix-ui: specifier: ^1.4.3 - version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) react-native: specifier: '*' - version: 0.81.4(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4) + version: 0.81.4(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.0.0) sonner: specifier: ^2.0.7 - version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 2.0.7(react-dom@19.2.4(react@19.0.0))(react@19.0.0) tailwind-merge: specifier: ^3.5.0 version: 3.5.0 @@ -613,8 +624,8 @@ importers: specifier: 'catalog:' version: 3.8.1 react: - specifier: catalog:react19 - version: 19.2.4 + specifier: 19.0.0 + version: 19.0.0 typescript: specifier: 'catalog:' version: 6.0.2 @@ -1921,7 +1932,7 @@ packages: resolution: {integrity: sha512-bY4/rfcZ0f43DvOtMn8/kmPlmo01tex5hRoc5hKbwBwQjqWQuQt0ACwu7akR9IHI4j0WNG48eL6cZB6dZUFrzg==} peerDependencies: expo: '*' - react: '*' + react: 19.0.0 react-native: '*' '@expo/env@1.0.7': @@ -1964,8 +1975,8 @@ packages: resolution: {integrity: sha512-H5ZFj7nisMJ5a4joMGpF4Xt/m4hWDAroMNv5ld/2iniWoXLvNt+YQpMdyecu/lHpydKAjHzXcyE08hTGgURaIA==} peerDependencies: expo: '*' - react: '*' - react-dom: '*' + react: 19.0.0 + react-dom: 19.0.0 react-native: '*' peerDependenciesMeta: react-dom: @@ -2018,7 +2029,7 @@ packages: resolution: {integrity: sha512-7T09UE9h8QDTsUeMGymB4i+iqvtEeaO5VvUjryFB4tugDTG/bkzViWA74hm5pfjjDEhYMXWaX112mcvhccmIwQ==} peerDependencies: expo-font: '*' - react: '*' + react: 19.0.0 react-native: '*' '@expo/ws-tunnel@1.0.6': @@ -2037,8 +2048,8 @@ packages: '@floating-ui/react-dom@2.1.8': resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' + react: 19.0.0 + react-dom: 19.0.0 '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} @@ -2421,7 +2432,7 @@ packages: '@legendapp/list@2.0.19': resolution: {integrity: sha512-zDWg8yg0smKxxk+M7gwAbZAnf5uczohPA+IjqLSkImz7+e9ytxeT0Mq35RBO9RTKODOXfV/aIgm1uqUHLBEdmg==} peerDependencies: - react: '*' + react: 19.0.0 react-native: '*' '@mixmark-io/domino@2.2.0': @@ -2669,8 +2680,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2682,8 +2693,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2695,8 +2706,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2708,8 +2719,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2721,8 +2732,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2734,8 +2745,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2747,8 +2758,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2760,8 +2771,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2773,8 +2784,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2785,7 +2796,7 @@ packages: resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2795,8 +2806,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2807,7 +2818,7 @@ packages: resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2817,8 +2828,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2829,7 +2840,7 @@ packages: resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2839,8 +2850,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2852,8 +2863,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2864,7 +2875,7 @@ packages: resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2874,8 +2885,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2887,8 +2898,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2900,8 +2911,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2911,13 +2922,13 @@ packages: '@radix-ui/react-icons@1.3.2': resolution: {integrity: sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==} peerDependencies: - react: ^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc + react: 19.0.0 '@radix-ui/react-id@1.1.1': resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2927,8 +2938,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2940,8 +2951,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2953,8 +2964,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2966,8 +2977,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2979,8 +2990,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -2992,8 +3003,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3005,8 +3016,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3018,8 +3029,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3031,8 +3042,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3044,8 +3055,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3057,8 +3068,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3070,8 +3081,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3083,8 +3094,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3096,8 +3107,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3109,8 +3120,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3122,8 +3133,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3135,8 +3146,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3148,8 +3159,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3160,7 +3171,7 @@ packages: resolution: {integrity: sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3169,7 +3180,7 @@ packages: resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3179,8 +3190,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3192,8 +3203,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3205,8 +3216,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3218,8 +3229,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3231,8 +3242,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3244,8 +3255,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3257,8 +3268,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3269,7 +3280,7 @@ packages: resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3278,7 +3289,7 @@ packages: resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3287,7 +3298,7 @@ packages: resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3296,7 +3307,7 @@ packages: resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3305,7 +3316,7 @@ packages: resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3314,7 +3325,7 @@ packages: resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3323,7 +3334,7 @@ packages: resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3332,7 +3343,7 @@ packages: resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3341,7 +3352,7 @@ packages: resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3351,8 +3362,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -3362,6 +3373,12 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@react-native-async-storage/async-storage@3.1.0': + resolution: {integrity: sha512-ENwbn3kj/dOapdR3GVgfX5x9f9/rxHnsqol1bUkt26lrv0aAq6UoXnTlv7y2dT7RH0AShh9B1+KAPDVjJXnk0w==} + peerDependencies: + react: 19.0.0 + react-native: '*' + '@react-native/assets-registry@0.79.6': resolution: {integrity: sha512-UVSP1224PWg0X+mRlZNftV5xQwZGfawhivuW8fGgxNK9MS/U84xZ+16lkqcPh1ank6MOt239lIWHQ1S33CHgqA==} engines: {node: '>=18'} @@ -3489,7 +3506,7 @@ packages: engines: {node: '>=18'} peerDependencies: '@types/react': ^19.0.0 - react: '*' + react: 19.0.0 react-native: '*' peerDependenciesMeta: '@types/react': @@ -3500,7 +3517,7 @@ packages: engines: {node: '>= 20.19.4'} peerDependencies: '@types/react': ^19.1.0 - react: '*' + react: 19.0.0 react-native: '*' peerDependenciesMeta: '@types/react': @@ -3510,7 +3527,7 @@ packages: resolution: {integrity: sha512-Ou28A1aZLj5wiFQ3F93aIsrI4NCwn3IJzkkjNo9KLFXsc0Yks+UqrVaFlffHFLsrbajuGRG/OQpnMA1ljayY5Q==} peerDependencies: '@react-navigation/native': ^7.2.2 - react: '>= 18.2.0' + react: 19.0.0 react-native: '*' react-native-safe-area-context: '>= 4.0.0' react-native-screens: '>= 4.0.0' @@ -3518,14 +3535,14 @@ packages: '@react-navigation/core@7.17.2': resolution: {integrity: sha512-Rt2OZwcgOmjv401uLGAKaRM6xo0fiBce/A7LfRHI1oe5FV+KooWcgAoZ2XOtgKj6UzVMuQWt3b2e6rxo/mDJRA==} peerDependencies: - react: '>= 18.2.0' + react: 19.0.0 '@react-navigation/elements@2.9.14': resolution: {integrity: sha512-lKqzu+su2pI/YIZmR7L7xdOs4UL+rVXKJAMpRMBrwInEy96SjIFst6QDGpE89Dunnu3VjVpjWfByo9f2GWBHDQ==} peerDependencies: '@react-native-masked-view/masked-view': '>= 0.2.0' '@react-navigation/native': ^7.2.2 - react: '>= 18.2.0' + react: 19.0.0 react-native: '*' react-native-safe-area-context: '>= 4.0.0' peerDependenciesMeta: @@ -3536,7 +3553,7 @@ packages: resolution: {integrity: sha512-mCbYbYhi7Em2R2nEgwYGdLU38smy+KK+HMMVcwuzllWsF3Qb+jOUEYbB6Or7LvE7SS77BZ6sHdx4HptCEv50hQ==} peerDependencies: '@react-navigation/native': ^7.2.2 - react: '>= 18.2.0' + react: 19.0.0 react-native: '*' react-native-safe-area-context: '>= 4.0.0' react-native-screens: '>= 4.0.0' @@ -3544,7 +3561,7 @@ packages: '@react-navigation/native@7.2.2': resolution: {integrity: sha512-kem1Ko2BcbAjmbQIv66dNmr6EtfDut3QU0qjsVhMnLLhktwyXb6FzZYp8gTrUb6AvkAbaJoi+BF5Pl55pAUa5w==} peerDependencies: - react: '>= 18.2.0' + react: 19.0.0 react-native: '*' '@react-navigation/routers@7.5.3': @@ -3553,7 +3570,7 @@ packages: '@ronradtke/react-native-markdown-display@8.1.0': resolution: {integrity: sha512-pAtefWI76vpkxsEgIFivyq1q6ej8rDyR7oVM/cWAxUydyBej9LOvULjLAeFuFLbYAelHTNoYXmGxQOlFLBa0+w==} peerDependencies: - react: '>=16.2.0' + react: 19.0.0 react-native: '>=0.50.4' '@rtsao/scc@1.1.0': @@ -3715,7 +3732,7 @@ packages: resolution: {integrity: sha512-CL8IeWkeXnEEDsHt5wBuIOZvSYrKiLRtsC9ca0LzfJJ22SYCma9cBmh1UX1EBX0o3gH2U21PmUf+y5f9OJNoEQ==} peerDependencies: '@tanstack/react-start': '*' - react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react: 19.0.0 peerDependenciesMeta: '@tanstack/react-start': optional: true @@ -3723,13 +3740,13 @@ packages: '@tanstack/react-query@5.95.2': resolution: {integrity: sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA==} peerDependencies: - react: ^18 || ^19 + react: 19.0.0 '@tanstack/react-store@0.9.3': resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: 19.0.0 + react-dom: 19.0.0 '@tanstack/store@0.9.3': resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} @@ -3754,7 +3771,7 @@ packages: '@tanstack/react-query': ^5.80.3 '@trpc/client': 11.16.0 '@trpc/server': 11.16.0 - react: '>=18.2.0' + react: 19.0.0 typescript: '>=5.7.2' '@tsconfig/node10@1.0.12': @@ -4251,8 +4268,8 @@ packages: next: ^14.0.0 || ^15.0.0 || ^16.0.0 pg: ^8.0.0 prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 + react: 19.0.0 + react-dom: 19.0.0 solid-js: ^1.0.0 svelte: ^4.0.0 || ^5.0.0 vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 @@ -4313,8 +4330,8 @@ packages: next: ^14.0.0 || ^15.0.0 || ^16.0.0 pg: ^8.0.0 prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 + react: 19.0.0 + react-dom: 19.0.0 solid-js: ^1.0.0 svelte: ^4.0.0 || ^5.0.0 vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 @@ -5312,14 +5329,14 @@ packages: resolution: {integrity: sha512-b5P8GpjUh08fRCf6m5XPVAh7ra42cQrHBIMgH2UXP+xsj4Wufl6pLy6jRF5w6U7DranUMbsXm8TOyq4EHy7ADg==} peerDependencies: expo: '*' - react: '*' + react: 19.0.0 react-native: '*' expo-blur@14.1.5: resolution: {integrity: sha512-CCLJHxN4eoAl06ESKT3CbMasJ98WsjF9ZQEJnuxtDb9ffrYbZ+g9ru84fukjNUOTtc8A8yXE5z8NgY1l0OMrmQ==} peerDependencies: expo: '*' - react: '*' + react: 19.0.0 react-native: '*' expo-build-properties@0.14.8: @@ -5366,13 +5383,13 @@ packages: resolution: {integrity: sha512-wUlMdpqURmQ/CNKK/+BIHkDA5nGjMqNlYmW0pJFXY/KE/OG80Qcavdu2sHsL4efAIiNGvYdBS10WztuQYU4X0A==} peerDependencies: expo: '*' - react: '*' + react: 19.0.0 expo-image@2.4.1: resolution: {integrity: sha512-yHp0Cy4ylOYyLR21CcH6i70DeRyLRPc0yAIPFPn4BT/BpkJNaX5QMXDppcHa58t4WI3Bb8QRJRLuAQaeCtDF8A==} peerDependencies: expo: '*' - react: '*' + react: 19.0.0 react-native: '*' react-native-web: '*' peerDependenciesMeta: @@ -5386,19 +5403,19 @@ packages: resolution: {integrity: sha512-wU9qOnosy4+U4z/o4h8W9PjPvcFMfZXrlUoKTMBW7F4pLqhkkP/5G4EviPZixv4XWFMjn1ExQ5rV6BX8GwJsWA==} peerDependencies: expo: '*' - react: '*' + react: 19.0.0 expo-linear-gradient@14.1.5: resolution: {integrity: sha512-BSN3MkSGLZoHMduEnAgfhoj3xqcDWaoICgIr4cIYEx1GcHfKMhzA/O4mpZJ/WC27BP1rnAqoKfbclk1eA70ndQ==} peerDependencies: expo: '*' - react: '*' + react: 19.0.0 react-native: '*' expo-linking@7.1.7: resolution: {integrity: sha512-ZJaH1RIch2G/M3hx2QJdlrKbYFUTOjVVW4g39hfxrE5bPX9xhZUYXqxqQtzMNl1ylAevw9JkgEfWbBWddbZ3UA==} peerDependencies: - react: '*' + react: 19.0.0 react-native: '*' expo-manifests@0.16.6: @@ -5417,7 +5434,7 @@ packages: resolution: {integrity: sha512-VNgxNe3Y1xo00zMzFy7Q+35qWnSJnjZ9RRLtW3Nu/ITtv9ak+BIghfWj1PANLYB3ZkWzY5656R1YIkkRkeDukg==} peerDependencies: expo: '*' - react: '*' + react: 19.0.0 expo-router@5.1.11: resolution: {integrity: sha512-6YQGqQM2rviVSiU6++hrJDPMByHZ7Oiux4XmgoSaGdaHku5QOn9911f2puEUZh2H9ALKBipw5v3ZkrECBd6Zbw==} @@ -5454,7 +5471,7 @@ packages: expo-status-bar@2.2.3: resolution: {integrity: sha512-+c8R3AESBoduunxTJ8353SqKAKpxL6DvcD8VKBuh81zzJyUUbfB4CVjr1GufSJEKsMzNPXZU+HJwXx7Xh7lx8Q==} peerDependencies: - react: '*' + react: 19.0.0 react-native: '*' expo-structured-headers@4.1.0: @@ -5480,7 +5497,7 @@ packages: hasBin: true peerDependencies: expo: '*' - react: '*' + react: 19.0.0 expo-web-browser@14.2.0: resolution: {integrity: sha512-6S51d8pVlDRDsgGAp8BPpwnxtyKiMWEFdezNz+5jVIyT+ctReW42uxnjRgtsdn5sXaqzhaX+Tzk/CWaKCyC0hw==} @@ -5494,7 +5511,7 @@ packages: peerDependencies: '@expo/dom-webview': '*' '@expo/metro-runtime': '*' - react: '*' + react: 19.0.0 react-native: '*' react-native-webview: '*' peerDependenciesMeta: @@ -5835,6 +5852,9 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + idb@8.0.3: + resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -6344,8 +6364,8 @@ packages: lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} - lodash@4.17.23: - resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} log-symbols@2.2.0: resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} @@ -6593,8 +6613,8 @@ packages: next-themes@0.4.6: resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} peerDependencies: - react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 next@16.2.1: resolution: {integrity: sha512-VaChzNL7o9rbfdt60HUj8tev4m6d7iC1igAy157526+cJlXOQu5LzsBXNT+xaJnTP/k+utSX5vMv7m0G+zKH+Q==} @@ -6604,8 +6624,8 @@ packages: '@opentelemetry/api': ^1.1.0 '@playwright/test': ^1.51.1 babel-plugin-react-compiler: '*' - react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react: 19.0.0 + react-dom: 19.0.0 sass: ^1.3.0 peerDependenciesMeta: '@opentelemetry/api': @@ -7121,8 +7141,8 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -7146,12 +7166,12 @@ packages: react-dom@19.0.0: resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} peerDependencies: - react: ^19.0.0 + react: 19.0.0 react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: - react: ^19.2.4 + react: 19.0.0 react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} @@ -7160,7 +7180,7 @@ packages: resolution: {integrity: sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==} engines: {node: '>=10'} peerDependencies: - react: '>=17.0.0' + react: 19.0.0 react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -7176,13 +7196,13 @@ packages: peerDependencies: '@expo/metro-config': ~0.20.18 lightningcss: 1.30.1 - react: '>=19' + react: 19.0.0 react-native: '>=0.81' react-native-edge-to-edge@1.6.0: resolution: {integrity: sha512-2WCNdE3Qd6Fwg9+4BpbATUxCLcouF6YRY7K+J36KJ4l3y+tWN6XCqAC4DuoGblAAbb2sLkhEDp4FOlbOIot2Og==} peerDependencies: - react: '*' + react: 19.0.0 react-native: '*' react-native-fit-image@1.5.5: @@ -7191,51 +7211,51 @@ packages: react-native-gesture-handler@2.24.0: resolution: {integrity: sha512-ZdWyOd1C8axKJHIfYxjJKCcxjWEpUtUWgTOVY2wynbiveSQDm8X/PDyAKXSer/GOtIpjudUbACOndZXCN3vHsw==} peerDependencies: - react: '*' + react: 19.0.0 react-native: '*' react-native-is-edge-to-edge@1.1.7: resolution: {integrity: sha512-EH6i7E8epJGIcu7KpfXYXiV2JFIYITtq+rVS8uEb+92naMRBdxhTuS8Wn2Q7j9sqyO0B+Xbaaf9VdipIAmGW4w==} peerDependencies: - react: '*' + react: 19.0.0 react-native: '*' react-native-is-edge-to-edge@1.3.1: resolution: {integrity: sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA==} peerDependencies: - react: '*' + react: 19.0.0 react-native: '*' react-native-reanimated@3.17.5: resolution: {integrity: sha512-SxBK7wQfJ4UoWoJqQnmIC7ZjuNgVb9rcY5Xc67upXAFKftWg0rnkknTw6vgwnjRcvYThrjzUVti66XoZdDJGtw==} peerDependencies: '@babel/core': ^7.0.0-0 - react: '*' + react: 19.0.0 react-native: '*' react-native-safe-area-context@5.4.0: resolution: {integrity: sha512-JaEThVyJcLhA+vU0NU8bZ0a1ih6GiF4faZ+ArZLqpYbL6j7R3caRqj+mE3lEtKCuHgwjLg3bCxLL1GPUJZVqUA==} peerDependencies: - react: '*' + react: 19.0.0 react-native: '*' react-native-screens@4.11.1: resolution: {integrity: sha512-F0zOzRVa3ptZfLpD0J8ROdo+y1fEPw+VBFq1MTY/iyDu08al7qFUO5hLMd+EYMda5VXGaTFCa8q7bOppUszhJw==} peerDependencies: - react: '*' + react: 19.0.0 react-native: '*' react-native-svg@15.11.2: resolution: {integrity: sha512-+YfF72IbWQUKzCIydlijV1fLuBsQNGMT6Da2kFlo1sh+LE3BIm/2Q7AR1zAAR6L0BFLi1WaQPLfFUC9bNZpOmw==} peerDependencies: - react: '*' + react: 19.0.0 react-native: '*' react-native-web@0.20.0: resolution: {integrity: sha512-OOSgrw+aON6R3hRosCau/xVxdLzbjEcsLysYedka0ZON4ZZe6n9xgeN9ZkoejhARM36oTlUgHIQqxGutEJ9Wxg==} peerDependencies: - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 + react: 19.0.0 + react-dom: 19.0.0 react-native@0.79.6: resolution: {integrity: sha512-kvIWSmf4QPfY41HC25TR285N7Fv0Pyn3DAEK8qRL9dA35usSaxsJkHfw+VqnonqJjXOaoKCEanwudRAJ60TBGA==} @@ -7243,7 +7263,7 @@ packages: hasBin: true peerDependencies: '@types/react': ^19.0.0 - react: ^19.0.0 + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -7254,7 +7274,7 @@ packages: hasBin: true peerDependencies: '@types/react': ^19.1.0 - react: ^19.1.0 + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -7268,7 +7288,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -7278,7 +7298,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -7288,7 +7308,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -7297,10 +7317,6 @@ packages: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} - react@19.2.4: - resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} - engines: {node: '>=0.10.0'} - readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -7579,8 +7595,8 @@ packages: sonner@2.0.7: resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} peerDependencies: - react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc - react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react: 19.0.0 + react-dom: 19.0.0 source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} @@ -7714,7 +7730,7 @@ packages: peerDependencies: '@babel/core': '*' babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + react: 19.0.0 peerDependenciesMeta: '@babel/core': optional: true @@ -7976,7 +7992,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -7984,14 +8000,19 @@ packages: use-latest-callback@0.2.6: resolution: {integrity: sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==} peerDependencies: - react: '>=16.8' + react: 19.0.0 + + use-latest-callback@0.3.4: + resolution: {integrity: sha512-IcR5xK/dJFzUUsAKBqr/mZw4dGPftRdVvx5+cNsLzDlf7V1GPNfnRTjiuTxSsfPtUJGW/KNrScl7xwQoOsvhkg==} + peerDependencies: + react: 19.0.0 use-sidecar@1.1.3: resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} engines: {node: '>=10'} peerDependencies: '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react: 19.0.0 peerDependenciesMeta: '@types/react': optional: true @@ -7999,7 +8020,7 @@ packages: use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: 19.0.0 utf-8-validate@6.0.4: resolution: {integrity: sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==} @@ -8933,7 +8954,7 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@better-auth/cli@1.4.21(@better-fetch/fetch@1.1.21)(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-call@1.3.2(zod@4.3.6))(drizzle-kit@0.31.10)(gel@2.0.0)(jose@6.2.2)(kysely@0.28.14)(mysql2@3.11.3)(nanostores@1.2.0)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(postgres@3.4.4)(prisma@5.22.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@better-auth/cli@1.4.21(@better-fetch/fetch@1.1.21)(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/better-sqlite3@7.6.13)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-call@1.3.2(zod@4.3.6))(drizzle-kit@0.31.10)(gel@2.0.0)(jose@6.2.2)(kysely@0.28.14)(mysql2@3.11.3)(nanostores@1.2.0)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(postgres@3.4.4)(prisma@5.22.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@babel/core': 7.29.0 '@babel/preset-react': 7.28.5(@babel/core@7.29.0) @@ -8945,7 +8966,7 @@ snapshots: '@mrleebo/prisma-ast': 0.13.1 '@prisma/client': 5.22.0(prisma@5.22.0) '@types/pg': 8.20.0 - better-auth: 1.4.21(@prisma/client@5.22.0(prisma@5.22.0))(better-sqlite3@12.8.0)(drizzle-kit@0.31.10)(drizzle-orm@0.41.0(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0))(mysql2@3.11.3)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@5.22.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + better-auth: 1.4.21(@prisma/client@5.22.0(prisma@5.22.0))(better-sqlite3@12.8.0)(drizzle-kit@0.31.10)(drizzle-orm@0.41.0(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0))(mysql2@3.11.3)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(pg@8.20.0)(prisma@5.22.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) better-sqlite3: 12.8.0 c12: 3.3.3 chalk: 4.1.2 @@ -9114,12 +9135,12 @@ snapshots: dependencies: '@chevrotain/gast': 10.5.0 '@chevrotain/types': 10.5.0 - lodash: 4.17.23 + lodash: 4.18.1 '@chevrotain/gast@10.5.0': dependencies: '@chevrotain/types': 10.5.0 - lodash: 4.17.23 + lodash: 4.18.1 '@chevrotain/types@10.5.0': {} @@ -9776,11 +9797,11 @@ snapshots: '@floating-ui/core': 1.7.5 '@floating-ui/utils': 0.2.11 - '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@floating-ui/dom': 1.7.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) '@floating-ui/utils@0.2.11': {} @@ -10322,117 +10343,117 @@ snapshots: '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-accessible-icon@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-aspect-ratio@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) @@ -10443,443 +10464,443 @@ snapshots: optionalDependencies: '@types/react': 19.1.17 - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.0.0)': dependencies: - react: 19.2.4 + react: 19.0.0 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.0.0)': dependencies: - react: 19.2.4 + react: 19.0.0 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) aria-hidden: 1.2.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.0.0)': dependencies: - react: 19.2.4 + react: 19.0.0 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.0.0)': dependencies: - react: 19.2.4 + react: 19.0.0 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-form@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-icons@1.3.2(react@19.2.4)': + '@radix-ui/react-icons@1.3.2(react@19.0.0)': dependencies: - react: 19.2.4 + react: 19.0.0 - '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.0.0)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-label@2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.0.0) aria-hidden: 1.2.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-menubar@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-one-time-password-field@0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-password-toggle-field@0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) aria-hidden: 1.2.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': - dependencies: - '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.0.0) '@radix-ui/rect': 1.1.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-progress@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) aria-hidden: 1.2.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/number': 1.1.1 '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) @@ -10891,197 +10912,203 @@ snapshots: optionalDependencies: '@types/react': 19.1.17 - '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.0.0)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-toast@1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-toolbar@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.0.0)': dependencies: - react: 19.2.4 + react: 19.0.0 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.0.0)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.0.0)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.0.0)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.2.14)(react@19.0.0)': dependencies: - react: 19.2.4 - use-sync-external-store: 1.6.0(react@19.2.4) + react: 19.0.0 + use-sync-external-store: 1.6.0(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.0.0)': dependencies: - react: 19.2.4 + react: 19.0.0 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.0.0)': dependencies: - react: 19.2.4 + react: 19.0.0 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.0.0)': dependencies: '@radix-ui/rect': 1.1.1 - react: 19.2.4 + react: 19.0.0 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.0.0)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.0.0) + react: 19.0.0 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) '@radix-ui/rect@1.1.1': {} + '@react-native-async-storage/async-storage@3.1.0(react-native@0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4))(react@19.0.0)': + dependencies: + idb: 8.0.3 + react: 19.0.0 + react-native: 0.79.6(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.0.0)(utf-8-validate@6.0.4) + '@react-native/assets-registry@0.79.6': {} '@react-native/assets-registry@0.81.4': {} @@ -11341,12 +11368,12 @@ snapshots: optionalDependencies: '@types/react': 19.1.17 - '@react-native/virtualized-lists@0.81.4(@types/react@19.2.14)(react-native@0.81.4(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4))(react@19.2.4)': + '@react-native/virtualized-lists@0.81.4(@types/react@19.2.14)(react-native@0.81.4(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.0.0))(react@19.0.0)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 - react: 19.2.4 - react-native: 0.81.4(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4) + react: 19.0.0 + react-native: 0.81.4(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 @@ -11537,11 +11564,11 @@ snapshots: '@tanstack/query-core@5.95.2': {} - '@tanstack/react-form@1.28.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-form@1.28.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@tanstack/form-core': 1.28.5 - '@tanstack/react-store': 0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 + '@tanstack/react-store': 0.9.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react: 19.0.0 transitivePeerDependencies: - react-dom @@ -11550,17 +11577,12 @@ snapshots: '@tanstack/query-core': 5.95.2 react: 19.0.0 - '@tanstack/react-query@5.95.2(react@19.2.4)': - dependencies: - '@tanstack/query-core': 5.95.2 - react: 19.2.4 - - '@tanstack/react-store@0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-store@0.9.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@tanstack/store': 0.9.3 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - use-sync-external-store: 1.6.0(react@19.2.4) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + use-sync-external-store: 1.6.0(react@19.0.0) '@tanstack/store@0.9.3': {} @@ -11581,14 +11603,6 @@ snapshots: react: 19.0.0 typescript: 6.0.2 - '@trpc/tanstack-react-query@11.16.0(@tanstack/react-query@5.95.2(react@19.2.4))(@trpc/client@11.16.0(@trpc/server@11.16.0(typescript@6.0.2))(typescript@6.0.2))(@trpc/server@11.16.0(typescript@6.0.2))(react@19.2.4)(typescript@6.0.2)': - dependencies: - '@tanstack/react-query': 5.95.2(react@19.2.4) - '@trpc/client': 11.16.0(@trpc/server@11.16.0(typescript@6.0.2))(typescript@6.0.2) - '@trpc/server': 11.16.0(typescript@6.0.2) - react: 19.2.4 - typescript: 6.0.2 - '@tsconfig/node10@1.0.12': {} '@tsconfig/node12@1.0.11': {} @@ -12208,7 +12222,7 @@ snapshots: baseline-browser-mapping@2.10.12: {} - better-auth@1.4.21(@prisma/client@5.22.0(prisma@5.22.0))(better-sqlite3@12.8.0)(drizzle-kit@0.31.10)(drizzle-orm@0.41.0(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0))(mysql2@3.11.3)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@5.22.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + better-auth@1.4.21(@prisma/client@5.22.0(prisma@5.22.0))(better-sqlite3@12.8.0)(drizzle-kit@0.31.10)(drizzle-orm@0.41.0(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0))(mysql2@3.11.3)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(pg@8.20.0)(prisma@5.22.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@better-auth/core': 1.4.21(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0) '@better-auth/telemetry': 1.4.21(@better-auth/core@1.4.21(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0)) @@ -12228,13 +12242,13 @@ snapshots: drizzle-kit: 0.31.10 drizzle-orm: 0.41.0(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0) mysql2: 3.11.3 - next: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) pg: 8.20.0 prisma: 5.22.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) - better-auth@1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(better-sqlite3@12.8.0)(drizzle-kit@0.31.10)(drizzle-orm@0.41.0(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0))(mysql2@3.11.3)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@5.22.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + better-auth@1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(better-sqlite3@12.8.0)(drizzle-kit@0.31.10)(drizzle-orm@0.41.0(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0))(mysql2@3.11.3)(next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(pg@8.20.0)(prisma@5.22.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0) '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.41.0(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0)) @@ -12259,11 +12273,11 @@ snapshots: drizzle-kit: 0.31.10 drizzle-orm: 0.41.0(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0) mysql2: 3.11.3 - next: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) pg: 8.20.0 prisma: 5.22.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' @@ -12302,40 +12316,6 @@ snapshots: - '@cloudflare/workers-types' - '@opentelemetry/api' - better-auth@1.5.6(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(better-sqlite3@12.8.0)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0))(mysql2@3.11.3)(next@16.2.1(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@5.22.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0) - '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.2(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0)) - '@better-auth/kysely-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(kysely@0.28.14) - '@better-auth/memory-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) - '@better-auth/mongo-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1) - '@better-auth/prisma-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(@prisma/client@5.22.0(prisma@5.22.0))(prisma@5.22.0) - '@better-auth/telemetry': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@4.3.6))(jose@6.2.2)(kysely@0.28.14)(nanostores@1.2.0)) - '@better-auth/utils': 0.3.1 - '@better-fetch/fetch': 1.1.21 - '@noble/ciphers': 2.1.1 - '@noble/hashes': 2.0.1 - better-call: 1.3.2(zod@4.3.6) - defu: 6.1.4 - jose: 6.2.2 - kysely: 0.28.14 - nanostores: 1.2.0 - zod: 4.3.6 - optionalDependencies: - '@prisma/client': 5.22.0(prisma@5.22.0) - better-sqlite3: 12.8.0 - drizzle-kit: 0.31.10 - drizzle-orm: 0.45.2(@neondatabase/serverless@0.10.0)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(better-sqlite3@12.8.0)(gel@2.0.0)(kysely@0.28.14)(mysql2@3.11.3)(pg@8.20.0)(postgres@3.4.4)(prisma@5.22.0) - mysql2: 3.11.3 - next: 16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - pg: 8.20.0 - prisma: 5.22.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - transitivePeerDependencies: - - '@cloudflare/workers-types' - - '@opentelemetry/api' - better-call@1.1.8(zod@4.3.6): dependencies: '@better-auth/utils': 0.3.1 @@ -12525,7 +12505,7 @@ snapshots: '@chevrotain/gast': 10.5.0 '@chevrotain/types': 10.5.0 '@chevrotain/utils': 10.5.0 - lodash: 4.17.23 + lodash: 4.18.1 regexp-to-ast: 0.5.0 chokidar@5.0.0: @@ -13949,6 +13929,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + idb@8.0.3: {} + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -14437,7 +14419,7 @@ snapshots: lodash.throttle@4.1.1: {} - lodash@4.17.23: {} + lodash@4.18.1: {} log-symbols@2.2.0: dependencies: @@ -14843,10 +14825,10 @@ snapshots: nested-error-stacks@2.0.1: {} - next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next-themes@0.4.6(react-dom@19.2.4(react@19.0.0))(react@19.0.0): dependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: @@ -14874,34 +14856,6 @@ snapshots: transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - optional: true - - next@16.2.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - '@next/env': 16.2.1 - '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.12 - caniuse-lite: 1.0.30001781 - postcss: 8.4.31 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) - optionalDependencies: - '@next/swc-darwin-arm64': 16.2.1 - '@next/swc-darwin-x64': 16.2.1 - '@next/swc-linux-arm64-gnu': 16.2.1 - '@next/swc-linux-arm64-musl': 16.2.1 - '@next/swc-linux-x64-gnu': 16.2.1 - '@next/swc-linux-x64-musl': 16.2.1 - '@next/swc-win32-arm64-msvc': 16.2.1 - '@next/swc-win32-x64-msvc': 16.2.1 - '@opentelemetry/api': 1.9.0 - '@playwright/test': 1.58.2 - babel-plugin-react-compiler: 1.0.0 - sharp: 0.34.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros node-abi@3.89.0: dependencies: @@ -15379,65 +15333,65 @@ snapshots: dependencies: inherits: 2.0.4 - radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + radix-ui@1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0): dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-accessible-icon': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-aspect-ratio': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-avatar': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-form': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-label': 2.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-menubar': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-one-time-password-field': 0.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-password-toggle-field': 0.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-progress': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-separator': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-toast': 1.2.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-toolbar': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.0.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.0.0))(react@19.0.0) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) @@ -15469,9 +15423,9 @@ snapshots: react: 19.0.0 scheduler: 0.25.0 - react-dom@19.2.4(react@19.2.4): + react-dom@19.2.4(react@19.0.0): dependencies: - react: 19.2.4 + react: 19.0.0 scheduler: 0.27.0 react-fast-compare@3.2.2: {} @@ -15631,7 +15585,7 @@ snapshots: - supports-color - utf-8-validate - react-native@0.81.4(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4): + react-native@0.81.4(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.0.0): dependencies: '@jest/create-cache-key-function': 29.7.0 '@react-native/assets-registry': 0.81.4 @@ -15640,7 +15594,7 @@ snapshots: '@react-native/gradle-plugin': 0.81.4 '@react-native/js-polyfills': 0.81.4 '@react-native/normalize-colors': 0.81.4 - '@react-native/virtualized-lists': 0.81.4(@types/react@19.2.14)(react-native@0.81.4(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.4))(react@19.2.4) + '@react-native/virtualized-lists': 0.81.4(@types/react@19.2.14)(react-native@0.81.4(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.0.0))(react@19.0.0) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 @@ -15658,7 +15612,7 @@ snapshots: nullthrows: 1.1.1 pretty-format: 29.7.0 promise: 8.3.0 - react: 19.2.4 + react: 19.0.0 react-devtools-core: 6.1.5(bufferutil@4.1.0)(utf-8-validate@6.0.4) react-refresh: 0.14.2 regenerator-runtime: 0.13.11 @@ -15680,37 +15634,35 @@ snapshots: react-refresh@0.14.2: {} - react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4): + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.0.0): dependencies: - react: 19.2.4 - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.0.0 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.0.0) tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.14 - react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.4): + react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.0.0): dependencies: - react: 19.2.4 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.4) - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.0.0 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.0.0) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.0.0) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.4) - use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.4) + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.0.0) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.0.0) optionalDependencies: '@types/react': 19.2.14 - react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4): + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.0.0): dependencies: get-nonce: 1.0.1 - react: 19.2.4 + react: 19.0.0 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.14 react@19.0.0: {} - react@19.2.4: {} - readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -16051,10 +16003,10 @@ snapshots: slugify@1.6.8: {} - sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + sonner@2.0.7(react-dom@19.2.4(react@19.0.0))(react@19.0.0): dependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.0.0 + react-dom: 19.2.4(react@19.0.0) source-map-js@1.2.1: {} @@ -16197,14 +16149,6 @@ snapshots: react: 19.0.0 optionalDependencies: '@babel/core': 7.29.0 - optional: true - - styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.4): - dependencies: - client-only: 0.0.1 - react: 19.2.4 - optionalDependencies: - '@babel/core': 7.29.0 styleq@0.1.3: {} @@ -16476,9 +16420,9 @@ snapshots: dependencies: punycode: 2.3.1 - use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4): + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.0.0): dependencies: - react: 19.2.4 + react: 19.0.0 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.14 @@ -16487,10 +16431,14 @@ snapshots: dependencies: react: 19.0.0 - use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4): + use-latest-callback@0.3.4(react@19.0.0): + dependencies: + react: 19.0.0 + + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.0.0): dependencies: detect-node-es: 1.1.0 - react: 19.2.4 + react: 19.0.0 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.14 @@ -16499,10 +16447,6 @@ snapshots: dependencies: react: 19.0.0 - use-sync-external-store@1.6.0(react@19.2.4): - dependencies: - react: 19.2.4 - utf-8-validate@6.0.4: dependencies: node-gyp-build: 4.8.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b8b7bf6..f6c90a2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -25,8 +25,8 @@ catalogs: react19: "@types/react": ^19.2.14 "@types/react-dom": ^19.2.3 - react: ^19.2.4 - react-dom: ^19.2.4 + react: 19.0.0 + react-dom: 19.0.0 linkWorkspacePackages: true @@ -34,6 +34,8 @@ onlyBuiltDependencies: - esbuild overrides: + react: 19.0.0 + react-dom: 19.0.0 "@types/minimatch": 5.1.2 esbuild@<=0.24.2: ">=0.25.0" file-type@>=13.0.0 <21.3.1: ">=21.3.1" diff --git a/turbo.json b/turbo.json index 3efdc1a..e5e7b50 100644 --- a/turbo.json +++ b/turbo.json @@ -51,7 +51,10 @@ "AUTH_DISCORD_SECRET", "AUTH_REDIRECT_PROXY_URL", "BETTER_AUTH_SECRET", - "PORT" + "PORT", + "GOOGLE_CIVIC_API_KEY", + "CA_SOS_API_KEY", + "OPEN_STATES_API_KEY" ], "globalPassThroughEnv": [ "NODE_ENV",