From 4eac0917d0d8387cb228eb60972c0356b3ec4fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C4=B0pek?= Date: Sun, 23 Nov 2025 13:28:51 +0300 Subject: [PATCH] feat: Implement platform locking with shadcn-ui tooltips and remove preset match badge. --- components.json | 22 ++++ package.json | 1 + pnpm-lock.yaml | 36 ++++++ src/app/globals.css | 33 +++-- src/components/PlatformSelector.tsx | 108 +++++++++++------ src/components/RecommendationCard.tsx | 11 +- src/components/RecommendationListItem.tsx | 8 +- src/components/RepoHubApp.tsx | 28 ++++- src/components/ui/tooltip.tsx | 30 +++++ src/lib/utils.ts | 2 +- tailwind.config.js | 141 ++++++++++++---------- 11 files changed, 293 insertions(+), 127 deletions(-) create mode 100644 components.json create mode 100644 src/components/ui/tooltip.tsx diff --git a/components.json b/components.json new file mode 100644 index 0000000..1511f74 --- /dev/null +++ b/components.json @@ -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": {} +} \ No newline at end of file diff --git a/package.json b/package.json index 61073fd..22c8fa4 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-tooltip": "^1.2.8", "cheerio": "^1.1.2", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a01bad9..7c8b52e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@radix-ui/react-tabs': specifier: ^1.0.4 version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: ^1.2.8 + version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) cheerio: specifier: ^1.1.2 version: 1.1.2 @@ -529,6 +532,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-tooltip@1.2.8': + 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/react-use-callback-ref@1.1.1': resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} peerDependencies: @@ -2869,6 +2885,26 @@ snapshots: '@types/react': 18.3.26 '@types/react-dom': 18.3.7(@types/react@18.3.26) + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.26)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.26)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.26)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.26)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.26)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.26 + '@types/react-dom': 18.3.7(@types/react@18.3.26) + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.26)(react@18.3.1)': dependencies: react: 18.3.1 diff --git a/src/app/globals.css b/src/app/globals.css index 7b06b6d..aa7f0b1 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -10,20 +10,25 @@ --card-foreground: 222.2 84% 4.9%; --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; - --primary: 221.2 83.2% 53.3%; + --primary: 218 63% 38%; --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96%; - --secondary-foreground: 222.2 84% 4.9%; - --muted: 210 40% 96%; + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; - --accent: 210 40% 96%; - --accent-foreground: 222.2 84% 4.9%; + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --border: 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; + --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 { @@ -33,8 +38,8 @@ --card-foreground: 210 40% 98%; --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 222.2 84% 4.9%; + --primary: 218 63% 38%; + --primary-foreground: 210 40% 98%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; @@ -45,7 +50,12 @@ --destructive-foreground: 210 40% 98%; --border: 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,7 +63,8 @@ * { @apply border-border; } + body { @apply bg-background text-foreground; } -} +} \ No newline at end of file diff --git a/src/components/PlatformSelector.tsx b/src/components/PlatformSelector.tsx index 5b627b9..40404f8 100644 --- a/src/components/PlatformSelector.tsx +++ b/src/components/PlatformSelector.tsx @@ -7,16 +7,26 @@ import { apiClient } from '@/lib/api/client' import { useLocale } from '@/contexts/LocaleContext' import { Platform } from '@/types' +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' +import { Lock } from 'lucide-react' + interface PlatformSelectorProps { selectedPlatform: Platform | null 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 [platforms, setPlatforms] = useState([]) const [loading, setLoading] = useState(true) - + const iconSlug: Record = { debian: 'debian', ubuntu: 'ubuntu', @@ -66,43 +76,73 @@ export function PlatformSelector({ selectedPlatform, onPlatformSelect }: Platfor return ( - {t('platform.select')} + + {t('platform.select')} + {isLocked && } + {t('platform.description')} -
- {platforms.map((platform) => ( - - ))} -
+ +
+ {platforms.map((platform) => { + const isSelected = selectedPlatform?.id === platform.id + const isDisabled = isLocked && !isSelected + + const ButtonContent = ( + + ) + + if (isLocked && !isSelected) { + return ( + + +
+ {ButtonContent} +
+
+ +

{lockedMessage}

+
+
+ ) + } + + return ButtonContent + })} +
+
) diff --git a/src/components/RecommendationCard.tsx b/src/components/RecommendationCard.tsx index 6281d97..2aa34b2 100644 --- a/src/components/RecommendationCard.tsx +++ b/src/components/RecommendationCard.tsx @@ -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 { Card, CardContent } from '@/components/ui/card' import { RecommendedPackage } from '@/types/recommendations' @@ -19,14 +19,7 @@ export function RecommendationCard({ pkg, isSelected, onToggle }: Recommendation }`} onClick={() => onToggle(pkg)} > - {pkg.presetMatch && ( -
- - - {t('recommendations.preset_badge')} - -
- )} +
diff --git a/src/components/RecommendationListItem.tsx b/src/components/RecommendationListItem.tsx index 765db64..e0ed0f7 100644 --- a/src/components/RecommendationListItem.tsx +++ b/src/components/RecommendationListItem.tsx @@ -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 { RecommendedPackage } from '@/types/recommendations' @@ -24,12 +24,6 @@ export function RecommendationListItem({ pkg, isSelected, onToggle }: Recommenda

{pkg.name}

- {pkg.presetMatch && ( - - - Essential - - )} {pkg.version}

{pkg.description}

diff --git a/src/components/RepoHubApp.tsx b/src/components/RepoHubApp.tsx index 574b31e..3e14116 100644 --- a/src/components/RepoHubApp.tsx +++ b/src/components/RepoHubApp.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { LocaleProvider } from '@/contexts/LocaleContext' import { Header } from './Header' import { PlatformSelector } from './PlatformSelector' @@ -28,7 +28,8 @@ function RepoHubAppContent({ cryptomusEnabled }: { cryptomusEnabled: boolean }) isLoading: isProfileLoading, hasCompletedOnboarding, saveProfile, - detectedOS + detectedOS, + getEffectiveOS } = useRecommendationProfile() const [showOnboarding, setShowOnboarding] = useState(false) @@ -148,7 +149,24 @@ function RepoHubAppContent({ cryptomusEnabled }: { cryptomusEnabled: boolean }) const handleCloseScriptPreview = () => { setGeneratedScript(null) } + // Track previous effective OS to detect profile changes + const prevEffectiveOS = useRef(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 (
)} + + + + {/* Platform Selector */} 0} + lockedMessage="Clear your selection to switch platforms" /> {/* Package Browser */} diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..30fc44d --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index d084cca..bd0c391 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,4 +1,4 @@ -import { type ClassValue, clsx } from "clsx" +import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" export function cn(...inputs: ClassValue[]) { diff --git a/tailwind.config.js b/tailwind.config.js index da23e8e..e660708 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -8,69 +8,84 @@ module.exports = { './src/**/*.{ts,tsx}', ], theme: { - container: { - center: true, - padding: "2rem", - screens: { - "2xl": "1400px", - }, - }, - extend: { - colors: { - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - }, - borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - keyframes: { - "accordion-down": { - from: { height: 0 }, - to: { height: "var(--radix-accordion-content-height)" }, - }, - "accordion-up": { - from: { height: "var(--radix-accordion-content-height)" }, - to: { height: 0 }, - }, - }, - animation: { - "accordion-down": "accordion-down 0.2s ease-out", - "accordion-up": "accordion-up 0.2s ease-out", - }, - }, + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px' + } + }, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + card: { + DEFAULT: 'hsl(var(--card))', + 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: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + keyframes: { + 'accordion-down': { + from: { + height: 0 + }, + to: { + height: 'var(--radix-accordion-content-height)' + } + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)' + }, + to: { + height: 0 + } + } + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out' + } + } }, plugins: [require("tailwindcss-animate")], }