your-ai-workflow-firebase-os 1.2.6 → 1.2.8

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "your-ai-workflow-firebase-os",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "description": "A complete Firebase-powered admin OS — one React component.",
5
5
  "type": "module",
6
6
  "main": "dist/your-ai-workflow-firebase-os.cjs.js",
@@ -32,7 +32,8 @@
32
32
  "build:lib": "vite build",
33
33
  "lint": "eslint .",
34
34
  "preview": "vite preview",
35
- "postinstall": "node scripts/postinstall.js"
35
+ "postinstall": "node scripts/postinstall.js",
36
+ "release": "npm version patch && npm run build:lib && npm pack"
36
37
  },
37
38
  "dependencies": {
38
39
  "@eslint/js": "^9.39.4",
@@ -78,4 +79,4 @@
78
79
  "react-dom": ">=18",
79
80
  "react-router-dom": ">=6"
80
81
  }
81
- }
82
+ }
@@ -9,8 +9,6 @@ import { doc, onSnapshot, setDoc, collection, getDocs, getDoc } from 'firebase/f
9
9
  import { ConfirmModal } from '../components/ConfirmModal';
10
10
  import { JsonNode } from './ThemeAdmin';
11
11
  import { defaultHomeConfig } from '../configs/pages/home.config';
12
- import { defaultContactConfig } from '../configs/pages/contact.config';
13
- import { defaultSupportConfig } from '../configs/pages/support.config';
14
12
  import { defaultPublicConfig } from '../configs/pages/pub.config';
15
13
  import { userPageConfig as defaultPrivateConfig } from '../configs/pages/user.config';
16
14
  import { adminPageConfig as defaultAdminConfig } from '../configs/pages/admin.config';
