feat(13-02): add GdriveAuthToggle and BackendIcons components

- GdriveAuthToggle: two-tab segmented control (OAuth Token / Service Account)
  following AzureAuthToggle pattern; both fields always registered via CSS
  hidden/block; OAuth tab embeds OAuthInstructions component
- BackendIcons: inline SVG icon map for all 18 backends (Partial<Record<BackendType,...>>)
  using fill="currentColor" for MD3 dark-mode compatibility, no external deps
- Created src/components/icons/ directory
This commit is contained in:
2026-04-01 16:27:14 +02:00
parent e00fc2b03b
commit 683faf6f11
2 changed files with 272 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
// BackendIcons.tsx
// Inline SVG icon map keyed by BackendType.
// All icons use fill="currentColor" for MD3 dark-mode compatibility.
// Each icon is designed for 24x24px viewBox and works as monochrome.
import type { BackendType } from '../../schemas/registry';
export interface IconProps {
className?: string;
}
// Azure Blob Storage — cloud with A shape
const AzureBlobIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M6.5 20a4.5 4.5 0 01-.5-8.97V11a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
<path d="M10 14l2-4 2 4h-1.25v2h-1.5v-2H10z" />
</svg>
);
// Amazon S3 — cloud with S shape
const S3Icon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M6.5 20a4.5 4.5 0 01-.5-8.97V11a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
<text x="8.5" y="17" fontSize="7" fontFamily="sans-serif" fontWeight="bold">S3</text>
</svg>
);
// S3-Compatible — generic cloud with layers
const S3CompatibleIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M6.5 18a4.5 4.5 0 01-.5-8.97V9a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
<rect x="9" y="14" width="6" height="1.5" rx="0.75" />
<rect x="9" y="16.5" width="6" height="1.5" rx="0.75" />
</svg>
);
// Google Cloud Storage — cloud with G
const GCSIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M6.5 20a4.5 4.5 0 01-.5-8.97V11a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
<text x="8" y="17.5" fontSize="7" fontFamily="sans-serif" fontWeight="bold">G</text>
</svg>
);
// Backblaze B2 — cloud with B2
const B2Icon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M6.5 20a4.5 4.5 0 01-.5-8.97V11a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
<text x="7.5" y="17.5" fontSize="6.5" fontFamily="sans-serif" fontWeight="bold">B2</text>
</svg>
);
// Azure Files — file-share icon (folder with network lines)
const AzureFilesIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M2 6.5A2.5 2.5 0 014.5 4H9l2 2h8.5A2.5 2.5 0 0122 8.5v9a2.5 2.5 0 01-2.5 2.5h-15A2.5 2.5 0 012 17.5V6.5z" />
<path fillRule="evenodd" d="M8 12h8v1.5H8V12zm2 2.5h4V16h-4v-1.5z" clipRule="evenodd" />
</svg>
);
// OpenStack Swift — server/globe icon
const SwiftIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<circle cx="12" cy="12" r="9" fillOpacity="0.15" />
<path d="M12 3a9 9 0 100 18A9 9 0 0012 3zm0 1.5c.9 0 1.9.6 2.7 1.7H9.3C10.1 5.1 11.1 4.5 12 4.5zm-4.5 3.2h9a7.5 7.5 0 010 8.6h-9a7.5 7.5 0 010-8.6z" />
<path d="M3 12h18M12 3v18" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.4" />
</svg>
);
// OneDrive — cloud with one-drive O shape
const OnedriveIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M14.5 8a5.5 5.5 0 015.46 4.83A4 4 0 0120 20H7a5 5 0 01-1-9.9V10a7.5 7.5 0 017.5-7.5V8z" />
<path d="M4 14a5 5 0 015-5h1a5.5 5.5 0 00-1 3.16A4 4 0 014.5 20H4a4 4 0 010-8z" fillOpacity="0.7" />
</svg>
);
// Google Drive — drive/triangle icon
const GdriveIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M8.5 3.5L2 15h5l6.5-11.5H8.5z" />
<path d="M22 15l-6.5-11.5h-1.5L22 15z" fillOpacity="0.7" />
<path d="M2 15l4 6.5h12L22 15H2z" fillOpacity="0.5" />
</svg>
);
// Dropbox — box icon
const DropboxIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M12 4.5L6 8l6 3.5 6-3.5-6-3.5z" />
<path d="M6 8l-4 2.5 6 3.5 4-2.5L6 8z" fillOpacity="0.7" />
<path d="M18 8l-6 3.5 4 2.5 6-3.5L18 8z" fillOpacity="0.7" />
<path d="M8 15l4 2.5 4-2.5-4-2.5L8 15z" />
<circle cx="12" cy="20" r="1.5" />
</svg>
);
// Box — stylized box/square icon
const BoxIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<rect x="3" y="8" width="18" height="13" rx="2" />
<path d="M9 8V5a3 3 0 016 0v3" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
<circle cx="12" cy="15" r="2" fill="none" stroke="currentColor" strokeWidth="1.5" />
</svg>
);
// pCloud — cloud with p letter
const PcloudIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M6.5 20a4.5 4.5 0 01-.5-8.97V11a6 6 0 1111.9 1H18a4 4 0 010 8H6.5z" />
<text x="9" y="17.5" fontSize="7" fontFamily="sans-serif" fontWeight="bold">p</text>
</svg>
);
// SFTP — server with lock icon
const SftpIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<rect x="3" y="4" width="18" height="5" rx="1" />
<rect x="3" y="11" width="18" height="5" rx="1" fillOpacity="0.6" />
<circle cx="19" cy="6.5" r="1" fill="white" fillOpacity="0.8" />
<circle cx="19" cy="13.5" r="1" fill="white" fillOpacity="0.8" />
<path d="M12 18a2.5 2.5 0 012.5 2.5v1h-5v-1A2.5 2.5 0 0112 18z" />
<rect x="10" y="16" width="4" height="3" rx="1" />
</svg>
);
// FTP — server icon (simple)
const FtpIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<rect x="3" y="4" width="18" height="5" rx="1" />
<rect x="3" y="11" width="18" height="5" rx="1" fillOpacity="0.6" />
<rect x="3" y="18" width="18" height="3" rx="1" fillOpacity="0.3" />
<circle cx="19" cy="6.5" r="1" fill="white" fillOpacity="0.8" />
<circle cx="19" cy="13.5" r="1" fill="white" fillOpacity="0.8" />
</svg>
);
// WebDAV — globe with network lines
const WebdavIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M12 2a10 10 0 100 20A10 10 0 0012 2zm0 1.5c1 0 2.3.8 3.3 2.5H8.7C9.7 4.3 11 3.5 12 3.5zm-6.5 4h13a8.5 8.5 0 010 9H5.5a8.5 8.5 0 010-9z" fillOpacity="0.8" />
<path d="M2 12h20M12 2v20" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.3" />
</svg>
);
// SMB / Windows Share — folder with network
const SmbIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M2 6.5A2.5 2.5 0 014.5 4H9l2 2h8.5A2.5 2.5 0 0122 8.5v9a2.5 2.5 0 01-2.5 2.5h-15A2.5 2.5 0 012 17.5V6.5z" />
<circle cx="9" cy="14" r="1.5" fill="white" fillOpacity="0.7" />
<circle cx="15" cy="14" r="1.5" fill="white" fillOpacity="0.7" />
<path d="M9 14h6" stroke="white" strokeWidth="1" strokeOpacity="0.7" />
</svg>
);
// HTTP — globe/web icon
const HttpIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<path d="M12 2a10 10 0 100 20A10 10 0 0012 2zm0 1.5c1.1 0 2.5.9 3.5 2.5H8.5C9.5 4.4 10.9 3.5 12 3.5zm-7 5h14a8.5 8.5 0 010 7H5a8.5 8.5 0 010-7z" />
<path d="M2 12h20" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.5" />
</svg>
);
// Seafile — sea/wave icon
const SeafileIcon = ({ className }: IconProps) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={className}>
<rect x="4" y="4" width="16" height="11" rx="2" fillOpacity="0.8" />
<path d="M4 19c2-2 4-2 6 0s4 2 6 0" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" />
<path d="M4 22c2-2 4-2 6 0s4 2 6 0" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" opacity="0.4" />
</svg>
);
export const BACKEND_ICONS: Partial<Record<BackendType, React.FC<IconProps>>> = {
azureblob: AzureBlobIcon,
s3: S3Icon,
's3-compatible': S3CompatibleIcon,
gcs: GCSIcon,
b2: B2Icon,
'azure-files': AzureFilesIcon,
swift: SwiftIcon,
onedrive: OnedriveIcon,
gdrive: GdriveIcon,
dropbox: DropboxIcon,
box: BoxIcon,
pcloud: PcloudIcon,
sftp: SftpIcon,
ftp: FtpIcon,
webdav: WebdavIcon,
smb: SmbIcon,
http: HttpIcon,
seafile: SeafileIcon,
};
@@ -0,0 +1,80 @@
import { useState } from 'react';
import type { UseFormRegister, FieldError } from 'react-hook-form';
import { PasswordField } from '../ui/PasswordField';
import { OAuthInstructions } from './OAuthInstructions';
type GdriveAuthMethod = 'oauth' | 'service-account';
interface GdriveAuthToggleProps {
register: UseFormRegister<any>;
errors: {
token?: FieldError;
service_account_credentials?: FieldError;
};
}
export function GdriveAuthToggle({ register, errors }: GdriveAuthToggleProps) {
const [authMethod, setAuthMethod] = useState<GdriveAuthMethod>('oauth');
return (
<div className="flex flex-col gap-3">
{/* Segmented control */}
<div className="flex rounded-md border border-outline overflow-hidden">
<button
type="button"
onClick={() => setAuthMethod('oauth')}
className={[
'flex-1 py-1.5 text-sm font-medium transition-colors',
authMethod === 'oauth'
? 'bg-primary text-on-primary'
: 'bg-surface-container text-on-surface-container hover:bg-surface',
].join(' ')}
>
OAuth Token
</button>
<button
type="button"
onClick={() => setAuthMethod('service-account')}
className={[
'flex-1 py-1.5 text-sm font-medium transition-colors',
authMethod === 'service-account'
? 'bg-primary text-on-primary'
: 'bg-surface-container text-on-surface-container hover:bg-surface',
].join(' ')}
>
Service Account
</button>
</div>
{/* Both fields always registered — only active one visible via CSS */}
<div className={authMethod === 'oauth' ? 'block' : 'hidden'}>
<div className="flex flex-col gap-3">
<OAuthInstructions
backendName="Google Drive"
authorizeCommand='rclone authorize "drive"'
/>
<PasswordField
id="token"
label="OAuth Token (JSON)"
error={errors.token}
registration={register('token')}
placeholder='{"access_token":"...","token_type":"Bearer","refresh_token":"...","expiry":"..."}'
helpText='Paste the JSON token from: rclone authorize "drive"'
tooltipText='Run `rclone authorize "drive"` on a machine with a browser. A browser window opens, you sign in with Google, and rclone prints a JSON token — paste that entire JSON blob here.'
/>
</div>
</div>
<div className={authMethod === 'service-account' ? 'block' : 'hidden'}>
<PasswordField
id="service_account_credentials"
label="Service Account JSON"
error={errors.service_account_credentials}
registration={register('service_account_credentials')}
placeholder='{"type":"service_account","project_id":"..."}'
helpText="For unattended/server use: paste the full service account JSON key file content. Use this OR an OAuth token, not both."
tooltipText="Create a service account at Google Cloud Console > IAM & Admin > Service Accounts, share the target Drive with that service account email, then download the JSON key and paste its contents here."
/>
</div>
</div>
);
}