diff --git a/apps/web/src/components/SettingsView.tsx b/apps/web/src/components/SettingsView.tsx index 3fda19ed..217d4e70 100644 --- a/apps/web/src/components/SettingsView.tsx +++ b/apps/web/src/components/SettingsView.tsx @@ -10,6 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Alert, AlertDescription } from '@/components/ui/alert' import { Progress } from '@/components/ui/progress' import { hasFeature } from '@/lib/featureFlags' +import { useKonstaToggle } from '@/hooks/useKonstaOverride' import { CaretLeft, GithubLogo, @@ -105,6 +106,7 @@ const METICULOUS_ADDON_UPDATE_SNIPPET = 'docker exec -it meticai bash -lc "cd /a export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollowSystem, onToggleTheme, onSetFollowSystem, platformTheme, onSetPlatformTheme }: SettingsViewProps) { const { t } = useTranslation() + const { enabled: useKonstaUi, setEnabled: setUseKonstaUi } = useKonstaToggle() const [settings, setSettings] = useState({ geminiApiKey: '', @@ -1166,6 +1168,19 @@ export function SettingsView({ onBack, showBlobs, onToggleBlobs, isDark, isFollo )} + {/* Konsta UI toggle */} +
+
+ +

{t('appearance.useKonstaUiDescription')}

+
+ +
+ )} diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx index d831d42d..45d81951 100644 --- a/apps/web/src/components/ui/button.tsx +++ b/apps/web/src/components/ui/button.tsx @@ -1,6 +1,8 @@ import { ComponentProps } from "react" import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" +import { Button as KButton } from 'konsta/react' +import { useKonstaOverride } from '@/hooks/useKonstaOverride' import { cn } from "@/lib/utils" @@ -43,7 +45,7 @@ const buttonVariants = cva( } ) -function Button({ +function ShadcnButton({ className, variant, size, @@ -64,4 +66,35 @@ function Button({ ) } +function Button(props: ComponentProps<"button"> & VariantProps & { asChild?: boolean }) { + const useKonsta = useKonstaOverride() + const { variant, size, asChild = false, className, children, ...rest } = props + + if (!useKonsta || size === 'icon' || asChild) { + return + } + + const isOutline = variant === 'outline' + const isClear = variant === 'ghost' || variant === 'link' + const isTonal = variant === 'ember' || variant === 'secondary' + const isRounded = variant === 'liquid' || variant === 'dark-brew' || variant === 'frosted' || variant === 'ember' + const isSmall = size === 'sm' + const isLarge = size === 'lg' + + return ( + + {children} + + ) +} + export { Button, buttonVariants } diff --git a/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx index e955b4be..82de8d7c 100644 --- a/apps/web/src/components/ui/card.tsx +++ b/apps/web/src/components/ui/card.tsx @@ -1,8 +1,10 @@ import { ComponentProps } from "react" +import { Card as KCard } from 'konsta/react' +import { useKonstaOverride } from '@/hooks/useKonstaOverride' import { cn } from "@/lib/utils" -function Card({ className, ...props }: ComponentProps<"div">) { +function ShadcnCard({ className, ...props }: ComponentProps<"div">) { return (
) { ) } +function Card({ className, ...props }: ComponentProps<"div">) { + const useKonsta = useKonstaOverride() + if (!useKonsta) return + // contentWrap={false} because sub-components (CardHeader, CardContent, etc.) handle their own spacing + return +} + function CardHeader({ className, ...props }: ComponentProps<"div">) { return (
) { @@ -29,4 +31,35 @@ function Checkbox({ ) } +function Checkbox({ + className, + ...props +}: ComponentProps) { + const useKonsta = useKonstaOverride() + + if (!useKonsta) { + return + } + + const { checked, onCheckedChange, disabled, name, ...rest } = props + // Forward id, aria-*, data-* attributes for accessibility + const forwardedProps: Record = {} + for (const [key, val] of Object.entries(rest)) { + if (key === 'id' || key === 'value' || key.startsWith('aria-') || key.startsWith('data-')) { + forwardedProps[key] = val + } + } + + return ( + ) => onCheckedChange?.(e.target.checked)} + disabled={disabled} + name={name} + className={className} + {...forwardedProps} + /> + ) +} + export { Checkbox } diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx index e7eac9df..fe8a7d92 100644 --- a/apps/web/src/components/ui/dialog.tsx +++ b/apps/web/src/components/ui/dialog.tsx @@ -1,6 +1,7 @@ import { ComponentProps } from "react" import * as DialogPrimitive from "@radix-ui/react-dialog" import XIcon from "lucide-react/dist/esm/icons/x" +import { useKonstaOverride } from '@/hooks/useKonstaOverride' import { cn } from "@/lib/utils" @@ -44,18 +45,25 @@ function DialogOverlay({ ) } +// CSS-only adaptation: Konsta Dialog uses a fundamentally different API (imperative +// title/content/buttons props) that doesn't map to Radix's declarative portal pattern. +// Instead, we enhance Radix DialogContent with mobile-friendly styling when Konsta is active. function DialogContent({ className, children, ...props }: ComponentProps) { + const useKonsta = useKonstaOverride() + return ( ) { +function ShadcnInput({ className, type, ...props }: ComponentProps<"input">) { return ( ) { ) } +function Input({ className, type, ...props }: ComponentProps<"input">) { + const useKonsta = useKonstaOverride() + + if (!useKonsta) { + return + } + + return ( + + ) +} + export { Input } diff --git a/apps/web/src/components/ui/progress.tsx b/apps/web/src/components/ui/progress.tsx index 737bb1be..b7edc376 100644 --- a/apps/web/src/components/ui/progress.tsx +++ b/apps/web/src/components/ui/progress.tsx @@ -1,20 +1,14 @@ import { ComponentProps } from "react" import * as ProgressPrimitive from "@radix-ui/react-progress" - +import { Progressbar } from 'konsta/react' +import { useKonstaOverride } from '@/hooks/useKonstaOverride' import { cn } from "@/lib/utils" -function Progress({ - className, - value, - ...props -}: ComponentProps) { +function ShadcnProgress({ className, value, ...props }: ComponentProps) { return ( ) { + const useKonsta = useKonstaOverride() + + if (!useKonsta) { + return + } + + // Konsta Progressbar expects 0-1, shadcn Progress uses 0-100 + // Forward id, aria-*, and data-* attributes for accessibility + const { id, ...rest } = props as Record + const forwardProps: Record = {} + if (id) forwardProps.id = id + for (const [key, val] of Object.entries(rest)) { + if (key.startsWith('aria-') || key.startsWith('data-')) forwardProps[key] = val + } + + return ( + + ) +} + export { Progress } diff --git a/apps/web/src/components/ui/select.tsx b/apps/web/src/components/ui/select.tsx index 517728c2..d90ca72f 100644 --- a/apps/web/src/components/ui/select.tsx +++ b/apps/web/src/components/ui/select.tsx @@ -4,6 +4,7 @@ import CheckIcon from "lucide-react/dist/esm/icons/check" import ChevronDownIcon from "lucide-react/dist/esm/icons/chevron-down" import ChevronUpIcon from "lucide-react/dist/esm/icons/chevron-up" +import { useKonstaOverride } from '@/hooks/useKonstaOverride' import { cn } from "@/lib/utils" function Select({ @@ -33,12 +34,16 @@ function SelectTrigger({ }: ComponentProps & { size?: "sm" | "default" }) { + const useKonsta = useKonstaOverride() + return ( ) { + const useKonsta = useKonstaOverride() + + if (!useKonsta) { + return + } + + const singleValue = Array.isArray(value) ? value[0] : (Array.isArray(defaultValue) ? defaultValue[0] : min) + + // Forward id, aria-*, data-* attributes + const forwardedProps: Record = {} + for (const [key, val] of Object.entries(rest)) { + if (key === 'id' || key.startsWith('aria-') || key.startsWith('data-')) { + forwardedProps[key] = val + } + } + + return ( + ) => onValueChange?.([Number(e.target.value)])} + onChange={(e: React.ChangeEvent) => onValueCommit?.([Number(e.target.value)])} + className={className} + {...forwardedProps} + /> + ) +} + export { Slider } diff --git a/apps/web/src/components/ui/switch.tsx b/apps/web/src/components/ui/switch.tsx index 3367d083..1819b4be 100644 --- a/apps/web/src/components/ui/switch.tsx +++ b/apps/web/src/components/ui/switch.tsx @@ -2,10 +2,12 @@ import { ComponentProps } from "react" import * as SwitchPrimitive from "@radix-ui/react-switch" +import { Toggle } from 'konsta/react' +import { useKonstaOverride } from '@/hooks/useKonstaOverride' import { cn } from "@/lib/utils" -function Switch({ +function ShadcnSwitch({ className, ...props }: ComponentProps) { @@ -28,4 +30,36 @@ function Switch({ ) } +function Switch({ + className, + ...props +}: ComponentProps) { + const useKonsta = useKonstaOverride() + + if (!useKonsta) { + return + } + + const { checked, defaultChecked, onCheckedChange, disabled, ...rest } = props + + // Forward id, name, aria-*, data-* for accessibility and label association + const forwardedProps: Record = {} + for (const [key, val] of Object.entries(rest)) { + if (key === 'id' || key === 'name' || key === 'value' || key.startsWith('aria-') || key.startsWith('data-')) { + forwardedProps[key] = val + } + } + + return ( + ) => onCheckedChange?.(e.target.checked)} + disabled={disabled} + className={className} + {...(defaultChecked !== undefined ? { defaultChecked } : {})} + {...forwardedProps} + /> + ) +} + export { Switch } diff --git a/apps/web/src/components/ui/tabs.tsx b/apps/web/src/components/ui/tabs.tsx index d1a5b7a3..ebc6fec6 100644 --- a/apps/web/src/components/ui/tabs.tsx +++ b/apps/web/src/components/ui/tabs.tsx @@ -2,31 +2,24 @@ import { ComponentProps } from "react" import * as TabsPrimitive from "@radix-ui/react-tabs" - +import { useKonstaOverride } from '@/hooks/useKonstaOverride' import { cn } from "@/lib/utils" -function Tabs({ - className, - ...props -}: ComponentProps) { +function Tabs({ className, ...props }: ComponentProps) { return ( - + ) } -function TabsList({ - className, - ...props -}: ComponentProps) { +function TabsList({ className, ...props }: ComponentProps) { + const useKonsta = useKonstaOverride() return ( ) { +function TabsTrigger({ className, ...props }: ComponentProps) { + const useKonsta = useKonstaOverride() return ( ) { +function TabsContent({ className, ...props }: ComponentProps) { return ( - + ) } diff --git a/apps/web/src/hooks/useKonstaOverride.ts b/apps/web/src/hooks/useKonstaOverride.ts index a7aca3b9..8cb38f5e 100644 --- a/apps/web/src/hooks/useKonstaOverride.ts +++ b/apps/web/src/hooks/useKonstaOverride.ts @@ -12,17 +12,25 @@ function getStoredValue(): boolean { } } -// Subscribe to both cross-tab (StorageEvent) and same-tab (custom event) changes +// Shared subscription: single pair of global listeners fans out to all subscribers +const subscribers = new Set<() => void>() +let listening = false + +function ensureListeners() { + if (typeof window === 'undefined' || listening) return + window.addEventListener('storage', (e: StorageEvent) => { + if (e.key === STORAGE_KEYS.USE_KONSTA_UI) subscribers.forEach(cb => cb()) + }) + window.addEventListener(KONSTA_CHANGED, () => { + subscribers.forEach(cb => cb()) + }) + listening = true +} + function subscribe(callback: () => void) { - const storageHandler = (e: StorageEvent) => { - if (e.key === STORAGE_KEYS.USE_KONSTA_UI) callback() - } - window.addEventListener('storage', storageHandler) - window.addEventListener(KONSTA_CHANGED, callback) - return () => { - window.removeEventListener('storage', storageHandler) - window.removeEventListener(KONSTA_CHANGED, callback) - } + subscribers.add(callback) + ensureListeners() + return () => { subscribers.delete(callback) } } /** diff --git a/apps/web/vitest.config.ts b/apps/web/vitest.config.ts index c7152848..1f305e09 100644 --- a/apps/web/vitest.config.ts +++ b/apps/web/vitest.config.ts @@ -12,6 +12,11 @@ export default defineConfig({ setupFiles: './src/test/setup.ts', css: true, exclude: ['**/node_modules/**', '**/dist/**', '**/e2e/**', '**/.{idea,git,cache,output,temp}/**'], + server: { + deps: { + inline: ['konsta'], + }, + }, coverage: { provider: 'v8', reporter: ['text', 'json', 'html'],