diff --git a/apps/demo/.gitignore b/apps/demo/.gitignore
new file mode 100644
index 000000000..88feeab14
--- /dev/null
+++ b/apps/demo/.gitignore
@@ -0,0 +1,44 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+/public/admin
+/public/admin.html
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/apps/demo/README.md b/apps/demo/README.md
new file mode 100644
index 000000000..e215bc4cc
--- /dev/null
+++ b/apps/demo/README.md
@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
diff --git a/apps/demo/components.json b/apps/demo/components.json
new file mode 100644
index 000000000..edcaef267
--- /dev/null
+++ b/apps/demo/components.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "registries": {}
+}
diff --git a/apps/demo/content/pages/array.json b/apps/demo/content/pages/array.json
new file mode 100644
index 000000000..02ad670b9
--- /dev/null
+++ b/apps/demo/content/pages/array.json
@@ -0,0 +1,53 @@
+{
+ "_id": "363hSAskC1OdY74vN9YkhBSIAzU",
+ "_type": "Form",
+ "_index": "a0l",
+ "title": "Array",
+ "description": "",
+ "form": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "test": {
+ "type": "array",
+ "title": "Test",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name"
+ },
+ "lastname": {
+ "type": "string",
+ "title": "Lastname"
+ },
+ "project-specific": {
+ "type": "string",
+ "title": "Project specific",
+ "format": "data-url"
+ }
+ }
+ }
+ },
+ "bla": {
+ "type": "string",
+ "title": "Bla",
+ "format": "data-url"
+ }
+ }
+ },
+ "ui": {
+ "test": {
+ "test": {
+ "project-specific": {
+ "ui:widget": "my-custom-widget"
+ }
+ }
+ },
+ "bla": {
+ "ui:widget": "my-custom-widget"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/demo/content/pages/arrays.json b/apps/demo/content/pages/arrays.json
new file mode 100644
index 000000000..a11c02597
--- /dev/null
+++ b/apps/demo/content/pages/arrays.json
@@ -0,0 +1,205 @@
+{
+ "_id": "34n5maI5YJYxknkKItzFm26Mgbr",
+ "_type": "Form",
+ "_index": "a2",
+ "title": "Arrays",
+ "description": "",
+ "form": {
+ "schema": {
+ "definitions": {
+ "Thing": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "default": "Default name"
+ }
+ }
+ }
+ },
+ "type": "object",
+ "properties": {
+ "listOfStrings": {
+ "type": "array",
+ "title": "A list of strings",
+ "items": {
+ "type": "string",
+ "default": "bazinga"
+ }
+ },
+ "multipleChoicesList": {
+ "type": "array",
+ "title": "A multiple choices list",
+ "items": {
+ "type": "string",
+ "enum": [
+ "foo",
+ "bar",
+ "fuzz",
+ "qux"
+ ]
+ },
+ "uniqueItems": true
+ },
+ "fixedItemsList": {
+ "type": "array",
+ "title": "A list of fixed items",
+ "items": [
+ {
+ "title": "A string value",
+ "type": "string",
+ "default": "lorem ipsum"
+ },
+ {
+ "title": "a boolean value",
+ "type": "boolean"
+ }
+ ],
+ "additionalItems": {
+ "title": "Additional item",
+ "type": "number"
+ }
+ },
+ "minItemsList": {
+ "type": "array",
+ "title": "A list with a minimal number of items",
+ "minItems": 3,
+ "items": {
+ "$ref": "#/definitions/Thing"
+ }
+ },
+ "defaultsAndMinItems": {
+ "type": "array",
+ "title": "List and item level defaults",
+ "minItems": 5,
+ "default": [
+ "carp",
+ "trout",
+ "bream"
+ ],
+ "items": {
+ "type": "string",
+ "default": "unidentified"
+ }
+ },
+ "nestedList": {
+ "type": "array",
+ "title": "Nested list",
+ "items": {
+ "type": "array",
+ "title": "Inner list",
+ "items": {
+ "type": "string",
+ "default": "lorem ipsum"
+ }
+ }
+ },
+ "unorderable": {
+ "title": "Unorderable items",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "default": "lorem ipsum"
+ }
+ },
+ "copyable": {
+ "title": "Copyable items",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "default": "lorem ipsum"
+ }
+ },
+ "unremovable": {
+ "title": "Unremovable items",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "default": "lorem ipsum"
+ }
+ },
+ "noToolbar": {
+ "title": "No add, remove and order buttons",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "default": "lorem ipsum"
+ }
+ },
+ "fixedNoToolbar": {
+ "title": "Fixed array without buttons",
+ "type": "array",
+ "items": [
+ {
+ "title": "A number",
+ "type": "number",
+ "default": 42
+ },
+ {
+ "title": "A boolean",
+ "type": "boolean",
+ "default": false
+ }
+ ],
+ "additionalItems": {
+ "title": "A string",
+ "type": "string",
+ "default": "lorem ipsum"
+ }
+ }
+ }
+ },
+ "ui": {
+ "listOfStrings": {
+ "items": {
+ "ui:emptyValue": ""
+ }
+ },
+ "multipleChoicesList": {
+ "ui:widget": "checkboxes"
+ },
+ "fixedItemsList": {
+ "items": [
+ {
+ "ui:widget": "textarea"
+ },
+ {
+ "ui:widget": "select"
+ }
+ ],
+ "additionalItems": {
+ "ui:widget": "updown"
+ }
+ },
+ "unorderable": {
+ "ui:options": {
+ "orderable": false
+ }
+ },
+ "copyable": {
+ "ui:options": {
+ "copyable": true
+ }
+ },
+ "unremovable": {
+ "ui:options": {
+ "removable": false
+ }
+ },
+ "noToolbar": {
+ "ui:options": {
+ "addable": false,
+ "orderable": false,
+ "removable": false
+ }
+ },
+ "fixedNoToolbar": {
+ "ui:options": {
+ "addable": false,
+ "orderable": false,
+ "removable": false
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/demo/content/pages/basic-form.json b/apps/demo/content/pages/basic-form.json
new file mode 100644
index 000000000..06bbb4b54
--- /dev/null
+++ b/apps/demo/content/pages/basic-form.json
@@ -0,0 +1,77 @@
+{
+ "_id": "34SuSRdmN52pNxrySIyFTBdYn0H",
+ "_type": "Form",
+ "_index": "a1",
+ "title": "Basic form",
+ "description": "Simple form fields",
+ "form": {
+ "schema": {
+ "title": "A registration form",
+ "description": "A simple form example.",
+ "type": "object",
+ "required": ["firstName", "lastName"],
+ "properties": {
+ "firstName": {
+ "type": "string",
+ "title": "First name",
+ "default": "Chuck"
+ },
+ "lastName": {
+ "type": "string",
+ "title": "Last name"
+ },
+ "age": {
+ "type": "integer",
+ "title": "Age",
+ "minimum": 18
+ },
+ "bio": {
+ "type": "string",
+ "title": "Bio"
+ },
+ "password": {
+ "type": "string",
+ "title": "Password",
+ "minLength": 3
+ },
+ "telephone": {
+ "type": "string",
+ "title": "Telephone",
+ "minLength": 10
+ }
+ }
+ },
+ "ui": {
+ "firstName": {
+ "ui:autofocus": true,
+ "ui:emptyValue": "",
+ "ui:placeholder": "ui:emptyValue causes this field to always be valid despite being required",
+ "ui:autocomplete": "family-name",
+ "ui:enableMarkdownInDescription": true,
+ "ui:description": "Make text **bold** or *italic*. Take a look at other options [here](https://markdown-to-jsx.quantizor.dev/)."
+ },
+ "lastName": {
+ "ui:autocomplete": "given-name",
+ "ui:enableMarkdownInDescription": true,
+ "ui:description": "Make things **bold** or *italic*. Embed snippets of `code`. NOTE: Unsafe HTML, not rendered "
+ },
+ "age": {
+ "ui:widget": "updown",
+ "ui:title": "Age of person",
+ "ui:description": "(earth year)"
+ },
+ "bio": {
+ "ui:widget": "textarea"
+ },
+ "password": {
+ "ui:widget": "password",
+ "ui:help": "Hint: Make it strong!"
+ },
+ "telephone": {
+ "ui:options": {
+ "inputType": "tel"
+ }
+ }
+ }
+ }
+}
diff --git a/apps/demo/content/pages/basic-input-field.json b/apps/demo/content/pages/basic-input-field.json
new file mode 100644
index 000000000..8b0ea2d96
--- /dev/null
+++ b/apps/demo/content/pages/basic-input-field.json
@@ -0,0 +1,34 @@
+{
+ "_id": "361JxeCk2GXIXYIMyyy6T0sLGVt",
+ "_type": "Form",
+ "_index": "Zz",
+ "title": "Basic input field",
+ "description": "",
+ "form": {
+ "schema": {
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name"
+ },
+ "textarea": {
+ "type": "string",
+ "title": "Text area"
+ },
+ "email": {
+ "type": "string",
+ "title": "Email input"
+ }
+ }
+ },
+ "ui": {
+ "name": {
+ "ui:placeholder": "Your name"
+ },
+ "textarea": {
+ "ui:widget": "textarea"
+ },
+ "email": {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/demo/content/pages/file-field.json b/apps/demo/content/pages/file-field.json
new file mode 100644
index 000000000..1bc77c99b
--- /dev/null
+++ b/apps/demo/content/pages/file-field.json
@@ -0,0 +1,24 @@
+{
+ "_id": "363QRkgQVIswyZ3Z6V8vyax4JS2",
+ "_type": "Form",
+ "_index": "a0V",
+ "title": "File field",
+ "description": "",
+ "form": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "test": {
+ "type": "string",
+ "title": "Test",
+ "format": "data-url"
+ }
+ }
+ },
+ "ui": {
+ "test": {
+ "ui:widget": "my-custom-widget"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/demo/content/pages/nested.json b/apps/demo/content/pages/nested.json
new file mode 100644
index 000000000..0c894dd9b
--- /dev/null
+++ b/apps/demo/content/pages/nested.json
@@ -0,0 +1,82 @@
+{
+ "_id": "34yTHHpc2Bo0sqmevyVuMWEUgwk",
+ "_type": "Form",
+ "_index": "a4",
+ "title": "Nested",
+ "description": "",
+ "form": {
+ "schema": {
+ "title": "A list of tasks",
+ "type": "object",
+ "required": [
+ "title"
+ ],
+ "properties": {
+ "title": {
+ "type": "string",
+ "title": "Task list title"
+ },
+ "tasks": {
+ "type": "array",
+ "title": "Tasks",
+ "items": {
+ "type": "object",
+ "required": [
+ "title"
+ ],
+ "properties": {
+ "title": {
+ "type": "string",
+ "title": "Title",
+ "description": "A sample title"
+ },
+ "details": {
+ "type": "string",
+ "title": "Task details",
+ "description": "Enter the task details"
+ },
+ "done": {
+ "type": "boolean",
+ "title": "Done?",
+ "default": false
+ }
+ }
+ }
+ }
+ }
+ },
+ "ui": {
+ "title": {
+ "ui:placeholder": "Enter a title for your task list",
+ "ui:className": "font-semibold"
+ },
+ "tasks": {
+ "ui:description": "Add your tasks below",
+ "items": {
+ "title": {
+ "ui:placeholder": "What needs to be done?",
+ "ui:className": "font-semibold"
+ },
+ "details": {
+ "ui:widget": "textarea",
+ "ui:placeholder": "Additional details about this task...",
+ "ui:options": {
+ "rows": 3
+ }
+ },
+ "done": {
+ "ui:widget": "checkbox",
+ "ui:options": {
+ "label": false
+ }
+ },
+ "ui:order": [
+ "title",
+ "details",
+ "done"
+ ]
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/demo/content/pages/numbers.json b/apps/demo/content/pages/numbers.json
new file mode 100644
index 000000000..f8e52ed1a
--- /dev/null
+++ b/apps/demo/content/pages/numbers.json
@@ -0,0 +1,71 @@
+{
+ "_id": "34n61xdCt948cA108Lw04RleQgp",
+ "_type": "Form",
+ "_index": "a3",
+ "title": "Numbers",
+ "description": "",
+ "form": {
+ "schema": {
+ "type": "object",
+ "title": "Number fields & widgets",
+ "properties": {
+ "number": {
+ "title": "Number",
+ "type": "number"
+ },
+ "integer": {
+ "title": "Integer",
+ "type": "integer"
+ },
+ "numberEnum": {
+ "type": "number",
+ "title": "Number enum",
+ "enum": [
+ 1,
+ 2,
+ 3
+ ]
+ },
+ "numberEnumRadio": {
+ "type": "number",
+ "title": "Number enum",
+ "enum": [
+ 1,
+ 2,
+ 3
+ ]
+ },
+ "integerRange": {
+ "title": "Integer range",
+ "type": "integer",
+ "minimum": -50,
+ "maximum": 50
+ },
+ "integerRangeSteps": {
+ "title": "Integer range (by 10)",
+ "type": "integer",
+ "minimum": 50,
+ "maximum": 100,
+ "multipleOf": 10
+ }
+ }
+ },
+ "ui": {
+ "integer": {
+ "ui:widget": "updown"
+ },
+ "numberEnumRadio": {
+ "ui:widget": "radio",
+ "ui:options": {
+ "inline": true
+ }
+ },
+ "integerRange": {
+ "ui:widget": "range"
+ },
+ "integerRangeSteps": {
+ "ui:widget": "range"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/demo/content/pages/select-field.json b/apps/demo/content/pages/select-field.json
new file mode 100644
index 000000000..c9525b769
--- /dev/null
+++ b/apps/demo/content/pages/select-field.json
@@ -0,0 +1,56 @@
+{
+ "_id": "360wsxfTZnSJQzOm32QcoKfJpWu",
+ "_type": "Form",
+ "_index": "a0",
+ "title": "Select field",
+ "description": "",
+ "form": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "color-radio": {
+ "type": "string",
+ "title": "Color as radio",
+ "oneOf": [
+ {
+ "const": "red",
+ "title": "Red"
+ },
+ {
+ "const": "blue",
+ "title": "Blue"
+ },
+ {
+ "const": "green",
+ "title": "Green"
+ }
+ ]
+ },
+ "color-as-dropdown": {
+ "type": "string",
+ "title": "Color as dropdown",
+ "oneOf": [
+ {
+ "const": "red",
+ "title": "Red"
+ },
+ {
+ "const": "blue",
+ "title": "Blue"
+ }
+ ]
+ }
+ }
+ },
+ "ui": {
+ "color-radio": {
+ "ui:placeholder": "",
+ "ui:widget": "radio"
+ },
+ "color-as-dropdown": {
+ "ui:placeholder": "",
+ "ui:widget": "select"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/demo/content/pages/single-input.json b/apps/demo/content/pages/single-input.json
new file mode 100644
index 000000000..469fd99de
--- /dev/null
+++ b/apps/demo/content/pages/single-input.json
@@ -0,0 +1,19 @@
+{
+ "_id": "36469OLDe2mleYkwDQ0wohti56D",
+ "_type": "Form",
+ "_index": "a5",
+ "title": "Single input",
+ "description": "",
+ "form": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name"
+ }
+ }
+ },
+ "ui": {}
+ }
+}
\ No newline at end of file
diff --git a/apps/demo/next.config.ts b/apps/demo/next.config.ts
new file mode 100644
index 000000000..e9ffa3083
--- /dev/null
+++ b/apps/demo/next.config.ts
@@ -0,0 +1,7 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+ /* config options here */
+};
+
+export default nextConfig;
diff --git a/apps/demo/package.json b/apps/demo/package.json
new file mode 100644
index 000000000..b5e9ad816
--- /dev/null
+++ b/apps/demo/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@alinea/demo",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "bun run --cwd ../.. build && node ../../dist/cli.js build -- next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "@rjsf/core": "6.1.2",
+ "@rjsf/daisyui": "6.1.2",
+ "@rjsf/utils": "6.1.2",
+ "@rjsf/validator-ajv8": "6.1.2",
+ "@types/better-sqlite3": "^7.6.13",
+ "better-sqlite3": "^12.4.1",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.546.0",
+ "next": "=15.4.2",
+ "react": "=19.2.0",
+ "react-dom": "=19.2.0",
+ "tailwind-merge": "^3.3.1",
+ "tw-animate-css": "^1.4.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "^4",
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "daisyui": "^5.3.8",
+ "tailwindcss": "^4.1.16",
+ "typescript": "^5"
+ }
+}
diff --git a/apps/demo/postcss.config.mjs b/apps/demo/postcss.config.mjs
new file mode 100644
index 000000000..849d665d6
--- /dev/null
+++ b/apps/demo/postcss.config.mjs
@@ -0,0 +1,7 @@
+const config = {
+ plugins: {
+ '@tailwindcss/postcss': {}
+ }
+}
+
+export default config
diff --git a/apps/demo/public/file.svg b/apps/demo/public/file.svg
new file mode 100644
index 000000000..004145cdd
--- /dev/null
+++ b/apps/demo/public/file.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/demo/public/globe.svg b/apps/demo/public/globe.svg
new file mode 100644
index 000000000..567f17b0d
--- /dev/null
+++ b/apps/demo/public/globe.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/demo/public/next.svg b/apps/demo/public/next.svg
new file mode 100644
index 000000000..5174b28c5
--- /dev/null
+++ b/apps/demo/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/demo/public/vercel.svg b/apps/demo/public/vercel.svg
new file mode 100644
index 000000000..770539603
--- /dev/null
+++ b/apps/demo/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/demo/public/window.svg b/apps/demo/public/window.svg
new file mode 100644
index 000000000..b2b2a44f6
--- /dev/null
+++ b/apps/demo/public/window.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/apps/demo/src/Form.schema.ts b/apps/demo/src/Form.schema.ts
new file mode 100644
index 000000000..8250d6b21
--- /dev/null
+++ b/apps/demo/src/Form.schema.ts
@@ -0,0 +1,21 @@
+import {Config, Field} from 'alinea'
+import {FormFileField} from 'alinea/field/form/base/FormFileField'
+import {FormSelectField} from 'alinea/field/form/base/FormSelectField'
+import {FormTextField} from 'alinea/field/form/base/FormTextField'
+import {MyField} from './project-specific-field/MyField'
+
+export const Form = Config.type('Form', {
+ fields: {
+ title: Field.text('Title'),
+ path: Field.path('Path'),
+ description: Field.text('Description', {multiline: true}),
+ form: Field.form('Form', {
+ baseFields: {
+ MyField,
+ FormFileField,
+ FormSelectField,
+ FormTextField
+ }
+ })
+ }
+})
diff --git a/apps/demo/src/app/[form]/ThemedForm.tsx b/apps/demo/src/app/[form]/ThemedForm.tsx
new file mode 100644
index 000000000..e006aa26f
--- /dev/null
+++ b/apps/demo/src/app/[form]/ThemedForm.tsx
@@ -0,0 +1,220 @@
+/** biome-ignore-all lint/correctness/noUnusedVariables: used to prevent certain parameters from being passed to the DOM */
+'use client'
+
+import DaisyUIForm from '@rjsf/daisyui'
+import {
+ type ArrayFieldTitleProps,
+ type DescriptionFieldProps,
+ type FieldErrorProps,
+ getSubmitButtonOptions,
+ type IconButtonProps,
+ type ObjectFieldTemplateProps,
+ type SubmitButtonProps,
+ type TitleFieldProps,
+ titleId
+} from '@rjsf/utils'
+import validator from '@rjsf/validator-ajv8'
+import type {Infer} from 'alinea'
+import {cx} from 'class-variance-authority'
+import {MoveDown, MoveUp, Trash2Icon, TriangleAlert} from 'lucide-react'
+import {type ReactNode, startTransition, useActionState, useState} from 'react'
+import type {Form} from '@/Form.schema'
+import {MyFieldWidget} from '../../project-specific-field/MyFieldWidget'
+import {handleRjsfSubmit, type RjsfFormState} from './handleRjsfSubmit.action'
+
+function TitleFieldTemplate(props: TitleFieldProps) {
+ const {id, required, title} = props
+
+ console.log(props)
+ return (
+
+ {title}
+ {required && *}
+
+ )
+}
+
+function ArrayFieldTitleTemplate(
+ props: ArrayFieldTitleProps & {fieldPathId: any}
+) {
+ const {title, fieldPathId} = props
+ const id = titleId(fieldPathId)
+ return (
+
+ {title}
+
+ )
+}
+
+function AddButton(props: IconButtonProps) {
+ const {icon, iconType, uiSchema, popover, ...btnProps} = props
+ return (
+
+ )
+}
+
+function MoveUpButton(props: IconButtonProps) {
+ const {icon, iconType, uiSchema, popover, ...btnProps} = props
+ return (
+
+ )
+}
+
+function MoveDownButton(props: IconButtonProps) {
+ const {icon, iconType, uiSchema, popover, ...btnProps} = props
+ return (
+
+ )
+}
+
+function RemoveButton(props: IconButtonProps) {
+ const {icon, iconType, uiSchema, popover, ...btnProps} = props
+ return (
+
+ )
+}
+
+function SubmitButton(props: SubmitButtonProps) {
+ const {uiSchema} = props
+ const {norender} = getSubmitButtonOptions(uiSchema)
+ if (norender) {
+ return null
+ }
+ return (
+
+ )
+}
+
+function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
+ return (
+
+ {props.title &&
{props.title}
}
+ {props.description as string}
+ {props.properties.map(element => (
+
+ {element.content as any}
+
+ ))}
+
+ )
+}
+
+function FieldErrorTemplate(props: FieldErrorProps) {
+ const {errors} = props
+
+ if (!errors) return null
+
+ let errorString = ''
+
+ if (typeof errors === 'string') {
+ errorString = errors
+ } else if (Array.isArray(errors)) {
+ errorString = errors.join(', ')
+ } else {
+ errorString = `${errors}`
+ }
+ return (
+
+
+ {errorString}
+
+ )
+}
+
+export const ThemedForm: React.FC<{
+ page: Infer.Entry
+}> = ({page}) => {
+ const [theme, setTheme] = useState('light')
+ const {schema, ui} = page.form
+ const [state, dispatch, isPending] = useActionState(
+ handleRjsfSubmit,
+ {
+ key: 'initial',
+ formId: page._id
+ }
+ )
+ const onSubmit = (formData: any) => {
+ console.log('Form submitted', formData)
+ startTransition(() => {
+ dispatch(formData)
+ })
+ }
+
+ if (state.key === 'submitted') {
+ console.log(state.submissions)
+ return (
+
+ Thank you for your submission!
+ {state.submissions?.length &&
+ state.submissions.map((submission, index) => (
+
+ {submission.id}:
+ {submission.data}
+
+ ))}
+
+ )
+ }
+
+ return (
+
+
+ {
+ onSubmit(formData)
+ event.preventDefault()
+ }}
+ schema={schema}
+ uiSchema={ui!}
+ widgets={{
+ 'my-custom-widget': MyFieldWidget
+ }}
+ templates={{
+ ArrayFieldTitleTemplate,
+ TitleFieldTemplate,
+ ObjectFieldTemplate,
+ FieldErrorTemplate,
+ ButtonTemplates: {
+ AddButton,
+ MoveUpButton,
+ MoveDownButton,
+ RemoveButton,
+ SubmitButton
+ } as any
+ }}
+ showErrorList="bottom"
+ >
+
+
+
+ )
+}
diff --git a/apps/demo/src/app/[form]/handleRjsfSubmit.action.ts b/apps/demo/src/app/[form]/handleRjsfSubmit.action.ts
new file mode 100644
index 000000000..a4f7ef010
--- /dev/null
+++ b/apps/demo/src/app/[form]/handleRjsfSubmit.action.ts
@@ -0,0 +1,101 @@
+'use server'
+
+import validator from '@rjsf/validator-ajv8'
+import {cms} from '@/cms'
+import {Form} from '@/Form.schema'
+import {getDb} from '@/lib/db'
+
+export type RjsfFormState =
+ | {
+ key: 'initial'
+ formId: string
+ }
+ | {
+ key: 'error'
+ formId: string
+ formData: any
+ errors: any
+ }
+ | {
+ key: 'submitted'
+ formId: string
+ submissions?: any[]
+ }
+
+export async function handleRjsfSubmit(
+ state: RjsfFormState,
+ formData: any
+): Promise {
+ console.log('state', state)
+ console.log('formData', formData)
+
+ const page = await cms.first({type: Form, id: state.formId})
+ if (!page) return state
+
+ // Validate data
+
+ const validationData = validator.validateFormData(
+ formData,
+ page.form.schema,
+ undefined,
+ undefined,
+ page.form.ui
+ )
+
+ if (validationData.errors.length > 0) {
+ console.error(state, formData, validationData.errors)
+ return {
+ key: 'error',
+ formId: state.formId,
+ formData,
+ errors: validationData
+ }
+ }
+
+ const db = getDb()
+ try {
+ await db
+ .prepare(
+ `INSERT INTO submissions (id, form_id, data) VALUES (@id, @form_id, @data)`
+ )
+ .run({
+ id: crypto.randomUUID(),
+ form_id: state.formId,
+ data: JSON.stringify(formData)
+ })
+ } catch (error) {
+ console.error(error)
+ return {
+ key: 'error',
+ formId: state.formId,
+ formData,
+ errors: [
+ {
+ name: 'dbStorageFailed',
+ property: 'general',
+ message: 'Failed writing data to the database. Please try again.',
+ stack: 'Failed writing data to the database. Please try again.'
+ }
+ ]
+ }
+ }
+
+ try {
+ const db = getDb()
+ const submissions = await db
+ .prepare(
+ 'SELECT * FROM submissions WHERE form_id = @form_id ORDER BY inserted_at DESC'
+ )
+ .all({form_id: state.formId})
+ return {
+ key: 'submitted',
+ formId: state.formId,
+ submissions
+ }
+ } catch (e) {}
+
+ return {
+ key: 'submitted',
+ formId: state.formId
+ }
+}
diff --git a/apps/demo/src/app/[form]/page.tsx b/apps/demo/src/app/[form]/page.tsx
new file mode 100644
index 000000000..6966302e2
--- /dev/null
+++ b/apps/demo/src/app/[form]/page.tsx
@@ -0,0 +1,33 @@
+import Link from 'next/link'
+import {notFound} from 'next/navigation.js'
+import {cms} from '@/cms'
+import {Form} from '@/Form.schema'
+import {ThemedForm} from './ThemedForm'
+
+export default async function Home({
+ params
+}: {
+ params: Promise<{form: string}>
+}) {
+ const {form: path} = await params
+ const page = await cms.first({type: Form, url: `/${path}`})
+ if (!page) notFound()
+
+ return (
+
+
+
+
+ {page?.title}
+
+
+ Back
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/demo/src/app/api/cms/route.ts b/apps/demo/src/app/api/cms/route.ts
new file mode 100644
index 000000000..3ac550a7b
--- /dev/null
+++ b/apps/demo/src/app/api/cms/route.ts
@@ -0,0 +1,8 @@
+//import {db} from '@vercel/postgres'
+import {createHandler} from 'alinea/next'
+import {cms} from '@/cms'
+
+const handler = createHandler({cms})
+
+export const GET = handler
+export const POST = handler
diff --git a/apps/demo/src/app/error.tsx b/apps/demo/src/app/error.tsx
new file mode 100644
index 000000000..0072535b9
--- /dev/null
+++ b/apps/demo/src/app/error.tsx
@@ -0,0 +1,31 @@
+'use client' // Error boundaries must be Client Components
+
+import {useEffect} from 'react'
+
+export default function ErrorPage({
+ error,
+ reset
+}: {
+ error: Error & {digest?: string}
+ reset: () => void
+}) {
+ useEffect(() => {
+ // Log the error to an error reporting service
+ console.error(error)
+ }, [error])
+
+ return (
+
+
Something went wrong!
+
+
+ )
+}
diff --git a/apps/demo/src/app/favicon.ico b/apps/demo/src/app/favicon.ico
new file mode 100644
index 000000000..718d6fea4
Binary files /dev/null and b/apps/demo/src/app/favicon.ico differ
diff --git a/apps/demo/src/app/globals.css b/apps/demo/src/app/globals.css
new file mode 100644
index 000000000..a1f4ddde3
--- /dev/null
+++ b/apps/demo/src/app/globals.css
@@ -0,0 +1,130 @@
+@import 'tailwindcss';
+@import 'tw-animate-css';
+
+@source "../../../../node_modules/@rjsf/daisyui";
+
+@plugin "daisyui" {
+ themes: all;
+}
+
+/* @source inline("dropdown","btn","input","select","textarea","label","toggle","checkbox","radio","alert","card"); */
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --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);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+}
+
+:root {
+ --radius: 0.625rem;
+ --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.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.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.922 0 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.269 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);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/apps/demo/src/app/layout.tsx b/apps/demo/src/app/layout.tsx
new file mode 100644
index 000000000..f08542d76
--- /dev/null
+++ b/apps/demo/src/app/layout.tsx
@@ -0,0 +1,34 @@
+import type {Metadata} from 'next'
+import {Geist, Geist_Mono} from 'next/font/google'
+import './globals.css'
+
+const geistSans = Geist({
+ variable: '--font-geist-sans',
+ subsets: ['latin']
+})
+
+const geistMono = Geist_Mono({
+ variable: '--font-geist-mono',
+ subsets: ['latin']
+})
+
+export const metadata: Metadata = {
+ title: 'Create Next App',
+ description: 'Generated by create next app'
+}
+
+export default function RootLayout({
+ children
+}: Readonly<{
+ children: React.ReactNode
+}>) {
+ return (
+
+
+ {children}
+
+
+ )
+}
diff --git a/apps/demo/src/app/not-found.tsx b/apps/demo/src/app/not-found.tsx
new file mode 100644
index 000000000..841cd7ca5
--- /dev/null
+++ b/apps/demo/src/app/not-found.tsx
@@ -0,0 +1,11 @@
+import Link from 'next/link'
+
+export default function NotFound() {
+ return (
+
+
Not Found
+
Could not find requested resource
+
Return Home
+
+ )
+}
diff --git a/apps/demo/src/app/page.tsx b/apps/demo/src/app/page.tsx
new file mode 100644
index 000000000..595e4da48
--- /dev/null
+++ b/apps/demo/src/app/page.tsx
@@ -0,0 +1,36 @@
+import Link from 'next/link.js'
+import {cms} from '@/cms'
+import {Form} from '@/Form.schema'
+
+export default async function Home() {
+ const forms = await cms.find({type: Form})
+
+ return (
+
+
+
+
+ Form Builder Demo123
+
+
+ Explore different form implementations and capabilities
+
+
+
+
+ {forms.map(form => {
+ return (
+
+ {form.title}
+
+ )
+ })}
+
+
+
+ )
+}
diff --git a/apps/demo/src/cms.ts b/apps/demo/src/cms.ts
new file mode 100644
index 000000000..b4de4a420
--- /dev/null
+++ b/apps/demo/src/cms.ts
@@ -0,0 +1,41 @@
+import {Config, Field} from 'alinea'
+import {createCMS} from 'alinea/next'
+import {Form} from './Form.schema'
+
+export const cms = createCMS({
+ // List out available types in your schema
+ schema: {
+ Form
+ },
+
+ // Define the content structure of your CMS
+ workspaces: {
+ main: Config.workspace('Form examples', {
+ preview: true,
+ source: 'content',
+ mediaDir: 'public',
+ roots: {
+ pages: Config.root('Forms', {
+ contains: ['Form']
+ }),
+ media: Config.media()
+ }
+ })
+ },
+
+ baseUrl: {
+ // Point to your local website
+ development: 'http://localhost:3000',
+ // The production URL of your website
+ production: 'https://alinea-blue.vercel.app/'
+ },
+
+ // Enable live previews after adding to your layout
+ // preview: true,
+
+ // The handler route URL
+ handlerUrl: '/api/cms',
+
+ // The admin dashboard will be bundled in this static file
+ dashboardFile: 'admin.html'
+})
diff --git a/apps/demo/src/lib/db.ts b/apps/demo/src/lib/db.ts
new file mode 100644
index 000000000..70ef0496c
--- /dev/null
+++ b/apps/demo/src/lib/db.ts
@@ -0,0 +1,66 @@
+import Database from 'better-sqlite3'
+import {randomUUID} from 'crypto'
+import fs from 'fs'
+import path from 'path'
+
+let db: Database.Database | null = null
+
+export function getDb(): Database.Database {
+ if (db) return db
+
+ // Use the temporary filesystem (ephemeral on serverless)
+ const tmp = process.env.TMPDIR || '/tmp'
+ const file = path.join(tmp, 'demo.sqlite')
+ const firstTime = !fs.existsSync(file)
+
+ db = new Database(file)
+
+ if (firstTime) {
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS submissions (
+ id TEXT PRIMARY KEY,
+ form_id TEXT NOT NULL,
+ inserted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ data JSON
+ );
+ `)
+ console.log('Database initialized at', file)
+ }
+
+ return db!
+}
+
+export async function GET() {
+ const db = getDb()
+ const rows = db
+ .prepare('SELECT * FROM submissions ORDER BY inserted_at DESC')
+ .all()
+ return Response.json(rows)
+}
+
+export async function POST(req: Request) {
+ const payload = await req.json()
+ const db = getDb()
+
+ if (!payload.form_id) {
+ return Response.json(
+ {error: 'Missing required field: form_id'},
+ {status: 400}
+ )
+ }
+
+ const submission = {
+ id: randomUUID(),
+ form_id: payload.form_id,
+ data: JSON.stringify(payload.data ?? {})
+ }
+
+ db.prepare(
+ `INSERT INTO submissions (id, form_id, data) VALUES (@id, @form_id, @data)`
+ ).run(submission)
+
+ return Response.json({
+ ok: true,
+ submission
+ })
+}
diff --git a/apps/demo/src/lib/utils.ts b/apps/demo/src/lib/utils.ts
new file mode 100644
index 000000000..bd0c391dd
--- /dev/null
+++ b/apps/demo/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/apps/demo/src/project-specific-field/MyField.tsx b/apps/demo/src/project-specific-field/MyField.tsx
new file mode 100644
index 000000000..fd62a1fd8
--- /dev/null
+++ b/apps/demo/src/project-specific-field/MyField.tsx
@@ -0,0 +1,59 @@
+import type {Infer} from 'alinea/core/Infer'
+import {type} from 'alinea/core/Type'
+import {path} from 'alinea/field/path'
+import {text} from 'alinea/field/text/TextField'
+import type {JSONSchema7, JSONSchema7Definition} from 'json-schema'
+import type {
+ FormDefinition,
+ FormFieldDefinition
+} from '../../../../dist/field/form.js'
+
+const Schema = type('Project-specific-field', {
+ fields: {
+ title: text('Label', {required: true, width: 0.5}),
+ key: path('Key', {required: true, width: 0.5})
+ }
+})
+
+function rjsfToField(
+ key: string,
+ schema: FormDefinition['schema'],
+ uiSchema: FormDefinition['ui']
+): Infer | undefined {
+ if (schema.type !== 'string') return undefined
+ const uiFieldSchema = uiSchema[key] || {}
+ if (uiFieldSchema?.['ui:widget'] !== 'my-custom-widget') return undefined
+
+ return {
+ title: schema.title || '[No label]',
+ key: key
+ }
+}
+
+function addFieldToRjsf(
+ properties: Record,
+ uiSchema: FormDefinition['ui'],
+ field: Infer.ListItem
+) {
+ const key = field.key || field._id
+
+ const schema: JSONSchema7Definition = {
+ type: 'string',
+ title: field.title || '[No label]',
+ format: 'data-url'
+ }
+ properties[key] = schema
+
+ const ui: any = {}
+ ui['ui:widget'] = 'my-custom-widget'
+
+ if (Object.keys(ui).length > 0) {
+ uiSchema[key] = ui
+ }
+}
+
+export const MyField = {
+ schema: Schema,
+ rjsfToField,
+ addFieldToRjsf
+} satisfies FormFieldDefinition
diff --git a/apps/demo/src/project-specific-field/MyFieldWidget.tsx b/apps/demo/src/project-specific-field/MyFieldWidget.tsx
new file mode 100644
index 000000000..0ac35c782
--- /dev/null
+++ b/apps/demo/src/project-specific-field/MyFieldWidget.tsx
@@ -0,0 +1,10 @@
+export const MyFieldWidget: React.FC<{
+ label: string
+}> = ({label}) => {
+ return (
+
+
{label}
+
My Custom Widget
+
+ )
+}
diff --git a/apps/demo/tsconfig.json b/apps/demo/tsconfig.json
new file mode 100644
index 000000000..34939b2a9
--- /dev/null
+++ b/apps/demo/tsconfig.json
@@ -0,0 +1,42 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts",
+ "**/*.mts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/apps/dev/content/primary/fields/examples/form-field.json b/apps/dev/content/primary/fields/examples/form-field.json
new file mode 100644
index 000000000..f6b359d46
--- /dev/null
+++ b/apps/dev/content/primary/fields/examples/form-field.json
@@ -0,0 +1,52 @@
+{
+ "_id": "34Bj3ZsdjBWfsO5nyI5hjIuk44S",
+ "_type": "FormExample",
+ "_index": "Zz",
+ "title": "Form field",
+ "text": {
+ "schema": {
+ "properties": {
+ "firstname": {
+ "type": "string",
+ "title": "Firstname",
+ "default": ""
+ },
+ "lastname": {
+ "type": "string",
+ "title": "Lastname",
+ "default": ""
+ },
+ "lastname2": {
+ "type": "string",
+ "title": "Lastname 22",
+ "default": ""
+ },
+ "email": {
+ "type": "string",
+ "format": "email",
+ "title": "Email"
+ }
+ }
+ },
+ "ui": {
+ "firstname": {
+ "ui:placeholder": "Bill123"
+ },
+ "lastname": {
+ "ui:placeholder": "Merckx"
+ },
+ "lastname2": {
+ "ui:placeholder": ""
+ }
+ }
+ },
+ "metadata": {
+ "title": "",
+ "description": "",
+ "openGraph": {
+ "image": {},
+ "title": "",
+ "description": ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/dev/src/schema/example/FormExample.tsx b/apps/dev/src/schema/example/FormExample.tsx
new file mode 100644
index 000000000..6a9f03c31
--- /dev/null
+++ b/apps/dev/src/schema/example/FormExample.tsx
@@ -0,0 +1,7 @@
+import {Config, Field} from 'alinea'
+
+export const FormExample = Config.document('Form', {
+ fields: {
+ text: Field.form('Text field')
+ }
+})
diff --git a/apps/dev/src/schema/example/index.ts b/apps/dev/src/schema/example/index.ts
index 117b318fa..5c775bbb4 100644
--- a/apps/dev/src/schema/example/index.ts
+++ b/apps/dev/src/schema/example/index.ts
@@ -4,6 +4,7 @@ export * from './CustomFieldExample'
export * from './CustomPageExample'
export * from './CustomViewExample'
export * from './ExtraTabExample'
+export * from './FormExample'
export * from './I18nFields'
export * from './InlineFields'
export * from './LayoutFields'
@@ -11,5 +12,5 @@ export * from './LinkFields'
export * from './ListFields'
export * from './QuizExample'
export * from './RichTextFields'
-export * from './TabsExample'
export * from './SharedFields'
+export * from './TabsExample'
diff --git a/bun.lock b/bun.lock
index 484e1046e..707fd03a5 100644
--- a/bun.lock
+++ b/bun.lock
@@ -14,6 +14,9 @@
"@esbx/reporter": "^0.0.20",
"@esbx/workspaces": "^0.0.20",
"@ladle/react": "^4.1.2",
+ "@rjsf/core": "6.1.2",
+ "@rjsf/utils": "6.1.2",
+ "@rjsf/validator-ajv8": "6.1.2",
"@types/bun": "^1.1.17",
"@types/fs-extra": "^11.0.1",
"@types/glob": "^7.1.4",
@@ -30,8 +33,6 @@
"npm-run-all": "^4.1.5",
"postcss-modules": "^6.0.0",
"postcss-pxtorem": "^6.0.0",
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
"sass": "^1.63.4",
"sass-embedded": "^1.83.4",
"sharp": "0.32.6",
@@ -42,8 +43,37 @@
"yjs-src": "npm:yjs@13.6.11",
},
"peerDependencies": {
- "react": "*",
- "react-dom": "*",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ },
+ },
+ "apps/demo": {
+ "name": "@alinea/demo",
+ "version": "0.1.0",
+ "dependencies": {
+ "@rjsf/core": "6.1.2",
+ "@rjsf/daisyui": "6.1.2",
+ "@rjsf/utils": "6.1.2",
+ "@rjsf/validator-ajv8": "6.1.2",
+ "@types/better-sqlite3": "^7.6.13",
+ "better-sqlite3": "^12.4.1",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.546.0",
+ "next": "=15.4.2",
+ "react": "=19.2.0",
+ "react-dom": "=19.2.0",
+ "tailwind-merge": "^3.3.1",
+ "tw-animate-css": "^1.4.0",
+ },
+ "devDependencies": {
+ "@tailwindcss/postcss": "^4",
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "daisyui": "^5.3.8",
+ "tailwindcss": "^4.1.16",
+ "typescript": "^5",
},
},
"apps/dev": {
@@ -181,6 +211,8 @@
"@alinea/dashboard": ["@alinea/dashboard@workspace:src/dashboard"],
+ "@alinea/demo": ["@alinea/demo@workspace:apps/demo"],
+
"@alinea/dev": ["@alinea/dev@workspace:apps/dev"],
"@alinea/field.richtext": ["@alinea/field.richtext@workspace:src/field/richtext"],
@@ -193,6 +225,8 @@
"@alinea/ui": ["@alinea/ui@workspace:src/ui"],
+ "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
+
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
"@babel/code-frame": ["@babel/code-frame@7.24.7", "", { "dependencies": { "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" } }, "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA=="],
@@ -265,6 +299,8 @@
"@bundled-es-modules/tough-cookie": ["@bundled-es-modules/tough-cookie@0.1.6", "", { "dependencies": { "@types/tough-cookie": "^4.0.5", "tough-cookie": "^4.1.4" } }, "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw=="],
+ "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
+
"@dependents/detective-less": ["@dependents/detective-less@3.0.2", "", { "dependencies": { "gonzales-pe": "^4.3.0", "node-source-walk": "^5.0.1" } }, "sha512-1YUvQ+e0eeTWAHoN8Uz2x2U37jZs6IGutiIE5LXId7cxfUGhtZjzxE06FdUiuiRrW+UE0vNCdSNPH2lY4dQCOQ=="],
"@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.0", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ=="],
@@ -349,6 +385,14 @@
"@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="],
+ "@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@7.1.0", "", {}, "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA=="],
+
+ "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.1.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" } }, "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA=="],
+
+ "@fortawesome/free-solid-svg-icons": ["@fortawesome/free-solid-svg-icons@7.1.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" } }, "sha512-Udu3K7SzAo9N013qt7qmm22/wo2hADdheXtBfxFTecp+ogsc0caQNRKEb7pkvvagUGOpG9wJC1ViH6WXs8oXIA=="],
+
+ "@fortawesome/react-fontawesome": ["@fortawesome/react-fontawesome@3.1.0", "", { "peerDependencies": { "@fortawesome/fontawesome-svg-core": "~6 || ~7", "react": "^18.0.0 || ^19.0.0" } }, "sha512-5OUQH9aDH/xHJwnpD4J7oEdGvFGJgYnGe0UebaPIdMW9UxYC/f5jv2VjVEgnikdJN0HL8yQxp9Nq+7gqGZpIIA=="],
+
"@headless-tree/core": ["@headless-tree/core@0.0.15", "", {}, "sha512-tWzBP/NqIqYo5xgRf/WpspOvSx4geHuPje+QtawplhoCb8Jtu0u01bE8d+8vMSlcOQrWW2f74igMYPqWPu+UEQ=="],
"@headless-tree/react": ["@headless-tree/react@0.0.15", "", { "peerDependencies": { "@headless-tree/core": "*", "react": "*", "react-dom": "*" } }, "sha512-iZP7mDbOEdc5QYDzZ6r4/6jxmV9DajS48bXwaTy088/mNebJcwuDAwE1/pGYAhvo4VcnG/LR4maAZYWaZlrPGw=="],
@@ -409,6 +453,8 @@
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.5", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg=="],
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
@@ -493,6 +539,14 @@
"@remirror/core-constants": ["@remirror/core-constants@3.0.0", "", {}, "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg=="],
+ "@rjsf/core": ["@rjsf/core@6.1.2", "", { "dependencies": { "lodash": "^4.17.21", "lodash-es": "^4.17.21", "markdown-to-jsx": "^8.0.0", "prop-types": "^15.8.1" }, "peerDependencies": { "@rjsf/utils": "^6.x", "react": ">=18" } }, "sha512-fcEO6kArMcVIzTBoBxNStqxzAL417NDw049nmNx11pIcMwUnU5sAkSW18c8kgZOT6v1xaZhQrY+X5cBzzHy9+g=="],
+
+ "@rjsf/daisyui": ["@rjsf/daisyui@6.1.2", "", { "dependencies": { "@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/free-solid-svg-icons": "^7.1.0", "@fortawesome/react-fontawesome": "^3.1.0", "date-fns": "^4.1.0", "react-day-picker": "^9.11.1", "tailwindcss": "^4.1.12" }, "peerDependencies": { "@rjsf/core": "^6.x", "@rjsf/utils": "^6.x", "daisyui": "^5.0.29", "react": ">=18" } }, "sha512-vaeADgIJzNxh10WDzUkMUK4RO0G3HkRzHXetIswb6u4/2sLZ/0KWH/jV6++3FBdnUbkmDDR5CekSQPpJYl4L0Q=="],
+
+ "@rjsf/utils": ["@rjsf/utils@6.1.2", "", { "dependencies": { "@x0k/json-schema-merge": "^1.0.2", "fast-uri": "^3.1.0", "jsonpointer": "^5.0.1", "lodash": "^4.17.21", "lodash-es": "^4.17.21", "react-is": "^18.3.1" }, "peerDependencies": { "react": ">=18" } }, "sha512-Px3FIkE1KK0745Qng9v88RZ0O7hcLf/1JUu0j00g+r6C8Zyokna42Hz/5TKyyQSKJqgVYcj2Z47YroVLenUM3A=="],
+
+ "@rjsf/validator-ajv8": ["@rjsf/validator-ajv8@6.1.2", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^2.1.1", "lodash": "^4.17.21", "lodash-es": "^4.17.21" }, "peerDependencies": { "@rjsf/utils": "^6.x" } }, "sha512-9P3np2d+TaZcFTEFLocbj19fqrAWB/bxtY0Y8EjP8Oiz8LL+/wUITaN3Wx9uxzWerJyphfpZXWhUS9XkllDLig=="],
+
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.21.1", "", { "os": "android", "cpu": "arm" }, "sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.21.1", "", { "os": "android", "cpu": "arm64" }, "sha512-t1lLYn4V9WgnIFHXy1d2Di/7gyzBWS8G5pQSXdZqfrdCGTwi1VasRMSS81DTYb+avDs/Zz4A6dzERki5oRYz1g=="],
@@ -555,6 +609,36 @@
"@swc/types": ["@swc/types@0.1.12", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA=="],
+ "@tailwindcss/node": ["@tailwindcss/node@4.1.16", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.16" } }, "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw=="],
+
+ "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.16", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.16", "@tailwindcss/oxide-darwin-arm64": "4.1.16", "@tailwindcss/oxide-darwin-x64": "4.1.16", "@tailwindcss/oxide-freebsd-x64": "4.1.16", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.16", "@tailwindcss/oxide-linux-arm64-musl": "4.1.16", "@tailwindcss/oxide-linux-x64-gnu": "4.1.16", "@tailwindcss/oxide-linux-x64-musl": "4.1.16", "@tailwindcss/oxide-wasm32-wasi": "4.1.16", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.16", "@tailwindcss/oxide-win32-x64-msvc": "4.1.16" } }, "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg=="],
+
+ "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.16", "", { "os": "android", "cpu": "arm64" }, "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA=="],
+
+ "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA=="],
+
+ "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg=="],
+
+ "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.16", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg=="],
+
+ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16", "", { "os": "linux", "cpu": "arm" }, "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw=="],
+
+ "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w=="],
+
+ "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ=="],
+
+ "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.16", "", { "os": "linux", "cpu": "x64" }, "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew=="],
+
+ "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.16", "", { "os": "linux", "cpu": "x64" }, "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw=="],
+
+ "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.16", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q=="],
+
+ "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A=="],
+
+ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.16", "", { "os": "win32", "cpu": "x64" }, "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg=="],
+
+ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.16", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.16", "@tailwindcss/oxide": "4.1.16", "postcss": "^8.4.41", "tailwindcss": "4.1.16" } }, "sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A=="],
+
"@tanstack/react-virtual": ["@tanstack/react-virtual@3.10.6", "", { "dependencies": { "@tanstack/virtual-core": "3.10.6" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-xaSy6uUxB92O8mngHZ6CvbhGuqxQ5lIZWCBy+FjhrbHmOwc6BnOnKkYm2FsB1/BpKw/+FVctlMbEtI+F6I1aJg=="],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.10.6", "", {}, "sha512-1giLc4dzgEKLMx5pgKjL6HlG5fjZMgCjzlKAlpr7yoUtetVPELgER1NtephAI910nMwfPTHNyWKSFmJdHkz2Cw=="],
@@ -625,6 +709,8 @@
"@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="],
+ "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="],
+
"@types/bun": ["@types/bun@1.1.17", "", { "dependencies": { "bun-types": "1.1.44" } }, "sha512-zZt0Kao/8hAwNOXh4bmt8nKbMEd4QD8n7PeTGF+NZTVY5ouXhU/TX7jUj4He1p7mgY+WdplnU1B6MB1j17vdzg=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
@@ -647,6 +733,8 @@
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
+ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
+
"@types/json5": ["@types/json5@0.0.30", "", {}, "sha512-sqm9g7mHlPY/43fcSNrCYfOeX9zkTTK+euO5E6+CVijSMm5tTjkVdwdqRkY3ljjIAf8679vps5jKUoJBCLsMDA=="],
"@types/jsonfile": ["@types/jsonfile@6.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ=="],
@@ -717,6 +805,8 @@
"@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@3.7.0", "", { "dependencies": { "@swc/core": "^1.5.7" }, "peerDependencies": { "vite": "^4 || ^5" } }, "sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA=="],
+ "@x0k/json-schema-merge": ["@x0k/json-schema-merge@1.0.2", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-1734qiJHNX3+cJGDMMw2yz7R+7kpbAtl5NdPs1c/0gO5kYT6s4dMbLXiIfpZNsOYhGZI3aH7FWrj4Zxz7epXNg=="],
+
"@zkochan/rimraf": ["@zkochan/rimraf@3.0.2", "", {}, "sha512-GBf4ua7ogWTr7fATnzk/JLowZDBnBJMm8RkMaC/KcvxZ9gxbMWix0/jImd815LmqKyIHZ7h7lADRddGMdGBuCA=="],
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
@@ -725,6 +815,10 @@
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
+ "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
+
+ "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
+
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
"ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="],
@@ -777,10 +871,14 @@
"better-path-resolve": ["better-path-resolve@1.0.0", "", { "dependencies": { "is-windows": "^1.0.0" } }, "sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g=="],
+ "better-sqlite3": ["better-sqlite3@12.4.1", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ=="],
+
"big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="],
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
+ "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
+
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
@@ -833,6 +931,8 @@
"cito": ["cito@0.3.3", "", {}, "sha512-0m6c54DYkU8Q4IcEnnO098EIH0eteLrAyCPQHEcBE4Rro97XA0RzAkJlj+CSBKbKveb+EDuoauO/P9Jk6QoPmw=="],
+ "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
+
"classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
@@ -903,6 +1003,8 @@
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+ "daisyui": ["daisyui@5.3.8", "", {}, "sha512-ihDXb07IzM/2ugkwBWdy2LFCaepdn1oGsKIsR3gNG/VuTAmS60+HUG9rskjR5BzyJOVVUDDpWoiX3PBDIT3DYQ=="],
+
"data-view-buffer": ["data-view-buffer@1.0.1", "", { "dependencies": { "call-bind": "^1.0.6", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA=="],
"data-view-byte-length": ["data-view-byte-length@1.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ=="],
@@ -911,6 +1013,10 @@
"dataloader": ["dataloader@2.2.2", "", {}, "sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g=="],
+ "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
+
+ "date-fns-jalali": ["date-fns-jalali@4.1.0-0", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="],
+
"debug": ["debug@4.3.6", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg=="],
"decode-named-character-reference": ["decode-named-character-reference@1.0.2", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg=="],
@@ -997,7 +1103,7 @@
"end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="],
- "enhanced-resolve": ["enhanced-resolve@5.17.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg=="],
+ "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
@@ -1063,8 +1169,12 @@
"fast-glob": ["fast-glob@3.3.2", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow=="],
+ "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
+
"fastq": ["fastq@1.17.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w=="],
+ "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
+
"filing-cabinet": ["filing-cabinet@3.3.1", "", { "dependencies": { "app-module-path": "^2.2.0", "commander": "^2.20.3", "debug": "^4.3.3", "enhanced-resolve": "^5.8.3", "is-relative-path": "^1.0.2", "module-definition": "^3.3.1", "module-lookup-amd": "^7.0.1", "resolve": "^1.21.0", "resolve-dependency-path": "^2.0.0", "sass-lookup": "^3.0.0", "stylus-lookup": "^3.0.1", "tsconfig-paths": "^3.10.1", "typescript": "^3.9.7" }, "bin": { "filing-cabinet": "bin/cli.js" } }, "sha512-renEK4Hh6DUl9Vl22Y3cxBq1yh8oNvbAdXnhih0wVpmea+uyKjC9K4QeRjUaybIiIewdzfum+Fg15ZqJ/GyCaA=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
@@ -1299,6 +1409,8 @@
"isomorphic.js": ["isomorphic.js@0.2.5", "", {}, "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw=="],
+ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
+
"jotai": ["jotai@2.9.3", "", { "peerDependencies": { "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-IqMWKoXuEzWSShjd9UhalNsRGbdju5G2FrqNLQJT+Ih6p41VNYe2sav5hnwQx4HJr25jq9wRqvGSWGviGG6Gjw=="],
"js-sha3": ["js-sha3@0.8.0", "", {}, "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="],
@@ -1309,12 +1421,16 @@
"json-parse-better-errors": ["json-parse-better-errors@1.0.2", "", {}, "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="],
+ "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
"jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="],
+ "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="],
+
"jsonstream-next": ["jsonstream-next@3.0.0", "", { "dependencies": { "jsonparse": "^1.2.0", "through2": "^4.0.2" }, "bin": { "jsonstream-next": "bin.js" } }, "sha512-aAi6oPhdt7BKyQn1SrIIGZBt0ukKuOUE1qV6kJ3GgioSOYzsRc8z9Hfr1BVmacA/jLe9nARfmgMGgn68BqIAgg=="],
"keygrip": ["keygrip@1.1.0", "", { "dependencies": { "tsscmp": "1.0.6" } }, "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ=="],
@@ -1331,6 +1447,30 @@
"lib0": ["lib0@0.2.88", "", { "dependencies": { "isomorphic.js": "^0.2.4" }, "bin": { "0serve": "bin/0serve.js", "0gentesthtml": "bin/gentesthtml.js" } }, "sha512-KyroiEvCeZcZEMx5Ys+b4u4eEBbA1ch7XUaBhYpwa/nPMrzTjUhI4RfcytmQfYoTBPcdyx+FX6WFNIoNuJzJfQ=="],
+ "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
+
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
+
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
@@ -1339,6 +1479,10 @@
"loader-utils": ["loader-utils@3.3.1", "", {}, "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg=="],
+ "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
+
+ "lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
+
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
@@ -1351,8 +1495,12 @@
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+ "lucide-react": ["lucide-react@0.546.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="],
+
"madge": ["madge@6.1.0", "", { "dependencies": { "chalk": "^4.1.1", "commander": "^7.2.0", "commondir": "^1.0.1", "debug": "^4.3.1", "dependency-tree": "^9.0.0", "detective-amd": "^4.0.1", "detective-cjs": "^4.0.0", "detective-es6": "^3.0.0", "detective-less": "^1.0.2", "detective-postcss": "^6.1.0", "detective-sass": "^4.0.1", "detective-scss": "^3.0.0", "detective-stylus": "^2.0.1", "detective-typescript": "^9.0.0", "ora": "^5.4.1", "pluralize": "^8.0.0", "precinct": "^8.1.0", "pretty-ms": "^7.0.1", "rc": "^1.2.7", "stream-to-array": "^2.3.0", "ts-graphviz": "^1.5.0", "walkdir": "^0.4.1" }, "peerDependencies": { "typescript": "^3.9.5 || ^4.9.5 || ^5" }, "optionalPeers": ["typescript"], "bin": { "madge": "bin/cli.js" } }, "sha512-irWhT5RpFOc6lkzGHKLihonCVgM0YtfNUh4IrFeW3EqHpnt/JHUG3z26j8PeJEktCGB4tmGOOOJi1Rl/ACWucQ=="],
+ "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
+
"make-dir": ["make-dir@2.1.0", "", { "dependencies": { "pify": "^4.0.1", "semver": "^5.6.0" } }, "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA=="],
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
@@ -1361,6 +1509,8 @@
"markdown-table": ["markdown-table@3.0.3", "", {}, "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw=="],
+ "markdown-to-jsx": ["markdown-to-jsx@8.0.0", "", { "peerDependencies": { "react": ">= 0.14.0" }, "optionalPeers": ["react"] }, "sha512-hWEaRxeCDjes1CVUQqU+Ov0mCqBqkGhLKjL98KdbwHSgEWZZSJQeGlJQatVfeZ3RaxrfTrZZ3eczl2dhp5c/pA=="],
+
"match-sorter": ["match-sorter@6.3.4", "", { "dependencies": { "@babel/runtime": "^7.23.8", "remove-accents": "0.5.0" } }, "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg=="],
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA=="],
@@ -1709,15 +1859,17 @@
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
- "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
+ "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
- "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="],
+ "react-day-picker": ["react-day-picker@9.11.1", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw=="],
+
+ "react-dom": ["react-dom@19.2.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ=="],
"react-hotkeys-hook": ["react-hotkeys-hook@4.5.0", "", { "peerDependencies": { "react": ">=16.8.1", "react-dom": ">=16.8.1" } }, "sha512-Samb85GSgAWFQNvVt3PS90LPPGSf9mkH/r4au81ZP1yOIFayLC3QAvqTgGtJ8YEDMXtPmaVBs6NgipHO6h4Mug=="],
"react-inspector": ["react-inspector@6.0.2", "", { "peerDependencies": { "react": "^16.8.4 || ^17.0.0 || ^18.0.0" } }, "sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ=="],
- "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
+ "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"react-query": ["react-query@3.39.3", "", { "dependencies": { "@babel/runtime": "^7.5.5", "broadcast-channel": "^3.4.1", "match-sorter": "^6.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g=="],
@@ -1757,6 +1909,8 @@
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
+ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
+
"requirejs": ["requirejs@2.3.7", "", { "bin": { "r.js": "bin/r.js", "r_js": "bin/r.js" } }, "sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw=="],
"requirejs-config-file": ["requirejs-config-file@4.0.0", "", { "dependencies": { "esprima": "^4.0.0", "stringify-object": "^3.2.1" } }, "sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw=="],
@@ -1845,7 +1999,7 @@
"sax": ["sax@1.3.0", "", {}, "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA=="],
- "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
@@ -1955,6 +2109,10 @@
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
+ "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
+
+ "tailwindcss": ["tailwindcss@4.1.16", "", {}, "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA=="],
+
"tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
"tar-fs": ["tar-fs@3.0.6", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^2.1.1", "bare-path": "^2.1.0" } }, "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w=="],
@@ -2001,6 +2159,8 @@
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
+ "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
+
"type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
@@ -2119,6 +2279,12 @@
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
+ "@alinea/demo/@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="],
+
+ "@alinea/demo/@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
+
+ "@alinea/demo/@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
+
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@@ -2141,6 +2307,22 @@
"@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+ "@tailwindcss/node/source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.6.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.6.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@tailwindcss/postcss/postcss": ["postcss@8.4.41", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.1", "source-map-js": "^1.2.0" } }, "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ=="],
+
"@types/fs-extra/@types/node": ["@types/node@20.0.0", "", {}, "sha512-cD2uPTDnQQCVpmRefonO98/PPijuOnnEy5oytWJFPY1N9aJCz2wJ5kSGWO+zJoed2cY2JxQh6yBuUq4vIn61hw=="],
"@types/glob/@types/node": ["@types/node@20.0.0", "", {}, "sha512-cD2uPTDnQQCVpmRefonO98/PPijuOnnEy5oytWJFPY1N9aJCz2wJ5kSGWO+zJoed2cY2JxQh6yBuUq4vIn61hw=="],
@@ -2203,6 +2385,8 @@
"filing-cabinet/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
+ "filing-cabinet/enhanced-resolve": ["enhanced-resolve@5.17.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg=="],
+
"filing-cabinet/tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
"filing-cabinet/typescript": ["typescript@3.9.10", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q=="],
@@ -2215,12 +2399,16 @@
"less/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
+ "lightningcss/detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
"load-json-file/strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
"madge/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
"madge/pretty-ms": ["pretty-ms@7.0.1", "", { "dependencies": { "parse-ms": "^2.1.0" } }, "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q=="],
+ "magic-string/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
"make-dir/pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="],
"make-dir/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
@@ -2271,6 +2459,8 @@
"precinct/node-source-walk": ["node-source-walk@4.3.0", "", { "dependencies": { "@babel/parser": "^7.0.0" } }, "sha512-8Q1hXew6ETzqKRAs3jjLioSxNfT1cx74ooiF8RlAONwVMcfq+UdzLC2eB5qcPldUxaE5w3ytLkrmV1TGddhZTA=="],
+ "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
+
"prosemirror-trailing-node/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"read-pkg/path-type": ["path-type@3.0.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg=="],
@@ -2315,6 +2505,8 @@
"yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+ "@alinea/demo/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
+
"@babel/highlight/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
"@esbx/reporter/pretty-ms/parse-ms": ["parse-ms@2.1.0", "", {}, "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA=="],
diff --git a/dev.js b/dev.js
index 733ae823e..61744c33f 100644
--- a/dev.js
+++ b/dev.js
@@ -28,7 +28,7 @@ async function run({production, dir, config}) {
sade('dev', true)
.option('--production', 'Run in production mode')
- .option('--dir', 'Development directory', 'apps/web')
+ .option('--dir', 'Development directory')
.option('--config', 'Config file')
.action(opts => run(opts))
.parse(process.argv)
diff --git a/package.json b/package.json
index 13b14368a..121de7619 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"type": "module",
"scripts": {
"dev": "run-p dev:run",
- "web": "run-p dev:check web:run",
+ "demo": "run-p dev:check demo:run",
"tag": "bun tag.js",
"bump": "bun bump.js",
"stories": "ladle serve",
@@ -21,7 +21,8 @@
"type": "tsc --diagnostics",
"dev:check": "tsc -w",
"dev:run": "bun build.ts --watch -- node dev.js --dir apps/dev",
- "web:run": "bun build.ts --watch -- node dev.js -- bun run --cwd apps/web dev",
+ "demo:build": "cd apps/demo && bun run build",
+ "demo:run": "bun build.ts --watch -- node dev.js --dir apps/demo -- bun run --cwd apps/demo dev",
"trace": "tsc --diagnostics --generateTrace private/trace && analyze-trace private/trace & speedscope private/trace/trace.json",
"prepublishOnly": "bun run build"
},
@@ -38,6 +39,7 @@
],
"workspaces": [
"apps/dev",
+ "apps/demo",
"src/adapter",
"src/backend",
"src/cli",
@@ -51,8 +53,8 @@
"esbuild": "^0.25.4"
},
"peerDependencies": {
- "react": "*",
- "react-dom": "*"
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
},
"devDependencies": {
"@alinea/styler": "^1.0.3",
@@ -60,6 +62,9 @@
"@biomejs/biome": "^2.1.2",
"@esbx/reporter": "^0.0.20",
"@esbx/workspaces": "^0.0.20",
+ "@rjsf/core": "6.1.2",
+ "@rjsf/utils": "6.1.2",
+ "@rjsf/validator-ajv8": "6.1.2",
"@ladle/react": "^4.1.2",
"@types/bun": "^1.1.17",
"@types/fs-extra": "^11.0.1",
@@ -77,8 +82,6 @@
"npm-run-all": "^4.1.5",
"postcss-modules": "^6.0.0",
"postcss-pxtorem": "^6.0.0",
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
"sass": "^1.63.4",
"sass-embedded": "^1.83.4",
"sharp": "0.32.6",
diff --git a/src/dashboard/editor/DefaultViews.tsx b/src/dashboard/editor/DefaultViews.tsx
index abb7d293c..eeb4861a7 100644
--- a/src/dashboard/editor/DefaultViews.tsx
+++ b/src/dashboard/editor/DefaultViews.tsx
@@ -1,6 +1,7 @@
import {CheckInput} from 'alinea/field/check/CheckField.view'
import {CodeInput} from 'alinea/field/code/CodeField.view'
import {DateInput} from 'alinea/field/date/DateField.view'
+import {FormInput} from 'alinea/field/form/FormField.view'
import {HiddenInput} from 'alinea/field/hidden/HiddenField.view'
import {JsonInput} from 'alinea/field/json/JsonField.view'
import {
@@ -43,5 +44,6 @@ export const defaultViews = {
[viewKeys.MediaExplorer]: MediaExplorer,
[viewKeys.MediaFile]: FileEntry,
[viewKeys.FileSummaryRow]: FileSummaryRow,
- [viewKeys.FileSummaryThumb]: FileSummaryThumb
+ [viewKeys.FileSummaryThumb]: FileSummaryThumb,
+ [viewKeys.FormInput]: FormInput
}
diff --git a/src/dashboard/editor/ViewKeys.ts b/src/dashboard/editor/ViewKeys.ts
index f32fadc35..5d190a070 100644
--- a/src/dashboard/editor/ViewKeys.ts
+++ b/src/dashboard/editor/ViewKeys.ts
@@ -19,5 +19,6 @@ export const viewKeys = {
MediaExplorer: 'alinea/dashboard/view/MediaExplorer#MediaExplorer',
MediaFile: 'alinea/dashboard/view/media/FileEntry#FileEntry',
FileSummaryRow: 'alinea/dashboard/view/media/FileSummary#FileSummaryRow',
- FileSummaryThumb: 'alinea/dashboard/view/media/FileSummary#FileSummaryThumb'
+ FileSummaryThumb: 'alinea/dashboard/view/media/FileSummary#FileSummaryThumb',
+ FormInput: 'alinea/field/form/FormField.view#FormInput'
}
diff --git a/src/field.ts b/src/field.ts
index c85bb8995..030b4e79f 100644
--- a/src/field.ts
+++ b/src/field.ts
@@ -2,6 +2,7 @@ export * from 'alinea/core/field/CreateField'
export {check} from 'alinea/field/check'
export {code} from 'alinea/field/code'
export {date} from 'alinea/field/date'
+export {form} from 'alinea/field/form'
export {json} from 'alinea/field/json'
export {entry} from 'alinea/field/link/EntryLink'
export {file} from 'alinea/field/link/FileLink'
diff --git a/src/field/form.ts b/src/field/form.ts
new file mode 100644
index 000000000..19be5b740
--- /dev/null
+++ b/src/field/form.ts
@@ -0,0 +1 @@
+export * from 'alinea/field/form/FormField'
diff --git a/src/field/form/FormField.module.scss b/src/field/form/FormField.module.scss
new file mode 100644
index 000000000..8d6ed6c7b
--- /dev/null
+++ b/src/field/form/FormField.module.scss
@@ -0,0 +1,21 @@
+.root {
+ &-input {
+ display: block;
+ background: transparent;
+ border: none;
+ color: inherit;
+ font: inherit;
+ width: 100%;
+ resize: none;
+ padding: 9px 14px;
+ border-radius: var(--alinea-border-radius);
+ line-height: 1.5;
+ background: var(--alinea-fields);
+ box-shadow: var(--alinea-fields-shadow);
+
+ &:focus {
+ background: var(--alinea-fields-selected);
+ outline: 2px solid var(--alinea-fields-focus);
+ }
+ }
+}
diff --git a/src/field/form/FormField.stories.tsx b/src/field/form/FormField.stories.tsx
new file mode 100644
index 000000000..7e41eb70c
--- /dev/null
+++ b/src/field/form/FormField.stories.tsx
@@ -0,0 +1,31 @@
+import {type} from 'alinea/core/Type'
+import {useForm} from 'alinea/dashboard/atoms/FormAtoms'
+import {InputForm} from 'alinea/dashboard/editor/InputForm'
+import {VStack} from 'alinea/ui'
+import {UIStory} from 'alinea/ui/UIStory'
+import {form} from './FormField.js'
+
+const fields = type('Field', {
+ fields: {
+ date: form('Form', {}),
+ focused: form('Form', {}),
+ readOnly: form('Form (read-only)', {
+ readOnly: true
+ })
+ }
+})
+
+export function DateField() {
+ const form = useForm(fields)
+ return (
+
+
+
+
+
+ )
+}
+
+export default {
+ title: 'Fields / Date'
+}
diff --git a/src/field/form/FormField.ts b/src/field/form/FormField.ts
new file mode 100644
index 000000000..9e241f122
--- /dev/null
+++ b/src/field/form/FormField.ts
@@ -0,0 +1,91 @@
+import type {FieldOptions, WithoutLabel} from 'alinea/core'
+import {ScalarField} from 'alinea/core/field/ScalarField'
+import type {Infer} from 'alinea/core/Infer.js'
+import type {Schema} from 'alinea/core/Schema.js'
+import type {Type} from 'alinea/core/Type.js'
+import {viewKeys} from 'alinea/dashboard/editor/ViewKeys'
+import type {JSONSchema7} from 'json-schema'
+import type {ReactNode} from 'react'
+import {FormFileField} from './base/FormFileField.js'
+import {FormSelectField} from './base/FormSelectField.js'
+import {FormTextField} from './base/FormTextField.js'
+import {FormArrayField} from './composed/FormArrayField.js'
+import type {RjsfHandler} from './RjsfHandler.js'
+
+export type FormDefinition = {
+ schema: JSONSchema7
+ ui: any
+}
+
+/** Optional settings to configure a text field */
+export interface FormOptions extends FieldOptions {
+ /** Width of the field in the dashboard UI (0-1) */
+ width?: number
+ /** Add instructional text to a field */
+ help?: ReactNode
+ /** Display a minimal version */
+ initialValue?: FormDefinition
+ /** Simple form fields like text input, checkboxes, selects, etc */
+ baseFields?: Record>
+ /** Composed fields like arrays, objects, etc */
+ composedFields?: Record>
+}
+
+export type FormFieldDefinition = {
+ schema: Type
+ rjsfToField: (
+ key: string,
+ schema: FormDefinition['schema'],
+ uiSchema: FormDefinition['ui'],
+ handler: RjsfHandler
+ ) => Infer | undefined
+ addFieldToRjsf: (
+ properties: Record,
+ uiSchema: Record,
+ row: Infer.ListItem,
+ handler: RjsfHandler
+ ) => void
+}
+
+export type ComposedFormFieldDefinition = {
+ generateSchema: (baseFields: Schema) => Type
+ rjsfToField: (
+ key: string,
+ fieldSchema: JSONSchema7,
+ schema: FormDefinition['schema'],
+ uiSchema: FormDefinition['ui']
+ ) => Infer | undefined
+ addFieldToRjsf: (
+ properties: Record,
+ uiSchema: Record,
+ row: Infer.ListItem,
+ handler: RjsfHandler
+ ) => void
+}
+
+/** Internal representation of a date field */
+export class FormField extends ScalarField {}
+
+/** Create a date field configuration */
+export function form(
+ label: string,
+ options: WithoutLabel = {}
+): FormField {
+ const defaultBaseFields = {
+ FormFileField,
+ FormSelectField,
+ FormTextField
+ } as unknown as Record> // TODO: fix typing
+ const composedBaseFields = {
+ FormArrayField
+ } as unknown as Record> // TODO: fix typing
+ return new FormField({
+ options: {
+ label,
+ ...options,
+ baseFields: options.baseFields || defaultBaseFields,
+ composedFields: options.composedFields || composedBaseFields
+ },
+ view: viewKeys.FormInput
+ })
+}
diff --git a/src/field/form/FormField.view.tsx b/src/field/form/FormField.view.tsx
new file mode 100644
index 000000000..16c80c15d
--- /dev/null
+++ b/src/field/form/FormField.view.tsx
@@ -0,0 +1,235 @@
+import styler from '@alinea/styler'
+import Form from '@rjsf/core'
+import validator from '@rjsf/validator-ajv8'
+import {type} from 'alinea/core/Type'
+import {useForm} from 'alinea/dashboard/atoms/FormAtoms'
+import {InputForm} from 'alinea/dashboard/editor/InputForm'
+import {useField} from 'alinea/dashboard/editor/UseField'
+import {HStack, VStack} from 'alinea/ui/Stack'
+import {TextareaAutosize} from 'alinea/ui/util/TextareaAutosize'
+import {useAtomValue} from 'jotai'
+import {useEffect, useMemo, useState} from 'react'
+import {list} from '../list.js'
+// import {VisualBuilder} from './VisualBuilder.js'
+import type {FormDefinition, FormField, FormOptions} from './FormField'
+import css from './FormField.module.scss'
+import {RjsfHandler} from './RjsfHandler.js'
+
+const styles = styler(css)
+
+export interface FormInputProps {
+ field: FormField
+}
+
+const tabs = ['builder', 'schema', 'preview'] as const
+type Tab = (typeof tabs)[number]
+
+export function FormInput({field}: FormInputProps) {
+ const {options, value, mutator} = useField(field)
+
+ const [tab, setTab] = useState('builder')
+ const formSchema = value?.schema || {}
+ const uiSchema = value?.ui || {}
+
+ return (
+
+
+ {tabs.map(t => (
+
+ ))}
+
+
+ {tab === 'builder' && (
+
mutator(value)}
+ />
+ )}
+ {tab === 'schema' && (
+ mutator({schema, ui: uiSchema})}
+ setUiSchema={ui => mutator({schema: formSchema, ui})}
+ />
+ )}
+ {tab === 'preview' && (
+
+ )}
+
+
+ )
+}
+
+function Preview({
+ formSchema,
+ uiSchema
+}: {
+ formSchema: FormDefinition['schema']
+ uiSchema: FormDefinition['ui']
+}) {
+ const log = (type: string) => console.log.bind(console, type)
+ return (
+
+ )
+}
+
+function FormSchema({
+ formSchema,
+ uiSchema,
+ setFormSchema,
+ setUiSchema
+}: {
+ formSchema: FormDefinition['schema']
+ uiSchema: FormDefinition['ui']
+ setFormSchema: (schema: FormDefinition['schema']) => void
+ setUiSchema: (ui: FormDefinition['ui']) => void
+}) {
+ const [text, setText] = useState(JSON.stringify(formSchema, null, 2))
+ const [valid, setValid] = useState(true)
+
+ return (
+
+
Schema definition
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function JSONTextAreaField({
+ value,
+ mutator
+}: {
+ value: any
+ mutator: (value: any) => void
+}) {
+ const [text, setText] = useState(JSON.stringify(value, null, 2))
+ const [valid, setValid] = useState(true)
+ return (
+
+ {
+ setText(e.currentTarget.value)
+ try {
+ const parsed = JSON.parse(e.currentTarget.value)
+ mutator(parsed)
+ setValid(true)
+ } catch {
+ setValid(false)
+ }
+ }}
+ onKeyDown={e => {
+ if (e.key === 'Tab') {
+ const target = e.target as HTMLInputElement
+ const start = target.selectionStart!
+ const end = target.selectionEnd!
+ const value = target.value
+
+ if (end !== value.length) {
+ e.preventDefault()
+ target.value = `${value.substring(0, start)} ${value.substring(end)}`
+ target.selectionStart = target.selectionEnd = start + 2
+ }
+ }
+ }}
+ onBlur={e => {
+ if (valid) {
+ setText(JSON.stringify(value, null, 2))
+ }
+ }}
+ placeholder={'{}'}
+ />
+
+ )
+}
+
+export function VisualBuilder({
+ options,
+ formSchema,
+ uiSchema,
+ setSchemas
+}: {
+ options: FormOptions
+ formSchema: FormDefinition['schema']
+ uiSchema: FormDefinition['ui']
+ setSchemas: (value: FormDefinition) => void
+}) {
+ const fields = useMemo(() => {
+ const baseSchema: any = {}
+ for (const entry of Object.entries(options.baseFields || {})) {
+ const [key, fieldDef] = entry
+ baseSchema[key] = fieldDef.schema
+ }
+ const composedSchema: any = {}
+ for (const entry of Object.entries(options.composedFields || {})) {
+ const [key, fieldDef] = entry
+ composedSchema[key] = fieldDef.generateSchema(baseSchema)
+ }
+ return type('Fields', {
+ fields: {
+ list: list('List', {
+ schema: {
+ ...baseSchema,
+ ...composedSchema
+ }
+ })
+ }
+ })
+ }, [])
+
+ const listInitialValue = useMemo(() => {
+ const handler = new RjsfHandler(formSchema, uiSchema, options)
+ return handler.generateFields()
+ }, [])
+
+ const form = useForm(fields, {
+ initialValue: {list: listInitialValue}
+ })
+ const listField = form.fieldInfo(fields.list)
+ const data = useAtomValue(listField.value)
+
+ useEffect(() => {
+ const handler = new RjsfHandler(formSchema, uiSchema, options)
+ const newSchemas = handler.rebuildSchemas(data)
+ setSchemas(newSchemas)
+ }, [data])
+
+ return (
+
+
+
+ )
+}
diff --git a/src/field/form/RjsfHandler.ts b/src/field/form/RjsfHandler.ts
new file mode 100644
index 000000000..d37e9ad2f
--- /dev/null
+++ b/src/field/form/RjsfHandler.ts
@@ -0,0 +1,103 @@
+import {createId} from 'alinea/core/Id'
+import type {Infer} from 'alinea/core/Infer.js'
+import {generateKeyBetween} from 'alinea/core/util/FractionalIndexing'
+import type {JSONSchema7} from 'json-schema'
+import type {FormDefinition, FormOptions} from './FormField.js'
+
+export class RjsfHandler {
+ public constructor(
+ public formSchema: FormDefinition['schema'],
+ public uiSchema: FormDefinition['ui'],
+ public options: FormOptions
+ ) {}
+
+ public generateFields(): Infer.ListItem[] {
+ const fields: Infer.ListItem[] = []
+ for (const key of Object.keys(this.formSchema?.properties || {})) {
+ const schema = this.formSchema.properties?.[key]
+ if (!schema || schema === true) continue
+
+ const uiSchema = this.uiSchema?.[key] || {}
+
+ const field = this.rjsfToField(key, schema, uiSchema)
+ if (field) {
+ const previousField = fields[fields.length - 1]
+ const _index = generateKeyBetween(previousField?._index || null, null)
+ fields.push({...field, _index})
+ }
+ }
+
+ return fields
+ }
+
+ public rjsfToField(
+ key: string,
+ schema: FormDefinition['schema'],
+ uiSchema: FormDefinition['ui']
+ ): Infer.ListItem | undefined {
+ const _id = createId()
+ const _index = generateKeyBetween(null, null)
+
+ for (const entry of Object.entries(this.options.baseFields || {})) {
+ const [_type, fieldDef] = entry
+ const field = fieldDef.rjsfToField(key, schema, uiSchema, this)
+ if (field) return {_id, _index, _type, ...field}
+ }
+ for (const entry of Object.entries(this.options.composedFields || {})) {
+ const [_type, fieldDef] = entry
+ const field = fieldDef.rjsfToField(key, schema, uiSchema, this)
+ if (field) return {_id, _index, _type, ...field}
+ }
+ return undefined
+ }
+
+ public rebuildSchemas(fields: Infer.ListItem[]): FormDefinition {
+ const properties: Record = {}
+ const newUiSchema: FormDefinition['ui'] = {}
+
+ for (const field of fields) {
+ for (const entry of Object.entries(this.options.baseFields || {})) {
+ const [key, fieldDef] = entry
+ if (field._type === key) {
+ fieldDef.addFieldToRjsf(properties, newUiSchema, field, this)
+ break
+ }
+ }
+ for (const entry of Object.entries(this.options.composedFields || {})) {
+ const [key, fieldDef] = entry
+ if (field._type === key) {
+ fieldDef.addFieldToRjsf(properties, newUiSchema, field, this)
+ break
+ }
+ }
+ }
+ return {
+ schema: {
+ type: 'object',
+ properties
+ },
+ ui: newUiSchema
+ }
+ }
+
+ public addFieldToRjsf(
+ properties: Record,
+ uiSchema: Record,
+ field: Infer.ListItem
+ ) {
+ for (const entry of Object.entries(this.options.baseFields || {})) {
+ const [key, fieldDef] = entry
+ if (field._type === key) {
+ fieldDef.addFieldToRjsf(properties, uiSchema, field, this)
+ return
+ }
+ }
+ for (const entry of Object.entries(this.options.composedFields || {})) {
+ const [key, fieldDef] = entry
+ if (field._type === key) {
+ fieldDef.addFieldToRjsf(properties, uiSchema, field, this)
+ return
+ }
+ }
+ }
+}
diff --git a/src/field/form/base/FormFileField.tsx b/src/field/form/base/FormFileField.tsx
new file mode 100644
index 000000000..372e9d181
--- /dev/null
+++ b/src/field/form/base/FormFileField.tsx
@@ -0,0 +1,58 @@
+import type {Infer} from 'alinea/core/Infer'
+import {type} from 'alinea/core/Type'
+import {path} from 'alinea/field/path'
+import {text} from 'alinea/field/text/TextField'
+import type {JSONSchema7, JSONSchema7Definition} from 'json-schema'
+import type {FormDefinition, FormFieldDefinition} from '../FormField.js'
+
+export const Schema = type('File', {
+ fields: {
+ title: text('Label', {required: true, width: 0.5}),
+ key: path('Key', {required: true, width: 0.5}),
+ placeholder: text('Placeholder')
+ }
+})
+
+export function rjsfToField(
+ key: string,
+ schema: FormDefinition['schema'],
+ uiSchema: FormDefinition['ui']
+): Infer | undefined {
+ if (schema.type !== 'string') return undefined
+ if (uiSchema['ui:widget'] !== 'file' && schema.format !== 'data-url')
+ return undefined
+
+ return {
+ title: schema.title || '[No label]',
+ key: key,
+ placeholder: uiSchema['ui:placeholder'] || ''
+ }
+}
+
+export function addFieldToRjsf(
+ properties: Record,
+ uiSchema: FormDefinition['ui'],
+ field: Infer.ListItem
+) {
+ const key = field.key || field._id
+
+ const schema: JSONSchema7Definition = {
+ type: 'string',
+ title: field.title || '[No label]',
+ format: 'data-url'
+ }
+ properties[key] = schema
+
+ const ui: any = {}
+ ui['ui:widget'] = 'file'
+
+ if (Object.keys(ui).length > 0) {
+ uiSchema[key] = ui
+ }
+}
+
+export const FormFileField = {
+ schema: Schema,
+ rjsfToField,
+ addFieldToRjsf
+} satisfies FormFieldDefinition
diff --git a/src/field/form/base/FormSelectField.tsx b/src/field/form/base/FormSelectField.tsx
new file mode 100644
index 000000000..58c5dc80a
--- /dev/null
+++ b/src/field/form/base/FormSelectField.tsx
@@ -0,0 +1,100 @@
+import type {RJSFSchema} from '@rjsf/utils'
+import {createId} from 'alinea/core/Id'
+import type {Infer} from 'alinea/core/Infer'
+import {type} from 'alinea/core/Type'
+import {generateKeyBetween} from 'alinea/core/util/FractionalIndexing'
+import {list} from 'alinea/field/list/ListField'
+import {path} from 'alinea/field/path'
+import {text} from 'alinea/field/text/TextField'
+import type {JSONSchema7, JSONSchema7Definition} from 'json-schema'
+import {select} from '../../select.js'
+import type {FormDefinition, FormFieldDefinition} from '../FormField.js'
+
+const Schema = type('Select', {
+ fields: {
+ title: text('Label', {required: true, width: 0.5}),
+ key: path('Key', {required: true, width: 0.5}),
+ placeholder: text('Placeholder'),
+ options: list('Fields', {
+ schema: {
+ Option: type('Option', {
+ fields: {
+ value: text('Value', {required: true, width: 0.5}),
+ label: text('Label', {required: true, width: 0.5})
+ }
+ })
+ }
+ }),
+ defaultValue: text('Default value'),
+ widget: select('Widget', {
+ options: {
+ radio: 'Radio Buttons',
+ select: 'Dropdown'
+ }
+ })
+ }
+})
+
+function rjsfToField(
+ key: string,
+ schema: FormDefinition['schema'],
+ uiSchema: FormDefinition['ui']
+): Infer | undefined {
+ if (schema.type !== 'string') return undefined
+ if (!schema.oneOf) return undefined
+
+ return {
+ title: schema.title || '[No label]',
+ key,
+ options: schema.oneOf.flatMap((option: any) => {
+ const optionId = createId()
+ const index = generateKeyBetween(null, null)
+ const value = option?.const
+ const label = option?.title || key
+ if (!key) return []
+ return [
+ {
+ _type: 'Option',
+ _id: optionId,
+ _index: index,
+ value,
+ label
+ }
+ ]
+ }),
+ placeholder: '',
+ defaultValue: '',
+ widget: uiSchema['ui:widget'] === 'select' ? 'select' : 'radio'
+ }
+}
+
+function addFieldToRjsf(
+ properties: Record,
+ uiSchema: Record,
+ field: Infer.ListItem
+) {
+ const key = field.key || field._id
+
+ const schema: JSONSchema7Definition = {
+ type: 'string',
+ title: field.title || '[No label]',
+ oneOf: field.options.map(option => {
+ return {
+ const: option.value,
+ title: option.label
+ }
+ })
+ }
+ properties[key] = schema
+ if (field.defaultValue) schema.default = field.defaultValue
+ uiSchema[key] = {
+ 'ui:placeholder': field.placeholder || '',
+ 'ui:widget': field.widget
+ }
+}
+
+export const FormSelectField = {
+ schema: Schema,
+ rjsfToField,
+ addFieldToRjsf
+} satisfies FormFieldDefinition
diff --git a/src/field/form/base/FormTextField.tsx b/src/field/form/base/FormTextField.tsx
new file mode 100644
index 000000000..03c5e2fb9
--- /dev/null
+++ b/src/field/form/base/FormTextField.tsx
@@ -0,0 +1,91 @@
+import type {Infer} from 'alinea/core/Infer'
+import {type} from 'alinea/core/Type'
+import {number} from 'alinea/field/number'
+import {path} from 'alinea/field/path'
+import {text} from 'alinea/field/text/TextField'
+import type {JSONSchema7, JSONSchema7Definition} from 'json-schema'
+import {select} from '../../select.js'
+import type {FormDefinition, FormFieldDefinition} from '../FormField.js'
+import {
+ autoCompleteValues,
+ isAutoCompleteValue
+} from '../utils/autoCompleteValues.js'
+
+const Schema = type('Text', {
+ fields: {
+ title: text('Label', {required: true, width: 0.5}),
+ key: path('Key', {required: true, width: 0.5}),
+ widget: select('Widget', {
+ options: {
+ text: 'Text Input',
+ textarea: 'Text Area',
+ email: 'Email',
+ password: 'Password'
+ }
+ }),
+ placeholder: text('Placeholder'),
+ autocomplete: select('Autocomplete', {
+ options: autoCompleteValues
+ }),
+ defaultValue: text('Default value'),
+ maxLength: number('Max Length')
+ }
+})
+
+function rjsfToField(
+ key: string,
+ schema: FormDefinition['schema'],
+ uiSchema: FormDefinition['ui']
+): Infer | undefined {
+ if (schema.type !== 'string') return undefined
+
+ let widget: Infer['widget'] = 'text'
+ if (uiSchema['ui:widget'] === 'textarea') widget = 'textarea'
+ if (uiSchema['ui:widget'] === 'password') widget = 'password'
+ if (schema.format === 'email') widget = 'email'
+
+ const autocomplete = uiSchema['ui:autocomplete']
+ return {
+ title: schema.title || '[No label]',
+ key: key,
+ widget,
+ placeholder: uiSchema['ui:placeholder'] || '',
+ autocomplete: isAutoCompleteValue(autocomplete) ? autocomplete : null,
+ defaultValue: (schema.default as string) || '',
+ maxLength: schema.maxLength || null
+ }
+}
+
+function addFieldToRjsf(
+ properties: Record,
+ uiSchema: FormDefinition['ui'],
+ field: Infer.ListItem
+) {
+ const key = field.key || field._id
+ console.log(key)
+
+ const schema: JSONSchema7Definition = {
+ type: 'string',
+ title: field.title || '[No label]'
+ }
+ properties[key] = schema
+ if (field.maxLength) schema.maxLength = field.maxLength
+ if (field.defaultValue) schema.default = field.defaultValue
+
+ const ui: any = {}
+ if (field.autocomplete) ui['ui:autocomplete'] = field.autocomplete
+ if (field.placeholder) ui['ui:placeholder'] = field.placeholder
+ if (field.widget === 'textarea') ui['ui:widget'] = 'textarea'
+ if (field.widget === 'email') schema.format = 'email'
+ if (field.widget === 'password') ui['ui:widget'] = 'password'
+
+ if (Object.keys(ui).length > 0) {
+ uiSchema[key] = ui
+ }
+}
+
+export const FormTextField = {
+ schema: Schema,
+ rjsfToField,
+ addFieldToRjsf
+} satisfies FormFieldDefinition
diff --git a/src/field/form/composed/FormArrayField.tsx b/src/field/form/composed/FormArrayField.tsx
new file mode 100644
index 000000000..803ce64ac
--- /dev/null
+++ b/src/field/form/composed/FormArrayField.tsx
@@ -0,0 +1,94 @@
+import type {Infer} from 'alinea/core/Infer'
+import type {Schema} from 'alinea/core/Schema.js'
+import {type} from 'alinea/core/Type'
+import {generateKeyBetween} from 'alinea/core/util/FractionalIndexing'
+import {path} from 'alinea/field/path'
+import {text} from 'alinea/field/text/TextField'
+import type {JSONSchema7, JSONSchema7Definition} from 'json-schema'
+import {list} from '../../list.js'
+import type {ComposedFormFieldDefinition, FormDefinition} from '../FormField.js'
+import type {RjsfHandler} from '../RjsfHandler.js'
+
+type SchemaType = ReturnType
+
+function generateSchema(baseSchema: Schema) {
+ return type('Array', {
+ fields: {
+ title: text('Label', {required: true, width: 0.5}),
+ key: path('Key', {required: true, width: 0.5}),
+ items: list('Fields', {schema: baseSchema})
+ }
+ })
+}
+
+export function rjsfToField(
+ key: string,
+ schema: FormDefinition['schema'],
+ uiSchema: FormDefinition['ui'],
+ handler: RjsfHandler
+): Infer | undefined {
+ if (schema.type !== 'array') return undefined
+ if (!schema.items || schema.items === true) return undefined
+ if (Array.isArray(schema.items)) return undefined
+ if (schema.items.type !== 'object') return undefined
+ if (!schema.items.properties) return undefined
+
+ const properties = schema.items.properties
+ const children: Infer.ListItem[] = []
+ for (const entry of Object.entries(properties)) {
+ const [childKey, childSchema] = entry
+ const childUiSchema = uiSchema?.[key] || {}
+ if (!childSchema || childSchema === true) continue
+
+ const previousChild = children[children.length - 1]
+ const _index = generateKeyBetween(previousChild?._index || null, null)
+
+ const child = handler.rjsfToField(childKey, childSchema, childUiSchema)
+ if (child) {
+ children.push({...child, _index})
+ }
+ }
+
+ return {
+ title: schema.title || '[No label]',
+ key: key,
+ items: children
+ }
+}
+
+export function addFieldToRjsf(
+ properties: Record,
+ uiSchema: FormDefinition['ui'],
+ field: Infer.ListItem,
+ handler: RjsfHandler
+) {
+ const key = field.key || field._id
+ const childProperties: Record = {}
+ const childUiSchema: Record = {}
+ const schema: JSONSchema7Definition = {
+ type: 'array',
+ title: field.title || '[No label]',
+ items: {
+ type: 'object',
+ properties: childProperties
+ }
+ }
+ properties[key] = schema
+
+ for (const child of field.items) {
+ handler.addFieldToRjsf(childProperties, childUiSchema, child)
+ }
+
+ const ui: any = {}
+ ui[key] = {...childUiSchema}
+
+ if (Object.keys(ui).length > 0) {
+ uiSchema[key] = ui
+ }
+}
+
+export const FormArrayField = {
+ generateSchema,
+ rjsfToField,
+ addFieldToRjsf
+} satisfies ComposedFormFieldDefinition
diff --git a/src/field/form/composed/TwoColumnsField.tsx b/src/field/form/composed/TwoColumnsField.tsx
new file mode 100644
index 000000000..42c99823c
--- /dev/null
+++ b/src/field/form/composed/TwoColumnsField.tsx
@@ -0,0 +1,94 @@
+import type {Infer} from 'alinea/core/Infer'
+import type {Schema} from 'alinea/core/Schema.js'
+import {type} from 'alinea/core/Type'
+import {generateKeyBetween} from 'alinea/core/util/FractionalIndexing'
+import {path} from 'alinea/field/path'
+import {text} from 'alinea/field/text/TextField'
+import type {JSONSchema7, JSONSchema7Definition} from 'json-schema'
+import {list} from '../../list.js'
+import type {ComposedFormFieldDefinition, FormDefinition} from '../FormField.js'
+import type {RjsfHandler} from '../RjsfHandler.js'
+
+type SchemaType = ReturnType
+
+function generateSchema(baseSchema: Schema) {
+ return type('Array', {
+ fields: {
+ title: text('Label', {required: true, width: 0.5}),
+ key: path('Key', {required: true, width: 0.5}),
+ items: list('Fields', {schema: baseSchema})
+ }
+ })
+}
+
+export function rjsfToField(
+ key: string,
+ schema: FormDefinition['schema'],
+ uiSchema: FormDefinition['ui'],
+ handler: RjsfHandler
+): Infer | undefined {
+ if (schema.type !== 'array') return undefined
+ if (!schema.items || schema.items === true) return undefined
+ if (Array.isArray(schema.items)) return undefined
+ if (schema.items.type !== 'object') return undefined
+ if (!schema.items.properties) return undefined
+
+ const properties = schema.items.properties
+ const children: Infer.ListItem[] = []
+ for (const entry of Object.entries(properties)) {
+ const [childKey, childSchema] = entry
+ const childUiSchema = uiSchema?.[key] || {}
+ if (!childSchema || childSchema === true) continue
+
+ const previousChild = children[children.length - 1]
+ const _index = generateKeyBetween(previousChild?._index || null, null)
+
+ const child = handler.rjsfToField(childKey, childSchema, childUiSchema)
+ if (child) {
+ children.push({...child, _index})
+ }
+ }
+
+ return {
+ title: schema.title || '[No label]',
+ key: key,
+ items: children
+ }
+}
+
+export function addFieldToRjsf(
+ properties: Record,
+ uiSchema: FormDefinition['ui'],
+ field: Infer.ListItem,
+ handler: RjsfHandler
+) {
+ const key = field.key || field._id
+ const childProperties: Record = {}
+ const childUiSchema: Record = {}
+ const schema: JSONSchema7Definition = {
+ type: 'array',
+ title: field.title || '[No label]',
+ items: {
+ type: 'object',
+ properties: childProperties
+ }
+ }
+ properties[key] = schema
+
+ for (const child of field.items) {
+ handler.addFieldToRjsf(childProperties, childUiSchema, child)
+ }
+
+ const ui: any = {}
+ ui[key] = {...childUiSchema}
+
+ if (Object.keys(ui).length > 0) {
+ uiSchema[key] = ui
+ }
+}
+
+export const TwoColumnsField = {
+ generateSchema,
+ rjsfToField,
+ addFieldToRjsf
+} satisfies ComposedFormFieldDefinition
diff --git a/src/field/form/utils/autoCompleteValues.ts b/src/field/form/utils/autoCompleteValues.ts
new file mode 100644
index 000000000..7f159a391
--- /dev/null
+++ b/src/field/form/utils/autoCompleteValues.ts
@@ -0,0 +1,46 @@
+export const autoCompleteValues = {
+ "name": "Name",
+ "given-name": "Given name",
+ "family-name": "Family name",
+ "username": "Username",
+ "nickname": "Nickname",
+
+ "email": "Email",
+ "new-password": "New password",
+ "current-password": "Current password",
+ "one-time-code": "One-time code",
+
+ "tel": "Telephone",
+ "street-address": "Street address",
+ "address-line1": "Address line 1",
+ "address-line2": "Address line 2",
+ "postal-code": "Postal code",
+ "country": "Country (code)",
+ "country-name": "Country name",
+ "address-level2": "City",
+ "address-level1": "State / Province",
+
+ "cc-number": "Credit card number",
+ "cc-exp": "Credit card expiry (MM/YY)",
+ "cc-exp-month": "Credit card expiry month",
+ "cc-exp-year": "Credit card expiry year",
+
+ // Extra commonly used values
+ "organization": "Organization",
+ "birthdate": "Birthdate",
+ "bday-day": "Birthday (day)",
+ "bday-month": "Birthday (month)",
+ "bday-year": "Birthday (year)",
+ "url": "Website URL",
+ "sex": "Sex",
+ "gender-identity": "Gender identity",
+
+ // On/Off toggles
+ "on": "Autocomplete on",
+ "off": "Autocomplete off"
+}
+
+export function isAutoCompleteValue(value: string | undefined): value is keyof typeof autoCompleteValues {
+ if(!value) return false
+ return value in autoCompleteValues
+}
\ No newline at end of file