firebase-os 1.1.4 → 1.1.5

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.
Files changed (124) hide show
  1. package/dist/FirebaseOS.d.ts +15 -0
  2. package/dist/firebase-os.cjs.js +2 -17
  3. package/dist/firebase-os.es.js +63 -74
  4. package/dist/index.d.ts +3 -0
  5. package/dist/lib/ConfigContext.d.ts +12 -0
  6. package/package.json +3 -2
  7. package/scripts/postinstall.js +86 -15
  8. package/src/App.css +184 -0
  9. package/src/App.tsx +214 -0
  10. package/src/FirebaseOS.tsx +80 -0
  11. package/src/assets/hero.png +0 -0
  12. package/src/assets/react.svg +1 -0
  13. package/src/assets/vite.svg +1 -0
  14. package/src/components/AdminNotifications.test.tsx +98 -0
  15. package/src/components/AdminNotifications.tsx +194 -0
  16. package/src/components/Button.test.tsx +22 -0
  17. package/src/components/Button.tsx +53 -0
  18. package/src/components/ConfirmModal.test.tsx +98 -0
  19. package/src/components/ConfirmModal.tsx +73 -0
  20. package/src/components/ContactPopup.test.tsx +98 -0
  21. package/src/components/ContactPopup.tsx +437 -0
  22. package/src/components/CustomSelect.test.tsx +47 -0
  23. package/src/components/CustomSelect.tsx +89 -0
  24. package/src/components/DashboardNav.test.tsx +98 -0
  25. package/src/components/DashboardNav.tsx +281 -0
  26. package/src/components/Input.test.tsx +33 -0
  27. package/src/components/Input.tsx +61 -0
  28. package/src/components/JsonEditor.tsx +579 -0
  29. package/src/components/Navbar.test.tsx +98 -0
  30. package/src/components/Navbar.tsx +563 -0
  31. package/src/configs/forms/contactForm.config.ts +15 -0
  32. package/src/configs/forms/index.ts +29 -0
  33. package/src/configs/forms/pubForm.config.ts +11 -0
  34. package/src/configs/forms/supportForm.config.ts +14 -0
  35. package/src/configs/forms/userForm.config.ts +11 -0
  36. package/src/configs/pages/admin.config.ts +29 -0
  37. package/src/configs/pages/contact.config.ts +6 -0
  38. package/src/configs/pages/home.config.ts +18 -0
  39. package/src/configs/pages/mem.config.ts +2 -0
  40. package/src/configs/pages/menuOrders.config.ts +11 -0
  41. package/src/configs/pages/pub.config.ts +11 -0
  42. package/src/configs/pages/shared.config.ts +29 -0
  43. package/src/configs/pages/support.config.ts +7 -0
  44. package/src/configs/pages/tabOrders.config.ts +33 -0
  45. package/src/configs/pages/user.config.ts +29 -0
  46. package/src/configs/theme.config.ts +93 -0
  47. package/src/index.css +403 -0
  48. package/src/index.ts +22 -0
  49. package/src/lib/AuthContext.test.tsx +88 -0
  50. package/src/lib/AuthContext.tsx +191 -0
  51. package/src/lib/ConfigContext.tsx +45 -0
  52. package/src/lib/ThemeContext.tsx +227 -0
  53. package/src/lib/firebase.ts +91 -0
  54. package/src/main.tsx +22 -0
  55. package/src/microcomponents/AdminExampleContent.tsx +44 -0
  56. package/src/microcomponents/PrivateExampleContent.tsx +39 -0
  57. package/src/microcomponents/Public.tsx +126 -0
  58. package/src/microcomponents/SharedExampleContent.tsx +53 -0
  59. package/src/pages/Dashboard.test.tsx +98 -0
  60. package/src/pages/Dashboard.tsx +60 -0
  61. package/src/pages/DynamicPage.tsx +237 -0
  62. package/src/pages/FormsAdmin.test.tsx +98 -0
  63. package/src/pages/FormsAdmin.tsx +459 -0
  64. package/src/pages/Home.test.tsx +98 -0
  65. package/src/pages/Home.tsx +144 -0
  66. package/src/pages/Login.test.tsx +98 -0
  67. package/src/pages/Login.tsx +108 -0
  68. package/src/pages/PagesAdmin.test.tsx +98 -0
  69. package/src/pages/PagesAdmin.tsx +1022 -0
  70. package/src/pages/Profile.test.tsx +98 -0
  71. package/src/pages/Profile.tsx +319 -0
  72. package/src/pages/Register.test.tsx +98 -0
  73. package/src/pages/Register.tsx +116 -0
  74. package/src/pages/Requests.test.tsx +95 -0
  75. package/src/pages/Requests.tsx +422 -0
  76. package/src/pages/ResetPassword.test.tsx +98 -0
  77. package/src/pages/ResetPassword.tsx +92 -0
  78. package/src/pages/Settings.test.tsx +98 -0
  79. package/src/pages/Settings.tsx +393 -0
  80. package/src/pages/Setup.tsx +401 -0
  81. package/src/pages/StorageAdmin.test.tsx +150 -0
  82. package/src/pages/StorageAdmin.tsx +769 -0
  83. package/src/pages/Submissions.test.tsx +95 -0
  84. package/src/pages/Submissions.tsx +372 -0
  85. package/src/pages/Templates.test.tsx +98 -0
  86. package/src/pages/Templates.tsx +103 -0
  87. package/src/pages/ThemeAdmin.test.tsx +144 -0
  88. package/src/pages/ThemeAdmin.tsx +1000 -0
  89. package/src/pages/Users.test.tsx +95 -0
  90. package/src/pages/Users.tsx +334 -0
  91. package/src/pages/Verify.test.tsx +98 -0
  92. package/src/pages/Verify.tsx +95 -0
  93. package/src/prompts/index.ts +13 -0
  94. package/src/prompts/pages/publicPage.ts +44 -0
  95. package/src/prompts/sharedConstants.ts +12 -0
  96. package/src/prompts/tabs/board/adminboard.ts +32 -0
  97. package/src/prompts/tabs/board/privateboard.ts +36 -0
  98. package/src/prompts/tabs/board/publicboard.ts +36 -0
  99. package/src/prompts/tabs/calendar/admincalendar.ts +32 -0
  100. package/src/prompts/tabs/calendar/privatecalendar.ts +36 -0
  101. package/src/prompts/tabs/calendar/publiccalendar.ts +36 -0
  102. package/src/prompts/tabs/crud/admin.ts +54 -0
  103. package/src/prompts/tabs/crud/private.ts +55 -0
  104. package/src/prompts/tabs/crud/shared.ts +53 -0
  105. package/src/prompts/tabs/table/admintable.ts +32 -0
  106. package/src/prompts/tabs/table/privatetable.ts +36 -0
  107. package/src/prompts/tabs/table/publictable.ts +36 -0
  108. package/src/setupTests.ts +1 -0
  109. package/src/templates/AdminPageTemplate.tsx +678 -0
  110. package/src/templates/PrivatePageTemplate.tsx +594 -0
  111. package/src/templates/PublicPageTemplate.tsx +92 -0
  112. package/src/templates/SharedPageTemplate.tsx +551 -0
  113. package/src/templates/TemplateBoard.test.tsx +106 -0
  114. package/src/templates/TemplateBoard.tsx +642 -0
  115. package/src/templates/TemplateCalendar.test.tsx +106 -0
  116. package/src/templates/TemplateCalendar.tsx +848 -0
  117. package/src/templates/TemplateConfirmation.test.tsx +106 -0
  118. package/src/templates/TemplateConfirmation.tsx +145 -0
  119. package/src/templates/TemplateInlineForm.test.tsx +106 -0
  120. package/src/templates/TemplateInlineForm.tsx +129 -0
  121. package/src/templates/TemplatePopupForm.test.tsx +106 -0
  122. package/src/templates/TemplatePopupForm.tsx +174 -0
  123. package/src/templates/TemplateTable.test.tsx +106 -0
  124. package/src/templates/TemplateTable.tsx +675 -0
