your-ai-workflow-firebase-os 1.2.20 → 1.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,14 @@
1
1
  import React, { useState, useEffect, useRef } from 'react';
2
+ import { createPortal } from 'react-dom';
2
3
  import { motion, AnimatePresence } from 'framer-motion';
3
- import { Bot, Plus, X, Loader2, Trash2, FileText, Download, Check, Edit2, Info } from 'lucide-react';
4
+ import { Bot, Plus, X, Loader2, Trash2, FileText, Download, Check, Edit2, Info, Link , Image , Video , Archive , Code , FileIcon } from 'lucide-react';
4
5
  import * as LucideIcons from 'lucide-react';
5
6
  import { DashboardNav } from '../components/DashboardNav';
6
7
  import { Button } from '../components/Button';
7
8
  import { useAuth } from '../lib/AuthContext';
8
9
  import { useTheme } from '../lib/ThemeContext';
9
10
  import { db, storage } from '../lib/firebase';
10
- import { collection, addDoc, getDocs, query, orderBy, where, serverTimestamp, deleteDoc, updateDoc, doc, onSnapshot, limit } from 'firebase/firestore';
11
+ import { collection, addDoc, getDocs, query, orderBy, where, serverTimestamp, deleteDoc, updateDoc, doc, onSnapshot, limit, deleteField } from 'firebase/firestore';
11
12
  import { ref, uploadBytes, getDownloadURL, deleteObject } from 'firebase/storage';
12
13
  import { ConfirmModal } from '../components/ConfirmModal';
13
14
 
