chore: add spec-kit and standardize signatures

- Added GitHub spec-kit for development workflow
- Standardized file signatures to Descomplicar® format
- Updated development configuration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Emanuel Almeida
2025-09-12 01:27:34 +01:00
parent 4a60be990e
commit 38bb926742
118 changed files with 40694 additions and 0 deletions

View File

@@ -0,0 +1,301 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
/**
* Care Booking Block - Frontend CSS
*
* Base styles for enhanced KiviCare integration and graceful degradation
*
* @package CareBookingBlock
*/
/* === LOADING STATES === */
.care-booking-loading {
position: relative;
opacity: 0.7;
pointer-events: none;
}
.care-booking-loading::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
animation: care-booking-spin 1s linear infinite;
z-index: 1000;
}
.care-booking-loading::after {
content: "Loading...";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, 20px);
font-size: 12px;
color: #666;
z-index: 1001;
}
@keyframes care-booking-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* === FALLBACK STATES === */
.care-booking-fallback {
opacity: 0.7;
pointer-events: none;
position: relative;
}
.care-booking-fallback::after {
content: "Service temporarily unavailable";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #666;
border: 1px dashed #ccc;
z-index: 100;
}
/* === ENHANCED KIVICARE SELECTORS === */
.care-booking-enhanced {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.care-booking-enhanced:hover {
opacity: 0.9;
transform: translateY(-1px);
}
/* KiviCare 3.0+ compatibility */
.kc-doctor-item,
.kc-service-item,
.kivicare-doctor,
.kivicare-service {
transition: all 0.2s ease;
}
.kc-doctor-item[data-blocked="true"],
.kc-service-item[data-blocked="true"],
.kivicare-doctor[data-blocked="true"],
.kivicare-service[data-blocked="true"] {
opacity: 0;
height: 0;
overflow: hidden;
margin: 0;
padding: 0;
border: none;
}
/* === FORM ENHANCEMENTS === */
.care-booking-form-container {
position: relative;
}
.care-booking-form-container .field-error {
color: #dc3545;
font-size: 12px;
margin-top: 4px;
display: block;
}
.care-booking-form-container input.error,
.care-booking-form-container select.error {
border-color: #dc3545;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}
.care-booking-form-container .success-message {
color: #28a745;
background-color: #d4edda;
border: 1px solid #c3e6cb;
padding: 8px 12px;
border-radius: 4px;
margin: 10px 0;
}
.care-booking-form-container .error-message {
color: #721c24;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
padding: 8px 12px;
border-radius: 4px;
margin: 10px 0;
}
.care-booking-retry {
background-color: #007cba;
color: white;
border: none;
padding: 6px 12px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
margin-left: 8px;
}
.care-booking-retry:hover {
background-color: #005a87;
}
/* === OFFLINE STATES === */
.care-booking-offline-message {
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: #ff6b6b;
color: white;
padding: 10px;
text-align: center;
z-index: 10000;
animation: care-booking-slide-down 0.3s ease;
}
@keyframes care-booking-slide-down {
from { transform: translateY(-100%); }
to { transform: translateY(0); }
}
/* === ACCESSIBILITY === */
.care-booking-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* === RESPONSIVE DESIGN === */
@media (max-width: 768px) {
.care-booking-loading::after {
font-size: 11px;
transform: translate(-50%, 15px);
}
.care-booking-fallback::after {
font-size: 12px;
padding: 10px;
}
.care-booking-offline-message {
font-size: 14px;
padding: 8px;
}
}
@media (max-width: 480px) {
.care-booking-loading::before {
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
}
.care-booking-loading::after {
font-size: 10px;
transform: translate(-50%, 12px);
}
}
/* === HIGH CONTRAST MODE === */
@media (prefers-contrast: high) {
.care-booking-fallback::after {
background: #000;
color: #fff;
border: 2px solid #fff;
}
.care-booking-offline-message {
background-color: #000;
border-bottom: 2px solid #fff;
}
}
/* === REDUCED MOTION === */
@media (prefers-reduced-motion: reduce) {
.care-booking-enhanced,
.kc-doctor-item,
.kc-service-item,
.kivicare-doctor,
.kivicare-service {
transition: none;
}
@keyframes care-booking-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(0deg); }
}
.care-booking-offline-message {
animation: none;
}
}
/* === PRINT STYLES === */
@media print {
.care-booking-loading,
.care-booking-loading::before,
.care-booking-loading::after,
.care-booking-offline-message,
.care-booking-retry {
display: none !important;
}
.care-booking-fallback::after {
display: none;
}
.care-booking-fallback {
opacity: 1;
pointer-events: all;
}
}
/* === DARK MODE SUPPORT === */
@media (prefers-color-scheme: dark) {
.care-booking-loading::after {
color: #ccc;
}
.care-booking-fallback::after {
background: rgba(40, 40, 40, 0.95);
color: #ccc;
border-color: #666;
}
.care-booking-form-container .field-error {
color: #ff6b6b;
}
.care-booking-form-container .success-message {
background-color: #1e4d2b;
border-color: #2d5a35;
color: #86efac;
}
.care-booking-form-container .error-message {
background-color: #4d1e24;
border-color: #5a2d35;
color: #fca5a5;
}
}

