@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.
@@ -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.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 '@/types/deploy';
2565
- import { DEPLOY_STEPS } from '@/lib/deploySteps';
2566
- import { startCloudSaveStream } from '@/lib/cloudSaveStream';
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 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'
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 '@/lib/base-schemas';
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 '@/types/deploy';
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 '@/types/deploy';
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 '@/types/deploy';
3963
- import { DEPLOY_STEPS } from '@/lib/deploySteps';
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 '@/lib/utils'
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 "@/lib/utils"
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 "@/lib/utils"
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 '@/lib/utils'
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 "@/lib/utils"
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 '@/lib/utils'
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 '@/lib/utils'
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 "@/lib/utils"
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 "@/lib/utils"
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 "@/lib/utils"
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 "@/lib/utils"
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 '@/lib/utils'
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 '@/lib/utils'
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 "@/lib/utils"
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 "@/lib/utils"
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 "@/lib/utils"
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 "@/lib/utils"
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 '@/lib/utils'
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 "@/lib/utils"
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 '@/lib/utils';
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 "@/lib/utils"
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 "@/lib/utils"
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 "@/lib/utils"
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 "@/lib/utils"
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 "@/lib/utils"
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 "@/lib/utils"
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 "@/lib/utils"
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
- "pages": [
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
- default: return {};
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 '@/lib/base-schemas';
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/utils.ts..."
9681
- cat << 'END_OF_FILE_CONTENT' > "src/lib/utils.ts"
9682
- import { clsx, type ClassValue } from 'clsx';
9683
- import { twMerge } from 'tailwind-merge';
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
- export function cn(...inputs: ClassValue[]) {
9686
- return twMerge(clsx(inputs));
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;