From 4a55ffc47dbbca6475fceca15979dccb03855a7d Mon Sep 17 00:00:00 2001 From: sujit Date: Tue, 5 Aug 2025 07:18:15 +0545 Subject: [PATCH] update --- examples/app/schema.json | 525 ++++++++++++++++++++--- examples/app/templates/form.css | 715 ++++++++++++++++++++++++++++++-- renderer/schema.go | 527 ++++++++++++++++++----- 3 files changed, 1571 insertions(+), 196 deletions(-) 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": "70%" + } + }, + "rating_meter": { + "type": "string", + "order": 35, + "ui": { + "element": "div", + "class": "mb-4", + "contentHTML": "8 out of 10" + } + }, + "media_section": { + "type": "string", + "order": 40, + "ui": { + "element": "section", + "class": "mt-8 mb-4", + "contentHTML": "

Media Examples

" + } + }, + "demo_image": { + "type": "string", + "order": 41, + "ui": { + "element": "figure", + "class": "mb-4", + "contentHTML": "Demo placeholder image
Sample image with caption
" + } + }, + "table_section": { + "type": "string", + "order": 50, + "ui": { + "element": "div", + "class": "mt-8 mb-4", + "contentHTML": "

Data Table Example

NameRoleExperience
John DoeDeveloper5 years
Jane SmithDesigner3 years
" + } + }, + "list_examples": { + "type": "string", + "order": 60, + "ui": { + "element": "div", + "class": "mt-8 mb-4", + "contentHTML": "

List Examples

Unordered List:

  • First item
  • Second item
  • Third item

Ordered List:

  1. Step one
  2. Step two
  3. Step three

Description List:

Term 1:
Definition 1
Term 2:
Definition 2
" + } + }, + "interactive_details": { + "type": "string", + "order": 70, + "ui": { + "element": "details", + "class": "border border-gray-300 rounded p-4 mb-4", + "contentHTML": "Click to expand advanced options
" + } + }, + "code_example": { + "type": "string", + "order": 80, + "ui": { + "element": "div", + "class": "mt-8 mb-4", + "contentHTML": "

Code Example

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": "

Text Formatting Examples

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 = ` +
+ {{if .Title.Text}} + {{if .Title.Class}} +
{{.Title.Text}}
+ {{else}} +

{{.Title.Text}}

+ {{end}} + {{end}} +
{{.FieldsHTML}}
+
+` + +// Templates for field rendering - now supports all HTML DOM elements +var fieldTemplates = map[string]string{ + // Form elements + "input": `
{{.LabelHTML}}{{.ContentHTML}}
`, + "textarea": `
{{.LabelHTML}}{{.ContentHTML}}
`, + "select": `
{{.LabelHTML}}{{.ContentHTML}}
`, + "button": ``, + "option": ``, + "optgroup": `{{.OptionsHTML}}`, + "label": ``, + "fieldset": `
{{.ContentHTML}}
`, + "legend": `{{.Content}}`, + "datalist": `{{.OptionsHTML}}`, + "output": `{{.Content}}`, + "progress": `{{.Content}}`, + "meter": `{{.Content}}`, + + // Text content elements + "h1": `

{{.Content}}

`, + "h2": `

{{.Content}}

`, + "h3": `

{{.Content}}

`, + "h4": `

{{.Content}}

`, + "h5": `
{{.Content}}
`, + "h6": `
{{.Content}}
`, + "p": `

{{.Content}}

`, + "div": `
{{.ContentHTML}}
`, + "span": `{{.Content}}`, + "pre": `
{{.Content}}
`, + "code": `{{.Content}}`, + "blockquote": `
{{.ContentHTML}}
`, + "cite": `{{.Content}}`, + "strong": `{{.Content}}`, + "em": `{{.Content}}`, + "small": `{{.Content}}`, + "mark": `{{.Content}}`, + "del": `{{.Content}}`, + "ins": `{{.Content}}`, + "sub": `{{.Content}}`, + "sup": `{{.Content}}`, + "abbr": `{{.Content}}`, + "address": `
{{.ContentHTML}}
`, + "time": ``, + + // List elements + "ul": ``, + "ol": `
    {{.ContentHTML}}