@@ -31,10 +29,6 @@ interface PageItem {
31
29
  const initialPages: PageItem[] = [
32
30
  // Public
33
31
  { id: 'home', title: 'Home Page', path: '/', type: 'public', isSystem: true, enabled: true },
34
- { id: 'contact', title: 'Contact', path: '/contact', type: 'public', isSystem: true, enabled: true },
35
-
36
- // Private
37
- { id: 'support', title: 'Request Support', path: '/support', type: 'private', isSystem: true, enabled: true },
38
32
  ];
39
33
 
40
34
  const Toggle = ({ enabled, onChange, disabled, className = '' }: { enabled: boolean, onChange: (val: boolean) => void, disabled?: boolean, className?: string }) => (
@@ -76,14 +70,6 @@ export function PagesAdmin() {
76
70
  const [homeConfig, setHomeConfig] = useState<any>(null);
77
71
  const [savingHome, setSavingHome] = useState(false);
78
72
  const [resettingHome, setResettingHome] = useState(false);
79
-
80
- const [contactConfig, setContactConfig] = useState<any>(null);
81
- const [savingContact, setSavingContact] = useState(false);
82
- const [resettingContact, setResettingContact] = useState(false);
83
-
84
- const [supportConfig, setSupportConfig] = useState<any>(null);
85
- const [savingSupport, setSavingSupport] = useState(false);
86
- const [resettingSupport, setResettingSupport] = useState(false);
87
73
  // ── Order-based visibility (single source of truth) ─────────────────────
88
74
  const [pageHiddenIds, setPageHiddenIds] = useState<Set<string>>(new Set());
89
75
  const [tabHiddenIds, setTabHiddenIds] = useState<Set<string>>(new Set());
@@ -126,13 +112,7 @@ export function PagesAdmin() {
126
112
  const unsubHome = onSnapshot(doc(db, 'sys_pages', 'home'), (docSnap) => {
127
113
  setHomeConfig(docSnap.exists() ? mergeConfig(defaultHomeConfig, docSnap.data()) : defaultHomeConfig);
128
114
  });
129
- const unsubContact = onSnapshot(doc(db, 'sys_pages', 'contact'), (docSnap) => {
130
- setContactConfig(docSnap.exists() ? mergeConfig(defaultContactConfig, docSnap.data()) : defaultContactConfig);
131
- });
132
- const unsubSupport = onSnapshot(doc(db, 'sys_tabs', 'support'), (docSnap) => {
133
- setSupportConfig(docSnap.exists() ? mergeConfig(defaultSupportConfig, docSnap.data()) : defaultSupportConfig);
134
- });
135
- return () => { unsubHome(); unsubContact(); unsubSupport(); };
115
+ return () => { unsubHome(); };
136
116
  }, []);
137
117
 
138
118
  useEffect(() => {
@@ -199,12 +179,21 @@ export function PagesAdmin() {
199
179
  const [copyStateId, setCopyStateId] = useState<string | null>(null);
200
180
 
201
181
  const handleCopyBtn = (id: string, val: any) => {
202
- let str = JSON.stringify(val, null, 2);
182
+ let str: string;
183
+ if (id === 'home') {
184
+ // Output a complete TypeScript file so it can be pasted directly into home.config.ts
185
+ const json = JSON.stringify(val, null, 2);
186
+ str = `export const defaultHomeConfig = ${json};\n`;
187
+ } else {
188
+ str = JSON.stringify(val, null, 2);
189
+ }
203
190
  navigator.clipboard.writeText(str);
204
191
  setCopyStateId(id);
205
192
  setTimeout(() => setCopyStateId(null), 2000);
206
193
  };
207
194
 
195
+
196
+
208
197
  const handleSaveConfig = async (id: string, val: any) => {
209
198
  if (val.route && !val.route.startsWith('/')) {
210
199
  val.route = '/' + val.route;
@@ -218,14 +207,12 @@ export function PagesAdmin() {
218
207
  val.pageType = pageType;
219
208
  }
220
209
 
221
- const isSystemPage = ['home', 'contact', 'support'].includes(id);
210
+ const isSystemPage = ['home'].includes(id);
222
211
  const targetId = isSystemPage
223
212
  ? id
224
213
  : (val.route ? val.route.replace(/^\/+/, '').replace(/[^a-zA-Z0-9_\-]/g, '_').toLowerCase() : id);
225
214
 
226
215
  if (id === 'home') setSavingHome(true);
227
- else if (id === 'contact') setSavingContact(true);
228
- else if (id === 'support') setSavingSupport(true);
229
216
  else setSavingConfigMap(p => ({ ...p, [id]: true, [targetId]: true }));
230
217
 
231
218
  try {
@@ -267,8 +254,6 @@ export function PagesAdmin() {
267
254
  } else {
268
255
  await setDoc(doc(db, collectionName, id), val);
269
256
  if (id === 'home') setHomeConfig(val);
270
- else if (id === 'contact') setContactConfig(val);
271
- else if (id === 'support') setSupportConfig(val);
272
257
  else {
273
258
  setDraftConfigs(p => ({ ...p, [id]: val }));
274
259
  setPages(prev => prev.map(p => p.id === id ? { ...p, path: val.route || p.path, title: val.pageName || val.tabName || val.title || p.title } : p));
@@ -279,8 +264,6 @@ export function PagesAdmin() {
279
264
  } finally {
280
265
  setTimeout(() => {
281
266
  if (id === 'home') setSavingHome(false);
282
- else if (id === 'contact') setSavingContact(false);
283
- else if (id === 'support') setSavingSupport(false);
284
267
  else setSavingConfigMap(p => ({ ...p, [id]: false, [targetId]: false }));
285
268
  }, 500);
286
269
  }
@@ -292,17 +275,6 @@ export function PagesAdmin() {
292
275
  setTimeout(() => setResettingHome(false), 500);
293
276
  };
294
277
 
295
- const handleResetContact = async (e: React.MouseEvent) => {
296
- e.stopPropagation(); setResettingContact(true);
297
- await handleSaveConfig('contact', defaultContactConfig);
298
- setTimeout(() => setResettingContact(false), 500);
299
- };
300
-
301
- const handleResetSupport = async (e: React.MouseEvent) => {
302
- e.stopPropagation(); setResettingSupport(true);
303
- await handleSaveConfig('support', defaultSupportConfig);
304
- setTimeout(() => setResettingSupport(false), 500);
305
- };
306
278
 
307
279
  const handleResetPage = async (e: React.MouseEvent, pageId: string, defConfig: any) => {
308
280
  e.stopPropagation(); setResettingMap(p => ({ ...p, [pageId]: true }));
@@ -428,12 +400,11 @@ export function PagesAdmin() {
428
400
  setPages(prev => [...prev, newPage]);
429
401
 
430
402
  let sourceConfig = draftConfigs[originalPage.id];
431
- if (!sourceConfig) {
432
- if (originalPage.id === 'contact') sourceConfig = contactConfig || defaultContactConfig;
433
- if (originalPage.id === 'support') sourceConfig = supportConfig || defaultSupportConfig;
403
+ if (!sourceConfig && originalPage.id === 'home') {
404
+ sourceConfig = homeConfig;
434
405
  }
435
406
 
436
- const newConfig = { ...sourceConfig, route: newPage.path, pageName: newPage.title };
407
+ const newConfig = { ...(sourceConfig || {}), route: newPage.path, pageName: newPage.title };
437
408
  if (newPage.type !== 'public') {
438
409
  newConfig.tabName = newPage.title;
439
410
  newConfig.pageType = newPage.type;
@@ -483,13 +454,9 @@ export function PagesAdmin() {
483
454
  try {
484
455
  const { deleteDoc } = await import('firebase/firestore');
485
456
  await Promise.all([
486
- deleteDoc(doc(db, 'sys_pages', 'home')),
487
- deleteDoc(doc(db, 'sys_pages', 'contact')),
488
- deleteDoc(doc(db, 'sys_tabs', 'support'))
457
+ deleteDoc(doc(db, 'sys_pages', 'home'))
489
458
  ]);
490
459
  setHomeConfig(defaultHomeConfig);
491
- setContactConfig(defaultContactConfig);
492
- setSupportConfig(defaultSupportConfig);
493
460
  } catch (e) {
494
461
  console.error("Error resetting defaults:", e);
495
462
  }
@@ -699,10 +666,8 @@ export function PagesAdmin() {
699
666
 
700
667
  const renderPage = (page: PageItem, pIdx: number, isHidden: boolean) => {
701
668
  const isHome = page.id === 'home';
702
- const isContact = page.id === 'contact';
703
- const isSupport = page.id === 'support';
704
669
  const isExpanded = expandedPageId === page.id;
705
- const isConfigurable = isHome || isContact || isSupport || !page.isSystem;
670
+ const isConfigurable = isHome || !page.isSystem;
706
671
 
707
672
  return (
708
673
  <div
@@ -721,7 +686,7 @@ export function PagesAdmin() {
721
686
  <div className="flex items-center gap-1 w-full flex-1">
722
687
  <div className="flex items-center gap-2">
723
688
  <h3 className="text-[14px] font-bold text-foreground truncate">{page.title}</h3>
724
- {((isHome && savingHome) || (isContact && savingContact) || (isSupport && savingSupport)) && <Loader2 className="w-3 h-3 animate-spin text-accent ml-2" />}
689
+ {isHome && savingHome && <Loader2 className="w-3 h-3 animate-spin text-accent ml-2" />}
725
690
  </div>
726
691
  <div className={`text-[11px] font-medium font-mono px-2 py-0.5 rounded w-fit ml-6 border ${isHidden ? 'text-foreground/50 border-foreground/10 bg-foreground/5' : 'text-accent/70 bg-accent/5 border-accent/10'}`}>
727
692
  {window.location.host}{page.path}
@@ -780,6 +745,7 @@ export function PagesAdmin() {
780
745
  <button onClick={(e) => { e.stopPropagation(); handleCopyBtn('home', homeConfig); }} className="w-8 h-8 p-0 border-transparent bg-transparent hover:bg-foreground/10 rounded-lg flex items-center justify-center shrink-0 transition-colors" title="Copy JSON">
781
746
  {copyStateId === 'home' ? <CheckCircle2 className="w-3.5 h-3.5 text-emerald-500" /> : <Copy className="w-3.5 h-3.5 text-foreground/40 hover:text-foreground transition-colors" />}
782
747
  </button>
748
+
783
749
  </div>
784
750
  <JsonNode
785
751
  value={homeConfig}
@@ -795,77 +761,7 @@ export function PagesAdmin() {
795
761
  </div>
796
762
  </motion.div>
797
763
  )}
798
- {isExpanded && isContact && contactConfig && (
799
- <motion.div
800
- initial={{ height: 0, opacity: 0 }}
801
- animate={{ height: "auto", opacity: 1 }}
802
- exit={{ height: 0, opacity: 0 }}
803
- className="overflow-hidden mt-4 pt-4 border-t border-[var(--panel-border)]/50 pb-4"
804
- onClick={e => e.stopPropagation()}
805
- >
806
- <div className="flex flex-col font-mono text-[13px] leading-relaxed relative bg-background/50 p-4 py-6 rounded-xl border border-[var(--panel-border)] shadow-inner">
807
764
 
808
- <div className="absolute top-4 right-4 flex items-center gap-1 z-20">
809
- <button onClick={handleResetContact} className="w-8 h-8 p-0 border-transparent bg-transparent hover:bg-red-500/10 rounded-lg flex items-center justify-center shrink-0 transition-colors group" title="Reset to Defaults">
810
- {resettingContact ? <Loader2 className="w-3.5 h-3.5 text-red-500 animate-spin" /> : <RotateCcw className="w-3.5 h-3.5 text-foreground/40 group-hover:text-red-500 transition-colors" />}
811
- </button>
812
- <button onClick={(e) => { e.stopPropagation(); handleCopyBtn('contact', contactConfig); }} className="w-8 h-8 p-0 border-transparent bg-transparent hover:bg-foreground/10 rounded-lg flex items-center justify-center shrink-0 transition-colors" title="Copy JSON">
813
- {copyStateId === 'contact' ? <CheckCircle2 className="w-3.5 h-3.5 text-emerald-500" /> : <Copy className="w-3.5 h-3.5 text-foreground/40 hover:text-foreground transition-colors" />}
814
- </button>
815
- </div>
816
- <JsonNode
817
- value={contactConfig}
818
- keysToRender={contactConfig.showButton !== false ? ['pageName', 'route', 'template', 'showButton', 'form'] : ['pageName', 'route', 'template', 'showButton']}
819
- disabledKeys={['route']}
820
- onChange={(val: any) => handleSaveConfig('contact', val)}
821
- isLast={true}
822
- isGlobalSaving={savingContact}
823
- customEnums={{ form: publicForms, template: ['popup_form', 'inline_form'] }}
824
- customExplanations={{
825
- pageName: "This defines the text that actually appears globally in the Navigation menus.",
826
- template: "This just simply tells our app when a user clicks on the contact page item what type of form do we load? is it a page or popup? this doesn't define form content! this setting just defines page layout.",
827
- form: "This is the form that will be used. Go to /forms to edit it."
828
- }}
829
- />
830
- </div>
831
- </motion.div>
832
- )}
833
- {isExpanded && isSupport && supportConfig && (
834
- <motion.div
835
- initial={{ height: 0, opacity: 0 }}
836
- animate={{ height: "auto", opacity: 1 }}
837
- exit={{ height: 0, opacity: 0 }}
838
- className="overflow-hidden mt-4 pt-4 border-t border-[var(--panel-border)]/50 pb-4"
839
- onClick={e => e.stopPropagation()}
840
- >
841
- <div className="flex flex-col font-mono text-[13px] leading-relaxed relative bg-background/50 p-4 py-6 rounded-xl border border-[var(--panel-border)] shadow-inner">
842
-
843
- <div className="absolute top-4 right-4 flex items-center gap-1 z-20">
844
- <button onClick={handleResetSupport} className="w-8 h-8 p-0 border-transparent bg-transparent hover:bg-red-500/10 rounded-lg flex items-center justify-center shrink-0 transition-colors group" title="Reset to Defaults">
845
- {resettingSupport ? <Loader2 className="w-3.5 h-3.5 text-red-500 animate-spin" /> : <RotateCcw className="w-3.5 h-3.5 text-foreground/40 group-hover:text-red-500 transition-colors" />}
846
- </button>
847
- <button onClick={(e) => { e.stopPropagation(); handleCopyBtn('support', supportConfig); }} className="w-8 h-8 p-0 border-transparent bg-transparent hover:bg-foreground/10 rounded-lg flex items-center justify-center shrink-0 transition-colors" title="Copy JSON">
848
- {copyStateId === 'support' ? <CheckCircle2 className="w-3.5 h-3.5 text-emerald-500" /> : <Copy className="w-3.5 h-3.5 text-foreground/40 hover:text-foreground transition-colors" />}
849
- </button>
850
- </div>
851
- <JsonNode
852
- value={supportConfig}
853
- keysToRender={supportConfig.showButton !== false ? ['pageName', 'iconName', 'route', 'template', 'showButton', 'form'] : ['pageName', 'iconName', 'route', 'template', 'showButton']}
854
- disabledKeys={['route']}
855
- onChange={(val: any) => handleSaveConfig('support', val)}
856
- isLast={true}
857
- isGlobalSaving={savingSupport}
858
- customEnums={{ form: privateForms, template: ['popup_form', 'inline_form'] }}
859
- customExplanations={{
860
- pageName: "Defines the textual title label rendering in internal sidebars.",
861
- iconName: "To modify this icon, visit https://iconoir.com/, find an icon, and copy its exact name (e.g. 'learning', 'server'). Then paste it here.",
862
- template: "This just simply tells our app when a user clicks on the support page item what type of form do we load? is it a page or popup? this doesn't define form content! this setting just defines page layout.",
863
- form: "This is the form that will be used. Go to /forms to edit it."
864
- }}
865
- />
866
- </div>
867
- </motion.div>
868
- )}
869
765
  {isExpanded && !page.isSystem && draftConfigs[page.id] && (
870
766
  <motion.div
871
767
  initial={{ height: 0, opacity: 0 }}
@@ -881,8 +777,9 @@ export function PagesAdmin() {
881
777
  {resettingMap[page.id] ? <Loader2 className="w-3.5 h-3.5 text-red-500 animate-spin" /> : <RotateCcw className="w-3.5 h-3.5 text-foreground/40 group-hover:text-red-500 transition-colors" />}
882
778
  </button>
883
779
  <button onClick={(e) => { e.stopPropagation(); handleCopyBtn(page.id, draftConfigs[page.id]); }} className="w-8 h-8 p-0 border-transparent bg-transparent hover:bg-foreground/10 rounded-lg flex items-center justify-center shrink-0 transition-colors" title="Copy JSON">
884
- {copyStateId === page.id ? <CheckCircle2 className="w-10 h-10 p-0 border-transparent bg-transparent hover:bg-foreground/10 rounded-lg flex items-center justify-center shrink-0 transition-colors" /> : <Copy className="w-3.5 h-3.5 text-foreground/40 hover:text-foreground transition-colors" />}
780
+ {copyStateId === page.id ? <CheckCircle2 className="w-3.5 h-3.5 text-emerald-500" /> : <Copy className="w-3.5 h-3.5 text-foreground/40 hover:text-foreground transition-colors" />}
885
781
  </button>
782
+
886
783
  </div>
887
784
  <JsonNode
888
785
  value={draftConfigs[page.id]}