diff --git a/apps/expo/features/packs/components/PackCard.tsx b/apps/expo/features/packs/components/PackCard.tsx index 64eb89870f..5e41ae5b8d 100644 --- a/apps/expo/features/packs/components/PackCard.tsx +++ b/apps/expo/features/packs/components/PackCard.tsx @@ -5,9 +5,9 @@ import { Icon } from '@roninoss/icons'; import { WeightBadge } from 'expo-app/components/initial/WeightBadge'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { router } from 'expo-router'; -import { Alert, Image, Pressable, View } from 'react-native'; +import { ActivityIndicator, Alert, Image, Pressable, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { useDeletePack, usePackDetailsFromStore } from '../hooks'; +import { useDeletePack, useDuplicatePack, usePackDetailsFromStore } from '../hooks'; import { usePackOwnershipCheck } from '../hooks/usePackOwnershipCheck'; import type { Pack, PackInStore } from '../types'; @@ -15,10 +15,17 @@ type PackCardProps = { pack: Pack | PackInStore; onPress?: (pack: Pack) => void; isGenUI?: boolean; // Used to tweak styling & layout when card is being used in a generative UI context. + showDuplicateButton?: boolean; }; -export function PackCard({ pack: packArg, onPress, isGenUI = false }: PackCardProps) { +export function PackCard({ + pack: packArg, + onPress, + isGenUI = false, + showDuplicateButton = false, +}: PackCardProps) { const deletePack = useDeletePack(); + const { duplicatePack, isLoading: isDuplicating } = useDuplicatePack(); const { colors } = useColorScheme(); const { showActionSheetWithOptions } = useActionSheet(); const insets = useSafeAreaInsets(); @@ -119,18 +126,64 @@ export function PackCard({ pack: packArg, onPress, isGenUI = false }: PackCardPr - {pack.tags && isArray(pack.tags) && pack.tags.length > 0 && ( - - {pack.tags.map((tag) => ( - + {pack.tags && isArray(pack.tags) && pack.tags.length > 0 ? ( + + {pack.tags.map((tag) => ( + + #{tag} + + ))} + + ) : null} + + + {/* Duplicate button for non-owned packs when showDuplicateButton is true */} + {!isOwnedByUser && showDuplicateButton && ( + + )} + + {/* Delete button for owned packs */} + {!isGenUI && isOwnedByUser && ( + + )} - )} + ); diff --git a/apps/expo/features/packs/hooks/index.ts b/apps/expo/features/packs/hooks/index.ts index 6edec786ca..ea304d144a 100644 --- a/apps/expo/features/packs/hooks/index.ts +++ b/apps/expo/features/packs/hooks/index.ts @@ -2,12 +2,14 @@ export * from './useAddCatalogItem'; export * from './useBulkAddCatalogItems'; export * from './useCategoriesCount'; export * from './useCreatePack'; +export * from './useCreatePackFromPack'; export * from './useCreatePackItem'; export * from './useCreatePackWithItems'; export * from './useCurrentPack'; export * from './useDeletePack'; export * from './useDeletePackItem'; export * from './useDetailedPacks'; +export * from './useDuplicatePack'; export * from './useHasMinimumInventory'; export * from './useImagePicker'; export * from './usePackDetailsFromApi'; diff --git a/apps/expo/features/packs/hooks/useCreatePackFromPack.ts b/apps/expo/features/packs/hooks/useCreatePackFromPack.ts new file mode 100644 index 0000000000..32f99abe1d --- /dev/null +++ b/apps/expo/features/packs/hooks/useCreatePackFromPack.ts @@ -0,0 +1,56 @@ +import { packItemsStore, packsStore } from 'expo-app/features/packs/store'; +import { recordPackWeight } from 'expo-app/features/packs/store/packWeightHistory'; +import type { Pack, PackInput, PackInStore } from 'expo-app/features/packs/types'; +import { nanoid } from 'nanoid/non-secure'; +import { useCallback } from 'react'; + +export function useCreatePackFromPack() { + const createPackFromPack = useCallback((sourcePack: Pack, packData: Partial) => { + const newPackId = nanoid(); + const timestamp = new Date().toISOString(); + + // Create the new pack with custom data, falling back to source pack data + const newPack: PackInStore = { + id: newPackId, + name: packData.name || `${sourcePack.name} (Copy)`, + description: packData.description || sourcePack.description, + category: packData.category || sourcePack.category, + isPublic: packData.isPublic !== undefined ? packData.isPublic : false, // Default to private + image: packData.image !== undefined ? packData.image : sourcePack.image, + tags: packData.tags || sourcePack.tags || [], + localCreatedAt: timestamp, + localUpdatedAt: timestamp, + deleted: false, + }; + + // @ts-ignore: Safe because Legend-State uses Proxy + packsStore[newPackId].set(newPack); + + // Copy each item from the source pack + if (sourcePack.items) { + for (const item of sourcePack.items) { + if (!item.deleted) { + const newItemId = nanoid(); + const newItem = { + ...item, + id: newItemId, + packId: newPackId, + deleted: false, + createdAt: undefined, // Reset server timestamps + updatedAt: undefined, + }; + + // @ts-ignore: Safe because Legend-State uses Proxy + packItemsStore[newItemId].set(newItem); + } + } + } + + // Recalculate pack weight + recordPackWeight(newPackId); + + return newPackId; + }, []); + + return createPackFromPack; +} diff --git a/apps/expo/features/packs/hooks/useDuplicatePack.ts b/apps/expo/features/packs/hooks/useDuplicatePack.ts new file mode 100644 index 0000000000..753fe5862f --- /dev/null +++ b/apps/expo/features/packs/hooks/useDuplicatePack.ts @@ -0,0 +1,49 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import axiosInstance from 'expo-app/lib/api/client'; +import { useCallback } from 'react'; +import type { Pack, PackInput } from '../types'; +import { useCreatePackFromPack } from './useCreatePackFromPack'; + +export function useDuplicatePack() { + const createPackFromPack = useCreatePackFromPack(); + const queryClient = useQueryClient(); + + const duplicatePackMutation = useMutation({ + mutationFn: async ({ + packId, + packData = {}, + }: { + packId: string; + packData?: Partial; + }) => { + // First, fetch the full pack details with items + const response = await queryClient.fetchQuery({ + queryKey: ['pack', packId], + queryFn: async () => { + const res = await axiosInstance.get(`/api/packs/${packId}`); + return res.data as Pack; + }, + }); + + // Create the new pack from the fetched pack + const newPackId = createPackFromPack(response, packData); + return newPackId; + }, + onError: (error) => { + console.error('Error duplicating pack:', error); + }, + }); + + const duplicatePack = useCallback( + (packId: string, packData?: Partial) => { + return duplicatePackMutation.mutateAsync({ packId, packData }); + }, + [duplicatePackMutation], + ); + + return { + duplicatePack, + isLoading: duplicatePackMutation.isPending, + error: duplicatePackMutation.error, + }; +} diff --git a/apps/expo/features/packs/screens/PackListScreen.tsx b/apps/expo/features/packs/screens/PackListScreen.tsx index ce075a7115..b52e1271b7 100644 --- a/apps/expo/features/packs/screens/PackListScreen.tsx +++ b/apps/expo/features/packs/screens/PackListScreen.tsx @@ -215,7 +215,11 @@ export function PackListScreen() { stickyHeaderIndices={[0]} renderItem={({ item: pack }) => ( - + )} refreshControl={ diff --git a/apps/expo/features/trips/components/TripForm.tsx b/apps/expo/features/trips/components/TripForm.tsx index 2e38c31c03..b520d64565 100644 --- a/apps/expo/features/trips/components/TripForm.tsx +++ b/apps/expo/features/trips/components/TripForm.tsx @@ -57,6 +57,22 @@ export const TripForm = ({ trip }: { trip?: Trip }) => { const { location, setLocation } = useTripLocation(); const packs = usePacks(); + // Initialize location store with trip's location when component mounts or + // trip ID changes. We intentionally depend only on trip?.id (not trip?.location) + // so that after the user picks a new location via location-search, a + // re-render of the same trip object does not overwrite their selection in + // the store. + // biome-ignore lint/correctness/useExhaustiveDependencies: intentional — see comment above; reseeding on trip?.location would stomp user-picked values + useEffect(() => { + // Set location from trip, or null if trip has no location + setLocation(trip?.location ?? null); + + // Cleanup: clear location when component unmounts + return () => { + setLocation(null); + }; + }, [trip?.id, setLocation]); + const [showPackModal, setShowPackModal] = useState(false); const [showEndPicker, setShowEndPicker] = useState(false); const [showStartPicker, setShowStartPicker] = useState(false); diff --git a/apps/expo/features/trips/screens/TripDetailScreen.tsx b/apps/expo/features/trips/screens/TripDetailScreen.tsx index 9d76c0d880..46be6ca73b 100644 --- a/apps/expo/features/trips/screens/TripDetailScreen.tsx +++ b/apps/expo/features/trips/screens/TripDetailScreen.tsx @@ -8,7 +8,7 @@ import { useLocations } from 'expo-app/features/weather/hooks'; import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { useLocalSearchParams, useRouter } from 'expo-router'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { Modal, ScrollView, Share, View } from 'react-native'; import MapView, { Marker, PROVIDER_GOOGLE } from 'react-native-maps'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -30,6 +30,16 @@ export function TripDetailScreen() { const trip = useTripDetailsFromStore(id as string) as Trip; const packs = useDetailedPacks(); + // Create a stable key for MapView based on location coordinates + // This forces remount when location changes, fixing iOS initialRegion issue + const mapKey = useMemo( + () => + trip?.location + ? `map-${trip.location.latitude}-${trip.location.longitude}` + : 'map-no-location', + [trip?.location], + ); + if (!trip) { return ( @@ -159,6 +169,7 @@ export function TripDetailScreen() { (null); export function useTripLocation() { const location = use$(() => tripLocationStore.get()); - const setLocation = (loc: TripLocation | null) => { + const setLocation = useCallback((loc: TripLocation | null) => { tripLocationStore.set(loc); - }; + }, []); return { location, setLocation }; } diff --git a/apps/landing/components/sections/download.tsx b/apps/landing/components/sections/download.tsx index 5b4137f741..3ae50f6504 100644 --- a/apps/landing/components/sections/download.tsx +++ b/apps/landing/components/sections/download.tsx @@ -1,21 +1,9 @@ -'use client'; - import { siteConfig } from 'landing-app/config/site'; import { Apple, Check, Store } from 'lucide-react'; import Image from 'next/image'; import Link from 'next/link'; -import type React from 'react'; export default function DownloadSection() { - const scrollToSection = (e: React.MouseEvent, href: string) => { - e.preventDefault(); - const targetId = href.substring(1); - const element = document.getElementById(targetId); - if (element) { - element.scrollIntoView({ behavior: 'smooth' }); - } - }; - return (
{/* Subtle Apple-style background gradient */} @@ -25,8 +13,8 @@ export default function DownloadSection() {
{/* Text content */} -
-
+
+
Get Started Today
@@ -39,7 +27,7 @@ export default function DownloadSection() { {siteConfig.download.subtitle}

-
+
{['Free to use', 'Offline access', 'Regular updates', 'Community support'].map( (item) => (
@@ -52,10 +40,11 @@ export default function DownloadSection() { )}
-
+
scrollToSection(e, siteConfig.download.appStoreLink)} + target="_blank" + rel="noopener noreferrer" className="inline-flex items-center justify-center gap-2 rounded-full bg-apple-blue text-white px-8 h-12 text-sm font-medium hover:bg-apple-blue/90 transition-colors" > @@ -64,7 +53,8 @@ export default function DownloadSection() { scrollToSection(e, siteConfig.download.googlePlayLink)} + target="_blank" + rel="noopener noreferrer" className="inline-flex items-center justify-center gap-2 rounded-full border border-border bg-background px-8 h-12 text-sm font-medium hover:bg-black/5 dark:hover:bg-white/10 transition-colors" > diff --git a/apps/landing/components/sections/feature-section.tsx b/apps/landing/components/sections/feature-section.tsx index 442ff2d904..372f627c30 100644 --- a/apps/landing/components/sections/feature-section.tsx +++ b/apps/landing/components/sections/feature-section.tsx @@ -190,14 +190,13 @@ export default function FeatureSection() { {/* Other features grid */}
{siteConfig.features.slice(3).map((feature) => ( -
- -
+ ))}
diff --git a/apps/landing/components/sections/how-it-works.tsx b/apps/landing/components/sections/how-it-works.tsx index b629cf7dda..5f392c7e90 100644 --- a/apps/landing/components/sections/how-it-works.tsx +++ b/apps/landing/components/sections/how-it-works.tsx @@ -23,8 +23,8 @@ export default function HowItWorksSection() { {/* Step cards */}
- {/* Connector line – visible on large screens */} -
+ {/* Connector line – visible on large screens only (when 3-col grid is active) */} +
{siteConfig.howItWorks.steps.map((step, index) => { diff --git a/apps/landing/components/sections/landing-hero.tsx b/apps/landing/components/sections/landing-hero.tsx index 978724f6b2..18ea36eed7 100644 --- a/apps/landing/components/sections/landing-hero.tsx +++ b/apps/landing/components/sections/landing-hero.tsx @@ -43,14 +43,14 @@ export default function LandingHero() {
{/* Text column */} {/* Badge */} -
+
{siteConfig.hero.badge}
@@ -69,14 +69,17 @@ export default function LandingHero() { {/* Subtitle */} {siteConfig.hero.subtitle} {/* CTA buttons */} - + scrollToSection(e, siteConfig.cta.primary.href)} @@ -99,7 +102,7 @@ export default function LandingHero() { {/* Social proof */} {siteConfig.hero.socialProof && ( @@ -109,7 +112,7 @@ export default function LandingHero() { {/* Stats */}
diff --git a/apps/landing/components/sections/testimonials.tsx b/apps/landing/components/sections/testimonials.tsx index c33f64c59d..bd1880a271 100644 --- a/apps/landing/components/sections/testimonials.tsx +++ b/apps/landing/components/sections/testimonials.tsx @@ -21,8 +21,8 @@ export default function TestimonialsSection() {
{siteConfig.testimonials.items.map((testimonial) => ( -
- +
+
diff --git a/apps/landing/components/site-footer.tsx b/apps/landing/components/site-footer.tsx index 50f4f2ffbb..0e52ad3ddc 100644 --- a/apps/landing/components/site-footer.tsx +++ b/apps/landing/components/site-footer.tsx @@ -51,7 +51,7 @@ export default function SiteFooter() {
{/* Product links */} -
+

Product

    {siteConfig.footerLinks.product.map((item) => ( diff --git a/apps/landing/components/ui/button.tsx b/apps/landing/components/ui/button.tsx index f6ec6f9d4c..0d70c3d30b 100644 --- a/apps/landing/components/ui/button.tsx +++ b/apps/landing/components/ui/button.tsx @@ -1,48 +1,6 @@ -import { Slot } from '@radix-ui/react-slot'; -import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from 'landing-app/lib/utils'; -import * as React from 'react'; - -const buttonVariants = cva( - 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', - { - variants: { - variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary/90', - destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', - outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', - secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', - ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline', - }, - size: { - default: 'h-10 px-4 py-2', - sm: 'h-9 rounded-md px-3', - lg: 'h-11 rounded-md px-8', - icon: 'h-10 w-10', - }, - }, - defaultVariants: { - variant: 'default', - size: 'default', - }, - }, -); - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean; -} - -const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : 'button'; - return ( - - ); - }, -); -Button.displayName = 'Button'; - -export { Button, buttonVariants }; +// This module re-exports the Button component from the shared @packrat/web-ui +// package so existing `landing-app/components/ui/button` imports keep working +// while we migrate apps/landing onto the shared shadcn-style package. +// +// New code should import directly from `@packrat/web-ui`. +export { Button, type ButtonProps, buttonVariants } from '@packrat/web-ui'; diff --git a/apps/landing/next.config.mjs b/apps/landing/next.config.mjs index 05c153583f..04b4671d05 100644 --- a/apps/landing/next.config.mjs +++ b/apps/landing/next.config.mjs @@ -4,6 +4,7 @@ const nextConfig = { images: { unoptimized: true, }, + transpilePackages: ['@packrat/web-ui'], }; export default nextConfig; diff --git a/apps/landing/package.json b/apps/landing/package.json index ebe063e699..6834235b89 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -12,6 +12,7 @@ "dependencies": { "@emotion/is-prop-valid": "^1.3.1", "@hookform/resolvers": "^3.10.0", + "@packrat/web-ui": "workspace:*", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-aspect-ratio": "^1.1.7", diff --git a/apps/landing/tailwind.config.js b/apps/landing/tailwind.config.js index dabd0de129..c998490364 100644 --- a/apps/landing/tailwind.config.js +++ b/apps/landing/tailwind.config.js @@ -9,6 +9,7 @@ module.exports = { './app/**/*.{ts,tsx}', './src/**/*.{ts,tsx}', '*.{js,ts,jsx,tsx,mdx}', + '../../packages/web-ui/src/**/*.{ts,tsx}', ], theme: { container: { diff --git a/apps/landing/tsconfig.json b/apps/landing/tsconfig.json index 2df66ab114..23db25e589 100644 --- a/apps/landing/tsconfig.json +++ b/apps/landing/tsconfig.json @@ -20,7 +20,9 @@ ], "paths": { "landing-app/*": ["./*"], - "@packrat/api/*": ["../../packages/api/src/*"] + "@packrat/api/*": ["../../packages/api/src/*"], + "@packrat/web-ui": ["../../packages/web-ui/src"], + "@packrat/web-ui/*": ["../../packages/web-ui/src/*"] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], diff --git a/bun.lock b/bun.lock index 3431a1b505..28c43e1dfb 100644 --- a/bun.lock +++ b/bun.lock @@ -53,11 +53,12 @@ "@stardazed/streams-text-encoding": "^1.0.2", "@tanstack/react-form": "^1.0.5", "@tanstack/react-query": "^5.70.0", - "ai": "catalog:", - "axios": "catalog:", + "ai": "^5.0.136", + "axios": "^1.8.4", "burnt": "^0.13.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "expo": "^54.0.0", "expo-apple-authentication": "~8.0.8", "expo-blur": "~15.0.8", @@ -88,9 +89,9 @@ "jotai": "^2.12.2", "llama.rn": "0.10.1", "nativewind": "^4.2.3", - "radash": "catalog:", - "react": "catalog:", - "react-dom": "catalog:", + "radash": "^12.1.1", + "react": "19.1.0", + "react-dom": "19.1.0", "react-i18next": "^16.5.6", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", @@ -106,7 +107,7 @@ "react-native-web": "^0.21.0", "tailwind-merge": "^2.5.5", "use-debounce": "^10.0.5", - "zod": "catalog:", + "zod": "^3.24.2", }, "devDependencies": { "@babel/core": "^7.20.0", @@ -122,8 +123,8 @@ "prettier": "^3.2.5", "prettier-plugin-tailwindcss": "^0.5.11", "rimraf": "^6.0.1", - "tailwindcss": "catalog:", - "typescript": "catalog:", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.2", "vitest": "~3.1.0", }, }, @@ -134,40 +135,40 @@ "@ai-sdk/openai": "^2.0.11", "@hookform/resolvers": "^3.10.0", "@packrat/api": "workspace:*", - "@radix-ui/react-accordion": "catalog:", - "@radix-ui/react-alert-dialog": "catalog:", - "@radix-ui/react-aspect-ratio": "catalog:", - "@radix-ui/react-avatar": "catalog:", - "@radix-ui/react-checkbox": "catalog:", - "@radix-ui/react-collapsible": "catalog:", - "@radix-ui/react-context-menu": "catalog:", - "@radix-ui/react-dialog": "catalog:", - "@radix-ui/react-dropdown-menu": "catalog:", - "@radix-ui/react-hover-card": "catalog:", - "@radix-ui/react-label": "catalog:", - "@radix-ui/react-menubar": "catalog:", - "@radix-ui/react-navigation-menu": "catalog:", - "@radix-ui/react-popover": "catalog:", - "@radix-ui/react-progress": "catalog:", - "@radix-ui/react-radio-group": "catalog:", - "@radix-ui/react-scroll-area": "catalog:", - "@radix-ui/react-select": "catalog:", - "@radix-ui/react-separator": "catalog:", - "@radix-ui/react-slider": "catalog:", - "@radix-ui/react-slot": "catalog:", - "@radix-ui/react-switch": "catalog:", - "@radix-ui/react-tabs": "catalog:", - "@radix-ui/react-toast": "catalog:", - "@radix-ui/react-toggle": "catalog:", - "@radix-ui/react-toggle-group": "catalog:", - "@radix-ui/react-tooltip": "catalog:", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-context-menu": "^2.2.15", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.15", + "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-toast": "^1.2.14", + "@radix-ui/react-toggle": "^1.1.9", + "@radix-ui/react-toggle-group": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.7", "@tailwindcss/typography": "^0.5.16", "@tanstack/react-query": "^5.70.0", "@tanstack/react-query-devtools": "^5.70.0", - "ai": "catalog:", + "ai": "^5.0.11", "autoprefixer": "^10.4.21", - "axios": "catalog:", - "chalk": "catalog:", + "axios": "^1.12.0", + "chalk": "^5.6.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "1.0.4", @@ -181,9 +182,9 @@ "next": "^15.3.4", "next-themes": "^0.4.6", "path": "^0.12.7", - "react": "catalog:", + "react": "19.0.0", "react-day-picker": "8.10.1", - "react-dom": "catalog:", + "react-dom": "19.0.0", "react-hook-form": "^7.58.1", "react-resizable-panels": "^2.1.9", "recharts": "2.15.0", @@ -194,7 +195,7 @@ "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "vaul": "^0.9.9", - "zod": "catalog:", + "zod": "^3.24.2", }, "devDependencies": { "@types/mdx": "^2.0.13", @@ -202,8 +203,8 @@ "@types/react": "~19.0.10", "@types/react-dom": "^19.1.6", "postcss": "^8.5.6", - "tailwindcss": "catalog:", - "typescript": "catalog:", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.2", }, }, "apps/landing": { @@ -212,33 +213,34 @@ "dependencies": { "@emotion/is-prop-valid": "^1.3.1", "@hookform/resolvers": "^3.10.0", - "@radix-ui/react-accordion": "catalog:", - "@radix-ui/react-alert-dialog": "catalog:", - "@radix-ui/react-aspect-ratio": "catalog:", - "@radix-ui/react-avatar": "catalog:", - "@radix-ui/react-checkbox": "catalog:", - "@radix-ui/react-collapsible": "catalog:", - "@radix-ui/react-context-menu": "catalog:", - "@radix-ui/react-dialog": "catalog:", - "@radix-ui/react-dropdown-menu": "catalog:", - "@radix-ui/react-hover-card": "catalog:", - "@radix-ui/react-label": "catalog:", - "@radix-ui/react-menubar": "catalog:", - "@radix-ui/react-navigation-menu": "catalog:", - "@radix-ui/react-popover": "catalog:", - "@radix-ui/react-progress": "catalog:", - "@radix-ui/react-radio-group": "catalog:", - "@radix-ui/react-scroll-area": "catalog:", - "@radix-ui/react-select": "catalog:", - "@radix-ui/react-separator": "catalog:", - "@radix-ui/react-slider": "catalog:", - "@radix-ui/react-slot": "catalog:", - "@radix-ui/react-switch": "catalog:", - "@radix-ui/react-tabs": "catalog:", - "@radix-ui/react-toast": "catalog:", - "@radix-ui/react-toggle": "catalog:", - "@radix-ui/react-toggle-group": "catalog:", - "@radix-ui/react-tooltip": "catalog:", + "@packrat/web-ui": "workspace:*", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-context-menu": "^2.2.15", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.15", + "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-toast": "^1.2.14", + "@radix-ui/react-toggle": "^1.1.9", + "@radix-ui/react-toggle-group": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.7", "autoprefixer": "^10.4.21", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -250,9 +252,9 @@ "lucide-react": "^0.454.0", "next": "^15.3.4", "next-themes": "^0.4.6", - "react": "catalog:", + "react": "19.0.0", "react-day-picker": "8.10.1", - "react-dom": "catalog:", + "react-dom": "19.0.0", "react-hook-form": "^7.58.1", "react-resizable-panels": "^2.1.9", "recharts": "2.15.0", @@ -260,15 +262,15 @@ "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "vaul": "^0.9.9", - "zod": "catalog:", + "zod": "^3.24.2", }, "devDependencies": { "@types/node": "^22.15.33", "@types/react": "~19.0.10", "@types/react-dom": "^19.1.6", "postcss": "^8.5.6", - "tailwindcss": "catalog:", - "typescript": "catalog:", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.2", }, }, "packages/analytics": { @@ -276,13 +278,13 @@ "version": "0.1.0", "dependencies": { "@duckdb/node-api": "1.5.0-r.1", - "chalk": "catalog:", + "chalk": "^5.4.1", "citty": "^0.2.1", "cli-table3": "^0.6.5", "consola": "^3.4.2", "magic-regexp": "^0.11.0", - "radash": "catalog:", - "zod": "catalog:", + "radash": "^12.1.1", + "zod": "^3.24.2", }, "devDependencies": { "@types/bun": "latest", @@ -306,7 +308,7 @@ "@packrat/guards": "workspace:*", "@scalar/hono-api-reference": "^0.8.0", "@types/nodemailer": "^6.4.17", - "ai": "catalog:", + "ai": "^5.0.11", "bcryptjs": "^3.0.2", "csv-parse": "^5.6.0", "drizzle-kit": "^0.30.6", @@ -319,12 +321,12 @@ "linkedom": "^0.18.11", "nodemailer": "^6.10.0", "pg": "^8.16.3", - "radash": "catalog:", + "radash": "^12.1.1", "resend": "^4.2.0", "workers-ai-provider": "^0.7.2", "ws": "^8.18.1", "youtube-transcript": "^1.3.0", - "zod": "catalog:", + "zod": "^3.24.2", "zod-openapi": "^4.2.4", }, "devDependencies": { @@ -344,7 +346,7 @@ "name": "@packrat/guards", "version": "0.0.1", "dependencies": { - "radash": "catalog:", + "radash": "^12.1.0", }, }, "packages/ui": { @@ -354,48 +356,28 @@ "@packrat-ai/nativewindui": "^2.0.1-alpha.0", }, }, + "packages/web-ui": { + "name": "@packrat/web-ui", + "version": "0.0.1", + "dependencies": { + "@radix-ui/react-slot": "^1.2.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "tailwind-merge": "^2.5.5", + }, + "devDependencies": { + "@types/react": "~19.0.10", + "react": "19.0.0", + "typescript": "^5.8.2", + }, + "peerDependencies": { + "react": "^19.0.0", + }, + }, }, "trustedDependencies": [ "@sentry/cli", ], - "catalog": { - "@radix-ui/react-accordion": "^1.2.11", - "@radix-ui/react-alert-dialog": "^1.1.14", - "@radix-ui/react-aspect-ratio": "^1.1.7", - "@radix-ui/react-avatar": "^1.1.10", - "@radix-ui/react-checkbox": "^1.3.2", - "@radix-ui/react-collapsible": "^1.1.11", - "@radix-ui/react-context-menu": "^2.2.15", - "@radix-ui/react-dialog": "^1.1.14", - "@radix-ui/react-dropdown-menu": "^2.1.15", - "@radix-ui/react-hover-card": "^1.1.14", - "@radix-ui/react-label": "^2.1.7", - "@radix-ui/react-menubar": "^1.1.15", - "@radix-ui/react-navigation-menu": "^1.2.13", - "@radix-ui/react-popover": "^1.1.14", - "@radix-ui/react-progress": "^1.1.7", - "@radix-ui/react-radio-group": "^1.3.7", - "@radix-ui/react-scroll-area": "^1.2.9", - "@radix-ui/react-select": "^2.2.5", - "@radix-ui/react-separator": "^1.1.7", - "@radix-ui/react-slider": "^1.3.5", - "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-switch": "^1.2.5", - "@radix-ui/react-tabs": "^1.1.12", - "@radix-ui/react-toast": "^1.2.14", - "@radix-ui/react-toggle": "^1.1.9", - "@radix-ui/react-toggle-group": "^1.1.10", - "@radix-ui/react-tooltip": "^1.2.7", - "ai": "^5.0.136", - "axios": "^1.12.0", - "chalk": "^5.6.2", - "radash": "^12.1.1", - "react": "19.1.0", - "react-dom": "19.1.0", - "tailwindcss": "^3.4.17", - "typescript": "^5.8.2", - "zod": "^3.24.2", - }, "packages": { "@0no-co/graphql.web": ["@0no-co/graphql.web@1.2.0", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw=="], @@ -1043,6 +1025,8 @@ "@packrat/ui": ["@packrat/ui@workspace:packages/ui"], + "@packrat/web-ui": ["@packrat/web-ui@workspace:packages/web-ui"], + "@petamoriken/float16": ["@petamoriken/float16@3.9.3", "", {}, "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g=="], "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], @@ -3049,7 +3033,7 @@ "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], - "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], + "react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], "react-day-picker": ["react-day-picker@8.10.1", "", { "peerDependencies": { "date-fns": "^2.28.0 || ^3.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA=="], @@ -3649,6 +3633,8 @@ "@ai-sdk/react/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.21", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-veuMwTLxsgh31Jjn0SnBABnM1f7ebHhRWcV2ZuY3hP3iJDCZ8VXBaYqcHXoOQDqUXTCas08sKQcHyWK+zl882Q=="], + "@ai-sdk/react/react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], + "@apidevtools/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "@aws-crypto/sha1-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], @@ -4057,6 +4043,12 @@ "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + "packrat-expo-app/react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], + + "packrat-guides-app/react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="], + + "packrat-landing-app/react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="], + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "prop-types/object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -4065,12 +4057,18 @@ "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "react-day-picker/react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], + "react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + "react-dom/react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], + "react-i18next/@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], "react-native/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "react-native/react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], + "react-native/ws": ["ws@6.2.3", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA=="], "react-native-blob-util/glob": ["glob@13.0.1", "", { "dependencies": { "minimatch": "^10.1.2", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-B7U/vJpE3DkJ5WXTgTpTRN63uV42DseiXXKMwG14LQBXmsdeIoHAPbU/MEo6II0k5ED74uc2ZGTC6MwHFQhF6w=="], @@ -4133,6 +4131,8 @@ "util/inherits": ["inherits@2.0.3", "", {}, "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="], + "vaul/react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], + "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "vitest/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], @@ -4493,6 +4493,10 @@ "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "packrat-guides-app/react-dom/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], + + "packrat-landing-app/react-dom/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], + "react-native-css-interop/lightningcss/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="], "react-native-css-interop/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gl/lqIXY+d+ySmMbgDf0pgaWSqrWYxVHoc88q+Vhf2YNzZ8DwoRzGt5NZDVqqIW5ScpSnmmjcgXP87Dn2ylSSQ=="], diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json new file mode 100644 index 0000000000..f00039caf2 --- /dev/null +++ b/packages/web-ui/package.json @@ -0,0 +1,32 @@ +{ + "name": "@packrat/web-ui", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./components/*": "./src/components/*.tsx", + "./lib/utils": "./src/lib/utils.ts", + "./styles/globals.css": "./src/styles/globals.css", + "./tailwind/preset": "./src/tailwind/preset.ts" + }, + "scripts": { + "check-types": "tsc --noEmit" + }, + "dependencies": { + "@radix-ui/react-slot": "^1.2.3", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "tailwind-merge": "^2.5.5" + }, + "devDependencies": { + "@types/react": "~19.0.10", + "react": "19.0.0", + "typescript": "^5.8.2" + }, + "peerDependencies": { + "react": "^19.0.0" + } +} diff --git a/packages/web-ui/src/components/button.tsx b/packages/web-ui/src/components/button.tsx new file mode 100644 index 0000000000..510c9c088f --- /dev/null +++ b/packages/web-ui/src/components/button.tsx @@ -0,0 +1,48 @@ +import { cn } from '@packrat/web-ui/lib/utils'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; +import * as React from 'react'; + +const buttonVariants = cva( + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; + return ( + + ); + }, +); +Button.displayName = 'Button'; + +export { Button, buttonVariants }; diff --git a/packages/web-ui/src/index.ts b/packages/web-ui/src/index.ts new file mode 100644 index 0000000000..6e6689e37b --- /dev/null +++ b/packages/web-ui/src/index.ts @@ -0,0 +1,2 @@ +export { Button, type ButtonProps, buttonVariants } from './components/button'; +export { cn } from './lib/utils'; diff --git a/packages/web-ui/src/lib/utils.ts b/packages/web-ui/src/lib/utils.ts new file mode 100644 index 0000000000..9ad0df4269 --- /dev/null +++ b/packages/web-ui/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/packages/web-ui/src/styles/globals.css b/packages/web-ui/src/styles/globals.css new file mode 100644 index 0000000000..85c6896bd1 --- /dev/null +++ b/packages/web-ui/src/styles/globals.css @@ -0,0 +1,8 @@ +/* + * Shared globals.css stub for @packrat/web-ui. + * + * A future PR will move the base layer + theme tokens currently duplicated + * between apps/landing/app/globals.css and apps/guides/app/globals.css here, + * so both Next.js apps can `@import '@packrat/web-ui/styles/globals.css'` + * and only keep app-specific overrides locally. + */ diff --git a/packages/web-ui/src/tailwind/preset.ts b/packages/web-ui/src/tailwind/preset.ts new file mode 100644 index 0000000000..58c5439db0 --- /dev/null +++ b/packages/web-ui/src/tailwind/preset.ts @@ -0,0 +1,16 @@ +import type { Config } from 'tailwindcss'; + +/** + * Shared Tailwind preset for PackRat web apps. + * + * Stub for the @packrat/web-ui package skeleton. A future PR will populate + * this with the theme tokens / plugins currently duplicated between + * apps/landing/tailwind.config.js and apps/guides/tailwind.config.ts, so both + * apps can just extend this preset. + */ +export const webUiPreset = { + content: [], + theme: {}, +} satisfies Partial; + +export default webUiPreset; diff --git a/packages/web-ui/tsconfig.json b/packages/web-ui/tsconfig.json new file mode 100644 index 0000000000..f4ac8bd48e --- /dev/null +++ b/packages/web-ui/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "jsx": "preserve", + "lib": ["dom", "dom.iterable", "esnext"], + "isolatedModules": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "baseUrl": ".", + "paths": { + "@packrat/web-ui": ["./src"], + "@packrat/web-ui/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules", "dist"] +} diff --git a/scripts/check-all.ts b/scripts/check-all.ts index 119237d470..1a3e15424f 100644 --- a/scripts/check-all.ts +++ b/scripts/check-all.ts @@ -30,6 +30,13 @@ import { join } from 'node:path'; const ROOT = join(import.meta.dir, '..'); +// Top-level regex constants for extractSummary patterns +const RE_FOUND_N = /found \d+/i; +const RE_N_CYCLE = /\d+ cycle/i; +const RE_N_VIOLATION = /\d+ violation/i; +const RE_N_FILE_OUT_OF_ORDER = /\d+ file.*out of order/i; +const RE_N_ERROR = /\d+ error/i; + // --------------------------------------------------------------------------- // Check definitions // --------------------------------------------------------------------------- @@ -83,7 +90,7 @@ interface CheckResult { // --------------------------------------------------------------------------- function extractSummary(stdout: string, stderr: string): string { - const combined = (stdout + '\n' + stderr).trim(); + const combined = `${stdout}\n${stderr}`.trim(); if (!combined) return ''; // Look for lines that have useful counts/summaries @@ -92,16 +99,16 @@ function extractSummary(stdout: string, stderr: string): string { // Patterns that often appear as the key summary line for (const line of lines) { const l = line.trim(); - if (/found \d+/i.test(l)) return l; - if (/\d+ cycle/i.test(l)) return l; - if (/\d+ violation/i.test(l)) return l; - if (/\d+ file.*out of order/i.test(l)) return l; - if (/\d+ error/i.test(l)) return l; + if (RE_FOUND_N.test(l)) return l; + if (RE_N_CYCLE.test(l)) return l; + if (RE_N_VIOLATION.test(l)) return l; + if (RE_N_FILE_OUT_OF_ORDER.test(l)) return l; + if (RE_N_ERROR.test(l)) return l; } // Fall back to first non-empty line, truncated const first = lines[0] ?? ''; - return first.length > 60 ? first.slice(0, 57) + '…' : first; + return first.length > 60 ? `${first.slice(0, 57)}…` : first; } // --------------------------------------------------------------------------- @@ -158,7 +165,7 @@ function padRight(str: string, width: number): string { } function formatDuration(ms: number): string { - return (ms / 1000).toFixed(1) + 's'; + return `${(ms / 1000).toFixed(1)}s`; } function renderRow(result: CheckResult): string { diff --git a/scripts/lint/no-circular-deps.ts b/scripts/lint/no-circular-deps.ts index d2d572afec..e72a48ecc8 100644 --- a/scripts/lint/no-circular-deps.ts +++ b/scripts/lint/no-circular-deps.ts @@ -24,6 +24,9 @@ import { dirname, join, normalize, relative, resolve } from 'node:path'; const ROOT = resolve(join(import.meta.dir, '..', '..')); +// Regex for stripping trailing /* from tsconfig path aliases +const TRAILING_GLOB_RE = /\/\*$/; + // --------------------------------------------------------------------------- // Path-alias map built from tsconfig.json paths + package.json exports // --------------------------------------------------------------------------- @@ -44,8 +47,8 @@ function buildAliasMap(): AliasEntry[] { for (const [alias, targets] of Object.entries(paths)) { if (!targets[0]) continue; // Strip trailing /* from alias and target - const aliasClean = alias.replace(/\/\*$/, ''); - const targetClean = targets[0].replace(/\/\*$/, ''); + const aliasClean = alias.replace(TRAILING_GLOB_RE, ''); + const targetClean = targets[0].replace(TRAILING_GLOB_RE, ''); aliases.push({ prefix: aliasClean, target: resolve(ROOT, targetClean), diff --git a/tsconfig.json b/tsconfig.json index 50bc7a07ef..195af6106d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,8 @@ "@packrat/guards": ["./packages/guards/src"], "@packrat/guards/*": ["./packages/guards/src/*"], "@packrat/ui/*": ["./packages/ui/*"], + "@packrat/web-ui": ["./packages/web-ui/src"], + "@packrat/web-ui/*": ["./packages/web-ui/src/*"], "@packrat/analytics/*": ["./packages/analytics/src/*"], "nativewindui/*": ["./apps/expo/components/ui/*"] }