`, + "li": `
  • {{.Content}}
  • `, + "dl": `
    {{.ContentHTML}}
    `, + "dt": `
    {{.Content}}
    `, + "dd": `
    {{.Content}}
    `, + + // Links and media + "a": `{{.Content}}`, + "img": ``, + "figure": `
    {{.ContentHTML}}
    `, + "figcaption": `
    {{.Content}}
    `, + "audio": ``, + "video": ``, + "source": ``, + "track": ``, + + // Table elements + "table": `{{.ContentHTML}}
    `, + "caption": `{{.Content}}`, + "thead": `{{.ContentHTML}}`, + "tbody": `{{.ContentHTML}}`, + "tfoot": `{{.ContentHTML}}`, + "tr": `{{.ContentHTML}}`, + "th": `{{.Content}}`, + "td": `{{.Content}}`, + "colgroup": `{{.ContentHTML}}`, + "col": ``, + + // Sectioning elements + "article": `
    {{.ContentHTML}}
    `, + "section": `
    {{.ContentHTML}}
    `, + "nav": ``, + "aside": ``, + "header": `
    {{.ContentHTML}}
    `, + "footer": ``, + "main": `
    {{.ContentHTML}}
    `, + + // Interactive elements + "details": `
    {{.ContentHTML}}
    `, + "summary": `{{.Content}}`, + "dialog": `{{.ContentHTML}}`, + + // Embedded content + "iframe": ``, + "embed": ``, + "object": `{{.ContentHTML}}`, + "param": ``, + "picture": `{{.ContentHTML}}`, + "canvas": `{{.Content}}`, + "svg": `{{.ContentHTML}}`, + + // Meta elements + "br": `
    `, + "hr": `
    `, + "wbr": ``, + + // Generic template for any unlisted element + "generic": `<{{.Element}} {{.AllAttributes}}>{{.ContentHTML}}`, + "void": `<{{.Element}} {{.AllAttributes}} />`, +} + +// Void elements that don't have closing tags +var voidElements = map[string]bool{ + "area": true, "base": true, "br": true, "col": true, "embed": true, + "hr": true, "img": true, "input": true, "link": true, "meta": true, + "param": true, "source": true, "track": true, "wbr": true, +} + +var standardAttrs = []string{ + "id", "class", "name", "type", "value", "placeholder", "href", "src", + "alt", "title", "target", "rel", "role", "tabindex", "accesskey", + "contenteditable", "draggable", "hidden", "spellcheck", "translate", + "autocomplete", "autofocus", "disabled", "readonly", "required", + "multiple", "checked", "selected", "defer", "async", "loop", "muted", + "controls", "autoplay", "preload", "poster", "width", "height", + "rows", "cols", "size", "maxlength", "minlength", "min", "max", + "step", "pattern", "accept", "capture", "form", "formaction", + "formenctype", "formmethod", "formnovalidate", "formtarget", + "colspan", "rowspan", "headers", "scope", "start", "reversed", + "datetime", "open", "label", "high", "low", "optimum", "span", +} + // FieldInfo represents metadata for a field extracted from JSONSchema type FieldInfo struct { Name string @@ -198,21 +340,6 @@ func parseGroupsFromSchema(schema map[string]any) []GroupInfo { // renderGroup generates HTML for a single group func renderGroup(group GroupInfo) string { var groupHTML bytes.Buffer - - // A single template for the entire group structure - const groupTemplateStr = ` -
    - {{if .Title.Text}} - {{if .Title.Class}} -
    {{.Title.Text}}
    - {{else}} -

    {{.Title.Text}}

    - {{end}} - {{end}} -
    {{.FieldsHTML}}
    -
    -` - // Render fields var fieldsHTML bytes.Buffer for _, field := range group.Fields { @@ -236,123 +363,327 @@ func renderGroup(group GroupInfo) string { return groupHTML.String() } -// Templates for field rendering -var fieldTemplates = map[string]string{ - "input": `
    `, - "textarea": `
    `, - "select": `
    `, - "h": `<{{.Control}} class="{{.Class}}" id="{{.Name}}" {{.AdditionalAttributes}}>{{.Title}}`, - "p": `

    {{.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) +}