feat: Implement platform locking with shadcn-ui tooltips and remove preset match badge.

This commit is contained in:
Yusuf İpek
2025-11-23 13:28:51 +03:00
parent 5b66d4700a
commit 4eac0917d0
11 changed files with 293 additions and 127 deletions
+22
View File
@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
+1
View File
@@ -18,6 +18,7 @@
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.2.8",
"cheerio": "^1.1.2", "cheerio": "^1.1.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
+36
View File
@@ -26,6 +26,9 @@ importers:
'@radix-ui/react-tabs': '@radix-ui/react-tabs':
specifier: ^1.0.4 specifier: ^1.0.4
version: 1.1.13(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected]) version: 1.1.13(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
'@radix-ui/react-tooltip':
specifier: ^1.2.8
version: 1.2.8(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
cheerio: cheerio:
specifier: ^1.1.2 specifier: ^1.1.2
version: 1.1.2 version: 1.1.2
@@ -529,6 +532,19 @@ packages:
'@types/react-dom': '@types/react-dom':
optional: true optional: true
'@radix-ui/[email protected]':
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
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
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/[email protected]': '@radix-ui/[email protected]':
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies: peerDependencies:
@@ -2869,6 +2885,26 @@ snapshots:
'@types/react': 18.3.26 '@types/react': 18.3.26
'@types/react-dom': 18.3.7(@types/[email protected]) '@types/react-dom': 18.3.7(@types/[email protected])
'@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
'@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
'@radix-ui/react-dismissable-layer': 1.1.11(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
'@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
'@radix-ui/react-popper': 1.2.8(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
'@radix-ui/react-portal': 1.1.9(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
'@radix-ui/react-presence': 1.1.5(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
'@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
'@radix-ui/react-slot': 1.2.3(@types/[email protected])([email protected])
'@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
'@radix-ui/react-visually-hidden': 1.2.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
react: 18.3.1
react-dom: 18.3.1([email protected])
optionalDependencies:
'@types/react': 18.3.26
'@types/react-dom': 18.3.7(@types/[email protected])
'@radix-ui/[email protected](@types/[email protected])([email protected])': '@radix-ui/[email protected](@types/[email protected])([email protected])':
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
+21 -10
View File
@@ -10,20 +10,25 @@
--card-foreground: 222.2 84% 4.9%; --card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%; --popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%; --primary: 218 63% 38%;
--primary-foreground: 210 40% 98%; --primary-foreground: 210 40% 98%;
--secondary: 210 40% 96%; --secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 84% 4.9%; --secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96%; --muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%; --muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96%; --accent: 210 40% 96.1%;
--accent-foreground: 222.2 84% 4.9%; --accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%; --border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%; --ring: 222.2 84% 4.9%;
--radius: 0.5rem; --radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
} }
.dark { .dark {
@@ -33,8 +38,8 @@
--card-foreground: 210 40% 98%; --card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%; --popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%; --popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%; --primary: 218 63% 38%;
--primary-foreground: 222.2 84% 4.9%; --primary-foreground: 210 40% 98%;
--secondary: 217.2 32.6% 17.5%; --secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%; --secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%; --muted: 217.2 32.6% 17.5%;
@@ -45,7 +50,12 @@
--destructive-foreground: 210 40% 98%; --destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%; --border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 94.1%; --ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
} }
} }
@@ -53,6 +63,7 @@
* { * {
@apply border-border; @apply border-border;
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} }
+49 -9
View File
@@ -7,12 +7,22 @@ import { apiClient } from '@/lib/api/client'
import { useLocale } from '@/contexts/LocaleContext' import { useLocale } from '@/contexts/LocaleContext'
import { Platform } from '@/types' import { Platform } from '@/types'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { Lock } from 'lucide-react'
interface PlatformSelectorProps { interface PlatformSelectorProps {
selectedPlatform: Platform | null selectedPlatform: Platform | null
onPlatformSelect: (platform: Platform) => void onPlatformSelect: (platform: Platform) => void
isLocked?: boolean
lockedMessage?: string
} }
export function PlatformSelector({ selectedPlatform, onPlatformSelect }: PlatformSelectorProps) { export function PlatformSelector({
selectedPlatform,
onPlatformSelect,
isLocked = false,
lockedMessage = "Platform selection is locked"
}: PlatformSelectorProps) {
const { t } = useLocale() const { t } = useLocale()
const [platforms, setPlatforms] = useState<Platform[]>([]) const [platforms, setPlatforms] = useState<Platform[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@@ -66,25 +76,36 @@ export function PlatformSelector({ selectedPlatform, onPlatformSelect }: Platfor
return ( return (
<Card className="w-full"> <Card className="w-full">
<CardHeader className="pb-4"> <CardHeader className="pb-4">
<CardTitle className="text-lg">{t('platform.select')}</CardTitle> <CardTitle className="text-lg flex items-center gap-2">
{t('platform.select')}
{isLocked && <Lock className="h-4 w-4 text-muted-foreground" />}
</CardTitle>
<CardDescription className="text-sm"> <CardDescription className="text-sm">
{t('platform.description')} {t('platform.description')}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="pt-0"> <CardContent className="pt-0">
<TooltipProvider>
<div className="grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-6 gap-2"> <div className="grid grid-cols-3 sm:grid-cols-4 lg:grid-cols-6 gap-2">
{platforms.map((platform) => ( {platforms.map((platform) => {
const isSelected = selectedPlatform?.id === platform.id
const isDisabled = isLocked && !isSelected
const ButtonContent = (
<Button <Button
key={platform.id} key={platform.id}
variant="outline" variant="outline"
className={`h-12 px-2 py-1 flex flex-col items-center justify-center gap-1 rounded-md border transition-colors className={`h-12 px-2 py-1 flex flex-col items-center justify-center gap-1 rounded-md border transition-colors w-full
${selectedPlatform?.id === platform.id ${isSelected
? 'border-primary ring-2 ring-primary/60 bg-primary/5' ? 'border-primary ring-2 ring-primary/60 bg-primary/5'
: 'border-border hover:bg-secondary/60'}`} : 'border-border hover:bg-secondary/60'}
onClick={() => onPlatformSelect(platform)} ${isDisabled ? 'opacity-50 cursor-not-allowed' : ''}
`}
onClick={() => !isLocked && onPlatformSelect(platform)}
disabled={isDisabled}
> >
<div <div
className={`h-5 w-5 ${selectedPlatform?.id === platform.id ? 'text-foreground' : 'text-muted-foreground'}`} className={`h-5 w-5 ${isSelected ? 'text-foreground' : 'text-muted-foreground'}`}
style={{ style={{
WebkitMaskImage: `url(${iconBase(iconSlug[platform.id] || 'linux')})`, WebkitMaskImage: `url(${iconBase(iconSlug[platform.id] || 'linux')})`,
maskImage: `url(${iconBase(iconSlug[platform.id] || 'linux')})`, maskImage: `url(${iconBase(iconSlug[platform.id] || 'linux')})`,
@@ -101,8 +122,27 @@ export function PlatformSelector({ selectedPlatform, onPlatformSelect }: Platfor
<div className="font-medium text-xs leading-tight truncate max-w-[90px]">{platform.name}</div> <div className="font-medium text-xs leading-tight truncate max-w-[90px]">{platform.name}</div>
</div> </div>
</Button> </Button>
))} )
if (isLocked && !isSelected) {
return (
<Tooltip key={platform.id}>
<TooltipTrigger asChild>
<div className="w-full cursor-not-allowed">
{ButtonContent}
</div> </div>
</TooltipTrigger>
<TooltipContent>
<p>{lockedMessage}</p>
</TooltipContent>
</Tooltip>
)
}
return ButtonContent
})}
</div>
</TooltipProvider>
</CardContent> </CardContent>
</Card> </Card>
) )
+2 -9
View File
@@ -1,4 +1,4 @@
import { Package as PackageIcon, Star } from 'lucide-react' import { Package as PackageIcon } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card' import { Card, CardContent } from '@/components/ui/card'
import { RecommendedPackage } from '@/types/recommendations' import { RecommendedPackage } from '@/types/recommendations'
@@ -19,14 +19,7 @@ export function RecommendationCard({ pkg, isSelected, onToggle }: Recommendation
}`} }`}
onClick={() => onToggle(pkg)} onClick={() => onToggle(pkg)}
> >
{pkg.presetMatch && (
<div className="absolute top-2 right-2">
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold bg-primary text-primary-foreground">
<Star className="h-3 w-3" />
{t('recommendations.preset_badge')}
</span>
</div>
)}
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-start gap-3 mb-3"> <div className="flex items-start gap-3 mb-3">
+1 -7
View File
@@ -1,4 +1,4 @@
import { Package as PackageIcon, Star, Info } from 'lucide-react' import { Package as PackageIcon } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { RecommendedPackage } from '@/types/recommendations' import { RecommendedPackage } from '@/types/recommendations'
@@ -24,12 +24,6 @@ export function RecommendationListItem({ pkg, isSelected, onToggle }: Recommenda
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h4 className="font-semibold text-sm truncate">{pkg.name}</h4> <h4 className="font-semibold text-sm truncate">{pkg.name}</h4>
{pkg.presetMatch && (
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-semibold bg-primary text-primary-foreground">
<Star className="h-2.5 w-2.5" />
Essential
</span>
)}
<span className="text-xs text-muted-foreground">{pkg.version}</span> <span className="text-xs text-muted-foreground">{pkg.version}</span>
</div> </div>
<p className="text-xs text-muted-foreground truncate">{pkg.description}</p> <p className="text-xs text-muted-foreground truncate">{pkg.description}</p>
+26 -2
View File
@@ -1,6 +1,6 @@
"use client" "use client"
import { useState, useEffect } from 'react' import { useState, useEffect, useRef } from 'react'
import { LocaleProvider } from '@/contexts/LocaleContext' import { LocaleProvider } from '@/contexts/LocaleContext'
import { Header } from './Header' import { Header } from './Header'
import { PlatformSelector } from './PlatformSelector' import { PlatformSelector } from './PlatformSelector'
@@ -28,7 +28,8 @@ function RepoHubAppContent({ cryptomusEnabled }: { cryptomusEnabled: boolean })
isLoading: isProfileLoading, isLoading: isProfileLoading,
hasCompletedOnboarding, hasCompletedOnboarding,
saveProfile, saveProfile,
detectedOS detectedOS,
getEffectiveOS
} = useRecommendationProfile() } = useRecommendationProfile()
const [showOnboarding, setShowOnboarding] = useState(false) const [showOnboarding, setShowOnboarding] = useState(false)
@@ -148,7 +149,24 @@ function RepoHubAppContent({ cryptomusEnabled }: { cryptomusEnabled: boolean })
const handleCloseScriptPreview = () => { const handleCloseScriptPreview = () => {
setGeneratedScript(null) setGeneratedScript(null)
} }
// Track previous effective OS to detect profile changes
const prevEffectiveOS = useRef<string | null>(null)
// Calculate current effective OS
const effectiveOS = getEffectiveOS()
// Sync platform selection with profile changes
useEffect(() => {
if (hasCompletedOnboarding && availablePlatforms.length > 0) {
const platform = availablePlatforms.find(p => p.id === effectiveOS)
// If profile OS changed, or if no platform is selected yet, update selection
if (platform && (effectiveOS !== prevEffectiveOS.current || !selectedPlatform)) {
setSelectedPlatform(platform)
prevEffectiveOS.current = effectiveOS
}
}
}, [hasCompletedOnboarding, effectiveOS, availablePlatforms, selectedPlatform])
return ( return (
<div key={locale} className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800"> <div key={locale} className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800">
<Header <Header
@@ -183,10 +201,16 @@ function RepoHubAppContent({ cryptomusEnabled }: { cryptomusEnabled: boolean })
/> />
)} )}
{/* Platform Selector */} {/* Platform Selector */}
<PlatformSelector <PlatformSelector
selectedPlatform={selectedPlatform} selectedPlatform={selectedPlatform}
onPlatformSelect={handlePlatformSelect} onPlatformSelect={handlePlatformSelect}
isLocked={selectedPackages.length > 0}
lockedMessage="Clear your selection to switch platforms"
/> />
{/* Package Browser */} {/* Package Browser */}
+30
View File
@@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
+1 -1
View File
@@ -1,4 +1,4 @@
import { type ClassValue, clsx } from "clsx" import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
+50 -35
View File
@@ -10,67 +10,82 @@ module.exports = {
theme: { theme: {
container: { container: {
center: true, center: true,
padding: "2rem", padding: '2rem',
screens: { screens: {
"2xl": "1400px", '2xl': '1400px'
}, }
}, },
extend: { extend: {
colors: { colors: {
border: "hsl(var(--border))", border: 'hsl(var(--border))',
input: "hsl(var(--input))", input: 'hsl(var(--input))',
ring: "hsl(var(--ring))", ring: 'hsl(var(--ring))',
background: "hsl(var(--background))", background: 'hsl(var(--background))',
foreground: "hsl(var(--foreground))", foreground: 'hsl(var(--foreground))',
primary: { primary: {
DEFAULT: "hsl(var(--primary))", DEFAULT: 'hsl(var(--primary))',
foreground: "hsl(var(--primary-foreground))", foreground: 'hsl(var(--primary-foreground))'
}, },
secondary: { secondary: {
DEFAULT: "hsl(var(--secondary))", DEFAULT: 'hsl(var(--secondary))',
foreground: "hsl(var(--secondary-foreground))", foreground: 'hsl(var(--secondary-foreground))'
}, },
destructive: { destructive: {
DEFAULT: "hsl(var(--destructive))", DEFAULT: 'hsl(var(--destructive))',
foreground: "hsl(var(--destructive-foreground))", foreground: 'hsl(var(--destructive-foreground))'
}, },
muted: { muted: {
DEFAULT: "hsl(var(--muted))", DEFAULT: 'hsl(var(--muted))',
foreground: "hsl(var(--muted-foreground))", foreground: 'hsl(var(--muted-foreground))'
}, },
accent: { accent: {
DEFAULT: "hsl(var(--accent))", DEFAULT: 'hsl(var(--accent))',
foreground: "hsl(var(--accent-foreground))", foreground: 'hsl(var(--accent-foreground))'
}, },
popover: { popover: {
DEFAULT: "hsl(var(--popover))", DEFAULT: 'hsl(var(--popover))',
foreground: "hsl(var(--popover-foreground))", foreground: 'hsl(var(--popover-foreground))'
}, },
card: { card: {
DEFAULT: "hsl(var(--card))", DEFAULT: 'hsl(var(--card))',
foreground: "hsl(var(--card-foreground))", foreground: 'hsl(var(--card-foreground))'
}, },
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
}, },
borderRadius: { borderRadius: {
lg: "var(--radius)", lg: 'var(--radius)',
md: "calc(var(--radius) - 2px)", md: 'calc(var(--radius) - 2px)',
sm: "calc(var(--radius) - 4px)", sm: 'calc(var(--radius) - 4px)'
}, },
keyframes: { keyframes: {
"accordion-down": { 'accordion-down': {
from: { height: 0 }, from: {
to: { height: "var(--radix-accordion-content-height)" }, height: 0
}, },
"accordion-up": { to: {
from: { height: "var(--radix-accordion-content-height)" }, height: 'var(--radix-accordion-content-height)'
to: { height: 0 }, }
}, },
'accordion-up': {
from: {
height: 'var(--radix-accordion-content-height)'
},
to: {
height: 0
}
}
}, },
animation: { animation: {
"accordion-down": "accordion-down 0.2s ease-out", 'accordion-down': 'accordion-down 0.2s ease-out',
"accordion-up": "accordion-up 0.2s ease-out", 'accordion-up': 'accordion-up 0.2s ease-out'
}, }
}, }
}, },
plugins: [require("tailwindcss-animate")], plugins: [require("tailwindcss-animate")],
} }