@@ -41,11 +42,25 @@ export function SharedPageTemplate({ config }: { config: any }) {
41
42
  const [copied, setCopied] = useState(false);
42
43
  const [uploading, setUploading] = useState(false);
43
44
  const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
45
+ const [_previewFile, _setPreviewFile] = useState<any | null>(null);
46
+ const setPreviewFile = (f: any) => {
47
+ _setPreviewFile(f);
48
+ if (f) {
49
+ const t = f.fileType || '';
50
+ const canPreview = t.startsWith('image/') || t.startsWith('video/') || t === 'application/pdf';
51
+ setPreviewLoading(canPreview);
52
+ }
53
+ };
54
+ const previewFile = _previewFile;
55
+ const [previewLoading, setPreviewLoading] = useState(true);
44
56
  const [isDeleting, setIsDeleting] = useState(false);
45
57
  const [editingId, setEditingId] = useState<string | null>(null);
46
58
  const [editName, setEditName] = useState('');
47
59
  const [editNote, setEditNote] = useState('');
60
+ const [editFields, setEditFields] = useState<{key: string; value: string}[]>([]);
48
61
  const fileInputRef = useRef<HTMLInputElement>(null);
62
+ const [copiedLink, setCopiedLink] = useState<string | null>(null);
63
+ const originalFieldKeysRef = useRef<string[]>([]);
49
64
 
50
65
  const parsedTabName = (config.tabName || config.pageId || 'shared_page')
51
66
  .toLowerCase().replace(/[^a-z0-9]+/g, '_');
@@ -231,31 +246,146 @@ export function SharedPageTemplate({ config }: { config: any }) {
231
246
  };
232
247
 
233
248
  // ── Edit record (inline) ──────────────────────────────────────────────────
249
+ // Inline rename state for file records (Drive-style)
250
+ const [renamingFileId, setRenamingFileId] = useState<string | null>(null);
251
+ const [renameFileName, setRenameFileName] = useState('');
252
+ const [isRenamingFile, setIsRenamingFile] = useState(false);
253
+
234
254
  const startEdit = (record: SavedRecord) => {
255
+ // For file records, use inline rename (Drive-style)
256
+ if (record.recordType === 'file') {
257
+ setRenamingFileId(record.id);
258
+ setRenameFileName(record.name);
259
+ return;
260
+ }
235
261
  setEditingId(record.id);
236
262
  setEditName(record.name);
237
263
  setEditNote(record.note || '');
264
+ // Load any extra custom fields (anything beyond the reserved keys)
265
+ const reserved = ['name','note','uid','recordType','fileName','fileType','fileSize','downloadURL','createdAt','creatorName','creatorEmail','creatorAvatar'];
266
+ const extras = Object.entries(record as any)
267
+ .filter(([k]) => !reserved.includes(k) && k !== 'id')
268
+ .map(([key, value]) => ({ key, value: String(value ?? '') }));
269
+ setEditFields(extras);
270
+ originalFieldKeysRef.current = extras.map(f => f.key);
238
271
  };
239
272
 
240
- const handleSaveEdit = async () => {
241
- if (!editingId || !editName.trim()) { setEditingId(null); return; }
242
- const original = records.find(r => r.id === editingId);
243
- if (original && original.name === editName.trim() && (original.note || '') === editNote.trim()) {
244
- setEditingId(null); return;
273
+ const handleRenameFile = async () => {
274
+ if (!renamingFileId || !renameFileName.trim()) { setRenamingFileId(null); return; }
275
+ const original = records.find(r => r.id === renamingFileId);
276
+ if (original && original.name === renameFileName.trim()) { setRenamingFileId(null); return; }
277
+ setIsRenamingFile(true);
278
+ try {
279
+ await updateDoc(doc(db, recordsCollection, renamingFileId), {
280
+ name: renameFileName.trim(),
281
+ });
282
+ setRecords(prev => prev.map(r =>
283
+ r.id === renamingFileId ? { ...r, name: renameFileName.trim() } : r
284
+ ));
285
+ } catch (e) {
286
+ console.error('Error renaming file:', e);
245
287
  }
288
+ setIsRenamingFile(false);
289
+ setRenamingFileId(null);
290
+ };
291
+
292
+ const handleSaveEdit = async () => {
293
+ await handleSilentSave();
294
+ setEditingId(null);
295
+ };
296
+
297
+ // Silent save: persist to Firestore without closing modal, show tiny loader
298
+ const handleSilentSave = async () => {
299
+ if (!editingId || !editName.trim()) return;
300
+ setSaving(true);
301
+ const extraData: Record<string, any> = {};
302
+ editFields.forEach(({ key, value }) => {
303
+ if (key.trim()) extraData[key.trim()] = value;
304
+ });
305
+ const currentKeys = editFields.map(f => f.key.trim()).filter(Boolean);
306
+ const removedKeys = originalFieldKeysRef.current.filter(k => !currentKeys.includes(k));
307
+ removedKeys.forEach(k => { extraData[k] = deleteField(); });
246
308
  try {
247
309
  await updateDoc(doc(db, recordsCollection, editingId), {
248
310
  name: editName.trim(),
249
311
  note: editNote.trim(),
312
+ ...extraData,
250
313
  });
251
- // onSnapshot will update the records list automatically
314
+ setRecords(prev => prev.map(r => {
315
+ if (r.id !== editingId) return r;
316
+ const updated = { ...r, name: editName.trim(), note: editNote.trim() };
317
+ removedKeys.forEach(k => { delete (updated as any)[k]; });
318
+ editFields.forEach(({ key, value }) => { if (key.trim()) (updated as any)[key.trim()] = value; });
319
+ return updated;
320
+ }));
321
+ originalFieldKeysRef.current = currentKeys;
252
322
  } catch (e) {
253
- console.error('Error updating shared record:', e);
323
+ console.error('Error saving record:', e);
254
324
  }
255
- setEditingId(null);
325
+ setSaving(false);
256
326
  };
257
327
 
258
328
  // ── Utils ─────────────────────────────────────────────────────────────────
329
+
330
+ // ── File type icon renderer ───────────────────────────────────────────────
331
+ const renderFileIcon = (type?: string, url?: string) => {
332
+ if (!type) return <FileText className="w-4 h-4" />;
333
+ if (type.startsWith('image/')) {
334
+ if (url) return <div className="w-9 h-9 rounded-xl bg-cover bg-center shrink-0" style={{ backgroundImage: `url(${url})` }} />;
335
+ return <Image className="w-4 h-4" />;
336
+ }
337
+ if (type.startsWith('video/') || type.startsWith('audio/')) return <Video className="w-4 h-4" />;
338
+ if (type.includes('pdf') || type.includes('word') || type.includes('text')) return <FileText className="w-4 h-4" />;
339
+ if (type.includes('zip') || type.includes('rar') || type.includes('archive')) return <Archive className="w-4 h-4" />;
340
+ if (type.includes('html') || type.includes('csv') || type.includes('json')) return <Code className="w-4 h-4" />;
341
+ return <FileIcon className="w-4 h-4" />;
342
+ };
343
+
344
+ const renderFileIconBg = (type?: string) => {
345
+ if (!type) return 'bg-foreground/5 text-foreground/40';
346
+ if (type.startsWith('image/')) return 'bg-emerald-500/10 text-emerald-500';
347
+ if (type.startsWith('video/') || type.startsWith('audio/')) return 'bg-purple-500/10 text-purple-500';
348
+ if (type.includes('pdf') || type.includes('word') || type.includes('text')) return 'bg-blue-500/10 text-blue-500';
349
+ if (type.includes('zip') || type.includes('rar') || type.includes('archive')) return 'bg-yellow-500/10 text-yellow-500';
350
+ if (type.includes('html') || type.includes('csv') || type.includes('json')) return 'bg-emerald-500/10 text-emerald-500';
351
+ return 'bg-foreground/5 text-foreground/40';
352
+ };
353
+
354
+ const formatExtension = (type?: string) => {
355
+ if (!type) return 'FILE';
356
+ const mimeMap: Record<string, string> = {
357
+ 'application/pdf': 'PDF',
358
+ 'application/zip': 'ZIP',
359
+ 'application/x-zip-compressed': 'ZIP',
360
+ 'application/x-rar-compressed': 'RAR',
361
+ 'application/x-7z-compressed': '7Z',
362
+ 'application/json': 'JSON',
363
+ 'application/xml': 'XML',
364
+ 'application/javascript': 'JS',
365
+ 'application/typescript': 'TS',
366
+ 'application/msword': 'DOC',
367
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'DOCX',
368
+ 'application/vnd.ms-excel': 'XLS',
369
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'XLSX',
370
+ 'application/vnd.ms-powerpoint': 'PPT',
371
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'PPTX',
372
+ 'application/rtf': 'RTF',
373
+ 'application/x-tar': 'TAR',
374
+ 'application/gzip': 'GZ',
375
+ 'text/plain': 'TXT',
376
+ 'text/html': 'HTML',
377
+ 'text/css': 'CSS',
378
+ 'text/csv': 'CSV',
379
+ 'text/markdown': 'MD',
380
+ };
381
+ const lower = type.toLowerCase();
382
+ if (mimeMap[lower]) return mimeMap[lower];
383
+ const parts = lower.split('/');
384
+ const sub = parts[parts.length - 1];
385
+ const clean = sub.replace(/^x-/, '').replace(/^vnd\..*\./, '');
386
+ return clean.toUpperCase().slice(0, 10);
387
+ };
388
+
259
389
  const formatSize = (bytes: number) => {
260
390
  if (!bytes) return '0 B';
261
391
  const k = 1024;
@@ -449,23 +579,29 @@ export function SharedPageTemplate({ config }: { config: any }) {
449
579
  initial={false}
450
580
  animate={{ opacity: 1, y: 0 }}
451
581
  exit={{ opacity: 0, scale: 0.95 }}
452
- className={`group flex flex-col bg-background border rounded-2xl p-5 transition-all relative ${isEditing ? 'border-accent/40 shadow-lg ring-1 ring-accent/20' : 'border-[var(--panel-border)] hover:border-accent/30 hover:shadow-lg'
582
+ onClick={() => owned ? startEdit(record) : null}
583
+ className={`group flex flex-col bg-background border rounded-2xl p-5 transition-all relative ${owned ? 'cursor-pointer' : ''} ${isEditing ? 'border-accent/40 shadow-lg ring-1 ring-accent/20' : 'border-[var(--panel-border)] hover:border-accent/30 hover:shadow-lg'
453
584
  }`}
454
585
  >
455
586
  {/* Top-right action icons — download + edit/delete (owner only) */}
456
- {!isEditing && (
457
- <div className="absolute top-3 right-3 flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-all">
587
+ {true && (
588
+ <div className="absolute top-3 right-3 flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-all z-10">
458
589
  {record.recordType === 'file' && record.downloadURL && (
459
- <a href={record.downloadURL} target="_blank" rel="noreferrer" className="p-1.5 rounded-lg text-foreground/25 hover:text-blue-500 hover:bg-blue-500/10 transition-all" title="Download">
460
- <Download className="w-3.5 h-3.5" />
461
- </a>
590
+ <>
591
+ <button onClick={(e) => { e.stopPropagation(); navigator.clipboard.writeText(record.downloadURL!); setCopiedLink(record.id); setTimeout(() => setCopiedLink(null), 2000); }} className="p-1.5 rounded-lg text-foreground/25 hover:text-emerald-500 hover:bg-emerald-500/10 transition-all cursor-pointer" title="Copy Link">
592
+ {copiedLink === record.id ? <Check className="w-3.5 h-3.5 text-emerald-500" /> : <Link className="w-3.5 h-3.5" />}
593
+ </button>
594
+ <a href={record.downloadURL} target="_blank" rel="noreferrer" onClick={e => e.stopPropagation()} className="p-1.5 rounded-lg text-foreground/25 hover:text-blue-500 hover:bg-blue-500/10 transition-all" title="Download">
595
+ <Download className="w-3.5 h-3.5" />
596
+ </a>
597
+ </>
462
598
  )}
463
599
  {owned && (
464
600
  <>
465
- <button onClick={() => startEdit(record)} className="p-1.5 rounded-lg text-foreground/25 hover:text-accent hover:bg-accent/10 transition-all cursor-pointer" title="Edit">
601
+ <button onClick={(e) => { e.stopPropagation(); startEdit(record); }} className="p-1.5 rounded-lg text-foreground/25 hover:text-accent hover:bg-accent/10 transition-all cursor-pointer" title="Edit">
466
602
  <Edit2 className="w-3.5 h-3.5" />
467
603
  </button>
468
- <button onClick={() => setDeleteConfirm(record.id)} className="p-1.5 rounded-lg text-foreground/25 hover:text-red-500 hover:bg-red-500/10 transition-all cursor-pointer" title="Delete">
604
+ <button onClick={(e) => { e.stopPropagation(); setDeleteConfirm(record.id); }} className="p-1.5 rounded-lg text-foreground/25 hover:text-red-500 hover:bg-red-500/10 transition-all cursor-pointer" title="Delete">
469
605
  <Trash2 className="w-3.5 h-3.5" />
470
606
  </button>
471
607
  </>
@@ -474,33 +610,48 @@ export function SharedPageTemplate({ config }: { config: any }) {
474
610
  )}
475
611
 
476
612
  <div className="flex items-start gap-3 mb-2">
477
- <div className={`w-9 h-9 rounded-xl flex items-center justify-center shrink-0 ${record.recordType === 'file' ? 'bg-blue-500/10 text-blue-500' : 'bg-accent/10 text-accent'}`}>
478
- {record.recordType === 'file' ? <FileText className="w-4 h-4" /> : <div className="w-2 h-2 rounded-full bg-current" />}
613
+ <div
614
+ onClick={(e) => { e.stopPropagation(); if (record.recordType === 'file') setPreviewFile(record); }}
615
+ className={`w-9 h-9 rounded-xl flex items-center justify-center shrink-0 ${record.recordType === 'file' ? `cursor-pointer hover:scale-105 transition-transform ${renderFileIconBg(record.fileType)}` : 'bg-accent/10 text-accent'}`}
616
+ >
617
+ {record.recordType === 'file' ? renderFileIcon(record.fileType, record.downloadURL) : <div className="w-2 h-2 rounded-full bg-current" />}
479
618
  </div>
480
- <div className="flex flex-col min-w-0 flex-1">
481
- {isEditing ? (
482
- <input autoFocus value={editName} onChange={e => setEditName(e.target.value)} onBlur={handleSaveEdit}
483
- onKeyDown={e => { if (e.key === 'Enter') handleSaveEdit(); if (e.key === 'Escape') setEditingId(null); }}
484
- className="text-[14px] font-bold text-foreground bg-transparent border-b border-accent/30 outline-none pb-0.5 w-full" />
485
- ) : (
486
- <span className="text-[14px] font-bold text-foreground truncate pr-12">{record.name}</span>
487
- )}
619
+ <div className="flex flex-col min-w-0 flex-1 relative">
620
+ {renamingFileId === record.id ? (
621
+ <input
622
+ autoFocus
623
+ type="text"
624
+ value={renameFileName}
625
+ onChange={e => setRenameFileName(e.target.value)}
626
+ onBlur={() => handleRenameFile()}
627
+ onKeyDown={e => { if (e.key === 'Enter') handleRenameFile(); if (e.key === 'Escape') setRenamingFileId(null); }}
628
+ onClick={e => e.stopPropagation()}
629
+ className="no-glow text-[14px] font-bold text-foreground bg-transparent border-b border-accent/30 outline-none pb-0.5 w-full pr-12"
630
+ />
631
+ ) : (
632
+ <span className="text-[14px] font-bold text-foreground truncate pr-12">{record.name}</span>
633
+ )}
634
+ {record.recordType === 'file' && <div className="absolute inset-0 z-0 cursor-pointer" onClick={() => setPreviewFile(record)} />}
488
635
  <span className="text-[11px] text-foreground/35 font-bold uppercase tracking-wider mt-0.5">
489
636
  {formatDate(record.createdAt)}
490
637
  </span>
491
638
  </div>
492
639
  </div>
493
640
 
494
- {isEditing ? (
495
- <textarea value={editNote} onChange={e => setEditNote(e.target.value)} onBlur={handleSaveEdit}
496
- onKeyDown={e => { if (e.key === 'Escape') setEditingId(null); }}
497
- placeholder="Note (optional)" rows={2}
498
- className="text-[13px] text-foreground/70 font-medium bg-transparent border-b border-accent/20 outline-none resize-none w-full mb-1 placeholder:text-foreground/30" />
499
- ) : (
500
- record.note && (
501
- <p className="text-[13px] text-foreground/55 font-medium leading-relaxed line-clamp-3 mb-1">{record.note}</p>
502
- )
641
+ {record.note && (
642
+ <p className="text-[13px] text-foreground/55 font-medium leading-relaxed line-clamp-3 mb-1">{record.note}</p>
503
643
  )}
644
+ {/* Extra custom fields display */}
645
+ {Object.entries(record as any)
646
+ .filter(([k]) => !['name','note','uid','recordType','fileName','fileType','fileSize','downloadURL','createdAt','creatorName','creatorEmail','creatorAvatar'].includes(k) && k !== 'id')
647
+ .slice(0, 3)
648
+ .map(([k, v]) => (
649
+ <div key={k} className="flex items-center gap-1.5 mt-1">
650
+ <span className="text-[10px] font-bold text-foreground/30 uppercase tracking-wider shrink-0">{k}:</span>
651
+ <span className="text-[11px] font-semibold text-foreground/60 truncate">{String(v)}</span>
652
+ </div>
653
+ ))
654
+ }
504
655
 
505
656
 
506
657
 
@@ -558,12 +709,255 @@ export function SharedPageTemplate({ config }: { config: any }) {
558
709
  )}
559
710
  </AnimatePresence>
560
711
 
712
+ {/* ── File Preview Modal (Drive-style) ───────────────────────────────────── */}
713
+ {typeof window !== 'undefined' && createPortal(
714
+ <AnimatePresence>
715
+ {previewFile && (
716
+ <div className="fixed inset-0 z-[9999] flex items-center justify-center p-4 md:p-12">
717
+ <motion.div onClick={() => setPreviewFile(null)} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="absolute inset-0 bg-background/95 backdrop-blur-xl cursor-pointer" />
718
+ <button onClick={() => setPreviewFile(null)} className="absolute top-6 right-6 z-[160] w-12 h-12 bg-foreground/10 hover:bg-foreground/20 text-foreground rounded-full flex items-center justify-center backdrop-blur-md transition-all cursor-pointer outline-none">
719
+ <X className="w-5 h-5" />
720
+ </button>
721
+ <motion.div
722
+ initial={{ opacity: 0, scale: 0.95, y: 10 }}
723
+ animate={{ opacity: 1, scale: 1, y: 0 }}
724
+ exit={{ opacity: 0, scale: 0.95, y: 10 }}
725
+ className="relative z-10 w-full max-w-6xl max-h-[85vh] flex flex-col pointer-events-auto shadow-2xl rounded-3xl overflow-hidden glass-panel border border-[var(--panel-border)]"
726
+ >
727
+ {/* File Header */}
728
+ <div className="flex items-center justify-between p-6 bg-background/80 backdrop-blur-md border-b border-[var(--panel-border)] shrink-0 shadow-sm z-20">
729
+ <div className="flex flex-col min-w-0 pr-4">
730
+ <span className="text-xl font-extrabold text-foreground truncate">{previewFile.fileName || previewFile.name}</span>
731
+ <span className="text-[12px] text-foreground/70 font-bold uppercase tracking-wider mt-1">
732
+ {formatExtension(previewFile.fileType)} • {formatSize(previewFile.fileSize || 0)}
733
+ </span>
734
+ </div>
735
+ <div className="flex items-center gap-2 shrink-0">
736
+ <button
737
+ onClick={() => { navigator.clipboard.writeText(previewFile.downloadURL!); setCopiedLink('preview'); setTimeout(() => setCopiedLink(null), 2000); }}
738
+ className="p-2.5 bg-foreground/5 hover:bg-foreground/10 text-foreground/70 transition-colors rounded-xl shadow-sm hover:text-emerald-500 cursor-pointer"
739
+ title="Copy Link"
740
+ >
741
+ {copiedLink === 'preview' ? <Check className="w-4 h-4 text-emerald-500" /> : <Link className="w-4 h-4" />}
742
+ </button>
743
+ <button
744
+ onClick={() => { setPreviewFile(null); setRenamingFileId(previewFile.id); setRenameFileName(previewFile.name); }}
745
+ className="p-2.5 bg-foreground/5 hover:bg-foreground/10 text-foreground/70 hover:text-accent transition-colors rounded-xl shadow-sm cursor-pointer"
746
+ title="Rename"
747
+ >
748
+ <Edit2 className="w-4 h-4" />
749
+ </button>
750
+ <button
751
+ onClick={() => { setPreviewFile(null); setDeleteConfirm(previewFile.id); }}
752
+ className="p-2.5 bg-red-500/10 hover:bg-red-500/20 text-red-500 transition-colors rounded-xl shadow-sm cursor-pointer"
753
+ title="Delete"
754
+ >
755
+ <Trash2 className="w-4 h-4" />
756
+ </button>
757
+ <a
758
+ href={previewFile.downloadURL}
759
+ download
760
+ target="_blank"
761
+ rel="noreferrer"
762
+ className="p-2.5 btn-primary transition-colors rounded-xl shadow-lg ml-1"
763
+ title="Download"
764
+ >
765
+ <Download className="w-4 h-4" />
766
+ </a>
767
+ </div>
768
+ </div>
769
+
770
+ {/* Preview Content */}
771
+ <div className="flex-1 w-full relative bg-foreground/[0.02] min-h-[50vh] md:min-h-[600px]" onClick={() => setPreviewFile(null)}>
772
+ <div className="absolute inset-x-4 inset-y-8 md:inset-x-12 md:inset-y-12 flex items-center justify-center">
773
+ {previewLoading && (
774
+ <div className="absolute inset-0 flex items-center justify-center rounded-lg z-50 pointer-events-none">
775
+ <div className="w-12 h-12 rounded-full bg-background/80 backdrop-blur-md shadow-xl flex items-center justify-center border border-[var(--panel-border)]/50">
776
+ <Loader2 className="w-5 h-5 text-accent animate-spin" />
777
+ </div>
778
+ </div>
779
+ )}
780
+ {previewFile.fileType?.startsWith('image/') ? (
781
+ <img
782
+ onLoad={() => setPreviewLoading(false)}
783
+ onError={() => setPreviewLoading(false)}
784
+ src={previewFile.downloadURL}
785
+ alt={previewFile.fileName || previewFile.name}
786
+ className={`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-auto h-auto max-w-full max-h-full object-contain drop-shadow-[0_0_40px_rgba(0,0,0,0.5)] rounded-lg pointer-events-auto transition-opacity duration-500 ${previewLoading ? 'opacity-0' : 'opacity-100'}`}
787
+ onClick={e => e.stopPropagation()}
788
+ />
789
+ ) : previewFile.fileType?.startsWith('video/') ? (
790
+ <video
791
+ onLoadedData={() => setPreviewLoading(false)}
792
+ onError={() => setPreviewLoading(false)}
793
+ src={previewFile.downloadURL}
794
+ controls
795
+ autoPlay
796
+ className={`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-auto h-auto max-w-full max-h-full outline-none bg-black/40 rounded-lg pointer-events-auto drop-shadow-[0_0_40px_rgba(0,0,0,0.5)] transition-opacity duration-500 ${previewLoading ? 'opacity-0' : 'opacity-100'}`}
797
+ onClick={e => e.stopPropagation()}
798
+ />
799
+ ) : previewFile.fileType === 'application/pdf' ? (
800
+ <iframe
801
+ onLoad={() => setPreviewLoading(false)}
802
+ src={previewFile.downloadURL}
803
+ className={`w-full h-full border-none rounded-lg relative z-10 transition-opacity duration-500 ${previewLoading ? 'opacity-0' : 'opacity-100'}`}
804
+ style={{ backgroundColor: 'white' }}
805
+ title="PDF Preview"
806
+ onClick={e => e.stopPropagation()}
807
+ />
808
+ ) : (
809
+ <div
810
+ className="w-full h-full flex flex-col items-center justify-center text-foreground/40 text-center relative z-10"
811
+ onClick={e => { e.stopPropagation(); setPreviewLoading(false); }}
812
+ >
813
+ <FileText className="w-24 h-24 mb-6 opacity-40 drop-shadow-sm" />
814
+ <span className="text-[20px] font-extrabold text-foreground">Preview Unavailable</span>
815
+ <span className="text-[14px] mt-2 text-foreground/60 font-medium max-w-xs">This file format cannot be previewed. Please download it instead.</span>
816
+ </div>
817
+ )}
818
+ </div>
819
+ </div>
820
+ </motion.div>
821
+ </div>
822
+ )}
823
+ </AnimatePresence>,
824
+ document.body
825
+ )}
826
+
827
+ {/* ── Edit Record Modal ──────────────────────────────────────────────── */}
828
+ <AnimatePresence>
829
+ {editingId && (
830
+ <div className="fixed inset-0 z-[120] flex items-center justify-center p-4">
831
+ <motion.div
832
+ onClick={() => handleSaveEdit()}
833
+ initial={{ opacity: 0 }}
834
+ animate={{ opacity: 1 }}
835
+ exit={{ opacity: 0 }}
836
+ className="absolute inset-0 bg-background/60 backdrop-blur-sm cursor-pointer"
837
+ />
838
+ <motion.div
839
+ initial={{ opacity: 0, scale: 0.97, y: 6 }}
840
+ animate={{ opacity: 1, scale: 1, y: 0 }}
841
+ exit={{ opacity: 0, scale: 0.97, y: 6 }}
842
+ transition={{ duration: 0.2 }}
843
+ className="w-full max-w-[440px] border-[var(--panel-border)] border rounded-2xl relative z-10 glass-panel shadow-2xl bg-background overflow-hidden"
844
+ >
845
+ <div className="p-5 flex flex-col gap-3 max-h-[80vh] overflow-y-auto">
846
+ {/* Header */}
847
+ <div className="flex items-center justify-between mb-2 mt-2 px-1">
848
+ <div className="flex items-center gap-2">
849
+ <span className="text-[14px] font-bold text-foreground">Edit Record</span>
850
+ {saving && <Loader2 className="w-3 h-3 animate-spin text-foreground/40" />}
851
+ </div>
852
+ <button
853
+ type="button"
854
+ onClick={() => handleSaveEdit()}
855
+ className="text-foreground/30 hover:text-foreground/60 transition-colors cursor-pointer"
856
+ >
857
+ <X className="w-4 h-4" />
858
+ </button>
859
+ </div>
860
+
861
+ <div className="flex flex-col gap-0 max-h-[70vh] overflow-y-auto no-scrollbar px-1">
862
+ {/* Name field */}
863
+ <div className="flex flex-col relative group/field">
864
+ <input
865
+ readOnly
866
+ value="NAME"
867
+ className="no-glow bg-transparent text-[10px] font-bold text-foreground/30 uppercase tracking-[0.15em] outline-none pt-3 pb-0.5"
868
+ />
869
+ <input
870
+ autoFocus
871
+ required
872
+ value={editName}
873
+ onChange={e => setEditName(e.target.value)}
874
+ onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }}
875
+ onBlur={() => handleSilentSave()}
876
+ placeholder="Name *"
877
+ className="no-glow bg-transparent text-[13px] font-medium text-foreground outline-none placeholder:text-foreground/15 pb-1 caret-foreground/50"
878
+ />
879
+ <div className="border-b border-dashed border-[var(--panel-border)]/60" />
880
+ </div>
881
+
882
+ {/* Note field */}
883
+ <div className="flex flex-col relative group/field">
884
+ <input
885
+ readOnly
886
+ value="NOTE"
887
+ className="no-glow bg-transparent text-[10px] font-bold text-foreground/30 uppercase tracking-[0.15em] outline-none pt-3 pb-0.5"
888
+ />
889
+ <textarea
890
+ ref={el => { if (el) { el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; } }}
891
+ value={editNote}
892
+ onChange={e => { setEditNote(e.target.value); e.target.style.height = 'auto'; e.target.style.height = e.target.scrollHeight + 'px'; }}
893
+ onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); (e.target as HTMLTextAreaElement).blur(); } }}
894
+ onBlur={() => handleSilentSave()}
895
+ placeholder="Add a note..."
896
+ rows={1}
897
+ className="no-glow bg-transparent text-[13px] font-medium text-foreground outline-none placeholder:text-foreground/15 pb-1 caret-foreground/50 resize-none overflow-hidden"
898
+ />
899
+ <div className="border-b border-dashed border-[var(--panel-border)]/60" />
900
+ </div>
901
+
902
+ {/* Custom Fields */}
903
+ {editFields.map((field, idx) => (
904
+ <div key={idx} className="flex flex-col relative group/field">
905
+ <input
906
+ placeholder="FIELD NAME"
907
+ value={field.key}
908
+ onChange={e => {
909
+ const next = [...editFields];
910
+ next[idx].key = e.target.value;
911
+ setEditFields(next);
912
+ }}
913
+ onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }}
914
+ onBlur={() => handleSilentSave()}
915
+ className="no-glow bg-transparent text-[10px] font-bold text-foreground/30 uppercase tracking-[0.15em] outline-none placeholder:text-foreground/15 pt-3 pb-0.5 caret-foreground/40"
916
+ />
917
+ <input
918
+ placeholder="Field value..."
919
+ value={field.value}
920
+ onChange={e => {
921
+ const next = [...editFields];
922
+ next[idx].value = e.target.value;
923
+ setEditFields(next);
924
+ }}
925
+ onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }}
926
+ onBlur={() => handleSilentSave()}
927
+ className="no-glow bg-transparent text-[13px] font-medium text-foreground outline-none placeholder:text-foreground/15 pt-1.5 pb-1 caret-foreground/50"
928
+ />
929
+ <div className="border-b border-dashed border-[var(--panel-border)]/60" />
930
+ <button
931
+ type="button"
932
+ onClick={() => setEditFields(editFields.filter((_, i) => i !== idx))}
933
+ className="absolute top-3 right-0 p-1 text-foreground/0 group-hover/field:text-foreground/20 hover:!text-red-400 transition-colors cursor-pointer"
934
+ >
935
+ <X className="w-3 h-3" />
936
+ </button>
937
+ </div>
938
+ ))}
939
+
940
+ {/* Add Field */}
941
+ <button
942
+ type="button"
943
+ onClick={() => setEditFields([...editFields, { key: '', value: '' }])}
944
+ className="flex items-center gap-1.5 text-[11px] font-medium text-foreground/25 hover:text-foreground/40 transition-colors cursor-pointer mt-4 mb-4"
945
+ >
946
+ <Plus className="w-3 h-3" /> Add Field
947
+ </button>
948
+ </div>
949
+ </div>
950
+ </motion.div>
951
+ </div>
952
+ )}
953
+ </AnimatePresence>
954
+
561
955
  <ConfirmModal
562
956
  isOpen={!!deleteConfirm}
563
957
  onCancel={() => setDeleteConfirm(null)}
564
958
  onConfirm={handleDelete}
565
959
  title="Delete Record"
566
- message="Are you sure you want to delete this record? This action cannot be undone."
960
+ message={<>Are you sure you want to delete <strong className="text-foreground block my-0.5 text-left overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{records.find(r => r.id === deleteConfirm)?.name}</strong> This action cannot be undone.</>}
567
961
  confirmText="Delete"
568
962
  isProcessing={isDeleting}
569
963
  />
@@ -378,7 +378,16 @@ export function TemplateBoard({
378
378
  <ArrowLeft className="w-4 h-4" /> Go Back
379
379
  </Link>
380
380
  )}
381
- {isTabMode && <div />}
381
+ {isTabMode && (
382
+ <div className="flex-1 min-w-0">
383
+ <h2 className="text-xl font-extrabold text-foreground tracking-tight truncate">
384
+ {config?.tabTitle || config?.tabName || title}
385
+ </h2>
386
+ <p className="text-[13px] font-medium text-foreground/50 mt-1">
387
+ {tasks.length} {tasks.length === 1 ? 'item' : 'items'} · {sortedCategories.length} {sortedCategories.length === 1 ? 'category' : 'categories'}
388
+ </p>
389
+ </div>
390
+ )}
382
391
  <div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
383
392
  {config?.showPrompt && (
384
393
  <motion.button
@@ -501,11 +510,11 @@ export function TemplateBoard({
501
510
  <div key={i} className="flex items-start bg-foreground/[0.04] border border-[var(--panel-border)]/50 rounded-[8px] px-2.5 py-1.5 w-full">
502
511
  <input
503
512
  placeholder={keyLabel}
504
- style={{ width: `${Math.max(25, f.key.length * 7)}px` }}
505
513
  value={f.key}
506
514
  onChange={e => updateField(i, e.target.value, f.value)}
507
515
  onKeyDown={e => e.key === 'Enter' && handleSaveInline(col.id)}
508
- className="no-glow bg-transparent outline-none text-[11px] text-foreground/50 font-bold placeholder:text-foreground/30 relative top-[1px]"
516
+ className="no-glow bg-transparent outline-none text-[11px] text-foreground/50 font-bold placeholder:text-foreground/30 relative top-[1px] shrink-0"
517
+ style={{ width: `${Math.max(3, f.key.length || keyLabel.length)}ch` }}
509
518
  />
510
519
  <span className="text-[11px] text-foreground/50 font-bold mr-1.5 relative top-[1px]">:</span>
511
520
  <textarea
@@ -547,9 +556,9 @@ export function TemplateBoard({
547
556
  {item.fields && item.fields.length > 0 && (
548
557
  <div className="flex flex-row flex-wrap gap-2 mt-2">
549
558
  {item.fields.map((f, i) => (
550
- <div key={i} className="inline-block bg-foreground/[0.04] border border-[var(--panel-border)]/50 rounded-[8px] px-2.5 py-1.5 max-w-full">
551
- <span className="text-[11px] text-foreground/50 font-bold mr-1.5 relative -top-[1px]">{f.key}:</span>
552
- <span className="text-[12px] text-foreground/90 font-medium break-words leading-tight">{f.value}</span>
559
+ <div key={i} className="inline-flex bg-foreground/[0.04] border border-[var(--panel-border)]/50 rounded-[8px] px-2.5 py-1.5 max-w-full">
560
+ <span className="text-[11px] text-foreground/50 font-bold mr-1.5 relative -top-[1px] shrink-0 break-all">{f.key}:</span>
561
+ <span className="text-[12px] text-foreground/90 font-medium break-words leading-tight min-w-0">{f.value}</span>
553
562
  </div>
554
563
  ))}
555
564
  </div>
@@ -597,7 +606,7 @@ export function TemplateBoard({
597
606
  <div key={i} className="flex items-start bg-foreground/[0.04] border border-[var(--panel-border)]/50 rounded-[8px] px-2.5 py-1.5 w-full">
598
607
  <input
599
608
  placeholder={keyLabel}
600
- style={{ width: `${Math.max(25, f.key.length * 7)}px` }}
609
+ style={{ width: `${Math.max(3, f.key.length || keyLabel.length)}ch` }}
601
610
  value={f.key}
602
611
  onChange={e => updateField(i, e.target.value, f.value)}
603
612
  onKeyDown={e => e.key === 'Enter' && handleSaveInline(col.id)}
@@ -641,7 +650,7 @@ export function TemplateBoard({
641
650
  <ConfirmModal
642
651
  isOpen={!!confirmDelete}
643
652
  title="Confirm Deletion"
644
- message={<>Are you entirely sure you wish to delete <strong className="text-foreground">{confirmDelete?.title}</strong>? This action is permanent.</>}
653
+ message={<>Are you sure you want to delete <strong className="text-foreground block my-0.5 text-left overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{confirmDelete?.title}</strong> This action is permanent.</>}
645
654
  onConfirm={executeDelete}
646
655
  onCancel={() => setConfirmDelete(null)}
647
656
  isProcessing={isDeleting}