From 0acb005523d62427dc395b7e7c494aabf27172f0 Mon Sep 17 00:00:00 2001 From: Devin Clark Date: Sun, 25 Jan 2026 12:14:36 -0800 Subject: [PATCH 1/9] chore: init shadcn renderer package --- CLAUDE.md | 254 + package.json | 6 + packages/react-shadcn/README.md | 304 + packages/react-shadcn/components.json | 21 + packages/react-shadcn/example/index.html | 12 + packages/react-shadcn/example/package.json | 27 + packages/react-shadcn/example/src/App.tsx | 81 + packages/react-shadcn/example/src/globals.css | 83 + packages/react-shadcn/example/src/main.tsx | 10 + packages/react-shadcn/example/tsconfig.json | 31 + .../react-shadcn/example/tsconfig.node.json | 10 + packages/react-shadcn/example/vite.config.ts | 14 + packages/react-shadcn/package.json | 128 + packages/react-shadcn/rollup.config.js | 53 + packages/react-shadcn/src/cells/TextCell.tsx | 78 + packages/react-shadcn/src/cells/index.ts | 1 + .../src/controls/ShadcnTextControl.tsx | 137 + packages/react-shadcn/src/controls/index.ts | 4 + packages/react-shadcn/src/index.ts | 67 + .../src/layouts/VerticalLayout.tsx | 76 + packages/react-shadcn/src/layouts/index.ts | 4 + packages/react-shadcn/src/renderers.ts | 44 + .../src/shadcn/components/ui/button.tsx | 61 + .../src/shadcn/components/ui/field.tsx | 226 + .../src/shadcn/components/ui/index.ts | 3 + .../src/shadcn/components/ui/input.tsx | 19 + .../src/shadcn/components/ui/label.tsx | 22 + .../src/shadcn/components/ui/select.tsx | 184 + .../src/shadcn/components/ui/separator.tsx | 28 + .../react-shadcn/src/styles/default-theme.css | 48 + packages/react-shadcn/src/styles/index.css | 81 + packages/react-shadcn/src/styles/index.ts | 7 + .../react-shadcn/src/styles/styleContext.tsx | 29 + packages/react-shadcn/src/util/index.ts | 6 + packages/react-shadcn/src/util/props.tsx | 75 + packages/react-shadcn/tsconfig.json | 13 + packages/react-shadcn/tsconfig.test.json | 11 + pnpm-lock.yaml | 5454 +++++++++++++---- 38 files changed, 6356 insertions(+), 1356 deletions(-) create mode 100644 CLAUDE.md create mode 100644 packages/react-shadcn/README.md create mode 100644 packages/react-shadcn/components.json create mode 100644 packages/react-shadcn/example/index.html create mode 100644 packages/react-shadcn/example/package.json create mode 100644 packages/react-shadcn/example/src/App.tsx create mode 100644 packages/react-shadcn/example/src/globals.css create mode 100644 packages/react-shadcn/example/src/main.tsx create mode 100644 packages/react-shadcn/example/tsconfig.json create mode 100644 packages/react-shadcn/example/tsconfig.node.json create mode 100644 packages/react-shadcn/example/vite.config.ts create mode 100644 packages/react-shadcn/package.json create mode 100644 packages/react-shadcn/rollup.config.js create mode 100644 packages/react-shadcn/src/cells/TextCell.tsx create mode 100644 packages/react-shadcn/src/cells/index.ts create mode 100644 packages/react-shadcn/src/controls/ShadcnTextControl.tsx create mode 100644 packages/react-shadcn/src/controls/index.ts create mode 100644 packages/react-shadcn/src/index.ts create mode 100644 packages/react-shadcn/src/layouts/VerticalLayout.tsx create mode 100644 packages/react-shadcn/src/layouts/index.ts create mode 100644 packages/react-shadcn/src/renderers.ts create mode 100644 packages/react-shadcn/src/shadcn/components/ui/button.tsx create mode 100644 packages/react-shadcn/src/shadcn/components/ui/field.tsx create mode 100644 packages/react-shadcn/src/shadcn/components/ui/index.ts create mode 100644 packages/react-shadcn/src/shadcn/components/ui/input.tsx create mode 100644 packages/react-shadcn/src/shadcn/components/ui/label.tsx create mode 100644 packages/react-shadcn/src/shadcn/components/ui/select.tsx create mode 100644 packages/react-shadcn/src/shadcn/components/ui/separator.tsx create mode 100644 packages/react-shadcn/src/styles/default-theme.css create mode 100644 packages/react-shadcn/src/styles/index.css create mode 100644 packages/react-shadcn/src/styles/index.ts create mode 100644 packages/react-shadcn/src/styles/styleContext.tsx create mode 100644 packages/react-shadcn/src/util/index.ts create mode 100644 packages/react-shadcn/src/util/props.tsx create mode 100644 packages/react-shadcn/tsconfig.json create mode 100644 packages/react-shadcn/tsconfig.test.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..ec40b01e6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,254 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +JSON Forms is a declarative framework for automatically generating web forms from JSON Schema. It provides a framework-agnostic core with bindings for React, Angular, and Vue, plus multiple renderer sets (Material-UI, Angular Material, Vuetify, and vanilla HTML/CSS). + +Website: https://jsonforms.io + +## Setup and Prerequisites + +- Node.js v22+ (< 23) +- pnpm 10.4.1+ (managed via corepack or direct install) +- Install dependencies: `pnpm i --frozen-lockfile` + +## Common Commands + +### Building +- `pnpm run build` - Build all packages in the monorepo +- `pnpm run clean` - Delete all dist folders +- `cd packages/ && pnpm run build` - Build a specific package + +### Testing +- `pnpm run test` - Run tests for all packages +- `pnpm run test-cov` - Run tests with coverage for all packages +- `cd packages/core && pnpm run test` - Run core tests (uses AVA) +- `cd packages/react && pnpm run test` - Run React tests (uses Jest) +- `cd packages/angular && pnpm run test` - Run Angular tests (uses AVA) +- `cd packages/vue && pnpm run test` - Run Vue tests (uses Jest) + +### Linting +- `pnpm run lint` - Lint all packages +- `pnpm run lint:fix` - Auto-fix linting issues in all packages + +### Development Servers +Each renderer package has example apps you can run: +- `cd packages/vanilla-renderers && pnpm run dev` - React with vanilla renderers +- `cd packages/material-renderers && pnpm run dev` - React with Material-UI renderers +- `cd packages/angular-material && pnpm run dev` - Angular with Material renderers +- `cd packages/vue-vanilla && pnpm run serve` - Vue with vanilla renderers +- `cd packages/vue-vuetify && pnpm run dev` - Vue with Vuetify renderers + +### Documentation +- `pnpm run doc` - Generate TypeDoc documentation for all packages + +## Architecture + +### Monorepo Structure + +This is a Lerna-managed pnpm workspace monorepo with three types of packages: + +1. **Core Package** (`@jsonforms/core`) + - Framework-agnostic core functionality + - JSON Schema validation (using AJV) + - State management via reducers + - Tester system for renderer dispatch + - UI Schema and JSON Schema type definitions + +2. **Framework Integration Packages** + - `@jsonforms/react` - React Context API integration + - `@jsonforms/angular` - Angular services and directives with RxJS + - `@jsonforms/vue` - Vue 3 Composition API integration + +3. **Renderer Packages** + - `@jsonforms/vanilla-renderers` - Basic HTML/CSS renderers for React + - `@jsonforms/material-renderers` - Material-UI renderers for React + - `@jsonforms/angular-material` - Angular Material renderers + - `@jsonforms/vue-vanilla` - Basic renderers for Vue + - `@jsonforms/vue-vuetify` - Vuetify renderers for Vue + +4. **Support Packages** + - `@jsonforms/examples` - Shared example data, schemas, and UI schemas for testing + +### Key Concepts + +#### UI Schema +Declarative JSON structure that describes form layout and controls: +- `ControlElement` - Binds to data via `scope` (JSON Pointer path like `#/properties/name`) +- Layout elements - `VerticalLayout`, `HorizontalLayout`, `GroupLayout` +- `Categorization` & `Category` - For tabbed/stepped layouts +- `rules` - Dynamic show/hide/enable/disable conditions +- `options` - Customization per control + +#### Testers +Functions that determine which renderer handles which UI schema + JSON schema combination: +```typescript +type RankedTester = ( + uischema: UISchemaElement, + schema: JsonSchema, + context: TesterContext +) => number; +``` +- Returns `-1` (NOT_APPLICABLE) if renderer cannot handle +- Returns positive number indicating priority (higher = more specific) +- Common testers: `isStringControl`, `isBooleanControl`, `isDateControl` +- Combine with `and()`, `or()`, `rankWith(priority, tester)` + +Example: +```typescript +const myTester = rankWith( + 2, // priority + and( + uiTypeIs('Control'), + schemaTypeIs('string'), + formatIs('email') + ) +); +``` + +#### Reducers +Core state management (packages/core/src/reducers/): +- `coreReducer` - data, schema, uischema, validation errors +- `renderers` - registry of available renderers +- `cells` - registry of cell renderers (for tables) +- `config` - app-wide configuration +- `i18n` - internationalization state +- `uischemas` - dynamic UI schema registry + +#### Mappers +Convert store state into component props: +- `mapStateToControlProps` - For control renderers +- `mapStateToLayoutProps` - For layout renderers +- Handle visibility, enablement, validation, labels + +### Framework-Specific Patterns + +#### React +```typescript +// 1. Define component +const MyControl = (props: ControlProps) => { /* render */ }; + +// 2. Create tester +const myControlTester = rankWith(2, isStringControl); + +// 3. Connect to state +export default withJsonFormsControlProps(MyControl); + +// 4. Register +const renderers = [ + { tester: myControlTester, renderer: MyControl } +]; +``` + +#### Angular +```typescript +// 1. Extend JsonFormsControl +@Component({...}) +export class MyControl extends JsonFormsControl {} + +// 2. Create tester +export const myControlTester = rankWith(2, isStringControl); + +// 3. Register in module +JsonFormsAngularService.setRenderers([ + { tester: myControlTester, renderer: MyControl } +]); +``` + +#### Vue +```typescript +// 1. Use composables +export default defineComponent({ + props: rendererProps(), + setup(props) { + const control = useJsonFormsControl(props); + return { control }; + } +}); + +// 2. Create tester +export const myControlTester = rankWith(2, isStringControl); +``` + +### Data Flow + +``` +User Input + → Renderer Component + → handleChange callback + → Framework Integration Layer + → Core Actions (UPDATE_DATA) + → Reducer (coreReducer) + → Validation (AJV) + → Updated State + → Mappers + → Renderer Re-render +``` + +## Testing + +Different packages use different test runners: +- **Core**: AVA (`pnpm run test` in packages/core) +- **React packages**: Jest with ts-jest and Enzyme (`pnpm run test` in packages/react) +- **Angular packages**: AVA (`pnpm run test` in packages/angular) +- **Vue packages**: Jest with Vue Test Utils + +Test files are typically in `test/` directory within each package. + +## Important Development Notes + +### When Creating New Renderers + +1. Determine the UI schema element type and JSON schema properties your renderer handles +2. Create a tester with appropriate priority (higher = more specific) +3. Common priorities: + - 1 = basic type match (e.g., all strings) + - 2 = format-specific (e.g., email, date) + - 3 = very specific (e.g., enum with specific values) + - 10+ = custom overrides +4. Use framework-specific HOC/composable to connect to state +5. Register in the renderers array + +### Working with JSON Pointers + +Scopes use JSON Pointer notation (RFC 6901): +- `#/properties/firstName` - root level property +- `#/properties/address/properties/street` - nested property +- `#/properties/items/0` - array index + +Core utilities in `packages/core/src/util/path.ts` handle path manipulation. + +### Validation + +Validation uses AJV (JSON Schema validator): +- Custom error messages via `errorMessage` in schema +- Custom formats registered via `ajv.addFormat()` +- Custom keywords via `ajv.addKeyword()` +- Validation errors stored in core state and passed to renderers + +### i18n Support + +JSON Forms has built-in i18n: +- Register translators via i18n reducer +- Default translator function for labels +- Override via `options.i18n` in UI schema +- Translate error messages from AJV + +## Contributing + +- All contributors must sign a CLA (checked automatically on PRs) +- Pass all automated checks (tests, linting, formatting) +- Include unit tests for new code +- Add or modify examples to demonstrate new features +- Keep core lean - discuss major features on the [Community Board](https://jsonforms.discourse.group) first + +## Key Files and Locations + +- `packages/core/src/models/` - TypeScript interfaces for schemas +- `packages/core/src/reducers/` - State management +- `packages/core/src/testers/` - Tester functions +- `packages/core/src/util/` - Helper utilities +- `packages//src/` - Framework integration code +- `packages/-/src/` - Renderer implementations diff --git a/package.json b/package.json index 890ae37c6..2070d48ff 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,12 @@ "pnpm": "^10.4.1" }, "packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c", + "pnpm": { + "overrides": { + "@types/react": "^17.0.80", + "@types/react-dom": "^17.0.25" + } + }, "scripts": { "lerna": "lerna", "preparePublish": "git clean -dfx && pnpm i --frozen-lockfile && pnpm run clean && pnpm run build && pnpm run doc && pnpm run test", diff --git a/packages/react-shadcn/README.md b/packages/react-shadcn/README.md new file mode 100644 index 000000000..26943dba1 --- /dev/null +++ b/packages/react-shadcn/README.md @@ -0,0 +1,304 @@ +# @jsonforms/react-shadcn + +shadcn/ui renderer set for JSON Forms (React). + +This package provides form renderers built with [shadcn/ui](https://ui.shadcn.com/) components and styled with Tailwind CSS. It follows the shadcn philosophy of providing copy-pasteable components while respecting your application's design system through CSS variables. + +## Installation + +```bash +npm install @jsonforms/react-shadcn @jsonforms/react @jsonforms/core +``` + +## Prerequisites + +- React 16.12.0 or higher +- **Tailwind CSS 4.0.0 or higher** (required) +- Node.js 22+ (< 23) + +## Setup + +### 1. Install Tailwind CSS 4 + +If you haven't already, install Tailwind CSS 4 and the Vite plugin in your project: + +```bash +npm install -D tailwindcss@latest @tailwindcss/vite +``` + +**Note:** This package requires Tailwind CSS v4 and the `@tailwindcss/vite` plugin. If you're using v3, please upgrade to v4 first. + +### 2. Add CSS Variables + +Add the following CSS variables to your global CSS file (e.g., `globals.css`, `index.css`, or `App.css`): + +```css +@import "tailwindcss"; + +@theme { + --color-background: oklch(100% 0 0); + --color-foreground: oklch(9.8% 0.006 285.8); + --color-card: oklch(100% 0 0); + --color-card-foreground: oklch(9.8% 0.006 285.8); + --color-popover: oklch(100% 0 0); + --color-popover-foreground: oklch(9.8% 0.006 285.8); + --color-primary: oklch(11.2% 0.012 285.8); + --color-primary-foreground: oklch(98% 0.002 285.8); + --color-secondary: oklch(96.1% 0 0); + --color-secondary-foreground: oklch(11.2% 0.012 285.8); + --color-muted: oklch(96.1% 0 0); + --color-muted-foreground: oklch(46.9% 0.004 285.8); + --color-accent: oklch(96.1% 0 0); + --color-accent-foreground: oklch(11.2% 0.012 285.8); + --color-destructive: oklch(60.2% 0.177 29.2); + --color-destructive-foreground: oklch(98% 0.002 285.8); + --color-border: oklch(91.4% 0.002 285.8); + --color-input: oklch(91.4% 0.002 285.8); + --color-ring: oklch(9.8% 0.006 285.8); + --radius: 0.5rem; +} + +/* Dark mode (optional) */ +@media (prefers-color-scheme: dark) { + @theme { + --color-background: oklch(9.8% 0.006 285.8); + --color-foreground: oklch(98% 0.002 285.8); + --color-card: oklch(9.8% 0.006 285.8); + --color-card-foreground: oklch(98% 0.002 285.8); + --color-popover: oklch(9.8% 0.006 285.8); + --color-popover-foreground: oklch(98% 0.002 285.8); + --color-primary: oklch(98% 0.002 285.8); + --color-primary-foreground: oklch(11.2% 0.012 285.8); + --color-secondary: oklch(17.5% 0.009 285.8); + --color-secondary-foreground: oklch(98% 0.002 285.8); + --color-muted: oklch(17.5% 0.009 285.8); + --color-muted-foreground: oklch(65.1% 0.005 285.8); + --color-accent: oklch(17.5% 0.009 285.8); + --color-accent-foreground: oklch(98% 0.002 285.8); + --color-destructive: oklch(30.6% 0.132 29.2); + --color-destructive-foreground: oklch(98% 0.002 285.8); + --color-border: oklch(17.5% 0.009 285.8); + --color-input: oklch(17.5% 0.009 285.8); + --color-ring: oklch(83.9% 0.007 285.8); + } +} +``` + +**Important:** These CSS variables are required for the components to render correctly. Tailwind v4 uses OKLCH color format for better color accuracy and perceptual uniformity. You can customize the colors to match your brand. + +### 3. Configure Vite + +**Required:** Add the Tailwind CSS Vite plugin to your `vite.config.ts`: + +```typescript +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [react(), tailwindcss()], +}); +``` + +**Important:** The `@tailwindcss/vite` plugin is required for Tailwind CSS v4 to work in Vite projects. + +## Usage + +### Basic Example + +```tsx +import { JsonForms } from '@jsonforms/react'; +import { shadcnRenderers, shadcnCells } from '@jsonforms/react-shadcn'; +import { useState } from 'react'; + +const schema = { + type: 'object', + properties: { + firstName: { + type: 'string', + minLength: 3, + }, + lastName: { + type: 'string', + }, + }, + required: ['firstName'], +}; + +const uischema = { + type: 'VerticalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/firstName', + }, + { + type: 'Control', + scope: '#/properties/lastName', + }, + ], +}; + +function App() { + const [data, setData] = useState({}); + + return ( + setData(data)} + /> + ); +} +``` + +## Customization + +### Via CSS Variables (Recommended) + +The easiest way to customize the appearance is by overriding CSS variables in your global CSS: + +```css +@theme { + --color-primary: 220 90% 56%; /* Custom primary color */ + --color-destructive: 0 100% 50%; /* Custom error color */ + --radius: 0.25rem; /* Smaller border radius */ +} +``` + +### Via Style Context (Advanced) + +For component-level customization, use the `ShadcnStyleProvider`: + +```tsx +import { JsonForms } from '@jsonforms/react'; +import { shadcnRenderers, ShadcnStyleProvider } from '@jsonforms/react-shadcn'; + +const styleOverrides = { + inputClasses: 'border-blue-500 focus:ring-blue-500', + labelClasses: 'text-lg font-bold', + errorClasses: 'text-red-600', +}; + +function App() { + return ( + + setData(data)} + /> + + ); +} +``` + +## Dark Mode + +Dark mode is supported via CSS `@media (prefers-color-scheme: dark)` or by adding a `.dark` class: + +```tsx +// Toggle dark mode by adding/removing class on document element +document.documentElement.classList.toggle('dark'); +``` + +Or use a library like [next-themes](https://github.com/pacocoursey/next-themes) for more advanced dark mode support. + +## About shadcn/ui Components + +This package includes shadcn/ui components (Input, Label) as internal implementation details. These components follow shadcn's philosophy of being copy-pasteable and customizable through CSS variables. + +You don't need to install shadcn/ui separately or run the shadcn CLI - the necessary components are bundled with this package and will work with your existing Tailwind CSS setup. + +## Phase 1 - Current Implementation + +This is a Phase 1 implementation focusing on establishing the architecture. Currently supported: + +- ✅ Text input controls (basic string types) +- ✅ VerticalLayout renderer +- ✅ Basic validation display +- ✅ Error messages with destructive styling +- ✅ Description text display +- ✅ Required field indicators +- ✅ Tailwind CSS 4 support with OKLCH colors +- ✅ Email input control + +Not yet implemented (future phases): + +- ⏳ Additional controls (number, boolean, enum, textarea, date, time) +- ⏳ More layout renderers (horizontal, group, categorization) +- ⏳ Complex renderers (arrays, objects, combinators) +- ⏳ Enhanced controls (radio groups, checkboxes, select dropdowns) + +## Tailwind CSS 4 - What's Different? + +This package uses Tailwind CSS v4, which has several improvements: + +- **CSS-first configuration** using `@import "tailwindcss"` instead of `@tailwind` directives +- **Theme variables** defined in CSS using `@theme` instead of JavaScript config +- **Simpler setup** with no need for `tailwind.config.js` or `postcss.config.js` +- **Better performance** with faster build times + +If you're upgrading from Tailwind v3, see the [Tailwind CSS v4 migration guide](https://tailwindcss.com/docs/upgrade-guide). + +## Architecture + +This package follows modern best practices: + +- **No runtime CSS processing**: Tailwind classes are exported as-is and processed by your build pipeline +- **CSS variables for theming**: Respects your application's design system +- **Copy-paste friendly**: shadcn components are included in the package, not as a peer dependency +- **Modern styling**: Uses Tailwind CSS 4 with `@theme` and CSS-first configuration + +## Browser Support + +Modern browsers with ES6 support: + +- Chrome (latest) +- Firefox (latest) +- Safari (latest) +- Edge (latest) + +## Troubleshooting + +### Styles not appearing + +Make sure you've: +1. ✅ Installed Tailwind CSS v4 and the Vite plugin (`npm install -D tailwindcss@latest @tailwindcss/vite`) +2. ✅ Added the Tailwind Vite plugin to your `vite.config.ts` (`plugins: [react(), tailwindcss()]`) +3. ✅ Added the CSS variables to your global CSS file using `@theme` directive +4. ✅ Imported your global CSS file in your app entry point +5. ✅ Using `@import "tailwindcss"` (not `@tailwind` directives from v3) +6. ✅ Using OKLCH color format in `@theme` (not HSL in `:root`) + +### Upgrading from Tailwind v3 to v4 + +1. Install Tailwind v4 and Vite plugin: `npm install -D tailwindcss@latest @tailwindcss/vite` +2. Add the Vite plugin to your `vite.config.ts`: `import tailwindcss from '@tailwindcss/vite'` +3. Remove `tailwind.config.js` and `postcss.config.js` (optional in v4) +4. Replace `@tailwind` directives with `@import "tailwindcss"` +5. Move CSS variables from `:root {}` to `@theme {}` +6. Update color variable names: `--background` → `--color-background` +7. Convert color values from HSL to OKLCH format (e.g., `0 0% 100%` → `oklch(100% 0 0)`) + +See the [Tailwind CSS v4 upgrade guide](https://tailwindcss.com/docs/upgrade-guide) for full details. + +## Contributing + +This package is part of the JSON Forms project. See the main [JSON Forms repository](https://github.com/eclipsesource/jsonforms) for contribution guidelines. + +## License + +MIT + +## Links + +- [JSON Forms Documentation](https://jsonforms.io) +- [shadcn/ui Documentation](https://ui.shadcn.com) +- [Tailwind CSS v4 Documentation](https://tailwindcss.com) +- [GitHub Repository](https://github.com/eclipsesource/jsonforms) diff --git a/packages/react-shadcn/components.json b/packages/react-shadcn/components.json new file mode 100644 index 000000000..e55c1d4b1 --- /dev/null +++ b/packages/react-shadcn/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "radix-vega", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/shadcn/components", + "utils": "@/shadcn/lib/utils", + "ui": "@/shadcn/components/ui", + "lib": "@/shadcn/lib", + "hooks": "@/shadcn/hooks" + } +} diff --git a/packages/react-shadcn/example/index.html b/packages/react-shadcn/example/index.html new file mode 100644 index 000000000..262e2973e --- /dev/null +++ b/packages/react-shadcn/example/index.html @@ -0,0 +1,12 @@ + + + + + + JSON Forms shadcn/ui Example + + +
+ + + diff --git a/packages/react-shadcn/example/package.json b/packages/react-shadcn/example/package.json new file mode 100644 index 000000000..cbca777dd --- /dev/null +++ b/packages/react-shadcn/example/package.json @@ -0,0 +1,27 @@ +{ + "name": "@jsonforms/react-shadcn-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@jsonforms/core": "workspace:*", + "@jsonforms/react": "workspace:*", + "@jsonforms/react-shadcn": "workspace:*", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.18", + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "tailwindcss": "^4.0.0", + "typescript": "^5.2.2", + "vite": "^5.0.8" + } +} diff --git a/packages/react-shadcn/example/src/App.tsx b/packages/react-shadcn/example/src/App.tsx new file mode 100644 index 000000000..4dc946f93 --- /dev/null +++ b/packages/react-shadcn/example/src/App.tsx @@ -0,0 +1,81 @@ +import React, { useState } from 'react'; +import { JsonForms } from '@jsonforms/react'; +import { shadcnRenderers, shadcnCells } from '@jsonforms/react-shadcn'; + +const schema = { + type: 'object', + properties: { + firstName: { + type: 'string', + minLength: 3, + description: 'Please enter your first name', + }, + lastName: { + type: 'string', + minLength: 3, + description: 'Please enter your last name', + }, + email: { + type: 'string', + format: 'email', + }, + }, + required: ['firstName', 'lastName'], +}; + +const uischema = { + type: 'VerticalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/firstName', + }, + { + type: 'Control', + scope: '#/properties/lastName', + }, + { + type: 'Control', + scope: '#/properties/email', + }, + ], +}; + +const initialData = { + firstName: 'John', + lastName: 'Doe', +}; + +function App() { + const [data, setData] = useState(initialData); + + return ( +
+
+

+ JSON Forms shadcn/ui Example +

+ +
+ setData(data)} + /> +
+ +
+

