style: apply prettier formatting to all files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,19 +1,19 @@
|
||||
import { getRecording } from "$lib/services";
|
||||
|
||||
export async function load({ locals, url, fetch }) {
|
||||
const recordingId = url.searchParams.get("recording");
|
||||
const recordingId = url.searchParams.get("recording");
|
||||
|
||||
let recording = null;
|
||||
if (recordingId && locals.authStatus.authenticated) {
|
||||
try {
|
||||
recording = await getRecording(recordingId, fetch);
|
||||
} catch (error) {
|
||||
console.error("Failed to load recording:", error);
|
||||
}
|
||||
}
|
||||
let recording = null;
|
||||
if (recordingId && locals.authStatus.authenticated) {
|
||||
try {
|
||||
recording = await getRecording(recordingId, fetch);
|
||||
} catch (error) {
|
||||
console.error("Failed to load recording:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
authStatus: locals.authStatus,
|
||||
recording,
|
||||
};
|
||||
return {
|
||||
authStatus: locals.authStatus,
|
||||
recording,
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,171 +1,166 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import { SvelteMap } from "svelte/reactivity";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import type { BluetoothDevice, DeviceInfo } from "$lib/types";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { SvelteMap } from "svelte/reactivity";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import type { BluetoothDevice, DeviceInfo } from "$lib/types";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
recordedDevices: DeviceInfo[];
|
||||
connectedDevices: BluetoothDevice[];
|
||||
onConfirm: (mappings: Map<string, BluetoothDevice>) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
interface Props {
|
||||
open: boolean;
|
||||
recordedDevices: DeviceInfo[];
|
||||
connectedDevices: BluetoothDevice[];
|
||||
onConfirm: (mappings: Map<string, BluetoothDevice>) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
let { open, recordedDevices, connectedDevices, onConfirm, onCancel }: Props = $props();
|
||||
let { open, recordedDevices, connectedDevices, onConfirm, onCancel }: Props = $props();
|
||||
|
||||
// Device mappings: recorded device name -> connected device
|
||||
let mappings = new SvelteMap<string, BluetoothDevice>();
|
||||
// Device mappings: recorded device name -> connected device
|
||||
let mappings = new SvelteMap<string, BluetoothDevice>();
|
||||
|
||||
// Check if a connected device is compatible with a recorded device
|
||||
function isCompatible(recordedDevice: DeviceInfo, connectedDevice: BluetoothDevice): boolean {
|
||||
const connectedActuators = connectedDevice.actuators.map(
|
||||
(a) => a.outputType,
|
||||
);
|
||||
// Check if a connected device is compatible with a recorded device
|
||||
function isCompatible(recordedDevice: DeviceInfo, connectedDevice: BluetoothDevice): boolean {
|
||||
const connectedActuators = connectedDevice.actuators.map((a) => a.outputType);
|
||||
|
||||
// Check if all required actuator types from recording exist on connected device
|
||||
return recordedDevice.capabilities.every(requiredType =>
|
||||
connectedActuators.includes(requiredType)
|
||||
);
|
||||
}
|
||||
// Check if all required actuator types from recording exist on connected device
|
||||
return recordedDevice.capabilities.every((requiredType) =>
|
||||
connectedActuators.includes(requiredType),
|
||||
);
|
||||
}
|
||||
|
||||
// Get compatible devices for a recorded device
|
||||
function getCompatibleDevices(recordedDevice: DeviceInfo): BluetoothDevice[] {
|
||||
return connectedDevices.filter(device => isCompatible(recordedDevice, device));
|
||||
}
|
||||
// Get compatible devices for a recorded device
|
||||
function getCompatibleDevices(recordedDevice: DeviceInfo): BluetoothDevice[] {
|
||||
return connectedDevices.filter((device) => isCompatible(recordedDevice, device));
|
||||
}
|
||||
|
||||
// Auto-map devices on open
|
||||
$effect(() => {
|
||||
if (open && recordedDevices.length > 0 && connectedDevices.length > 0) {
|
||||
const newMappings = new SvelteMap<string, BluetoothDevice>();
|
||||
// Auto-map devices on open
|
||||
$effect(() => {
|
||||
if (open && recordedDevices.length > 0 && connectedDevices.length > 0) {
|
||||
const newMappings = new SvelteMap<string, BluetoothDevice>();
|
||||
|
||||
recordedDevices.forEach(recordedDevice => {
|
||||
// Try to find exact name match first
|
||||
let match = connectedDevices.find(d => d.name === recordedDevice.name);
|
||||
recordedDevices.forEach((recordedDevice) => {
|
||||
// Try to find exact name match first
|
||||
let match = connectedDevices.find((d) => d.name === recordedDevice.name);
|
||||
|
||||
// If no exact match, find first compatible device
|
||||
if (!match) {
|
||||
const compatible = getCompatibleDevices(recordedDevice);
|
||||
if (compatible.length > 0) {
|
||||
match = compatible[0];
|
||||
}
|
||||
}
|
||||
// If no exact match, find first compatible device
|
||||
if (!match) {
|
||||
const compatible = getCompatibleDevices(recordedDevice);
|
||||
if (compatible.length > 0) {
|
||||
match = compatible[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (match) {
|
||||
newMappings.set(recordedDevice.name, match);
|
||||
}
|
||||
});
|
||||
if (match) {
|
||||
newMappings.set(recordedDevice.name, match);
|
||||
}
|
||||
});
|
||||
|
||||
mappings = newMappings;
|
||||
}
|
||||
});
|
||||
mappings = newMappings;
|
||||
}
|
||||
});
|
||||
|
||||
function handleConfirm() {
|
||||
// Validate that all devices are mapped
|
||||
const allMapped = recordedDevices.every(rd => mappings.has(rd.name));
|
||||
if (!allMapped) {
|
||||
return;
|
||||
}
|
||||
onConfirm(mappings);
|
||||
}
|
||||
function handleConfirm() {
|
||||
// Validate that all devices are mapped
|
||||
const allMapped = recordedDevices.every((rd) => mappings.has(rd.name));
|
||||
if (!allMapped) {
|
||||
return;
|
||||
}
|
||||
onConfirm(mappings);
|
||||
}
|
||||
|
||||
function handleDeviceSelect(recordedDeviceName: string, deviceId: string) {
|
||||
if (!deviceId) return;
|
||||
function handleDeviceSelect(recordedDeviceName: string, deviceId: string) {
|
||||
if (!deviceId) return;
|
||||
|
||||
const device = connectedDevices.find(d => d.id === deviceId);
|
||||
if (device) {
|
||||
const newMappings = new SvelteMap(mappings);
|
||||
newMappings.set(recordedDeviceName, device);
|
||||
mappings = newMappings;
|
||||
}
|
||||
}
|
||||
const device = connectedDevices.find((d) => d.id === deviceId);
|
||||
if (device) {
|
||||
const newMappings = new SvelteMap(mappings);
|
||||
newMappings.set(recordedDeviceName, device);
|
||||
mappings = newMappings;
|
||||
}
|
||||
}
|
||||
|
||||
const allDevicesMapped = $derived(
|
||||
recordedDevices.every(rd => mappings.has(rd.name))
|
||||
);
|
||||
const allDevicesMapped = $derived(recordedDevices.every((rd) => mappings.has(rd.name)));
|
||||
</script>
|
||||
|
||||
<Dialog.Root {open}>
|
||||
<Dialog.Content class="max-w-2xl">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Map Devices for Playback</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Assign your connected devices to match the recorded devices. Only compatible devices are shown.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Dialog.Content class="max-w-2xl">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Map Devices for Playback</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Assign your connected devices to match the recorded devices. Only compatible devices are
|
||||
shown.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="space-y-4 py-4">
|
||||
{#each recordedDevices as recordedDevice (recordedDevice.name)}
|
||||
{@const compatibleDevices = getCompatibleDevices(recordedDevice)}
|
||||
{@const currentMapping = mappings.get(recordedDevice.name)}
|
||||
<div class="space-y-4 py-4">
|
||||
{#each recordedDevices as recordedDevice (recordedDevice.name)}
|
||||
{@const compatibleDevices = getCompatibleDevices(recordedDevice)}
|
||||
{@const currentMapping = mappings.get(recordedDevice.name)}
|
||||
|
||||
<div class="flex items-center gap-4 p-4 bg-muted/30 rounded-lg border border-border/50">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="icon-[ri--router-line] w-5 h-5 text-primary"></span>
|
||||
<h3 class="font-semibold text-card-foreground">{recordedDevice.name}</h3>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each recordedDevice.capabilities as capability (capability)}
|
||||
<span class="text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20">
|
||||
{capability}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 p-4 bg-muted/30 rounded-lg border border-border/50">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="icon-[ri--router-line] w-5 h-5 text-primary"></span>
|
||||
<h3 class="font-semibold text-card-foreground">{recordedDevice.name}</h3>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each recordedDevice.capabilities as capability (capability)}
|
||||
<span
|
||||
class="text-xs px-2 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20"
|
||||
>
|
||||
{capability}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-px h-12 bg-border"></div>
|
||||
<div class="w-px h-12 bg-border"></div>
|
||||
|
||||
<div class="flex-1">
|
||||
{#if compatibleDevices.length === 0}
|
||||
<div class="flex items-center gap-2 text-destructive">
|
||||
<span class="icon-[ri--error-warning-line] w-5 h-5"></span>
|
||||
<span class="text-sm">No compatible devices</span>
|
||||
</div>
|
||||
{:else}
|
||||
<select
|
||||
class="w-full px-3 py-2 rounded-md border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
value={currentMapping?.id || ''}
|
||||
onchange={(e) => handleDeviceSelect(recordedDevice.name, e.currentTarget.value)}
|
||||
>
|
||||
<option value="" disabled>Select device...</option>
|
||||
{#each compatibleDevices as device (device.name)}
|
||||
<option value={device.id}>
|
||||
{device.name}
|
||||
{#if device.name === recordedDevice.name}(exact match){/if}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="flex-1">
|
||||
{#if compatibleDevices.length === 0}
|
||||
<div class="flex items-center gap-2 text-destructive">
|
||||
<span class="icon-[ri--error-warning-line] w-5 h-5"></span>
|
||||
<span class="text-sm">No compatible devices</span>
|
||||
</div>
|
||||
{:else}
|
||||
<select
|
||||
class="w-full px-3 py-2 rounded-md border border-border bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
value={currentMapping?.id || ""}
|
||||
onchange={(e) => handleDeviceSelect(recordedDevice.name, e.currentTarget.value)}
|
||||
>
|
||||
<option value="" disabled>Select device...</option>
|
||||
{#each compatibleDevices as device (device.name)}
|
||||
<option value={device.id}>
|
||||
{device.name}
|
||||
{#if device.name === recordedDevice.name}(exact match){/if}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if recordedDevices.length === 0}
|
||||
<div class="text-center py-8 text-muted-foreground">
|
||||
No devices in this recording
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if recordedDevices.length === 0}
|
||||
<div class="text-center py-8 text-muted-foreground">No devices in this recording</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Dialog.Footer class="flex gap-2">
|
||||
<Button variant="outline" onclick={onCancel} class="cursor-pointer">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onclick={handleConfirm}
|
||||
disabled={!allDevicesMapped}
|
||||
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||
>
|
||||
{#if !allDevicesMapped}
|
||||
<span class="icon-[ri--error-warning-line] w-4 h-4 mr-2"></span>
|
||||
Map All Devices
|
||||
{:else}
|
||||
<span class="icon-[ri--play-fill] w-4 h-4 mr-2"></span>
|
||||
Start Playback
|
||||
{/if}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
<Dialog.Footer class="flex gap-2">
|
||||
<Button variant="outline" onclick={onCancel} class="cursor-pointer">Cancel</Button>
|
||||
<Button
|
||||
onclick={handleConfirm}
|
||||
disabled={!allDevicesMapped}
|
||||
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||
>
|
||||
{#if !allDevicesMapped}
|
||||
<span class="icon-[ri--error-warning-line] w-4 h-4 mr-2"></span>
|
||||
Map All Devices
|
||||
{:else}
|
||||
<span class="icon-[ri--play-fill] w-4 h-4 mr-2"></span>
|
||||
Start Playback
|
||||
{/if}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -1,174 +1,157 @@
|
||||
<script lang="ts">
|
||||
import { _ } from "svelte-i18n";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { TagsInput } from "$lib/components/ui/tags-input";
|
||||
import type { RecordedEvent, DeviceInfo } from "$lib/types";
|
||||
import { _ } from "svelte-i18n";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
import { TagsInput } from "$lib/components/ui/tags-input";
|
||||
import type { RecordedEvent, DeviceInfo } from "$lib/types";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
events: RecordedEvent[];
|
||||
deviceInfo: DeviceInfo[];
|
||||
duration: number;
|
||||
onSave: (data: {
|
||||
title: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
}) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
interface Props {
|
||||
open: boolean;
|
||||
events: RecordedEvent[];
|
||||
deviceInfo: DeviceInfo[];
|
||||
duration: number;
|
||||
onSave: (data: { title: string; description: string; tags: string[] }) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
let { open, events, deviceInfo, duration, onSave, onCancel }: Props = $props();
|
||||
let { open, events, deviceInfo, duration, onSave, onCancel }: Props = $props();
|
||||
|
||||
let title = $state("");
|
||||
let description = $state("");
|
||||
let tags = $state<string[]>([]);
|
||||
let isSaving = $state(false);
|
||||
let title = $state("");
|
||||
let description = $state("");
|
||||
let tags = $state<string[]>([]);
|
||||
let isSaving = $state(false);
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
const totalSeconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
||||
}
|
||||
function formatDuration(ms: number): string {
|
||||
const totalSeconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!title.trim()) return;
|
||||
async function handleSave() {
|
||||
if (!title.trim()) return;
|
||||
|
||||
isSaving = true;
|
||||
try {
|
||||
await onSave({ title: title.trim(), description: description.trim(), tags });
|
||||
// Reset form
|
||||
title = "";
|
||||
description = "";
|
||||
tags = [];
|
||||
} finally {
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
isSaving = true;
|
||||
try {
|
||||
await onSave({ title: title.trim(), description: description.trim(), tags });
|
||||
// Reset form
|
||||
title = "";
|
||||
description = "";
|
||||
tags = [];
|
||||
} finally {
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
title = "";
|
||||
description = "";
|
||||
tags = [];
|
||||
onCancel();
|
||||
}
|
||||
function handleCancel() {
|
||||
title = "";
|
||||
description = "";
|
||||
tags = [];
|
||||
onCancel();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root {open} onOpenChange={(isOpen) => !isOpen && handleCancel()}>
|
||||
<Dialog.Content class="max-w-2xl">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Save Recording</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Save your recording to view and play it later from your dashboard
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Dialog.Content class="max-w-2xl">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Save Recording</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Save your recording to view and play it later from your dashboard
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="space-y-6 py-4">
|
||||
<!-- Recording Stats -->
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div
|
||||
class="flex flex-col items-center p-4 rounded-lg bg-muted/30 border border-border/30"
|
||||
>
|
||||
<span class="icon-[ri--time-line] w-5 h-5 text-primary mb-2"></span>
|
||||
<span class="text-xs text-muted-foreground mb-1">Duration</span>
|
||||
<span class="font-semibold">{formatDuration(duration)}</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center p-4 rounded-lg bg-muted/30 border border-border/30"
|
||||
>
|
||||
<span class="icon-[ri--pulse-line] w-5 h-5 text-accent mb-2"></span>
|
||||
<span class="text-xs text-muted-foreground mb-1">Events</span>
|
||||
<span class="font-semibold">{events.length}</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-center p-4 rounded-lg bg-muted/30 border border-border/30"
|
||||
>
|
||||
<span class="icon-[ri--gamepad-line] w-5 h-5 text-primary mb-2"></span>
|
||||
<span class="text-xs text-muted-foreground mb-1">Devices</span>
|
||||
<span class="font-semibold">{deviceInfo.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6 py-4">
|
||||
<!-- Recording Stats -->
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="flex flex-col items-center p-4 rounded-lg bg-muted/30 border border-border/30">
|
||||
<span class="icon-[ri--time-line] w-5 h-5 text-primary mb-2"></span>
|
||||
<span class="text-xs text-muted-foreground mb-1">Duration</span>
|
||||
<span class="font-semibold">{formatDuration(duration)}</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center p-4 rounded-lg bg-muted/30 border border-border/30">
|
||||
<span class="icon-[ri--pulse-line] w-5 h-5 text-accent mb-2"></span>
|
||||
<span class="text-xs text-muted-foreground mb-1">Events</span>
|
||||
<span class="font-semibold">{events.length}</span>
|
||||
</div>
|
||||
<div class="flex flex-col items-center p-4 rounded-lg bg-muted/30 border border-border/30">
|
||||
<span class="icon-[ri--gamepad-line] w-5 h-5 text-primary mb-2"></span>
|
||||
<span class="text-xs text-muted-foreground mb-1">Devices</span>
|
||||
<span class="font-semibold">{deviceInfo.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Device Info -->
|
||||
<div class="space-y-2">
|
||||
<Label>Devices Used</Label>
|
||||
{#each deviceInfo as device (device.name)}
|
||||
<div
|
||||
class="flex items-center gap-2 text-sm bg-muted/20 rounded px-3 py-2"
|
||||
>
|
||||
<span class="icon-[ri--rocket-line] w-4 h-4"></span>
|
||||
<span class="font-medium">{device.name}</span>
|
||||
<span class="text-muted-foreground text-xs">
|
||||
• {device.capabilities.join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<!-- Device Info -->
|
||||
<div class="space-y-2">
|
||||
<Label>Devices Used</Label>
|
||||
{#each deviceInfo as device (device.name)}
|
||||
<div class="flex items-center gap-2 text-sm bg-muted/20 rounded px-3 py-2">
|
||||
<span class="icon-[ri--rocket-line] w-4 h-4"></span>
|
||||
<span class="font-medium">{device.name}</span>
|
||||
<span class="text-muted-foreground text-xs">
|
||||
• {device.capabilities.join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Form Fields -->
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="title">Title *</Label>
|
||||
<Input
|
||||
id="title"
|
||||
bind:value={title}
|
||||
placeholder="My awesome pattern"
|
||||
required
|
||||
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<!-- Form Fields -->
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="title">Title *</Label>
|
||||
<Input
|
||||
id="title"
|
||||
bind:value={title}
|
||||
placeholder="My awesome pattern"
|
||||
required
|
||||
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
bind:value={description}
|
||||
placeholder="Describe your recording..."
|
||||
rows={3}
|
||||
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
bind:value={description}
|
||||
placeholder="Describe your recording..."
|
||||
rows={3}
|
||||
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="tags">Tags</Label>
|
||||
<TagsInput
|
||||
id="tags"
|
||||
bind:value={tags}
|
||||
placeholder="Add tags..."
|
||||
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="tags">Tags</Label>
|
||||
<TagsInput
|
||||
id="tags"
|
||||
bind:value={tags}
|
||||
placeholder="Add tags..."
|
||||
class="bg-background/50 border-primary/20 focus:border-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={handleCancel}
|
||||
disabled={isSaving}
|
||||
class="cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onclick={handleSave}
|
||||
disabled={!title.trim() || isSaving}
|
||||
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||
>
|
||||
{#if isSaving}
|
||||
<div
|
||||
class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2"
|
||||
></div>
|
||||
Saving...
|
||||
{:else}
|
||||
<span class="icon-[ri--save-line] w-4 h-4 mr-2"></span>
|
||||
Save Recording
|
||||
{/if}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" onclick={handleCancel} disabled={isSaving} class="cursor-pointer">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onclick={handleSave}
|
||||
disabled={!title.trim() || isSaving}
|
||||
class="cursor-pointer bg-gradient-to-r from-primary to-accent hover:from-primary/90 hover:to-accent/90"
|
||||
>
|
||||
{#if isSaving}
|
||||
<div
|
||||
class="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2"
|
||||
></div>
|
||||
Saving...
|
||||
{:else}
|
||||
<span class="icon-[ri--save-line] w-4 h-4 mr-2"></span>
|
||||
Save Recording
|
||||
{/if}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
Reference in New Issue
Block a user