@olonjs/cli 3.0.82 → 3.0.84

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.
@@ -596,7 +596,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
596
596
  "@tiptap/extension-link": "^2.11.5",
597
597
  "@tiptap/react": "^2.11.5",
598
598
  "@tiptap/starter-kit": "^2.11.5",
599
- "@olonjs/core": "^1.0.70",
599
+ "@olonjs/core": "^1.0.72",
600
600
  "clsx": "^2.1.1",
601
601
  "lucide-react": "^0.474.0",
602
602
  "react": "^19.0.0",
@@ -941,6 +941,7 @@ import themeData from '@/data/config/theme.json';
941
941
  import menuData from '@/data/config/menu.json';
942
942
  import { getFilePages } from '@/lib/getFilePages';
943
943
  import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
944
+ import { Skeleton } from '@/components/ui/skeleton';
944
945
 
945
946
  import tenantCss from './index.css?inline';
946
947
 
@@ -1194,6 +1195,16 @@ function cloudFingerprint(apiBase: string, apiKey: string): string {
1194
1195
  return `${normalizeApiBase(apiBase)}::${apiKey.slice(-8)}`;
1195
1196
  }
1196
1197
 
1198
+ function normalizeSlugForCache(slug: string): string {
1199
+ return (
1200
+ slug
1201
+ .trim()
1202
+ .toLowerCase()
1203
+ .replace(/[^a-z0-9/_-]/g, '-')
1204
+ .replace(/^\/+|\/+$/g, '') || 'home'
1205
+ );
1206
+ }
1207
+
1197
1208
  function readCachedCloudContent(fingerprint: string): CachedCloudContent | null {
1198
1209
  try {
1199
1210
  const raw = localStorage.getItem(CLOUD_CACHE_KEY);
@@ -1248,7 +1259,6 @@ function App() {
1248
1259
  const activeCloudSaveController = useRef<AbortController | null>(null);
1249
1260
  const contentLoadInFlight = useRef<Promise<void> | null>(null);
1250
1261
  const pendingCloudSave = useRef<{ state: ProjectState; slug: string } | null>(null);
1251
- const warmBootFromCloudCache = useRef(false);
1252
1262
  const cloudApiCandidates = useMemo(
1253
1263
  () => (isCloudMode && CLOUD_API_URL ? buildApiCandidates(CLOUD_API_URL) : []),
1254
1264
  [isCloudMode, CLOUD_API_URL]
@@ -1288,26 +1298,16 @@ function App() {
1288
1298
  const primaryApiBase = cloudApiCandidates[0] ?? normalizeApiBase(CLOUD_API_URL);
1289
1299
  const fingerprint = cloudFingerprint(primaryApiBase, CLOUD_API_KEY);
1290
1300
  const cached = readCachedCloudContent(fingerprint);
1301
+ const cachedPages = cached ? toPagesRecord(cached.pages) : null;
1302
+ const cachedSite = cached ? coerceSiteConfig(cached.siteConfig) : null;
1303
+ const hasCachedFallback = Boolean((cachedPages && Object.keys(cachedPages).length > 0) || cachedSite);
1291
1304
  if (cached) {
1292
- const cachedPages = toPagesRecord(cached.pages);
1293
- const cachedSite = coerceSiteConfig(cached.siteConfig);
1294
- if (cachedPages && Object.keys(cachedPages).length > 0) {
1295
- setPages(cachedPages);
1296
- }
1297
- if (cachedSite) {
1298
- setSiteConfig(cachedSite);
1299
- }
1300
- setContentMode('cloud');
1301
- setContentFallback(null);
1302
- warmBootFromCloudCache.current = true;
1303
- setShowTopProgress(false);
1304
- setHasInitialCloudResolved(true);
1305
1305
  logBootstrapEvent('boot.cloud.cache_hit', { ageMs: Date.now() - cached.savedAt });
1306
- } else {
1307
- warmBootFromCloudCache.current = false;
1308
- setShowTopProgress(true);
1309
- setHasInitialCloudResolved(false);
1310
1306
  }
1307
+ setContentMode('cloud');
1308
+ setContentFallback(null);
1309
+ setShowTopProgress(true);
1310
+ setHasInitialCloudResolved(false);
1311
1311
  logBootstrapEvent('boot.start', { mode: 'cloud', apiCandidates: cloudApiCandidates.length });
1312
1312
 
1313
1313
  const loadCloudContent = async () => {
@@ -1320,6 +1320,7 @@ function App() {
1320
1320
  try {
1321
1321
  const res = await fetch(`${apiBase}/content`, {
1322
1322
  method: 'GET',
1323
+ cache: 'no-store',
1323
1324
  headers: {
1324
1325
  Authorization: `Bearer ${CLOUD_API_KEY}`,
1325
1326
  },
@@ -1422,7 +1423,13 @@ function App() {
1422
1423
  } catch (error: unknown) {
1423
1424
  if (controller.signal.aborted) return;
1424
1425
  const failure = toCloudLoadFailure(error);
1425
- if (warmBootFromCloudCache.current) {
1426
+ if (hasCachedFallback) {
1427
+ if (cachedPages && Object.keys(cachedPages).length > 0) {
1428
+ setPages(cachedPages);
1429
+ }
1430
+ if (cachedSite) {
1431
+ setSiteConfig(cachedSite);
1432
+ }
1426
1433
  setContentMode('cloud');
1427
1434
  setContentFallback({
1428
1435
  reasonCode: 'CLOUD_REFRESH_FAILED',
@@ -1589,14 +1596,26 @@ function App() {
1589
1596
  },
1590
1597
  body: JSON.stringify({
1591
1598
  slug,
1592
- type: 'page',
1593
- data: state.page,
1599
+ page: state.page,
1600
+ siteConfig: state.site,
1594
1601
  }),
1595
1602
  });
1596
1603
  const body = (await res.json().catch(() => ({}))) as { error?: string; code?: string };
1597
1604
  if (!res.ok) {
1598
1605
  throw new Error(body.error || body.code || `Hot save failed: ${res.status}`);
1599
1606
  }
1607
+ const keyFingerprint = cloudFingerprint(apiBase, CLOUD_API_KEY);
1608
+ const normalizedSlug = normalizeSlugForCache(slug);
1609
+ const existing = readCachedCloudContent(keyFingerprint);
1610
+ writeCachedCloudContent({
1611
+ keyFingerprint,
1612
+ savedAt: Date.now(),
1613
+ siteConfig: state.site ?? null,
1614
+ pages: {
1615
+ ...(existing?.pages ?? {}),
1616
+ [normalizedSlug]: state.page,
1617
+ },
1618
+ });
1600
1619
  },
1601
1620
  showLegacySave: !isCloudMode,
1602
1621
  showHotSave: isCloudMode,
@@ -1667,6 +1686,26 @@ function App() {
1667
1686
  </div>
1668
1687
  </>
1669
1688
  ) : null}
1689
+ {isCloudMode && !hasInitialCloudResolved ? (
1690
+ <div className="fixed inset-0 z-[1290] bg-background/80 backdrop-blur-sm">
1691
+ <div className="mx-auto w-full max-w-[1600px] p-6">
1692
+ <div className="grid gap-4 lg:grid-cols-[1fr_420px]">
1693
+ <div className="space-y-4">
1694
+ <Skeleton className="h-10 w-64" />
1695
+ <Skeleton className="h-[220px] w-full rounded-xl" />
1696
+ <Skeleton className="h-[220px] w-full rounded-xl" />
1697
+ </div>
1698
+ <div className="space-y-3 rounded-xl border border-border/50 bg-card/60 p-4">
1699
+ <Skeleton className="h-8 w-32" />
1700
+ <Skeleton className="h-5 w-full" />
1701
+ <Skeleton className="h-5 w-5/6" />
1702
+ <Skeleton className="h-5 w-4/6" />
1703
+ <Skeleton className="h-24 w-full rounded-lg" />
1704
+ </div>
1705
+ </div>
1706
+ </div>
1707
+ </div>
1708
+ ) : null}
1670
1709
  {shouldRenderEngine ? <JsonPagesEngine config={config} /> : null}
1671
1710
  {isCloudMode && (contentMode === 'error' || contentFallback?.reasonCode === 'CLOUD_REFRESH_FAILED') ? (
1672
1711
  <div
@@ -1741,102 +1780,6 @@ function App() {
1741
1780
  export default App;
1742
1781
 
1743
1782
 
1744
- END_OF_FILE_CONTENT
1745
- # SKIP: src/App.tsx:Zone.Identifier is binary and cannot be embedded as text.
1746
- echo "Creating src/App_.tsx..."
1747
- cat << 'END_OF_FILE_CONTENT' > "src/App_.tsx"
1748
- import { useState, useEffect } from 'react';
1749
- import { JsonPagesEngine } from '@jsonpages/core';
1750
- import type { LibraryImageEntry } from '@jsonpages/core';
1751
- import { ComponentRegistry } from '@/lib/ComponentRegistry';
1752
- import { SECTION_SCHEMAS } from '@/lib/schemas';
1753
- import { addSectionConfig } from '@/lib/addSectionConfig';
1754
- import { getHydratedData } from '@/lib/draftStorage';
1755
- import type { JsonPagesConfig, ProjectState } from '@jsonpages/core';
1756
- import type { SiteConfig, ThemeConfig, MenuConfig } from '@/types';
1757
-
1758
- import siteData from '@/data/config/site.json';
1759
- import themeData from '@/data/config/theme.json';
1760
- import menuData from '@/data/config/menu.json';
1761
- import { getFilePages } from '@/lib/getFilePages';
1762
-
1763
- import fontsCss from './fonts.css?inline';
1764
- import tenantCss from './index.css?inline';
1765
-
1766
- const themeConfig = themeData as unknown as ThemeConfig;
1767
- const menuConfig = menuData as unknown as MenuConfig;
1768
- const TENANT_ID = 'alpha';
1769
-
1770
- const filePages = getFilePages();
1771
- const fileSiteConfig = siteData as unknown as SiteConfig;
1772
- const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
1773
-
1774
- function getInitialData() {
1775
- return getHydratedData(TENANT_ID, filePages, fileSiteConfig);
1776
- }
1777
-
1778
- function App() {
1779
- const [{ pages, siteConfig }] = useState(getInitialData);
1780
- const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
1781
-
1782
- useEffect(() => {
1783
- fetch('/api/list-assets')
1784
- .then((r) => (r.ok ? r.json() : []))
1785
- .then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
1786
- .catch(() => setAssetsManifest([]));
1787
- }, []);
1788
-
1789
- const config: JsonPagesConfig = {
1790
- tenantId: TENANT_ID,
1791
- registry: ComponentRegistry as JsonPagesConfig['registry'],
1792
- schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
1793
- pages,
1794
- siteConfig,
1795
- themeConfig,
1796
- menuConfig,
1797
- themeCss: { tenant: fontsCss + '\n' + tenantCss },
1798
- addSection: addSectionConfig,
1799
- persistence: {
1800
- async saveToFile(state: ProjectState, slug: string): Promise<void> {
1801
- const res = await fetch('/api/save-to-file', {
1802
- method: 'POST',
1803
- headers: { 'Content-Type': 'application/json' },
1804
- body: JSON.stringify({ projectState: state, slug }),
1805
- });
1806
- const body = (await res.json().catch(() => ({}))) as { error?: string };
1807
- if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
1808
- },
1809
- },
1810
- assets: {
1811
- assetsBaseUrl: '/assets',
1812
- assetsManifest,
1813
- async onAssetUpload(file: File): Promise<string> {
1814
- if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
1815
- if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
1816
- const base64 = await new Promise<string>((resolve, reject) => {
1817
- const reader = new FileReader();
1818
- reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
1819
- reader.onerror = () => reject(reader.error);
1820
- reader.readAsDataURL(file);
1821
- });
1822
- const res = await fetch('/api/upload-asset', {
1823
- method: 'POST',
1824
- headers: { 'Content-Type': 'application/json' },
1825
- body: JSON.stringify({ filename: file.name, mimeType: file.type || undefined, data: base64 }),
1826
- });
1827
- const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
1828
- if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
1829
- if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
1830
- return body.url;
1831
- },
1832
- },
1833
- };
1834
-
1835
- return <JsonPagesEngine config={config} />;
1836
- }
1837
-
1838
- export default App;
1839
-
1840
1783
  END_OF_FILE_CONTENT
1841
1784
  mkdir -p "src/components"
1842
1785
  echo "Creating src/components/NotFound.tsx..."
@@ -7186,6 +7129,26 @@ export { Separator }
7186
7129
 
7187
7130
 
7188
7131
 
7132
+ END_OF_FILE_CONTENT
7133
+ echo "Creating src/components/ui/skeleton.tsx..."
7134
+ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/skeleton.tsx"
7135
+ import { cn } from '@/lib/utils';
7136
+ import type { HTMLAttributes } from 'react';
7137
+
7138
+ function Skeleton({
7139
+ className,
7140
+ ...props
7141
+ }: HTMLAttributes<HTMLDivElement>) {
7142
+ return (
7143
+ <div
7144
+ className={cn('animate-pulse rounded-md bg-muted', className)}
7145
+ {...props}
7146
+ />
7147
+ );
7148
+ }
7149
+
7150
+ export { Skeleton };
7151
+
7189
7152
  END_OF_FILE_CONTENT
7190
7153
  echo "Creating src/components/ui/textarea.tsx..."
7191
7154
  cat << 'END_OF_FILE_CONTENT' > "src/components/ui/textarea.tsx"
@@ -9335,24 +9298,6 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
9335
9298
 
9336
9299
 
9337
9300
 
9338
- END_OF_FILE_CONTENT
9339
- echo "Creating src/main_.tsx..."
9340
- cat << 'END_OF_FILE_CONTENT' > "src/main_.tsx"
9341
- import '@/types'; // TBP: load type augmentation from capsule-driven types
9342
- import React from 'react';
9343
- import ReactDOM from 'react-dom/client';
9344
- import App from './App';
9345
- // ... resto del file
9346
-
9347
- ReactDOM.createRoot(document.getElementById('root')!).render(
9348
- <React.StrictMode>
9349
- <App />
9350
- </React.StrictMode>
9351
- );
9352
-
9353
-
9354
-
9355
-
9356
9301
  END_OF_FILE_CONTENT
9357
9302
  # SKIP: src/registry-types.ts is binary and cannot be embedded as text.
9358
9303
  mkdir -p "src/server"
@@ -596,7 +596,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
596
596
  "@tiptap/extension-link": "^2.11.5",
597
597
  "@tiptap/react": "^2.11.5",
598
598
  "@tiptap/starter-kit": "^2.11.5",
599
- "@olonjs/core": "^1.0.70",
599
+ "@olonjs/core": "^1.0.72",
600
600
  "clsx": "^2.1.1",
601
601
  "lucide-react": "^0.474.0",
602
602
  "react": "^19.0.0",
@@ -1074,6 +1074,30 @@ function App() {
1074
1074
  const body = (await res.json().catch(() => ({}))) as { error?: string };
1075
1075
  if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
1076
1076
  },
1077
+ async hotSave(state: ProjectState, slug: string): Promise<void> {
1078
+ if (!isCloudMode || !CLOUD_API_URL || !CLOUD_API_KEY) {
1079
+ throw new Error('Cloud mode is not configured for hot save.');
1080
+ }
1081
+ const apiBase = CLOUD_API_URL.replace(/\/$/, '');
1082
+ const res = await fetch(`${apiBase}/hotSave`, {
1083
+ method: 'POST',
1084
+ headers: {
1085
+ 'Content-Type': 'application/json',
1086
+ Authorization: `Bearer ${CLOUD_API_KEY}`,
1087
+ },
1088
+ body: JSON.stringify({
1089
+ slug,
1090
+ page: state.page,
1091
+ siteConfig: state.site,
1092
+ }),
1093
+ });
1094
+ const body = (await res.json().catch(() => ({}))) as { error?: string; code?: string };
1095
+ if (!res.ok) {
1096
+ throw new Error(body.error || body.code || `Hot save failed: ${res.status}`);
1097
+ }
1098
+ },
1099
+ showLegacySave: !isCloudMode,
1100
+ showHotSave: isCloudMode,
1077
1101
  },
1078
1102
  assets: {
1079
1103
  assetsBaseUrl: '/assets',
@@ -1120,264 +1144,6 @@ function App() {
1120
1144
 
1121
1145
  export default App;
1122
1146
 
1123
- END_OF_FILE_CONTENT
1124
- echo "Creating src/App_.tsx..."
1125
- cat << 'END_OF_FILE_CONTENT' > "src/App_.tsx"
1126
- import { useState, useEffect } from 'react';
1127
- import { JsonPagesEngine } from '@jsonpages/core';
1128
- import type { LibraryImageEntry } from '@jsonpages/core';
1129
- import { ComponentRegistry } from '@/lib/ComponentRegistry';
1130
- import { SECTION_SCHEMAS } from '@/lib/schemas';
1131
- import { addSectionConfig } from '@/lib/addSectionConfig';
1132
- import { getHydratedData } from '@/lib/draftStorage';
1133
- import type { JsonPagesConfig, ProjectState } from '@jsonpages/core';
1134
- import type { SiteConfig, ThemeConfig, MenuConfig } from '@/types';
1135
-
1136
- import siteData from '@/data/config/site.json';
1137
- import themeData from '@/data/config/theme.json';
1138
- import menuData from '@/data/config/menu.json';
1139
- import { getFilePages } from '@/lib/getFilePages';
1140
-
1141
- import fontsCss from './fonts.css?inline';
1142
- import tenantCss from './index.css?inline';
1143
-
1144
- const themeConfig = themeData as unknown as ThemeConfig;
1145
- const menuConfig = menuData as unknown as MenuConfig;
1146
- const TENANT_ID = 'alpha';
1147
-
1148
- const filePages = getFilePages();
1149
- const fileSiteConfig = siteData as unknown as SiteConfig;
1150
- const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
1151
-
1152
- function getInitialData() {
1153
- return getHydratedData(TENANT_ID, filePages, fileSiteConfig);
1154
- }
1155
-
1156
- function App() {
1157
- const [{ pages, siteConfig }] = useState(getInitialData);
1158
- const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
1159
-
1160
- useEffect(() => {
1161
- fetch('/api/list-assets')
1162
- .then((r) => (r.ok ? r.json() : []))
1163
- .then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
1164
- .catch(() => setAssetsManifest([]));
1165
- }, []);
1166
-
1167
- const config: JsonPagesConfig = {
1168
- tenantId: TENANT_ID,
1169
- registry: ComponentRegistry as JsonPagesConfig['registry'],
1170
- schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
1171
- pages,
1172
- siteConfig,
1173
- themeConfig,
1174
- menuConfig,
1175
- themeCss: { tenant: fontsCss + '\n' + tenantCss },
1176
- addSection: addSectionConfig,
1177
- persistence: {
1178
- async saveToFile(state: ProjectState, slug: string): Promise<void> {
1179
- const res = await fetch('/api/save-to-file', {
1180
- method: 'POST',
1181
- headers: { 'Content-Type': 'application/json' },
1182
- body: JSON.stringify({ projectState: state, slug }),
1183
- });
1184
- const body = (await res.json().catch(() => ({}))) as { error?: string };
1185
- if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
1186
- },
1187
- },
1188
- assets: {
1189
- assetsBaseUrl: '/assets',
1190
- assetsManifest,
1191
- async onAssetUpload(file: File): Promise<string> {
1192
- if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
1193
- if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
1194
- const base64 = await new Promise<string>((resolve, reject) => {
1195
- const reader = new FileReader();
1196
- reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
1197
- reader.onerror = () => reject(reader.error);
1198
- reader.readAsDataURL(file);
1199
- });
1200
- const res = await fetch('/api/upload-asset', {
1201
- method: 'POST',
1202
- headers: { 'Content-Type': 'application/json' },
1203
- body: JSON.stringify({ filename: file.name, mimeType: file.type || undefined, data: base64 }),
1204
- });
1205
- const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
1206
- if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
1207
- if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
1208
- return body.url;
1209
- },
1210
- },
1211
- };
1212
-
1213
- return <JsonPagesEngine config={config} />;
1214
- }
1215
-
1216
- export default App;
1217
-
1218
- END_OF_FILE_CONTENT
1219
- echo "Creating src/_App.tsx..."
1220
- cat << 'END_OF_FILE_CONTENT' > "src/_App.tsx"
1221
- /**
1222
- * Thin Entry Point (Tenant).
1223
- * Data from getHydratedData (file-backed or draft); assets from public/assets/images.
1224
- * Supports Hybrid Persistence: Local Filesystem (Dev) or Cloud Bridge (Prod).
1225
- */
1226
- import { useState, useEffect } from 'react';
1227
- import { JsonPagesEngine } from '@jsonpages/core';
1228
- import type { LibraryImageEntry } from '@jsonpages/core';
1229
- import { ComponentRegistry } from '@/lib/ComponentRegistry';
1230
- import { SECTION_SCHEMAS } from '@/lib/schemas';
1231
- import { addSectionConfig } from '@/lib/addSectionConfig';
1232
- import { getHydratedData } from '@/lib/draftStorage';
1233
- import type { JsonPagesConfig, ProjectState } from '@jsonpages/core';
1234
- import type { SiteConfig, ThemeConfig, MenuConfig } from '@/types';
1235
-
1236
- import siteData from '@/data/config/site.json';
1237
- import themeData from '@/data/config/theme.json';
1238
- import menuData from '@/data/config/menu.json';
1239
- import { getFilePages } from '@/lib/getFilePages';
1240
-
1241
- import fontsCss from './fonts.css?inline';
1242
- import tenantCss from './index.css?inline';
1243
-
1244
- // Cloud Configuration (Injected by Vercel/Netlify Env Vars)
1245
- const CLOUD_API_URL = import.meta.env.VITE_JSONPAGES_CLOUD_URL;
1246
- const CLOUD_API_KEY = import.meta.env.VITE_JSONPAGES_API_KEY;
1247
-
1248
- const themeConfig = themeData as unknown as ThemeConfig;
1249
- const menuConfig = menuData as unknown as MenuConfig;
1250
- const TENANT_ID = 'alpha';
1251
-
1252
- const filePages = getFilePages();
1253
- const fileSiteConfig = siteData as unknown as SiteConfig;
1254
- const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
1255
-
1256
- function getInitialData() {
1257
- return getHydratedData(TENANT_ID, filePages, fileSiteConfig);
1258
- }
1259
-
1260
- function App() {
1261
- const [{ pages, siteConfig }] = useState(getInitialData);
1262
- const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
1263
-
1264
- useEffect(() => {
1265
- // In Cloud mode, listing assets might be different or disabled for MVP
1266
- // For now, we keep the local fetch which will fail gracefully on Vercel (404)
1267
- fetch('/api/list-assets')
1268
- .then((r) => (r.ok ? r.json() : []))
1269
- .then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
1270
- .catch(() => setAssetsManifest([]));
1271
- }, []);
1272
-
1273
- console.log("🔍 DEBUG ENV:", {
1274
- URL: import.meta.env.VITE_JSONPAGES_CLOUD_URL,
1275
- KEY: import.meta.env.VITE_JSONPAGES_API_KEY ? "PRESENT" : "MISSING"
1276
- });
1277
- const config: JsonPagesConfig = {
1278
- tenantId: TENANT_ID,
1279
- registry: ComponentRegistry as JsonPagesConfig['registry'],
1280
- schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
1281
- pages,
1282
- siteConfig,
1283
- themeConfig,
1284
- menuConfig,
1285
- themeCss: { tenant: fontsCss + '\n' + tenantCss },
1286
- addSection: addSectionConfig,
1287
- persistence: {
1288
- async saveToFile(state: ProjectState, slug: string): Promise<void> {
1289
-
1290
- // ☁️ SCENARIO A: CLOUD BRIDGE (Production)
1291
- if (CLOUD_API_URL && CLOUD_API_KEY) {
1292
- console.log(`☁️ Saving ${slug} via Cloud Bridge...`);
1293
-
1294
- const res = await fetch(`${CLOUD_API_URL}/save`, {
1295
- method: 'POST',
1296
- headers: {
1297
- 'Content-Type': 'application/json',
1298
- 'Authorization': `Bearer ${CLOUD_API_KEY}`
1299
- },
1300
- body: JSON.stringify({
1301
- // Mapping logical slug to physical path in repo
1302
- path: `src/data/pages/${slug}.json`,
1303
- // We save the page config specifically
1304
- content: state.page,
1305
- message: `Content update for ${slug} via Visual Editor`
1306
- }),
1307
- });
1308
-
1309
- if (!res.ok) {
1310
- const err = await res.json().catch(() => ({}));
1311
- throw new Error(err.error || `Cloud save failed: ${res.status}`);
1312
- }
1313
- return;
1314
- }
1315
-
1316
- // 💻 SCENARIO B: LOCAL FILESYSTEM (Development)
1317
- console.log(`💻 Saving ${slug} to Local Filesystem...`);
1318
- const res = await fetch('/api/save-to-file', {
1319
- method: 'POST',
1320
- headers: { 'Content-Type': 'application/json' },
1321
- body: JSON.stringify({ projectState: state, slug }),
1322
- });
1323
-
1324
- const body = (await res.json().catch(() => ({}))) as { error?: string };
1325
- if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
1326
- },
1327
- },
1328
- assets: {
1329
- assetsBaseUrl: '/assets',
1330
- assetsManifest,
1331
- async onAssetUpload(file: File): Promise<string> {
1332
- // Note: Asset upload in Cloud Mode requires the R2 Bridge (Next Step in Roadmap)
1333
- // For now, this works in Local Mode.
1334
- if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
1335
- if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
1336
-
1337
- const base64 = await new Promise<string>((resolve, reject) => {
1338
- const reader = new FileReader();
1339
- reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
1340
- reader.onerror = () => reject(reader.error);
1341
- reader.readAsDataURL(file);
1342
- });
1343
-
1344
- const res = await fetch('/api/upload-asset', {
1345
- method: 'POST',
1346
- headers: { 'Content-Type': 'application/json' },
1347
- body: JSON.stringify({ filename: file.name, mimeType: file.type || undefined, data: base64 }),
1348
- });
1349
-
1350
- const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
1351
- if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
1352
- if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
1353
- return body.url;
1354
- },
1355
- },
1356
- };
1357
-
1358
- return <JsonPagesEngine config={config} />;
1359
- }
1360
-
1361
- export default App;
1362
-
1363
- END_OF_FILE_CONTENT
1364
- echo "Creating src/_main.tsx..."
1365
- cat << 'END_OF_FILE_CONTENT' > "src/_main.tsx"
1366
- import '@/types'; // TBP: load type augmentation from capsule-driven types
1367
- import React from 'react';
1368
- import ReactDOM from 'react-dom/client';
1369
- import App from './App';
1370
- // ... resto del file
1371
-
1372
- ReactDOM.createRoot(document.getElementById('root')!).render(
1373
- <React.StrictMode>
1374
- <App />
1375
- </React.StrictMode>
1376
- );
1377
-
1378
-
1379
-
1380
-
1381
1147
  END_OF_FILE_CONTENT
1382
1148
  mkdir -p "src/components"
1383
1149
  echo "Creating src/components/NotFound.tsx..."
@@ -10909,24 +10675,6 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
10909
10675
 
10910
10676
 
10911
10677
 
10912
- END_OF_FILE_CONTENT
10913
- echo "Creating src/main_.tsx..."
10914
- cat << 'END_OF_FILE_CONTENT' > "src/main_.tsx"
10915
- import '@/types'; // TBP: load type augmentation from capsule-driven types
10916
- import React from 'react';
10917
- import ReactDOM from 'react-dom/client';
10918
- import App from './App';
10919
- // ... resto del file
10920
-
10921
- ReactDOM.createRoot(document.getElementById('root')!).render(
10922
- <React.StrictMode>
10923
- <App />
10924
- </React.StrictMode>
10925
- );
10926
-
10927
-
10928
-
10929
-
10930
10678
  END_OF_FILE_CONTENT
10931
10679
  # SKIP: src/registry-types.ts is binary and cannot be embedded as text.
10932
10680
  mkdir -p "src/server"
@@ -596,7 +596,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
596
596
  "@tiptap/extension-link": "^2.11.5",
597
597
  "@tiptap/react": "^2.11.5",
598
598
  "@tiptap/starter-kit": "^2.11.5",
599
- "@olonjs/core": "^1.0.70",
599
+ "@olonjs/core": "^1.0.72",
600
600
  "clsx": "^2.1.1",
601
601
  "lucide-react": "^0.474.0",
602
602
  "react": "^19.0.0",
@@ -941,6 +941,7 @@ import themeData from '@/data/config/theme.json';
941
941
  import menuData from '@/data/config/menu.json';
942
942
  import { getFilePages } from '@/lib/getFilePages';
943
943
  import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
944
+ import { Skeleton } from '@/components/ui/skeleton';
944
945
 
945
946
  import tenantCss from './index.css?inline';
946
947
 
@@ -1194,6 +1195,16 @@ function cloudFingerprint(apiBase: string, apiKey: string): string {
1194
1195
  return `${normalizeApiBase(apiBase)}::${apiKey.slice(-8)}`;
1195
1196
  }
1196
1197
 
1198
+ function normalizeSlugForCache(slug: string): string {
1199
+ return (
1200
+ slug
1201
+ .trim()
1202
+ .toLowerCase()
1203
+ .replace(/[^a-z0-9/_-]/g, '-')
1204
+ .replace(/^\/+|\/+$/g, '') || 'home'
1205
+ );
1206
+ }
1207
+
1197
1208
  function readCachedCloudContent(fingerprint: string): CachedCloudContent | null {
1198
1209
  try {
1199
1210
  const raw = localStorage.getItem(CLOUD_CACHE_KEY);
@@ -1248,7 +1259,6 @@ function App() {
1248
1259
  const activeCloudSaveController = useRef<AbortController | null>(null);
1249
1260
  const contentLoadInFlight = useRef<Promise<void> | null>(null);
1250
1261
  const pendingCloudSave = useRef<{ state: ProjectState; slug: string } | null>(null);
1251
- const warmBootFromCloudCache = useRef(false);
1252
1262
  const cloudApiCandidates = useMemo(
1253
1263
  () => (isCloudMode && CLOUD_API_URL ? buildApiCandidates(CLOUD_API_URL) : []),
1254
1264
  [isCloudMode, CLOUD_API_URL]
@@ -1288,26 +1298,16 @@ function App() {
1288
1298
  const primaryApiBase = cloudApiCandidates[0] ?? normalizeApiBase(CLOUD_API_URL);
1289
1299
  const fingerprint = cloudFingerprint(primaryApiBase, CLOUD_API_KEY);
1290
1300
  const cached = readCachedCloudContent(fingerprint);
1301
+ const cachedPages = cached ? toPagesRecord(cached.pages) : null;
1302
+ const cachedSite = cached ? coerceSiteConfig(cached.siteConfig) : null;
1303
+ const hasCachedFallback = Boolean((cachedPages && Object.keys(cachedPages).length > 0) || cachedSite);
1291
1304
  if (cached) {
1292
- const cachedPages = toPagesRecord(cached.pages);
1293
- const cachedSite = coerceSiteConfig(cached.siteConfig);
1294
- if (cachedPages && Object.keys(cachedPages).length > 0) {
1295
- setPages(cachedPages);
1296
- }
1297
- if (cachedSite) {
1298
- setSiteConfig(cachedSite);
1299
- }
1300
- setContentMode('cloud');
1301
- setContentFallback(null);
1302
- warmBootFromCloudCache.current = true;
1303
- setShowTopProgress(false);
1304
- setHasInitialCloudResolved(true);
1305
1305
  logBootstrapEvent('boot.cloud.cache_hit', { ageMs: Date.now() - cached.savedAt });
1306
- } else {
1307
- warmBootFromCloudCache.current = false;
1308
- setShowTopProgress(true);
1309
- setHasInitialCloudResolved(false);
1310
1306
  }
1307
+ setContentMode('cloud');
1308
+ setContentFallback(null);
1309
+ setShowTopProgress(true);
1310
+ setHasInitialCloudResolved(false);
1311
1311
  logBootstrapEvent('boot.start', { mode: 'cloud', apiCandidates: cloudApiCandidates.length });
1312
1312
 
1313
1313
  const loadCloudContent = async () => {
@@ -1320,6 +1320,7 @@ function App() {
1320
1320
  try {
1321
1321
  const res = await fetch(`${apiBase}/content`, {
1322
1322
  method: 'GET',
1323
+ cache: 'no-store',
1323
1324
  headers: {
1324
1325
  Authorization: `Bearer ${CLOUD_API_KEY}`,
1325
1326
  },
@@ -1422,7 +1423,13 @@ function App() {
1422
1423
  } catch (error: unknown) {
1423
1424
  if (controller.signal.aborted) return;
1424
1425
  const failure = toCloudLoadFailure(error);
1425
- if (warmBootFromCloudCache.current) {
1426
+ if (hasCachedFallback) {
1427
+ if (cachedPages && Object.keys(cachedPages).length > 0) {
1428
+ setPages(cachedPages);
1429
+ }
1430
+ if (cachedSite) {
1431
+ setSiteConfig(cachedSite);
1432
+ }
1426
1433
  setContentMode('cloud');
1427
1434
  setContentFallback({
1428
1435
  reasonCode: 'CLOUD_REFRESH_FAILED',
@@ -1589,14 +1596,26 @@ function App() {
1589
1596
  },
1590
1597
  body: JSON.stringify({
1591
1598
  slug,
1592
- type: 'page',
1593
- data: state.page,
1599
+ page: state.page,
1600
+ siteConfig: state.site,
1594
1601
  }),
1595
1602
  });
1596
1603
  const body = (await res.json().catch(() => ({}))) as { error?: string; code?: string };
1597
1604
  if (!res.ok) {
1598
1605
  throw new Error(body.error || body.code || `Hot save failed: ${res.status}`);
1599
1606
  }
1607
+ const keyFingerprint = cloudFingerprint(apiBase, CLOUD_API_KEY);
1608
+ const normalizedSlug = normalizeSlugForCache(slug);
1609
+ const existing = readCachedCloudContent(keyFingerprint);
1610
+ writeCachedCloudContent({
1611
+ keyFingerprint,
1612
+ savedAt: Date.now(),
1613
+ siteConfig: state.site ?? null,
1614
+ pages: {
1615
+ ...(existing?.pages ?? {}),
1616
+ [normalizedSlug]: state.page,
1617
+ },
1618
+ });
1600
1619
  },
1601
1620
  showLegacySave: !isCloudMode,
1602
1621
  showHotSave: isCloudMode,
@@ -1667,6 +1686,26 @@ function App() {
1667
1686
  </div>
1668
1687
  </>
1669
1688
  ) : null}
1689
+ {isCloudMode && !hasInitialCloudResolved ? (
1690
+ <div className="fixed inset-0 z-[1290] bg-background/80 backdrop-blur-sm">
1691
+ <div className="mx-auto w-full max-w-[1600px] p-6">
1692
+ <div className="grid gap-4 lg:grid-cols-[1fr_420px]">
1693
+ <div className="space-y-4">
1694
+ <Skeleton className="h-10 w-64" />
1695
+ <Skeleton className="h-[220px] w-full rounded-xl" />
1696
+ <Skeleton className="h-[220px] w-full rounded-xl" />
1697
+ </div>
1698
+ <div className="space-y-3 rounded-xl border border-border/50 bg-card/60 p-4">
1699
+ <Skeleton className="h-8 w-32" />
1700
+ <Skeleton className="h-5 w-full" />
1701
+ <Skeleton className="h-5 w-5/6" />
1702
+ <Skeleton className="h-5 w-4/6" />
1703
+ <Skeleton className="h-24 w-full rounded-lg" />
1704
+ </div>
1705
+ </div>
1706
+ </div>
1707
+ </div>
1708
+ ) : null}
1670
1709
  {shouldRenderEngine ? <JsonPagesEngine config={config} /> : null}
1671
1710
  {isCloudMode && (contentMode === 'error' || contentFallback?.reasonCode === 'CLOUD_REFRESH_FAILED') ? (
1672
1711
  <div
@@ -1741,102 +1780,6 @@ function App() {
1741
1780
  export default App;
1742
1781
 
1743
1782
 
1744
- END_OF_FILE_CONTENT
1745
- # SKIP: src/App.tsx:Zone.Identifier is binary and cannot be embedded as text.
1746
- echo "Creating src/App_.tsx..."
1747
- cat << 'END_OF_FILE_CONTENT' > "src/App_.tsx"
1748
- import { useState, useEffect } from 'react';
1749
- import { JsonPagesEngine } from '@jsonpages/core';
1750
- import type { LibraryImageEntry } from '@jsonpages/core';
1751
- import { ComponentRegistry } from '@/lib/ComponentRegistry';
1752
- import { SECTION_SCHEMAS } from '@/lib/schemas';
1753
- import { addSectionConfig } from '@/lib/addSectionConfig';
1754
- import { getHydratedData } from '@/lib/draftStorage';
1755
- import type { JsonPagesConfig, ProjectState } from '@jsonpages/core';
1756
- import type { SiteConfig, ThemeConfig, MenuConfig } from '@/types';
1757
-
1758
- import siteData from '@/data/config/site.json';
1759
- import themeData from '@/data/config/theme.json';
1760
- import menuData from '@/data/config/menu.json';
1761
- import { getFilePages } from '@/lib/getFilePages';
1762
-
1763
- import fontsCss from './fonts.css?inline';
1764
- import tenantCss from './index.css?inline';
1765
-
1766
- const themeConfig = themeData as unknown as ThemeConfig;
1767
- const menuConfig = menuData as unknown as MenuConfig;
1768
- const TENANT_ID = 'alpha';
1769
-
1770
- const filePages = getFilePages();
1771
- const fileSiteConfig = siteData as unknown as SiteConfig;
1772
- const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
1773
-
1774
- function getInitialData() {
1775
- return getHydratedData(TENANT_ID, filePages, fileSiteConfig);
1776
- }
1777
-
1778
- function App() {
1779
- const [{ pages, siteConfig }] = useState(getInitialData);
1780
- const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
1781
-
1782
- useEffect(() => {
1783
- fetch('/api/list-assets')
1784
- .then((r) => (r.ok ? r.json() : []))
1785
- .then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
1786
- .catch(() => setAssetsManifest([]));
1787
- }, []);
1788
-
1789
- const config: JsonPagesConfig = {
1790
- tenantId: TENANT_ID,
1791
- registry: ComponentRegistry as JsonPagesConfig['registry'],
1792
- schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
1793
- pages,
1794
- siteConfig,
1795
- themeConfig,
1796
- menuConfig,
1797
- themeCss: { tenant: fontsCss + '\n' + tenantCss },
1798
- addSection: addSectionConfig,
1799
- persistence: {
1800
- async saveToFile(state: ProjectState, slug: string): Promise<void> {
1801
- const res = await fetch('/api/save-to-file', {
1802
- method: 'POST',
1803
- headers: { 'Content-Type': 'application/json' },
1804
- body: JSON.stringify({ projectState: state, slug }),
1805
- });
1806
- const body = (await res.json().catch(() => ({}))) as { error?: string };
1807
- if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
1808
- },
1809
- },
1810
- assets: {
1811
- assetsBaseUrl: '/assets',
1812
- assetsManifest,
1813
- async onAssetUpload(file: File): Promise<string> {
1814
- if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
1815
- if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
1816
- const base64 = await new Promise<string>((resolve, reject) => {
1817
- const reader = new FileReader();
1818
- reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
1819
- reader.onerror = () => reject(reader.error);
1820
- reader.readAsDataURL(file);
1821
- });
1822
- const res = await fetch('/api/upload-asset', {
1823
- method: 'POST',
1824
- headers: { 'Content-Type': 'application/json' },
1825
- body: JSON.stringify({ filename: file.name, mimeType: file.type || undefined, data: base64 }),
1826
- });
1827
- const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
1828
- if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
1829
- if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
1830
- return body.url;
1831
- },
1832
- },
1833
- };
1834
-
1835
- return <JsonPagesEngine config={config} />;
1836
- }
1837
-
1838
- export default App;
1839
-
1840
1783
  END_OF_FILE_CONTENT
1841
1784
  mkdir -p "src/components"
1842
1785
  echo "Creating src/components/NotFound.tsx..."
@@ -7186,6 +7129,26 @@ export { Separator }
7186
7129
 
7187
7130
 
7188
7131
 
7132
+ END_OF_FILE_CONTENT
7133
+ echo "Creating src/components/ui/skeleton.tsx..."
7134
+ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/skeleton.tsx"
7135
+ import { cn } from '@/lib/utils';
7136
+ import type { HTMLAttributes } from 'react';
7137
+
7138
+ function Skeleton({
7139
+ className,
7140
+ ...props
7141
+ }: HTMLAttributes<HTMLDivElement>) {
7142
+ return (
7143
+ <div
7144
+ className={cn('animate-pulse rounded-md bg-muted', className)}
7145
+ {...props}
7146
+ />
7147
+ );
7148
+ }
7149
+
7150
+ export { Skeleton };
7151
+
7189
7152
  END_OF_FILE_CONTENT
7190
7153
  echo "Creating src/components/ui/textarea.tsx..."
7191
7154
  cat << 'END_OF_FILE_CONTENT' > "src/components/ui/textarea.tsx"
@@ -9335,24 +9298,6 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
9335
9298
 
9336
9299
 
9337
9300
 
9338
- END_OF_FILE_CONTENT
9339
- echo "Creating src/main_.tsx..."
9340
- cat << 'END_OF_FILE_CONTENT' > "src/main_.tsx"
9341
- import '@/types'; // TBP: load type augmentation from capsule-driven types
9342
- import React from 'react';
9343
- import ReactDOM from 'react-dom/client';
9344
- import App from './App';
9345
- // ... resto del file
9346
-
9347
- ReactDOM.createRoot(document.getElementById('root')!).render(
9348
- <React.StrictMode>
9349
- <App />
9350
- </React.StrictMode>
9351
- );
9352
-
9353
-
9354
-
9355
-
9356
9301
  END_OF_FILE_CONTENT
9357
9302
  # SKIP: src/registry-types.ts is binary and cannot be embedded as text.
9358
9303
  mkdir -p "src/server"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olonjs/cli",
3
- "version": "3.0.82",
3
+ "version": "3.0.84",
4
4
  "description": "The Sovereign CLI Engine for OlonJS.",
5
5
  "type": "module",
6
6
  "bin": {