Form Data

+
+            {JSON.stringify(data, null, 2)}
+          
+
+
+
+ ); +} + +export default App; diff --git a/packages/react-shadcn/example/src/globals.css b/packages/react-shadcn/example/src/globals.css new file mode 100644 index 000000000..e44967bca --- /dev/null +++ b/packages/react-shadcn/example/src/globals.css @@ -0,0 +1,83 @@ +@import "tailwindcss"; + +@source "../../src"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.58 0.22 27); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --radius: 0.625rem; +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.87 0.00 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.371 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); +} + +@theme inline { + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --color-foreground: var(--foreground); + --color-background: var(--background); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --radius-2xl: calc(var(--radius) + 8px); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/packages/react-shadcn/example/src/main.tsx b/packages/react-shadcn/example/src/main.tsx new file mode 100644 index 000000000..b1e3e8a2e --- /dev/null +++ b/packages/react-shadcn/example/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './globals.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/packages/react-shadcn/example/tsconfig.json b/packages/react-shadcn/example/tsconfig.json new file mode 100644 index 000000000..4b674e6e8 --- /dev/null +++ b/packages/react-shadcn/example/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Path Aliases */ + "baseUrl": ".", + "paths": { + "@/*": ["../src/*"] + }, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/packages/react-shadcn/example/tsconfig.node.json b/packages/react-shadcn/example/tsconfig.node.json new file mode 100644 index 000000000..42872c59f --- /dev/null +++ b/packages/react-shadcn/example/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/packages/react-shadcn/example/vite.config.ts b/packages/react-shadcn/example/vite.config.ts new file mode 100644 index 000000000..914ee6966 --- /dev/null +++ b/packages/react-shadcn/example/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; +import path from 'path'; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + '@jsonforms/react-shadcn': path.resolve(__dirname, '../src'), + '@': path.resolve(__dirname, '../src'), + }, + }, +}); diff --git a/packages/react-shadcn/package.json b/packages/react-shadcn/package.json new file mode 100644 index 000000000..963d26066 --- /dev/null +++ b/packages/react-shadcn/package.json @@ -0,0 +1,128 @@ +{ + "name": "@jsonforms/react-shadcn", + "version": "3.7.0", + "description": "shadcn/ui Renderer Set for JSON Forms (React)", + "repository": { + "type": "git", + "url": "git+https://github.com/eclipsesource/jsonforms.git", + "directory": "packages/react-shadcn" + }, + "bugs": "https://github.com/eclipsesource/jsonforms/issues", + "homepage": "http://jsonforms.io/", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "main": "lib/jsonforms-react-shadcn.cjs.js", + "module": "lib/jsonforms-react-shadcn.esm.js", + "typings": "lib/index.d.ts", + "scripts": { + "build": "rollup -c rollup.config.js", + "dev": "cd example && pnpm run dev", + "clean": "rimraf lib coverage dist .nyc_output example/dist 2> /dev/null", + "lint": "eslint .", + "lint:fix": "eslint --fix .", + "test": "jest --no-cache", + "test-cov": "jest --no-cache --coverage", + "doc": "typedoc --name 'JSON Forms shadcn/ui Renderers' --excludeExternals --out docs src" + }, + "files": [ + "lib", + "src", + "README.md" + ], + "keywords": [ + "shadcn", + "shadcn-ui", + "tailwind", + "tailwindcss", + "form", + "forms", + "json", + "jsonforms", + "frontend", + "generator", + "input", + "renderengine", + "jsonschema", + "schema", + "uischema", + "layout", + "customization" + ], + "dependencies": { + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "lodash": "^4.17.21", + "lucide-react": "^0.563.0", + "radix-ui": "^1.4.3", + "tailwind-merge": "^2.2.0" + }, + "peerDependencies": { + "@jsonforms/core": "3.7.0", + "@jsonforms/react": "3.7.0", + "react": "^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "tailwindcss": "^4.0.0" + }, + "devDependencies": { + "@jsonforms/core": "workspace:*", + "@jsonforms/react": "workspace:*", + "@rollup/plugin-commonjs": "^23.0.3", + "@rollup/plugin-json": "^5.0.2", + "@rollup/plugin-node-resolve": "^15.0.1", + "@rollup/plugin-replace": "^5.0.1", + "@types/jest": "^29.5.14", + "@types/lodash": "^4.17.0", + "@types/react": "^17.0.24", + "@types/react-dom": "^17.0.9", + "@typescript-eslint/eslint-plugin": "^5.54.1", + "@typescript-eslint/parser": "^5.54.1", + "eslint": "^8.56.0", + "eslint-config-prettier": "^8.7.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-react": "^7.32.2", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jsdom": "^27.2.0", + "prettier": "^2.8.4", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "rimraf": "^6.1.0", + "rollup": "^2.78.0", + "rollup-plugin-cleanup": "^3.2.1", + "rollup-plugin-typescript2": "^0.34.1", + "rollup-plugin-visualizer": "^5.4.1", + "ts-jest": "^29.4.5", + "ts-node": "^10.4.0", + "tslib": "^2.5.0", + "typedoc": "~0.25.3" + }, + "jest": { + "moduleFileExtensions": [ + "ts", + "tsx", + "js" + ], + "transform": { + "^.+\\.(ts|tsx)$": "ts-jest" + }, + "testEnvironment": "jsdom", + "testMatch": [ + "**/test/**/*.test.tsx" + ], + "testPathIgnorePatterns": [ + "/node_modules/", + "/dist/", + "/lib/" + ], + "setupFilesAfterEnv": [ + "/test/setup.ts" + ], + "globals": { + "ts-jest": { + "tsconfig": "tsconfig.test.json" + } + } + } +} diff --git a/packages/react-shadcn/rollup.config.js b/packages/react-shadcn/rollup.config.js new file mode 100644 index 000000000..574b1519a --- /dev/null +++ b/packages/react-shadcn/rollup.config.js @@ -0,0 +1,53 @@ +import typescript from 'rollup-plugin-typescript2'; +import cleanup from 'rollup-plugin-cleanup'; +import { visualizer } from 'rollup-plugin-visualizer'; + +const packageJson = require('./package.json'); + +const baseConfig = { + input: 'src/index.ts', + external: [ + ...Object.keys(packageJson.dependencies ?? {}), + ...Object.keys(packageJson.peerDependencies), + 'react', + 'react-dom', + /^lodash\/.*/, + /^class-variance-authority.*/, + /^clsx.*/, + /^tailwind-merge.*/, + ], +}; + +export default [ + { + ...baseConfig, + output: { + file: packageJson.module, + format: 'esm', + sourcemap: true, + }, + plugins: [ + typescript(), + cleanup({ extensions: ['js', 'ts', 'jsx', 'tsx'] }), + visualizer({ open: false }), + ], + }, + { + ...baseConfig, + output: { + file: packageJson.main, + format: 'cjs', + sourcemap: true, + }, + plugins: [ + typescript({ + tsconfigOverride: { + compilerOptions: { + target: 'ES5', + }, + }, + }), + cleanup({ extensions: ['js', 'ts', 'jsx', 'tsx'] }), + ], + }, +]; diff --git a/packages/react-shadcn/src/cells/TextCell.tsx b/packages/react-shadcn/src/cells/TextCell.tsx new file mode 100644 index 000000000..81dac3da9 --- /dev/null +++ b/packages/react-shadcn/src/cells/TextCell.tsx @@ -0,0 +1,78 @@ +/* + The MIT License + + Copyright (c) 2017-2019 EclipseSource Munich + https://github.com/eclipsesource/jsonforms + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +import React from 'react'; +import { + CellProps, + isStringControl, + RankedTester, + rankWith, +} from '@jsonforms/core'; +import { withJsonFormsCellProps } from '@jsonforms/react'; +import { Input } from '../shadcn/components/ui/input'; +import { cn } from '../shadcn/lib/utils'; +import { withShadcnCellProps } from '../util/props'; +import type { ShadcnRendererProps } from '../util/props'; +import merge from 'lodash/merge'; + +export const TextCell = (props: CellProps & ShadcnRendererProps) => { + const { + config, + data, + id, + enabled, + uischema, + schema, + path, + handleChange, + styleOverrides, + } = props; + + const maxLength = schema.maxLength; + const appliedUiSchemaOptions = merge({}, config, uischema.options); + + return ( + + handleChange(path, ev.target.value === '' ? undefined : ev.target.value) + } + className={cn(styleOverrides?.inputClasses)} + id={id} + disabled={!enabled} + autoFocus={appliedUiSchemaOptions.focus} + placeholder={appliedUiSchemaOptions.placeholder} + maxLength={appliedUiSchemaOptions.restrict ? maxLength : undefined} + /> + ); +}; + +/** + * Default tester for text-based/string controls. + */ +export const textCellTester: RankedTester = rankWith(1, isStringControl); + +export default withJsonFormsCellProps(withShadcnCellProps(TextCell)); diff --git a/packages/react-shadcn/src/cells/index.ts b/packages/react-shadcn/src/cells/index.ts new file mode 100644 index 000000000..8bb287d98 --- /dev/null +++ b/packages/react-shadcn/src/cells/index.ts @@ -0,0 +1 @@ +export { default as TextCell, textCellTester } from './TextCell'; diff --git a/packages/react-shadcn/src/controls/ShadcnTextControl.tsx b/packages/react-shadcn/src/controls/ShadcnTextControl.tsx new file mode 100644 index 000000000..8278cce21 --- /dev/null +++ b/packages/react-shadcn/src/controls/ShadcnTextControl.tsx @@ -0,0 +1,137 @@ +/* + The MIT License + + Copyright (c) 2017-2019 EclipseSource Munich + https://github.com/eclipsesource/jsonforms + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +import React from 'react'; +import { withJsonFormsControlProps } from '@jsonforms/react'; +import { + ControlProps, + isStringControl, + rankWith, + and, + schemaMatches, +} from '@jsonforms/core'; +import { Input } from '../shadcn/components/ui/input'; +import { + Field, + FieldLabel, + FieldDescription, + FieldError, + FieldContent, +} from '../shadcn/components/ui/field'; +import { cn } from '../shadcn/lib/utils'; +import { useShadcnStyles } from '../styles/styleContext'; + +export const ShadcnTextControl = (props: ControlProps) => { + const { + data, + path, + handleChange, + label, + errors, + enabled, + visible, + description, + required, + id, + schema, + } = props; + + const styleOverrides = useShadcnStyles(); + + if (!visible) { + return null; + } + + const hasErrors = errors && errors.length > 0; + const showDescription = description && !hasErrors; + + // Convert JSON Forms error string to FieldError format + const errorArray = hasErrors + ? [{ message: errors }] + : undefined; + + // Determine input type based on schema format + const inputType = schema.format === 'email' ? 'email' : 'text'; + + return ( + + + {label && ( + + {label} + + )} + handleChange(path, e.target.value)} + disabled={!enabled} + className={cn( + hasErrors && 'border-destructive focus-visible:ring-destructive', + styleOverrides?.inputClasses + )} + /> + {showDescription && ( + + {description} + + )} + {hasErrors && ( + + )} + + + ); +}; + +/** + * Tester for ShadcnTextControl. + * Matches string controls with text or email formats. + */ +export const shadcnTextControlTester = rankWith( + 1, + and( + isStringControl, + schemaMatches((schema) => !schema.format || schema.format === 'text' || schema.format === 'email') + ) +); + +export default withJsonFormsControlProps(ShadcnTextControl); diff --git a/packages/react-shadcn/src/controls/index.ts b/packages/react-shadcn/src/controls/index.ts new file mode 100644 index 000000000..2285577be --- /dev/null +++ b/packages/react-shadcn/src/controls/index.ts @@ -0,0 +1,4 @@ +export { + default as ShadcnTextControl, + shadcnTextControlTester, +} from './ShadcnTextControl'; diff --git a/packages/react-shadcn/src/index.ts b/packages/react-shadcn/src/index.ts new file mode 100644 index 000000000..b8b09ed8e --- /dev/null +++ b/packages/react-shadcn/src/index.ts @@ -0,0 +1,67 @@ +/* + The MIT License + + Copyright (c) 2017-2019 EclipseSource Munich + https://github.com/eclipsesource/jsonforms + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +// Renderers and Cells +export * from './renderers'; +export * from './cells'; +export * from './controls'; +export * from './layouts'; + +// shadcn/ui components (for customization) +export * from './shadcn/components/ui'; + +// Styles +export * from './styles'; + +// Utilities +export { cn } from './shadcn/lib/utils'; +export * from './util'; + +/** + * CSS variables required in consumer's globals.css for shadcn/ui theming. + * These should be defined in the :root selector. + */ +export const REQUIRED_CSS_VARIABLES = [ + '--background', + '--foreground', + '--card', + '--card-foreground', + '--popover', + '--popover-foreground', + '--primary', + '--primary-foreground', + '--secondary', + '--secondary-foreground', + '--muted', + '--muted-foreground', + '--accent', + '--accent-foreground', + '--destructive', + '--destructive-foreground', + '--border', + '--input', + '--ring', + '--radius', +] as const; diff --git a/packages/react-shadcn/src/layouts/VerticalLayout.tsx b/packages/react-shadcn/src/layouts/VerticalLayout.tsx new file mode 100644 index 000000000..376bfb9e4 --- /dev/null +++ b/packages/react-shadcn/src/layouts/VerticalLayout.tsx @@ -0,0 +1,76 @@ +/* + The MIT License + + Copyright (c) 2017-2019 EclipseSource Munich + https://github.com/eclipsesource/jsonforms + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +import React from 'react'; +import { + LayoutProps, + RankedTester, + rankWith, + uiTypeIs, + UISchemaElement, + Layout, +} from '@jsonforms/core'; +import { withJsonFormsLayoutProps, JsonFormsDispatch } from '@jsonforms/react'; +import { cn } from '../shadcn/lib/utils'; +import { useShadcnStyles } from '../styles/styleContext'; + +export const VerticalLayout = (props: LayoutProps) => { + const { uischema, schema, path, enabled, visible, renderers, cells } = props; + const styleOverrides = useShadcnStyles(); + + if (!visible) { + return null; + } + + const layout = uischema as Layout; + + return ( +
+ {layout.elements.map((child: UISchemaElement, index: number) => ( + + ))} +
+ ); +}; + +export const verticalLayoutTester: RankedTester = rankWith( + 1, + uiTypeIs('VerticalLayout') +); + +export default withJsonFormsLayoutProps(VerticalLayout); diff --git a/packages/react-shadcn/src/layouts/index.ts b/packages/react-shadcn/src/layouts/index.ts new file mode 100644 index 000000000..699161ee0 --- /dev/null +++ b/packages/react-shadcn/src/layouts/index.ts @@ -0,0 +1,4 @@ +export { + default as VerticalLayout, + verticalLayoutTester, +} from './VerticalLayout'; diff --git a/packages/react-shadcn/src/renderers.ts b/packages/react-shadcn/src/renderers.ts new file mode 100644 index 000000000..d17228b12 --- /dev/null +++ b/packages/react-shadcn/src/renderers.ts @@ -0,0 +1,44 @@ +/* + The MIT License + + Copyright (c) 2017-2019 EclipseSource Munich + https://github.com/eclipsesource/jsonforms + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +import type { + JsonFormsRendererRegistryEntry, + JsonFormsCellRendererRegistryEntry, +} from '@jsonforms/core'; + +import ShadcnTextControl, { + shadcnTextControlTester, +} from './controls/ShadcnTextControl'; +import TextCell, { textCellTester } from './cells/TextCell'; +import VerticalLayout, { verticalLayoutTester } from './layouts/VerticalLayout'; + +export const shadcnRenderers: JsonFormsRendererRegistryEntry[] = [ + { tester: shadcnTextControlTester, renderer: ShadcnTextControl }, + { tester: verticalLayoutTester, renderer: VerticalLayout }, +]; + +export const shadcnCells: JsonFormsCellRendererRegistryEntry[] = [ + { tester: textCellTester, cell: TextCell }, +]; diff --git a/packages/react-shadcn/src/shadcn/components/ui/button.tsx b/packages/react-shadcn/src/shadcn/components/ui/button.tsx new file mode 100644 index 000000000..43d042267 --- /dev/null +++ b/packages/react-shadcn/src/shadcn/components/ui/button.tsx @@ -0,0 +1,61 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/shadcn/lib/utils" + +const buttonVariants = cva( + "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-[3px] aria-invalid:ring-[3px] [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/80", + outline: "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground shadow-xs", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + ghost: "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground", + destructive: "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5", + lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3", + icon: "size-9", + "icon-xs": "size-6 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant = "default", size = "default", asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot.Root : "button" + + return ( + } + {...props} + /> + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/packages/react-shadcn/src/shadcn/components/ui/field.tsx b/packages/react-shadcn/src/shadcn/components/ui/field.tsx new file mode 100644 index 000000000..6c85a2357 --- /dev/null +++ b/packages/react-shadcn/src/shadcn/components/ui/field.tsx @@ -0,0 +1,226 @@ +import * as React from "react" +import { useMemo } from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/shadcn/lib/utils" +import { Label } from "@/shadcn/components/ui/label" +import { Separator } from "@/shadcn/components/ui/separator" + +function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { + return ( +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3 flex flex-col", className)} + {...props} + /> + ) +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { + return ( + + ) +} + +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
[data-slot=field-group]]:gap-4 group/field-group @container/field-group flex w-full flex-col", + className + )} + {...props} + /> + ) +} + +const fieldVariants = cva("data-[invalid=true]:text-destructive gap-3 group/field flex w-full", { + variants: { + orientation: { + vertical: + "flex-col [&>*]:w-full [&>.sr-only]:w-auto", + horizontal: + "flex-row items-center [&>[data-slot=field-label]]:flex-auto has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + responsive: + "flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto @md/field-group:[&>[data-slot=field-label]]:flex-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + }, + }, + defaultVariants: { + orientation: "vertical", + }, +}) + +function Field({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function FieldLabel({ + className, + ...props +}: React.ComponentProps) { + return ( +