diff --git a/examples/app/schema.json b/examples/app/schema.json
index 0c0cc42..ec92b22 100644
--- a/examples/app/schema.json
+++ b/examples/app/schema.json
@@ -1,113 +1,528 @@
{
"type": "object",
"properties": {
+ "page_title": {
+ "type": "string",
+ "title": "Main Page Title",
+ "order": 1,
+ "ui": {
+ "element": "h1",
+ "class": "text-3xl font-bold text-center mb-6",
+ "name": "page_title",
+ "content": "Comprehensive HTML Form Demo"
+ }
+ },
+ "intro_paragraph": {
+ "type": "string",
+ "title": "Introduction",
+ "order": 2,
+ "ui": {
+ "element": "p",
+ "class": "text-gray-600 mb-4",
+ "name": "intro",
+ "content": "This form demonstrates all supported HTML DOM elements in the renderer."
+ }
+ },
+ "divider": {
+ "type": "string",
+ "order": 3,
+ "ui": {
+ "element": "hr",
+ "class": "my-6 border-gray-300"
+ }
+ },
"first_name": {
"type": "string",
"title": "First Name",
- "order": 1,
+ "placeholder": "Enter your first name",
+ "order": 10,
"ui": {
"element": "input",
+ "type": "text",
"class": "form-group",
- "name": "first_name"
+ "name": "first_name",
+ "autocomplete": "given-name",
+ "maxlength": "50"
}
},
"last_name": {
"type": "string",
"title": "Last Name",
- "order": 2,
+ "placeholder": "Enter your last name",
+ "order": 11,
"ui": {
"element": "input",
+ "type": "text",
"class": "form-group",
- "name": "last_name"
+ "name": "last_name",
+ "autocomplete": "family-name",
+ "maxlength": "50"
}
},
"email": {
"type": "email",
"title": "Email Address",
- "order": 3,
+ "placeholder": "your.email@example.com",
+ "order": 12,
"ui": {
"element": "input",
"type": "email",
"class": "form-group",
- "name": "email"
+ "name": "email",
+ "autocomplete": "email"
}
},
- "user_type": {
- "type": "string",
- "title": "User Type",
- "order": 4,
- "ui": {
- "element": "select",
- "class": "form-group",
- "name": "user_type",
- "options": [ "new", "premium", "standard" ]
- }
- },
- "priority": {
- "type": "string",
- "title": "Priority Level",
- "order": 5,
- "ui": {
- "element": "select",
- "class": "form-group",
- "name": "priority",
- "options": [ "low", "medium", "high", "urgent" ]
- }
- },
- "subject": {
- "type": "string",
- "title": "Subject",
- "order": 6,
+ "password": {
+ "type": "password",
+ "title": "Password",
+ "placeholder": "Enter a secure password",
+ "order": 13,
"ui": {
"element": "input",
+ "type": "password",
"class": "form-group",
- "name": "subject"
+ "name": "password",
+ "minlength": "8"
}
},
- "message": {
- "type": "textarea",
- "title": "Message",
- "order": 7,
+ "age": {
+ "type": "number",
+ "title": "Age",
+ "order": 14,
+ "ui": {
+ "element": "input",
+ "type": "number",
+ "class": "form-group",
+ "name": "age",
+ "min": "18",
+ "max": "120"
+ }
+ },
+ "birth_date": {
+ "type": "date",
+ "title": "Birth Date",
+ "order": 15,
+ "ui": {
+ "element": "input",
+ "type": "date",
+ "class": "form-group",
+ "name": "birth_date"
+ }
+ },
+ "website": {
+ "type": "url",
+ "title": "Personal Website",
+ "placeholder": "https://example.com",
+ "order": 16,
+ "ui": {
+ "element": "input",
+ "type": "url",
+ "class": "form-group",
+ "name": "website"
+ }
+ },
+ "phone": {
+ "type": "tel",
+ "title": "Phone Number",
+ "placeholder": "+1-555-123-4567",
+ "order": 17,
+ "ui": {
+ "element": "input",
+ "type": "tel",
+ "class": "form-group",
+ "name": "phone"
+ }
+ },
+ "favorite_color": {
+ "type": "color",
+ "title": "Favorite Color",
+ "order": 18,
+ "ui": {
+ "element": "input",
+ "type": "color",
+ "class": "form-group",
+ "name": "favorite_color",
+ "value": "#3B82F6"
+ }
+ },
+ "satisfaction": {
+ "type": "range",
+ "title": "Satisfaction Level",
+ "order": 19,
+ "ui": {
+ "element": "input",
+ "type": "range",
+ "class": "form-group",
+ "name": "satisfaction",
+ "min": "1",
+ "max": "10",
+ "value": "5"
+ }
+ },
+ "profile_picture": {
+ "type": "file",
+ "title": "Profile Picture",
+ "order": 20,
+ "ui": {
+ "element": "input",
+ "type": "file",
+ "class": "form-group",
+ "name": "profile_picture",
+ "accept": "image/*"
+ }
+ },
+ "newsletter": {
+ "type": "boolean",
+ "title": "Subscribe to Newsletter",
+ "order": 21,
+ "ui": {
+ "element": "input",
+ "type": "checkbox",
+ "class": "form-group",
+ "name": "newsletter",
+ "value": "yes"
+ }
+ },
+ "gender": {
+ "type": "string",
+ "title": "Gender",
+ "order": 22,
+ "ui": {
+ "element": "select",
+ "class": "form-group",
+ "name": "gender",
+ "options": [
+ { "value": "", "text": "Select Gender", "selected": true },
+ { "value": "male", "text": "Male" },
+ { "value": "female", "text": "Female" },
+ { "value": "other", "text": "Other" },
+ { "value": "prefer_not_to_say", "text": "Prefer not to say" }
+ ]
+ }
+ },
+ "country": {
+ "type": "string",
+ "title": "Country",
+ "order": 23,
+ "ui": {
+ "element": "select",
+ "class": "form-group",
+ "name": "country",
+ "options": [
+ { "value": "us", "text": "United States" },
+ { "value": "ca", "text": "Canada" },
+ { "value": "uk", "text": "United Kingdom" },
+ { "value": "de", "text": "Germany" },
+ { "value": "fr", "text": "France" },
+ { "value": "jp", "text": "Japan" },
+ { "value": "au", "text": "Australia" }
+ ]
+ }
+ },
+ "skills": {
+ "type": "string",
+ "title": "Skills",
+ "order": 24,
+ "ui": {
+ "element": "select",
+ "class": "form-group",
+ "name": "skills",
+ "multiple": true,
+ "options": [
+ { "value": "javascript", "text": "JavaScript" },
+ { "value": "python", "text": "Python" },
+ { "value": "go", "text": "Go" },
+ { "value": "rust", "text": "Rust" },
+ { "value": "java", "text": "Java" },
+ { "value": "csharp", "text": "C#" }
+ ]
+ }
+ },
+ "bio": {
+ "type": "string",
+ "title": "Biography",
+ "placeholder": "Tell us about yourself...",
+ "order": 25,
"ui": {
"element": "textarea",
"class": "form-group",
- "name": "message"
+ "name": "bio",
+ "rows": "4",
+ "cols": "50",
+ "maxlength": "500"
+ }
+ },
+ "section_header": {
+ "type": "string",
+ "order": 30,
+ "ui": {
+ "element": "h2",
+ "class": "text-2xl font-semibold mt-8 mb-4",
+ "content": "Additional Information"
+ }
+ },
+ "experience_fieldset": {
+ "type": "object",
+ "order": 31,
+ "ui": {
+ "element": "fieldset",
+ "class": "border border-gray-300 rounded p-4 mb-4",
+ "children": [
+ {
+ "ui": {
+ "element": "legend",
+ "class": "font-medium px-2",
+ "content": "Work Experience"
+ }
+ },
+ {
+ "title": "Years of Experience",
+ "ui": {
+ "element": "input",
+ "type": "number",
+ "name": "years_experience",
+ "class": "form-group",
+ "min": "0",
+ "max": "50"
+ }
+ },
+ {
+ "title": "Current Position",
+ "ui": {
+ "element": "input",
+ "type": "text",
+ "name": "current_position",
+ "class": "form-group",
+ "placeholder": "e.g., Software Engineer"
+ }
+ }
+ ]
+ }
+ },
+ "technologies_datalist": {
+ "type": "string",
+ "title": "Preferred Technology",
+ "order": 32,
+ "ui": {
+ "element": "input",
+ "type": "text",
+ "class": "form-group",
+ "name": "preferred_tech",
+ "list": "technologies",
+ "placeholder": "Start typing..."
+ }
+ },
+ "tech_datalist": {
+ "type": "string",
+ "order": 33,
+ "ui": {
+ "element": "datalist",
+ "id": "technologies",
+ "options": [
+ "React", "Vue.js", "Angular", "Node.js", "Express",
+ "Django", "Flask", "Spring Boot", "ASP.NET", "Laravel"
+ ]
+ }
+ },
+ "completion_progress": {
+ "type": "string",
+ "order": 34,
+ "ui": {
+ "element": "div",
+ "class": "mb-4",
+ "contentHTML": ""
+ }
+ },
+ "rating_meter": {
+ "type": "string",
+ "order": 35,
+ "ui": {
+ "element": "div",
+ "class": "mb-4",
+ "contentHTML": "
Name | Role | Experience |
---|---|---|
John Doe | Developer | 5 years |
Jane Smith | Designer | 3 years |
function greetUser(name) {\n return `Hello, ${name}!`;\n}\n\nconsole.log(greetUser('World'));
"
+ }
+ },
+ "text_formatting": {
+ "type": "string",
+ "order": 90,
+ "ui": {
+ "element": "div",
+ "class": "mt-8 mb-4",
+ "contentHTML": "This paragraph contains various text formatting: bold text, italic text, highlighted text, small text, deleted text, inserted text, superscript, subscript, and HTML abbreviation.
This is a blockquote that can contain longer quoted text with proper styling and indentation.Contact: demo@example.com" + } + }, + "break_line": { + "type": "string", + "order": 95, + "ui": { + "element": "br" + } + }, + "final_divider": { + "type": "string", + "order": 96, + "ui": { + "element": "hr", + "class": "my-8 border-gray-300" + } + }, + "footer_note": { + "type": "string", + "order": 97, + "ui": { + "element": "footer", + "class": "text-center text-gray-500 text-sm", + "contentHTML": "
This comprehensive form demonstrates the full capabilities of the enhanced JSON Schema renderer.
" } } }, - "required": [ "first_name", "last_name", "email", "user_type", "priority", "subject", "message" ], + "required": [ + "first_name", "last_name", "email", "age", "birth_date", "gender", "bio" + ], "form": { - "class": "form-horizontal", - "action": "/process?task_id={{task_id}}&next=true", + "class": "max-w-4xl mx-auto p-6 bg-white shadow-lg rounded-lg", + "action": "/process?task_id={{task_id}}&form=comprehensive", "method": "POST", - "enctype": "application/x-www-form-urlencoded", + "enctype": "multipart/form-data", "groups": [ { "title": { - "text": "User Information", - "class": "text-lg font-semibold mb-2" + "text": "Header Section", + "class": "sr-only" }, - "fields": [ "first_name", "last_name", "email" ], - "class": "flex gap-2 items-center justify-between" + "fields": [ "page_title", "intro_paragraph", "divider" ], + "class": "mb-8" }, { "title": { - "text": "Details", - "class": "text-lg font-semibold mb-2" + "text": "Personal Information", + "class": "text-2xl font-semibold mb-6 text-gray-800" }, - "fields": [ "user_type", "priority", "subject", "message" ], - "class": "flex gap-2 items-center justify-between" + "fields": [ + "first_name", "last_name", "email", "password", "age", + "birth_date", "website", "phone", "favorite_color", + "satisfaction", "profile_picture", "newsletter" + ], + "class": "grid grid-cols-1 md:grid-cols-2 gap-4 mb-8" + }, + { + "title": { + "text": "Preferences & Background", + "class": "text-2xl font-semibold mb-6 text-gray-800" + }, + "fields": [ "gender", "country", "skills", "bio" ], + "class": "space-y-4 mb-8" + }, + { + "title": { + "text": "Professional Details", + "class": "sr-only" + }, + "fields": [ + "section_header", "experience_fieldset", "technologies_datalist", + "tech_datalist", "completion_progress", "rating_meter" + ], + "class": "mb-8" + }, + { + "title": { + "text": "Media & Content Examples", + "class": "sr-only" + }, + "fields": [ "media_section", "demo_image" ], + "class": "mb-8" + }, + { + "title": { + "text": "Data & Text Examples", + "class": "sr-only" + }, + "fields": [ + "table_section", "list_examples", "interactive_details", + "code_example", "text_formatting" + ], + "class": "mb-8" + }, + { + "title": { + "text": "Footer", + "class": "sr-only" + }, + "fields": [ "break_line", "final_divider", "footer_note" ], + "class": "mt-8" } ], "submit": { "type": "submit", - "label": "Submit", - "class": "btn btn-primary px-2 py-1" + "label": "Submit Complete Form", + "class": "bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors duration-200 mr-4" }, "reset": { "type": "reset", - "label": "Reset", - "class": "btn btn-secondary px-2 py-1" - } + "label": "Reset All Fields", + "class": "bg-gray-500 hover:bg-gray-600 text-white font-medium py-3 px-6 rounded-lg transition-colors duration-200 mr-4" + }, + "buttons": [ + { + "type": "button", + "label": "Save Draft", + "class": "bg-green-600 hover:bg-green-700 text-white font-medium py-3 px-6 rounded-lg transition-colors duration-200", + "onclick": "saveDraft()" + } + ] } } diff --git a/examples/app/templates/form.css b/examples/app/templates/form.css index f67fdc7..9409522 100644 --- a/examples/app/templates/form.css +++ b/examples/app/templates/form.css @@ -1,18 +1,212 @@ -/* Normalize and style form controls */ -body { - font-family: 'Arial', sans-serif; - background-color: #f8f9fa; +/* CSS Reset and Base Styles */ +* { + box-sizing: border-box; margin: 0; padding: 0; } +html { + font-size: 16px; + line-height: 1.6; + scroll-behavior: smooth; +} + +body { + font-family: 'Arial', 'Helvetica', sans-serif; + background-color: #f8f9fa; + color: #333; + margin: 0; + padding: 0; +} + +/* Typography and Text Elements */ +h1, h2, h3, h4, h5, h6 { + font-weight: bold; + line-height: 1.2; + margin-bottom: 0.5rem; + color: #2c3e50; +} + +h1 { font-size: 2.5rem; } +h2 { font-size: 2rem; } +h3 { font-size: 1.75rem; } +h4 { font-size: 1.5rem; } +h5 { font-size: 1.25rem; } +h6 { font-size: 1rem; } + +p { + margin-bottom: 1rem; + line-height: 1.6; +} + +/* Inline text elements */ +strong, b { + font-weight: bold; +} + +em, i { + font-style: italic; +} + +small { + font-size: 0.875rem; + color: #666; +} + +mark { + background-color: #fff3cd; + padding: 0.125rem 0.25rem; + border-radius: 0.125rem; +} + +del { + text-decoration: line-through; + color: #dc3545; +} + +ins { + text-decoration: underline; + color: #28a745; + background-color: #d4edda; +} + +sub, sup { + font-size: 0.75rem; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { bottom: -0.25rem; } +sup { top: -0.5rem; } + +abbr { + text-decoration: underline dotted; + cursor: help; +} + +code { + font-family: 'Courier New', monospace; + background-color: #f8f9fa; + padding: 0.125rem 0.25rem; + border-radius: 0.125rem; + font-size: 0.875rem; + color: #e83e8c; +} + +pre { + font-family: 'Courier New', monospace; + background-color: #f8f9fa; + padding: 1rem; + border-radius: 0.25rem; + overflow-x: auto; + margin-bottom: 1rem; + border: 1px solid #dee2e6; +} + +pre code { + background: none; + padding: 0; + font-size: inherit; + color: inherit; +} + +blockquote { + margin: 1rem 0; + padding: 1rem; + border-left: 4px solid #007bff; + background-color: #f8f9fa; + font-style: italic; +} + +cite { + font-style: italic; + color: #666; +} + +address { + font-style: normal; + margin-bottom: 1rem; +} + +time { + color: #666; +} + +/* Links */ +a { + color: #007bff; + text-decoration: none; + transition: color 0.2s ease; +} + +a:hover { + color: #0056b3; + text-decoration: underline; +} + +a:focus { + outline: 2px solid #007bff; + outline-offset: 2px; +} + +a:visited { + color: #6f42c1; +} + +/* Lists */ +ul, ol { + margin-bottom: 1rem; + padding-left: 2rem; +} + +ul { + list-style-type: disc; +} + +ol { + list-style-type: decimal; +} + +li { + margin-bottom: 0.25rem; +} + +ul ul, ol ol, ul ol, ol ul { + margin-bottom: 0; + margin-top: 0.25rem; +} + +/* Nested list styles */ +ul ul { list-style-type: circle; } +ul ul ul { list-style-type: square; } + +ol ol { list-style-type: lower-alpha; } +ol ol ol { list-style-type: lower-roman; } + +/* Description lists */ +dl { + margin-bottom: 1rem; +} + +dt { + font-weight: bold; + margin-top: 0.5rem; +} + +dd { + margin-left: 1rem; + margin-bottom: 0.5rem; +} + +/* Form Elements */ .form-group { margin-bottom: 1rem; width: 100%; } .required { - color: #dc3545; /* Bootstrap's danger color */ + color: #dc3545; } .form-group label { @@ -22,10 +216,35 @@ body { color: #333; } -.form-group input[type="text"], -.form-group input[type="email"], -.form-group select, -.form-group textarea { +/* Input elements */ +input[type="text"], +input[type="email"], +input[type="password"], +input[type="number"], +input[type="tel"], +input[type="url"], +input[type="search"], +input[type="date"], +input[type="time"], +input[type="datetime-local"], +input[type="month"], +input[type="week"], +input[type="color"], +input[type="file"], +select, +textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid #ccc; + border-radius: 0.25rem; + font-size: 1rem; + font-family: inherit; + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +/* Ensure form-group input styles apply to all types including password */ +.form-group input[type="password"] { width: 100%; padding: 0.75rem; border: 1px solid #ccc; @@ -34,8 +253,19 @@ body { box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); } +input:focus, +select:focus, +textarea:focus { + border-color: #007bff; + outline: none; + box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); +} + +/* Specific focus styles for form-group inputs */ +.form-group input[type="password"]:focus, .form-group input[type="text"]:focus, .form-group input[type="email"]:focus, +.form-group input[type="number"]:focus, .form-group select:focus, .form-group textarea:focus { border-color: #007bff; @@ -43,51 +273,208 @@ body { box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); } -.form-group select { +input:invalid { + border-color: #dc3545; +} + +input:invalid:focus { + box-shadow: 0 0 5px rgba(220, 53, 69, 0.5); +} + +/* Range input */ +input[type="range"] { + width: 100%; + height: 0.5rem; + background: #ddd; + border-radius: 0.25rem; + outline: none; + padding: 0; + box-shadow: none; +} + +input[type="range"]::-webkit-slider-thumb { + appearance: none; + width: 1.5rem; + height: 1.5rem; + background: #007bff; + border-radius: 50%; + cursor: pointer; +} + +input[type="range"]::-moz-range-thumb { + width: 1.5rem; + height: 1.5rem; + background: #007bff; + border-radius: 50%; + cursor: pointer; + border: none; +} + +/* Checkbox and radio */ +input[type="checkbox"], +input[type="radio"] { + width: auto; + margin-right: 0.5rem; + padding: 0; + box-shadow: none; +} + +input[type="checkbox"] { + border-radius: 0.125rem; +} + +input[type="radio"] { + border-radius: 50%; +} + +/* Select styling */ +select { appearance: none; background: url('data:image/svg+xml;charset=US-ASCII,%3Csvg xmlns%3D%22http%3A//www.w3.org/2000/svg%22 viewBox%3D%220 0 4 5%22%3E%3Cpath fill%3D%22%23000%22 d%3D%22M2 0L0 2h4z%22/%3E%3C/svg%3E') no-repeat right 0.75rem center; background-size: 0.5rem; + padding-right: 2.5rem; } -.form-group textarea { +select[multiple] { + background-image: none; + padding-right: 0.75rem; + height: auto; + min-height: 6rem; +} + +optgroup { + font-weight: bold; + color: #666; +} + +option { + padding: 0.25rem; +} + +/* Textarea */ +textarea { resize: vertical; + min-height: 6rem; } -.form-group .form-control-error { - color: #dc3545; - font-size: 0.875rem; - margin-top: 0.25rem; +/* Fieldset and Legend */ +fieldset { + border: 1px solid #ccc; + border-radius: 0.25rem; + padding: 1rem; + margin-bottom: 1rem; +} + +legend { + font-weight: bold; + padding: 0 0.5rem; + color: #333; +} + +/* Progress and Meter */ +progress, meter { + width: 100%; + height: 1.5rem; + appearance: none; + border: none; + border-radius: 0.25rem; + background-color: #e9ecef; +} + +progress::-webkit-progress-bar, +meter::-webkit-meter-bar { + background-color: #e9ecef; + border-radius: 0.25rem; +} + +progress::-webkit-progress-value { + background-color: #007bff; + border-radius: 0.25rem; +} + +meter::-webkit-meter-optimum-value { + background-color: #28a745; +} + +meter::-webkit-meter-suboptimum-value { + background-color: #ffc107; +} + +meter::-webkit-meter-even-less-good-value { + background-color: #dc3545; +} + +/* Output */ +output { + display: inline-block; + padding: 0.375rem 0.75rem; + background-color: #f8f9fa; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} + +/* Datalist styling (limited browser support) */ +datalist { + display: none; } /* Buttons */ -button { +button, +input[type="button"], +input[type="submit"], +input[type="reset"] { display: inline-block; padding: 0.75rem 1.5rem; font-size: 1rem; font-weight: bold; + font-family: inherit; border: none; border-radius: 0.25rem; cursor: pointer; - transition: background-color 0.3s ease, transform 0.2s ease; + text-align: center; + text-decoration: none; + transition: all 0.2s ease; + background-color: #6c757d; + color: white; } -button:hover { - transform: scale(1.05); +button:hover, +input[type="button"]:hover, +input[type="submit"]:hover, +input[type="reset"]:hover { + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } -button:active { - transform: scale(0.95); +button:active, +input[type="button"]:active, +input[type="submit"]:active, +input[type="reset"]:active { + transform: translateY(0); } -button:disabled { +button:disabled, +input[type="button"]:disabled, +input[type="submit"]:disabled, +input[type="reset"]:disabled { background-color: #ccc; cursor: not-allowed; + transform: none; + box-shadow: none; } -/* Primary Button */ +button:focus, +input[type="button"]:focus, +input[type="submit"]:focus, +input[type="reset"]:focus { + outline: 2px solid #007bff; + outline-offset: 2px; +} + +/* Button variants */ .btn-primary { - color: #fff; background-color: #007bff; + color: white; } .btn-primary:hover { @@ -98,10 +485,9 @@ button:disabled { background-color: #004085; } -/* Secondary Button */ .btn-secondary { - color: #fff; background-color: #6c757d; + color: white; } .btn-secondary:hover { @@ -112,40 +498,283 @@ button:disabled { background-color: #4e555b; } -/* Additional layout-specific styles */ -.bg-gray-100 { +.btn-success { + background-color: #28a745; + color: white; +} + +.btn-success:hover { + background-color: #218838; +} + +.btn-danger { + background-color: #dc3545; + color: white; +} + +.btn-danger:hover { + background-color: #c82333; +} + +.btn-warning { + background-color: #ffc107; + color: #212529; +} + +.btn-warning:hover { + background-color: #e0a800; +} + +.btn-info { + background-color: #17a2b8; + color: white; +} + +.btn-info:hover { + background-color: #138496; +} + +.btn-light { background-color: #f8f9fa; + color: #212529; + border: 1px solid #dee2e6; } -.bg-white { - background-color: #fff; +.btn-light:hover { + background-color: #e2e6ea; } -.bg-gray-200 { - background-color: #e9ecef; +.btn-dark { + background-color: #343a40; + color: white; } -.shadow-md { - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +.btn-dark:hover { + background-color: #23272b; } -.rounded { +/* Tables */ +table { + width: 100%; + border-collapse: collapse; + margin-bottom: 1rem; + background-color: white; +} + +th, td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid #dee2e6; + vertical-align: top; +} + +th { + font-weight: bold; + background-color: #f8f9fa; + border-top: 1px solid #dee2e6; +} + +tr:hover { + background-color: #f5f5f5; +} + +caption { + font-weight: bold; + margin-bottom: 0.5rem; + color: #666; +} + +thead th { + vertical-align: bottom; + border-bottom: 2px solid #dee2e6; +} + +tbody + tbody { + border-top: 2px solid #dee2e6; +} + +/* Table variants */ +.table-striped tbody tr:nth-of-type(odd) { + background-color: #f9f9f9; +} + +.table-bordered { + border: 1px solid #dee2e6; +} + +.table-bordered th, +.table-bordered td { + border: 1px solid #dee2e6; +} + +/* Images and Media */ +img { + max-width: 100%; + height: auto; border-radius: 0.25rem; } -.rounded-lg { - border-radius: 0.5rem; +figure { + margin: 1rem 0; + text-align: center; } -.text-xl { - font-size: 1.25rem; - font-weight: bold; +figcaption { + font-size: 0.875rem; + color: #666; + margin-top: 0.5rem; + font-style: italic; } -.font-bold { - font-weight: bold; +audio, video { + width: 100%; + max-width: 100%; } -.mb-4 { +canvas { + max-width: 100%; + height: auto; +} + +svg { + max-width: 100%; + height: auto; + fill: currentColor; +} + +/* Sectioning Elements */ +article, section, nav, aside { margin-bottom: 1rem; } + +header, footer { + padding: 1rem 0; +} + +main { + min-height: calc(100vh - 200px); +} + +/* Interactive Elements */ +details { + margin-bottom: 1rem; + border: 1px solid #dee2e6; + border-radius: 0.25rem; + padding: 1rem; +} + +summary { + font-weight: bold; + cursor: pointer; + margin-bottom: 0.5rem; + padding: 0.5rem; + background-color: #f8f9fa; + border-radius: 0.25rem; + transition: background-color 0.2s ease; +} + +summary:hover { + background-color: #e9ecef; +} + +details[open] summary { + margin-bottom: 1rem; +} + +dialog { + border: 1px solid #ccc; + border-radius: 0.5rem; + padding: 1rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + background: white; +} + +dialog::backdrop { + background: rgba(0, 0, 0, 0.5); +} + +/* Embedded Content */ +iframe { + width: 100%; + border: none; + border-radius: 0.25rem; +} + +embed, object { + max-width: 100%; +} + +/* Custom Layout Utilities (Non-Tailwind) */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +.row { + display: flex; + flex-wrap: wrap; + margin: -0.5rem; +} + +.col { + flex: 1; + padding: 0.5rem; +} + +/* Error states */ +.form-control-error { + color: #dc3545; + font-size: 0.875rem; + margin-top: 0.25rem; +} + +.is-invalid { + border-color: #dc3545; +} + +.is-valid { + border-color: #28a745; +} + +/* Print styles */ +@media print { + * { + box-shadow: none !important; + text-shadow: none !important; + } + + a, a:visited { + text-decoration: underline; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + pre, blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + + thead { + display: table-header-group; + } + + tr, img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + p, h2, h3 { + orphans: 3; + widows: 3; + } + + h2, h3 { + page-break-after: avoid; + } +} diff --git a/renderer/schema.go b/renderer/schema.go index 2d3d936..9c543aa 100644 --- a/renderer/schema.go +++ b/renderer/schema.go @@ -5,8 +5,150 @@ import ( "fmt" "html/template" "sort" + "strings" ) +// A single template for the entire group structure +const groupTemplateStr = ` +{{.Content}}
`, + "div": `{{.Content}}`, + "code": `
{{.Content}}
`,
+ "blockquote": `{{.ContentHTML}}`, + "cite": `{{.Content}}`, + "strong": `{{.Content}}`, + "em": `{{.Content}}`, + "small": `{{.Content}}`, + "mark": `{{.Content}}`, + "del": `
{{.Title}}
`, - "a": `{{.Title}}`, - "button": ``, -} - func renderField(field FieldInfo) string { ui, ok := field.Definition["ui"].(map[string]any) if !ok { return "" } - control, _ := ui["element"].(string) - class, _ := ui["class"].(string) - name, _ := ui["name"].(string) - title, _ := field.Definition["title"].(string) - placeholder, _ := field.Definition["placeholder"].(string) - - isRequired, _ := field.Definition["isRequired"].(bool) - required := "" - titleHTML := title - if isRequired { - required = "required" - titleHTML += ` *` + element, _ := ui["element"].(string) + if element == "" { + return "" } - inputType := "text" - if uiType, ok := ui["type"].(string); ok { - inputType = uiType - } else if fieldType, ok := field.Definition["type"].(string); ok && fieldType == "email" { - inputType = "email" - } + // Build all attributes + allAttributes := buildAllAttributes(field, ui) - var additionalAttributes bytes.Buffer - for key, value := range field.Definition { - switch key { - case "title", "ui", "placeholder", "type", "order", "isRequired": - continue - default: - additionalAttributes.WriteString(fmt.Sprintf(` %s="%v"`, key, value)) - } - } + // Get content + content := getFieldContent(field.Definition, ui) + contentHTML := getFieldContentHTML(field.Definition, ui) + + // Generate label if needed + labelHTML := generateLabel(field, ui) data := map[string]any{ - "Class": class, - "Name": name, - "Title": template.HTML(titleHTML), - "Placeholder": placeholder, - "Required": required, - "AdditionalAttributes": template.HTML(additionalAttributes.String()), - "InputType": inputType, - "Control": control, - "Href": "", + "Element": element, + "AllAttributes": template.HTMLAttr(allAttributes), + "Content": content, + "ContentHTML": template.HTML(contentHTML), + "LabelHTML": template.HTML(labelHTML), + "Class": getUIValue(ui, "class"), + "OptionsHTML": template.HTML(generateOptions(ui)), } - // Handle options for select - if control == "select" { - options, _ := ui["options"].([]any) - var optionsHTML bytes.Buffer - for _, option := range options { - optionsHTML.WriteString(fmt.Sprintf(``, option, option)) - } - data["OptionsHTML"] = template.HTML(optionsHTML.String()) + // Use specific template if available, otherwise use generic + var tmplStr string + if template, exists := fieldTemplates[element]; exists { + tmplStr = template + } else if voidElements[element] { + tmplStr = fieldTemplates["void"] + } else { + tmplStr = fieldTemplates["generic"] } - // Handle href for links - if control == "a" { - if href, ok := ui["href"].(string); ok { - data["Href"] = href + tmpl := template.Must(template.New(element).Parse(tmplStr)) + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return "" + } + return buf.String() +} + +func buildAllAttributes(field FieldInfo, ui map[string]any) string { + var attributes []string + + // Add standard attributes from ui + for _, attr := range standardAttrs { + if value, exists := ui[attr]; exists { + if attr == "class" && value == "" { + continue // Skip empty class + } + attributes = append(attributes, fmt.Sprintf(`%s="%v"`, attr, value)) } } - if tmplStr, ok := fieldTemplates[control]; ok { - tmpl := template.Must(template.New(control).Parse(tmplStr)) - var buf bytes.Buffer - tmpl.Execute(&buf, data) - return buf.String() + // Handle required field + if isRequired, ok := field.Definition["isRequired"].(bool); ok && isRequired { + if !contains(attributes, "required=") { + attributes = append(attributes, `required="required"`) + } } - // Handle generic 'h' template for h1-h6 - if control[0] == 'h' && len(control) == 2 && control[1] >= '1' && control[1] <= '6' { - data["Control"] = control - tmpl := template.Must(template.New("h").Parse(fieldTemplates["h"])) - var buf bytes.Buffer - tmpl.Execute(&buf, data) - return buf.String() + + // Handle input type based on field type + element, _ := ui["element"].(string) + if element == "input" { + if inputType := getInputType(field.Definition, ui); inputType != "" { + if !contains(attributes, "type=") { + attributes = append(attributes, fmt.Sprintf(`type="%s"`, inputType)) + } + } + } + + // Add data-* and aria-* attributes + for key, value := range ui { + if strings.HasPrefix(key, "data-") || strings.HasPrefix(key, "aria-") { + attributes = append(attributes, fmt.Sprintf(`%s="%v"`, key, value)) + } + } + + // Add custom attributes from field definition (excluding known schema properties) + excludeFields := map[string]bool{ + "type": true, "title": true, "ui": true, "placeholder": true, + "order": true, "isRequired": true, "content": true, "children": true, + } + + for key, value := range field.Definition { + if !excludeFields[key] && !strings.HasPrefix(key, "ui") { + attributes = append(attributes, fmt.Sprintf(`%s="%v"`, key, value)) + } + } + + return strings.Join(attributes, " ") +} + +func getInputType(fieldDef map[string]any, ui map[string]any) string { + // Check ui type first + if uiType, ok := ui["type"].(string); ok { + return uiType + } + + // Map schema types to input types + if fieldType, ok := fieldDef["type"].(string); ok { + switch fieldType { + case "email": + return "email" + case "password": + return "password" + case "number", "integer": + return "number" + case "boolean": + return "checkbox" + case "date": + return "date" + case "time": + return "time" + case "datetime": + return "datetime-local" + case "url": + return "url" + case "tel": + return "tel" + case "color": + return "color" + case "range": + return "range" + case "file": + return "file" + case "hidden": + return "hidden" + default: + return "text" + } + } + + return "text" +} + +func getFieldContent(fieldDef map[string]any, ui map[string]any) string { + // Check for content in ui first + if content, ok := ui["content"].(string); ok { + return content + } + + // Check for content in field definition + if content, ok := fieldDef["content"].(string); ok { + return content + } + + // Use title as fallback for some elements + if title, ok := fieldDef["title"].(string); ok { + return title } return "" } +func getFieldContentHTML(fieldDef map[string]any, ui map[string]any) string { + // Check for HTML content in ui + if contentHTML, ok := ui["contentHTML"].(string); ok { + return contentHTML + } + + // Check for children elements + if children, ok := ui["children"].([]any); ok { + return renderChildren(children) + } + + return "" +} + +func renderChildren(children []any) string { + var result strings.Builder + for _, child := range children { + if childMap, ok := child.(map[string]any); ok { + // Create a temporary field info for the child + childField := FieldInfo{ + Name: getMapValue(childMap, "name", ""), + Definition: childMap, + } + result.WriteString(renderField(childField)) + } + } + return result.String() +} + +func generateLabel(field FieldInfo, ui map[string]any) string { + // Check if label should be generated + if showLabel, ok := ui["showLabel"].(bool); !showLabel && ok { + return "" + } + + title, _ := field.Definition["title"].(string) + if title == "" { + return "" + } + + name := getUIValue(ui, "name") + if name == "" { + name = field.Name + } + + // Check if field is required + isRequired, _ := field.Definition["isRequired"].(bool) + requiredSpan := "" + if isRequired { + requiredSpan = ` *` + } + + return fmt.Sprintf(``, name, title, requiredSpan) +} + +func generateOptions(ui map[string]any) string { + options, ok := ui["options"].([]any) + if !ok { + return "" + } + + var optionsHTML strings.Builder + for _, option := range options { + if optionMap, ok := option.(map[string]any); ok { + // Complex option with attributes + value := getMapValue(optionMap, "value", "") + text := getMapValue(optionMap, "text", value) + selected := "" + if isSelected, ok := optionMap["selected"].(bool); ok && isSelected { + selected = ` selected="selected"` + } + disabled := "" + if isDisabled, ok := optionMap["disabled"].(bool); ok && isDisabled { + disabled = ` disabled="disabled"` + } + optionsHTML.WriteString(fmt.Sprintf(``, + value, selected, disabled, text)) + } else { + // Simple option (just value) + optionsHTML.WriteString(fmt.Sprintf(``, option, option)) + } + } + return optionsHTML.String() +} + +func getUIValue(ui map[string]any, key string) string { + if value, ok := ui[key].(string); ok { + return value + } + return "" +} + +func getMapValue(m map[string]any, key, defaultValue string) string { + if value, ok := m[key].(string); ok { + return value + } + return defaultValue +} + +func contains(slice []string, substr string) bool { + for _, item := range slice { + if strings.Contains(item, substr) { + return true + } + } + return false +} + // renderButtons generates HTML for form buttons func renderButtons(formConfig map[string]any) string { var buttonsHTML bytes.Buffer - tmpl := template.Must(template.New("button").Parse(fieldTemplates["button"])) if submitConfig, ok := formConfig["submit"].(map[string]any); ok { - data := map[string]any{ - "Type": submitConfig["type"], - "Class": submitConfig["class"], - "Label": submitConfig["label"], - } - tmpl.Execute(&buttonsHTML, data) + buttonHTML := renderButtonFromConfig(submitConfig, "submit") + buttonsHTML.WriteString(buttonHTML) } if resetConfig, ok := formConfig["reset"].(map[string]any); ok { - data := map[string]any{ - "Type": resetConfig["type"], - "Class": resetConfig["class"], - "Label": resetConfig["label"], + buttonHTML := renderButtonFromConfig(resetConfig, "reset") + buttonsHTML.WriteString(buttonHTML) + } + + // Support for additional custom buttons + if buttons, ok := formConfig["buttons"].([]any); ok { + for _, button := range buttons { + if buttonMap, ok := button.(map[string]any); ok { + buttonType := getMapValue(buttonMap, "type", "button") + buttonHTML := renderButtonFromConfig(buttonMap, buttonType) + buttonsHTML.WriteString(buttonHTML) + } } - tmpl.Execute(&buttonsHTML, data) } return buttonsHTML.String() } + +func renderButtonFromConfig(config map[string]any, defaultType string) string { + var attributes []string + + buttonType := getMapValue(config, "type", defaultType) + attributes = append(attributes, fmt.Sprintf(`type="%s"`, buttonType)) + + if class := getMapValue(config, "class", ""); class != "" { + attributes = append(attributes, fmt.Sprintf(`class="%s"`, class)) + } + + // Add other button attributes + for key, value := range config { + switch key { + case "type", "class", "label", "content": + continue // Already handled + default: + attributes = append(attributes, fmt.Sprintf(`%s="%v"`, key, value)) + } + } + + content := getMapValue(config, "label", getMapValue(config, "content", "Button")) + + return fmt.Sprintf(``, + strings.Join(attributes, " "), content) +}