View File

@@ -0,0 +1,6 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
.care-booking-loading{position:relative;opacity:0.7;pointer-events:none;}.care-booking-loading::before{content:"";position:absolute;top:50%;left:50%;width:20px;height:20px;margin:-10px 0 0 -10px;border:2px solid #f3f3f3;border-top:2px solid #3498db;border-radius:50%;animation:care-booking-spin 1s linear infinite;z-index:1000;}.care-booking-loading::after{content:"Loading...";position:absolute;top:50%;left:50%;transform:translate(-50%,20px);font-size:12px;color:#666;z-index:1001;}@keyframes care-booking-spin{0%{transform:rotate(0deg);}100%{transform:rotate(360deg);}}.care-booking-fallback{opacity:0.7;pointer-events:none;position:relative;}.care-booking-fallback::after{content:"Service temporarily unavailable";position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(255,255,255,0.9);display:flex;align-items:center;justify-content:center;font-size:14px;color:#666;border:1px dashed #ccc;z-index:100;}.care-booking-enhanced{transition:opacity 0.3s ease,transform 0.3s ease;}.care-booking-enhanced:hover{opacity:0.9;transform:translateY(-1px);}.kc-doctor-item,.kc-service-item,.kivicare-doctor,.kivicare-service{transition:all 0.2s ease;}.kc-doctor-item[data-blocked="true"],.kc-service-item[data-blocked="true"],.kivicare-doctor[data-blocked="true"],.kivicare-service[data-blocked="true"]{opacity:0;height:0;overflow:hidden;margin:0;padding:0;border:none;}.care-booking-form-container{position:relative;}.care-booking-form-container .field-error{color:#dc3545;font-size:12px;margin-top:4px;display:block;}.care-booking-form-container input.error,.care-booking-form-container select.error{border-color:#dc3545;box-shadow:0 0 0 0.2rem rgba(220,53,69,0.25);}.care-booking-form-container .success-message{color:#28a745;background-color:#d4edda;border:1px solid #c3e6cb;padding:8px 12px;border-radius:4px;margin:10px 0;}.care-booking-form-container .error-message{color:#721c24;background-color:#f8d7da;border:1px solid #f5c6cb;padding:8px 12px;border-radius:4px;margin:10px 0;}.care-booking-retry{background-color:#007cba;color:white;border:none;padding:6px 12px;border-radius:3px;cursor:pointer;font-size:12px;margin-left:8px;}.care-booking-retry:hover{background-color:#005a87;}.care-booking-offline-message{position:fixed;top:0;left:0;right:0;background-color:#ff6b6b;color:white;padding:10px;text-align:center;z-index:10000;animation:care-booking-slide-down 0.3s ease;}@keyframes care-booking-slide-down{from{transform:translateY(-100%);}to{transform:translateY(0);}}.care-booking-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}@media (max-width:768px){.care-booking-loading::after{font-size:11px;transform:translate(-50%,15px);}.care-booking-fallback::after{font-size:12px;padding:10px;}.care-booking-offline-message{font-size:14px;padding:8px;}}@media (max-width:480px){.care-booking-loading::before{width:16px;height:16px;margin:-8px 0 0 -8px;}.care-booking-loading::after{font-size:10px;transform:translate(-50%,12px);}}@media (prefers-contrast:high){.care-booking-fallback::after{background:#000;color:#fff;border:2px solid #fff;}.care-booking-offline-message{background-color:#000;border-bottom:2px solid #fff;}}@media (prefers-reduced-motion:reduce){.care-booking-enhanced,.kc-doctor-item,.kc-service-item,.kivicare-doctor,.kivicare-service{transition:none;}@keyframes care-booking-spin{0%{transform:rotate(0deg);}100%{transform:rotate(0deg);}}.care-booking-offline-message{animation:none;}}@media print{.care-booking-loading,.care-booking-loading::before,.care-booking-loading::after,.care-booking-offline-message,.care-booking-retry{display:none !important;}.care-booking-fallback::after{display:none;}.care-booking-fallback{opacity:1;pointer-events:all;}}@media (prefers-color-scheme:dark){.care-booking-loading::after{color:#ccc;}.care-booking-fallback::after{background:rgba(40,40,40,0.95);color:#ccc;border-color:#666;}.care-booking-form-container .field-error{color:#ff6b6b;}.care-booking-form-container .success-message{background-color:#1e4d2b;border-color:#2d5a35;color:#86efac;}.care-booking-form-container .error-message{background-color:#4d1e24;border-color:#5a2d35;color:#fca5a5;}}

View File

@@ -0,0 +1,482 @@
/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
/**
* Care Booking Block - Frontend JavaScript
*
* Provides graceful degradation and enhanced interaction for KiviCare integration
*
* @package CareBookingBlock
*/
(function($, config) {
'use strict';
// Global configuration
const CareBooking = {
config: config || {},
initialized: false,
retryCount: 0,
observers: [],
/**
* Initialize the Care Booking frontend functionality
*/
init: function() {
if (this.initialized) {
return;
}
if (this.config.debug) {
console.log('Care Booking Block: Initializing frontend scripts');
}
this.setupObservers();
this.enhanceExistingElements();
this.setupEventListeners();
this.setupFallbacks();
this.initialized = true;
},
/**
* Setup MutationObserver to watch for dynamically added content
*/
setupObservers: function() {
if (!window.MutationObserver) {
if (this.config.debug) {
console.warn('Care Booking Block: MutationObserver not supported');
}
return;
}
const observer = new MutationObserver((mutations) => {
let hasNewContent = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
// Check if new node contains KiviCare content
if (this.hasKiviCareContent(node)) {
hasNewContent = true;
}
}
});
}
});
if (hasNewContent) {
this.enhanceNewContent();
}
});
// Start observing
observer.observe(document.body, {
childList: true,
subtree: true
});
this.observers.push(observer);
},
/**
* Check if element contains KiviCare content
* @param {Element} element
* @returns {boolean}
*/
hasKiviCareContent: function(element) {
const selectors = [
this.config.selectors.doctors,
this.config.selectors.services,
this.config.selectors.forms
].join(', ');
return $(element).find(selectors).length > 0 || $(element).is(selectors);
},
/**
* Enhance existing KiviCare elements on page load
*/
enhanceExistingElements: function() {
this.enhanceLoadingStates();
this.enhanceFormValidation();
this.enhanceFallbackElements();
},
/**
* Enhance newly added content
*/
enhanceNewContent: function() {
if (this.config.debug) {
console.log('Care Booking Block: Enhancing new content');
}
// Add a small delay to ensure DOM is stable
setTimeout(() => {
this.enhanceExistingElements();
}, 100);
},
/**
* Setup loading states for better UX
*/
enhanceLoadingStates: function() {
const $forms = $(this.config.selectors.forms);
$forms.each((index, form) => {
const $form = $(form);
// Add loading indicator
if (!$form.find('.care-booking-loading').length) {
$form.prepend('<div class="care-booking-loading" style="display: none;">Loading...</div>');
}
// Handle form submissions
$form.on('submit', (e) => {
this.showLoadingState($form);
});
// Handle AJAX requests
$(document).on('ajaxStart', () => {
if (this.isKiviCareAjax()) {
this.showLoadingState($form);
}
});
$(document).on('ajaxComplete', () => {
this.hideLoadingState($form);
});
});
},
/**
* Show loading state
* @param {jQuery} $element
*/
showLoadingState: function($element) {
$element.addClass('care-booking-loading');
$element.find('.care-booking-loading').show();
},
/**
* Hide loading state
* @param {jQuery} $element
*/
hideLoadingState: function($element) {
$element.removeClass('care-booking-loading');
$element.find('.care-booking-loading').hide();
},
/**
* Check if current AJAX request is KiviCare related
* @returns {boolean}
*/
isKiviCareAjax: function() {
// This is a simplified check - could be enhanced based on KiviCare's AJAX patterns
return window.location.href.indexOf('kivicare') !== -1 ||
document.body.className.indexOf('kivicare') !== -1;
},
/**
* Enhance form validation
*/
enhanceFormValidation: function() {
const $forms = $(this.config.selectors.forms);
$forms.each((index, form) => {
const $form = $(form);
$form.on('submit', (e) => {
if (!this.validateBookingForm($form)) {
e.preventDefault();
return false;
}
});
// Real-time validation for select fields
$form.find('select').on('change', (e) => {
this.validateSelectField($(e.target));
});
});
},
/**
* Validate booking form
* @param {jQuery} $form
* @returns {boolean}
*/
validateBookingForm: function($form) {
let isValid = true;
const requiredFields = $form.find('select[required], input[required]');
requiredFields.each((index, field) => {
const $field = $(field);
if (!$field.val() || $field.val() === '0' || $field.val() === '') {
isValid = false;
this.showFieldError($field, 'This field is required');
} else {
this.clearFieldError($field);
}
});
return isValid;
},
/**
* Validate individual select field
* @param {jQuery} $field
*/
validateSelectField: function($field) {
const value = $field.val();
if ($field.attr('required') && (!value || value === '0' || value === '')) {
this.showFieldError($field, 'Please make a selection');
} else {
this.clearFieldError($field);
}
},
/**
* Show field error
* @param {jQuery} $field
* @param {string} message
*/
showFieldError: function($field, message) {
$field.addClass('error');
let $error = $field.siblings('.field-error');
if (!$error.length) {
$error = $('<div class="field-error"></div>');
$field.after($error);
}
$error.text(message).show();
},
/**
* Clear field error
* @param {jQuery} $field
*/
clearFieldError: function($field) {
$field.removeClass('error');
$field.siblings('.field-error').hide();
},
/**
* Setup fallback elements for graceful degradation
*/
enhanceFallbackElements: function() {
// Add fallback classes to elements that might be blocked
$(this.config.selectors.doctors).each((index, element) => {
const $element = $(element);
if (!$element.hasClass('care-booking-fallback')) {
$element.addClass('care-booking-enhanced');
}
});
$(this.config.selectors.services).each((index, element) => {
const $element = $(element);
if (!$element.hasClass('care-booking-fallback')) {
$element.addClass('care-booking-enhanced');
}
});
},
/**
* Setup event listeners
*/
setupEventListeners: function() {
// Handle dynamic doctor selection
$(document).on('change', 'select[name="doctor_id"], .doctor-selection', (e) => {
this.handleDoctorChange($(e.target));
});
// Handle service selection
$(document).on('change', 'select[name="service_id"], .service-selection', (e) => {
this.handleServiceChange($(e.target));
});
// Handle retry buttons
$(document).on('click', '.care-booking-retry', (e) => {
e.preventDefault();
this.retryOperation($(e.target));
});
},
/**
* Handle doctor selection change
* @param {jQuery} $select
*/
handleDoctorChange: function($select) {
const doctorId = $select.val();
if (this.config.debug) {
console.log('Care Booking Block: Doctor changed to', doctorId);
}
// Clear service selection if doctor changed
const $serviceSelect = $select.closest('form').find('select[name="service_id"], .service-selection');
if ($serviceSelect.length) {
$serviceSelect.val('').trigger('change');
this.updateServiceOptions($serviceSelect, doctorId);
}
},
/**
* Handle service selection change
* @param {jQuery} $select
*/
handleServiceChange: function($select) {
const serviceId = $select.val();
if (this.config.debug) {
console.log('Care Booking Block: Service changed to', serviceId);
}
// Additional service-specific logic can be added here
},
/**
* Update service options based on selected doctor
* @param {jQuery} $serviceSelect
* @param {string} doctorId
*/
updateServiceOptions: function($serviceSelect, doctorId) {
if (!doctorId || doctorId === '0') {
return;
}
// This would typically make an AJAX request to get services
// For now, we'll rely on KiviCare's existing functionality
$serviceSelect.trigger('doctor_changed', [doctorId]);
},
/**
* Setup fallback mechanisms
*/
setupFallbacks: function() {
if (!this.config.fallbackEnabled) {
return;
}
// Setup automatic retry for failed operations
this.setupAutoRetry();
// Setup offline detection
this.setupOfflineDetection();
},
/**
* Setup automatic retry for failed operations
*/
setupAutoRetry: function() {
$(document).on('ajaxError', (event, jqXHR, ajaxSettings, thrownError) => {
if (this.isKiviCareAjax() && this.retryCount < this.config.retryAttempts) {
setTimeout(() => {
this.retryCount++;
if (this.config.debug) {
console.log('Care Booking Block: Retrying operation, attempt', this.retryCount);
}
// Retry the failed request
$.ajax(ajaxSettings);
}, this.config.retryDelay);
}
});
},
/**
* Setup offline detection
*/
setupOfflineDetection: function() {
$(window).on('online offline', (e) => {
const isOnline = e.type === 'online';
if (this.config.debug) {
console.log('Care Booking Block: Connection status changed to', isOnline ? 'online' : 'offline');
}
if (isOnline) {
// Retry any pending operations
this.retryPendingOperations();
} else {
// Show offline message
this.showOfflineMessage();
}
});
},
/**
* Retry pending operations when back online
*/
retryPendingOperations: function() {
// Implementation would depend on what operations need to be retried
if (this.config.debug) {
console.log('Care Booking Block: Retrying pending operations');
}
},
/**
* Show offline message
*/
showOfflineMessage: function() {
const message = '<div class="care-booking-offline-message">You appear to be offline. Some features may not work properly.</div>';
if (!$('.care-booking-offline-message').length) {
$('body').prepend(message);
setTimeout(() => {
$('.care-booking-offline-message').fadeOut();
}, 5000);
}
},
/**
* Retry a specific operation
* @param {jQuery} $button
*/
retryOperation: function($button) {
const $container = $button.closest('.care-booking-container');
this.showLoadingState($container);
// Simulate retry - in practice, this would repeat the failed operation
setTimeout(() => {
this.hideLoadingState($container);
$button.closest('.error-message').fadeOut();
}, 1000);
},
/**
* Cleanup resources
*/
destroy: function() {
// Remove observers
this.observers.forEach(observer => observer.disconnect());
this.observers = [];
// Remove event listeners
$(document).off('.careBooking');
this.initialized = false;
}
};
// Initialize when DOM is ready
$(document).ready(() => {
CareBooking.init();
});
// Handle page unload
$(window).on('beforeunload', () => {
CareBooking.destroy();
});
// Expose to global scope for debugging
if (config && config.debug) {
window.CareBooking = CareBooking;
}
})(jQuery, window.careBookingConfig);

File diff suppressed because one or more lines are too long