fix: Amber Capacitor APK + mobile UI improvements
Amber login (critical): - Replace window.location.href with window.open(_system) in Capacitor WebViews - Capacitor's WebView throws "malformed nostrsigner request" on href navigation - New openAmberUrl() helper detects Capacitor via window.Capacitor.isNativePlatform() - Add openAmberForLoginNoCallback() for PWA/Capacitor clipboard flow - Fix all 6 Amber URL opens in amber.ts + LoginDialog.tsx Mobile nav: - Newsletter and Sasquatch AI (/pyramid) added to hamburger menu - Sasquatch floating guide now shows on mobile (removed hidden md:block) Lab page mobile: - Action buttons (Offer Coaching/Filming/Editing/Get Card) now 2-col grid on mobile - Service type filter (Coaches/Filmers/Editors) now 3-col grid, full width - Reduced top padding on mobile (py-6 → py-12 on md+) - Text truncates cleanly, icons don't overflow Wallet page mobile: - Reduced padding (py-6 md:py-8) - Tab triggers use h-auto + py-2.5 to avoid overflow - Smaller header text on mobile Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
576df73a6b
commit
a677e8687d
5 changed files with 105 additions and 68 deletions
|
|
@ -45,6 +45,8 @@ import {
|
||||||
Zap,
|
Zap,
|
||||||
Heart,
|
Heart,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
|
Radio,
|
||||||
|
Bot,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { OfflineIndicator } from '@/components/OfflineIndicator';
|
import { OfflineIndicator } from '@/components/OfflineIndicator';
|
||||||
import { UserStatusIndicator } from '@/components/UserStatusIndicator';
|
import { UserStatusIndicator } from '@/components/UserStatusIndicator';
|
||||||
|
|
@ -680,6 +682,20 @@ export function Layout({ children }: LayoutProps) {
|
||||||
isActive={isActive('/supporters')}
|
isActive={isActive('/supporters')}
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
<NavLink
|
||||||
|
href="/newsletter"
|
||||||
|
label="Newsletter"
|
||||||
|
icon={Radio}
|
||||||
|
isActive={isActive('/newsletter')}
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
/>
|
||||||
|
<NavLink
|
||||||
|
href="/pyramid"
|
||||||
|
label="Sasquatch AI"
|
||||||
|
icon={Bot}
|
||||||
|
isActive={isActive('/pyramid')}
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
/>
|
||||||
<NavLink
|
<NavLink
|
||||||
href="/settings"
|
href="/settings"
|
||||||
label={t('nav.settings')}
|
label={t('nav.settings')}
|
||||||
|
|
@ -811,13 +827,11 @@ export function Layout({ children }: LayoutProps) {
|
||||||
t={t}
|
t={t}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Sasquatch Guide (Desktop only) */}
|
{/* Sasquatch Guide (all screens) */}
|
||||||
<div className="hidden md:block">
|
<SasquatchGuide
|
||||||
<SasquatchGuide
|
message={currentMessage}
|
||||||
message={currentMessage}
|
onDismiss={dismissCurrentMessage}
|
||||||
onDismiss={dismissCurrentMessage}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Set Status Dialog */}
|
{/* Set Status Dialog */}
|
||||||
<SetStatusDialog
|
<SetStatusDialog
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { useLoginActions } from '@/hooks/useLoginActions';
|
import { useLoginActions } from '@/hooks/useLoginActions';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { subscribeToAmberReturn } from '@/lib/amber';
|
import { subscribeToAmberReturn, openAmberForLogin, openAmberForLoginNoCallback } from '@/lib/amber';
|
||||||
|
|
||||||
interface LoginDialogProps {
|
interface LoginDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
@ -96,16 +96,13 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin, onS
|
||||||
|| ('standalone' in window.navigator && (window.navigator as { standalone?: boolean }).standalone === true);
|
|| ('standalone' in window.navigator && (window.navigator as { standalone?: boolean }).standalone === true);
|
||||||
|
|
||||||
if (isPWA) {
|
if (isPWA) {
|
||||||
// ── PWA mode ──────────────────────────────────────────────────────────
|
// ── PWA / Capacitor mode ───────────────────────────────────────────────
|
||||||
// When installed to home screen, the app is single-tab. Opening
|
// When installed to home screen or running as a Capacitor APK, the app
|
||||||
// nostrsigner: with a callbackUrl sends the callback to the browser,
|
// is single-tab. Use openAmberForLogin() which handles Capacitor WebViews
|
||||||
// not back to the PWA — so the PWA never sees the result.
|
// correctly via window.open(_system) instead of window.location.href.
|
||||||
//
|
// Amber copies the pubkey to clipboard when there's no callback.
|
||||||
// Instead: open Amber WITHOUT a callbackUrl. Amber copies the pubkey
|
// When the user comes back, visibilitychange fires and we read the clipboard.
|
||||||
// to clipboard when there's no callback. When the user comes back,
|
openAmberForLoginNoCallback();
|
||||||
// the visibilitychange fires and we auto-read the clipboard.
|
|
||||||
const amberUrl = 'nostrsigner:?compressionType=none&returnType=signature&type=get_public_key';
|
|
||||||
window.location.href = amberUrl;
|
|
||||||
|
|
||||||
// When user returns to the PWA after approving in Amber, auto-read clipboard
|
// When user returns to the PWA after approving in Amber, auto-read clipboard
|
||||||
unsubAmberRef.current = subscribeToAmberReturn(async () => {
|
unsubAmberRef.current = subscribeToAmberReturn(async () => {
|
||||||
|
|
@ -166,9 +163,8 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin, onS
|
||||||
unsubAmberRef.current = null;
|
unsubAmberRef.current = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const callbackUrl = `${window.location.origin}/amber-callback?event=`;
|
// Browser (tab) mode — use the callback URL flow
|
||||||
const amberUrl = `nostrsigner:?compressionType=none&returnType=signature&type=get_public_key&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
openAmberForLogin();
|
||||||
window.location.href = amberUrl;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBunkerLogin = async () => {
|
const handleBunkerLogin = async () => {
|
||||||
|
|
|
||||||
|
|
@ -246,6 +246,32 @@ function buildCallbackUrl(): string {
|
||||||
return `${window.location.origin}/amber-callback?event=`;
|
return `${window.location.origin}/amber-callback?event=`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to an Amber nostrsigner: URL.
|
||||||
|
* In Capacitor WebViews, window.location.href causes a "malformed request"
|
||||||
|
* error because the WebView tries to navigate instead of firing an intent.
|
||||||
|
* Use window.open() with _system target to hand off to Android properly.
|
||||||
|
*/
|
||||||
|
function openAmberUrl(url: string): void {
|
||||||
|
// Capacitor / Android WebView: use _system to fire the intent
|
||||||
|
const isCapacitor = !!(window as unknown as { Capacitor?: { isNativePlatform?: () => boolean } }).Capacitor?.isNativePlatform?.();
|
||||||
|
if (isCapacitor) {
|
||||||
|
window.open(url, '_system');
|
||||||
|
} else {
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open Amber for get_public_key WITHOUT a callback URL.
|
||||||
|
* Used in PWA/Capacitor mode: Amber copies pubkey to clipboard,
|
||||||
|
* the app reads it on visibilitychange.
|
||||||
|
*/
|
||||||
|
export function openAmberForLoginNoCallback(): void {
|
||||||
|
const amberUrl = 'nostrsigner:?compressionType=none&returnType=signature&type=get_public_key';
|
||||||
|
openAmberUrl(amberUrl);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open Amber for get_public_key operation.
|
* Open Amber for get_public_key operation.
|
||||||
* Uses a dedicated /amber-callback route so the app state is preserved
|
* Uses a dedicated /amber-callback route so the app state is preserved
|
||||||
|
|
@ -267,7 +293,7 @@ export function openAmberForLogin(): void {
|
||||||
const amberUrl = `nostrsigner:?compressionType=none&returnType=signature&type=get_public_key&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
const amberUrl = `nostrsigner:?compressionType=none&returnType=signature&type=get_public_key&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
||||||
|
|
||||||
console.log('[Amber] Opening Amber for login, callback:', callbackUrl);
|
console.log('[Amber] Opening Amber for login, callback:', callbackUrl);
|
||||||
window.location.href = amberUrl;
|
openAmberUrl(amberUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -309,7 +335,7 @@ export function openAmberForSignEvent(event: Omit<NostrEvent, 'id' | 'sig'>): vo
|
||||||
const encodedEvent = encodeURIComponent(eventJson);
|
const encodedEvent = encodeURIComponent(eventJson);
|
||||||
const amberUrl = `nostrsigner:${encodedEvent}?compressionType=none&returnType=event&type=sign_event&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
const amberUrl = `nostrsigner:${encodedEvent}?compressionType=none&returnType=event&type=sign_event&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
||||||
|
|
||||||
window.location.href = amberUrl;
|
openAmberUrl(amberUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -330,7 +356,7 @@ export function openAmberForNip04Encrypt(pubkey: string, plaintext: string): voi
|
||||||
const encodedText = encodeURIComponent(plaintext);
|
const encodedText = encodeURIComponent(plaintext);
|
||||||
const amberUrl = `nostrsigner:${encodedText}?pubkey=${pubkey}&compressionType=none&returnType=signature&type=nip04_encrypt&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
const amberUrl = `nostrsigner:${encodedText}?pubkey=${pubkey}&compressionType=none&returnType=signature&type=nip04_encrypt&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
||||||
|
|
||||||
window.location.href = amberUrl;
|
openAmberUrl(amberUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -351,7 +377,7 @@ export function openAmberForNip04Decrypt(pubkey: string, ciphertext: string): vo
|
||||||
const encodedText = encodeURIComponent(ciphertext);
|
const encodedText = encodeURIComponent(ciphertext);
|
||||||
const amberUrl = `nostrsigner:${encodedText}?pubkey=${pubkey}&compressionType=none&returnType=signature&type=nip04_decrypt&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
const amberUrl = `nostrsigner:${encodedText}?pubkey=${pubkey}&compressionType=none&returnType=signature&type=nip04_decrypt&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
||||||
|
|
||||||
window.location.href = amberUrl;
|
openAmberUrl(amberUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -372,7 +398,7 @@ export function openAmberForNip44Encrypt(pubkey: string, plaintext: string): voi
|
||||||
const encodedText = encodeURIComponent(plaintext);
|
const encodedText = encodeURIComponent(plaintext);
|
||||||
const amberUrl = `nostrsigner:${encodedText}?pubkey=${pubkey}&compressionType=none&returnType=signature&type=nip44_encrypt&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
const amberUrl = `nostrsigner:${encodedText}?pubkey=${pubkey}&compressionType=none&returnType=signature&type=nip44_encrypt&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
||||||
|
|
||||||
window.location.href = amberUrl;
|
openAmberUrl(amberUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -393,7 +419,7 @@ export function openAmberForNip44Decrypt(pubkey: string, ciphertext: string): vo
|
||||||
const encodedText = encodeURIComponent(ciphertext);
|
const encodedText = encodeURIComponent(ciphertext);
|
||||||
const amberUrl = `nostrsigner:${encodedText}?pubkey=${pubkey}&compressionType=none&returnType=signature&type=nip44_decrypt&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
const amberUrl = `nostrsigner:${encodedText}?pubkey=${pubkey}&compressionType=none&returnType=signature&type=nip44_decrypt&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
||||||
|
|
||||||
window.location.href = amberUrl;
|
openAmberUrl(amberUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -102,8 +102,8 @@ export default function LabPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="container px-4 py-12">
|
<div className="container px-4 py-6 md:py-12">
|
||||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 mb-8">
|
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 mb-6 md:mb-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-4xl font-black uppercase mb-2 neon-header">
|
<h1 className="text-4xl font-black uppercase mb-2 neon-header">
|
||||||
<span className="neon-red">THE</span> <span className="neon-orange">LAB</span>
|
<span className="neon-red">THE</span> <span className="neon-orange">LAB</span>
|
||||||
|
|
@ -111,29 +111,29 @@ export default function LabPage() {
|
||||||
<p className="text-gray-400">{t('labPage.subtitle')}</p>
|
<p className="text-gray-400">{t('labPage.subtitle')}</p>
|
||||||
</div>
|
</div>
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="grid grid-cols-2 sm:flex sm:flex-wrap gap-2 w-full md:w-auto">
|
||||||
<Link to="/become-coach">
|
<Link to="/become-coach" className="col-span-1">
|
||||||
<Button className="bg-red-500 hover:bg-red-600 gap-2 uppercase font-black">
|
<Button className="w-full bg-red-500 hover:bg-red-600 gap-1.5 uppercase font-black text-xs sm:text-sm">
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
{t('labPage.offerCoaching')}
|
<span className="truncate">{t('labPage.offerCoaching')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/become-filmer">
|
<Link to="/become-filmer" className="col-span-1">
|
||||||
<Button className="bg-orange-500 hover:bg-orange-600 gap-2 uppercase font-black">
|
<Button className="w-full bg-orange-500 hover:bg-orange-600 gap-1.5 uppercase font-black text-xs sm:text-sm">
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
{t('labPage.offerFilming')}
|
<span className="truncate">{t('labPage.offerFilming')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/become-editor">
|
<Link to="/become-editor" className="col-span-1">
|
||||||
<Button className="bg-purple-600 hover:bg-purple-700 gap-2 uppercase font-black">
|
<Button className="w-full bg-purple-600 hover:bg-purple-700 gap-1.5 uppercase font-black text-xs sm:text-sm">
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
Offer Editing
|
<span className="truncate">Offer Editing</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/card">
|
<Link to="/card" className="col-span-1">
|
||||||
<Button variant="outline" className="gap-2 uppercase font-black border-[#ccff00]/50 text-[#ccff00] hover:bg-[#ccff00]/10">
|
<Button variant="outline" className="w-full gap-1.5 uppercase font-black border-[#ccff00]/50 text-[#ccff00] hover:bg-[#ccff00]/10 text-xs sm:text-sm">
|
||||||
<CreditCard className="h-4 w-4" />
|
<CreditCard className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
Get Your Card
|
<span className="truncate">Get Your Card</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -145,7 +145,7 @@ export default function LabPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Service Type Filter */}
|
{/* Service Type Filter */}
|
||||||
<div className="flex gap-2 mb-6">
|
<div className="grid grid-cols-3 gap-2 mb-6">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setServiceType('coaching');
|
setServiceType('coaching');
|
||||||
|
|
@ -153,12 +153,12 @@ export default function LabPage() {
|
||||||
}}
|
}}
|
||||||
variant={serviceType === 'coaching' ? 'default' : 'outline'}
|
variant={serviceType === 'coaching' ? 'default' : 'outline'}
|
||||||
className={cn(
|
className={cn(
|
||||||
'uppercase font-bold gap-2',
|
'uppercase font-bold gap-1.5 text-xs sm:text-sm',
|
||||||
serviceType === 'coaching' ? 'bg-red-500 hover:bg-red-600' : 'border-gray-700'
|
serviceType === 'coaching' ? 'bg-red-500 hover:bg-red-600' : 'border-gray-700'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<GraduationCap className="h-4 w-4" />
|
<GraduationCap className="h-4 w-4 flex-shrink-0" />
|
||||||
{t('labPage.coachesTab')}
|
<span className="truncate">{t('labPage.coachesTab')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -167,12 +167,12 @@ export default function LabPage() {
|
||||||
}}
|
}}
|
||||||
variant={serviceType === 'filming' ? 'default' : 'outline'}
|
variant={serviceType === 'filming' ? 'default' : 'outline'}
|
||||||
className={cn(
|
className={cn(
|
||||||
'uppercase font-bold gap-2',
|
'uppercase font-bold gap-1.5 text-xs sm:text-sm',
|
||||||
serviceType === 'filming' ? 'bg-orange-500 hover:bg-orange-600' : 'border-gray-700'
|
serviceType === 'filming' ? 'bg-orange-500 hover:bg-orange-600' : 'border-gray-700'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Video className="h-4 w-4" />
|
<Video className="h-4 w-4 flex-shrink-0" />
|
||||||
{t('labPage.filmersTab')}
|
<span className="truncate">{t('labPage.filmersTab')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -181,12 +181,13 @@ export default function LabPage() {
|
||||||
}}
|
}}
|
||||||
variant={serviceType === 'editing' ? 'default' : 'outline'}
|
variant={serviceType === 'editing' ? 'default' : 'outline'}
|
||||||
className={cn(
|
className={cn(
|
||||||
'uppercase font-bold gap-2',
|
'uppercase font-bold gap-1.5 text-xs sm:text-sm',
|
||||||
serviceType === 'editing' ? 'bg-purple-600 hover:bg-purple-700' : 'border-gray-700'
|
serviceType === 'editing' ? 'bg-purple-600 hover:bg-purple-700' : 'border-gray-700'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Scissors className="h-4 w-4" />
|
<Scissors className="h-4 w-4 flex-shrink-0" />
|
||||||
Video Editors
|
<span className="truncate hidden sm:inline">Video Editors</span>
|
||||||
|
<span className="truncate sm:hidden">Editors</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -295,29 +295,29 @@ export default function WalletPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="container px-4 py-8">
|
<div className="container px-4 py-6 md:py-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-6 md:mb-8">
|
||||||
<h1 className="text-3xl font-bold">Wallet</h1>
|
<h1 className="text-2xl md:text-3xl font-bold">Wallet</h1>
|
||||||
<p className="text-muted-foreground mt-1">
|
<p className="text-muted-foreground mt-1 text-sm md:text-base">
|
||||||
Manage your Bitcoin and send zaps to skaters and creators
|
Manage your Bitcoin and send zaps to skaters and creators
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4 md:space-y-6">
|
||||||
{/* Wallet Comparison Guide */}
|
{/* Wallet Comparison Guide */}
|
||||||
<WalletComparisonGuide />
|
<WalletComparisonGuide />
|
||||||
|
|
||||||
<Tabs defaultValue="cashu" className="w-full">
|
<Tabs defaultValue="cashu" className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
<TabsList className="grid w-full grid-cols-2 mb-4 md:mb-6 h-auto">
|
||||||
<TabsTrigger value="cashu" className="gap-2">
|
<TabsTrigger value="cashu" className="gap-1.5 py-2.5 text-sm">
|
||||||
<Nut className="h-4 w-4" />
|
<Nut className="h-4 w-4 flex-shrink-0" />
|
||||||
Cashu Wallet
|
<span>Cashu Wallet</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="lightning" className="gap-2">
|
<TabsTrigger value="lightning" className="gap-1.5 py-2.5 text-sm">
|
||||||
<Zap className="h-4 w-4" />
|
<Zap className="h-4 w-4 flex-shrink-0" />
|
||||||
Lightning
|
<span>Lightning</span>
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue