From 65875fd6b53189cf4f58b1cce50403a0c1ae8d9c Mon Sep 17 00:00:00 2001 From: guynikan Date: Thu, 29 Jan 2026 18:05:03 +0000 Subject: [PATCH 1/9] feat: add single-spa and configure react, vue and vanilla apps --- pnpm-lock.yaml | 190 +++++++++++++++++- pnpm-workspace.yaml | 1 + single-spa-experiment/README.md | 67 ++++++ single-spa-experiment/shell/index.html | 32 +++ single-spa-experiment/shell/package.json | 31 +++ single-spa-experiment/shell/src/main.ts | 49 +++++ .../react/components/ComponentRegistry.tsx | 5 + .../shell/src/react/index.tsx | 41 ++++ .../shell/src/react/pages/HomePage.tsx | 97 +++++++++ .../shell/src/react/pages/ReactFormPage.tsx | 60 ++++++ .../shell/src/vanilla/index.ts | 105 ++++++++++ .../shell/src/vue/components/VueFormPage.vue | 79 ++++++++ single-spa-experiment/shell/src/vue/index.ts | 47 +++++ single-spa-experiment/shell/vite.config.ts | 18 ++ 14 files changed, 821 insertions(+), 1 deletion(-) create mode 100644 single-spa-experiment/README.md create mode 100644 single-spa-experiment/shell/index.html create mode 100644 single-spa-experiment/shell/package.json create mode 100644 single-spa-experiment/shell/src/main.ts create mode 100644 single-spa-experiment/shell/src/react/components/ComponentRegistry.tsx create mode 100644 single-spa-experiment/shell/src/react/index.tsx create mode 100644 single-spa-experiment/shell/src/react/pages/HomePage.tsx create mode 100644 single-spa-experiment/shell/src/react/pages/ReactFormPage.tsx create mode 100644 single-spa-experiment/shell/src/vanilla/index.ts create mode 100644 single-spa-experiment/shell/src/vue/components/VueFormPage.vue create mode 100644 single-spa-experiment/shell/src/vue/index.ts create mode 100644 single-spa-experiment/shell/vite.config.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 690aa8b..794cfe3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -351,6 +351,157 @@ importers: specifier: ^5.0.8 version: 5.4.21(@types/node@20.19.25) + single-spa-experiment/react: + dependencies: + '@emotion/react': + specifier: ^11.11.1 + version: 11.14.0(@types/react@18.3.27)(react@18.3.1) + '@emotion/styled': + specifier: ^11.11.0 + version: 11.14.1(@emotion/react@11.14.0)(@types/react@18.3.27)(react@18.3.1) + '@mui/material': + specifier: ^5.15.0 + version: 5.18.0(@emotion/react@11.14.0)(@emotion/styled@11.14.1)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@schepta/adapter-react': + specifier: workspace:* + version: link:../../packages/adapters/react + '@schepta/core': + specifier: workspace:* + version: link:../../packages/core + '@schepta/factory-react': + specifier: workspace:* + version: link:../../packages/factories/react + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + react-icons: + specifier: ^5.5.0 + version: 5.5.0(react@18.3.1) + single-spa-react: + specifier: ^6.0.1 + version: 6.0.2(@types/react-dom@18.3.7)(@types/react@18.3.27)(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.2.47 + version: 18.3.27 + '@types/react-dom': + specifier: ^18.2.18 + version: 18.3.7(@types/react@18.3.27) + '@vitejs/plugin-react': + specifier: ^4.2.1 + version: 4.7.0(vite@5.4.21) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + vite: + specifier: ^5.0.8 + version: 5.4.21(@types/node@20.19.25) + vite-plugin-single-spa: + specifier: ^1.1.0 + version: 1.1.0(vite@5.4.21) + + single-spa-experiment/shell: + dependencies: + '@emotion/react': + specifier: ^11.11.0 + version: 11.14.0(@types/react@18.3.27)(react@18.3.1) + '@emotion/styled': + specifier: ^11.11.0 + version: 11.14.1(@emotion/react@11.14.0)(@types/react@18.3.27)(react@18.3.1) + '@mui/material': + specifier: ^5.15.0 + version: 5.18.0(@emotion/react@11.14.0)(@emotion/styled@11.14.1)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) + '@schepta/adapter-react': + specifier: workspace:* + version: link:../../packages/adapters/react + '@schepta/core': + specifier: workspace:* + version: link:../../packages/core + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + react-icons: + specifier: ^5.0.0 + version: 5.5.0(react@18.3.1) + single-spa: + specifier: ^6.0.1 + version: 6.0.3 + single-spa-react: + specifier: ^6.0.0 + version: 6.0.2(@types/react-dom@18.3.7)(@types/react@18.3.27)(react@18.3.1) + single-spa-vue: + specifier: ^3.0.1 + version: 3.0.1 + vue: + specifier: ^3.4.0 + version: 3.5.25(typescript@5.9.3) + devDependencies: + '@vitejs/plugin-react': + specifier: ^4.2.1 + version: 4.7.0(vite@5.4.21) + '@vitejs/plugin-vue': + specifier: ^5.0.0 + version: 5.2.4(vite@5.4.21)(vue@3.5.25) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + vite: + specifier: ^5.0.8 + version: 5.4.21(@types/node@20.19.25) + vite-plugin-single-spa: + specifier: ^1.1.0 + version: 1.1.0(vite@5.4.21) + + single-spa-experiment/vanilla: + dependencies: + '@schepta/core': + specifier: workspace:* + version: link:../../packages/core + devDependencies: + typescript: + specifier: ^5.3.3 + version: 5.9.3 + vite: + specifier: ^5.0.8 + version: 5.4.21(@types/node@20.19.25) + vite-plugin-single-spa: + specifier: ^1.1.0 + version: 1.1.0(vite@5.4.21) + + single-spa-experiment/vue: + dependencies: + '@schepta/adapter-vue': + specifier: workspace:* + version: link:../../packages/adapters/vue + '@schepta/core': + specifier: workspace:* + version: link:../../packages/core + single-spa-vue: + specifier: ^3.0.0 + version: 3.0.1 + vue: + specifier: ^3.3.0 + version: 3.5.25(typescript@5.9.3) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^5.0.0 + version: 5.2.4(vite@5.4.21)(vue@3.5.25) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + vite: + specifier: ^5.0.8 + version: 5.4.21(@types/node@20.19.25) + vite-plugin-single-spa: + specifier: ^1.1.0 + version: 1.1.0(vite@5.4.21) + tests: devDependencies: '@playwright/test': @@ -2104,7 +2255,6 @@ packages: '@types/react': ^18.0.0 dependencies: '@types/react': 18.3.27 - dev: true /@types/react-transition-group@4.4.12(@types/react@18.3.27): resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} @@ -2626,6 +2776,10 @@ packages: balanced-match: 1.0.2 dev: true + /browserslist-config-single-spa@1.0.1: + resolution: {integrity: sha512-nqOxTbatv6FcdgBvUTuH4MuojMZwvskspz5Y4dmpVcKd0uaQY8KEl3iALWus16+AwPVe3BIerBNEgELyaHZcQg==} + dev: false + /browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -4879,6 +5033,32 @@ packages: engines: {node: '>=14'} dev: true + /single-spa-react@6.0.2(@types/react-dom@18.3.7)(@types/react@18.3.27)(react@18.3.1): + resolution: {integrity: sha512-nRz5Izf57iYZXMXdTh+RmHnxxQojFL8NCIVUyA7PpXRib1ykmbat6JgT3yFPgXRs129BCC4+j+O5TLQOmLzF4Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: '*' + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@types/react': 18.3.27 + '@types/react-dom': 18.3.7(@types/react@18.3.27) + browserslist-config-single-spa: 1.0.1 + react: 18.3.1 + dev: false + + /single-spa-vue@3.0.1: + resolution: {integrity: sha512-GX6uQBBMGG3/ZQh6Z+tuqKGIuWKMbDhj3O/6xwX4j0NQJhW9kynHXbOWwwel1DFq6tkjzk7a0qBlhJwRCgn/lg==} + dev: false + + /single-spa@6.0.3: + resolution: {integrity: sha512-pXlwHXHhs3hx/MnGQHBLdFCVAN2rlm9TLBKazbDMyZ25QRxoen2vlUZ47MyXKa9K8Gd5UZs/gkuQad4TGbViwg==} + dev: false + /source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -5418,6 +5598,14 @@ packages: - terser dev: true + /vite-plugin-single-spa@1.1.0(vite@5.4.21): + resolution: {integrity: sha512-L6an1fWRcMtr2xQ7ljnzDVQv241E6R9VwTP09lbK3wo/1/MWm3aHJmRD1rLgbVDTGsRQWfaYInwjirfUxNlkwQ==} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + dependencies: + vite: 5.4.21(@types/node@20.19.25) + dev: true + /vite-plugin-vuetify@2.1.2(vite@5.4.21)(vue@3.5.25)(vuetify@3.11.2): resolution: {integrity: sha512-I/wd6QS+DO6lHmuGoi1UTyvvBTQ2KDzQZ9oowJQEJ6OcjWfJnscYXx2ptm6S7fJSASuZT8jGRBL3LV4oS3LpaA==} engines: {node: ^18.0.0 || >=20.0.0} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e34b522..767b41d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,7 @@ packages: - 'packages/**' - 'showcases/**' + - 'single-spa-experiment/**' - 'tests' - 'docs' diff --git a/single-spa-experiment/README.md b/single-spa-experiment/README.md new file mode 100644 index 0000000..b20ba55 --- /dev/null +++ b/single-spa-experiment/README.md @@ -0,0 +1,67 @@ +# Single-SPA + Vite Experiment + +Boilerplate experimental demonstrando como usar single-spa com Vite para criar microfrontends com diferentes frameworks em um único servidor. + +## Estrutura + +``` +single-spa-vite-experiment/ +├── shell/ # Shell principal (single-spa root config) +├── react-app/ # Microfrontend React +├── vue-app/ # Microfrontend Vue +├── vanilla-app/ # Microfrontend Vanilla JS +└── shared/ # Dependências compartilhadas +``` + +## Características + +- **Single-SPA**: Orquestração de microfrontends +- **Vite**: Build tool rápido para desenvolvimento +- **Importação Local**: Microfrontends como módulos ES locais (não Module Federation) +- **Múltiplos Frameworks**: React, Vue 3, Vanilla JS +- **Schepta Integration**: Preparado para demonstrações do Schepta + +## Como usar + +1. **Instalar dependências**: + ```bash + cd shell && npm install + ``` + +2. **Executar em desenvolvimento**: + ```bash + cd shell && npm run dev + ``` + +3. **Acessar**: + - Home: http://localhost:3000/ + - React: http://localhost:3000/react + - Vue: http://localhost:3000/vue + - Vanilla: http://localhost:3000/vanilla + +## Configuração + +### Shell Principal +- Configura o single-spa e registra os microfrontends +- Gerencia navegação e roteamento +- Serve como container principal + +### Microfrontends +- **React**: Usa single-spa-react + Material-UI +- **Vue**: Usa single-spa-vue + Vue 3 Composition API +- **Vanilla**: Lifecycle functions manuais + +## Vantagens desta abordagem + +1. **Desenvolvimento simples**: Todos os microfrontends no mesmo repositório +2. **Build unificado**: Vite gerencia todos os frameworks +3. **Hot reload**: Funciona para todos os microfrontends +4. **Shared dependencies**: Otimização automática de dependências +5. **Deploy único**: Uma única aplicação para deploy + +## Próximos passos + +- [ ] Integrar Schepta em cada microfrontend +- [ ] Adicionar testes +- [ ] Configurar CI/CD +- [ ] Otimizar build para produção \ No newline at end of file diff --git a/single-spa-experiment/shell/index.html b/single-spa-experiment/shell/index.html new file mode 100644 index 0000000..5ef5b2a --- /dev/null +++ b/single-spa-experiment/shell/index.html @@ -0,0 +1,32 @@ + + + + + + Single-SPA + Vite Experiment + + + + + +
+ + + + \ No newline at end of file diff --git a/single-spa-experiment/shell/package.json b/single-spa-experiment/shell/package.json new file mode 100644 index 0000000..62b05d3 --- /dev/null +++ b/single-spa-experiment/shell/package.json @@ -0,0 +1,31 @@ +{ + "name": "shell", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "single-spa": "^6.0.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "vue": "^3.4.0", + "single-spa-react": "^6.0.0", + "single-spa-vue": "^3.0.1", + "@schepta/core": "workspace:*", + "@schepta/adapter-react": "workspace:*", + "@mui/material": "^5.15.0", + "@emotion/react": "^11.11.0", + "@emotion/styled": "^11.11.0", + "react-icons": "^5.0.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^4.2.1", + "@vitejs/plugin-vue": "^5.0.0", + "vite": "^5.0.8", + "typescript": "^5.3.3", + "vite-plugin-single-spa": "^1.1.0" + } +} \ No newline at end of file diff --git a/single-spa-experiment/shell/src/main.ts b/single-spa-experiment/shell/src/main.ts new file mode 100644 index 0000000..5a34090 --- /dev/null +++ b/single-spa-experiment/shell/src/main.ts @@ -0,0 +1,49 @@ +import { registerApplication, start } from 'single-spa'; +import ReactDOM from "react-dom/client"; +import * as reactApp from './react'; +import * as vueApp from './vue'; +import * as vanillaApp from './vanilla'; +import { HomePage } from './react/pages/HomePage'; +import React from 'react'; + +registerApplication({ + name: 'home', + app: () => Promise.resolve({ + bootstrap: () => Promise.resolve(), + mount: () => { + if (window.location.pathname === '/') { + const root = ReactDOM.createRoot( + document.getElementById("root")!, + ); + root.render(React.createElement(HomePage)); + } + return Promise.resolve(); + }, + unmount: () => { + const container = document.getElementById('root'); + if (container) container.innerHTML = ''; + return Promise.resolve(); + } + }), + activeWhen: '/' +}); + +registerApplication({ + name: 'react', + app: () => Promise.resolve(reactApp), + activeWhen: '/react' +}); + +registerApplication({ + name: 'vue', + app: () => Promise.resolve(vueApp), + activeWhen: '/vue' +}); + +registerApplication({ + name: 'vanilla', + app: () => Promise.resolve(vanillaApp), + activeWhen: '/vanilla' +}); + +start(); \ No newline at end of file diff --git a/single-spa-experiment/shell/src/react/components/ComponentRegistry.tsx b/single-spa-experiment/shell/src/react/components/ComponentRegistry.tsx new file mode 100644 index 0000000..417a7cc --- /dev/null +++ b/single-spa-experiment/shell/src/react/components/ComponentRegistry.tsx @@ -0,0 +1,5 @@ +import { createComponentSpec } from "@schepta/core"; + +export const components = { + // Basic components for demo +}; \ No newline at end of file diff --git a/single-spa-experiment/shell/src/react/index.tsx b/single-spa-experiment/shell/src/react/index.tsx new file mode 100644 index 0000000..b58c5fb --- /dev/null +++ b/single-spa-experiment/shell/src/react/index.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { ScheptaProvider } from "@schepta/adapter-react"; +import { components } from "./components/ComponentRegistry"; +import { ReactFormPage } from "./pages/ReactFormPage"; + +const Root = () => ( + + + +); + +export function bootstrap() { + return Promise.resolve(); +} + +export function mount() { + return new Promise((resolve) => { + const root = ReactDOM.createRoot( + document.getElementById("root")!, + ); + root.render(); + resolve(); + }); +} + +export function unmount() { + return new Promise((resolve) => { + const container = document.getElementById("root"); + if (container) { + ReactDOM.unmountComponentAtNode(container); + } + resolve(); + }); +} \ No newline at end of file diff --git a/single-spa-experiment/shell/src/react/pages/HomePage.tsx b/single-spa-experiment/shell/src/react/pages/HomePage.tsx new file mode 100644 index 0000000..e10de0b --- /dev/null +++ b/single-spa-experiment/shell/src/react/pages/HomePage.tsx @@ -0,0 +1,97 @@ +import React from "react"; + +export function HomePage() { + return ( +
+
+

Bem-vindo ao Single-SPA + Vite Experiment

+
+

Escolha um framework para ver a demonstração do Schepta:

+ + +
+ ); +} diff --git a/single-spa-experiment/shell/src/react/pages/ReactFormPage.tsx b/single-spa-experiment/shell/src/react/pages/ReactFormPage.tsx new file mode 100644 index 0000000..4427d15 --- /dev/null +++ b/single-spa-experiment/shell/src/react/pages/ReactFormPage.tsx @@ -0,0 +1,60 @@ +import React, { useState } from "react"; +import { Tab, Tabs, Paper } from "@mui/material"; +import { FaReact } from "react-icons/fa"; + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +function TabPanel({ children, value, index }: TabPanelProps) { + return ( + + ); +} + +export function ReactFormPage() { + const [tabValue, setTabValue] = useState(0); + + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setTabValue(newValue); + }; + + return ( +
+
+ +

React Microfrontend - Schepta Forms

+
+ +
+ + + + + + + +

Simple React Form

+

Esta é uma demonstração básica do Schepta no React (monorepo unificado)

+
+

✅ Funcionando! Este microfrontend React está rodando dentro do shell principal.

+
+
+ + +

Complex React Form

+

Aqui seria um formulário mais complexo com Schepta

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/single-spa-experiment/shell/src/vanilla/index.ts b/single-spa-experiment/shell/src/vanilla/index.ts new file mode 100644 index 0000000..f18faf8 --- /dev/null +++ b/single-spa-experiment/shell/src/vanilla/index.ts @@ -0,0 +1,105 @@ +let containerElement: HTMLElement | null = null; + +export function bootstrap() { + console.log('Vanilla app bootstrapping'); + return Promise.resolve(); +} + +export function mount(props: any) { + console.log('Vanilla app mounting', props); + return new Promise((resolve) => { + containerElement = document.createElement('div'); + containerElement.style.cssText = ` + padding: 2rem; + background: #fffbf0; + border-radius: 8px; + margin: 1rem 0; + `; + + containerElement.innerHTML = ` +
+ + + +

Vanilla JS Microfrontend - Schepta Forms

+
+ +
+
+ + +
+ +
+
+

Simple Vanilla Form

+

Esta é uma demonstração básica do Schepta em Vanilla JS (monorepo unificado)

+
+

✅ Funcionando! Este microfrontend Vanilla JS está rodando dentro do shell principal.

+
+
+ +
+

Complex Vanilla Form

+

Aqui seria um formulário mais complexo com Schepta

+
+
+
+ `; + + // Add CSS for tabs + const style = document.createElement('style'); + style.textContent = ` + .tabs { display: flex; border-bottom: 1px solid #ddd; margin-bottom: 1rem; } + .tab { padding: 0.5rem 1rem; border: none; background: none; cursor: pointer; border-bottom: 2px solid transparent; font-family: inherit; } + .tab.active { border-bottom-color: #f7df1e; color: #333; font-weight: 600; } + .tab:hover { background-color: #f5f5f5; } + .tab-content { padding: 1rem 0; } + .tab-panel { display: none; } + .tab-panel.active { display: block; } + .tab-panel h3 { margin-bottom: 0.5rem; color: #333; } + .tab-panel p { color: #666; line-height: 1.5; } + `; + document.head.appendChild(style); + + // Add tab functionality + const tabs = containerElement.querySelectorAll('.tab'); + const panels = containerElement.querySelectorAll('.tab-panel'); + + tabs.forEach(tab => { + tab.addEventListener('click', (e) => { + const target = e.target as HTMLButtonElement; + const targetTab = target.dataset.tab; + + // Remove active class from all tabs and panels + tabs.forEach(t => t.classList.remove('active')); + panels.forEach(p => p.classList.remove('active')); + + // Add active class to clicked tab and corresponding panel + target.classList.add('active'); + const panel = containerElement!.querySelector(`#${targetTab}-content`); + panel?.classList.add('active'); + }); + }); + + // Mount to the container passed by single-spa + if (props.domElement) { + props.domElement.appendChild(containerElement); + } else { + document.getElementById('root')?.appendChild(containerElement); + } + + resolve(); + }); +} + +export function unmount() { + console.log('Vanilla app unmounting'); + return new Promise((resolve) => { + if (containerElement && containerElement.parentNode) { + containerElement.parentNode.removeChild(containerElement); + } + containerElement = null; + resolve(); + }); +} \ No newline at end of file diff --git a/single-spa-experiment/shell/src/vue/components/VueFormPage.vue b/single-spa-experiment/shell/src/vue/components/VueFormPage.vue new file mode 100644 index 0000000..9ab7ad4 --- /dev/null +++ b/single-spa-experiment/shell/src/vue/components/VueFormPage.vue @@ -0,0 +1,79 @@ + + + + + \ No newline at end of file diff --git a/single-spa-experiment/shell/src/vue/index.ts b/single-spa-experiment/shell/src/vue/index.ts new file mode 100644 index 0000000..0d0793a --- /dev/null +++ b/single-spa-experiment/shell/src/vue/index.ts @@ -0,0 +1,47 @@ +import { createApp, h } from 'vue'; +import VueFormPage from './components/VueFormPage.vue'; + +let vueApp: any = null; + +export function bootstrap() { + console.log('Vue app bootstrapping'); + return Promise.resolve(); +} + +export function mount(props: any) { + console.log('Vue app mounting', props); + return new Promise((resolve) => { + const container = document.createElement('div'); + + vueApp = createApp({ + render() { + return h(VueFormPage); + }, + }); + + vueApp.provide('appContext', { + user: { id: 2, name: 'Vue User' }, + api: 'https://api.example.com', + }); + + if (props.domElement) { + props.domElement.appendChild(container); + } else { + document.getElementById('root')?.appendChild(container); + } + + vueApp.mount(container); + resolve(); + }); +} + +export function unmount() { + console.log('Vue app unmounting'); + return new Promise((resolve) => { + if (vueApp) { + vueApp.unmount(); + vueApp = null; + } + resolve(); + }); +} \ No newline at end of file diff --git a/single-spa-experiment/shell/vite.config.ts b/single-spa-experiment/shell/vite.config.ts new file mode 100644 index 0000000..cf4eed2 --- /dev/null +++ b/single-spa-experiment/shell/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; +import vitePluginSingleSpa from 'vite-plugin-single-spa'; +import react from '@vitejs/plugin-react'; +import vue from '@vitejs/plugin-vue'; + +export default defineConfig({ + plugins: [ + react(), + vue(), + vitePluginSingleSpa({ + type: 'root', + imo: '3.1.1' + }) + ], + server: { + port: 3000 + } +}); \ No newline at end of file From 8f34e270a2cedc07679f4aa233f9af1374658eb5 Mon Sep 17 00:00:00 2001 From: guynikan Date: Sun, 1 Feb 2026 11:42:28 +0000 Subject: [PATCH 2/9] wip: single-spa (microfrontends) --- pnpm-lock.yaml | 155 ++++--------- pnpm-workspace.yaml | 2 +- shell/index.html | 37 +++ .../shell => shell}/package.json | 13 +- .../shell => shell}/src/main.ts | 22 ++ shell/src/react/App.tsx | 140 ++++++++++++ .../basic-ui/components/ComponentRegistry.tsx | 10 + .../components/Forms/FormWithFormik.tsx | 99 ++++++++ .../basic-ui/components/Forms/FormWithRHF.tsx | 97 ++++++++ .../basic-ui/components/Forms/ModalForm.tsx | 189 ++++++++++++++++ .../components/Forms/NativeComplexForm.tsx | 212 ++++++++++++++++++ .../basic-ui/components/Forms/NativeForm.tsx | 61 +++++ .../basic-ui/components/Inputs/InputText.tsx | 32 +++ .../components/formik/FormikFieldWrapper.tsx | 59 +++++ .../components/formik/FormikFormContainer.tsx | 102 +++++++++ .../components/rhf/RHFFieldWrapper.tsx | 57 +++++ .../components/rhf/RHFFormContainer.tsx | 97 ++++++++ .../react/basic-ui/pages/BasicFormPage.tsx | 52 +++++ .../components/ComponentRegistry.tsx | 88 ++++++++ .../components/Containers/FormContainer.tsx | 28 +++ .../components/Containers/FormField.tsx | 7 + .../Containers/FormSectionContainer.tsx | 7 + .../Containers/FormSectionGroup.tsx | 11 + .../Containers/FormSectionGroupContainer.tsx | 7 + .../Containers/FormSectionTitle.tsx | 11 + shell/src/react/chakra-ui/components/Form.tsx | 51 +++++ .../components/Inputs/InputCheckbox.tsx | 23 ++ .../chakra-ui/components/Inputs/InputDate.tsx | 25 +++ .../components/Inputs/InputNumber.tsx | 35 +++ .../components/Inputs/InputPhone.tsx | 30 +++ .../components/Inputs/InputSelect.tsx | 36 +++ .../chakra-ui/components/Inputs/InputText.tsx | 26 +++ .../components/Inputs/InputTextarea.tsx | 33 +++ .../chakra-ui/components/SubmitButton.tsx | 25 +++ .../react/chakra-ui/pages/ChakraFormPage.tsx | 44 ++++ shell/src/react/components/Header.tsx | 108 +++++++++ shell/src/react/index.css | 18 ++ .../shell => shell}/src/react/index.tsx | 7 +- .../components/ComponentRegistry.tsx | 88 ++++++++ .../components/Containers/FormContainer.tsx | 28 +++ .../components/Containers/FormField.tsx | 7 + .../Containers/FormSectionContainer.tsx | 14 ++ .../Containers/FormSectionGroup.tsx | 21 ++ .../Containers/FormSectionGroupContainer.tsx | 7 + .../Containers/FormSectionTitle.tsx | 19 ++ .../src/react/material-ui/components/Form.tsx | 55 +++++ .../components/Inputs/InputCheckbox.tsx | 27 +++ .../components/Inputs/InputDate.tsx | 26 +++ .../components/Inputs/InputNumber.tsx | 33 +++ .../components/Inputs/InputPhone.tsx | 27 +++ .../components/Inputs/InputSelect.tsx | 35 +++ .../components/Inputs/InputText.tsx | 26 +++ .../components/Inputs/InputTextarea.tsx | 29 +++ .../material-ui/components/SubmitButton.tsx | 23 ++ .../material-ui/pages/MaterialFormPage.tsx | 67 ++++++ .../src/react/pages/HomePage.tsx | 5 +- .../src/react/pages/ReactFormPage.tsx | 0 .../shell => shell}/src/vanilla/index.ts | 0 .../src/vue/components/VueFormPage.vue | 0 .../shell => shell}/src/vue/index.ts | 0 shell/tsconfig.json | 11 + .../shell => shell}/vite.config.ts | 0 single-spa-experiment/README.md | 67 ------ single-spa-experiment/shell/index.html | 32 --- .../react/components/ComponentRegistry.tsx | 5 - 65 files changed, 2483 insertions(+), 225 deletions(-) create mode 100644 shell/index.html rename {single-spa-experiment/shell => shell}/package.json (64%) rename {single-spa-experiment/shell => shell}/src/main.ts (67%) create mode 100644 shell/src/react/App.tsx create mode 100644 shell/src/react/basic-ui/components/ComponentRegistry.tsx create mode 100644 shell/src/react/basic-ui/components/Forms/FormWithFormik.tsx create mode 100644 shell/src/react/basic-ui/components/Forms/FormWithRHF.tsx create mode 100644 shell/src/react/basic-ui/components/Forms/ModalForm.tsx create mode 100644 shell/src/react/basic-ui/components/Forms/NativeComplexForm.tsx create mode 100644 shell/src/react/basic-ui/components/Forms/NativeForm.tsx create mode 100644 shell/src/react/basic-ui/components/Inputs/InputText.tsx create mode 100644 shell/src/react/basic-ui/components/formik/FormikFieldWrapper.tsx create mode 100644 shell/src/react/basic-ui/components/formik/FormikFormContainer.tsx create mode 100644 shell/src/react/basic-ui/components/rhf/RHFFieldWrapper.tsx create mode 100644 shell/src/react/basic-ui/components/rhf/RHFFormContainer.tsx create mode 100644 shell/src/react/basic-ui/pages/BasicFormPage.tsx create mode 100644 shell/src/react/chakra-ui/components/ComponentRegistry.tsx create mode 100644 shell/src/react/chakra-ui/components/Containers/FormContainer.tsx create mode 100644 shell/src/react/chakra-ui/components/Containers/FormField.tsx create mode 100644 shell/src/react/chakra-ui/components/Containers/FormSectionContainer.tsx create mode 100644 shell/src/react/chakra-ui/components/Containers/FormSectionGroup.tsx create mode 100644 shell/src/react/chakra-ui/components/Containers/FormSectionGroupContainer.tsx create mode 100644 shell/src/react/chakra-ui/components/Containers/FormSectionTitle.tsx create mode 100644 shell/src/react/chakra-ui/components/Form.tsx create mode 100644 shell/src/react/chakra-ui/components/Inputs/InputCheckbox.tsx create mode 100644 shell/src/react/chakra-ui/components/Inputs/InputDate.tsx create mode 100644 shell/src/react/chakra-ui/components/Inputs/InputNumber.tsx create mode 100644 shell/src/react/chakra-ui/components/Inputs/InputPhone.tsx create mode 100644 shell/src/react/chakra-ui/components/Inputs/InputSelect.tsx create mode 100644 shell/src/react/chakra-ui/components/Inputs/InputText.tsx create mode 100644 shell/src/react/chakra-ui/components/Inputs/InputTextarea.tsx create mode 100644 shell/src/react/chakra-ui/components/SubmitButton.tsx create mode 100644 shell/src/react/chakra-ui/pages/ChakraFormPage.tsx create mode 100644 shell/src/react/components/Header.tsx create mode 100644 shell/src/react/index.css rename {single-spa-experiment/shell => shell}/src/react/index.tsx (85%) create mode 100644 shell/src/react/material-ui/components/ComponentRegistry.tsx create mode 100644 shell/src/react/material-ui/components/Containers/FormContainer.tsx create mode 100644 shell/src/react/material-ui/components/Containers/FormField.tsx create mode 100644 shell/src/react/material-ui/components/Containers/FormSectionContainer.tsx create mode 100644 shell/src/react/material-ui/components/Containers/FormSectionGroup.tsx create mode 100644 shell/src/react/material-ui/components/Containers/FormSectionGroupContainer.tsx create mode 100644 shell/src/react/material-ui/components/Containers/FormSectionTitle.tsx create mode 100644 shell/src/react/material-ui/components/Form.tsx create mode 100644 shell/src/react/material-ui/components/Inputs/InputCheckbox.tsx create mode 100644 shell/src/react/material-ui/components/Inputs/InputDate.tsx create mode 100644 shell/src/react/material-ui/components/Inputs/InputNumber.tsx create mode 100644 shell/src/react/material-ui/components/Inputs/InputPhone.tsx create mode 100644 shell/src/react/material-ui/components/Inputs/InputSelect.tsx create mode 100644 shell/src/react/material-ui/components/Inputs/InputText.tsx create mode 100644 shell/src/react/material-ui/components/Inputs/InputTextarea.tsx create mode 100644 shell/src/react/material-ui/components/SubmitButton.tsx create mode 100644 shell/src/react/material-ui/pages/MaterialFormPage.tsx rename {single-spa-experiment/shell => shell}/src/react/pages/HomePage.tsx (96%) rename {single-spa-experiment/shell => shell}/src/react/pages/ReactFormPage.tsx (100%) rename {single-spa-experiment/shell => shell}/src/vanilla/index.ts (100%) rename {single-spa-experiment/shell => shell}/src/vue/components/VueFormPage.vue (100%) rename {single-spa-experiment/shell => shell}/src/vue/index.ts (100%) create mode 100644 shell/tsconfig.json rename {single-spa-experiment/shell => shell}/vite.config.ts (100%) delete mode 100644 single-spa-experiment/README.md delete mode 100644 single-spa-experiment/shell/index.html delete mode 100644 single-spa-experiment/shell/src/react/components/ComponentRegistry.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 794cfe3..0b79e77 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -287,13 +287,13 @@ importers: specifier: ^3.4.0 version: 3.5.25(typescript@5.9.3) - showcases: + shell: dependencies: '@chakra-ui/react': specifier: ^2.8.2 version: 2.10.9(@emotion/react@11.14.0)(@emotion/styled@11.14.1)(@types/react@18.3.27)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1) '@emotion/react': - specifier: ^11.11.1 + specifier: ^11.11.0 version: 11.14.0(@types/react@18.3.27)(react@18.3.1) '@emotion/styled': specifier: ^11.11.0 @@ -329,11 +329,23 @@ importers: specifier: ^7.52.2 version: 7.68.0(react@18.3.1) react-icons: - specifier: ^5.5.0 + specifier: ^5.0.0 version: 5.5.0(react@18.3.1) react-router-dom: specifier: ^6.21.0 version: 6.30.2(react-dom@18.3.1)(react@18.3.1) + single-spa: + specifier: ^6.0.1 + version: 6.0.3 + single-spa-react: + specifier: ^6.0.0 + version: 6.0.2(@types/react-dom@18.3.7)(@types/react@18.3.27)(react@18.3.1) + single-spa-vue: + specifier: ^3.0.1 + version: 3.0.1 + vue: + specifier: ^3.4.0 + version: 3.5.25(typescript@5.9.3) devDependencies: '@types/react': specifier: ^18.2.47 @@ -344,45 +356,66 @@ importers: '@vitejs/plugin-react': specifier: ^4.2.1 version: 4.7.0(vite@5.4.21) + '@vitejs/plugin-vue': + specifier: ^5.0.0 + version: 5.2.4(vite@5.4.21)(vue@3.5.25) typescript: specifier: ^5.3.3 version: 5.9.3 vite: specifier: ^5.0.8 version: 5.4.21(@types/node@20.19.25) + vite-plugin-single-spa: + specifier: ^1.1.0 + version: 1.1.0(vite@5.4.21) - single-spa-experiment/react: + showcases: dependencies: + '@chakra-ui/react': + specifier: ^2.8.2 + version: 2.10.9(@emotion/react@11.14.0)(@emotion/styled@11.14.1)(@types/react@18.3.27)(framer-motion@10.18.0)(react-dom@18.3.1)(react@18.3.1) '@emotion/react': specifier: ^11.11.1 version: 11.14.0(@types/react@18.3.27)(react@18.3.1) '@emotion/styled': specifier: ^11.11.0 version: 11.14.1(@emotion/react@11.14.0)(@types/react@18.3.27)(react@18.3.1) + '@hookform/resolvers': + specifier: ^3.9.0 + version: 3.10.0(react-hook-form@7.68.0) '@mui/material': specifier: ^5.15.0 version: 5.18.0(@emotion/react@11.14.0)(@emotion/styled@11.14.1)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) '@schepta/adapter-react': specifier: workspace:* - version: link:../../packages/adapters/react + version: link:../packages/adapters/react '@schepta/core': specifier: workspace:* - version: link:../../packages/core + version: link:../packages/core '@schepta/factory-react': specifier: workspace:* - version: link:../../packages/factories/react + version: link:../packages/factories/react + formik: + specifier: ^2.4.6 + version: 2.4.9(@types/react@18.3.27)(react@18.3.1) + framer-motion: + specifier: ^10.16.16 + version: 10.18.0(react-dom@18.3.1)(react@18.3.1) react: specifier: ^18.2.0 version: 18.3.1 react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: ^7.52.2 + version: 7.68.0(react@18.3.1) react-icons: specifier: ^5.5.0 version: 5.5.0(react@18.3.1) - single-spa-react: - specifier: ^6.0.1 - version: 6.0.2(@types/react-dom@18.3.7)(@types/react@18.3.27)(react@18.3.1) + react-router-dom: + specifier: ^6.21.0 + version: 6.30.2(react-dom@18.3.1)(react@18.3.1) devDependencies: '@types/react': specifier: ^18.2.47 @@ -399,108 +432,6 @@ importers: vite: specifier: ^5.0.8 version: 5.4.21(@types/node@20.19.25) - vite-plugin-single-spa: - specifier: ^1.1.0 - version: 1.1.0(vite@5.4.21) - - single-spa-experiment/shell: - dependencies: - '@emotion/react': - specifier: ^11.11.0 - version: 11.14.0(@types/react@18.3.27)(react@18.3.1) - '@emotion/styled': - specifier: ^11.11.0 - version: 11.14.1(@emotion/react@11.14.0)(@types/react@18.3.27)(react@18.3.1) - '@mui/material': - specifier: ^5.15.0 - version: 5.18.0(@emotion/react@11.14.0)(@emotion/styled@11.14.1)(@types/react@18.3.27)(react-dom@18.3.1)(react@18.3.1) - '@schepta/adapter-react': - specifier: workspace:* - version: link:../../packages/adapters/react - '@schepta/core': - specifier: workspace:* - version: link:../../packages/core - react: - specifier: ^18.2.0 - version: 18.3.1 - react-dom: - specifier: ^18.2.0 - version: 18.3.1(react@18.3.1) - react-icons: - specifier: ^5.0.0 - version: 5.5.0(react@18.3.1) - single-spa: - specifier: ^6.0.1 - version: 6.0.3 - single-spa-react: - specifier: ^6.0.0 - version: 6.0.2(@types/react-dom@18.3.7)(@types/react@18.3.27)(react@18.3.1) - single-spa-vue: - specifier: ^3.0.1 - version: 3.0.1 - vue: - specifier: ^3.4.0 - version: 3.5.25(typescript@5.9.3) - devDependencies: - '@vitejs/plugin-react': - specifier: ^4.2.1 - version: 4.7.0(vite@5.4.21) - '@vitejs/plugin-vue': - specifier: ^5.0.0 - version: 5.2.4(vite@5.4.21)(vue@3.5.25) - typescript: - specifier: ^5.3.3 - version: 5.9.3 - vite: - specifier: ^5.0.8 - version: 5.4.21(@types/node@20.19.25) - vite-plugin-single-spa: - specifier: ^1.1.0 - version: 1.1.0(vite@5.4.21) - - single-spa-experiment/vanilla: - dependencies: - '@schepta/core': - specifier: workspace:* - version: link:../../packages/core - devDependencies: - typescript: - specifier: ^5.3.3 - version: 5.9.3 - vite: - specifier: ^5.0.8 - version: 5.4.21(@types/node@20.19.25) - vite-plugin-single-spa: - specifier: ^1.1.0 - version: 1.1.0(vite@5.4.21) - - single-spa-experiment/vue: - dependencies: - '@schepta/adapter-vue': - specifier: workspace:* - version: link:../../packages/adapters/vue - '@schepta/core': - specifier: workspace:* - version: link:../../packages/core - single-spa-vue: - specifier: ^3.0.0 - version: 3.0.1 - vue: - specifier: ^3.3.0 - version: 3.5.25(typescript@5.9.3) - devDependencies: - '@vitejs/plugin-vue': - specifier: ^5.0.0 - version: 5.2.4(vite@5.4.21)(vue@3.5.25) - typescript: - specifier: ^5.3.3 - version: 5.9.3 - vite: - specifier: ^5.0.8 - version: 5.4.21(@types/node@20.19.25) - vite-plugin-single-spa: - specifier: ^1.1.0 - version: 1.1.0(vite@5.4.21) tests: devDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 767b41d..d05f961 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,7 +1,7 @@ packages: - 'packages/**' - 'showcases/**' - - 'single-spa-experiment/**' + - 'shell/**' - 'tests' - 'docs' diff --git a/shell/index.html b/shell/index.html new file mode 100644 index 0000000..bcd1fa8 --- /dev/null +++ b/shell/index.html @@ -0,0 +1,37 @@ + + + + + + Single-SPA + Vite Experiment + + + + + +
+ + + + diff --git a/single-spa-experiment/shell/package.json b/shell/package.json similarity index 64% rename from single-spa-experiment/shell/package.json rename to shell/package.json index 62b05d3..516f66c 100644 --- a/single-spa-experiment/shell/package.json +++ b/shell/package.json @@ -12,20 +12,29 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "vue": "^3.4.0", + "@chakra-ui/react": "^2.8.2", + "@hookform/resolvers": "^3.9.0", "single-spa-react": "^6.0.0", "single-spa-vue": "^3.0.1", "@schepta/core": "workspace:*", "@schepta/adapter-react": "workspace:*", + "@schepta/factory-react": "workspace:*", "@mui/material": "^5.15.0", "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", - "react-icons": "^5.0.0" + "react-icons": "^5.0.0", + "react-router-dom": "^6.21.0", + "formik": "^2.4.6", + "framer-motion": "^10.16.16", + "react-hook-form": "^7.52.2" }, "devDependencies": { "@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-vue": "^5.0.0", "vite": "^5.0.8", "typescript": "^5.3.3", - "vite-plugin-single-spa": "^1.1.0" + "vite-plugin-single-spa": "^1.1.0", + "@types/react": "^18.2.47", + "@types/react-dom": "^18.2.18" } } \ No newline at end of file diff --git a/single-spa-experiment/shell/src/main.ts b/shell/src/main.ts similarity index 67% rename from single-spa-experiment/shell/src/main.ts rename to shell/src/main.ts index 5a34090..c0cf8f2 100644 --- a/single-spa-experiment/shell/src/main.ts +++ b/shell/src/main.ts @@ -5,6 +5,28 @@ import * as vueApp from './vue'; import * as vanillaApp from './vanilla'; import { HomePage } from './react/pages/HomePage'; import React from 'react'; +import { Header } from './react/components/Header'; + + +registerApplication({ + name: 'header', + app: () => Promise.resolve({ + bootstrap: () => Promise.resolve(), + mount: () => { + const root = ReactDOM.createRoot( + document.getElementById("header")!, + ); + root.render(React.createElement(Header)); + return Promise.resolve(); + }, + unmount: () => { + const container = document.getElementById('header'); + if (container) container.innerHTML = ''; + return Promise.resolve(); + } + }), + activeWhen: '/' +}); registerApplication({ name: 'home', diff --git a/shell/src/react/App.tsx b/shell/src/react/App.tsx new file mode 100644 index 0000000..f65a230 --- /dev/null +++ b/shell/src/react/App.tsx @@ -0,0 +1,140 @@ +import React from "react"; +import { + BrowserRouter, + Routes, + Route, + Link, + useLocation, +} from "react-router-dom"; +import { BasicFormPage } from "./basic-ui/pages/BasicFormPage"; +import { ChakraFormPage } from "./chakra-ui/pages/ChakraFormPage"; +import { MaterialFormPage } from "./material-ui/pages/MaterialFormPage"; +import { FaReact } from "react-icons/fa"; +import { SiChakraui, SiMui } from "react-icons/si"; +import { + AppBar, + Toolbar, + Typography, + Box, + Container, + Tabs, + Tab, +} from "@mui/material"; +import { ScheptaProvider } from "@schepta/adapter-react"; +import { components } from "./basic-ui/components/ComponentRegistry"; + +const navigationItems = [ + { path: "/basic", label: "Basic UI", icon: }, + { path: "/chakra-ui", label: "Chakra UI", icon: }, + { path: "/material-ui", label: "Material UI", icon: }, +]; + +function Header() { + const location = useLocation(); + const currentPath = location.pathname; + + return ( + + + + + Schepta React Showcases + + + + + {navigationItems.map((item) => ( + + {item.label} + {item.icon} + + } + value={item.path} + component={Link} + to={item.path} + /> + ))} + + + + + + ); +} + +function App() { + const labelMiddleware = (props: any) => { + if (props.label) { + return { ...props, label: `[Provider] ${props.label}` }; + } + return props; + }; + + return ( + + + +
+ + + + } + /> + } + /> + } + /> + } + /> + + + + + + ); +} + +export default App; diff --git a/shell/src/react/basic-ui/components/ComponentRegistry.tsx b/shell/src/react/basic-ui/components/ComponentRegistry.tsx new file mode 100644 index 0000000..d2bd3e0 --- /dev/null +++ b/shell/src/react/basic-ui/components/ComponentRegistry.tsx @@ -0,0 +1,10 @@ +import { createComponentSpec } from "@schepta/core"; +import { InputText } from "./Inputs/InputText"; + +export const components = { + InputText: createComponentSpec({ + id: "InputText", + type: "field", + factory: (props, runtime) => InputText, + }), +}; \ No newline at end of file diff --git a/shell/src/react/basic-ui/components/Forms/FormWithFormik.tsx b/shell/src/react/basic-ui/components/Forms/FormWithFormik.tsx new file mode 100644 index 0000000..988b2c1 --- /dev/null +++ b/shell/src/react/basic-ui/components/Forms/FormWithFormik.tsx @@ -0,0 +1,99 @@ +/** + * Form with Formik + * + * Example component demonstrating how to use Schepta with Formik. + * This shows how to inject custom Formik components via the component registry + * with AJV validation using createFormikValidator. + */ + +import React, { useState, useMemo } from 'react'; +import { FormFactory } from '@schepta/factory-react'; +import { + createComponentSpec, + FormSchema, +} from '@schepta/core'; +import { FormikFieldWrapper } from '../formik/FormikFieldWrapper'; +import { FormikFormContainer } from '../formik/FormikFormContainer'; + +interface FormWithFormikProps { + schema: FormSchema; +} + +/** + * FormWithFormik Component + * + * Renders a form using Formik for state management with AJV validation. + * Demonstrates how to integrate external form libraries with Schepta. + */ +export const FormWithFormik: React.FC = ({ schema }) => { + const [submittedValues, setSubmittedValues] = useState | null>(null); + + const formikComponents = useMemo(() => ({ + // Register Formik FieldWrapper - this makes all fields use Formik's context + FieldWrapper: createComponentSpec({ + id: 'FieldWrapper', + type: 'field-wrapper', + factory: () => FormikFieldWrapper, + }), + // Register Formik FormContainer with validation - this provides the Formik context + FormContainer: createComponentSpec({ + id: 'FormContainer', + type: 'FormContainer', + factory: () => FormikFormContainer, + }), + }), []); + + const handleSubmit = (values: Record) => { + console.log('Form submitted (Formik):', values); + setSubmittedValues(values); + }; + + return ( + <> +
+
+ This form uses Formik with AJV validation. + The FieldWrapper and FormContainer are custom Formik implementations. +
+ +
+ {submittedValues && ( +
+

Submitted Values (Formik):

+
+            {JSON.stringify(submittedValues, null, 2)}
+          
+
+ )} + + ); +}; diff --git a/shell/src/react/basic-ui/components/Forms/FormWithRHF.tsx b/shell/src/react/basic-ui/components/Forms/FormWithRHF.tsx new file mode 100644 index 0000000..8732fd0 --- /dev/null +++ b/shell/src/react/basic-ui/components/Forms/FormWithRHF.tsx @@ -0,0 +1,97 @@ +/** + * Form with React Hook Form + * + * Example component demonstrating how to use Schepta with react-hook-form. + * This shows how to inject custom RHF components via the component registry + * with AJV validation using @hookform/resolvers/ajv. + */ + +import React, { useState, useMemo } from 'react'; +import { FormFactory } from '@schepta/factory-react'; +import { createComponentSpec, FormSchema } from '@schepta/core'; +import { RHFFormContainer } from '../rhf/RHFFormContainer'; +import { RHFFieldWrapper } from '../rhf/RHFFieldWrapper'; + +interface FormWithRHFProps { + schema: FormSchema; +} + +/** + * FormWithRHF Component + * + * Renders a form using react-hook-form for state management with AJV validation. + * Demonstrates how to integrate external form libraries with Schepta. + */ +export const FormWithRHF: React.FC = ({ schema }) => { + const [submittedValues, setSubmittedValues] = useState | null>(null); + + // Create RHF components with validation config + const rhfComponents = useMemo(() => ({ + // Register RHF FieldWrapper - this makes all fields use RHF's Controller + FieldWrapper: createComponentSpec({ + id: 'FieldWrapper', + type: 'field-wrapper', + factory: () => RHFFieldWrapper, + }), + // Register RHF FormContainer with validation - this provides the FormProvider context + FormContainer: createComponentSpec({ + id: 'FormContainer', + type: 'FormContainer', + factory: () => RHFFormContainer, + }), + }), []); + + const handleSubmit = (values: Record) => { + console.log('Form submitted (RHF):', values); + setSubmittedValues(values); + }; + + return ( + <> +
+
+ This form uses react-hook-form with AJV validation. + The FieldWrapper and FormContainer are custom RHF implementations. +
+ +
+ {submittedValues && ( +
+

Submitted Values (RHF):

+
+            {JSON.stringify(submittedValues, null, 2)}
+          
+
+ )} + + ); +}; diff --git a/shell/src/react/basic-ui/components/Forms/ModalForm.tsx b/shell/src/react/basic-ui/components/Forms/ModalForm.tsx new file mode 100644 index 0000000..a9bdf5f --- /dev/null +++ b/shell/src/react/basic-ui/components/Forms/ModalForm.tsx @@ -0,0 +1,189 @@ +import { FormFactory } from "@schepta/factory-react"; +import type { FormFactoryRef } from "@schepta/factory-react"; +import React, { useState, useRef } from "react"; +import { FormSchema } from "@schepta/core"; + +interface FormModalProps { + schema: FormSchema; + onSubmit?: (values: Record) => void; +} + +/** + * Example component demonstrating external submit button usage. + * The form is rendered in a modal-like structure where the submit button + * is outside the FormFactory component (in the footer). + */ +export const ModalForm = ({ schema, onSubmit }: FormModalProps) => { + const formRef = useRef(null); + const [submittedValues, setSubmittedValues] = useState | null>(null); + const [isOpen, setIsOpen] = useState(false); + + const handleSubmit = (values: Record) => { + console.log('Form submitted:', values); + setSubmittedValues(values); + setIsOpen(false); + onSubmit?.(values); + }; + + const handleExternalSubmit = () => { + // Call submit on the form ref, passing the submit handler + formRef.current?.submit(handleSubmit); + }; + + return ( + <> + {/* Button to open modal */} + + + {/* Modal overlay */} + {isOpen && ( +
setIsOpen(false)} + > + {/* Modal container */} +
e.stopPropagation()} + style={{ + backgroundColor: 'white', + borderRadius: '12px', + width: '100%', + maxWidth: '500px', + maxHeight: '90vh', + display: 'flex', + flexDirection: 'column', + boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)', + }} + > + {/* Modal header */} +
+

+ External Submit Example +

+ +
+ + {/* Modal body - Form WITHOUT onSubmit (no internal button) */} +
+ +
+ + {/* Modal footer - External submit button */} +
+ + +
+
+
+ )} + + {/* Display submitted values */} + {submittedValues && ( +
+

Submitted Values:

+
+            {JSON.stringify(submittedValues, null, 2)}
+          
+
+ )} + + ); +}; diff --git a/shell/src/react/basic-ui/components/Forms/NativeComplexForm.tsx b/shell/src/react/basic-ui/components/Forms/NativeComplexForm.tsx new file mode 100644 index 0000000..d6e4685 --- /dev/null +++ b/shell/src/react/basic-ui/components/Forms/NativeComplexForm.tsx @@ -0,0 +1,212 @@ +import React, { useState, useMemo } from "react"; +import { FormFactory, useScheptaFormAdapter } from "@schepta/factory-react"; +import { FormSchema, generateValidationSchema, createComponentSpec } from "@schepta/core"; + +interface FormProps { + schema: FormSchema; + initialValues?: Record; +} + +/** + * Custom component for Social Name input with toggle behavior + * - When openSocialName is false: shows a button to add + * - When openSocialName is true: shows the input and a button to remove + */ +interface SocialNameInputProps { + name: string; + schema: any; + externalContext: { + openSocialName: boolean; + setOpenSocialName: (value: boolean) => void; + }; +} + +const SocialNameInput = ({ name, schema, externalContext }: SocialNameInputProps) => { + const { openSocialName, setOpenSocialName } = externalContext; + const label = schema['x-component-props']?.label || 'Social Name'; + const placeholder = schema['x-component-props']?.placeholder || ''; + + // Get form adapter to register the field + const adapter = useScheptaFormAdapter(); + const register = adapter?.register; + + return ( +
+ + + {openSocialName && ( +
+ + +
+ )} +
+ ); +}; + +export const NativeComplexForm = ({ schema }: FormProps) => { + const [submittedValues, setSubmittedValues] = useState | null>(null); + const [openSocialName, setOpenSocialName] = useState(false); + + const initialValues = useMemo(() => { + const { initialValues: schemaInitialValues } = + generateValidationSchema(schema); + return { + ...schemaInitialValues, + ...{ + userInfo: { + enrollment: "8743", + }, + }, + }; + }, [schema]); + + // Register custom components for x-custom fields + const customComponents = useMemo(() => ({ + socialName: createComponentSpec({ + id: 'socialName', // Same as the key name in the schema + type: 'field', + factory: () => SocialNameInput, + }), + }), []); + + const handleSubmit = (values: Record) => { + console.log("Form submitted:", values); + setSubmittedValues(values); + }; + + return ( + <> +
+

+ What you can see in this form: +

+
    +
  • + Custom Components (x-custom): Social Name field with toggle behavior +
  • +
  • + Template Expressions: Conditional visibility (spouseName appears when maritalStatus is 'married') +
  • +
  • + Disabled Fields: Enrollment field is disabled +
  • +
  • + Required Fields: Email, Phone, First Name, Last Name, Accept Terms +
  • +
  • + Grid Layout: 2-column grid with full-width fields (socialName, spouseName) +
  • +
  • + Input Types: Text, Phone, Select, Date, Textarea, Checkbox +
  • +
  • + Sections: Organized with section containers and titles +
      +
    • + User Information contains two subsections: +
        +
      • basicInfo: enrollment, firstName, lastName, socialName, userType, birthDate, maritalStatus, spouseName
      • +
      • additionalInfo: bio, acceptTerms
      • +
      +
    • +
    +
  • +
  • + Field Ordering: Custom order via x-ui.order +
  • +
  • + Initial Values: Pre-filled enrollment value +
  • +
  • + External Context: State management for custom components +
  • +
+
+
+ +
+ {submittedValues && ( +
+

Submitted Values:

+
+            {JSON.stringify(submittedValues, null, 2)}
+          
+
+ )} + + ); +}; diff --git a/shell/src/react/basic-ui/components/Forms/NativeForm.tsx b/shell/src/react/basic-ui/components/Forms/NativeForm.tsx new file mode 100644 index 0000000..2ad93b1 --- /dev/null +++ b/shell/src/react/basic-ui/components/Forms/NativeForm.tsx @@ -0,0 +1,61 @@ +import React, { useState, useMemo } from "react"; +import { FormFactory } from '@schepta/factory-react'; +import { FormSchema, generateValidationSchema } from "@schepta/core"; + +interface FormProps { + schema: FormSchema; + initialValues?: Record; +} + +export const NativeForm = ({ schema, initialValues: externalInitialValues }: FormProps) => { + const [submittedValues, setSubmittedValues] = useState | null>(null); + + const initialValues = useMemo(() => { + const { initialValues: schemaInitialValues } = generateValidationSchema(schema); + return { ...schemaInitialValues, ...externalInitialValues }; + }, [schema, externalInitialValues]); + + const handleSubmit = (values: Record) => { + console.log('Form submitted:', values); + setSubmittedValues(values); + }; + + return ( + <> +
+ +
+ {submittedValues && ( +
+

Submitted Values:

+
+            {JSON.stringify(submittedValues, null, 2)}
+          
+
+ )} + + ); +}; diff --git a/shell/src/react/basic-ui/components/Inputs/InputText.tsx b/shell/src/react/basic-ui/components/Inputs/InputText.tsx new file mode 100644 index 0000000..0ab0676 --- /dev/null +++ b/shell/src/react/basic-ui/components/Inputs/InputText.tsx @@ -0,0 +1,32 @@ +import type { InputTextProps } from '@schepta/factory-react'; +import React from "react"; + +export const InputText: React.FC = ({ label, name, value, onChange, placeholder, externalContext, ...rest }) => { + return ( +
+ {label && ( + + )} + onChange?.(e.target.value)} + style={{ + width: "100%", + padding: "8px", + border: "1px solid #ccc", + borderRadius: "4px", + }} + {...rest} + /> +
+ ); +}; \ No newline at end of file diff --git a/shell/src/react/basic-ui/components/formik/FormikFieldWrapper.tsx b/shell/src/react/basic-ui/components/formik/FormikFieldWrapper.tsx new file mode 100644 index 0000000..f2d4a69 --- /dev/null +++ b/shell/src/react/basic-ui/components/formik/FormikFieldWrapper.tsx @@ -0,0 +1,59 @@ +/** + * Formik Field Wrapper + * + * Custom FieldWrapper that uses Formik's context. + * This demonstrates how to integrate Formik with Schepta forms. + */ + +import React from 'react'; +import { useFormikContext } from 'formik'; +import type { FieldWrapperProps } from '@schepta/factory-react'; + +/** + * Formik-based FieldWrapper component. + * Uses Formik's context to bind fields to form state. + * + * Register this component via the components prop to use Formik + * for form state management. + * + * @example + * ```tsx + * import { createComponentSpec } from '@schepta/core'; + * + * const components = { + * FieldWrapper: createComponentSpec({ + * id: 'FieldWrapper', + * type: 'wrapper', + * factory: () => FormikFieldWrapper, + * }), + * }; + * ``` + */ +export const FormikFieldWrapper: React.FC = ({ + name, + component: Component, + componentProps = {}, + children, +}) => { + const formik = useFormikContext>(); + + // Get value from Formik state (support nested paths) + const value = name.split('.').reduce((obj: any, key) => { + return obj?.[key]; + }, formik.values); + + const handleChange = (newValue: any) => { + formik.setFieldValue(name, newValue); + }; + + return ( + + {children} + + ); +}; diff --git a/shell/src/react/basic-ui/components/formik/FormikFormContainer.tsx b/shell/src/react/basic-ui/components/formik/FormikFormContainer.tsx new file mode 100644 index 0000000..25497ec --- /dev/null +++ b/shell/src/react/basic-ui/components/formik/FormikFormContainer.tsx @@ -0,0 +1,102 @@ +/** + * Formik Form Container + * + * Custom FormContainer that uses Formik for state management + * with AJV validation via createFormikValidator. + */ + +import React, { useMemo } from "react"; +import { Formik, Form } from "formik"; +import type { FormContainerProps } from "@schepta/factory-react"; +import { + createFormikValidator, + FormSchema, + generateValidationSchema, +} from "@schepta/core"; +import { useSchepta } from "@schepta/adapter-react"; + +/** + * Creates a Formik-based FormContainer component with validation. + * Uses Formik with a custom validate function powered by AJV. + * + * @param validate - Validation function from createFormikValidator + * @param defaultValues - Default form values + * @returns A FormContainer component configured with validation + * + + */ +export const FormikFormContainer: React.FC = ({ + children, + onSubmit, + ...props +}) => { + const { schema } = useSchepta(); + const { initialValues, validate } = useMemo(() => { + const { initialValues } = generateValidationSchema(schema as FormSchema, { + messages: { + en: { + required: '{{label}} is required', + minLength: '{{label}} must be at least {{minLength}} characters', + maxLength: '{{label}} must be at most {{maxLength}} characters', + pattern: '{{label}} format is invalid', + } + }, + locale: 'en' + }); + + const validate = createFormikValidator(schema as FormSchema, { + messages: { + en: { + required: '{{label}} is required', + minLength: '{{label}} must be at least {{minLength}} characters', + maxLength: '{{label}} must be at most {{maxLength}} characters', + pattern: '{{label}} format is invalid', + } + }, + locale: 'en' + }); + + return { initialValues, validate }; + }, [schema]); + + const handleSubmit = (values: Record) => { + if (onSubmit) { + onSubmit(values); + } + }; + + return ( + + {({ isSubmitting, errors, touched }) => ( +
+ {children} + {onSubmit && ( +
+ +
+ )} +
+ )} +
+ ); +} diff --git a/shell/src/react/basic-ui/components/rhf/RHFFieldWrapper.tsx b/shell/src/react/basic-ui/components/rhf/RHFFieldWrapper.tsx new file mode 100644 index 0000000..a64f065 --- /dev/null +++ b/shell/src/react/basic-ui/components/rhf/RHFFieldWrapper.tsx @@ -0,0 +1,57 @@ +/** + * RHF Field Wrapper + * + * Custom FieldWrapper that uses react-hook-form's Controller. + * This demonstrates how to integrate RHF with Schepta forms. + */ + +import React from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import type { FieldWrapperProps } from '@schepta/factory-react'; + +/** + * RHF-based FieldWrapper component. + * Uses react-hook-form's Controller to bind fields to form state. + * + * Register this component via the components prop to use RHF + * for form state management. + * + * @example + * ```tsx + * import { createComponentSpec } from '@schepta/core'; + * + * const components = { + * FieldWrapper: createComponentSpec({ + * id: 'FieldWrapper', + * type: 'wrapper', + * factory: () => RHFFieldWrapper, + * }), + * }; + * ``` + */ +export const RHFFieldWrapper: React.FC = ({ + name, + component: Component, + componentProps = {}, + children, +}) => { + const { control } = useFormContext(); + + return ( + ( + field.onChange(value)} + > + {children} + + )} + /> + ); +}; diff --git a/shell/src/react/basic-ui/components/rhf/RHFFormContainer.tsx b/shell/src/react/basic-ui/components/rhf/RHFFormContainer.tsx new file mode 100644 index 0000000..b538ea0 --- /dev/null +++ b/shell/src/react/basic-ui/components/rhf/RHFFormContainer.tsx @@ -0,0 +1,97 @@ +/** + * RHF Form Container + * + * Custom FormContainer that uses react-hook-form for state management + * with AJV validation via @hookform/resolvers/ajv. + */ + +import React, { useMemo } from 'react'; +import { useForm, FormProvider } from 'react-hook-form'; +import { ajvResolver } from '@hookform/resolvers/ajv'; +import type { FormContainerProps } from '@schepta/factory-react'; +import { FormSchema, generateValidationSchema } from '@schepta/core'; +import { useSchepta } from '@schepta/adapter-react'; + +/** + * Creates an RHF-based FormContainer component with validation. + * Uses react-hook-form's useForm with ajvResolver for validation. + * + * @param jsonSchema - JSON Schema for AJV validation + * @param defaultValues - Default form values + * @returns A FormContainer component configured with validation + * + * @example + * ```tsx + * import { createComponentSpec, generateValidationSchema } from '@schepta/core'; + * import { createRHFFormContainer } from './RHFFormContainer'; + * + * const { jsonSchema, initialValues } = generateValidationSchema(formSchema); + * + * const components = { + * FormContainer: createComponentSpec({ + * id: 'FormContainer', + * type: 'FormContainer', + * factory: () => createRHFFormContainer(jsonSchema, initialValues), + * }), + * }; + * ``` + */ + +export const RHFFormContainer: React.FC = ({ + children, + onSubmit, + ...props +}) => { + const { schema } = useSchepta(); + const { jsonSchema, initialValues } = useMemo(() => + generateValidationSchema(schema as FormSchema, { + messages: { + en: { + required: '{{label}} is required', + minLength: '{{label}} must be at least {{minLength}} characters', + maxLength: '{{label}} must be at most {{maxLength}} characters', + pattern: '{{label}} format is invalid', + } + }, + locale: 'en' + }), [schema]); + + const methods = useForm({ + defaultValues: initialValues, + resolver: jsonSchema ? ajvResolver(jsonSchema as any) : undefined, + }); + + const handleFormSubmit = methods.handleSubmit((values) => { + if (onSubmit) { + onSubmit(values); + } + }); + + return ( + +
+ {children} + {onSubmit && ( +
+ +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/shell/src/react/basic-ui/pages/BasicFormPage.tsx b/shell/src/react/basic-ui/pages/BasicFormPage.tsx new file mode 100644 index 0000000..3a0909b --- /dev/null +++ b/shell/src/react/basic-ui/pages/BasicFormPage.tsx @@ -0,0 +1,52 @@ +import React, { useState } from "react"; +import { Tab, Tabs, Paper } from "@mui/material"; +import simpleFormSchema from "../../../../../instances/form/simple-form.json"; +import complexFormSchema from "../../../../../instances/form/complex-form.json"; +import { NativeForm } from "../components/Forms/NativeForm"; +import { TabPanel } from "../../material-ui/pages/MaterialFormPage"; +import { ModalForm } from "../components/Forms/ModalForm"; +import { FormWithRHF } from "../components/Forms/FormWithRHF"; +import { FormWithFormik } from "../components/Forms/FormWithFormik"; +import { FormSchema } from "@schepta/core"; +import { NativeComplexForm } from "../components/Forms/NativeComplexForm"; + +export function BasicFormPage() { + const [tabValue, setTabValue] = useState(0); + const simpleSchema = simpleFormSchema as FormSchema; + const complexSchema = complexFormSchema as FormSchema; + + + const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { + setTabValue(newValue); + }; + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/shell/src/react/chakra-ui/components/ComponentRegistry.tsx b/shell/src/react/chakra-ui/components/ComponentRegistry.tsx new file mode 100644 index 0000000..1e4f845 --- /dev/null +++ b/shell/src/react/chakra-ui/components/ComponentRegistry.tsx @@ -0,0 +1,88 @@ +import { createComponentSpec } from "@schepta/core"; +import { InputText } from "./Inputs/InputText"; +import { InputSelect } from "./Inputs/InputSelect"; +import { InputCheckbox } from "./Inputs/InputCheckbox"; +import { InputTextarea } from "./Inputs/InputTextarea"; +import { InputNumber } from "./Inputs/InputNumber"; +import { InputDate } from "./Inputs/InputDate"; +import { SubmitButton } from "./SubmitButton"; +import { FormContainer } from "./Containers/FormContainer"; +import { FormField } from "./Containers/FormField"; +import { FormSectionContainer } from "./Containers/FormSectionContainer"; +import { FormSectionTitle } from "./Containers/FormSectionTitle"; +import { FormSectionGroupContainer } from "./Containers/FormSectionGroupContainer"; +import { FormSectionGroup } from "./Containers/FormSectionGroup"; + +export const components = { + 'FormContainer': createComponentSpec({ + id: "FormContainer", + type: "FormContainer", + factory: (props, runtime) => FormContainer, + }), + InputText: createComponentSpec({ + id: "InputText", + type: "field", + factory: (props, runtime) => InputText, + }), + InputSelect: createComponentSpec({ + id: "InputSelect", + type: "field", + factory: (props, runtime) => InputSelect, + }), + InputCheckbox: createComponentSpec({ + id: "InputCheckbox", + type: "field", + factory: (props, runtime) => InputCheckbox, + }), + InputPhone: createComponentSpec({ + id: "InputPhone", + type: "field", + factory: (props, runtime) => InputText, + defaultProps: { type: "tel" }, + }), + InputTextarea: createComponentSpec({ + id: "InputTextarea", + type: "field", + factory: (props, runtime) => InputTextarea, + }), + InputNumber: createComponentSpec({ + id: "InputNumber", + type: "field", + factory: (props, runtime) => InputNumber, + }), + InputDate: createComponentSpec({ + id: "InputDate", + type: "field", + factory: (props, runtime) => InputDate, + }), + SubmitButton: createComponentSpec({ + id: "SubmitButton", + type: 'content', + factory: (props, runtime) => SubmitButton, + }), + FormField: createComponentSpec({ + id: "FormField", + type: 'container', + factory: (props, runtime) => FormField, + }), + FormSectionContainer: createComponentSpec({ + id: "FormSectionContainer", + type: "container", + factory: (props, runtime) => FormSectionContainer, + }), + FormSectionTitle: createComponentSpec({ + id: "FormSectionTitle", + type: 'content', + factory: (props, runtime) => FormSectionTitle, + }), + FormSectionGroupContainer: createComponentSpec({ + id: "FormSectionGroupContainer", + type: 'container', + factory: (props, runtime) => FormSectionGroupContainer, + }), + FormSectionGroup: createComponentSpec({ + id: "FormSectionGroup", + type: 'container', + factory: (props, runtime) => FormSectionGroup, + }), +}; \ No newline at end of file diff --git a/shell/src/react/chakra-ui/components/Containers/FormContainer.tsx b/shell/src/react/chakra-ui/components/Containers/FormContainer.tsx new file mode 100644 index 0000000..69d2b77 --- /dev/null +++ b/shell/src/react/chakra-ui/components/Containers/FormContainer.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { Box } from "@chakra-ui/react"; +import type { FormContainerProps } from "@schepta/factory-react"; +import { SubmitButton } from "../SubmitButton"; +import { useScheptaFormAdapter } from "@schepta/factory-react"; + +export const FormContainer: React.FC = ({ + children, + onSubmit, +}) => { + const adapter = useScheptaFormAdapter(); + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (onSubmit) adapter.handleSubmit(onSubmit)(); + }; + + return ( + + {children} + {onSubmit && } + + ); +}; \ No newline at end of file diff --git a/shell/src/react/chakra-ui/components/Containers/FormField.tsx b/shell/src/react/chakra-ui/components/Containers/FormField.tsx new file mode 100644 index 0000000..5c8ab32 --- /dev/null +++ b/shell/src/react/chakra-ui/components/Containers/FormField.tsx @@ -0,0 +1,7 @@ +import React from "react"; +import { Box, BoxProps } from "@chakra-ui/react"; +import type { FormFieldProps } from "@schepta/factory-react"; + +export const FormField: React.FC = ({ children, ...props }) => { + return {children}; + }; \ No newline at end of file diff --git a/shell/src/react/chakra-ui/components/Containers/FormSectionContainer.tsx b/shell/src/react/chakra-ui/components/Containers/FormSectionContainer.tsx new file mode 100644 index 0000000..ba93b16 --- /dev/null +++ b/shell/src/react/chakra-ui/components/Containers/FormSectionContainer.tsx @@ -0,0 +1,7 @@ +import React from "react"; +import { Box, BoxProps } from "@chakra-ui/react"; +import type { FormSectionContainerProps } from "@schepta/factory-react"; + +export const FormSectionContainer: React.FC = ({ children, ...props }) => { + return {children}; + }; \ No newline at end of file diff --git a/shell/src/react/chakra-ui/components/Containers/FormSectionGroup.tsx b/shell/src/react/chakra-ui/components/Containers/FormSectionGroup.tsx new file mode 100644 index 0000000..6b88f48 --- /dev/null +++ b/shell/src/react/chakra-ui/components/Containers/FormSectionGroup.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { Grid, GridProps } from "@chakra-ui/react"; +import type { FormSectionGroupProps } from "@schepta/factory-react"; + +export const FormSectionGroup: React.FC = ({ children, ...props }) => { + return ( + + {children} + + ); + }; \ No newline at end of file diff --git a/shell/src/react/chakra-ui/components/Containers/FormSectionGroupContainer.tsx b/shell/src/react/chakra-ui/components/Containers/FormSectionGroupContainer.tsx new file mode 100644 index 0000000..bdd8957 --- /dev/null +++ b/shell/src/react/chakra-ui/components/Containers/FormSectionGroupContainer.tsx @@ -0,0 +1,7 @@ +import React from "react"; +import { Box, BoxProps } from "@chakra-ui/react"; +import type { FormSectionGroupContainerProps } from "@schepta/factory-react"; + +export const FormSectionGroupContainer: React.FC = ({ children, ...props }) => { + return {children}; + }; \ No newline at end of file diff --git a/shell/src/react/chakra-ui/components/Containers/FormSectionTitle.tsx b/shell/src/react/chakra-ui/components/Containers/FormSectionTitle.tsx new file mode 100644 index 0000000..7ed8d77 --- /dev/null +++ b/shell/src/react/chakra-ui/components/Containers/FormSectionTitle.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import { Heading, HeadingProps } from "@chakra-ui/react"; +import type { FormSectionTitleProps } from "@schepta/factory-react"; + +export const FormSectionTitle: React.FC = ({ 'x-content': content, children, ...props }) => { + return ( + + {content || children} + + ); + }; \ No newline at end of file diff --git a/shell/src/react/chakra-ui/components/Form.tsx b/shell/src/react/chakra-ui/components/Form.tsx new file mode 100644 index 0000000..7ac56df --- /dev/null +++ b/shell/src/react/chakra-ui/components/Form.tsx @@ -0,0 +1,51 @@ +import React, { useState } from "react"; +import { Box, Code, Heading } from "@chakra-ui/react"; +import { FormSchema } from "@schepta/core"; +import { FormFactory } from "@schepta/factory-react"; +import { components } from "./ComponentRegistry"; + +interface FormProps { + schema: FormSchema; + onSubmit?: (values: Record) => void; +} + +export const Form = ({ schema, onSubmit }: FormProps) => { + const [submittedValues, setSubmittedValues] = useState | null>(null); + + const handleSubmit = (values: Record) => { + console.log('Form submitted:', values); + setSubmittedValues(values); + onSubmit?.(values); + }; + + return ( + <> + + + + {submittedValues && ( + + Submitted Values: + + {JSON.stringify(submittedValues, null, 2)} + + + )} + + ); +}; diff --git a/shell/src/react/chakra-ui/components/Inputs/InputCheckbox.tsx b/shell/src/react/chakra-ui/components/Inputs/InputCheckbox.tsx new file mode 100644 index 0000000..8fec913 --- /dev/null +++ b/shell/src/react/chakra-ui/components/Inputs/InputCheckbox.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { Checkbox, CheckboxProps } from "@chakra-ui/react"; +import type { InputCheckboxProps } from "@schepta/factory-react"; + +export const InputCheckbox: React.FC = ({ + label, + name, + value, + onChange, + ...rest +}) => { + return ( + onChange?.(e.target.checked)} + data-test-id={name} + {...rest} + > + {label} + + ); +}; diff --git a/shell/src/react/chakra-ui/components/Inputs/InputDate.tsx b/shell/src/react/chakra-ui/components/Inputs/InputDate.tsx new file mode 100644 index 0000000..161c2b1 --- /dev/null +++ b/shell/src/react/chakra-ui/components/Inputs/InputDate.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { FormControl, FormLabel, Input, InputProps } from "@chakra-ui/react"; +import type { InputDateProps } from "@schepta/factory-react"; + +export const InputDate: React.FC = ({ + label, + name, + value, + onChange, + ...rest +}) => { + return ( + + {label && {label}} + onChange?.(e.target.value)} + data-test-id={name} + {...rest} + /> + + ); +}; diff --git a/shell/src/react/chakra-ui/components/Inputs/InputNumber.tsx b/shell/src/react/chakra-ui/components/Inputs/InputNumber.tsx new file mode 100644 index 0000000..37218df --- /dev/null +++ b/shell/src/react/chakra-ui/components/Inputs/InputNumber.tsx @@ -0,0 +1,35 @@ +import React from "react"; +import { FormControl, FormLabel, Input, InputProps } from "@chakra-ui/react"; +import type { InputNumberProps } from "@schepta/factory-react"; + +export const InputNumber: React.FC = ({ + label, + name, + value, + onChange, + placeholder, + min, + max, + step, + ...rest +}) => { + return ( + + {label && {label}} + + onChange?.(e.target.value ? Number(e.target.value) : "") + } + min={min} + max={max} + step={step} + data-test-id={name} + {...rest} + /> + + ); +}; diff --git a/shell/src/react/chakra-ui/components/Inputs/InputPhone.tsx b/shell/src/react/chakra-ui/components/Inputs/InputPhone.tsx new file mode 100644 index 0000000..5727bb9 --- /dev/null +++ b/shell/src/react/chakra-ui/components/Inputs/InputPhone.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { InputText } from "./InputText"; +import { InputProps } from "@chakra-ui/react"; +import type { InputPhoneProps } from "@schepta/factory-react"; + +export const InputPhone: React.FC = ({ + label, + name, + value, + onChange, + placeholder, + min, + max, + step, + ...rest +}) => { + return ( + + ); +}; diff --git a/shell/src/react/chakra-ui/components/Inputs/InputSelect.tsx b/shell/src/react/chakra-ui/components/Inputs/InputSelect.tsx new file mode 100644 index 0000000..11c78c9 --- /dev/null +++ b/shell/src/react/chakra-ui/components/Inputs/InputSelect.tsx @@ -0,0 +1,36 @@ +import React from "react"; +import { FormControl, FormLabel, Select, SelectProps } from "@chakra-ui/react"; +import type { InputSelectProps } from "@schepta/factory-react"; + +export const InputSelect: React.FC = ({ + label, + name, + value, + onChange, + options = [], + placeholder = "Select...", + ...rest +}) => { + return ( + + {label && {label}} + + + ); +}; diff --git a/shell/src/react/chakra-ui/components/Inputs/InputText.tsx b/shell/src/react/chakra-ui/components/Inputs/InputText.tsx new file mode 100644 index 0000000..bca6bf3 --- /dev/null +++ b/shell/src/react/chakra-ui/components/Inputs/InputText.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { FormControl, FormLabel, Input, InputProps } from "@chakra-ui/react"; +import type { InputTextProps } from "@schepta/factory-react"; + +export const InputText: React.FC = ({ + label, + name, + value, + onChange, + placeholder, + ...rest +}) => { + return ( + + {label && {label}} + onChange?.(e.target.value)} + data-test-id={name} + {...rest} + /> + + ); +}; diff --git a/shell/src/react/chakra-ui/components/Inputs/InputTextarea.tsx b/shell/src/react/chakra-ui/components/Inputs/InputTextarea.tsx new file mode 100644 index 0000000..def935e --- /dev/null +++ b/shell/src/react/chakra-ui/components/Inputs/InputTextarea.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { + FormControl, + FormLabel, + Textarea, + TextareaProps, +} from "@chakra-ui/react"; +import type { InputTextareaProps } from "@schepta/factory-react"; + +export const InputTextarea: React.FC = ({ + label, + name, + value, + onChange, + placeholder, + rows = 4, + ...rest +}) => { + return ( + + {label && {label}} +