@olonjs/cli 3.0.128 → 3.0.129

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.
@@ -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 ../../specs package.json public/assets/images/plug-graded-square.jpg",
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.117",
1677
+ "@olonjs/core": "^1.0.120",
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"
@@ -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 '@/lib/OlonFormsContext';
2607
+ import { iconMap } from '@/lib/IconResolver';
2575
2608
 
2576
2609
  import tenantCss from './index.css?inline';
2577
2610
 
@@ -2966,6 +2999,7 @@ function setTenantPreviewReady(ready: boolean): void {
2966
2999
  }
2967
3000
 
2968
3001
  function App() {
3002
+ const { states: formStates } = useOlonForms();
2969
3003
  const isCloudMode = Boolean(CLOUD_API_URL && CLOUD_API_KEY);
2970
3004
  const isSave2RepoMode = isCloudMode && SAVE2REPO_ENABLED;
2971
3005
  const isHotSaveMode = isCloudMode && !isSave2RepoMode;
@@ -3413,6 +3447,7 @@ function App() {
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 EmptyTenantViewProps = {
3776
- data?: EmptyTenantData;
3777
- };
3748
+ type Theme = 'dark' | 'light'
3778
3749
 
3779
- export function EmptyTenantView({ data }: EmptyTenantViewProps) {
3780
- const title = data?.title?.trim() || 'Your tenant is empty.';
3781
- const description = data?.description?.trim() || 'Create your first page to start building your site.';
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
- return (
3785
- <main className="min-h-screen flex items-center justify-center bg-background text-foreground px-6">
3786
- <section className="w-full max-w-xl rounded-xl border border-border bg-card p-8 shadow-sm">
3787
- <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
3788
- <p className="mt-3 text-sm text-muted-foreground">{description}</p>
3789
- </section>
3790
- </main>
3791
- );
3762
+ const STORAGE_KEY = 'olon:theme'
3763
+
3764
+ function isTheme(value: unknown): value is Theme {
3765
+ return value === 'dark' || value === 'light'
3792
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'
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..."
@@ -3807,7 +3843,6 @@ import { BaseSectionData } from '@/lib/base-schemas';
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,6 +3856,196 @@ 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 '@/lib/OlonFormsContext';
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 '@/lib/base-schemas';
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
+ END_OF_FILE_CONTENT
4041
+ echo "Creating src/components/form-demo/types.ts..."
4042
+ cat << 'END_OF_FILE_CONTENT' > "src/components/form-demo/types.ts"
4043
+ import { z } from 'zod';
4044
+ import { FormDemoSchema, FormDemoSettingsSchema } from './schema';
4045
+
4046
+ export type FormDemoData = z.infer<typeof FormDemoSchema>;
4047
+ export type FormDemoSettings = z.infer<typeof FormDemoSettingsSchema>;
4048
+
3824
4049
  END_OF_FILE_CONTENT
3825
4050
  mkdir -p "src/components/save-drawer"
3826
4051
  echo "Creating src/components/save-drawer/DeployConnector.tsx..."
@@ -7734,6 +7959,33 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/theme.json"
7734
7959
  }
7735
7960
  END_OF_FILE_CONTENT
7736
7961
  mkdir -p "src/data/pages"
7962
+ echo "Creating src/data/pages/home.json..."
7963
+ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
7964
+ {
7965
+ "id": "home-page",
7966
+ "slug": "home",
7967
+ "meta": {
7968
+ "title": "Home",
7969
+ "description": "OlonJS tenant alpha — form smoke test"
7970
+ },
7971
+ "sections": [
7972
+ {
7973
+ "id": "form-demo-1",
7974
+ "type": "form-demo",
7975
+ "data": {
7976
+ "anchorId": "form-demo",
7977
+ "title": "Contattaci",
7978
+ "description": "Compila il modulo e ti risponderemo al più presto.",
7979
+ "recipientEmail": "test@olonjs.io",
7980
+ "submitLabel": "Invia",
7981
+ "successMessage": "Richiesta inviata con successo.",
7982
+ "icon": "mail"
7983
+ }
7984
+ }
7985
+ ],
7986
+ "global-header": false
7987
+ }
7988
+ END_OF_FILE_CONTENT
7737
7989
  mkdir -p "src/emails"
7738
7990
  echo "Creating src/emails/LeadNotificationEmail.tsx..."
7739
7991
  cat << 'END_OF_FILE_CONTENT' > "src/emails/LeadNotificationEmail.tsx"
@@ -9185,11 +9437,13 @@ cat << 'END_OF_FILE_CONTENT' > "src/lib/ComponentRegistry.tsx"
9185
9437
  import type { SectionType } from '@/types';
9186
9438
  import type { SectionComponentPropsMap } from '@/types';
9187
9439
  import { EmptyTenantView } from '@/components/empty-tenant';
9440
+ import { FormDemoView } from '@/components/form-demo';
9188
9441
 
9189
9442
  export const ComponentRegistry: {
9190
9443
  [K in SectionType]: React.FC<SectionComponentPropsMap[K]>;
9191
9444
  } = {
9192
9445
  'empty-tenant': EmptyTenantView as React.FC<SectionComponentPropsMap['empty-tenant']>,
9446
+ 'form-demo': FormDemoView as React.FC<SectionComponentPropsMap['form-demo']>,
9193
9447
  };
9194
9448
 
9195
9449
  END_OF_FILE_CONTENT
@@ -9207,10 +9461,11 @@ import {
9207
9461
  X,
9208
9462
  Sparkles,
9209
9463
  Zap,
9464
+ Mail,
9210
9465
  type LucideIcon
9211
9466
  } from 'lucide-react';
9212
9467
 
9213
- const iconMap = {
9468
+ export const iconMap = {
9214
9469
  'layers': Layers,
9215
9470
  'github': Github,
9216
9471
  'arrow-right': ArrowRight,
@@ -9221,6 +9476,7 @@ const iconMap = {
9221
9476
  'x': X,
9222
9477
  'sparkles': Sparkles,
9223
9478
  'zap': Zap,
9479
+ 'mail': Mail,
9224
9480
  } as const satisfies Record<string, LucideIcon>;
9225
9481
 
9226
9482
  export type IconName = keyof typeof iconMap;
@@ -9250,15 +9506,46 @@ export const Icon: React.FC<IconProps> = ({ name, size = 20, className }) => {
9250
9506
 
9251
9507
 
9252
9508
 
9509
+ END_OF_FILE_CONTENT
9510
+ echo "Creating src/lib/OlonFormsContext.ts..."
9511
+ cat << 'END_OF_FILE_CONTENT' > "src/lib/OlonFormsContext.ts"
9512
+ import { createContext, useContext } from 'react';
9513
+
9514
+ export type FormStatus = 'idle' | 'submitting' | 'success' | 'error';
9515
+
9516
+ export interface FormState {
9517
+ status: FormStatus;
9518
+ message: string;
9519
+ }
9520
+
9521
+ const DEFAULT_FORM_STATE: FormState = { status: 'idle', message: '' };
9522
+
9523
+ /**
9524
+ * Context holding the submission state of every olon-managed form,
9525
+ * keyed by the form's id attribute (or anchorId).
9526
+ * Provided by App.tsx via useOlonForms().
9527
+ */
9528
+ export const OlonFormsContext = createContext<Record<string, FormState>>({});
9529
+
9530
+ /**
9531
+ * Read the submission state for a specific form.
9532
+ * @param formId - must match the id attribute on the <form> element.
9533
+ */
9534
+ export function useFormState(formId: string): FormState {
9535
+ const states = useContext(OlonFormsContext);
9536
+ return states[formId] ?? DEFAULT_FORM_STATE;
9537
+ }
9538
+
9253
9539
  END_OF_FILE_CONTENT
9254
9540
  echo "Creating src/lib/addSectionConfig.ts..."
9255
9541
  cat << 'END_OF_FILE_CONTENT' > "src/lib/addSectionConfig.ts"
9256
9542
  import type { AddSectionConfig } from '@olonjs/core';
9257
9543
 
9258
- const addableSectionTypes = ['empty-tenant'] as const;
9544
+ const addableSectionTypes = ['empty-tenant', 'form-demo'] as const;
9259
9545
 
9260
9546
  const sectionTypeLabels: Record<string, string> = {
9261
9547
  'empty-tenant': 'Empty Tenant',
9548
+ 'form-demo': 'Form Demo',
9262
9549
  };
9263
9550
 
9264
9551
  function getDefaultSectionData(type: string): Record<string, unknown> {
@@ -9267,9 +9554,17 @@ function getDefaultSectionData(type: string): Record<string, unknown> {
9267
9554
  return {
9268
9555
  title: 'Your tenant is empty.',
9269
9556
  description: 'Create your first page to start building your site.',
9270
- ctaLabel: 'Open Studio',
9271
9557
  };
9272
- default: return {};
9558
+ case 'form-demo':
9559
+ return {
9560
+ title: 'Contattaci',
9561
+ description: 'Compila il modulo e ti risponderemo al più presto.',
9562
+ recipientEmail: '',
9563
+ submitLabel: 'Invia',
9564
+ successMessage: 'Richiesta inviata con successo.',
9565
+ };
9566
+ default:
9567
+ return {};
9273
9568
  }
9274
9569
  }
9275
9570
 
@@ -9322,6 +9617,16 @@ export const CtaSchema = z.object({
9322
9617
  variant: z.enum(['primary', 'secondary', 'accent']).default('primary').describe('ui:select'),
9323
9618
  });
9324
9619
 
9620
+ /**
9621
+ * Mixin for any section capsule that includes a contact form.
9622
+ * Merge into the section data schema to expose recipientEmail
9623
+ * as an editable field in the Studio inspector.
9624
+ * The View must set data-olon-recipient={data.recipientEmail} on the <form>.
9625
+ */
9626
+ export const WithFormRecipient = z.object({
9627
+ recipientEmail: z.string().optional().describe('ui:text'),
9628
+ });
9629
+
9325
9630
  END_OF_FILE_CONTENT
9326
9631
  echo "Creating src/lib/cloudSaveStream.ts..."
9327
9632
  cat << 'END_OF_FILE_CONTENT' > "src/lib/cloudSaveStream.ts"
@@ -9572,9 +9877,11 @@ END_OF_FILE_CONTENT
9572
9877
  echo "Creating src/lib/schemas.ts..."
9573
9878
  cat << 'END_OF_FILE_CONTENT' > "src/lib/schemas.ts"
9574
9879
  import { EmptyTenantSchema } from '@/components/empty-tenant';
9880
+ import { FormDemoSchema } from '@/components/form-demo';
9575
9881
 
9576
9882
  export const SECTION_SCHEMAS = {
9577
9883
  'empty-tenant': EmptyTenantSchema,
9884
+ 'form-demo': FormDemoSchema,
9578
9885
  } as const;
9579
9886
 
9580
9887
  export type SectionType = keyof typeof SECTION_SCHEMAS;
@@ -9676,6 +9983,137 @@ export function useFormSubmit({ source, tenantId }: UseFormSubmitOptions) {
9676
9983
 
9677
9984
  return { submit, status, message, reset };
9678
9985
  }
9986
+ END_OF_FILE_CONTENT
9987
+ echo "Creating src/lib/useOlonForms.ts..."
9988
+ cat << 'END_OF_FILE_CONTENT' > "src/lib/useOlonForms.ts"
9989
+ import { useCallback, useEffect, useState } from 'react';
9990
+ import type { FormState } from './OlonFormsContext';
9991
+
9992
+ const API_BASE =
9993
+ (import.meta.env.VITE_OLONJS_CLOUD_URL as string | undefined) ??
9994
+ (import.meta.env.VITE_JSONPAGES_CLOUD_URL as string | undefined);
9995
+
9996
+ const API_KEY =
9997
+ (import.meta.env.VITE_OLONJS_API_KEY as string | undefined) ??
9998
+ (import.meta.env.VITE_JSONPAGES_API_KEY as string | undefined);
9999
+
10000
+ interface UseOlonFormsOptions {
10001
+ /** Override the submit endpoint. Defaults to VITE_OLONJS_CLOUD_URL/forms/submit */
10002
+ endpoint?: string;
10003
+ }
10004
+
10005
+ /**
10006
+ * Mount once in App.tsx. Scans the DOM for all <form data-olon-recipient="...">
10007
+ * elements and attaches submit handlers. Returns per-form states to be provided
10008
+ * via OlonFormsContext.Provider.
10009
+ *
10010
+ * Views consume state via useFormState(formId) — no direct coupling to this hook.
10011
+ */
10012
+ export function useOlonForms(options?: UseOlonFormsOptions): { states: Record<string, FormState> } {
10013
+ const [states, setStates] = useState<Record<string, FormState>>({});
10014
+
10015
+ const setFormState = useCallback((formId: string, state: FormState) => {
10016
+ setStates((prev) => ({ ...prev, [formId]: state }));
10017
+ }, []);
10018
+
10019
+ useEffect(() => {
10020
+ const resolvedBase = options?.endpoint
10021
+ ? options.endpoint.replace(/\/$/, '')
10022
+ : API_BASE
10023
+ ? API_BASE.replace(/\/$/, '')
10024
+ : null;
10025
+
10026
+ if (!resolvedBase || !API_KEY) {
10027
+ console.warn('[useOlonForms] Missing API endpoint or key — forms will not submit.');
10028
+ return;
10029
+ }
10030
+
10031
+ const endpoint = resolvedBase.endsWith('/forms/submit')
10032
+ ? resolvedBase
10033
+ : `${resolvedBase}/forms/submit`;
10034
+
10035
+ const forms = Array.from(
10036
+ document.querySelectorAll<HTMLFormElement>('form[data-olon-recipient]')
10037
+ );
10038
+
10039
+ const controllers: AbortController[] = [];
10040
+
10041
+ async function handleSubmit(form: HTMLFormElement, event: SubmitEvent) {
10042
+ event.preventDefault();
10043
+
10044
+ const formId = form.id || form.dataset.olonRecipient || 'olon-form';
10045
+ const recipientEmail = form.dataset.olonRecipient ?? '';
10046
+
10047
+ setFormState(formId, { status: 'submitting', message: 'Invio in corso...' });
10048
+
10049
+ const raw: Record<string, string> = {};
10050
+ new FormData(form).forEach((value, key) => {
10051
+ raw[key] = String(value).trim();
10052
+ });
10053
+
10054
+ const payload = {
10055
+ ...raw,
10056
+ recipientEmail,
10057
+ page: window.location.pathname,
10058
+ source: 'olon-form',
10059
+ submittedAt: new Date().toISOString(),
10060
+ };
10061
+
10062
+ const controller = new AbortController();
10063
+ controllers.push(controller);
10064
+
10065
+ try {
10066
+ const response = await fetch(endpoint, {
10067
+ method: 'POST',
10068
+ headers: {
10069
+ Authorization: `Bearer ${API_KEY}`,
10070
+ 'Content-Type': 'application/json',
10071
+ 'Idempotency-Key': `form-${formId}-${Date.now()}`,
10072
+ },
10073
+ body: JSON.stringify(payload),
10074
+ signal: controller.signal,
10075
+ });
10076
+
10077
+ const body = (await response.json().catch(() => ({}))) as {
10078
+ error?: string;
10079
+ code?: string;
10080
+ };
10081
+
10082
+ if (!response.ok) {
10083
+ throw new Error(body.error ?? body.code ?? `Submit failed (${response.status})`);
10084
+ }
10085
+
10086
+ setFormState(formId, {
10087
+ status: 'success',
10088
+ message: 'Richiesta inviata con successo.',
10089
+ });
10090
+ form.reset();
10091
+ } catch (error: unknown) {
10092
+ if (error instanceof Error && error.name === 'AbortError') return;
10093
+ const message =
10094
+ error instanceof Error ? error.message : 'Invio non riuscito. Riprova.';
10095
+ setFormState(formId, { status: 'error', message });
10096
+ }
10097
+ }
10098
+
10099
+ type Listener = { form: HTMLFormElement; handler: (e: Event) => void };
10100
+ const listeners: Listener[] = [];
10101
+
10102
+ forms.forEach((form) => {
10103
+ const handler = (e: Event) => void handleSubmit(form, e as SubmitEvent);
10104
+ form.addEventListener('submit', handler);
10105
+ listeners.push({ form, handler });
10106
+ });
10107
+
10108
+ return () => {
10109
+ controllers.forEach((c) => c.abort());
10110
+ listeners.forEach(({ form, handler }) => form.removeEventListener('submit', handler));
10111
+ };
10112
+ }, [options?.endpoint, setFormState]);
10113
+
10114
+ return { states };
10115
+ }
10116
+
9679
10117
  END_OF_FILE_CONTENT
9680
10118
  echo "Creating src/lib/utils.ts..."
9681
10119
  cat << 'END_OF_FILE_CONTENT' > "src/lib/utils.ts"
@@ -9741,17 +10179,21 @@ mkdir -p "src/types"
9741
10179
  echo "Creating src/types.ts..."
9742
10180
  cat << 'END_OF_FILE_CONTENT' > "src/types.ts"
9743
10181
  import type { EmptyTenantData, EmptyTenantSettings } from '@/components/empty-tenant';
10182
+ import type { FormDemoData, FormDemoSettings } from '@/components/form-demo';
9744
10183
 
9745
10184
  export type SectionComponentPropsMap = {
9746
10185
  'empty-tenant': { data: EmptyTenantData; settings?: EmptyTenantSettings };
10186
+ 'form-demo': { data: FormDemoData; settings?: FormDemoSettings };
9747
10187
  };
9748
10188
 
9749
10189
  declare module '@olonjs/core' {
9750
10190
  export interface SectionDataRegistry {
9751
10191
  'empty-tenant': EmptyTenantData;
10192
+ 'form-demo': FormDemoData;
9752
10193
  }
9753
10194
  export interface SectionSettingsRegistry {
9754
10195
  'empty-tenant': EmptyTenantSettings;
10196
+ 'form-demo': FormDemoSettings;
9755
10197
  }
9756
10198
  }
9757
10199