@@ -0,0 +1,675 @@
1
+ import { ArrowLeft, Search, Download, Plus, Trash, ChevronDown, Palette, X, Bot, Check } from 'lucide-react';
2
+ import { adminTablePrompt, privateTablePrompt, publicTablePrompt } from '../prompts';
3
+ import Fuse from 'fuse.js';
4
+ import { Link } from 'react-router-dom';
5
+ import { Button } from '../components/Button';
6
+ import { useState, useEffect } from 'react';
7
+ import { db } from '../lib/firebase';
8
+ import { collection, onSnapshot, doc, setDoc, deleteDoc, getDocs, query, where } from 'firebase/firestore';
9
+ import { motion, AnimatePresence } from 'framer-motion';
10
+ import { useAuth } from '../lib/AuthContext';
11
+ import { DashboardNav } from '../components/DashboardNav';
12
+
13
+ interface TableState {
14
+ id?: string;
15
+ name?: string;
16
+ columns: string[];
17
+ rows: Record<string, string>[];
18
+ colColors?: Record<string, string>;
19
+ rowColors?: Record<string, string>;
20
+ headerColor?: string;
21
+ colorTimestamps?: Record<string, number>;
22
+ }
23
+
24
+ const pastelColors = [
25
+ { text: 'text-red-400', bgCircle: 'bg-red-400', bgCard: 'bg-red-500/[0.08]', borderCard: 'border-red-500/30', bgLight: 'bg-red-500/15', bgHover: 'hover:bg-red-500', bgAlt: 'bg-red-500/[0.04]', name: 'Red' },
26
+ { text: 'text-orange-400', bgCircle: 'bg-orange-400', bgCard: 'bg-orange-500/[0.08]', borderCard: 'border-orange-500/30', bgLight: 'bg-orange-500/15', bgHover: 'hover:bg-orange-500', bgAlt: 'bg-orange-500/[0.04]', name: 'Orange' },
27
+ { text: 'text-yellow-400', bgCircle: 'bg-yellow-400', bgCard: 'bg-yellow-500/[0.08]', borderCard: 'border-yellow-500/30', bgLight: 'bg-yellow-500/15', bgHover: 'hover:bg-yellow-500', bgAlt: 'bg-yellow-500/[0.04]', name: 'Yellow' },
28
+ { text: 'text-green-400', bgCircle: 'bg-green-400', bgCard: 'bg-green-500/[0.08]', borderCard: 'border-green-500/30', bgLight: 'bg-green-500/15', bgHover: 'hover:bg-green-500', bgAlt: 'bg-green-500/[0.04]', name: 'Green' },
29
+ { text: 'text-blue-400', bgCircle: 'bg-blue-400', bgCard: 'bg-blue-500/[0.08]', borderCard: 'border-blue-500/30', bgLight: 'bg-blue-500/15', bgHover: 'hover:bg-blue-500', bgAlt: 'bg-blue-500/[0.04]', name: 'Blue' },
30
+ { text: 'text-accent', bgCircle: 'bg-accent', bgCard: 'bg-accent/[0.08]', borderCard: 'border-accent/30', bgLight: 'bg-accent/15', bgHover: 'hover:bg-accent', bgAlt: 'bg-accent/[0.04]', name: 'Purple' },
31
+ { text: 'text-pink-400', bgCircle: 'bg-pink-400', bgCard: 'bg-pink-500/[0.08]', borderCard: 'border-pink-500/30', bgLight: 'bg-pink-500/15', bgHover: 'hover:bg-pink-500', bgAlt: 'bg-pink-500/[0.04]', name: 'Pink' },
32
+ { text: 'text-foreground/60', bgCircle: 'bg-foreground/30', bgCard: 'bg-foreground/[0.04]', borderCard: 'border-[var(--panel-border)]/50', bgLight: 'bg-foreground/15', bgHover: 'hover:bg-foreground/30', bgAlt: 'bg-foreground/[0.02]', name: 'Default' },
33
+ ];
34
+
35
+ export interface TableProps {
36
+ title?: string;
37
+ subtitle?: string;
38
+ backUrl?: string;
39
+ tableCollection?: string;
40
+ config?: any;
41
+ }
42
+
43
+ export function TemplateTable({
44
+ title = 'Table View',
45
+ subtitle = '/templates/table',
46
+ backUrl = '/templates',
47
+ tableCollection = 'pub_table_data',
48
+ config
49
+ }: TableProps) {
50
+ const { user } = useAuth();
51
+ const isTabMode = !!config;
52
+ const isPrivate = config?.isPrivate && !config?.isAdmin && !config?.isShared;
53
+ const isAdminOrShared = config?.isAdmin || config?.isShared;
54
+ const showCreator = isTabMode ? isAdminOrShared : true;
55
+ const showAddButton = config?.showButton !== false;
56
+ const addButtonText = config?.buttonText || '+';
57
+ const addButtonStyle = config?.buttonStyle || 'secondary';
58
+ const showExportButton = config?.showExportButton !== false;
59
+ const showPrompt = config?.showPrompt;
60
+ const [tables, setTables] = useState<TableState[]>([]);
61
+ const [currentTableId, setCurrentTableId] = useState<string>('demo');
62
+ const [showTableSelector, setShowTableSelector] = useState(false);
63
+ const [editingTableName, setEditingTableName] = useState(false);
64
+ const [tableNameInput, setTableNameInput] = useState('');
65
+ const [isCreatingTable, setIsCreatingTable] = useState(false);
66
+ const [copied, setCopied] = useState(false);
67
+ const [availableForms, setAvailableForms] = useState<string[]>([]);
68
+
69
+ // Prompt text
70
+ const getPrompt = () => {
71
+ if (config?.isAdmin) return adminTablePrompt(config, title, tableCollection);
72
+ if (config?.isPrivate) return privateTablePrompt(config, title, tableCollection, availableForms);
73
+ return publicTablePrompt(config, title, tableCollection, availableForms);
74
+ };
75
+ const promptText = getPrompt();
76
+
77
+ const handleCopyPrompt = () => {
78
+ navigator.clipboard.writeText(promptText);
79
+ setCopied(true);
80
+ setTimeout(() => setCopied(false), 2000);
81
+ };
82
+
83
+ const [editingCell, setEditingCell] = useState<{ id: string, field: string } | null>(null);
84
+ const [editingHeader, setEditingHeader] = useState<string | null>(null);
85
+ const [searchQuery, setSearchQuery] = useState('');
86
+ const [headerInput, setHeaderInput] = useState('');
87
+ const [activeRowPicker, setActiveRowPicker] = useState<string | null>(null);
88
+ const [activeColPicker, setActiveColPicker] = useState<string | null>(null);
89
+ const [showHeaderPicker, setShowHeaderPicker] = useState(false);
90
+
91
+ useEffect(() => {
92
+ // Fetch forms
93
+ const isShared = config?.isShared;
94
+ const isAdmin = config?.isAdmin;
95
+ const isPrivate = config?.isPrivate && !isAdmin && !isShared;
96
+ const isPublic = !isAdmin && !isShared && !isPrivate;
97
+
98
+ if (isPrivate || isPublic) {
99
+ getDocs(collection(db, 'sys_forms')).then(snap => {
100
+ const type = isPublic ? 'public' : 'private';
101
+ setAvailableForms(snap.docs.filter(d => d.data().formType === type).map(doc => doc.id));
102
+ });
103
+ } else {
104
+ setAvailableForms([]);
105
+ }
106
+
107
+ const isTablePrivate = tableCollection.startsWith('user_');
108
+ if (isTablePrivate && !user) return; // Wait for auth
109
+
110
+ const tableQuery = (isTablePrivate && user)
111
+ ? query(collection(db, tableCollection), where('uid', '==', user.uid))
112
+ : collection(db, tableCollection);
113
+
114
+ const unsub = onSnapshot(tableQuery as any, (snapshot: any) => {
115
+ const fetched: TableState[] = [];
116
+ snapshot.forEach((docSnap: any) => {
117
+ fetched.push({ id: docSnap.id, ...docSnap.data() } as TableState);
118
+ });
119
+
120
+ if (fetched.length === 0) {
121
+ const initialState = {
122
+ name: 'Unnamed Table',
123
+ columns: ['Column 1', 'Column 2', 'Column 3', 'Column 4'],
124
+ ...(user ? { uid: user.uid } : {}),
125
+ rows: [
126
+ { id: '1', 'Column 1': '', 'Column 2': '', 'Column 3': '', 'Column 4': '' },
127
+ { id: '2', 'Column 1': '', 'Column 2': '', 'Column 3': '', 'Column 4': '' }
128
+ ]
129
+ };
130
+ setDoc(doc(db, tableCollection, 'demo'), initialState);
131
+ } else {
132
+ setTables(fetched.sort((a, b) => (a.name || '').localeCompare(b.name || '')));
133
+ }
134
+ });
135
+ return () => unsub();
136
+ }, [tableCollection, user]);
137
+
138
+ const activeTable = tables.find(t => t.id === currentTableId) || tables[0] || { columns: [], rows: [] };
139
+
140
+ const saveState = async (newState: TableState) => {
141
+ if (activeTable.id) {
142
+ if (user && tableCollection.startsWith('user_')) (newState as any).uid = user.uid;
143
+ await setDoc(doc(db, tableCollection, activeTable.id), newState, { merge: true });
144
+ }
145
+ };
146
+
147
+ const exportCSV = () => {
148
+ if (!activeTable.rows || activeTable.rows.length === 0) return;
149
+ const headers = activeTable.columns.join(',');
150
+ const csvRows = activeTable.rows.map(r => activeTable.columns.map(col => `"${r[col] || ''}"`).join(',')).join('\n');
151
+ const blob = new Blob([`${headers}\n${csvRows}`], { type: 'text/csv' });
152
+ const url = window.URL.createObjectURL(blob);
153
+ const a = document.createElement('a');
154
+ a.href = url;
155
+ a.download = `export_${activeTable.name || 'table'}.csv`;
156
+ a.click();
157
+ window.URL.revokeObjectURL(url);
158
+ };
159
+
160
+ const updateCell = (id: string, field: string, value: string) => {
161
+ saveState({ ...activeTable, rows: activeTable.rows.map(r => r.id === id ? { ...r, [field]: value } : r) });
162
+ };
163
+
164
+ const addRow = () => {
165
+ const newRow = { id: Date.now().toString() };
166
+ activeTable.columns.forEach(c => (newRow as any)[c] = '');
167
+ saveState({ ...activeTable, rows: [...(activeTable.rows || []), newRow] });
168
+ };
169
+
170
+ const deleteRow = (id: string) => {
171
+ saveState({ ...activeTable, rows: activeTable.rows.filter(r => r.id !== id) });
172
+ };
173
+
174
+ const addColumn = () => {
175
+ const newColName = `Column ${(activeTable.columns?.length || 0) + 1}`;
176
+ if (!activeTable.columns?.includes(newColName)) {
177
+ saveState({ ...activeTable, columns: [...(activeTable.columns || []), newColName] });
178
+ setTimeout(() => {
179
+ const container = document.getElementById('table-scroll-container');
180
+ if (container) container.scrollTo({ left: container.scrollWidth, behavior: 'smooth' });
181
+ }, 100);
182
+ }
183
+ };
184
+
185
+ const deleteColumn = (colName: string) => {
186
+ const newCols = (activeTable.columns || []).filter(c => c !== colName);
187
+ const newRows = (activeTable.rows || []).map(r => {
188
+ const nr = { ...r };
189
+ delete nr[colName];
190
+ return nr;
191
+ });
192
+ const newColColors = { ...(activeTable.colColors || {}) };
193
+ delete newColColors[colName];
194
+ saveState({ ...activeTable, columns: newCols, rows: newRows, colColors: newColColors });
195
+ };
196
+
197
+ const renameActiveTable = async (newName: string) => {
198
+ if (newName.trim() && activeTable.id && newName !== activeTable.name) {
199
+ await setDoc(doc(db, tableCollection, activeTable.id), { name: newName }, { merge: true });
200
+ }
201
+ };
202
+
203
+ const createNewTable = async () => {
204
+ setIsCreatingTable(true);
205
+ setShowTableSelector(false);
206
+ const newId = `tbl_${Date.now()}`;
207
+ await setDoc(doc(db, tableCollection, newId), {
208
+ name: 'Unnamed Table',
209
+ columns: ['Column 1', 'Column 2', 'Column 3'],
210
+ ...(user ? { uid: user.uid } : {}),
211
+ rows: []
212
+ });
213
+ setTimeout(() => {
214
+ setCurrentTableId(newId);
215
+ setIsCreatingTable(false);
216
+ }, 600);
217
+ };
218
+
219
+ const saveHeaderRename = (oldName: string) => {
220
+ const newName = headerInput.trim();
221
+ if (newName && newName !== oldName && !activeTable.columns?.includes(newName)) {
222
+ const newCols = activeTable.columns.map(c => c === oldName ? newName : c);
223
+ const newRows = (activeTable.rows || []).map(r => {
224
+ const nr = { ...r };
225
+ nr[newName] = nr[oldName] || '';
226
+ delete nr[oldName];
227
+ return nr;
228
+ });
229
+ const newColColors = { ...(activeTable.colColors || {}) };
230
+ if (newColColors[oldName]) {
231
+ newColColors[newName] = newColColors[oldName];
232
+ delete newColColors[oldName];
233
+ }
234
+ saveState({ ...activeTable, columns: newCols, rows: newRows, colColors: newColColors });
235
+ }
236
+ setEditingHeader(null);
237
+ };
238
+
239
+ let searchedRows = activeTable.rows || [];
240
+ if (searchQuery.trim() !== '') {
241
+ const fuseData = searchedRows.map(r => ({ ...r, _creatorMatch: 'Guest' }));
242
+ const keys = (activeTable.columns || []).concat(['_creatorMatch']);
243
+ const fuse = new Fuse(fuseData, { keys, threshold: 0.4, ignoreLocation: true });
244
+ searchedRows = fuse.search(searchQuery).map(result => {
245
+ const { _creatorMatch, ...originalRow } = result.item;
246
+ return originalRow as Record<string, string>;
247
+ });
248
+ }
249
+
250
+
251
+
252
+ return (
253
+ <main className="flex-1 w-full max-w-7xl mx-auto p-4 md:p-8 z-10 flex flex-col pt-12 min-h-screen">
254
+ <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mb-4">
255
+ <h1 className="w-fit inline-block text-3xl md:text-4xl lg:text-5xl font-extrabold tracking-tight mb-3 text-gradient mt-1">
256
+ {title}
257
+ </h1>
258
+ {subtitle && (
259
+ <div className="flex items-center gap-2 text-foreground/40 font-bold uppercase tracking-[0.2em] text-[11px]">
260
+ <span className="w-8 h-[1px] bg-foreground/10" />
261
+ {subtitle}
262
+ </div>
263
+ )}
264
+ </motion.div>
265
+
266
+ {isTabMode && <div className="mb-6"><DashboardNav /></div>}
267
+
268
+ <div className="flex justify-between items-center mb-6 border-b border-[var(--panel-border)] pb-6 relative z-20">
269
+ {!isTabMode && backUrl && (
270
+ <Link to={backUrl} className="px-4 md:px-5 py-2.5 border border-[var(--panel-border)] rounded-xl text-[13px] font-bold hover:bg-foreground/5 transition-all flex items-center gap-2 shrink-0">
271
+ <ArrowLeft className="w-4 h-4" /> <span className="hidden sm:inline">Go Back</span>
272
+ </Link>
273
+ )}
274
+ {isTabMode && <div />}
275
+ <div className="flex gap-2 shrink-0">
276
+ {showPrompt && (
277
+ <motion.button
278
+ whileHover={{ scale: 1.05 }}
279
+ whileTap={{ scale: 0.95 }}
280
+ onClick={handleCopyPrompt}
281
+ title={copied ? 'Copied!' : 'Copy Developer Prompt'}
282
+ className={`w-10 h-10 flex items-center justify-center rounded-xl transition-all duration-300 cursor-pointer ${copied
283
+ ? 'bg-emerald-500/10 text-emerald-500 border border-emerald-500/20'
284
+ : 'btn-secondary shadow-lg'
285
+ }`}
286
+ >
287
+ {copied ? <Check className="w-4 h-4" /> : <Bot className="w-4 h-4" />}
288
+ </motion.button>
289
+ )}
290
+ {showExportButton && (
291
+ <Button variant="secondary" onClick={exportCSV} className="px-3 py-2.5 font-bold rounded-xl shadow-sm border border-[var(--panel-border)] flex items-center gap-2">
292
+ <Download className="w-4 h-4" />
293
+ </Button>
294
+ )}
295
+ {showAddButton && (
296
+ <Button variant={addButtonStyle === 'primary' ? undefined : 'secondary'} onClick={addRow} className="px-3 py-2.5 font-bold rounded-xl border border-[var(--panel-border)] shadow-sm flex items-center gap-2">
297
+ {addButtonText === '+' ? <Plus className="w-4 h-4" /> : addButtonText}
298
+ </Button>
299
+ )}
300
+ </div>
301
+ </div>
302
+
303
+ <motion.div
304
+ initial={{ opacity: 0, y: 20 }}
305
+ animate={{ opacity: 1, y: 0 }}
306
+ className="glass-panel border border-[var(--panel-border)] rounded-3xl overflow-hidden shadow-2xl relative z-10 block"
307
+ >
308
+ <div className="absolute top-0 right-0 w-[400px] h-[400px] bg-accent/5 blur-[100px] rounded-full pointer-events-none -z-10" />
309
+
310
+ <div className="p-4 md:px-6 border-b border-[var(--panel-border)]/50 flex flex-col sm:flex-row items-start sm:items-center justify-between bg-foreground/[0.01] gap-4">
311
+
312
+ {/* Table Selector & Title Editor */}
313
+ <div className="flex flex-col w-full sm:w-auto relative">
314
+ <div className="flex items-center gap-2 relative">
315
+ <div className="relative flex items-center">
316
+ {editingTableName ? (
317
+ <div
318
+ contentEditable
319
+ suppressContentEditableWarning
320
+ onBlur={(e) => { setEditingTableName(false); renameActiveTable(e.currentTarget.textContent || ''); }}
321
+ onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); e.currentTarget.blur(); } }}
322
+ className="bg-transparent border-b-2 border-accent outline-none text-[16px] font-bold text-foreground inline-block min-w-[20px] whitespace-nowrap px-1 py-0.5"
323
+ >{activeTable.name || 'Unnamed Table'}</div>
324
+ ) : (
325
+ <h3 className="font-bold text-[16px] text-foreground/90 cursor-pointer hover:text-accent transition-colors truncate max-w-[200px]" onClick={() => setEditingTableName(true)} title="Click to rename table">
326
+ {activeTable.name || 'Unnamed Table'}
327
+ </h3>
328
+ )}
329
+
330
+ <AnimatePresence>
331
+ {showTableSelector && (
332
+ <>
333
+ <div className="fixed inset-0 z-40" onClick={() => setShowTableSelector(false)} />
334
+ <motion.div
335
+ initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -10 }}
336
+ className="absolute top-[120%] left-0 w-[220px] bg-background/95 backdrop-blur-xl border border-[var(--panel-border)] rounded-2xl shadow-2xl z-50 py-2 flex flex-col"
337
+ >
338
+ <div className="px-4 py-2 text-[10px] font-bold uppercase tracking-wider text-foreground/40 border-b border-[var(--panel-border)] mb-1">Your Tables</div>
339
+ <div className="max-h-[250px] overflow-y-auto styled-scrollbars z-50 relative">
340
+ {tables.map(t => (
341
+ <button key={t.id} onClick={() => { setCurrentTableId(t.id!); setShowTableSelector(false); }} className={`w-full px-4 py-2 text-[13px] font-bold text-left hover:bg-foreground/[0.03] transition-colors truncate ${currentTableId === t.id ? 'text-accent bg-accent/5' : 'text-foreground/70'}`}>
342
+ {t.name || 'Unnamed Table'}
343
+ </button>
344
+ ))}
345
+ </div>
346
+ <div className="w-full h-[1px] bg-[var(--panel-border)]/50 my-1" />
347
+ <button onClick={createNewTable} className="w-full px-4 py-2.5 text-[13px] font-bold text-left text-foreground/70 hover:text-foreground hover:bg-foreground/5 transition-colors flex items-center gap-2">
348
+ <Plus className="w-4 h-4" /> Create New Table
349
+ </button>
350
+ </motion.div>
351
+ </>
352
+ )}
353
+ </AnimatePresence>
354
+ </div>
355
+
356
+ <button onClick={() => setShowTableSelector(!showTableSelector)} className="p-1 px-1.5 rounded-lg hover:bg-foreground/5 transition-colors text-foreground/40 hover:text-foreground shrink-0 border border-transparent hover:border-[var(--panel-border)]">
357
+ <ChevronDown className={`w-4 h-4 transition-transform ${showTableSelector ? 'rotate-180 text-accent' : ''}`} />
358
+ </button>
359
+ </div>
360
+ <span className="opacity-40 text-[11px] font-bold mt-1 uppercase tracking-wider">{(activeTable.rows || []).length} Records</span>
361
+ </div>
362
+
363
+ <div className="relative w-full sm:max-w-[300px]">
364
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-foreground/40" />
365
+ <input
366
+ type="text"
367
+ placeholder="Search active records..."
368
+ value={searchQuery}
369
+ onChange={e => setSearchQuery(e.target.value)}
370
+ className="w-full bg-background border border-[var(--panel-border)] rounded-xl py-2.5 px-9 text-[13px] font-medium text-foreground focus:outline-none focus:border-accent transition-colors shadow-inner"
371
+ />
372
+ </div>
373
+ </div>
374
+
375
+ <div id="table-scroll-container" className="overflow-x-auto w-full block min-h-[300px] relative styled-scrollbars">
376
+ <AnimatePresence mode="wait">
377
+ {isCreatingTable ? (
378
+ <motion.div key="loader" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="absolute inset-0 flex flex-col items-center justify-center bg-background/40 backdrop-blur-[2px] z-50">
379
+ <div className="w-8 h-8 rounded-full border-2 border-accent border-r-transparent animate-spin mb-4" />
380
+ <p className="text-[11px] font-bold text-foreground/50 uppercase tracking-widest">Generating Table</p>
381
+ </motion.div>
382
+ ) : (
383
+ <motion.table key="table" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className="w-full text-left border-collapse table-auto relative min-w-[800px]">
384
+ <thead>
385
+ <tr className={`border-b border-[var(--panel-border)] transition-colors ${activeTable.headerColor ? pastelColors.find(c => c.text === activeTable.headerColor)?.bgAlt : 'bg-foreground/[0.02]'}`}>
386
+ <th className={`py-4 px-6 text-[12px] font-bold uppercase tracking-wider whitespace-nowrap text-foreground/50 w-32 border-r border-[var(--panel-border)]/20 ${!showCreator ? 'hidden' : ''}`}>Creator</th>
387
+ {(activeTable.columns || []).map((col, idx) => {
388
+ const colColor = (activeTable.colColors || {})[col];
389
+ const colTs = (activeTable.colorTimestamps || {})[`col_${col}`] || 0;
390
+ const headColor = activeTable.headerColor;
391
+ const headTs = (activeTable.colorTimestamps || {})['header'] || 0;
392
+
393
+ let colBgClass = 'bg-transparent';
394
+ let colTextClass = 'text-foreground/50';
395
+
396
+ if (colColor && colColor !== 'none' && colTs >= headTs) {
397
+ const colDef = pastelColors.find(c => c.text === colColor);
398
+ if (colDef) {
399
+ colBgClass = colDef.bgCard;
400
+ colTextClass = colDef.text;
401
+ }
402
+ }
403
+
404
+ return (
405
+ <th key={`col_${idx}_${col}`} className={`py-3 px-6 text-[12px] font-bold uppercase tracking-wider relative group h-[53px] min-w-[120px] ${colTextClass} ${colBgClass} border-r border-[var(--panel-border)]/20 last:border-r-0 select-none align-middle`}>
406
+ <div className="flex items-center justify-between gap-2 w-full h-full">
407
+ {editingHeader === col ? (
408
+ <div className="flex-1 min-w-[50px]">
409
+ <span
410
+ contentEditable
411
+ suppressContentEditableWarning
412
+ onBlur={(e) => {
413
+ setEditingHeader(null);
414
+ setHeaderInput(e.currentTarget.textContent || '');
415
+ if ((e.currentTarget.textContent || '').trim() !== '') saveHeaderRename(col);
416
+ }}
417
+ onKeyDown={e => {
418
+ if (e.key === 'Enter') {
419
+ e.preventDefault();
420
+ e.currentTarget.blur();
421
+ }
422
+ }}
423
+ className={`bg-transparent outline-none border-b border-accent/40 font-bold text-[12px] uppercase tracking-wider p-0 m-0 ${colTextClass} block w-full whitespace-nowrap overflow-hidden cursor-text`}
424
+ ref={el => { if (el) requestAnimationFrame(() => { el.focus(); }); }}
425
+ >{headerInput}</span>
426
+ </div>
427
+ ) : (
428
+ <div className="flex items-center gap-2 flex-1 overflow-hidden" onClick={() => { setEditingHeader(col); setHeaderInput(col); }}>
429
+ <span className="truncate cursor-text">{col}</span>
430
+ </div>
431
+ )}
432
+
433
+ <div className="relative shrink-0 flex items-center h-full gap-0.5">
434
+ <button onClick={() => setActiveColPicker(activeColPicker === col ? null : col)} className={`p-1.5 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity hover:bg-foreground/10 ${colColor && colColor !== 'none' ? '!opacity-100' : ''} text-current`}>
435
+ <Palette className="w-4 h-4" />
436
+ </button>
437
+ {(activeTable.columns || []).length > 1 && (
438
+ <button
439
+ onClick={(e) => { e.stopPropagation(); deleteColumn(col); }}
440
+ className="p-1.5 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-500/10 text-foreground/30 hover:text-red-500"
441
+ title="Delete column"
442
+ >
443
+ <Trash className="w-3.5 h-3.5" />
444
+ </button>
445
+ )}
446
+ {activeColPicker === col && (
447
+ <>
448
+ <div className="fixed inset-0 z-30" onClick={() => setActiveColPicker(null)} />
449
+ <div className="absolute top-8 right-0 flex gap-1.5 bg-background/95 backdrop-blur-xl border border-[var(--panel-border)] p-2 rounded-xl shadow-xl z-50 items-center">
450
+ <button
451
+ onClick={(e) => {
452
+ e.preventDefault(); e.stopPropagation();
453
+ const colors = { ...(activeTable.colColors || {}), [col]: 'none' };
454
+ const ts = { ...(activeTable.colorTimestamps || {}), [`col_${col}`]: Date.now() };
455
+ saveState({ ...activeTable, colColors: colors, colorTimestamps: ts });
456
+ setActiveColPicker(null);
457
+ }}
458
+ className="w-4 h-4 rounded-full flex items-center justify-center bg-foreground/10 hover:bg-red-500/20 hover:text-red-500 text-foreground/40 transition-colors border border-[var(--panel-border)]/50 shrink-0"
459
+ title="Remove Color"
460
+ >
461
+ <X className="w-3 h-3" />
462
+ </button>
463
+ <div className="h-4 w-px bg-[var(--panel-border)]/50 mx-0.5" />
464
+ {pastelColors.map(c => (
465
+ <button
466
+ key={c.name}
467
+ onClick={(e) => {
468
+ e.preventDefault();
469
+ e.stopPropagation();
470
+ const colors = { ...(activeTable.colColors || {}), [col]: c.text };
471
+ const ts = { ...(activeTable.colorTimestamps || {}), [`col_${col}`]: Date.now() };
472
+ saveState({ ...activeTable, colColors: colors, colorTimestamps: ts });
473
+ setActiveColPicker(null);
474
+ }}
475
+ className={`w-4 h-4 rounded-full ${c.bgCircle} hover:scale-125 transition-transform border border-[var(--panel-border)]/50 shrink-0`}
476
+ />
477
+ ))}
478
+ </div>
479
+ </>
480
+ )}
481
+ </div>
482
+ </div>
483
+ </th>
484
+ )
485
+ })}
486
+ <th className="py-4 px-4 w-14 border-l border-[var(--panel-border)]/30 relative">
487
+ <div className="flex items-center justify-end gap-0.5 relative z-20">
488
+ <button onClick={() => setShowHeaderPicker(!showHeaderPicker)} className={`p-1.5 rounded-lg transition-colors hover:bg-foreground/10 ${activeTable.headerColor ? pastelColors.find(p => p.text === activeTable.headerColor)?.text : 'text-foreground/30'}`}>
489
+ <Palette className="w-4 h-4" />
490
+ </button>
491
+ {showHeaderPicker && (
492
+ <>
493
+ <div className="fixed inset-0 z-30" onClick={() => setShowHeaderPicker(false)} />
494
+ <div className="absolute top-6 right-8 flex gap-1.5 bg-background/90 backdrop-blur-xl border border-[var(--panel-border)] p-2 rounded-xl shadow-xl z-50 items-center">
495
+ <button
496
+ onClick={() => {
497
+ saveState({ ...activeTable, headerColor: '' });
498
+ setShowHeaderPicker(false);
499
+ }}
500
+ className="w-4 h-4 rounded-full flex items-center justify-center bg-foreground/10 hover:bg-red-500/20 hover:text-red-500 text-foreground/40 transition-colors border border-[var(--panel-border)]/50 shrink-0"
501
+ title="Remove Header Color"
502
+ >
503
+ <X className="w-3 h-3" />
504
+ </button>
505
+ <div className="h-4 w-px bg-[var(--panel-border)]/50 mx-0.5" />
506
+ {pastelColors.map(c => (
507
+ <button
508
+ key={c.name}
509
+ onClick={() => {
510
+ const ts = { ...(activeTable.colorTimestamps || {}), ['header']: Date.now() };
511
+ saveState({ ...activeTable, headerColor: c.text, colorTimestamps: ts });
512
+ setShowHeaderPicker(false);
513
+ }}
514
+ className={`w-4 h-4 rounded-full ${c.bgCircle} hover:scale-125 transition-transform border border-[var(--panel-border)]/50`}
515
+ />
516
+ ))}
517
+ </div>
518
+ </>
519
+ )}
520
+ <button onClick={addColumn} className="p-1.5 rounded-lg text-foreground/40 hover:text-accent hover:bg-accent/10 transition-colors flex items-center justify-center">
521
+ <Plus className="w-4 h-4" />
522
+ </button>
523
+ </div>
524
+ </th>
525
+ </tr>
526
+ </thead>
527
+ <tbody>
528
+ {searchedRows.map((row, idx) => {
529
+ const isOdd = idx % 2 === 1;
530
+ const rowColorVal = (activeTable.rowColors || {})[row.id];
531
+ const creatorColorClass = rowColorVal ? (isOdd ? pastelColors.find(c => c.text === rowColorVal)?.bgAlt : pastelColors.find(c => c.text === rowColorVal)?.bgCard) : (isOdd ? 'bg-foreground/[0.02]' : 'bg-transparent');
532
+
533
+ return (
534
+ <tr key={row.id || `row_${idx}`} className="border-b border-[var(--panel-border)]/30 hover:bg-foreground/[0.04] transition-colors group">
535
+ <td className={`py-4 px-6 whitespace-nowrap h-[53px] border-r border-[var(--panel-border)]/20 ${creatorColorClass} ${!showCreator ? 'hidden' : ''}`}>
536
+ <div className="flex items-center gap-2">
537
+ {isTabMode && user ? (
538
+ <>
539
+ {user.photoURL ? (
540
+ <img src={user.photoURL} className="w-5 h-5 rounded-full shrink-0 object-cover" />
541
+ ) : (
542
+ <div className="w-5 h-5 rounded-full bg-accent/20 text-accent flex items-center justify-center text-[10px] font-bold shrink-0">
543
+ {(user.displayName || user.email || 'U')[0].toUpperCase()}
544
+ </div>
545
+ )}
546
+ <span className="text-[11px] font-bold text-foreground/80 tracking-tight shrink-0">{user.displayName || user.email?.split('@')[0] || 'User'}</span>
547
+ </>
548
+ ) : (
549
+ <>
550
+ <div className="w-5 h-5 rounded-full bg-accent/20 text-accent flex items-center justify-center text-[10px] font-bold shrink-0 shadow-inner group-hover:scale-110 transition-transform">
551
+ G
552
+ </div>
553
+ <span className="text-[11px] font-bold text-foreground/80 tracking-tight shrink-0">Guest</span>
554
+ </>
555
+ )}
556
+ </div>
557
+ </td>
558
+ {(activeTable.columns || []).map(field => {
559
+ const isEditing = editingCell?.id === row.id && editingCell?.field === field;
560
+ const colColorVal = (activeTable.colColors || {})[field];
561
+ const colTs = (activeTable.colorTimestamps || {})[`col_${field}`] || 0;
562
+ const rowColorVal = (activeTable.rowColors || {})[row.id];
563
+ const rowTs = (activeTable.colorTimestamps || {})[`row_${row.id}`] || 0;
564
+
565
+ let activeColorVal = '';
566
+ if (rowTs > colTs) {
567
+ activeColorVal = rowColorVal === 'none' ? '' : rowColorVal;
568
+ } else if (colTs > rowTs) {
569
+ activeColorVal = colColorVal === 'none' ? '' : colColorVal;
570
+ } else {
571
+ activeColorVal = rowColorVal === 'none' ? '' : rowColorVal || colColorVal || '';
572
+ }
573
+
574
+ if (activeColorVal === 'none') activeColorVal = '';
575
+
576
+ const activeColorDef = activeColorVal ? pastelColors.find(c => c.text === activeColorVal) : null;
577
+ const cellColorClass = activeColorDef ? (isOdd ? activeColorDef.bgAlt : activeColorDef.bgCard) : (isOdd ? 'bg-foreground/[0.02]' : 'bg-transparent');
578
+ const textClass = activeColorDef ? activeColorDef.text : 'text-foreground/80';
579
+
580
+ return (
581
+ <td
582
+ key={`cell_${row.id || idx}_${field}`}
583
+ className={`py-4 px-6 whitespace-pre-wrap min-w-[120px] text-[14px] font-medium relative ${cellColorClass} align-top border-r border-[var(--panel-border)]/10 last:border-r-0`}
584
+ onClick={() => !isEditing && setEditingCell({ id: row.id, field })}
585
+ >
586
+ {isEditing ? (
587
+ <textarea
588
+ autoFocus
589
+ value={(row as any)[field] || ''}
590
+ onChange={e => updateCell(row.id, field, e.target.value)}
591
+ onBlur={() => setEditingCell(null)}
592
+ onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { setEditingCell(null); } }}
593
+ className={`absolute inset-0 bg-transparent !border-0 !outline-none focus:!outline-none focus:!ring-0 !ring-0 !shadow-none focus:!shadow-none p-4 px-6 text-[14px] font-medium w-full h-full caret-accent ${textClass} z-20 resize-none styled-scrollbars`}
594
+ />
595
+ ) : null}
596
+ <span className={`block w-full min-h-[20px] break-words transition-all ${isEditing ? 'opacity-0' : 'opacity-100'} ${textClass} ${(field === 'Status' && (row as any)[field] === 'Active' && !activeColorVal) ? 'text-green-500' : ''}`}>
597
+ {(row as any)[field] || (
598
+ <span className="opacity-30 italic font-normal">empty</span>
599
+ )}
600
+ </span>
601
+ </td>
602
+ );
603
+ })}
604
+ <td className={`py-4 px-4 whitespace-nowrap text-right h-[53px] ${creatorColorClass} relative z-10`}>
605
+ <div className="flex items-center justify-end gap-1 opacity-20 group-hover:opacity-100 transition-opacity relative">
606
+ <button onClick={() => setActiveRowPicker(activeRowPicker === row.id ? null : row.id)} className={`p-1.5 rounded-lg transition-colors hover:bg-foreground/10 ${rowColorVal ? pastelColors.find(p => p.text === rowColorVal)?.text : 'text-foreground/40'}`}>
607
+ <Palette className="w-4 h-4" />
608
+ </button>
609
+
610
+ {activeRowPicker === row.id && (
611
+ <>
612
+ <div className="fixed inset-0 z-30" onClick={() => setActiveRowPicker(null)} />
613
+ <div className="absolute top-6 right-8 flex gap-1.5 bg-background/90 backdrop-blur-xl border border-[var(--panel-border)] p-2 rounded-xl shadow-xl z-50 items-center">
614
+ <button
615
+ onClick={(e) => {
616
+ e.preventDefault(); e.stopPropagation();
617
+ const rc = { ...(activeTable.rowColors || {}), [row.id]: 'none' };
618
+ const ts = { ...(activeTable.colorTimestamps || {}), [`row_${row.id}`]: Date.now() };
619
+ saveState({ ...activeTable, rowColors: rc, colorTimestamps: ts });
620
+ setActiveRowPicker(null);
621
+ }}
622
+ className="w-4 h-4 rounded-full flex items-center justify-center bg-foreground/10 hover:bg-red-500/20 hover:text-red-500 text-foreground/40 transition-colors border border-[var(--panel-border)]/50 shrink-0"
623
+ title="Remove Row Color"
624
+ >
625
+ <X className="w-3 h-3" />
626
+ </button>
627
+ <div className="h-4 w-px bg-[var(--panel-border)]/50 mx-0.5" />
628
+ {pastelColors.map(c => (
629
+ <button
630
+ key={c.name}
631
+ onClick={() => {
632
+ const ts = { ...(activeTable.colorTimestamps || {}), [`row_${row.id}`]: Date.now() };
633
+ saveState({ ...activeTable, rowColors: { ...(activeTable.rowColors || {}), [row.id]: c.text }, colorTimestamps: ts });
634
+ setActiveRowPicker(null);
635
+ }}
636
+ className={`w-4 h-4 rounded-full ${c.bgCircle} hover:scale-125 transition-transform border border-[var(--panel-border)]/50`}
637
+ />
638
+ ))}
639
+ </div>
640
+ </>
641
+ )}
642
+
643
+ <button onClick={() => deleteRow(row.id)} className="p-2 ml-auto rounded-lg text-foreground/20 hover:text-red-500 hover:bg-red-500/10 transition-colors flex items-center justify-center">
644
+ <Trash className="w-4 h-4" />
645
+ </button>
646
+ </div>
647
+ </td>
648
+ </tr>
649
+ )
650
+ })}
651
+ {searchedRows.length === 0 && (
652
+ <tr>
653
+ <td colSpan={(activeTable.columns || []).length + 2} className="py-12 text-center text-foreground/40 text-[14px] font-medium border border-[var(--panel-border)] m-4 rounded-xl border-dashed">
654
+ No records found in {activeTable.name || 'this table'}.
655
+ </td>
656
+ </tr>
657
+ )}
658
+ <tr>
659
+ <td colSpan={(activeTable.columns || []).length + 2} className="py-2 px-4 border-none bg-foreground/[0.01]">
660
+ <div className="flex items-center gap-4">
661
+ <button onClick={addRow} className="p-1.5 rounded-lg text-foreground/40 hover:text-accent hover:bg-accent/10 transition-colors flex items-center justify-center ml-2 border border-transparent hover:border-accent/20">
662
+ <Plus className="w-4 h-4" />
663
+ </button>
664
+ </div>
665
+ </td>
666
+ </tr>
667
+ </tbody>
668
+ </motion.table>
669
+ )}
670
+ </AnimatePresence>
671
+ </div>
672
+ </motion.div>
673
+ </main>
674
+ );
675
+ }