initiales bauen mit mistral

This commit is contained in:
Björn Runden committed 2026-08-01 15:40:30 +02:00
1 parent 7e2e8bdf59
commit c74bca1e13
22 files changed
+5642

No files matched your search

+357
View File
@@ -0,0 +1,357 @@
/* =============================================================================
Formeleon - Multi-tenant Form Generator
Author: Björn Runden <br@runden.net>
Script: style.css
Description: Custom styles for Formeleon admin interface.
=============================================================================
*/
/* Global Styles */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: #f8f9fa;
}
/* Card Styles */
.card {
border: none;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
}
.card-header {
background-color: rgba(0, 0, 0, 0.03);
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
}
.card-footer {
background-color: rgba(0, 0, 0, 0.03);
border-top: 1px solid rgba(0, 0, 0, 0.125);
}
/* Badge Styles */
.badge {
font-weight: 500;
padding: 0.35em 0.65em;
border-radius: 0.25rem;
}
/* Button Styles */
.btn {
border-radius: 0.375rem;
font-weight: 500;
transition: all 0.2s;
}
.btn-outline-* {
border-width: 2px;
}
/* Form Control Styles */
.form-control, .form-select {
border-radius: 0.375rem;
padding: 0.5rem 0.75rem;
border: 1px solid #ced4da;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-control:focus, .form-select:focus {
border-color: #80bdff;
box-shadow: 0 0 0 0.25rem rgba(0, 123, 255, 0.25);
}
/* Table Styles */
.table {
margin-bottom: 0;
}
.table th {
font-weight: 600;
border-bottom: 2px solid #dee2e6;
padding: 0.75rem 1rem;
}
.table td {
padding: 0.75rem 1rem;
vertical-align: middle;
}
.table-hover tbody tr:hover {
background-color: rgba(0, 0, 0, 0.05);
}
/* Dev Mode Banner */
.dev-mode-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(90deg, #ffc107, #ff8c00);
color: #000;
padding: 12px 20px;
font-size: 0.9rem;
font-weight: 500;
z-index: 1000;
box-shadow: 0 -2px 15px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 10px;
}
.dev-mode-banner i {
font-size: 1.2rem;
}
/* NavBar Styles */
.navbar {
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 0.5rem 1rem;
}
.navbar-brand {
font-weight: 700;
font-size: 1.25rem;
}
.navbar-nav .nav-link {
padding: 0.5rem 1rem;
font-weight: 500;
}
.navbar-nav .nav-link:hover {
color: #0d6efd;
}
.dropdown-menu {
border-radius: 0.5rem;
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
border: none;
padding: 0.5rem 0;
}
.dropdown-item {
padding: 0.5rem 1.5rem;
font-weight: 500;
}
.dropdown-item:hover, .dropdown-item:focus {
background-color: #f8f9fa;
color: #0d6efd;
}
/* Input Group Styles */
.input-group-text {
border-radius: 0.375rem;
}
/* Alert Styles */
.alert {
border-radius: 0.375rem;
border: none;
padding: 1rem 1.25rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.alert-heading {
font-weight: 600;
}
/* Code Blocks */
code {
background-color: rgba(0, 0, 0, 0.05);
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.875em;
}
pre {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 0.375rem;
border: 1px solid #dee2e6;
overflow-x: auto;
}
/* Utility Classes */
.text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.py-5 {
padding-top: 3rem !important;
padding-bottom: 3rem !important;
}
.display-4 {
font-size: calc(1.525rem + 3.3vw);
font-weight: 300;
line-height: 1.2;
}
/* Responsive Adjustments */
@media (max-width: 768px) {
.navbar-collapse {
background-color: rgba(0, 0, 0, 0.1);
}
.dropdown-menu {
position: static;
float: none;
}
.dev-mode-banner {
flex-direction: column;
text-align: center;
padding: 8px;
font-size: 0.8rem;
}
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Form Builder Specific Styles */
.form-builder {
background: #fff;
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.form-field {
border: 1px dashed #ced4da;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 0.375rem;
transition: all 0.2s;
}
.form-field:hover {
border-color: #80bdff;
background-color: rgba(0, 123, 255, 0.05);
}
.form-field-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.form-field-actions {
display: flex;
gap: 0.5rem;
}
/* JSON Editor Styles */
.json-editor {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1rem;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
white-space: pre;
overflow-x: auto;
max-height: 400px;
overflow-y: auto;
}
/* Status Badge Colors */
.bg-success {
background-color: #198754 !important;
}
.bg-warning {
background-color: #ffc107 !important;
}
.bg-danger {
background-color: #dc3545 !important;
}
.bg-primary {
background-color: #0d6efd !important;
}
.bg-info {
background-color: #0dcaf0 !important;
}
/* Toolbar Styles */
.toolbar {
background: #fff;
border-bottom: 1px solid #dee2e6;
padding: 0.75rem 1rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
.toolbar-group {
display: flex;
gap: 0.5rem;
align-items: center;
}
/* Modal Styles */
.modal-content {
border-radius: 0.5rem;
border: none;
}
.modal-header {
background-color: rgba(0, 0, 0, 0.03);
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
}
.modal-footer {
background-color: rgba(0, 0, 0, 0.03);
border-top: 1px solid rgba(0, 0, 0, 0.125);
}
/* Pagination Styles */
.pagination {
justify-content: center;
}
.page-item.active .page-link {
background-color: #0d6efd;
border-color: #0d6efd;
}
.page-link {
color: #0d6efd;
border-radius: 0.375rem;
}
.page-link:hover {
background-color: #f8f9fa;
}
+432
View File
@@ -0,0 +1,432 @@
/** =============================================================================
* Formeleon - Multi-tenant Form Generator
* Author: Björn Runden <br@runden.net>
* Script: main.js
* Description: Main JavaScript for Formeleon admin interface.
* Handles common functionality, AJAX requests, and UI interactions.
* =============================================================================
*/
// Wait for DOM to be ready
$(document).ready(function() {
// Initialize tooltips
$('[data-bs-toggle="tooltip"]').tooltip();
// Initialize popovers
$('[data-bs-toggle="popover"]').popover();
// Handle auto-dismiss alerts
setTimeout(function() {
$('.alert').alert('close');
}, 5000);
// Form field management (for form builder)
initFormBuilder();
// JSON editor initialization
initJsonEditor();
// Search functionality
initSearch();
// Confirmation for delete actions
initDeleteConfirmation();
});
/**
* Initialize form builder functionality
*/
function initFormBuilder() {
// Add field button
$(document).on('click', '.add-field', function(e) {
e.preventDefault();
const container = $(this).data('container');
addFieldToContainer(container);
});
// Remove field button
$(document).on('click', '.remove-field', function(e) {
e.preventDefault();
$(this).closest('.form-field').remove();
});
// Field type change handler
$(document).on('change', '.field-type-select', function() {
const fieldDiv = $(this).closest('.form-field');
const fieldType = $(this).val();
updateFieldOptions(fieldDiv, fieldType);
});
}
/**
* Add a new field to the form builder
* @param {string} container - CSS selector for the container
*/
function addFieldToContainer(container) {
const containerEl = $(container);
const fieldCount = containerEl.find('.form-field').length + 1;
const fieldHtml = `
<div class="form-field border p-3 mb-3 rounded">
<div class="form-field-header d-flex justify-content-between mb-2">
<h5>Field ${fieldCount}</h5>
<div class="form-field-actions">
<button type="button" class="btn btn-sm btn-outline-danger remove-field">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Field Name</label>
<input type="text" class="form-control" name="fields[${fieldCount}][name]" placeholder="field_name">
</div>
<div class="col-md-3">
<label class="form-label">Type</label>
<select class="form-select field-type-select" name="fields[${fieldCount}][type]">
<option value="text">Text</option>
<option value="email">Email</option>
<option value="textarea">Textarea</option>
<option value="select">Select</option>
<option value="checkbox">Checkbox</option>
<option value="radio">Radio</option>
<option value="number">Number</option>
<option value="date">Date</option>
<option value="file">File</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Label</label>
<input type="text" class="form-control" name="fields[${fieldCount}][label]" placeholder="Field Label">
</div>
<div class="col-md-3">
<div class="form-check form-switch mt-4">
<input class="form-check-input" type="checkbox" name="fields[${fieldCount}][required]" value="true">
<label class="form-check-label">Required</label>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-12">
<label class="form-label">Options (for select/radio/checkbox)</label>
<input type="text" class="form-control" name="fields[${fieldCount}][options]" placeholder="option1,option2,option3">
<div class="form-text">Comma-separated list of options</div>
</div>
</div>
</div>
`;
containerEl.append(fieldHtml);
}
/**
* Update field options based on field type
* @param {jQuery} fieldDiv - The field div element
* @param {string} fieldType - The selected field type
*/
function updateFieldOptions(fieldDiv, fieldType) {
const optionsGroup = fieldDiv.find('.field-options');
// Remove existing options
optionsGroup.remove();
// Add type-specific options
switch(fieldType) {
case 'select':
case 'radio':
case 'checkbox':
const optionsHtml = `
<div class="field-options row mt-2">
<div class="col-12">
<label class="form-label">Options</label>
<textarea class="form-control" name="fields[][options]" placeholder="option1\noption2\noption3"></textarea>
<div class="form-text">One option per line</div>
</div>
</div>
`;
fieldDiv.append(optionsHtml);
break;
case 'textarea':
const textareaHtml = `
<div class="field-options row mt-2">
<div class="col-md-6">
<label class="form-label">Rows</label>
<input type="number" class="form-control" name="fields[][rows]" value="3">
</div>
<div class="col-md-6">
<label class="form-label">Placeholder</label>
<input type="text" class="form-control" name="fields[][placeholder]" placeholder="Enter text here...">
</div>
</div>
`;
fieldDiv.append(textareaHtml);
break;
default:
// No additional options for basic field types
break;
}
}
/**
* Initialize JSON editor for form structure
*/
function initJsonEditor() {
// Pretty print JSON in textarea
$('.json-editor').each(function() {
const textarea = $(this);
try {
const json = JSON.parse(textarea.val());
textarea.val(JSON.stringify(json, null, 2));
} catch (e) {
// Not valid JSON, leave as is
}
});
// Format JSON button
$(document).on('click', '.format-json', function(e) {
e.preventDefault();
const target = $(this).data('target');
const textarea = $(target);
try {
const json = JSON.parse(textarea.val());
textarea.val(JSON.stringify(json, null, 2));
} catch (e) {
alert('Invalid JSON: ' + e.message);
}
});
// Minify JSON button
$(document).on('click', '.minify-json', function(e) {
e.preventDefault();
const target = $(this).data('target');
const textarea = $(target);
try {
const json = JSON.parse(textarea.val());
textarea.val(JSON.stringify(json));
} catch (e) {
alert('Invalid JSON: ' + e.message);
}
});
}
/**
* Initialize search functionality
*/
function initSearch() {
// Table search
$('#searchInput').on('input', function() {
const searchTerm = $(this).val().toLowerCase();
const table = $(this).closest('.card').find('table tbody tr');
table.each(function() {
const text = $(this).text().toLowerCase();
const visible = text.includes(searchTerm);
$(this).toggle(visible);
});
});
}
/**
* Initialize delete confirmation dialogs
*/
function initDeleteConfirmation() {
// Already handled inline with onsubmit="return confirm(...)"
// This is a fallback for more complex confirmations
$(document).on('click', '.confirm-delete', function(e) {
e.preventDefault();
const message = $(this).data('confirm-message') || 'Are you sure you want to delete this item?';
const form = $(this).closest('form');
if (confirm(message)) {
form.submit();
}
});
}
/**
* AJAX helper function
* @param {string} url - URL to request
* @param {string} method - HTTP method (GET, POST, PUT, DELETE)
* @param {object} data - Data to send
* @param {function} success - Success callback
* @param {function} error - Error callback
*/
function ajaxRequest(url, method, data, success, error) {
$.ajax({
url: url,
method: method,
data: data,
contentType: 'application/json',
dataType: 'json',
headers: {
'X-CSRFToken': getCsrfToken()
},
success: function(response) {
if (typeof success === 'function') {
success(response);
}
},
error: function(xhr, status, err) {
if (typeof error === 'function') {
error(xhr, status, err);
} else {
alert('Error: ' + (xhr.responseJSON?.message || err));
}
}
});
}
/**
* Get CSRF token from meta tag or cookie
* @return {string} CSRF token
*/
function getCsrfToken() {
return $('meta[name="csrf-token"]').attr('content') ||
getCookie('csrftoken') ||
getCookie('csrf_token');
}
/**
* Get cookie by name
* @param {string} name - Cookie name
* @return {string} Cookie value or null
*/
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
/**
* Show loading spinner
* @param {string|jQuery} element - Element or selector to add spinner to
*/
function showLoading(element) {
const el = $(element);
el.append('<span class="spinner-border spinner-border-sm ms-2" role="status" aria-hidden="true"></span>');
el.attr('disabled', true);
}
/**
* Hide loading spinner
* @param {string|jQuery} element - Element or selector to remove spinner from
*/
function hideLoading(element) {
const el = $(element);
el.find('.spinner-border').remove();
el.removeAttr('disabled');
}
/**
* Format date for display
* @param {string} dateString - ISO date string
* @return {string} Formatted date
*/
function formatDate(dateString) {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return date.toLocaleString();
}
/**
* Format bytes for display
* @param {number} bytes - Number of bytes
* @return {string} Formatted size
*/
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Generate a random ID
* @param {number} length - Length of ID (default: 8)
* @return {string} Random ID
*/
function generateId(length = 8) {
return Math.random().toString(36).substring(2, 2 + length);
}
/**
* Copy text to clipboard
* @param {string} text - Text to copy
* @param {function} callback - Optional callback
*/
function copyToClipboard(text, callback) {
navigator.clipboard.writeText(text).then(function() {
if (typeof callback === 'function') {
callback(true);
}
}).catch(function(err) {
if (typeof callback === 'function') {
callback(false, err);
}
});
}
/**
* Show toast notification
* @param {string} message - Message to display
* @param {string} type - Type (success, error, warning, info)
*/
function showToast(message, type = 'info') {
const toastId = 'toast-' + Date.now();
const toastHtml = `
<div id="${toastId}" class="toast align-items-center text-white bg-${type} border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`;
// Create toast container if it doesn't exist
let container = $('.toast-container');
if (container.length === 0) {
container = $('<div class="toast-container position-fixed bottom-0 end-0 p-3"></div>');
$('body').append(container);
}
container.append(toastHtml);
const toastElement = $('#' + toastId);
const toast = new bootstrap.Toast(toastElement[0], { autohide: true, delay: 3000 });
toast.show();
// Remove toast after it's hidden
toastElement.on('hidden.bs.toast', function() {
toastElement.remove();
});
}
// Export functions for use in other scripts
window.Formeleon = {
ajaxRequest: ajaxRequest,
showLoading: showLoading,
hideLoading: hideLoading,
formatDate: formatDate,
formatBytes: formatBytes,
generateId: generateId,
copyToClipboard: copyToClipboard,
showToast: showToast,
getCsrfToken: getCsrfToken
};