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/*"]
}