@olonjs/cli 3.0.128 → 3.0.130
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/assets/src_tenant_alpha.sh +555 -376
- package/assets/templates/alpha/src_tenant.sh +555 -376
- package/package.json +1 -1
|
@@ -1597,6 +1597,37 @@ If user says "use dark theme" or "change my theme":
|
|
|
1597
1597
|
4. Write the updated JSON back to the file
|
|
1598
1598
|
5. Inform the user the setting has been changed and whether a reload is needed
|
|
1599
1599
|
|
|
1600
|
+
END_OF_FILE_CONTENT
|
|
1601
|
+
echo "Creating components.json..."
|
|
1602
|
+
cat << 'END_OF_FILE_CONTENT' > "components.json"
|
|
1603
|
+
{
|
|
1604
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
1605
|
+
"style": "radix-nova",
|
|
1606
|
+
"rsc": false,
|
|
1607
|
+
"tsx": true,
|
|
1608
|
+
"tailwind": {
|
|
1609
|
+
"config": "",
|
|
1610
|
+
"css": "src/index.css",
|
|
1611
|
+
"baseColor": "zinc",
|
|
1612
|
+
"cssVariables": true,
|
|
1613
|
+
"prefix": ""
|
|
1614
|
+
},
|
|
1615
|
+
"iconLibrary": "lucide",
|
|
1616
|
+
"rtl": false,
|
|
1617
|
+
"aliases": {
|
|
1618
|
+
"components": "@/components",
|
|
1619
|
+
"utils": "@/lib/utils",
|
|
1620
|
+
"ui": "@/components/ui",
|
|
1621
|
+
"lib": "@/lib",
|
|
1622
|
+
"hooks": "@/hooks"
|
|
1623
|
+
},
|
|
1624
|
+
"menuColor": "default",
|
|
1625
|
+
"menuAccent": "bold",
|
|
1626
|
+
"registries": {}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
|
|
1600
1631
|
END_OF_FILE_CONTENT
|
|
1601
1632
|
echo "Creating index.html..."
|
|
1602
1633
|
cat << 'END_OF_FILE_CONTENT' > "index.html"
|
|
@@ -1632,7 +1663,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1632
1663
|
"verify:webmcp": "node scripts/webmcp-feature-check.mjs",
|
|
1633
1664
|
"prebuild": "node scripts/sync-pages-to-public.mjs && node scripts/generate-llms-txt.mjs",
|
|
1634
1665
|
"build": "tsc && vite build",
|
|
1635
|
-
"dist": "bash ./src2Code.sh --template alpha src .cursor vercel.json index.html tsconfig.json tsconfig.node.json vite.config.ts scripts
|
|
1666
|
+
"dist": "bash ./src2Code.sh --template alpha src .cursor vercel.json components.json index.html tsconfig.json package.json tsconfig.node.json vite.config.ts scripts ",
|
|
1636
1667
|
"preview": "vite preview",
|
|
1637
1668
|
"bake:email": "tsx scripts/bake-email.tsx",
|
|
1638
1669
|
"bakemail": "npm run bake:email --",
|
|
@@ -1643,7 +1674,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1643
1674
|
"@tiptap/extension-link": "^2.11.5",
|
|
1644
1675
|
"@tiptap/react": "^2.11.5",
|
|
1645
1676
|
"@tiptap/starter-kit": "^2.11.5",
|
|
1646
|
-
"@olonjs/core": "^1.0.
|
|
1677
|
+
"@olonjs/core": "^1.0.121",
|
|
1647
1678
|
"class-variance-authority": "^0.7.1",
|
|
1648
1679
|
"clsx": "^2.1.1",
|
|
1649
1680
|
"lucide-react": "^0.474.0",
|
|
@@ -1672,7 +1703,6 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
|
|
|
1672
1703
|
}
|
|
1673
1704
|
|
|
1674
1705
|
END_OF_FILE_CONTENT
|
|
1675
|
-
# SKIP: public/assets/images/plug-graded-square.jpg is binary and cannot be embedded as text.
|
|
1676
1706
|
mkdir -p "scripts"
|
|
1677
1707
|
echo "Creating scripts/bake-email.tsx..."
|
|
1678
1708
|
cat << 'END_OF_FILE_CONTENT' > "scripts/bake-email.tsx"
|
|
@@ -2557,13 +2587,13 @@ import { JsonPagesEngine } from '@olonjs/core';
|
|
|
2557
2587
|
import type { JsonPagesConfig, LibraryImageEntry, ProjectState } from '@olonjs/core';
|
|
2558
2588
|
import { normalizeBasePath, withBasePath } from '@olonjs/core';
|
|
2559
2589
|
import { ComponentRegistry } from '@/lib/ComponentRegistry';
|
|
2560
|
-
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
2590
|
+
import { SECTION_SCHEMAS, SECTION_SUBMISSION_SCHEMAS } from '@/lib/schemas';
|
|
2561
2591
|
import { addSectionConfig } from '@/lib/addSectionConfig';
|
|
2562
2592
|
import { getHydratedData } from '@/lib/draftStorage';
|
|
2563
2593
|
import type { SiteConfig, ThemeConfig, MenuConfig, PageConfig } from '@/types';
|
|
2564
|
-
import type { DeployPhase, StepId } from '
|
|
2565
|
-
import { DEPLOY_STEPS } from '
|
|
2566
|
-
import { startCloudSaveStream } from '
|
|
2594
|
+
import type { DeployPhase, StepId } from '@olonjs/core';
|
|
2595
|
+
import { DEPLOY_STEPS } from '@olonjs/core';
|
|
2596
|
+
import { startCloudSaveStream } from '@olonjs/core';
|
|
2567
2597
|
import siteData from '@/data/config/site.json';
|
|
2568
2598
|
import themeData from '@/data/config/theme.json';
|
|
2569
2599
|
import menuData from '@/data/config/menu.json';
|
|
@@ -2572,6 +2602,9 @@ import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
|
|
|
2572
2602
|
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
2573
2603
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
2574
2604
|
import { ThemeProvider } from '@/components/ThemeProvider';
|
|
2605
|
+
import { useOlonForms } from '@/lib/useOlonForms';
|
|
2606
|
+
import { OlonFormsContext } from '@olonjs/core';
|
|
2607
|
+
import { iconMap } from '@/lib/IconResolver';
|
|
2575
2608
|
|
|
2576
2609
|
import tenantCss from './index.css?inline';
|
|
2577
2610
|
|
|
@@ -2718,7 +2751,6 @@ function coerceSiteConfig(value: unknown): SiteConfig | null {
|
|
|
2718
2751
|
}
|
|
2719
2752
|
if (!isObjectRecord(input)) return null;
|
|
2720
2753
|
if (!isObjectRecord(input.identity)) return null;
|
|
2721
|
-
if (!Array.isArray(input.pages)) return null;
|
|
2722
2754
|
|
|
2723
2755
|
return input as unknown as SiteConfig;
|
|
2724
2756
|
}
|
|
@@ -2966,6 +2998,7 @@ function setTenantPreviewReady(ready: boolean): void {
|
|
|
2966
2998
|
}
|
|
2967
2999
|
|
|
2968
3000
|
function App() {
|
|
3001
|
+
const { states: formStates } = useOlonForms();
|
|
2969
3002
|
const isCloudMode = Boolean(CLOUD_API_URL && CLOUD_API_KEY);
|
|
2970
3003
|
const isSave2RepoMode = isCloudMode && SAVE2REPO_ENABLED;
|
|
2971
3004
|
const isHotSaveMode = isCloudMode && !isSave2RepoMode;
|
|
@@ -3408,11 +3441,13 @@ function App() {
|
|
|
3408
3441
|
basePath: APP_BASE_PATH,
|
|
3409
3442
|
registry: ComponentRegistry as JsonPagesConfig['registry'],
|
|
3410
3443
|
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
3444
|
+
submissionSchemas: SECTION_SUBMISSION_SCHEMAS as unknown as JsonPagesConfig['submissionSchemas'],
|
|
3411
3445
|
pages,
|
|
3412
3446
|
siteConfig,
|
|
3413
3447
|
themeConfig,
|
|
3414
3448
|
menuConfig,
|
|
3415
3449
|
refDocuments,
|
|
3450
|
+
iconRegistry: iconMap,
|
|
3416
3451
|
themeCss: { tenant: resolvedTenantCss },
|
|
3417
3452
|
addSection: addSectionConfig,
|
|
3418
3453
|
webmcp: {
|
|
@@ -3575,6 +3610,7 @@ function App() {
|
|
|
3575
3610
|
|
|
3576
3611
|
return (
|
|
3577
3612
|
<ThemeProvider>
|
|
3613
|
+
<OlonFormsContext.Provider value={formStates}>
|
|
3578
3614
|
<>
|
|
3579
3615
|
{isCloudMode && showTopProgress ? (
|
|
3580
3616
|
<>
|
|
@@ -3696,6 +3732,7 @@ function App() {
|
|
|
3696
3732
|
onRetry={retryCloudSave}
|
|
3697
3733
|
/>
|
|
3698
3734
|
</>
|
|
3735
|
+
</OlonFormsContext.Provider>
|
|
3699
3736
|
</ThemeProvider>
|
|
3700
3737
|
);
|
|
3701
3738
|
}
|
|
@@ -3706,90 +3743,89 @@ END_OF_FILE_CONTENT
|
|
|
3706
3743
|
mkdir -p "src/components"
|
|
3707
3744
|
echo "Creating src/components/ThemeProvider.tsx..."
|
|
3708
3745
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ThemeProvider.tsx"
|
|
3709
|
-
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'
|
|
3710
|
-
|
|
3711
|
-
type Theme = 'dark' | 'light'
|
|
3712
|
-
|
|
3713
|
-
interface ThemeContextValue {
|
|
3714
|
-
theme: Theme
|
|
3715
|
-
toggleTheme: () => void
|
|
3716
|
-
setTheme: (t: Theme) => void
|
|
3717
|
-
}
|
|
3718
|
-
|
|
3719
|
-
const ThemeContext = createContext<ThemeContextValue>({
|
|
3720
|
-
theme: 'dark',
|
|
3721
|
-
toggleTheme: () => {},
|
|
3722
|
-
setTheme: () => {},
|
|
3723
|
-
})
|
|
3724
|
-
|
|
3725
|
-
const STORAGE_KEY = 'olon:theme'
|
|
3726
|
-
|
|
3727
|
-
function isTheme(value: unknown): value is Theme {
|
|
3728
|
-
return value === 'dark' || value === 'light'
|
|
3729
|
-
}
|
|
3730
|
-
|
|
3731
|
-
function resolveInitialTheme(): Theme {
|
|
3732
|
-
if (typeof window === 'undefined') return 'dark'
|
|
3733
|
-
|
|
3734
|
-
const fromDom = document.documentElement.getAttribute('data-theme')
|
|
3735
|
-
if (isTheme(fromDom)) return fromDom
|
|
3736
|
-
|
|
3737
|
-
const fromStorage = window.localStorage.getItem(STORAGE_KEY)
|
|
3738
|
-
if (isTheme(fromStorage)) return fromStorage
|
|
3739
|
-
|
|
3740
|
-
const prefersLight = window.matchMedia?.('(prefers-color-scheme: light)').matches
|
|
3741
|
-
return prefersLight ? 'light' : 'dark'
|
|
3742
|
-
}
|
|
3743
|
-
|
|
3744
|
-
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
3745
|
-
const [theme, setThemeState] = useState<Theme>(resolveInitialTheme)
|
|
3746
|
-
|
|
3747
|
-
useEffect(() => {
|
|
3748
|
-
document.documentElement.setAttribute('data-theme', theme)
|
|
3749
|
-
window.localStorage.setItem(STORAGE_KEY, theme)
|
|
3750
|
-
}, [theme])
|
|
3751
|
-
|
|
3752
|
-
function setTheme(t: Theme) {
|
|
3753
|
-
setThemeState(t)
|
|
3754
|
-
}
|
|
3755
|
-
|
|
3756
|
-
function toggleTheme() {
|
|
3757
|
-
setThemeState((prev) => (prev === 'dark' ? 'light' : 'dark'))
|
|
3758
|
-
}
|
|
3759
|
-
|
|
3760
|
-
const value = useMemo(() => ({ theme, toggleTheme, setTheme }), [theme])
|
|
3761
|
-
|
|
3762
|
-
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
|
3763
|
-
}
|
|
3764
|
-
|
|
3765
|
-
export function useTheme() {
|
|
3766
|
-
return useContext(ThemeContext)
|
|
3767
|
-
}
|
|
3768
|
-
|
|
3769
|
-
END_OF_FILE_CONTENT
|
|
3770
|
-
mkdir -p "src/components/empty-tenant"
|
|
3771
|
-
echo "Creating src/components/empty-tenant/View.tsx..."
|
|
3772
|
-
cat << 'END_OF_FILE_CONTENT' > "src/components/empty-tenant/View.tsx"
|
|
3773
|
-
import type { EmptyTenantData } from './types';
|
|
3746
|
+
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react'
|
|
3774
3747
|
|
|
3775
|
-
type
|
|
3776
|
-
data?: EmptyTenantData;
|
|
3777
|
-
};
|
|
3748
|
+
type Theme = 'dark' | 'light'
|
|
3778
3749
|
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3750
|
+
interface ThemeContextValue {
|
|
3751
|
+
theme: Theme
|
|
3752
|
+
toggleTheme: () => void
|
|
3753
|
+
setTheme: (t: Theme) => void
|
|
3754
|
+
}
|
|
3782
3755
|
|
|
3756
|
+
const ThemeContext = createContext<ThemeContextValue>({
|
|
3757
|
+
theme: 'dark',
|
|
3758
|
+
toggleTheme: () => {},
|
|
3759
|
+
setTheme: () => {},
|
|
3760
|
+
})
|
|
3783
3761
|
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
)
|
|
3762
|
+
const STORAGE_KEY = 'olon:theme'
|
|
3763
|
+
|
|
3764
|
+
function isTheme(value: unknown): value is Theme {
|
|
3765
|
+
return value === 'dark' || value === 'light'
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
function resolveInitialTheme(): Theme {
|
|
3769
|
+
if (typeof window === 'undefined') return 'dark'
|
|
3770
|
+
|
|
3771
|
+
const fromDom = document.documentElement.getAttribute('data-theme')
|
|
3772
|
+
if (isTheme(fromDom)) return fromDom
|
|
3773
|
+
|
|
3774
|
+
const fromStorage = window.localStorage.getItem(STORAGE_KEY)
|
|
3775
|
+
if (isTheme(fromStorage)) return fromStorage
|
|
3776
|
+
|
|
3777
|
+
const prefersLight = window.matchMedia?.('(prefers-color-scheme: light)').matches
|
|
3778
|
+
return prefersLight ? 'light' : 'dark'
|
|
3792
3779
|
}
|
|
3780
|
+
|
|
3781
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
3782
|
+
const [theme, setThemeState] = useState<Theme>(resolveInitialTheme)
|
|
3783
|
+
|
|
3784
|
+
useEffect(() => {
|
|
3785
|
+
document.documentElement.setAttribute('data-theme', theme)
|
|
3786
|
+
window.localStorage.setItem(STORAGE_KEY, theme)
|
|
3787
|
+
}, [theme])
|
|
3788
|
+
|
|
3789
|
+
function setTheme(t: Theme) {
|
|
3790
|
+
setThemeState(t)
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
function toggleTheme() {
|
|
3794
|
+
setThemeState((prev) => (prev === 'dark' ? 'light' : 'dark'))
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3797
|
+
const value = useMemo(() => ({ theme, toggleTheme, setTheme }), [theme])
|
|
3798
|
+
|
|
3799
|
+
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
export function useTheme() {
|
|
3803
|
+
return useContext(ThemeContext)
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
END_OF_FILE_CONTENT
|
|
3807
|
+
mkdir -p "src/components/empty-tenant"
|
|
3808
|
+
echo "Creating src/components/empty-tenant/View.tsx..."
|
|
3809
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/empty-tenant/View.tsx"
|
|
3810
|
+
import type { EmptyTenantData } from './types';
|
|
3811
|
+
|
|
3812
|
+
type EmptyTenantViewProps = {
|
|
3813
|
+
data?: EmptyTenantData;
|
|
3814
|
+
};
|
|
3815
|
+
|
|
3816
|
+
export function EmptyTenantView({ data }: EmptyTenantViewProps) {
|
|
3817
|
+
const title = data?.title?.trim() || 'Your tenant is empty.';
|
|
3818
|
+
const description = data?.description?.trim() || 'Create your first page to start building your site.';
|
|
3819
|
+
|
|
3820
|
+
return (
|
|
3821
|
+
<main className="min-h-screen flex items-center justify-center bg-background text-foreground px-6">
|
|
3822
|
+
<section className="w-full max-w-xl rounded-xl border border-border bg-card p-8 shadow-sm">
|
|
3823
|
+
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
|
|
3824
|
+
<p className="mt-3 text-sm text-muted-foreground">{description}</p>
|
|
3825
|
+
</section>
|
|
3826
|
+
</main>
|
|
3827
|
+
);
|
|
3828
|
+
}
|
|
3793
3829
|
|
|
3794
3830
|
END_OF_FILE_CONTENT
|
|
3795
3831
|
echo "Creating src/components/empty-tenant/index.ts..."
|
|
@@ -3802,12 +3838,11 @@ END_OF_FILE_CONTENT
|
|
|
3802
3838
|
echo "Creating src/components/empty-tenant/schema.ts..."
|
|
3803
3839
|
cat << 'END_OF_FILE_CONTENT' > "src/components/empty-tenant/schema.ts"
|
|
3804
3840
|
import { z } from 'zod';
|
|
3805
|
-
import { BaseSectionData } from '
|
|
3841
|
+
import { BaseSectionData } from '@olonjs/core';
|
|
3806
3842
|
|
|
3807
3843
|
export const EmptyTenantSchema = BaseSectionData.extend({
|
|
3808
3844
|
title: z.string().optional().describe('ui:text'),
|
|
3809
3845
|
description: z.string().optional().describe('ui:textarea'),
|
|
3810
|
-
|
|
3811
3846
|
});
|
|
3812
3847
|
|
|
3813
3848
|
export const EmptyTenantSettingsSchema = z.object({});
|
|
@@ -3821,11 +3856,215 @@ import { EmptyTenantSchema, EmptyTenantSettingsSchema } from './schema';
|
|
|
3821
3856
|
export type EmptyTenantData = z.infer<typeof EmptyTenantSchema>;
|
|
3822
3857
|
export type EmptyTenantSettings = z.infer<typeof EmptyTenantSettingsSchema>;
|
|
3823
3858
|
|
|
3859
|
+
END_OF_FILE_CONTENT
|
|
3860
|
+
mkdir -p "src/components/form-demo"
|
|
3861
|
+
echo "Creating src/components/form-demo/View.tsx..."
|
|
3862
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/View.tsx"
|
|
3863
|
+
import { Icon } from '@/lib/IconResolver';
|
|
3864
|
+
import { useFormState } from '@olonjs/core';
|
|
3865
|
+
import type { FormDemoData } from './types';
|
|
3866
|
+
|
|
3867
|
+
type FormDemoViewProps = {
|
|
3868
|
+
data: FormDemoData;
|
|
3869
|
+
};
|
|
3870
|
+
|
|
3871
|
+
const missingEnv =
|
|
3872
|
+
!import.meta.env.VITE_JSONPAGES_CLOUD_URL &&
|
|
3873
|
+
!import.meta.env.VITE_OLONJS_CLOUD_URL;
|
|
3874
|
+
|
|
3875
|
+
function SetupGuide({ recipientEmail }: { recipientEmail?: string }) {
|
|
3876
|
+
const steps = [
|
|
3877
|
+
{
|
|
3878
|
+
done: !!recipientEmail,
|
|
3879
|
+
label: 'recipientEmail nel JSON della sezione',
|
|
3880
|
+
code: '"recipientEmail": "tu@esempio.it"',
|
|
3881
|
+
},
|
|
3882
|
+
{
|
|
3883
|
+
done: !missingEnv,
|
|
3884
|
+
label: 'VITE_JSONPAGES_CLOUD_URL nel file .env',
|
|
3885
|
+
code: 'VITE_JSONPAGES_CLOUD_URL=https://cloud.olonjs.io',
|
|
3886
|
+
},
|
|
3887
|
+
{
|
|
3888
|
+
done: !!import.meta.env.VITE_JSONPAGES_API_KEY || !!import.meta.env.VITE_OLONJS_API_KEY,
|
|
3889
|
+
label: 'VITE_JSONPAGES_API_KEY nel file .env',
|
|
3890
|
+
code: 'VITE_JSONPAGES_API_KEY=sk-...',
|
|
3891
|
+
},
|
|
3892
|
+
];
|
|
3893
|
+
|
|
3894
|
+
const allDone = steps.every((s) => s.done);
|
|
3895
|
+
if (allDone) return null;
|
|
3896
|
+
|
|
3897
|
+
return (
|
|
3898
|
+
<div className="rounded-lg border border-border bg-muted/40 p-4 space-y-3 text-sm">
|
|
3899
|
+
<p className="font-medium text-foreground">Quasi pronto — completa questi passaggi</p>
|
|
3900
|
+
<ol className="space-y-2">
|
|
3901
|
+
{steps.map((step, i) => (
|
|
3902
|
+
<li key={i} className="flex items-start gap-2">
|
|
3903
|
+
<span className={step.done ? 'text-green-500' : 'text-muted-foreground'}>
|
|
3904
|
+
{step.done ? '✓' : `${i + 1}.`}
|
|
3905
|
+
</span>
|
|
3906
|
+
<span className={step.done ? 'text-muted-foreground line-through' : 'text-foreground'}>
|
|
3907
|
+
{step.label}
|
|
3908
|
+
{!step.done && (
|
|
3909
|
+
<code className="block mt-0.5 text-xs bg-background rounded px-1.5 py-0.5 font-mono text-muted-foreground border border-border">
|
|
3910
|
+
{step.code}
|
|
3911
|
+
</code>
|
|
3912
|
+
)}
|
|
3913
|
+
</span>
|
|
3914
|
+
</li>
|
|
3915
|
+
))}
|
|
3916
|
+
</ol>
|
|
3917
|
+
</div>
|
|
3918
|
+
);
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
export function FormDemoView({ data }: FormDemoViewProps) {
|
|
3922
|
+
const formId = data.anchorId?.trim() || 'form-demo';
|
|
3923
|
+
const { status, message } = useFormState(formId);
|
|
3924
|
+
|
|
3925
|
+
return (
|
|
3926
|
+
<main className="min-h-screen flex items-center justify-center bg-background text-foreground px-6">
|
|
3927
|
+
<section className="w-full max-w-xl rounded-xl border border-border bg-card p-8 shadow-sm space-y-6">
|
|
3928
|
+
{data.icon && (
|
|
3929
|
+
<div data-jp-field="icon" className="mb-2">
|
|
3930
|
+
<Icon name={data.icon} size={24} />
|
|
3931
|
+
</div>
|
|
3932
|
+
)}
|
|
3933
|
+
{data.title && (
|
|
3934
|
+
<div>
|
|
3935
|
+
<h1
|
|
3936
|
+
data-jp-field="title"
|
|
3937
|
+
className="text-2xl font-semibold tracking-tight"
|
|
3938
|
+
>
|
|
3939
|
+
{data.title}
|
|
3940
|
+
</h1>
|
|
3941
|
+
{data.description && (
|
|
3942
|
+
<p
|
|
3943
|
+
data-jp-field="description"
|
|
3944
|
+
className="mt-3 text-sm text-muted-foreground"
|
|
3945
|
+
>
|
|
3946
|
+
{data.description}
|
|
3947
|
+
</p>
|
|
3948
|
+
)}
|
|
3949
|
+
</div>
|
|
3950
|
+
)}
|
|
3951
|
+
|
|
3952
|
+
<SetupGuide recipientEmail={data.recipientEmail} />
|
|
3953
|
+
|
|
3954
|
+
<form
|
|
3955
|
+
id={formId}
|
|
3956
|
+
data-olon-recipient={data.recipientEmail ?? ''}
|
|
3957
|
+
className="space-y-4"
|
|
3958
|
+
>
|
|
3959
|
+
<div>
|
|
3960
|
+
<label className="block text-xs font-medium text-muted-foreground mb-1">
|
|
3961
|
+
Nome
|
|
3962
|
+
</label>
|
|
3963
|
+
<input
|
|
3964
|
+
name="name"
|
|
3965
|
+
type="text"
|
|
3966
|
+
required
|
|
3967
|
+
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary"
|
|
3968
|
+
/>
|
|
3969
|
+
</div>
|
|
3970
|
+
|
|
3971
|
+
<div>
|
|
3972
|
+
<label className="block text-xs font-medium text-muted-foreground mb-1">
|
|
3973
|
+
Email
|
|
3974
|
+
</label>
|
|
3975
|
+
<input
|
|
3976
|
+
name="email"
|
|
3977
|
+
type="email"
|
|
3978
|
+
required
|
|
3979
|
+
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-primary"
|
|
3980
|
+
/>
|
|
3981
|
+
</div>
|
|
3982
|
+
|
|
3983
|
+
<div>
|
|
3984
|
+
<label className="block text-xs font-medium text-muted-foreground mb-1">
|
|
3985
|
+
Messaggio
|
|
3986
|
+
</label>
|
|
3987
|
+
<textarea
|
|
3988
|
+
name="message"
|
|
3989
|
+
required
|
|
3990
|
+
rows={4}
|
|
3991
|
+
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm resize-none focus:outline-none focus:ring-1 focus:ring-primary"
|
|
3992
|
+
/>
|
|
3993
|
+
</div>
|
|
3994
|
+
|
|
3995
|
+
{status === 'error' && (
|
|
3996
|
+
<p className="text-xs text-destructive">{message}</p>
|
|
3997
|
+
)}
|
|
3998
|
+
{status === 'success' && (
|
|
3999
|
+
<p className="text-xs text-green-600">
|
|
4000
|
+
{data.successMessage || message}
|
|
4001
|
+
</p>
|
|
4002
|
+
)}
|
|
4003
|
+
|
|
4004
|
+
<button
|
|
4005
|
+
type="submit"
|
|
4006
|
+
disabled={status === 'submitting'}
|
|
4007
|
+
className="w-full rounded-md bg-primary text-primary-foreground px-4 py-2 text-sm font-medium hover:opacity-90 disabled:opacity-60 disabled:cursor-not-allowed transition-opacity"
|
|
4008
|
+
>
|
|
4009
|
+
{status === 'submitting' ? 'Invio...' : (data.submitLabel || 'Invia')}
|
|
4010
|
+
</button>
|
|
4011
|
+
</form>
|
|
4012
|
+
</section>
|
|
4013
|
+
</main>
|
|
4014
|
+
);
|
|
4015
|
+
}
|
|
4016
|
+
|
|
4017
|
+
END_OF_FILE_CONTENT
|
|
4018
|
+
echo "Creating src/components/form-demo/index.ts..."
|
|
4019
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/index.ts"
|
|
4020
|
+
export * from './View';
|
|
4021
|
+
export * from './schema';
|
|
4022
|
+
export * from './types';
|
|
4023
|
+
|
|
4024
|
+
END_OF_FILE_CONTENT
|
|
4025
|
+
echo "Creating src/components/form-demo/schema.ts..."
|
|
4026
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/schema.ts"
|
|
4027
|
+
import { z } from 'zod';
|
|
4028
|
+
import { BaseSectionData, WithFormRecipient } from '@olonjs/core';
|
|
4029
|
+
|
|
4030
|
+
export const FormDemoSchema = BaseSectionData.merge(WithFormRecipient).extend({
|
|
4031
|
+
icon: z.string().optional().describe('ui:icon-picker'),
|
|
4032
|
+
title: z.string().optional().describe('ui:text'),
|
|
4033
|
+
description: z.string().optional().describe('ui:textarea'),
|
|
4034
|
+
submitLabel: z.string().default('Invia').describe('ui:text'),
|
|
4035
|
+
successMessage: z.string().default('Richiesta inviata con successo.').describe('ui:text'),
|
|
4036
|
+
});
|
|
4037
|
+
|
|
4038
|
+
export const FormDemoSettingsSchema = z.object({});
|
|
4039
|
+
|
|
4040
|
+
/**
|
|
4041
|
+
* Submission payload schema for the `form-demo` section.
|
|
4042
|
+
*
|
|
4043
|
+
* Describes the fields actually submitted by the rendered `<form>` in View.tsx
|
|
4044
|
+
* (name, email, message). Exposed via `JsonPagesConfig.submissionSchemas` so that
|
|
4045
|
+
* MCP agents can discover the submission contract for this section type without
|
|
4046
|
+
* scraping the DOM. See ADR-0002 (docs/decisions/ADR-0002-form-submission-schemas.md).
|
|
4047
|
+
*/
|
|
4048
|
+
export const FormDemoSubmissionSchema = z.object({
|
|
4049
|
+
name: z.string().min(1).describe('Full name of the person submitting the form'),
|
|
4050
|
+
email: z.string().email().describe('Contact email address where we will reply'),
|
|
4051
|
+
message: z.string().min(1).describe('Free-form message body'),
|
|
4052
|
+
});
|
|
4053
|
+
|
|
4054
|
+
END_OF_FILE_CONTENT
|
|
4055
|
+
echo "Creating src/components/form-demo/types.ts..."
|
|
4056
|
+
cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/types.ts"
|
|
4057
|
+
import { z } from 'zod';
|
|
4058
|
+
import { FormDemoSchema, FormDemoSettingsSchema } from './schema';
|
|
4059
|
+
|
|
4060
|
+
export type FormDemoData = z.infer<typeof FormDemoSchema>;
|
|
4061
|
+
export type FormDemoSettings = z.infer<typeof FormDemoSettingsSchema>;
|
|
4062
|
+
|
|
3824
4063
|
END_OF_FILE_CONTENT
|
|
3825
4064
|
mkdir -p "src/components/save-drawer"
|
|
3826
4065
|
echo "Creating src/components/save-drawer/DeployConnector.tsx..."
|
|
3827
4066
|
cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/DeployConnector.tsx"
|
|
3828
|
-
import type { StepState } from '
|
|
4067
|
+
import type { StepState } from '@olonjs/core';
|
|
3829
4068
|
|
|
3830
4069
|
interface DeployConnectorProps {
|
|
3831
4070
|
fromState: StepState;
|
|
@@ -3871,7 +4110,7 @@ END_OF_FILE_CONTENT
|
|
|
3871
4110
|
echo "Creating src/components/save-drawer/DeployNode.tsx..."
|
|
3872
4111
|
cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/DeployNode.tsx"
|
|
3873
4112
|
import type { CSSProperties } from 'react';
|
|
3874
|
-
import type { DeployStep, StepState } from '
|
|
4113
|
+
import type { DeployStep, StepState } from '@olonjs/core';
|
|
3875
4114
|
|
|
3876
4115
|
interface DeployNodeProps {
|
|
3877
4116
|
step: DeployStep;
|
|
@@ -3959,8 +4198,8 @@ echo "Creating src/components/save-drawer/DopaDrawer.tsx..."
|
|
|
3959
4198
|
cat << 'END_OF_FILE_CONTENT' > "src/components/save-drawer/DopaDrawer.tsx"
|
|
3960
4199
|
import { useEffect, useMemo, useState } from 'react';
|
|
3961
4200
|
import { createPortal } from 'react-dom';
|
|
3962
|
-
import type { StepId, StepState } from '
|
|
3963
|
-
import { DEPLOY_STEPS } from '
|
|
4201
|
+
import type { StepId, StepState } from '@olonjs/core';
|
|
4202
|
+
import { DEPLOY_STEPS } from '@olonjs/core';
|
|
3964
4203
|
import fontsCss from '@/fonts.css?inline';
|
|
3965
4204
|
import saverStyleCss from './saverStyle.css?inline';
|
|
3966
4205
|
import { DeployNode } from './DeployNode';
|
|
@@ -4909,7 +5148,7 @@ END_OF_FILE_CONTENT
|
|
|
4909
5148
|
mkdir -p "src/components/ui"
|
|
4910
5149
|
echo "Creating src/components/ui/OlonMark.tsx..."
|
|
4911
5150
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/OlonMark.tsx"
|
|
4912
|
-
import { cn } from '
|
|
5151
|
+
import { cn } from '@olonjs/core'
|
|
4913
5152
|
|
|
4914
5153
|
interface OlonMarkProps {
|
|
4915
5154
|
size?: number
|
|
@@ -4999,7 +5238,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/accordion.tsx"
|
|
|
4999
5238
|
import * as React from "react"
|
|
5000
5239
|
import { Accordion as AccordionPrimitive } from "radix-ui"
|
|
5001
5240
|
|
|
5002
|
-
import { cn } from "
|
|
5241
|
+
import { cn } from "@olonjs/core"
|
|
5003
5242
|
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
|
5004
5243
|
|
|
5005
5244
|
function Accordion({
|
|
@@ -5103,7 +5342,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/avatar.tsx"
|
|
|
5103
5342
|
import * as React from "react"
|
|
5104
5343
|
import { Avatar as AvatarPrimitive } from "radix-ui"
|
|
5105
5344
|
|
|
5106
|
-
import { cn } from "
|
|
5345
|
+
import { cn } from "@olonjs/core"
|
|
5107
5346
|
|
|
5108
5347
|
function Avatar({
|
|
5109
5348
|
className,
|
|
@@ -5218,7 +5457,7 @@ echo "Creating src/components/ui/badge.tsx..."
|
|
|
5218
5457
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/badge.tsx"
|
|
5219
5458
|
import * as React from 'react'
|
|
5220
5459
|
import { cva, type VariantProps } from 'class-variance-authority'
|
|
5221
|
-
import { cn } from '
|
|
5460
|
+
import { cn } from '@olonjs/core'
|
|
5222
5461
|
|
|
5223
5462
|
const badgeVariants = cva(
|
|
5224
5463
|
'inline-flex items-center px-2.5 py-0.5 text-xs font-medium transition-colors',
|
|
@@ -5253,7 +5492,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/breadcrumb.tsx"
|
|
|
5253
5492
|
import * as React from "react"
|
|
5254
5493
|
import { Slot } from "radix-ui"
|
|
5255
5494
|
|
|
5256
|
-
import { cn } from "
|
|
5495
|
+
import { cn } from "@olonjs/core"
|
|
5257
5496
|
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
|
|
5258
5497
|
|
|
5259
5498
|
function Breadcrumb({ className, ...props }: React.ComponentProps<"nav">) {
|
|
@@ -5380,7 +5619,7 @@ echo "Creating src/components/ui/button.tsx..."
|
|
|
5380
5619
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/button.tsx"
|
|
5381
5620
|
import * as React from 'react'
|
|
5382
5621
|
import { cva, type VariantProps } from 'class-variance-authority'
|
|
5383
|
-
import { cn } from '
|
|
5622
|
+
import { cn } from '@olonjs/core'
|
|
5384
5623
|
|
|
5385
5624
|
const buttonVariants = cva(
|
|
5386
5625
|
'inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40 shrink-0',
|
|
@@ -5441,7 +5680,7 @@ END_OF_FILE_CONTENT
|
|
|
5441
5680
|
echo "Creating src/components/ui/card.tsx..."
|
|
5442
5681
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/card.tsx"
|
|
5443
5682
|
import * as React from 'react'
|
|
5444
|
-
import { cn } from '
|
|
5683
|
+
import { cn } from '@olonjs/core'
|
|
5445
5684
|
|
|
5446
5685
|
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
5447
5686
|
({ className, ...props }, ref) => (
|
|
@@ -5500,7 +5739,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/checkbox.tsx"
|
|
|
5500
5739
|
import * as React from "react"
|
|
5501
5740
|
import { Checkbox as CheckboxPrimitive } from "radix-ui"
|
|
5502
5741
|
|
|
5503
|
-
import { cn } from "
|
|
5742
|
+
import { cn } from "@olonjs/core"
|
|
5504
5743
|
import { CheckIcon } from "lucide-react"
|
|
5505
5744
|
|
|
5506
5745
|
function Checkbox({
|
|
@@ -5540,7 +5779,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/dialog.tsx"
|
|
|
5540
5779
|
import * as React from "react"
|
|
5541
5780
|
import { Dialog as DialogPrimitive } from "radix-ui"
|
|
5542
5781
|
|
|
5543
|
-
import { cn } from "
|
|
5782
|
+
import { cn } from "@olonjs/core"
|
|
5544
5783
|
import { Button } from "@/components/ui/button"
|
|
5545
5784
|
import { XIcon } from "lucide-react"
|
|
5546
5785
|
|
|
@@ -5712,7 +5951,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/dropdown-menu.tsx"
|
|
|
5712
5951
|
import * as React from "react"
|
|
5713
5952
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
|
5714
5953
|
|
|
5715
|
-
import { cn } from "
|
|
5954
|
+
import { cn } from "@olonjs/core"
|
|
5716
5955
|
import { CheckIcon, ChevronRightIcon } from "lucide-react"
|
|
5717
5956
|
|
|
5718
5957
|
function DropdownMenu({
|
|
@@ -5987,7 +6226,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/hover-card.tsx"
|
|
|
5987
6226
|
import * as React from "react"
|
|
5988
6227
|
import { HoverCard as HoverCardPrimitive } from "radix-ui"
|
|
5989
6228
|
|
|
5990
|
-
import { cn } from "
|
|
6229
|
+
import { cn } from "@olonjs/core"
|
|
5991
6230
|
|
|
5992
6231
|
function HoverCard({
|
|
5993
6232
|
...props
|
|
@@ -6033,7 +6272,7 @@ END_OF_FILE_CONTENT
|
|
|
6033
6272
|
echo "Creating src/components/ui/input.tsx..."
|
|
6034
6273
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/input.tsx"
|
|
6035
6274
|
import * as React from 'react'
|
|
6036
|
-
import { cn } from '
|
|
6275
|
+
import { cn } from '@olonjs/core'
|
|
6037
6276
|
|
|
6038
6277
|
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
6039
6278
|
|
|
@@ -6064,7 +6303,7 @@ END_OF_FILE_CONTENT
|
|
|
6064
6303
|
echo "Creating src/components/ui/label.tsx..."
|
|
6065
6304
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/label.tsx"
|
|
6066
6305
|
import * as React from 'react'
|
|
6067
|
-
import { cn } from '
|
|
6306
|
+
import { cn } from '@olonjs/core'
|
|
6068
6307
|
|
|
6069
6308
|
const Label = React.forwardRef<
|
|
6070
6309
|
HTMLLabelElement,
|
|
@@ -6088,7 +6327,7 @@ import * as React from "react"
|
|
|
6088
6327
|
import { cva } from "class-variance-authority"
|
|
6089
6328
|
import { NavigationMenu as NavigationMenuPrimitive } from "radix-ui"
|
|
6090
6329
|
|
|
6091
|
-
import { cn } from "
|
|
6330
|
+
import { cn } from "@olonjs/core"
|
|
6092
6331
|
import { ChevronDownIcon } from "lucide-react"
|
|
6093
6332
|
|
|
6094
6333
|
function NavigationMenu({
|
|
@@ -6257,7 +6496,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/progress.tsx"
|
|
|
6257
6496
|
import * as React from "react"
|
|
6258
6497
|
import { Progress as ProgressPrimitive } from "radix-ui"
|
|
6259
6498
|
|
|
6260
|
-
import { cn } from "
|
|
6499
|
+
import { cn } from "@olonjs/core"
|
|
6261
6500
|
|
|
6262
6501
|
function Progress({
|
|
6263
6502
|
className,
|
|
@@ -6292,7 +6531,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/scroll-area.tsx"
|
|
|
6292
6531
|
import * as React from "react"
|
|
6293
6532
|
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
|
|
6294
6533
|
|
|
6295
|
-
import { cn } from "
|
|
6534
|
+
import { cn } from "@olonjs/core"
|
|
6296
6535
|
|
|
6297
6536
|
function ScrollArea({
|
|
6298
6537
|
className,
|
|
@@ -6351,7 +6590,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/select.tsx"
|
|
|
6351
6590
|
import * as React from "react"
|
|
6352
6591
|
import { Select as SelectPrimitive } from "radix-ui"
|
|
6353
6592
|
|
|
6354
|
-
import { cn } from "
|
|
6593
|
+
import { cn } from "@olonjs/core"
|
|
6355
6594
|
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
|
|
6356
6595
|
|
|
6357
6596
|
function Select({
|
|
@@ -6737,7 +6976,7 @@ END_OF_FILE_CONTENT
|
|
|
6737
6976
|
echo "Creating src/components/ui/separator.tsx..."
|
|
6738
6977
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/separator.tsx"
|
|
6739
6978
|
import * as React from 'react'
|
|
6740
|
-
import { cn } from '
|
|
6979
|
+
import { cn } from '@olonjs/core'
|
|
6741
6980
|
|
|
6742
6981
|
const Separator = React.forwardRef<
|
|
6743
6982
|
HTMLDivElement,
|
|
@@ -6766,7 +7005,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/sheet.tsx"
|
|
|
6766
7005
|
import * as React from "react"
|
|
6767
7006
|
import { Dialog as SheetPrimitive } from "radix-ui"
|
|
6768
7007
|
|
|
6769
|
-
import { cn } from "
|
|
7008
|
+
import { cn } from "@olonjs/core"
|
|
6770
7009
|
import { Button } from "@/components/ui/button"
|
|
6771
7010
|
import { XIcon } from "lucide-react"
|
|
6772
7011
|
|
|
@@ -6914,7 +7153,7 @@ END_OF_FILE_CONTENT
|
|
|
6914
7153
|
# SKIP: src/components/ui/sheet.tsx:Zone.Identifier is binary and cannot be embedded as text.
|
|
6915
7154
|
echo "Creating src/components/ui/skeleton.tsx..."
|
|
6916
7155
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/skeleton.tsx"
|
|
6917
|
-
import { cn } from '
|
|
7156
|
+
import { cn } from '@olonjs/core';
|
|
6918
7157
|
import type { HTMLAttributes } from 'react';
|
|
6919
7158
|
|
|
6920
7159
|
function Skeleton({
|
|
@@ -6938,7 +7177,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/switch.tsx"
|
|
|
6938
7177
|
import * as React from "react"
|
|
6939
7178
|
import { Switch as SwitchPrimitive } from "radix-ui"
|
|
6940
7179
|
|
|
6941
|
-
import { cn } from "
|
|
7180
|
+
import { cn } from "@olonjs/core"
|
|
6942
7181
|
|
|
6943
7182
|
function Switch({
|
|
6944
7183
|
className,
|
|
@@ -6974,7 +7213,7 @@ echo "Creating src/components/ui/table.tsx..."
|
|
|
6974
7213
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/table.tsx"
|
|
6975
7214
|
import * as React from "react"
|
|
6976
7215
|
|
|
6977
|
-
import { cn } from "
|
|
7216
|
+
import { cn } from "@olonjs/core"
|
|
6978
7217
|
|
|
6979
7218
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|
6980
7219
|
return (
|
|
@@ -7098,7 +7337,7 @@ import * as React from "react"
|
|
|
7098
7337
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
7099
7338
|
import { Tabs as TabsPrimitive } from "radix-ui"
|
|
7100
7339
|
|
|
7101
|
-
import { cn } from "
|
|
7340
|
+
import { cn } from "@olonjs/core"
|
|
7102
7341
|
|
|
7103
7342
|
function Tabs({
|
|
7104
7343
|
className,
|
|
@@ -7190,7 +7429,7 @@ echo "Creating src/components/ui/textarea.tsx..."
|
|
|
7190
7429
|
cat << 'END_OF_FILE_CONTENT' > "src/components/ui/textarea.tsx"
|
|
7191
7430
|
import * as React from "react"
|
|
7192
7431
|
|
|
7193
|
-
import { cn } from "
|
|
7432
|
+
import { cn } from "@olonjs/core"
|
|
7194
7433
|
|
|
7195
7434
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
7196
7435
|
return (
|
|
@@ -7219,7 +7458,7 @@ import * as React from "react"
|
|
|
7219
7458
|
import { type VariantProps } from "class-variance-authority"
|
|
7220
7459
|
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
|
|
7221
7460
|
|
|
7222
|
-
import { cn } from "
|
|
7461
|
+
import { cn } from "@olonjs/core"
|
|
7223
7462
|
import { toggleVariants } from "@/components/ui/toggle"
|
|
7224
7463
|
|
|
7225
7464
|
const ToggleGroupContext = React.createContext<
|
|
@@ -7314,7 +7553,7 @@ import * as React from "react"
|
|
|
7314
7553
|
import { cva, type VariantProps } from "class-variance-authority"
|
|
7315
7554
|
import { Toggle as TogglePrimitive } from "radix-ui"
|
|
7316
7555
|
|
|
7317
|
-
import { cn } from "
|
|
7556
|
+
import { cn } from "@olonjs/core"
|
|
7318
7557
|
|
|
7319
7558
|
const toggleVariants = cva(
|
|
7320
7559
|
"group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
@@ -7366,7 +7605,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/components/ui/tooltip.tsx"
|
|
|
7366
7605
|
import * as React from "react"
|
|
7367
7606
|
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
|
7368
7607
|
|
|
7369
|
-
import { cn } from "
|
|
7608
|
+
import { cn } from "@olonjs/core"
|
|
7370
7609
|
|
|
7371
7610
|
function TooltipProvider({
|
|
7372
7611
|
delayDuration = 0,
|
|
@@ -7503,16 +7742,7 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/site.json"
|
|
|
7503
7742
|
"title": "OlonJS",
|
|
7504
7743
|
"logoUrl": "/brand/mark/olon-mark-dark.svg"
|
|
7505
7744
|
},
|
|
7506
|
-
|
|
7507
|
-
{
|
|
7508
|
-
"slug": "home",
|
|
7509
|
-
"label": "Home"
|
|
7510
|
-
},
|
|
7511
|
-
{
|
|
7512
|
-
"slug": "design-system",
|
|
7513
|
-
"label": "Design System"
|
|
7514
|
-
}
|
|
7515
|
-
],
|
|
7745
|
+
|
|
7516
7746
|
"header": {
|
|
7517
7747
|
"id": "global-header",
|
|
7518
7748
|
"type": "header",
|
|
@@ -7734,6 +7964,33 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/theme.json"
|
|
|
7734
7964
|
}
|
|
7735
7965
|
END_OF_FILE_CONTENT
|
|
7736
7966
|
mkdir -p "src/data/pages"
|
|
7967
|
+
echo "Creating src/data/pages/home.json..."
|
|
7968
|
+
cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
|
|
7969
|
+
{
|
|
7970
|
+
"id": "home-page",
|
|
7971
|
+
"slug": "home",
|
|
7972
|
+
"meta": {
|
|
7973
|
+
"title": "Home",
|
|
7974
|
+
"description": "OlonJS tenant alpha — form smoke test"
|
|
7975
|
+
},
|
|
7976
|
+
"sections": [
|
|
7977
|
+
{
|
|
7978
|
+
"id": "form-demo-1",
|
|
7979
|
+
"type": "form-demo",
|
|
7980
|
+
"data": {
|
|
7981
|
+
"anchorId": "form-demo",
|
|
7982
|
+
"title": "Contattaci",
|
|
7983
|
+
"description": "Compila il modulo e ti risponderemo al più presto.",
|
|
7984
|
+
"recipientEmail": "test@olonjs.io",
|
|
7985
|
+
"submitLabel": "Invia",
|
|
7986
|
+
"successMessage": "Richiesta inviata con successo.",
|
|
7987
|
+
"icon": "mail"
|
|
7988
|
+
}
|
|
7989
|
+
}
|
|
7990
|
+
],
|
|
7991
|
+
"global-header": false
|
|
7992
|
+
}
|
|
7993
|
+
END_OF_FILE_CONTENT
|
|
7737
7994
|
mkdir -p "src/emails"
|
|
7738
7995
|
echo "Creating src/emails/LeadNotificationEmail.tsx..."
|
|
7739
7996
|
cat << 'END_OF_FILE_CONTENT' > "src/emails/LeadNotificationEmail.tsx"
|
|
@@ -9185,11 +9442,13 @@ cat << 'END_OF_FILE_CONTENT' > "src/lib/ComponentRegistry.tsx"
|
|
|
9185
9442
|
import type { SectionType } from '@/types';
|
|
9186
9443
|
import type { SectionComponentPropsMap } from '@/types';
|
|
9187
9444
|
import { EmptyTenantView } from '@/components/empty-tenant';
|
|
9445
|
+
import { FormDemoView } from '@/components/form-demo';
|
|
9188
9446
|
|
|
9189
9447
|
export const ComponentRegistry: {
|
|
9190
9448
|
[K in SectionType]: React.FC<SectionComponentPropsMap[K]>;
|
|
9191
9449
|
} = {
|
|
9192
9450
|
'empty-tenant': EmptyTenantView as React.FC<SectionComponentPropsMap['empty-tenant']>,
|
|
9451
|
+
'form-demo': FormDemoView as React.FC<SectionComponentPropsMap['form-demo']>,
|
|
9193
9452
|
};
|
|
9194
9453
|
|
|
9195
9454
|
END_OF_FILE_CONTENT
|
|
@@ -9207,10 +9466,11 @@ import {
|
|
|
9207
9466
|
X,
|
|
9208
9467
|
Sparkles,
|
|
9209
9468
|
Zap,
|
|
9469
|
+
Mail,
|
|
9210
9470
|
type LucideIcon
|
|
9211
9471
|
} from 'lucide-react';
|
|
9212
9472
|
|
|
9213
|
-
const iconMap = {
|
|
9473
|
+
export const iconMap = {
|
|
9214
9474
|
'layers': Layers,
|
|
9215
9475
|
'github': Github,
|
|
9216
9476
|
'arrow-right': ArrowRight,
|
|
@@ -9221,6 +9481,7 @@ const iconMap = {
|
|
|
9221
9481
|
'x': X,
|
|
9222
9482
|
'sparkles': Sparkles,
|
|
9223
9483
|
'zap': Zap,
|
|
9484
|
+
'mail': Mail,
|
|
9224
9485
|
} as const satisfies Record<string, LucideIcon>;
|
|
9225
9486
|
|
|
9226
9487
|
export type IconName = keyof typeof iconMap;
|
|
@@ -9255,10 +9516,11 @@ echo "Creating src/lib/addSectionConfig.ts..."
|
|
|
9255
9516
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/addSectionConfig.ts"
|
|
9256
9517
|
import type { AddSectionConfig } from '@olonjs/core';
|
|
9257
9518
|
|
|
9258
|
-
const addableSectionTypes = ['empty-tenant'] as const;
|
|
9519
|
+
const addableSectionTypes = ['empty-tenant', 'form-demo'] as const;
|
|
9259
9520
|
|
|
9260
9521
|
const sectionTypeLabels: Record<string, string> = {
|
|
9261
9522
|
'empty-tenant': 'Empty Tenant',
|
|
9523
|
+
'form-demo': 'Form Demo',
|
|
9262
9524
|
};
|
|
9263
9525
|
|
|
9264
9526
|
function getDefaultSectionData(type: string): Record<string, unknown> {
|
|
@@ -9267,9 +9529,17 @@ function getDefaultSectionData(type: string): Record<string, unknown> {
|
|
|
9267
9529
|
return {
|
|
9268
9530
|
title: 'Your tenant is empty.',
|
|
9269
9531
|
description: 'Create your first page to start building your site.',
|
|
9270
|
-
ctaLabel: 'Open Studio',
|
|
9271
9532
|
};
|
|
9272
|
-
|
|
9533
|
+
case 'form-demo':
|
|
9534
|
+
return {
|
|
9535
|
+
title: 'Contattaci',
|
|
9536
|
+
description: 'Compila il modulo e ti risponderemo al più presto.',
|
|
9537
|
+
recipientEmail: '',
|
|
9538
|
+
submitLabel: 'Invia',
|
|
9539
|
+
successMessage: 'Richiesta inviata con successo.',
|
|
9540
|
+
};
|
|
9541
|
+
default:
|
|
9542
|
+
return {};
|
|
9273
9543
|
}
|
|
9274
9544
|
}
|
|
9275
9545
|
|
|
@@ -9279,219 +9549,6 @@ export const addSectionConfig: AddSectionConfig = {
|
|
|
9279
9549
|
getDefaultSectionData,
|
|
9280
9550
|
};
|
|
9281
9551
|
|
|
9282
|
-
END_OF_FILE_CONTENT
|
|
9283
|
-
echo "Creating src/lib/base-schemas.ts..."
|
|
9284
|
-
cat << 'END_OF_FILE_CONTENT' > "src/lib/base-schemas.ts"
|
|
9285
|
-
import { z } from 'zod';
|
|
9286
|
-
|
|
9287
|
-
/**
|
|
9288
|
-
* Image picker field: object { url, alt? } with ui:image-picker for Form Factory.
|
|
9289
|
-
* Use in section data and resolve with resolveAssetUrl(url, tenantId) in View.
|
|
9290
|
-
*/
|
|
9291
|
-
export const ImageSelectionSchema = z
|
|
9292
|
-
.object({
|
|
9293
|
-
url: z.string(),
|
|
9294
|
-
alt: z.string().optional(),
|
|
9295
|
-
})
|
|
9296
|
-
.describe('ui:image-picker');
|
|
9297
|
-
|
|
9298
|
-
/**
|
|
9299
|
-
* Base schemas shared by section capsules (CIP governance).
|
|
9300
|
-
* Capsules extend these for consistent anchorId, array items, and settings.
|
|
9301
|
-
*/
|
|
9302
|
-
export const BaseSectionData = z.object({
|
|
9303
|
-
id: z.string().optional(),
|
|
9304
|
-
anchorId: z.string().optional().describe('ui:text'),
|
|
9305
|
-
});
|
|
9306
|
-
|
|
9307
|
-
export const BaseArrayItem = z.object({
|
|
9308
|
-
id: z.string().optional(),
|
|
9309
|
-
});
|
|
9310
|
-
|
|
9311
|
-
export const BaseSectionSettingsSchema = z.object({
|
|
9312
|
-
paddingTop: z.enum(['none', 'sm', 'md', 'lg', 'xl', '2xl']).default('md').describe('ui:select'),
|
|
9313
|
-
paddingBottom: z.enum(['none', 'sm', 'md', 'lg', 'xl', '2xl']).default('md').describe('ui:select'),
|
|
9314
|
-
theme: z.enum(['dark', 'light', 'accent']).default('dark').describe('ui:select'),
|
|
9315
|
-
container: z.enum(['boxed', 'fluid']).default('boxed').describe('ui:select'),
|
|
9316
|
-
});
|
|
9317
|
-
|
|
9318
|
-
export const CtaSchema = z.object({
|
|
9319
|
-
id: z.string().optional(),
|
|
9320
|
-
label: z.string().describe('ui:text'),
|
|
9321
|
-
href: z.string().describe('ui:text'),
|
|
9322
|
-
variant: z.enum(['primary', 'secondary', 'accent']).default('primary').describe('ui:select'),
|
|
9323
|
-
});
|
|
9324
|
-
|
|
9325
|
-
END_OF_FILE_CONTENT
|
|
9326
|
-
echo "Creating src/lib/cloudSaveStream.ts..."
|
|
9327
|
-
cat << 'END_OF_FILE_CONTENT' > "src/lib/cloudSaveStream.ts"
|
|
9328
|
-
import type { StepId } from '@/types/deploy';
|
|
9329
|
-
|
|
9330
|
-
interface SaveStreamStepEvent {
|
|
9331
|
-
id: StepId;
|
|
9332
|
-
status: 'running' | 'done';
|
|
9333
|
-
label?: string;
|
|
9334
|
-
}
|
|
9335
|
-
|
|
9336
|
-
interface SaveStreamLogEvent {
|
|
9337
|
-
stepId: StepId;
|
|
9338
|
-
message: string;
|
|
9339
|
-
}
|
|
9340
|
-
|
|
9341
|
-
interface SaveStreamDoneEvent {
|
|
9342
|
-
deployUrl?: string;
|
|
9343
|
-
commitSha?: string;
|
|
9344
|
-
}
|
|
9345
|
-
|
|
9346
|
-
interface SaveStreamErrorEvent {
|
|
9347
|
-
message?: string;
|
|
9348
|
-
}
|
|
9349
|
-
|
|
9350
|
-
interface StartCloudSaveStreamInput {
|
|
9351
|
-
apiBaseUrl: string;
|
|
9352
|
-
apiKey: string;
|
|
9353
|
-
path: string;
|
|
9354
|
-
content: unknown;
|
|
9355
|
-
message?: string;
|
|
9356
|
-
signal?: AbortSignal;
|
|
9357
|
-
onStep: (event: SaveStreamStepEvent) => void;
|
|
9358
|
-
onLog?: (event: SaveStreamLogEvent) => void;
|
|
9359
|
-
onDone: (event: SaveStreamDoneEvent) => void;
|
|
9360
|
-
}
|
|
9361
|
-
|
|
9362
|
-
function parseSseEventBlock(rawBlock: string): { event: string; data: string } | null {
|
|
9363
|
-
const lines = rawBlock
|
|
9364
|
-
.split('\n')
|
|
9365
|
-
.map((line) => line.trimEnd())
|
|
9366
|
-
.filter((line) => line.length > 0);
|
|
9367
|
-
|
|
9368
|
-
if (lines.length === 0) return null;
|
|
9369
|
-
|
|
9370
|
-
let eventName = 'message';
|
|
9371
|
-
const dataLines: string[] = [];
|
|
9372
|
-
for (const line of lines) {
|
|
9373
|
-
if (line.startsWith('event:')) {
|
|
9374
|
-
eventName = line.slice(6).trim();
|
|
9375
|
-
continue;
|
|
9376
|
-
}
|
|
9377
|
-
if (line.startsWith('data:')) {
|
|
9378
|
-
dataLines.push(line.slice(5).trim());
|
|
9379
|
-
}
|
|
9380
|
-
}
|
|
9381
|
-
return { event: eventName, data: dataLines.join('\n') };
|
|
9382
|
-
}
|
|
9383
|
-
|
|
9384
|
-
export async function startCloudSaveStream(input: StartCloudSaveStreamInput): Promise<void> {
|
|
9385
|
-
const response = await fetch(`${input.apiBaseUrl}/save-stream`, {
|
|
9386
|
-
method: 'POST',
|
|
9387
|
-
headers: {
|
|
9388
|
-
'Content-Type': 'application/json',
|
|
9389
|
-
Authorization: `Bearer ${input.apiKey}`,
|
|
9390
|
-
},
|
|
9391
|
-
body: JSON.stringify({
|
|
9392
|
-
path: input.path,
|
|
9393
|
-
content: input.content,
|
|
9394
|
-
message: input.message,
|
|
9395
|
-
}),
|
|
9396
|
-
signal: input.signal,
|
|
9397
|
-
});
|
|
9398
|
-
|
|
9399
|
-
if (!response.ok || !response.body) {
|
|
9400
|
-
const body = (await response.json().catch(() => ({}))) as SaveStreamErrorEvent;
|
|
9401
|
-
throw new Error(body.message ?? `Cloud save stream failed: ${response.status}`);
|
|
9402
|
-
}
|
|
9403
|
-
|
|
9404
|
-
const reader = response.body.getReader();
|
|
9405
|
-
const decoder = new TextDecoder();
|
|
9406
|
-
let buffer = '';
|
|
9407
|
-
let receivedDone = false;
|
|
9408
|
-
|
|
9409
|
-
while (true) {
|
|
9410
|
-
const { value, done } = await reader.read();
|
|
9411
|
-
if (!done) {
|
|
9412
|
-
buffer += decoder.decode(value, { stream: true });
|
|
9413
|
-
} else {
|
|
9414
|
-
buffer += decoder.decode();
|
|
9415
|
-
}
|
|
9416
|
-
|
|
9417
|
-
const chunks = buffer.split('\n\n');
|
|
9418
|
-
buffer = done ? '' : (chunks.pop() ?? '');
|
|
9419
|
-
|
|
9420
|
-
for (const chunk of chunks) {
|
|
9421
|
-
const parsed = parseSseEventBlock(chunk);
|
|
9422
|
-
if (!parsed) continue;
|
|
9423
|
-
if (!parsed.data) continue;
|
|
9424
|
-
|
|
9425
|
-
if (parsed.event === 'step') {
|
|
9426
|
-
const payload = JSON.parse(parsed.data) as SaveStreamStepEvent;
|
|
9427
|
-
input.onStep(payload);
|
|
9428
|
-
} else if (parsed.event === 'log') {
|
|
9429
|
-
const payload = JSON.parse(parsed.data) as SaveStreamLogEvent;
|
|
9430
|
-
input.onLog?.(payload);
|
|
9431
|
-
} else if (parsed.event === 'error') {
|
|
9432
|
-
const payload = JSON.parse(parsed.data) as SaveStreamErrorEvent;
|
|
9433
|
-
throw new Error(payload.message ?? 'Cloud save failed.');
|
|
9434
|
-
} else if (parsed.event === 'done') {
|
|
9435
|
-
const payload = JSON.parse(parsed.data) as SaveStreamDoneEvent;
|
|
9436
|
-
input.onDone(payload);
|
|
9437
|
-
receivedDone = true;
|
|
9438
|
-
}
|
|
9439
|
-
}
|
|
9440
|
-
|
|
9441
|
-
if (done) break;
|
|
9442
|
-
}
|
|
9443
|
-
|
|
9444
|
-
if (!receivedDone) {
|
|
9445
|
-
throw new Error('Cloud save stream ended before completion.');
|
|
9446
|
-
}
|
|
9447
|
-
}
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
END_OF_FILE_CONTENT
|
|
9451
|
-
echo "Creating src/lib/deploySteps.ts..."
|
|
9452
|
-
cat << 'END_OF_FILE_CONTENT' > "src/lib/deploySteps.ts"
|
|
9453
|
-
import type { DeployStep } from '@/types/deploy';
|
|
9454
|
-
|
|
9455
|
-
export const DEPLOY_STEPS: readonly DeployStep[] = [
|
|
9456
|
-
{
|
|
9457
|
-
id: 'commit',
|
|
9458
|
-
label: 'Commit',
|
|
9459
|
-
verb: 'Committing',
|
|
9460
|
-
poem: ['Crystallizing your edit', 'into permanent history.'],
|
|
9461
|
-
color: '#60a5fa',
|
|
9462
|
-
glyph: '◈',
|
|
9463
|
-
duration: 2200,
|
|
9464
|
-
},
|
|
9465
|
-
{
|
|
9466
|
-
id: 'push',
|
|
9467
|
-
label: 'Push',
|
|
9468
|
-
verb: 'Pushing',
|
|
9469
|
-
poem: ['Sending your vision', 'across the wire.'],
|
|
9470
|
-
color: '#a78bfa',
|
|
9471
|
-
glyph: '◎',
|
|
9472
|
-
duration: 2800,
|
|
9473
|
-
},
|
|
9474
|
-
{
|
|
9475
|
-
id: 'build',
|
|
9476
|
-
label: 'Build',
|
|
9477
|
-
verb: 'Building',
|
|
9478
|
-
poem: ['Assembling the pieces,', 'brick by digital brick.'],
|
|
9479
|
-
color: '#f59e0b',
|
|
9480
|
-
glyph: '⬡',
|
|
9481
|
-
duration: 7500,
|
|
9482
|
-
},
|
|
9483
|
-
{
|
|
9484
|
-
id: 'live',
|
|
9485
|
-
label: 'Live',
|
|
9486
|
-
verb: 'Going live',
|
|
9487
|
-
poem: ['Your content', 'is now breathing.'],
|
|
9488
|
-
color: '#34d399',
|
|
9489
|
-
glyph: '✦',
|
|
9490
|
-
duration: 1600,
|
|
9491
|
-
},
|
|
9492
|
-
] as const;
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
9552
|
END_OF_FILE_CONTENT
|
|
9496
9553
|
echo "Creating src/lib/draftStorage.ts..."
|
|
9497
9554
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/draftStorage.ts"
|
|
@@ -9572,9 +9629,22 @@ END_OF_FILE_CONTENT
|
|
|
9572
9629
|
echo "Creating src/lib/schemas.ts..."
|
|
9573
9630
|
cat << 'END_OF_FILE_CONTENT' > "src/lib/schemas.ts"
|
|
9574
9631
|
import { EmptyTenantSchema } from '@/components/empty-tenant';
|
|
9632
|
+
import { FormDemoSchema, FormDemoSubmissionSchema } from '@/components/form-demo';
|
|
9575
9633
|
|
|
9576
9634
|
export const SECTION_SCHEMAS = {
|
|
9577
9635
|
'empty-tenant': EmptyTenantSchema,
|
|
9636
|
+
'form-demo': FormDemoSchema,
|
|
9637
|
+
} as const;
|
|
9638
|
+
|
|
9639
|
+
/**
|
|
9640
|
+
* Registry of per-section-type submission schemas. Keys MUST match a key of
|
|
9641
|
+
* SECTION_SCHEMAS. A section type appearing here is declaring itself as
|
|
9642
|
+
* MCP-submittable: the Zod schema describes the payload accepted by the form.
|
|
9643
|
+
*
|
|
9644
|
+
* See ADR-0002 (docs/decisions/ADR-0002-form-submission-schemas.md).
|
|
9645
|
+
*/
|
|
9646
|
+
export const SECTION_SUBMISSION_SCHEMAS = {
|
|
9647
|
+
'form-demo': FormDemoSubmissionSchema,
|
|
9578
9648
|
} as const;
|
|
9579
9649
|
|
|
9580
9650
|
export type SectionType = keyof typeof SECTION_SCHEMAS;
|
|
@@ -9585,7 +9655,7 @@ export {
|
|
|
9585
9655
|
BaseSectionSettingsSchema,
|
|
9586
9656
|
CtaSchema,
|
|
9587
9657
|
ImageSelectionSchema,
|
|
9588
|
-
} from '
|
|
9658
|
+
} from '@olonjs/core';
|
|
9589
9659
|
|
|
9590
9660
|
END_OF_FILE_CONTENT
|
|
9591
9661
|
echo "Creating src/lib/useFormSubmit.ts..."
|
|
@@ -9677,13 +9747,134 @@ export function useFormSubmit({ source, tenantId }: UseFormSubmitOptions) {
|
|
|
9677
9747
|
return { submit, status, message, reset };
|
|
9678
9748
|
}
|
|
9679
9749
|
END_OF_FILE_CONTENT
|
|
9680
|
-
echo "Creating src/lib/
|
|
9681
|
-
cat << 'END_OF_FILE_CONTENT' > "src/lib/
|
|
9682
|
-
import {
|
|
9683
|
-
import {
|
|
9750
|
+
echo "Creating src/lib/useOlonForms.ts..."
|
|
9751
|
+
cat << 'END_OF_FILE_CONTENT' > "src/lib/useOlonForms.ts"
|
|
9752
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
9753
|
+
import type { FormState } from '@olonjs/core';
|
|
9684
9754
|
|
|
9685
|
-
|
|
9686
|
-
|
|
9755
|
+
const API_BASE =
|
|
9756
|
+
(import.meta.env.VITE_OLONJS_CLOUD_URL as string | undefined) ??
|
|
9757
|
+
(import.meta.env.VITE_JSONPAGES_CLOUD_URL as string | undefined);
|
|
9758
|
+
|
|
9759
|
+
const API_KEY =
|
|
9760
|
+
(import.meta.env.VITE_OLONJS_API_KEY as string | undefined) ??
|
|
9761
|
+
(import.meta.env.VITE_JSONPAGES_API_KEY as string | undefined);
|
|
9762
|
+
|
|
9763
|
+
interface UseOlonFormsOptions {
|
|
9764
|
+
/** Override the submit endpoint. Defaults to VITE_OLONJS_CLOUD_URL/forms/submit */
|
|
9765
|
+
endpoint?: string;
|
|
9766
|
+
}
|
|
9767
|
+
|
|
9768
|
+
/**
|
|
9769
|
+
* Mount once in App.tsx. Scans the DOM for all <form data-olon-recipient="...">
|
|
9770
|
+
* elements and attaches submit handlers. Returns per-form states to be provided
|
|
9771
|
+
* via OlonFormsContext.Provider.
|
|
9772
|
+
*
|
|
9773
|
+
* Views consume state via useFormState(formId) — no direct coupling to this hook.
|
|
9774
|
+
*/
|
|
9775
|
+
export function useOlonForms(options?: UseOlonFormsOptions): { states: Record<string, FormState> } {
|
|
9776
|
+
const [states, setStates] = useState<Record<string, FormState>>({});
|
|
9777
|
+
|
|
9778
|
+
const setFormState = useCallback((formId: string, state: FormState) => {
|
|
9779
|
+
setStates((prev) => ({ ...prev, [formId]: state }));
|
|
9780
|
+
}, []);
|
|
9781
|
+
|
|
9782
|
+
useEffect(() => {
|
|
9783
|
+
const resolvedBase = options?.endpoint
|
|
9784
|
+
? options.endpoint.replace(/\/$/, '')
|
|
9785
|
+
: API_BASE
|
|
9786
|
+
? API_BASE.replace(/\/$/, '')
|
|
9787
|
+
: null;
|
|
9788
|
+
|
|
9789
|
+
if (!resolvedBase || !API_KEY) {
|
|
9790
|
+
console.warn('[useOlonForms] Missing API endpoint or key — forms will not submit.');
|
|
9791
|
+
return;
|
|
9792
|
+
}
|
|
9793
|
+
|
|
9794
|
+
const endpoint = resolvedBase.endsWith('/forms/submit')
|
|
9795
|
+
? resolvedBase
|
|
9796
|
+
: `${resolvedBase}/forms/submit`;
|
|
9797
|
+
|
|
9798
|
+
const forms = Array.from(
|
|
9799
|
+
document.querySelectorAll<HTMLFormElement>('form[data-olon-recipient]')
|
|
9800
|
+
);
|
|
9801
|
+
|
|
9802
|
+
const controllers: AbortController[] = [];
|
|
9803
|
+
|
|
9804
|
+
async function handleSubmit(form: HTMLFormElement, event: SubmitEvent) {
|
|
9805
|
+
event.preventDefault();
|
|
9806
|
+
|
|
9807
|
+
const formId = form.id || form.dataset.olonRecipient || 'olon-form';
|
|
9808
|
+
const recipientEmail = form.dataset.olonRecipient ?? '';
|
|
9809
|
+
|
|
9810
|
+
setFormState(formId, { status: 'submitting', message: 'Invio in corso...' });
|
|
9811
|
+
|
|
9812
|
+
const raw: Record<string, string> = {};
|
|
9813
|
+
new FormData(form).forEach((value, key) => {
|
|
9814
|
+
raw[key] = String(value).trim();
|
|
9815
|
+
});
|
|
9816
|
+
|
|
9817
|
+
const payload = {
|
|
9818
|
+
...raw,
|
|
9819
|
+
recipientEmail,
|
|
9820
|
+
page: window.location.pathname,
|
|
9821
|
+
source: 'olon-form',
|
|
9822
|
+
submittedAt: new Date().toISOString(),
|
|
9823
|
+
};
|
|
9824
|
+
|
|
9825
|
+
const controller = new AbortController();
|
|
9826
|
+
controllers.push(controller);
|
|
9827
|
+
|
|
9828
|
+
try {
|
|
9829
|
+
const response = await fetch(endpoint, {
|
|
9830
|
+
method: 'POST',
|
|
9831
|
+
headers: {
|
|
9832
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
9833
|
+
'Content-Type': 'application/json',
|
|
9834
|
+
'Idempotency-Key': `form-${formId}-${Date.now()}`,
|
|
9835
|
+
},
|
|
9836
|
+
body: JSON.stringify(payload),
|
|
9837
|
+
signal: controller.signal,
|
|
9838
|
+
});
|
|
9839
|
+
|
|
9840
|
+
const body = (await response.json().catch(() => ({}))) as {
|
|
9841
|
+
error?: string;
|
|
9842
|
+
code?: string;
|
|
9843
|
+
};
|
|
9844
|
+
|
|
9845
|
+
if (!response.ok) {
|
|
9846
|
+
throw new Error(body.error ?? body.code ?? `Submit failed (${response.status})`);
|
|
9847
|
+
}
|
|
9848
|
+
|
|
9849
|
+
setFormState(formId, {
|
|
9850
|
+
status: 'success',
|
|
9851
|
+
message: 'Richiesta inviata con successo.',
|
|
9852
|
+
});
|
|
9853
|
+
form.reset();
|
|
9854
|
+
} catch (error: unknown) {
|
|
9855
|
+
if (error instanceof Error && error.name === 'AbortError') return;
|
|
9856
|
+
const message =
|
|
9857
|
+
error instanceof Error ? error.message : 'Invio non riuscito. Riprova.';
|
|
9858
|
+
setFormState(formId, { status: 'error', message });
|
|
9859
|
+
}
|
|
9860
|
+
}
|
|
9861
|
+
|
|
9862
|
+
type Listener = { form: HTMLFormElement; handler: (e: Event) => void };
|
|
9863
|
+
const listeners: Listener[] = [];
|
|
9864
|
+
|
|
9865
|
+
forms.forEach((form) => {
|
|
9866
|
+
const handler = (e: Event) => void handleSubmit(form, e as SubmitEvent);
|
|
9867
|
+
form.addEventListener('submit', handler);
|
|
9868
|
+
listeners.push({ form, handler });
|
|
9869
|
+
});
|
|
9870
|
+
|
|
9871
|
+
return () => {
|
|
9872
|
+
controllers.forEach((c) => c.abort());
|
|
9873
|
+
listeners.forEach(({ form, handler }) => form.removeEventListener('submit', handler));
|
|
9874
|
+
};
|
|
9875
|
+
}, [options?.endpoint, setFormState]);
|
|
9876
|
+
|
|
9877
|
+
return { states };
|
|
9687
9878
|
}
|
|
9688
9879
|
|
|
9689
9880
|
END_OF_FILE_CONTENT
|
|
@@ -9708,7 +9899,7 @@ END_OF_FILE_CONTENT
|
|
|
9708
9899
|
echo "Creating src/runtime.ts..."
|
|
9709
9900
|
cat << 'END_OF_FILE_CONTENT' > "src/runtime.ts"
|
|
9710
9901
|
import type { JsonPagesConfig, MenuConfig, PageConfig, SiteConfig, ThemeConfig } from '@/types';
|
|
9711
|
-
import { SECTION_SCHEMAS } from '@/lib/schemas';
|
|
9902
|
+
import { SECTION_SCHEMAS, SECTION_SUBMISSION_SCHEMAS } from '@/lib/schemas';
|
|
9712
9903
|
import { getFilePages } from '@/lib/getFilePages';
|
|
9713
9904
|
import siteData from '@/data/config/site.json';
|
|
9714
9905
|
import menuData from '@/data/config/menu.json';
|
|
@@ -9727,56 +9918,41 @@ export const refDocuments = {
|
|
|
9727
9918
|
export function getWebMcpBuildState(): {
|
|
9728
9919
|
pages: Record<string, PageConfig>;
|
|
9729
9920
|
schemas: JsonPagesConfig['schemas'];
|
|
9921
|
+
submissionSchemas: JsonPagesConfig['submissionSchemas'];
|
|
9730
9922
|
siteConfig: SiteConfig;
|
|
9731
9923
|
} {
|
|
9732
9924
|
return {
|
|
9733
9925
|
pages,
|
|
9734
9926
|
schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
|
|
9927
|
+
submissionSchemas: SECTION_SUBMISSION_SCHEMAS as unknown as JsonPagesConfig['submissionSchemas'],
|
|
9735
9928
|
siteConfig,
|
|
9736
9929
|
};
|
|
9737
9930
|
}
|
|
9738
9931
|
|
|
9739
9932
|
END_OF_FILE_CONTENT
|
|
9740
|
-
mkdir -p "src/types"
|
|
9741
9933
|
echo "Creating src/types.ts..."
|
|
9742
9934
|
cat << 'END_OF_FILE_CONTENT' > "src/types.ts"
|
|
9743
9935
|
import type { EmptyTenantData, EmptyTenantSettings } from '@/components/empty-tenant';
|
|
9936
|
+
import type { FormDemoData, FormDemoSettings } from '@/components/form-demo';
|
|
9744
9937
|
|
|
9745
9938
|
export type SectionComponentPropsMap = {
|
|
9746
9939
|
'empty-tenant': { data: EmptyTenantData; settings?: EmptyTenantSettings };
|
|
9940
|
+
'form-demo': { data: FormDemoData; settings?: FormDemoSettings };
|
|
9747
9941
|
};
|
|
9748
9942
|
|
|
9749
9943
|
declare module '@olonjs/core' {
|
|
9750
9944
|
export interface SectionDataRegistry {
|
|
9751
9945
|
'empty-tenant': EmptyTenantData;
|
|
9946
|
+
'form-demo': FormDemoData;
|
|
9752
9947
|
}
|
|
9753
9948
|
export interface SectionSettingsRegistry {
|
|
9754
9949
|
'empty-tenant': EmptyTenantSettings;
|
|
9950
|
+
'form-demo': FormDemoSettings;
|
|
9755
9951
|
}
|
|
9756
9952
|
}
|
|
9757
9953
|
|
|
9758
9954
|
export * from '@olonjs/core';
|
|
9759
9955
|
|
|
9760
|
-
END_OF_FILE_CONTENT
|
|
9761
|
-
echo "Creating src/types/deploy.ts..."
|
|
9762
|
-
cat << 'END_OF_FILE_CONTENT' > "src/types/deploy.ts"
|
|
9763
|
-
export type StepId = 'commit' | 'push' | 'build' | 'live';
|
|
9764
|
-
|
|
9765
|
-
export type StepState = 'pending' | 'active' | 'done';
|
|
9766
|
-
|
|
9767
|
-
export type DeployPhase = 'idle' | 'running' | 'done' | 'error';
|
|
9768
|
-
|
|
9769
|
-
export interface DeployStep {
|
|
9770
|
-
id: StepId;
|
|
9771
|
-
label: string;
|
|
9772
|
-
verb: string;
|
|
9773
|
-
poem: [string, string];
|
|
9774
|
-
color: string;
|
|
9775
|
-
glyph: string;
|
|
9776
|
-
duration: number;
|
|
9777
|
-
}
|
|
9778
|
-
|
|
9779
|
-
|
|
9780
9956
|
END_OF_FILE_CONTENT
|
|
9781
9957
|
echo "Creating src/vite-env.d.ts..."
|
|
9782
9958
|
cat << 'END_OF_FILE_CONTENT' > "src/vite-env.d.ts"
|
|
@@ -9968,6 +10144,7 @@ export default defineConfig({
|
|
|
9968
10144
|
sendJson(res, 200, buildSiteManifest({
|
|
9969
10145
|
pages: buildState.pages,
|
|
9970
10146
|
schemas: buildState.schemas,
|
|
10147
|
+
submissionSchemas: buildState.submissionSchemas,
|
|
9971
10148
|
siteConfig: buildState.siteConfig,
|
|
9972
10149
|
}));
|
|
9973
10150
|
return true;
|
|
@@ -9986,6 +10163,7 @@ export default defineConfig({
|
|
|
9986
10163
|
slug,
|
|
9987
10164
|
pageConfig,
|
|
9988
10165
|
schemas: buildState.schemas,
|
|
10166
|
+
submissionSchemas: buildState.submissionSchemas,
|
|
9989
10167
|
siteConfig: buildState.siteConfig,
|
|
9990
10168
|
}));
|
|
9991
10169
|
return true;
|
|
@@ -10004,6 +10182,7 @@ export default defineConfig({
|
|
|
10004
10182
|
slug,
|
|
10005
10183
|
pageConfig,
|
|
10006
10184
|
schemas: buildState.schemas,
|
|
10185
|
+
submissionSchemas: buildState.submissionSchemas,
|
|
10007
10186
|
siteConfig: buildState.siteConfig,
|
|
10008
10187
|
}));
|
|
10009
10188
|
return true;
|