@saastro/forms 0.1.3 → 0.2.1
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/cli.js +129 -2
- package/dist/{DateRenderers-3JUQNLKJ.js → DateRenderers-QKRBXFC6.js} +2 -2
- package/dist/{chunk-GHDCNAWC.js → chunk-G2ZSBYVS.js} +4 -2
- package/dist/{chunk-GHDCNAWC.js.map → chunk-G2ZSBYVS.js.map} +1 -1
- package/dist/index.d.ts +170 -3
- package/dist/index.js +396 -2
- package/dist/index.js.map +1 -1
- package/package.json +11 -5
- /package/dist/{DateRenderers-3JUQNLKJ.js.map → DateRenderers-QKRBXFC6.js.map} +0 -0
package/cli.js
CHANGED
|
@@ -37,6 +37,130 @@ function getPackageRoot() {
|
|
|
37
37
|
return __dirname;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// ═══════════════════════════════════════════════════════════
|
|
41
|
+
// PEER DEPENDENCIES — keep in sync with packages/forms/package.json
|
|
42
|
+
// ═══════════════════════════════════════════════════════════
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Peer dependencies that @saastro/forms requires the consumer to install.
|
|
46
|
+
* Keep this in sync with the `peerDependencies` block of
|
|
47
|
+
* packages/forms/package.json.
|
|
48
|
+
*
|
|
49
|
+
* Note: react and react-dom are intentionally excluded — any React project
|
|
50
|
+
* already has them, and forcing a version range here would clash with
|
|
51
|
+
* apps pinned to React 18 or 19. The published `peerDependencies` keeps
|
|
52
|
+
* the range there.
|
|
53
|
+
*/
|
|
54
|
+
const PEER_DEPENDENCIES = {
|
|
55
|
+
zod: '^4.0.0',
|
|
56
|
+
'react-hook-form': '^7.67.0',
|
|
57
|
+
'react-day-picker': '^9.11.3',
|
|
58
|
+
'date-fns': '^4.1.0',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const PM_INSTALL_COMMAND = {
|
|
62
|
+
pnpm: 'pnpm add',
|
|
63
|
+
bun: 'bun add',
|
|
64
|
+
yarn: 'yarn add',
|
|
65
|
+
npm: 'npm install',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Detect the package manager used by the consumer project by scanning for
|
|
70
|
+
* lockfiles in priority order: pnpm > bun > yarn > npm. Falls back to npm
|
|
71
|
+
* when no lockfile is present (a fresh project).
|
|
72
|
+
*/
|
|
73
|
+
function detectPackageManager(consumerRoot = process.cwd()) {
|
|
74
|
+
const lockfiles = [
|
|
75
|
+
['pnpm-lock.yaml', 'pnpm'],
|
|
76
|
+
['bun.lock', 'bun'],
|
|
77
|
+
['bun.lockb', 'bun'],
|
|
78
|
+
['yarn.lock', 'yarn'],
|
|
79
|
+
['package-lock.json', 'npm'],
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
for (const [lockfile, pm] of lockfiles) {
|
|
83
|
+
if (existsSync(join(consumerRoot, lockfile))) {
|
|
84
|
+
return pm;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return 'npm';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Read the consumer's package.json and return a flat map of every
|
|
92
|
+
* declared dependency (across dependencies, devDependencies and
|
|
93
|
+
* peerDependencies). Used to detect which peer-deps still need installing.
|
|
94
|
+
*/
|
|
95
|
+
function readConsumerDependencies(consumerRoot = process.cwd()) {
|
|
96
|
+
const pkgJsonPath = join(consumerRoot, 'package.json');
|
|
97
|
+
if (!existsSync(pkgJsonPath)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
103
|
+
return {
|
|
104
|
+
...(pkg.dependencies || {}),
|
|
105
|
+
...(pkg.devDependencies || {}),
|
|
106
|
+
...(pkg.peerDependencies || {}),
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.warn(`⚠ Error al leer package.json: ${error.message}`);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Verify that every peer dependency of @saastro/forms is declared in the
|
|
116
|
+
* consumer's package.json. If any are missing, install them using the
|
|
117
|
+
* detected package manager (pnpm/bun/yarn/npm).
|
|
118
|
+
*
|
|
119
|
+
* Returns true if nothing needed to be installed (everything was already
|
|
120
|
+
* present); false if a missing dep was either installed or could not be
|
|
121
|
+
* installed.
|
|
122
|
+
*/
|
|
123
|
+
function checkAndInstallPeerDeps(consumerRoot = process.cwd()) {
|
|
124
|
+
console.log('\n🔍 Verificando peer dependencies de @saastro/forms...');
|
|
125
|
+
|
|
126
|
+
const declared = readConsumerDependencies(consumerRoot);
|
|
127
|
+
if (declared === null) {
|
|
128
|
+
console.warn(
|
|
129
|
+
'⚠ No se encontró package.json o no se pudo parsear. Saltando peer-deps.',
|
|
130
|
+
);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const missing = Object.entries(PEER_DEPENDENCIES).filter(
|
|
135
|
+
([name]) => !declared[name],
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (missing.length === 0) {
|
|
139
|
+
console.log('✓ Todas las peer dependencies están declaradas.');
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const pm = detectPackageManager(consumerRoot);
|
|
144
|
+
const cmd = PM_INSTALL_COMMAND[pm];
|
|
145
|
+
const packagesArg = missing.map(([name, version]) => `${name}@${version}`).join(' ');
|
|
146
|
+
|
|
147
|
+
console.log(
|
|
148
|
+
`\n📦 Faltan ${missing.length} peer dependenc${missing.length === 1 ? 'y' : 'ies'}: ${missing.map(([n]) => n).join(', ')}`,
|
|
149
|
+
);
|
|
150
|
+
console.log(` Package manager detectado: ${pm}`);
|
|
151
|
+
console.log(` Comando: ${cmd} ${packagesArg}\n`);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
execSync(`${cmd} ${packagesArg}`, { stdio: 'inherit', cwd: consumerRoot });
|
|
155
|
+
console.log(`\n✓ Peer dependencies instaladas correctamente.`);
|
|
156
|
+
return false;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error(`\n❌ Error al instalar peer dependencies: ${error.message}`);
|
|
159
|
+
console.error(` Ejecuta manualmente: ${cmd} ${packagesArg}`);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
40
164
|
// Componentes shadcn requeridos por @saastro/forms
|
|
41
165
|
const SHADCN_COMPONENTS = [
|
|
42
166
|
// Core
|
|
@@ -615,7 +739,7 @@ function initProject() {
|
|
|
615
739
|
console.log('');
|
|
616
740
|
console.log('Done! Next steps:');
|
|
617
741
|
console.log('');
|
|
618
|
-
console.log(' 1. Install required shadcn components:');
|
|
742
|
+
console.log(' 1. Install required shadcn components + peer dependencies:');
|
|
619
743
|
console.log(' npx @saastro/forms install-deps');
|
|
620
744
|
console.log('');
|
|
621
745
|
console.log(' 2. Use the form in your page:');
|
|
@@ -673,6 +797,9 @@ function main() {
|
|
|
673
797
|
|
|
674
798
|
// Verificar e instalar componentes shadcn
|
|
675
799
|
checkAndInstallComponents(config);
|
|
800
|
+
|
|
801
|
+
// Verificar e instalar peer dependencies (zod, react-hook-form, etc.)
|
|
802
|
+
checkAndInstallPeerDeps();
|
|
676
803
|
return;
|
|
677
804
|
}
|
|
678
805
|
|
|
@@ -712,7 +839,7 @@ function main() {
|
|
|
712
839
|
console.log('');
|
|
713
840
|
console.log('Commands:');
|
|
714
841
|
console.log(' init Generate zero-config example forms (FormBuilder + JSON)');
|
|
715
|
-
console.log(' install-deps Install
|
|
842
|
+
console.log(' install-deps Install required shadcn/ui components and peer dependencies');
|
|
716
843
|
console.log(' add <name> Install a specific component (legacy)');
|
|
717
844
|
console.log('');
|
|
718
845
|
console.log('Quick start:');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MissingComponentFallback,
|
|
3
3
|
useComponents
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-G2ZSBYVS.js";
|
|
5
5
|
|
|
6
6
|
// src/factory/DateRenderers.tsx
|
|
7
7
|
import { ChevronDown } from "lucide-react";
|
|
@@ -136,4 +136,4 @@ var DateRenderers = ({
|
|
|
136
136
|
export {
|
|
137
137
|
DateRenderers
|
|
138
138
|
};
|
|
139
|
-
//# sourceMappingURL=DateRenderers-
|
|
139
|
+
//# sourceMappingURL=DateRenderers-QKRBXFC6.js.map
|
|
@@ -203,8 +203,10 @@ function MissingComponentFallback({
|
|
|
203
203
|
/* @__PURE__ */ jsx2("code", { className: "block mt-1 text-amber-700 dark:text-amber-300 break-words", children: missingComponents.join(", ") }),
|
|
204
204
|
/* @__PURE__ */ jsxs("div", { className: "mt-3 bg-amber-100 dark:bg-amber-900/30 rounded px-3 py-2 overflow-x-auto", children: [
|
|
205
205
|
/* @__PURE__ */ jsx2("p", { className: "text-xs text-amber-600 dark:text-amber-400 mb-1", children: "Install with:" }),
|
|
206
|
-
/* @__PURE__ */ jsx2("code", { className: "text-xs font-mono text-amber-800 dark:text-amber-200 whitespace-nowrap", children:
|
|
206
|
+
/* @__PURE__ */ jsx2("code", { className: "text-xs font-mono text-amber-800 dark:text-amber-200 whitespace-nowrap", children: "npx @saastro/forms install-deps" })
|
|
207
207
|
] }),
|
|
208
|
+
/* @__PURE__ */ jsx2("p", { className: "mt-2 text-xs text-amber-600 dark:text-amber-400", children: "Or install the specific shadcn components manually:" }),
|
|
209
|
+
/* @__PURE__ */ jsx2("code", { className: "block mt-1 text-xs font-mono text-amber-800 dark:text-amber-200 bg-amber-100 dark:bg-amber-900/30 rounded px-2 py-1 overflow-x-auto whitespace-nowrap", children: installCmd }),
|
|
208
210
|
/* @__PURE__ */ jsxs("p", { className: "mt-2 text-xs text-amber-600 dark:text-amber-400", children: [
|
|
209
211
|
"Or provide components via the",
|
|
210
212
|
" ",
|
|
@@ -244,4 +246,4 @@ export {
|
|
|
244
246
|
MissingComponentFallback,
|
|
245
247
|
createMissingComponentPlaceholder
|
|
246
248
|
};
|
|
247
|
-
//# sourceMappingURL=chunk-
|
|
249
|
+
//# sourceMappingURL=chunk-G2ZSBYVS.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/context/ComponentContext.tsx","../src/components/MissingComponentFallback.tsx"],"sourcesContent":["import React, { createContext, useContext, useMemo } from 'react';\nimport type { ComponentRegistry, PartialComponentRegistry } from '../types/components';\n\n/**\n * ============================================\n * COMPONENT CONTEXT - Dependency Injection\n * ============================================\n *\n * Este contexto permite inyectar componentes UI en el plugin,\n * haciendo que sea agnóstico de la librería UI utilizada.\n *\n * Supports two modes:\n * 1. Legacy mode: Full ComponentRegistry via ComponentProvider (required all components)\n * 2. Zero-config mode: Partial registry via InternalComponentContext (Form-level)\n */\n\n// Legacy context for full registry (backwards compatible)\nconst ComponentContext = createContext<ComponentRegistry | null>(null);\n\n// Internal context for Form-level partial registry (zero-config mode)\nconst InternalComponentContext = createContext<PartialComponentRegistry | null>(null);\n\nexport interface ComponentProviderProps {\n /** Registry completo de componentes UI */\n components: ComponentRegistry;\n /** Componentes hijos que tendrán acceso al registry */\n children: React.ReactNode;\n}\n\n/**\n * Provider que inyecta los componentes UI en el árbol de React.\n * This is the legacy provider that requires a full ComponentRegistry.\n *\n * @example\n * ```tsx\n * import { ComponentProvider, createComponentRegistry } from \"@saastro/forms\";\n * import * as shadcn from \"@/lib/form-components\";\n *\n * const registry = createComponentRegistry(shadcn);\n *\n * function App() {\n * return (\n * <ComponentProvider components={registry}>\n * <MyForm />\n * </ComponentProvider>\n * );\n * }\n * ```\n *\n * @deprecated Consider using the zero-config approach with inline components prop on Form\n */\nexport const ComponentProvider: React.FC<ComponentProviderProps> = ({ components, children }) => {\n return <ComponentContext.Provider value={components}>{children}</ComponentContext.Provider>;\n};\n\nexport interface InternalComponentProviderProps {\n /** Partial registry de componentes UI */\n components: PartialComponentRegistry;\n /** Componentes hijos que tendrán acceso al registry */\n children: React.ReactNode;\n}\n\n/**\n * Internal provider used by Form component for zero-config mode.\n * Accepts a partial registry - only the components needed for the form.\n *\n * @internal\n */\nexport const InternalComponentProvider: React.FC<InternalComponentProviderProps> = ({\n components,\n children,\n}) => {\n return (\n <InternalComponentContext.Provider value={components}>\n {children}\n </InternalComponentContext.Provider>\n );\n};\n\n/**\n * Hook para acceder al registry de componentes inyectados.\n *\n * Supports both legacy (ComponentProvider) and zero-config (InternalComponentContext) modes.\n * Legacy mode takes precedence for backwards compatibility.\n *\n * @throws Error si se usa fuera de cualquier provider\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { Input, Button } = useComponents();\n *\n * return (\n * <div>\n * <Input placeholder=\"Email\" />\n * <Button>Submit</Button>\n * </div>\n * );\n * }\n * ```\n */\nexport const useComponents = (): ComponentRegistry => {\n const legacyComponents = useContext(ComponentContext);\n const internalComponents = useContext(InternalComponentContext);\n\n // Legacy mode takes precedence\n if (legacyComponents) {\n return legacyComponents;\n }\n\n // Zero-config mode\n if (internalComponents) {\n // Return partial as full (consumers should handle missing components)\n return internalComponents as ComponentRegistry;\n }\n\n throw new Error(\n 'useComponents must be used within a Form component or ComponentProvider.\\n\\n' +\n 'Option 1 (Recommended - Zero Config):\\n' +\n ' <Form config={config} components={{ Input, Button }} />\\n\\n' +\n 'Option 2 (Legacy):\\n' +\n ' <ComponentProvider components={fullRegistry}>\\n' +\n ' <Form config={config} />\\n' +\n ' </ComponentProvider>\\n\\n' +\n 'See documentation: https://forms.saastro.io/getting-started',\n );\n};\n\n/**\n * Hook to access partial components safely (may have missing components).\n * Returns null for components that are not provided.\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const components = usePartialComponents();\n *\n * if (!components.Input) {\n * return <MissingComponentFallback missingComponents={['Input']} />;\n * }\n *\n * return <components.Input placeholder=\"Email\" />;\n * }\n * ```\n */\nexport const usePartialComponents = (): PartialComponentRegistry => {\n const legacyComponents = useContext(ComponentContext);\n const internalComponents = useContext(InternalComponentContext);\n\n if (legacyComponents) {\n return legacyComponents;\n }\n\n if (internalComponents) {\n return internalComponents;\n }\n\n return {};\n};\n\n/**\n * HOC para envolver componentes que necesitan acceso a los componentes UI.\n *\n * @example\n * ```tsx\n * const MyFormWithComponents = withComponents(MyForm);\n * ```\n */\nexport function withComponents<P extends object>(Component: React.ComponentType<P>): React.FC<P> {\n const WrappedComponent: React.FC<P> = (props) => {\n const components = useComponents();\n return <Component {...props} components={components} />;\n };\n\n WrappedComponent.displayName = `withComponents(${Component.displayName || Component.name || 'Component'})`;\n\n return WrappedComponent;\n}\n\n/**\n * Hook para verificar si estamos dentro de un ComponentProvider.\n * Útil para componentes que pueden funcionar con o sin provider.\n *\n * @returns true si hay un provider activo (legacy or internal), false en caso contrario\n */\nexport const useHasComponentProvider = (): boolean => {\n const legacyComponents = useContext(ComponentContext);\n const internalComponents = useContext(InternalComponentContext);\n return legacyComponents !== null || internalComponents !== null;\n};\n\n/**\n * Hook to check which mode we're operating in\n */\nexport const useComponentMode = (): 'legacy' | 'zero-config' | 'none' => {\n const legacyComponents = useContext(ComponentContext);\n const internalComponents = useContext(InternalComponentContext);\n\n if (legacyComponents) return 'legacy';\n if (internalComponents) return 'zero-config';\n return 'none';\n};\n\n/**\n * Merge multiple component registries with later ones taking precedence\n */\nexport function mergeComponentRegistries(\n ...registries: Array<PartialComponentRegistry | undefined>\n): PartialComponentRegistry {\n const result: PartialComponentRegistry = {};\n\n for (const registry of registries) {\n if (registry) {\n for (const [key, value] of Object.entries(registry)) {\n if (value !== undefined) {\n (result as Record<string, unknown>)[key] = value;\n }\n }\n }\n }\n\n return result;\n}\n\n// ═══════════════════════════════════════════════════════════\n// GLOB-BASED AUTO-DISCOVERY (Recommended for Vite projects)\n// ═══════════════════════════════════════════════════════════\n\n/**\n * Type for Vite's import.meta.glob result.\n * Accepts the actual Vite return type which is Record<string, unknown>.\n */\nexport type GlobModules = Record<string, unknown>;\n\n/**\n * Convert a file path to PascalCase component name\n * @example\n * '/components/ui/button.tsx' -> 'Button'\n * '/components/ui/radio-group.tsx' -> 'RadioGroup'\n * '/components/ui/input-otp.tsx' -> 'InputOTP'\n */\nfunction pathToComponentName(path: string): string {\n // Extract filename without extension\n const filename =\n path\n .split('/')\n .pop()\n ?.replace(/\\.(tsx?|jsx?)$/, '') || '';\n\n // Special cases for common naming patterns\n const specialCases: Record<string, string> = {\n 'input-otp': 'InputOTP',\n otp: 'OTP',\n };\n\n if (specialCases[filename]) {\n return specialCases[filename];\n }\n\n // Convert kebab-case to PascalCase\n return filename\n .split('-')\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join('');\n}\n\n/**\n * Parse Vite's import.meta.glob result into a component registry.\n *\n * This function extracts all named exports from the glob modules\n * and creates a flat registry of components.\n *\n * @param modules - Result of import.meta.glob('@/components/ui/*.tsx', { eager: true })\n * @returns Component registry ready to use with Form\n *\n * @example\n * ```tsx\n * const modules = import.meta.glob('@/components/ui/*.tsx', { eager: true });\n * const components = parseGlobModules(modules);\n * // { Button, Input, Label, Select, SelectTrigger, ... }\n * ```\n */\nexport function parseGlobModules(modules: GlobModules): PartialComponentRegistry {\n const registry: Record<string, React.ComponentType<unknown>> = {};\n\n for (const [path, mod] of Object.entries(modules)) {\n // Skip if module is not an object (shouldn't happen with eager: true)\n if (typeof mod !== 'object' || mod === null) continue;\n\n // Get the base component name from file path\n const baseName = pathToComponentName(path);\n\n // Extract all named exports that look like React components\n for (const [exportName, exportValue] of Object.entries(mod as Record<string, unknown>)) {\n // Skip non-component exports (like types, constants, etc.)\n if (\n typeof exportValue === 'function' ||\n (typeof exportValue === 'object' && exportValue !== null && '$$typeof' in exportValue)\n ) {\n // Use the export name directly (Button, SelectTrigger, etc.)\n if (exportName !== 'default') {\n registry[exportName] = exportValue as React.ComponentType<unknown>;\n } else {\n // For default exports, use the file name\n registry[baseName] = exportValue as React.ComponentType<unknown>;\n }\n }\n }\n }\n\n return registry as PartialComponentRegistry;\n}\n\nexport interface FormComponentsProviderProps {\n /**\n * Glob modules from import.meta.glob.\n * Use { eager: true } for synchronous loading.\n *\n * @example\n * ```tsx\n * <FormComponentsProvider\n * components={import.meta.glob('@/components/ui/*.tsx', { eager: true })}\n * >\n * ```\n */\n components: GlobModules;\n /** Child components */\n children: React.ReactNode;\n}\n\n/**\n * Provider for auto-discovered shadcn components using Vite's import.meta.glob.\n *\n * Place this once at your app's root layout. Forms will automatically\n * use any shadcn components installed in your project.\n *\n * @example\n * ```tsx\n * // app/layout.tsx or _app.tsx - ONE TIME SETUP\n * import { FormComponentsProvider } from '@saastro/forms';\n *\n * export default function RootLayout({ children }) {\n * return (\n * <FormComponentsProvider\n * components={import.meta.glob('@/components/ui/*.tsx', { eager: true })}\n * >\n * {children}\n * </FormComponentsProvider>\n * );\n * }\n * ```\n *\n * Then use Form anywhere without passing components:\n * ```tsx\n * <Form config={config} />\n * ```\n *\n * If a component is missing, a helpful message will appear\n * with installation instructions.\n */\nexport const FormComponentsProvider: React.FC<FormComponentsProviderProps> = ({\n components: globModules,\n children,\n}) => {\n // Parse glob modules into component registry (memoized)\n const registry = useMemo(() => parseGlobModules(globModules), [globModules]);\n\n return (\n <InternalComponentContext.Provider value={registry}>\n {children}\n </InternalComponentContext.Provider>\n );\n};\n","import React from 'react';\n\n/**\n * Props for MissingComponentFallback\n */\nexport interface MissingComponentFallbackProps {\n /** Name of the field that requires the missing component */\n fieldName: string;\n /** Type of the field (e.g., 'text', 'select', 'date') */\n fieldType: string;\n /** List of missing component names */\n missingComponents: string[];\n /** Optional custom message */\n message?: string;\n /** Optional className for layout (e.g., column span classes) */\n className?: string;\n}\n\n/**\n * Generates the npx shadcn command for installing missing components\n */\nfunction getInstallCommand(missingComponents: string[]): string {\n // Map component names to their shadcn package names\n const packageMap: Record<string, string> = {\n Input: 'input',\n Textarea: 'textarea',\n Button: 'button',\n Label: 'label',\n Checkbox: 'checkbox',\n Switch: 'switch',\n RadioGroup: 'radio-group',\n RadioGroupItem: 'radio-group',\n Select: 'select',\n SelectTrigger: 'select',\n SelectContent: 'select',\n SelectItem: 'select',\n SelectValue: 'select',\n NativeSelect: 'native-select',\n Slider: 'slider',\n Popover: 'popover',\n PopoverTrigger: 'popover',\n PopoverContent: 'popover',\n Tooltip: 'tooltip',\n TooltipTrigger: 'tooltip',\n TooltipContent: 'tooltip',\n TooltipProvider: 'tooltip',\n Separator: 'separator',\n Dialog: 'dialog',\n DialogTrigger: 'dialog',\n DialogContent: 'dialog',\n DialogHeader: 'dialog',\n DialogTitle: 'dialog',\n DialogDescription: 'dialog',\n Command: 'command',\n CommandInput: 'command',\n CommandList: 'command',\n CommandEmpty: 'command',\n CommandGroup: 'command',\n CommandItem: 'command',\n InputOTP: 'input-otp',\n InputOTPGroup: 'input-otp',\n InputOTPSlot: 'input-otp',\n Accordion: 'accordion',\n AccordionItem: 'accordion',\n AccordionTrigger: 'accordion',\n AccordionContent: 'accordion',\n Calendar: 'calendar',\n FormField: 'form',\n FormControl: 'form',\n Field: 'field',\n FieldLabel: 'field',\n FieldDescription: 'field',\n FieldError: 'field',\n };\n\n // Get unique package names\n const packages = new Set<string>();\n for (const component of missingComponents) {\n const pkg = packageMap[component];\n if (pkg) {\n packages.add(pkg);\n }\n }\n\n const packageList = Array.from(packages).join(' ');\n return `npx shadcn@latest add ${packageList}`;\n}\n\n/**\n * Fallback component displayed when required UI components are missing.\n * Shows a helpful warning with installation instructions.\n *\n * @example\n * ```tsx\n * <MissingComponentFallback\n * fieldName=\"birthdate\"\n * fieldType=\"date\"\n * missingComponents={['Calendar', 'Popover']}\n * />\n * ```\n */\nexport function MissingComponentFallback({\n fieldName,\n fieldType,\n missingComponents,\n message,\n className,\n}: MissingComponentFallbackProps): React.ReactElement {\n const installCmd = getInstallCommand(missingComponents);\n\n return (\n <div\n className={`border-2 border-dashed border-amber-400 bg-amber-50 dark:bg-amber-950/20 rounded-lg p-4 my-2 ${className || ''}`}\n role=\"alert\"\n aria-live=\"polite\"\n >\n <div className=\"flex items-start gap-3\">\n {/* Warning Icon */}\n <span\n className=\"text-amber-600 dark:text-amber-400 text-xl flex-shrink-0\"\n aria-hidden=\"true\"\n >\n ⚠️\n </span>\n\n <div className=\"text-sm min-w-0\">\n {/* Main message */}\n <p className=\"font-medium text-amber-800 dark:text-amber-200\">\n {message || (\n <>\n Field "{fieldName}" ({fieldType}) requires missing components:\n </>\n )}\n </p>\n\n {/* Missing components list */}\n <code className=\"block mt-1 text-amber-700 dark:text-amber-300 break-words\">\n {missingComponents.join(', ')}\n </code>\n\n {/* Install command */}\n <div className=\"mt-3 bg-amber-100 dark:bg-amber-900/30 rounded px-3 py-2 overflow-x-auto\">\n <p className=\"text-xs text-amber-600 dark:text-amber-400 mb-1\">Install with:</p>\n <code className=\"text-xs font-mono text-amber-800 dark:text-amber-200 whitespace-nowrap\">\n {installCmd}\n </code>\n </div>\n\n {/* Alternative: provide components prop */}\n <p className=\"mt-2 text-xs text-amber-600 dark:text-amber-400\">\n Or provide components via the{' '}\n <code className=\"bg-amber-100 dark:bg-amber-900/50 px-1 rounded\">components</code> prop:\n </p>\n <pre className=\"mt-1 text-xs bg-amber-100 dark:bg-amber-900/30 rounded px-2 py-1 overflow-x-auto\">\n <code>{`<Form config={config} components={{ ${missingComponents[0]}: MyCustom${missingComponents[0]} }} />`}</code>\n </pre>\n </div>\n </div>\n </div>\n );\n}\n\n/**\n * Creates a placeholder component that renders the MissingComponentFallback\n * Can be used as a drop-in replacement for missing components\n */\nexport function createMissingComponentPlaceholder(\n componentName: string,\n fieldType: string,\n): React.FC<{ name?: string }> {\n const Placeholder: React.FC<{ name?: string }> = ({ name = 'unknown' }) => (\n <MissingComponentFallback\n fieldName={name}\n fieldType={fieldType}\n missingComponents={[componentName]}\n />\n );\n Placeholder.displayName = `Missing${componentName}`;\n return Placeholder;\n}\n"],"mappings":";AAAA,SAAgB,eAAe,YAAY,eAAe;AAoDjD;AAnCT,IAAM,mBAAmB,cAAwC,IAAI;AAGrE,IAAM,2BAA2B,cAA+C,IAAI;AA+B7E,IAAM,oBAAsD,CAAC,EAAE,YAAY,SAAS,MAAM;AAC/F,SAAO,oBAAC,iBAAiB,UAAjB,EAA0B,OAAO,YAAa,UAAS;AACjE;AAeO,IAAM,4BAAsE,CAAC;AAAA,EAClF;AAAA,EACA;AACF,MAAM;AACJ,SACE,oBAAC,yBAAyB,UAAzB,EAAkC,OAAO,YACvC,UACH;AAEJ;AAwBO,IAAM,gBAAgB,MAAyB;AACpD,QAAM,mBAAmB,WAAW,gBAAgB;AACpD,QAAM,qBAAqB,WAAW,wBAAwB;AAG9D,MAAI,kBAAkB;AACpB,WAAO;AAAA,EACT;AAGA,MAAI,oBAAoB;AAEtB,WAAO;AAAA,EACT;AAEA,QAAM,IAAI;AAAA,IACR;AAAA,EAQF;AACF;AAmBO,IAAM,uBAAuB,MAAgC;AAClE,QAAM,mBAAmB,WAAW,gBAAgB;AACpD,QAAM,qBAAqB,WAAW,wBAAwB;AAE9D,MAAI,kBAAkB;AACpB,WAAO;AAAA,EACT;AAEA,MAAI,oBAAoB;AACtB,WAAO;AAAA,EACT;AAEA,SAAO,CAAC;AACV;AAUO,SAAS,eAAiC,WAAgD;AAC/F,QAAM,mBAAgC,CAAC,UAAU;AAC/C,UAAM,aAAa,cAAc;AACjC,WAAO,oBAAC,aAAW,GAAG,OAAO,YAAwB;AAAA,EACvD;AAEA,mBAAiB,cAAc,kBAAkB,UAAU,eAAe,UAAU,QAAQ,WAAW;AAEvG,SAAO;AACT;AAQO,IAAM,0BAA0B,MAAe;AACpD,QAAM,mBAAmB,WAAW,gBAAgB;AACpD,QAAM,qBAAqB,WAAW,wBAAwB;AAC9D,SAAO,qBAAqB,QAAQ,uBAAuB;AAC7D;AAKO,IAAM,mBAAmB,MAAyC;AACvE,QAAM,mBAAmB,WAAW,gBAAgB;AACpD,QAAM,qBAAqB,WAAW,wBAAwB;AAE9D,MAAI,iBAAkB,QAAO;AAC7B,MAAI,mBAAoB,QAAO;AAC/B,SAAO;AACT;AAKO,SAAS,4BACX,YACuB;AAC1B,QAAM,SAAmC,CAAC;AAE1C,aAAW,YAAY,YAAY;AACjC,QAAI,UAAU;AACZ,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,GAAG;AACnD,YAAI,UAAU,QAAW;AACvB,UAAC,OAAmC,GAAG,IAAI;AAAA,QAC7C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAmBA,SAAS,oBAAoB,MAAsB;AAEjD,QAAM,WACJ,KACG,MAAM,GAAG,EACT,IAAI,GACH,QAAQ,kBAAkB,EAAE,KAAK;AAGvC,QAAM,eAAuC;AAAA,IAC3C,aAAa;AAAA,IACb,KAAK;AAAA,EACP;AAEA,MAAI,aAAa,QAAQ,GAAG;AAC1B,WAAO,aAAa,QAAQ;AAAA,EAC9B;AAGA,SAAO,SACJ,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,EAC1D,KAAK,EAAE;AACZ;AAkBO,SAAS,iBAAiB,SAAgD;AAC/E,QAAM,WAAyD,CAAC;AAEhE,aAAW,CAAC,MAAM,GAAG,KAAK,OAAO,QAAQ,OAAO,GAAG;AAEjD,QAAI,OAAO,QAAQ,YAAY,QAAQ,KAAM;AAG7C,UAAM,WAAW,oBAAoB,IAAI;AAGzC,eAAW,CAAC,YAAY,WAAW,KAAK,OAAO,QAAQ,GAA8B,GAAG;AAEtF,UACE,OAAO,gBAAgB,cACtB,OAAO,gBAAgB,YAAY,gBAAgB,QAAQ,cAAc,aAC1E;AAEA,YAAI,eAAe,WAAW;AAC5B,mBAAS,UAAU,IAAI;AAAA,QACzB,OAAO;AAEL,mBAAS,QAAQ,IAAI;AAAA,QACvB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAiDO,IAAM,yBAAgE,CAAC;AAAA,EAC5E,YAAY;AAAA,EACZ;AACF,MAAM;AAEJ,QAAM,WAAW,QAAQ,MAAM,iBAAiB,WAAW,GAAG,CAAC,WAAW,CAAC;AAE3E,SACE,oBAAC,yBAAyB,UAAzB,EAAkC,OAAO,UACvC,UACH;AAEJ;;;AC9PQ,SAWM,UAXN,OAAAA,MAWM,YAXN;AAjGR,SAAS,kBAAkB,mBAAqC;AAE9D,QAAM,aAAqC;AAAA,IACzC,OAAO;AAAA,IACP,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,gBAAgB;AAAA,IAChB,QAAQ;AAAA,IACR,eAAe;AAAA,IACf,eAAe;AAAA,IACf,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,cAAc;AAAA,IACd,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,gBAAgB;AAAA,IAChB,gBAAgB;AAAA,IAChB,SAAS;AAAA,IACT,gBAAgB;AAAA,IAChB,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,eAAe;AAAA,IACf,eAAe;AAAA,IACf,cAAc;AAAA,IACd,aAAa;AAAA,IACb,mBAAmB;AAAA,IACnB,SAAS;AAAA,IACT,cAAc;AAAA,IACd,aAAa;AAAA,IACb,cAAc;AAAA,IACd,cAAc;AAAA,IACd,aAAa;AAAA,IACb,UAAU;AAAA,IACV,eAAe;AAAA,IACf,cAAc;AAAA,IACd,WAAW;AAAA,IACX,eAAe;AAAA,IACf,kBAAkB;AAAA,IAClB,kBAAkB;AAAA,IAClB,UAAU;AAAA,IACV,WAAW;AAAA,IACX,aAAa;AAAA,IACb,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,kBAAkB;AAAA,IAClB,YAAY;AAAA,EACd;AAGA,QAAM,WAAW,oBAAI,IAAY;AACjC,aAAW,aAAa,mBAAmB;AACzC,UAAM,MAAM,WAAW,SAAS;AAChC,QAAI,KAAK;AACP,eAAS,IAAI,GAAG;AAAA,IAClB;AAAA,EACF;AAEA,QAAM,cAAc,MAAM,KAAK,QAAQ,EAAE,KAAK,GAAG;AACjD,SAAO,yBAAyB,WAAW;AAC7C;AAeO,SAAS,yBAAyB;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAsD;AACpD,QAAM,aAAa,kBAAkB,iBAAiB;AAEtD,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,gGAAgG,aAAa,EAAE;AAAA,MAC1H,MAAK;AAAA,MACL,aAAU;AAAA,MAEV,+BAAC,SAAI,WAAU,0BAEb;AAAA,wBAAAA;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,eAAY;AAAA,YACb;AAAA;AAAA,QAED;AAAA,QAEA,qBAAC,SAAI,WAAU,mBAEb;AAAA,0BAAAA,KAAC,OAAE,WAAU,kDACV,qBACC,iCAAE;AAAA;AAAA,YACa;AAAA,YAAU;AAAA,YAAS;AAAA,YAAU;AAAA,aAC5C,GAEJ;AAAA,UAGA,gBAAAA,KAAC,UAAK,WAAU,6DACb,4BAAkB,KAAK,IAAI,GAC9B;AAAA,UAGA,qBAAC,SAAI,WAAU,4EACb;AAAA,4BAAAA,KAAC,OAAE,WAAU,mDAAkD,2BAAa;AAAA,YAC5E,gBAAAA,KAAC,UAAK,WAAU,0EACb,sBACH;AAAA,aACF;AAAA,UAGA,qBAAC,OAAE,WAAU,mDAAkD;AAAA;AAAA,YAC/B;AAAA,YAC9B,gBAAAA,KAAC,UAAK,WAAU,kDAAiD,wBAAU;AAAA,YAAO;AAAA,aACpF;AAAA,UACA,gBAAAA,KAAC,SAAI,WAAU,oFACb,0BAAAA,KAAC,UAAM,iDAAuC,kBAAkB,CAAC,CAAC,aAAa,kBAAkB,CAAC,CAAC,UAAS,GAC9G;AAAA,WACF;AAAA,SACF;AAAA;AAAA,EACF;AAEJ;AAMO,SAAS,kCACd,eACA,WAC6B;AAC7B,QAAM,cAA2C,CAAC,EAAE,OAAO,UAAU,MACnE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,MACX;AAAA,MACA,mBAAmB,CAAC,aAAa;AAAA;AAAA,EACnC;AAEF,cAAY,cAAc,UAAU,aAAa;AACjD,SAAO;AACT;","names":["jsx"]}
|
|
1
|
+
{"version":3,"sources":["../src/context/ComponentContext.tsx","../src/components/MissingComponentFallback.tsx"],"sourcesContent":["import React, { createContext, useContext, useMemo } from 'react';\nimport type { ComponentRegistry, PartialComponentRegistry } from '../types/components';\n\n/**\n * ============================================\n * COMPONENT CONTEXT - Dependency Injection\n * ============================================\n *\n * Este contexto permite inyectar componentes UI en el plugin,\n * haciendo que sea agnóstico de la librería UI utilizada.\n *\n * Supports two modes:\n * 1. Legacy mode: Full ComponentRegistry via ComponentProvider (required all components)\n * 2. Zero-config mode: Partial registry via InternalComponentContext (Form-level)\n */\n\n// Legacy context for full registry (backwards compatible)\nconst ComponentContext = createContext<ComponentRegistry | null>(null);\n\n// Internal context for Form-level partial registry (zero-config mode)\nconst InternalComponentContext = createContext<PartialComponentRegistry | null>(null);\n\nexport interface ComponentProviderProps {\n /** Registry completo de componentes UI */\n components: ComponentRegistry;\n /** Componentes hijos que tendrán acceso al registry */\n children: React.ReactNode;\n}\n\n/**\n * Provider que inyecta los componentes UI en el árbol de React.\n * This is the legacy provider that requires a full ComponentRegistry.\n *\n * @example\n * ```tsx\n * import { ComponentProvider, createComponentRegistry } from \"@saastro/forms\";\n * import * as shadcn from \"@/lib/form-components\";\n *\n * const registry = createComponentRegistry(shadcn);\n *\n * function App() {\n * return (\n * <ComponentProvider components={registry}>\n * <MyForm />\n * </ComponentProvider>\n * );\n * }\n * ```\n *\n * @deprecated Consider using the zero-config approach with inline components prop on Form\n */\nexport const ComponentProvider: React.FC<ComponentProviderProps> = ({ components, children }) => {\n return <ComponentContext.Provider value={components}>{children}</ComponentContext.Provider>;\n};\n\nexport interface InternalComponentProviderProps {\n /** Partial registry de componentes UI */\n components: PartialComponentRegistry;\n /** Componentes hijos que tendrán acceso al registry */\n children: React.ReactNode;\n}\n\n/**\n * Internal provider used by Form component for zero-config mode.\n * Accepts a partial registry - only the components needed for the form.\n *\n * @internal\n */\nexport const InternalComponentProvider: React.FC<InternalComponentProviderProps> = ({\n components,\n children,\n}) => {\n return (\n <InternalComponentContext.Provider value={components}>\n {children}\n </InternalComponentContext.Provider>\n );\n};\n\n/**\n * Hook para acceder al registry de componentes inyectados.\n *\n * Supports both legacy (ComponentProvider) and zero-config (InternalComponentContext) modes.\n * Legacy mode takes precedence for backwards compatibility.\n *\n * @throws Error si se usa fuera de cualquier provider\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { Input, Button } = useComponents();\n *\n * return (\n * <div>\n * <Input placeholder=\"Email\" />\n * <Button>Submit</Button>\n * </div>\n * );\n * }\n * ```\n */\nexport const useComponents = (): ComponentRegistry => {\n const legacyComponents = useContext(ComponentContext);\n const internalComponents = useContext(InternalComponentContext);\n\n // Legacy mode takes precedence\n if (legacyComponents) {\n return legacyComponents;\n }\n\n // Zero-config mode\n if (internalComponents) {\n // Return partial as full (consumers should handle missing components)\n return internalComponents as ComponentRegistry;\n }\n\n throw new Error(\n 'useComponents must be used within a Form component or ComponentProvider.\\n\\n' +\n 'Option 1 (Recommended - Zero Config):\\n' +\n ' <Form config={config} components={{ Input, Button }} />\\n\\n' +\n 'Option 2 (Legacy):\\n' +\n ' <ComponentProvider components={fullRegistry}>\\n' +\n ' <Form config={config} />\\n' +\n ' </ComponentProvider>\\n\\n' +\n 'See documentation: https://forms.saastro.io/getting-started',\n );\n};\n\n/**\n * Hook to access partial components safely (may have missing components).\n * Returns null for components that are not provided.\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const components = usePartialComponents();\n *\n * if (!components.Input) {\n * return <MissingComponentFallback missingComponents={['Input']} />;\n * }\n *\n * return <components.Input placeholder=\"Email\" />;\n * }\n * ```\n */\nexport const usePartialComponents = (): PartialComponentRegistry => {\n const legacyComponents = useContext(ComponentContext);\n const internalComponents = useContext(InternalComponentContext);\n\n if (legacyComponents) {\n return legacyComponents;\n }\n\n if (internalComponents) {\n return internalComponents;\n }\n\n return {};\n};\n\n/**\n * HOC para envolver componentes que necesitan acceso a los componentes UI.\n *\n * @example\n * ```tsx\n * const MyFormWithComponents = withComponents(MyForm);\n * ```\n */\nexport function withComponents<P extends object>(Component: React.ComponentType<P>): React.FC<P> {\n const WrappedComponent: React.FC<P> = (props) => {\n const components = useComponents();\n return <Component {...props} components={components} />;\n };\n\n WrappedComponent.displayName = `withComponents(${Component.displayName || Component.name || 'Component'})`;\n\n return WrappedComponent;\n}\n\n/**\n * Hook para verificar si estamos dentro de un ComponentProvider.\n * Útil para componentes que pueden funcionar con o sin provider.\n *\n * @returns true si hay un provider activo (legacy or internal), false en caso contrario\n */\nexport const useHasComponentProvider = (): boolean => {\n const legacyComponents = useContext(ComponentContext);\n const internalComponents = useContext(InternalComponentContext);\n return legacyComponents !== null || internalComponents !== null;\n};\n\n/**\n * Hook to check which mode we're operating in\n */\nexport const useComponentMode = (): 'legacy' | 'zero-config' | 'none' => {\n const legacyComponents = useContext(ComponentContext);\n const internalComponents = useContext(InternalComponentContext);\n\n if (legacyComponents) return 'legacy';\n if (internalComponents) return 'zero-config';\n return 'none';\n};\n\n/**\n * Merge multiple component registries with later ones taking precedence\n */\nexport function mergeComponentRegistries(\n ...registries: Array<PartialComponentRegistry | undefined>\n): PartialComponentRegistry {\n const result: PartialComponentRegistry = {};\n\n for (const registry of registries) {\n if (registry) {\n for (const [key, value] of Object.entries(registry)) {\n if (value !== undefined) {\n (result as Record<string, unknown>)[key] = value;\n }\n }\n }\n }\n\n return result;\n}\n\n// ═══════════════════════════════════════════════════════════\n// GLOB-BASED AUTO-DISCOVERY (Recommended for Vite projects)\n// ═══════════════════════════════════════════════════════════\n\n/**\n * Type for Vite's import.meta.glob result.\n * Accepts the actual Vite return type which is Record<string, unknown>.\n */\nexport type GlobModules = Record<string, unknown>;\n\n/**\n * Convert a file path to PascalCase component name\n * @example\n * '/components/ui/button.tsx' -> 'Button'\n * '/components/ui/radio-group.tsx' -> 'RadioGroup'\n * '/components/ui/input-otp.tsx' -> 'InputOTP'\n */\nfunction pathToComponentName(path: string): string {\n // Extract filename without extension\n const filename =\n path\n .split('/')\n .pop()\n ?.replace(/\\.(tsx?|jsx?)$/, '') || '';\n\n // Special cases for common naming patterns\n const specialCases: Record<string, string> = {\n 'input-otp': 'InputOTP',\n otp: 'OTP',\n };\n\n if (specialCases[filename]) {\n return specialCases[filename];\n }\n\n // Convert kebab-case to PascalCase\n return filename\n .split('-')\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join('');\n}\n\n/**\n * Parse Vite's import.meta.glob result into a component registry.\n *\n * This function extracts all named exports from the glob modules\n * and creates a flat registry of components.\n *\n * @param modules - Result of import.meta.glob('@/components/ui/*.tsx', { eager: true })\n * @returns Component registry ready to use with Form\n *\n * @example\n * ```tsx\n * const modules = import.meta.glob('@/components/ui/*.tsx', { eager: true });\n * const components = parseGlobModules(modules);\n * // { Button, Input, Label, Select, SelectTrigger, ... }\n * ```\n */\nexport function parseGlobModules(modules: GlobModules): PartialComponentRegistry {\n const registry: Record<string, React.ComponentType<unknown>> = {};\n\n for (const [path, mod] of Object.entries(modules)) {\n // Skip if module is not an object (shouldn't happen with eager: true)\n if (typeof mod !== 'object' || mod === null) continue;\n\n // Get the base component name from file path\n const baseName = pathToComponentName(path);\n\n // Extract all named exports that look like React components\n for (const [exportName, exportValue] of Object.entries(mod as Record<string, unknown>)) {\n // Skip non-component exports (like types, constants, etc.)\n if (\n typeof exportValue === 'function' ||\n (typeof exportValue === 'object' && exportValue !== null && '$$typeof' in exportValue)\n ) {\n // Use the export name directly (Button, SelectTrigger, etc.)\n if (exportName !== 'default') {\n registry[exportName] = exportValue as React.ComponentType<unknown>;\n } else {\n // For default exports, use the file name\n registry[baseName] = exportValue as React.ComponentType<unknown>;\n }\n }\n }\n }\n\n return registry as PartialComponentRegistry;\n}\n\nexport interface FormComponentsProviderProps {\n /**\n * Glob modules from import.meta.glob.\n * Use { eager: true } for synchronous loading.\n *\n * @example\n * ```tsx\n * <FormComponentsProvider\n * components={import.meta.glob('@/components/ui/*.tsx', { eager: true })}\n * >\n * ```\n */\n components: GlobModules;\n /** Child components */\n children: React.ReactNode;\n}\n\n/**\n * Provider for auto-discovered shadcn components using Vite's import.meta.glob.\n *\n * Place this once at your app's root layout. Forms will automatically\n * use any shadcn components installed in your project.\n *\n * @example\n * ```tsx\n * // app/layout.tsx or _app.tsx - ONE TIME SETUP\n * import { FormComponentsProvider } from '@saastro/forms';\n *\n * export default function RootLayout({ children }) {\n * return (\n * <FormComponentsProvider\n * components={import.meta.glob('@/components/ui/*.tsx', { eager: true })}\n * >\n * {children}\n * </FormComponentsProvider>\n * );\n * }\n * ```\n *\n * Then use Form anywhere without passing components:\n * ```tsx\n * <Form config={config} />\n * ```\n *\n * If a component is missing, a helpful message will appear\n * with installation instructions.\n */\nexport const FormComponentsProvider: React.FC<FormComponentsProviderProps> = ({\n components: globModules,\n children,\n}) => {\n // Parse glob modules into component registry (memoized)\n const registry = useMemo(() => parseGlobModules(globModules), [globModules]);\n\n return (\n <InternalComponentContext.Provider value={registry}>\n {children}\n </InternalComponentContext.Provider>\n );\n};\n","import React from 'react';\n\n/**\n * Props for MissingComponentFallback\n */\nexport interface MissingComponentFallbackProps {\n /** Name of the field that requires the missing component */\n fieldName: string;\n /** Type of the field (e.g., 'text', 'select', 'date') */\n fieldType: string;\n /** List of missing component names */\n missingComponents: string[];\n /** Optional custom message */\n message?: string;\n /** Optional className for layout (e.g., column span classes) */\n className?: string;\n}\n\n/**\n * Generates the npx shadcn command for installing missing components\n */\nfunction getInstallCommand(missingComponents: string[]): string {\n // Map component names to their shadcn package names\n const packageMap: Record<string, string> = {\n Input: 'input',\n Textarea: 'textarea',\n Button: 'button',\n Label: 'label',\n Checkbox: 'checkbox',\n Switch: 'switch',\n RadioGroup: 'radio-group',\n RadioGroupItem: 'radio-group',\n Select: 'select',\n SelectTrigger: 'select',\n SelectContent: 'select',\n SelectItem: 'select',\n SelectValue: 'select',\n NativeSelect: 'native-select',\n Slider: 'slider',\n Popover: 'popover',\n PopoverTrigger: 'popover',\n PopoverContent: 'popover',\n Tooltip: 'tooltip',\n TooltipTrigger: 'tooltip',\n TooltipContent: 'tooltip',\n TooltipProvider: 'tooltip',\n Separator: 'separator',\n Dialog: 'dialog',\n DialogTrigger: 'dialog',\n DialogContent: 'dialog',\n DialogHeader: 'dialog',\n DialogTitle: 'dialog',\n DialogDescription: 'dialog',\n Command: 'command',\n CommandInput: 'command',\n CommandList: 'command',\n CommandEmpty: 'command',\n CommandGroup: 'command',\n CommandItem: 'command',\n InputOTP: 'input-otp',\n InputOTPGroup: 'input-otp',\n InputOTPSlot: 'input-otp',\n Accordion: 'accordion',\n AccordionItem: 'accordion',\n AccordionTrigger: 'accordion',\n AccordionContent: 'accordion',\n Calendar: 'calendar',\n FormField: 'form',\n FormControl: 'form',\n Field: 'field',\n FieldLabel: 'field',\n FieldDescription: 'field',\n FieldError: 'field',\n };\n\n // Get unique package names\n const packages = new Set<string>();\n for (const component of missingComponents) {\n const pkg = packageMap[component];\n if (pkg) {\n packages.add(pkg);\n }\n }\n\n const packageList = Array.from(packages).join(' ');\n return `npx shadcn@latest add ${packageList}`;\n}\n\n/**\n * Fallback component displayed when required UI components are missing.\n * Shows a helpful warning with installation instructions.\n *\n * @example\n * ```tsx\n * <MissingComponentFallback\n * fieldName=\"birthdate\"\n * fieldType=\"date\"\n * missingComponents={['Calendar', 'Popover']}\n * />\n * ```\n */\nexport function MissingComponentFallback({\n fieldName,\n fieldType,\n missingComponents,\n message,\n className,\n}: MissingComponentFallbackProps): React.ReactElement {\n const installCmd = getInstallCommand(missingComponents);\n\n return (\n <div\n className={`border-2 border-dashed border-amber-400 bg-amber-50 dark:bg-amber-950/20 rounded-lg p-4 my-2 ${className || ''}`}\n role=\"alert\"\n aria-live=\"polite\"\n >\n <div className=\"flex items-start gap-3\">\n {/* Warning Icon */}\n <span\n className=\"text-amber-600 dark:text-amber-400 text-xl flex-shrink-0\"\n aria-hidden=\"true\"\n >\n ⚠️\n </span>\n\n <div className=\"text-sm min-w-0\">\n {/* Main message */}\n <p className=\"font-medium text-amber-800 dark:text-amber-200\">\n {message || (\n <>\n Field "{fieldName}" ({fieldType}) requires missing components:\n </>\n )}\n </p>\n\n {/* Missing components list */}\n <code className=\"block mt-1 text-amber-700 dark:text-amber-300 break-words\">\n {missingComponents.join(', ')}\n </code>\n\n {/* Install command — preferred path via CLI */}\n <div className=\"mt-3 bg-amber-100 dark:bg-amber-900/30 rounded px-3 py-2 overflow-x-auto\">\n <p className=\"text-xs text-amber-600 dark:text-amber-400 mb-1\">Install with:</p>\n <code className=\"text-xs font-mono text-amber-800 dark:text-amber-200 whitespace-nowrap\">\n npx @saastro/forms install-deps\n </code>\n </div>\n\n {/* Manual fallback */}\n <p className=\"mt-2 text-xs text-amber-600 dark:text-amber-400\">\n Or install the specific shadcn components manually:\n </p>\n <code className=\"block mt-1 text-xs font-mono text-amber-800 dark:text-amber-200 bg-amber-100 dark:bg-amber-900/30 rounded px-2 py-1 overflow-x-auto whitespace-nowrap\">\n {installCmd}\n </code>\n\n {/* Alternative: provide components prop */}\n <p className=\"mt-2 text-xs text-amber-600 dark:text-amber-400\">\n Or provide components via the{' '}\n <code className=\"bg-amber-100 dark:bg-amber-900/50 px-1 rounded\">components</code> prop:\n </p>\n <pre className=\"mt-1 text-xs bg-amber-100 dark:bg-amber-900/30 rounded px-2 py-1 overflow-x-auto\">\n <code>{`<Form config={config} components={{ ${missingComponents[0]}: MyCustom${missingComponents[0]} }} />`}</code>\n </pre>\n </div>\n </div>\n </div>\n );\n}\n\n/**\n * Creates a placeholder component that renders the MissingComponentFallback\n * Can be used as a drop-in replacement for missing components\n */\nexport function createMissingComponentPlaceholder(\n componentName: string,\n fieldType: string,\n): React.FC<{ name?: string }> {\n const Placeholder: React.FC<{ name?: string }> = ({ name = 'unknown' }) => (\n <MissingComponentFallback\n fieldName={name}\n fieldType={fieldType}\n missingComponents={[componentName]}\n />\n );\n Placeholder.displayName = `Missing${componentName}`;\n return Placeholder;\n}\n"],"mappings":";AAAA,SAAgB,eAAe,YAAY,eAAe;AAoDjD;AAnCT,IAAM,mBAAmB,cAAwC,IAAI;AAGrE,IAAM,2BAA2B,cAA+C,IAAI;AA+B7E,IAAM,oBAAsD,CAAC,EAAE,YAAY,SAAS,MAAM;AAC/F,SAAO,oBAAC,iBAAiB,UAAjB,EAA0B,OAAO,YAAa,UAAS;AACjE;AAeO,IAAM,4BAAsE,CAAC;AAAA,EAClF;AAAA,EACA;AACF,MAAM;AACJ,SACE,oBAAC,yBAAyB,UAAzB,EAAkC,OAAO,YACvC,UACH;AAEJ;AAwBO,IAAM,gBAAgB,MAAyB;AACpD,QAAM,mBAAmB,WAAW,gBAAgB;AACpD,QAAM,qBAAqB,WAAW,wBAAwB;AAG9D,MAAI,kBAAkB;AACpB,WAAO;AAAA,EACT;AAGA,MAAI,oBAAoB;AAEtB,WAAO;AAAA,EACT;AAEA,QAAM,IAAI;AAAA,IACR;AAAA,EAQF;AACF;AAmBO,IAAM,uBAAuB,MAAgC;AAClE,QAAM,mBAAmB,WAAW,gBAAgB;AACpD,QAAM,qBAAqB,WAAW,wBAAwB;AAE9D,MAAI,kBAAkB;AACpB,WAAO;AAAA,EACT;AAEA,MAAI,oBAAoB;AACtB,WAAO;AAAA,EACT;AAEA,SAAO,CAAC;AACV;AAUO,SAAS,eAAiC,WAAgD;AAC/F,QAAM,mBAAgC,CAAC,UAAU;AAC/C,UAAM,aAAa,cAAc;AACjC,WAAO,oBAAC,aAAW,GAAG,OAAO,YAAwB;AAAA,EACvD;AAEA,mBAAiB,cAAc,kBAAkB,UAAU,eAAe,UAAU,QAAQ,WAAW;AAEvG,SAAO;AACT;AAQO,IAAM,0BAA0B,MAAe;AACpD,QAAM,mBAAmB,WAAW,gBAAgB;AACpD,QAAM,qBAAqB,WAAW,wBAAwB;AAC9D,SAAO,qBAAqB,QAAQ,uBAAuB;AAC7D;AAKO,IAAM,mBAAmB,MAAyC;AACvE,QAAM,mBAAmB,WAAW,gBAAgB;AACpD,QAAM,qBAAqB,WAAW,wBAAwB;AAE9D,MAAI,iBAAkB,QAAO;AAC7B,MAAI,mBAAoB,QAAO;AAC/B,SAAO;AACT;AAKO,SAAS,4BACX,YACuB;AAC1B,QAAM,SAAmC,CAAC;AAE1C,aAAW,YAAY,YAAY;AACjC,QAAI,UAAU;AACZ,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,GAAG;AACnD,YAAI,UAAU,QAAW;AACvB,UAAC,OAAmC,GAAG,IAAI;AAAA,QAC7C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAmBA,SAAS,oBAAoB,MAAsB;AAEjD,QAAM,WACJ,KACG,MAAM,GAAG,EACT,IAAI,GACH,QAAQ,kBAAkB,EAAE,KAAK;AAGvC,QAAM,eAAuC;AAAA,IAC3C,aAAa;AAAA,IACb,KAAK;AAAA,EACP;AAEA,MAAI,aAAa,QAAQ,GAAG;AAC1B,WAAO,aAAa,QAAQ;AAAA,EAC9B;AAGA,SAAO,SACJ,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,EAC1D,KAAK,EAAE;AACZ;AAkBO,SAAS,iBAAiB,SAAgD;AAC/E,QAAM,WAAyD,CAAC;AAEhE,aAAW,CAAC,MAAM,GAAG,KAAK,OAAO,QAAQ,OAAO,GAAG;AAEjD,QAAI,OAAO,QAAQ,YAAY,QAAQ,KAAM;AAG7C,UAAM,WAAW,oBAAoB,IAAI;AAGzC,eAAW,CAAC,YAAY,WAAW,KAAK,OAAO,QAAQ,GAA8B,GAAG;AAEtF,UACE,OAAO,gBAAgB,cACtB,OAAO,gBAAgB,YAAY,gBAAgB,QAAQ,cAAc,aAC1E;AAEA,YAAI,eAAe,WAAW;AAC5B,mBAAS,UAAU,IAAI;AAAA,QACzB,OAAO;AAEL,mBAAS,QAAQ,IAAI;AAAA,QACvB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAiDO,IAAM,yBAAgE,CAAC;AAAA,EAC5E,YAAY;AAAA,EACZ;AACF,MAAM;AAEJ,QAAM,WAAW,QAAQ,MAAM,iBAAiB,WAAW,GAAG,CAAC,WAAW,CAAC;AAE3E,SACE,oBAAC,yBAAyB,UAAzB,EAAkC,OAAO,UACvC,UACH;AAEJ;;;AC9PQ,SAWM,UAXN,OAAAA,MAWM,YAXN;AAjGR,SAAS,kBAAkB,mBAAqC;AAE9D,QAAM,aAAqC;AAAA,IACzC,OAAO;AAAA,IACP,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,gBAAgB;AAAA,IAChB,QAAQ;AAAA,IACR,eAAe;AAAA,IACf,eAAe;AAAA,IACf,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,cAAc;AAAA,IACd,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,gBAAgB;AAAA,IAChB,gBAAgB;AAAA,IAChB,SAAS;AAAA,IACT,gBAAgB;AAAA,IAChB,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,WAAW;AAAA,IACX,QAAQ;AAAA,IACR,eAAe;AAAA,IACf,eAAe;AAAA,IACf,cAAc;AAAA,IACd,aAAa;AAAA,IACb,mBAAmB;AAAA,IACnB,SAAS;AAAA,IACT,cAAc;AAAA,IACd,aAAa;AAAA,IACb,cAAc;AAAA,IACd,cAAc;AAAA,IACd,aAAa;AAAA,IACb,UAAU;AAAA,IACV,eAAe;AAAA,IACf,cAAc;AAAA,IACd,WAAW;AAAA,IACX,eAAe;AAAA,IACf,kBAAkB;AAAA,IAClB,kBAAkB;AAAA,IAClB,UAAU;AAAA,IACV,WAAW;AAAA,IACX,aAAa;AAAA,IACb,OAAO;AAAA,IACP,YAAY;AAAA,IACZ,kBAAkB;AAAA,IAClB,YAAY;AAAA,EACd;AAGA,QAAM,WAAW,oBAAI,IAAY;AACjC,aAAW,aAAa,mBAAmB;AACzC,UAAM,MAAM,WAAW,SAAS;AAChC,QAAI,KAAK;AACP,eAAS,IAAI,GAAG;AAAA,IAClB;AAAA,EACF;AAEA,QAAM,cAAc,MAAM,KAAK,QAAQ,EAAE,KAAK,GAAG;AACjD,SAAO,yBAAyB,WAAW;AAC7C;AAeO,SAAS,yBAAyB;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAsD;AACpD,QAAM,aAAa,kBAAkB,iBAAiB;AAEtD,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,gGAAgG,aAAa,EAAE;AAAA,MAC1H,MAAK;AAAA,MACL,aAAU;AAAA,MAEV,+BAAC,SAAI,WAAU,0BAEb;AAAA,wBAAAA;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,eAAY;AAAA,YACb;AAAA;AAAA,QAED;AAAA,QAEA,qBAAC,SAAI,WAAU,mBAEb;AAAA,0BAAAA,KAAC,OAAE,WAAU,kDACV,qBACC,iCAAE;AAAA;AAAA,YACa;AAAA,YAAU;AAAA,YAAS;AAAA,YAAU;AAAA,aAC5C,GAEJ;AAAA,UAGA,gBAAAA,KAAC,UAAK,WAAU,6DACb,4BAAkB,KAAK,IAAI,GAC9B;AAAA,UAGA,qBAAC,SAAI,WAAU,4EACb;AAAA,4BAAAA,KAAC,OAAE,WAAU,mDAAkD,2BAAa;AAAA,YAC5E,gBAAAA,KAAC,UAAK,WAAU,0EAAyE,6CAEzF;AAAA,aACF;AAAA,UAGA,gBAAAA,KAAC,OAAE,WAAU,mDAAkD,iEAE/D;AAAA,UACA,gBAAAA,KAAC,UAAK,WAAU,yJACb,sBACH;AAAA,UAGA,qBAAC,OAAE,WAAU,mDAAkD;AAAA;AAAA,YAC/B;AAAA,YAC9B,gBAAAA,KAAC,UAAK,WAAU,kDAAiD,wBAAU;AAAA,YAAO;AAAA,aACpF;AAAA,UACA,gBAAAA,KAAC,SAAI,WAAU,oFACb,0BAAAA,KAAC,UAAM,iDAAuC,kBAAkB,CAAC,CAAC,aAAa,kBAAkB,CAAC,CAAC,UAAS,GAC9G;AAAA,WACF;AAAA,SACF;AAAA;AAAA,EACF;AAEJ;AAMO,SAAS,kCACd,eACA,WAC6B;AAC7B,QAAM,cAA2C,CAAC,EAAE,OAAO,UAAU,MACnE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,MACX;AAAA,MACA,mBAAmB,CAAC,aAAa;AAAA;AAAA,EACnC;AAEF,cAAY,cAAc,UAAU,aAAa;AACjD,SAAO;AACT;","names":["jsx"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -96,7 +96,7 @@ type ConditionGroup = {
|
|
|
96
96
|
* Cada acción puede tener su propio trigger (cuándo se ejecuta).
|
|
97
97
|
*/
|
|
98
98
|
|
|
99
|
-
type SubmitActionType = 'http' | 'webhook' | 'email' | 'custom';
|
|
99
|
+
type SubmitActionType = 'http' | 'webhook' | 'email' | 'custom' | 'integration';
|
|
100
100
|
interface HttpEndpointConfig {
|
|
101
101
|
url: string;
|
|
102
102
|
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
@@ -184,7 +184,26 @@ interface CustomSubmitAction {
|
|
|
184
184
|
/** Función que se ejecuta con los valores del formulario */
|
|
185
185
|
handler: (values: Record<string, unknown>) => Promise<unknown>;
|
|
186
186
|
}
|
|
187
|
-
|
|
187
|
+
/**
|
|
188
|
+
* References a `site_integrations` row in Saastro Hub. The form runtime
|
|
189
|
+
* itself doesn't execute integrations (Hub does, server-side, after
|
|
190
|
+
* receiving the submission); this action exists so the form schema can
|
|
191
|
+
* declare which integrations Hub should dispatch on submit.
|
|
192
|
+
*
|
|
193
|
+
* `integrationRef` is the UUID Hub assigned to the integration when
|
|
194
|
+
* the user configured it in `/w/{ws}/s/{site}/settings/integrations`.
|
|
195
|
+
*
|
|
196
|
+
* Optional `fieldMapping` lets the user rename form field keys to
|
|
197
|
+
* different names in the destination (e.g. form field "name" →
|
|
198
|
+
* Sheets column "Full Name"). Default: pass-through.
|
|
199
|
+
*/
|
|
200
|
+
interface IntegrationSubmitAction {
|
|
201
|
+
type: 'integration';
|
|
202
|
+
name: string;
|
|
203
|
+
integrationRef: string;
|
|
204
|
+
fieldMapping?: Record<string, string>;
|
|
205
|
+
}
|
|
206
|
+
type SubmitAction = HttpSubmitAction | WebhookSubmitAction | EmailSubmitAction | CustomSubmitAction | IntegrationSubmitAction;
|
|
188
207
|
type SubmitTriggerType = 'onSubmit' | 'onStepEnter' | 'onStepExit' | 'onFieldChange' | 'onFieldBlur' | 'onDelay' | 'manual';
|
|
189
208
|
interface SubmitTrigger {
|
|
190
209
|
type: SubmitTriggerType;
|
|
@@ -3697,6 +3716,154 @@ declare function executeSubmitActions(actions: SubmitActionNode[], values: Recor
|
|
|
3697
3716
|
*/
|
|
3698
3717
|
declare function executeSubmitActionsByTrigger(config: FormConfig, triggerType: SubmitTriggerType, values: Record<string, unknown>, triggerValue?: string): Promise<SubmitActionsResult>;
|
|
3699
3718
|
|
|
3719
|
+
/**
|
|
3720
|
+
* Contact form template — name, email, message.
|
|
3721
|
+
*
|
|
3722
|
+
* The classic three-field contact form: full name (required), email
|
|
3723
|
+
* (required + format), and a message textarea (required, min 10 chars).
|
|
3724
|
+
* Single step, single column.
|
|
3725
|
+
*/
|
|
3726
|
+
declare function createContactTemplate(): FormConfig;
|
|
3727
|
+
|
|
3728
|
+
/**
|
|
3729
|
+
* Newsletter signup template — email + consent.
|
|
3730
|
+
*
|
|
3731
|
+
* GDPR-friendly: a single email field plus an explicit checkbox the
|
|
3732
|
+
* subscriber has to tick (validation: must be true). Single step.
|
|
3733
|
+
*/
|
|
3734
|
+
declare function createNewsletterTemplate(): FormConfig;
|
|
3735
|
+
|
|
3736
|
+
/**
|
|
3737
|
+
* Lead-gen template — name, email, phone, company, message.
|
|
3738
|
+
*
|
|
3739
|
+
* Standard B2B lead-capture: required name + email, optional phone +
|
|
3740
|
+
* company (for context), and a message textarea. Two-column layout on
|
|
3741
|
+
* wide screens so the form doesn't feel huge.
|
|
3742
|
+
*/
|
|
3743
|
+
declare function createLeadGenTemplate(): FormConfig;
|
|
3744
|
+
|
|
3745
|
+
/**
|
|
3746
|
+
* Event RSVP template — name, email, attending, guests, dietary notes.
|
|
3747
|
+
*
|
|
3748
|
+
* For wedding-/conference-style RSVPs: required name + email + answer
|
|
3749
|
+
* to "will you attend?", optional guest count and dietary notes.
|
|
3750
|
+
*/
|
|
3751
|
+
declare function createEventRsvpTemplate(): FormConfig;
|
|
3752
|
+
|
|
3753
|
+
/**
|
|
3754
|
+
* Quote-request template — name, email, company, service, budget, details.
|
|
3755
|
+
*
|
|
3756
|
+
* Captures qualified inbound: required contact info plus a service
|
|
3757
|
+
* type and budget range so the sales team can route the lead before
|
|
3758
|
+
* replying. The free-text "details" field is required so we never get
|
|
3759
|
+
* empty leads.
|
|
3760
|
+
*/
|
|
3761
|
+
declare function createQuoteRequestTemplate(): FormConfig;
|
|
3762
|
+
|
|
3763
|
+
/**
|
|
3764
|
+
* Starter templates for common form patterns. Each factory returns a
|
|
3765
|
+
* fresh `FormConfig` so callers can safely mutate the result (e.g.
|
|
3766
|
+
* append fields) without polluting a shared instance.
|
|
3767
|
+
*
|
|
3768
|
+
* Used by:
|
|
3769
|
+
* - `saastro-hub`'s "New form → Start from" dropdown.
|
|
3770
|
+
* - The form-builder's template picker.
|
|
3771
|
+
*/
|
|
3772
|
+
declare const TEMPLATES: Record<TemplateId, () => FormConfig>;
|
|
3773
|
+
type TemplateId = 'contact' | 'newsletter' | 'lead-gen' | 'event-rsvp' | 'quote-request';
|
|
3774
|
+
/** Static metadata for surfacing templates in pickers without invoking the factory. */
|
|
3775
|
+
interface TemplateMeta {
|
|
3776
|
+
id: TemplateId;
|
|
3777
|
+
label: string;
|
|
3778
|
+
description: string;
|
|
3779
|
+
fieldCount: number;
|
|
3780
|
+
}
|
|
3781
|
+
declare const TEMPLATE_META: readonly TemplateMeta[];
|
|
3782
|
+
|
|
3783
|
+
/**
|
|
3784
|
+
* Build a `CustomSubmitConfig` that routes form submissions to a
|
|
3785
|
+
* Saastro Hub site. Used by Astro / React sites whose forms are
|
|
3786
|
+
* authored in Hub (`builder.saastro.io` embed mode) and stored there.
|
|
3787
|
+
*
|
|
3788
|
+
* The flow is:
|
|
3789
|
+
*
|
|
3790
|
+
* 1. Walk the field values and split into `payload` (scalar/JSON-safe)
|
|
3791
|
+
* and `files` (instances of File). Hub keeps text fields together;
|
|
3792
|
+
* files take a separate path.
|
|
3793
|
+
* 2. For each file, POST `/api/public/forms/:siteId/:slug/upload-url`
|
|
3794
|
+
* to get a presigned R2 PUT URL, then PUT the binary directly to
|
|
3795
|
+
* that URL. Hub never touches the file body — R2 receives it.
|
|
3796
|
+
* 3. POST `/api/public/forms/:siteId/:slug/submit` with the textual
|
|
3797
|
+
* payload + an `attachments` array describing the uploaded R2
|
|
3798
|
+
* keys. Hub inserts the submission row and dispatches integrations
|
|
3799
|
+
* (Sheets / Resend / etc.) per the form's schema.
|
|
3800
|
+
*
|
|
3801
|
+
* Turnstile: if the form schema declares `meta.requireCaptcha=true`,
|
|
3802
|
+
* the host app must render the Turnstile widget and pass the token
|
|
3803
|
+
* via the special `_turnstile` field in the form values. The helper
|
|
3804
|
+
* extracts it before sending.
|
|
3805
|
+
*
|
|
3806
|
+
* Honeypot: pass via `_hp` (or whatever `meta.honeypotField` says).
|
|
3807
|
+
* The Hub will silently 200 the request and not store anything if
|
|
3808
|
+
* the honeypot is non-empty.
|
|
3809
|
+
*/
|
|
3810
|
+
|
|
3811
|
+
interface CreateHubFormSubmitOptions {
|
|
3812
|
+
/** Hub origin, e.g. `https://hub.saastro.io`. No trailing slash. */
|
|
3813
|
+
hubUrl: string;
|
|
3814
|
+
siteId: string;
|
|
3815
|
+
formSlug: string;
|
|
3816
|
+
}
|
|
3817
|
+
/**
|
|
3818
|
+
* Build a `CustomSubmitConfig` to plug into `<Form config={{...,
|
|
3819
|
+
* submit: createHubFormSubmit({...})}} />`.
|
|
3820
|
+
*
|
|
3821
|
+
* On submit:
|
|
3822
|
+
* - Files are uploaded to R2 first (presigned URLs).
|
|
3823
|
+
* - Then the payload + attachments are POSTed to Hub's submit endpoint.
|
|
3824
|
+
* - Throws on any non-2xx so the caller's `<Form>` shows the error.
|
|
3825
|
+
*/
|
|
3826
|
+
declare function createHubFormSubmit(opts: CreateHubFormSubmitOptions): CustomSubmitConfig;
|
|
3827
|
+
|
|
3828
|
+
interface HubFormProps {
|
|
3829
|
+
/** Origin of the Hub API. e.g. `https://hub.saastro.io`. No trailing slash. */
|
|
3830
|
+
hubUrl: string;
|
|
3831
|
+
siteId: string;
|
|
3832
|
+
formSlug: string;
|
|
3833
|
+
/** Custom UI while the schema is being fetched. Defaults to a skeleton. */
|
|
3834
|
+
loadingFallback?: ReactNode;
|
|
3835
|
+
/**
|
|
3836
|
+
* Custom UI when the schema fetch fails. Receives the error message
|
|
3837
|
+
* as a string. Defaults to a minimal red banner.
|
|
3838
|
+
*/
|
|
3839
|
+
errorFallback?: (error: string) => ReactNode;
|
|
3840
|
+
/**
|
|
3841
|
+
* Custom UI when the schema is fetched OK but contains no fields/steps.
|
|
3842
|
+
* This happens when the form exists in Hub but the author hasn't
|
|
3843
|
+
* configured it yet. Defaults to a minimal info banner pointing back
|
|
3844
|
+
* to the Hub builder.
|
|
3845
|
+
*/
|
|
3846
|
+
unconfiguredFallback?: ReactNode;
|
|
3847
|
+
/**
|
|
3848
|
+
* Custom UI to render after a successful submission. Defaults to a
|
|
3849
|
+
* small thank-you panel. Pass `null` to render nothing (e.g. if you
|
|
3850
|
+
* want to unmount the surrounding modal in `onSuccess`).
|
|
3851
|
+
*/
|
|
3852
|
+
successFallback?: ReactNode;
|
|
3853
|
+
/**
|
|
3854
|
+
* Called once after a successful submission, with the submitted values.
|
|
3855
|
+
* Runs after Hub returned `{ ok: true }`.
|
|
3856
|
+
*/
|
|
3857
|
+
onSuccess?: (values: Record<string, unknown>) => void;
|
|
3858
|
+
/** Called if the submission itself fails (network error, Hub 4xx/5xx). */
|
|
3859
|
+
onError?: (error: Error) => void;
|
|
3860
|
+
/**
|
|
3861
|
+
* Forwarded to `<Form>` — every prop except `config` (which we own).
|
|
3862
|
+
*/
|
|
3863
|
+
formProps?: Omit<FormProps, 'config'>;
|
|
3864
|
+
}
|
|
3865
|
+
declare function HubForm({ hubUrl, siteId, formSlug, loadingFallback, errorFallback, unconfiguredFallback, successFallback, onSuccess, onError, formProps, }: HubFormProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | React$1.ReactPortal | React$1.ReactElement<unknown, string | React$1.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | react_jsx_runtime.JSX.Element;
|
|
3866
|
+
|
|
3700
3867
|
/**
|
|
3701
3868
|
* Test Data Generator — generates realistic field values for form testing.
|
|
3702
3869
|
*
|
|
@@ -3751,4 +3918,4 @@ declare function getFieldClass(formLayout?: {
|
|
|
3751
3918
|
*/
|
|
3752
3919
|
declare function getHiddenClasses(hidden?: Partial<Record<Breakpoint, 'visible' | 'hidden'>>): string;
|
|
3753
3920
|
|
|
3754
|
-
export { type AccordionContentProps, type AccordionItemProps, type AccordionProps, type AccordionTriggerProps, BUILD_METHODS, BUILTIN_RESOLVERS, type BaseFieldProps, type Breakpoint$1 as Breakpoint, type BuiltinTransform, type ButtonCardFieldProps, type ButtonCardOption, type ButtonCheckboxFieldProps, type ButtonConfig, type ButtonProps, type ButtonRadioFieldProps, COMMON_CLASSES, COMMON_METHODS, COMMON_STRINGS, type CalendarProps, type CheckboxFieldProps, type CheckboxGroupFieldProps, type CheckboxProps, type ColumnsValue, type ComboboxFieldProps, type CommandEmptyProps, type CommandFieldProps, type CommandGroupProps, type CommandInputProps, type CommandItemProps, type CommandListProps, type CommandProps, type ComponentName, type ComponentOverrides, ComponentProvider, type ComponentProviderProps, type ComponentRegistry, type ComponentRegistryInput, ComponentResolver, type ComponentResolverConfig, type Condition, type ConditionGroup, type ConditionOperator, type CurrencyFieldProps, type CustomSubmitAction, type CustomSubmitConfig, type DatabowlConfig, type DateFieldProps, type DateRangeFieldProps, type DefaultSubmitConfig, type DefinePlugin, type DialogContentProps, type DialogDescriptionProps, type DialogHeaderProps, type DialogProps, type DialogTitleProps, type DialogTriggerProps, type EmailProvider, type EmailSubmitAction, type EndpointConfig, FIELD_DEFAULTS, FieldBuilder, type FieldConfig, type FieldDescriptionProps, type FieldErrorProps, type FieldLabelProps, type FieldLayoutConfig, type FieldMapEntry, type FieldMapping, type FieldMappingConfig, type FieldProps, FieldRenderer, type FieldResolver, type FieldTransformFn, type Fields, type FileFieldProps, Form, FormBuilder, type FormButtonProps, type FormButtons, FormComponentsProvider, type FormComponentsProviderProps, type FormConfig, type FormControlProps, type FormFieldProps, type FormLayout, type FormPlugin, type FormProps, type FormRef, type FormStepsInfo, type GapValue, type GlobModules, type HiddenFieldProps, type HtmlFieldProps, type HttpAuthConfig, type HttpBodyConfig, type HttpEndpointConfig, type HttpRetryConfig, type HttpSubmitAction, type InputGroupFieldProps, type InputOTPGroupProps, type InputOTPProps, type InputOTPSlotProps, type InputProps, type InputSize, InternalComponentProvider, type InternalComponentProviderProps, type LabelProps, MissingComponentFallback, type MissingComponentFallbackProps, type NativeSelectFieldProps, type NativeSelectProps, OPTION_BASED_TYPES, type Option, type OtpFieldProps, type PartialComponentRegistry, PluginManager, type PopoverContentProps, type PopoverProps, type PopoverTriggerProps, type PropertyMapping, type RadioFieldProps, type RadioGroupItemProps, type RadioGroupProps, type RangeFieldProps, type RecaptchaConfig, type RecaptchaPluginConfig, type RepeaterFieldProps, type ResolvedComponents, SUSPENSE_CONFIG, type SchemaType, type SelectContentProps, type SelectFieldProps, type SelectItemProps, type SelectProps, type SelectTriggerProps, type SelectValueProps, type SeparatorProps, type SerializableFieldResolver, type SerializationHint, type SliderFieldProps, type SliderProps, type Step, type StepCondition, type StepInfo, type StepStatus, type Steps, StepsAccordion, StepsNavigation, StepsProgress, type SubmitAction, type SubmitActionCondition, type SubmitActionNode, type SubmitActionResult, type SubmitActionType, type SubmitActionsResult, type SubmitConfig, type SubmitConfirmationConfig, type SubmitExecutionConfig, type SubmitTrigger, type SubmitTriggerType, type SubmitType, type SwitchFieldProps, type SwitchGroupFieldProps, type SwitchProps, TYPE_SPECIFIC_PROPERTIES, type TestDataLocale, type TestDataOptions, type TextFieldProps, type TextareaFieldProps, type TextareaProps, type TooltipContentProps, type TooltipProps, type TooltipProviderProps, type TooltipTriggerProps, type UseSubmitConfirmationReturn, VALIDATION_METHODS, type ValidateComponentRegistry, type ValidationContext, type ValidationPresetMeta, type ValidationRules, type WebhookSubmitAction, analyticsPlugin, applyBuiltinTransform, applyFieldMapping, applyFieldMappingSync, applyFieldTransforms, applyTransform, autosavePlugin, compileValidationRules, configureComponents, coreComponents, createComponentRegistry, createMissingComponentPlaceholder, createShadcnRegistry, databowlAction, databowlPlugin, defaultSubmit, definePlugin, detectLocale, executeCustomAction, executeEmailAction, executeHttpAction, executeSubmitAction, executeSubmitActions, executeSubmitActionsByTrigger, executeWebhookAction, fieldTypeComponents, generateFieldValue, generateTestData, getActionsByTrigger, getAllKnownMethods, getAvailablePresets, getComponentResolver, getFieldClass, getFormGridClass, getHiddenClasses, getInstallCommand, getMinDelayMs, getMissingComponents, getRequiredComponents, globalPluginManager, groupMissingByPackage, iconVariants, iconVariantsConfig, inputVariants, inputVariantsConfig, isAdvancedMapping, isValidationRules, isZodSchema, localStoragePlugin, mergeComponentRegistries, parseGlobModules, pxToRem, recaptchaPlugin, registerPreset, resolvePreset, resolveValue, resolveValueSync, textareaVariants, textareaVariantsConfig, useComponentMode, useComponents, useComputedFields, useFormLayout, useFormState, useFormStepsInfo, useHasComponentProvider, useHiddenFieldResolvers, usePartialComponents, useRecaptcha, useSubmitActionTriggers, useSubmitConfirmation, withComponents };
|
|
3921
|
+
export { type AccordionContentProps, type AccordionItemProps, type AccordionProps, type AccordionTriggerProps, BUILD_METHODS, BUILTIN_RESOLVERS, type BaseFieldProps, type Breakpoint$1 as Breakpoint, type BuiltinTransform, type ButtonCardFieldProps, type ButtonCardOption, type ButtonCheckboxFieldProps, type ButtonConfig, type ButtonProps, type ButtonRadioFieldProps, COMMON_CLASSES, COMMON_METHODS, COMMON_STRINGS, type CalendarProps, type CheckboxFieldProps, type CheckboxGroupFieldProps, type CheckboxProps, type ColumnsValue, type ComboboxFieldProps, type CommandEmptyProps, type CommandFieldProps, type CommandGroupProps, type CommandInputProps, type CommandItemProps, type CommandListProps, type CommandProps, type ComponentName, type ComponentOverrides, ComponentProvider, type ComponentProviderProps, type ComponentRegistry, type ComponentRegistryInput, ComponentResolver, type ComponentResolverConfig, type Condition, type ConditionGroup, type ConditionOperator, type CreateHubFormSubmitOptions, type CurrencyFieldProps, type CustomSubmitAction, type CustomSubmitConfig, type DatabowlConfig, type DateFieldProps, type DateRangeFieldProps, type DefaultSubmitConfig, type DefinePlugin, type DialogContentProps, type DialogDescriptionProps, type DialogHeaderProps, type DialogProps, type DialogTitleProps, type DialogTriggerProps, type EmailProvider, type EmailSubmitAction, type EndpointConfig, FIELD_DEFAULTS, FieldBuilder, type FieldConfig, type FieldDescriptionProps, type FieldErrorProps, type FieldLabelProps, type FieldLayoutConfig, type FieldMapEntry, type FieldMapping, type FieldMappingConfig, type FieldProps, FieldRenderer, type FieldResolver, type FieldTransformFn, type Fields, type FileFieldProps, Form, FormBuilder, type FormButtonProps, type FormButtons, FormComponentsProvider, type FormComponentsProviderProps, type FormConfig, type FormControlProps, type FormFieldProps, type FormLayout, type FormPlugin, type FormProps, type FormRef, type FormStepsInfo, type GapValue, type GlobModules, type HiddenFieldProps, type HtmlFieldProps, type HttpAuthConfig, type HttpBodyConfig, type HttpEndpointConfig, type HttpRetryConfig, type HttpSubmitAction, HubForm, type HubFormProps, type InputGroupFieldProps, type InputOTPGroupProps, type InputOTPProps, type InputOTPSlotProps, type InputProps, type InputSize, type IntegrationSubmitAction, InternalComponentProvider, type InternalComponentProviderProps, type LabelProps, MissingComponentFallback, type MissingComponentFallbackProps, type NativeSelectFieldProps, type NativeSelectProps, OPTION_BASED_TYPES, type Option, type OtpFieldProps, type PartialComponentRegistry, PluginManager, type PopoverContentProps, type PopoverProps, type PopoverTriggerProps, type PropertyMapping, type RadioFieldProps, type RadioGroupItemProps, type RadioGroupProps, type RangeFieldProps, type RecaptchaConfig, type RecaptchaPluginConfig, type RepeaterFieldProps, type ResolvedComponents, SUSPENSE_CONFIG, type SchemaType, type SelectContentProps, type SelectFieldProps, type SelectItemProps, type SelectProps, type SelectTriggerProps, type SelectValueProps, type SeparatorProps, type SerializableFieldResolver, type SerializationHint, type SliderFieldProps, type SliderProps, type Step, type StepCondition, type StepInfo, type StepStatus, type Steps, StepsAccordion, StepsNavigation, StepsProgress, type SubmitAction, type SubmitActionCondition, type SubmitActionNode, type SubmitActionResult, type SubmitActionType, type SubmitActionsResult, type SubmitConfig, type SubmitConfirmationConfig, type SubmitExecutionConfig, type SubmitTrigger, type SubmitTriggerType, type SubmitType, type SwitchFieldProps, type SwitchGroupFieldProps, type SwitchProps, TEMPLATES, TEMPLATE_META, TYPE_SPECIFIC_PROPERTIES, type TemplateId, type TemplateMeta, type TestDataLocale, type TestDataOptions, type TextFieldProps, type TextareaFieldProps, type TextareaProps, type TooltipContentProps, type TooltipProps, type TooltipProviderProps, type TooltipTriggerProps, type UseSubmitConfirmationReturn, VALIDATION_METHODS, type ValidateComponentRegistry, type ValidationContext, type ValidationPresetMeta, type ValidationRules, type WebhookSubmitAction, analyticsPlugin, applyBuiltinTransform, applyFieldMapping, applyFieldMappingSync, applyFieldTransforms, applyTransform, autosavePlugin, compileValidationRules, configureComponents, coreComponents, createComponentRegistry, createContactTemplate, createEventRsvpTemplate, createHubFormSubmit, createLeadGenTemplate, createMissingComponentPlaceholder, createNewsletterTemplate, createQuoteRequestTemplate, createShadcnRegistry, databowlAction, databowlPlugin, defaultSubmit, definePlugin, detectLocale, executeCustomAction, executeEmailAction, executeHttpAction, executeSubmitAction, executeSubmitActions, executeSubmitActionsByTrigger, executeWebhookAction, fieldTypeComponents, generateFieldValue, generateTestData, getActionsByTrigger, getAllKnownMethods, getAvailablePresets, getComponentResolver, getFieldClass, getFormGridClass, getHiddenClasses, getInstallCommand, getMinDelayMs, getMissingComponents, getRequiredComponents, globalPluginManager, groupMissingByPackage, iconVariants, iconVariantsConfig, inputVariants, inputVariantsConfig, isAdvancedMapping, isValidationRules, isZodSchema, localStoragePlugin, mergeComponentRegistries, parseGlobModules, pxToRem, recaptchaPlugin, registerPreset, resolvePreset, resolveValue, resolveValueSync, textareaVariants, textareaVariantsConfig, useComponentMode, useComponents, useComputedFields, useFormLayout, useFormState, useFormStepsInfo, useHasComponentProvider, useHiddenFieldResolvers, usePartialComponents, useRecaptcha, useSubmitActionTriggers, useSubmitConfirmation, withComponents };
|