Compare commits
213 Commits
backup-est
...
61c67acca5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61c67acca5 | ||
|
|
ffe6ea8e65 | ||
|
|
36d5cf56de | ||
|
|
23339e3349 | ||
|
|
caa6413bc6 | ||
|
|
ea695010f3 | ||
|
|
e4c79d3f26 | ||
|
|
f4b45b7e17 | ||
|
|
c28fedd6e7 | ||
|
|
14138e7762 | ||
|
|
8735962f52 | ||
|
|
7fb5eda108 | ||
|
|
4cdc4db397 | ||
|
|
c732b5af05 | ||
|
|
29a69617e4 | ||
|
|
9e37ea93eb | ||
|
|
7472dbad11 | ||
|
|
ce66eeba6d | ||
|
|
565c275c16 | ||
|
|
faf5fc6db2 | ||
|
|
de66b77fe3 | ||
|
|
73e5ac4acd | ||
|
|
78ec902688 | ||
|
|
d8fa5cb609 | ||
|
|
e01605ec37 | ||
|
|
e1923b630d | ||
|
|
625d99d698 | ||
|
|
9f0ae9fcb6 | ||
|
|
647f177a35 | ||
|
|
49eff2223c | ||
|
|
c302c653c3 | ||
|
|
9cb0dd1491 | ||
|
|
423aae062c | ||
|
|
972c3c5de9 | ||
|
|
cc4de0eda7 | ||
|
|
80fc41afad | ||
|
|
0b34317cc6 | ||
|
|
0ea874876e | ||
|
|
fb74ccbdc2 | ||
|
|
9f5cc92ec6 | ||
|
|
c6450211a7 | ||
|
|
3c8e5982ba | ||
|
|
7667b7f02a | ||
|
|
c4dcdad14b | ||
|
|
d648e7ff4c | ||
|
|
842f529816 | ||
|
|
3b9a1cb299 | ||
|
|
c0172467b3 | ||
|
|
ee28baafd8 | ||
|
|
d145d4dfde | ||
|
|
8710895db5 | ||
|
|
163b8c6c2a | ||
|
|
0239191dfc | ||
|
|
3bf40787ad | ||
|
|
bc85854453 | ||
|
|
4e99fa5310 | ||
|
|
13e17a7b12 | ||
|
|
c7e8f14d83 | ||
|
|
0fba2d567c | ||
|
|
31d4a41fc9 | ||
|
|
9afdd6ee1d | ||
|
|
b4071bf598 | ||
|
|
62a0f17b21 | ||
|
|
5d4523e49a | ||
|
|
19b6c38fbf | ||
|
|
8a9c62e17e | ||
|
|
b7ae8cac21 | ||
|
|
371af1f7e5 | ||
|
|
a01ebf303e | ||
|
|
8361e14862 | ||
|
|
77a59d0db8 | ||
|
|
6004420620 | ||
|
|
d5a2fd2702 | ||
|
|
ce0179a134 | ||
|
|
38d7099bcd | ||
|
|
4f25297f14 | ||
|
|
6d03076032 | ||
|
|
f5089724c6 | ||
|
|
956819cf14 | ||
|
|
46ad8340c3 | ||
|
|
4294a7c07b | ||
|
|
8aba07fdbf | ||
|
|
13beaf7b06 | ||
|
|
1f0ce58b22 | ||
|
|
7edddada89 | ||
|
|
b96a13427e | ||
|
|
4d5cc1a58c | ||
|
|
e3d17db5ea | ||
|
|
a281448bf8 | ||
|
|
8c3fea964d | ||
|
|
cec8b8dccd | ||
|
|
e8ead33311 | ||
|
|
9e8ffdb26f | ||
|
|
ec64ea38ea | ||
|
|
e7fc0f1408 | ||
|
|
f4e3a61df8 | ||
|
|
961f663107 | ||
|
|
21ac98c969 | ||
|
|
de4f808a1a | ||
|
|
c9c6a5ac7b | ||
|
|
6b6ebd3c6d | ||
|
|
070ee7398c | ||
|
|
ce19345f78 | ||
|
|
1b9910165b | ||
|
|
6e2ef67dc4 | ||
|
|
72ef7580fc | ||
|
|
122bcd4750 | ||
|
|
0dfe3fcd2c | ||
|
|
2fa112ab7f | ||
|
|
55f061df67 | ||
|
|
1a069a1336 | ||
|
|
cfcc38c0f7 | ||
|
|
c564ee7a2a | ||
|
|
4119f2e86d | ||
|
|
e52df682ae | ||
|
|
58a4cc2c56 | ||
|
|
b9b21c390a | ||
|
|
22e9273b4f | ||
|
|
79b48ad94f | ||
|
|
82abdf047a | ||
|
|
b70e11be62 | ||
|
|
3279b7df2b | ||
|
|
a3fa5fe22e | ||
|
|
84441af9c0 | ||
|
|
651e8124d4 | ||
|
|
a10831e2c2 | ||
|
|
d7c42f26ef | ||
|
|
4dbf73f226 | ||
|
|
f4bd013271 | ||
|
|
371995d151 | ||
|
|
1c901ecdf9 | ||
|
|
281c05fa33 | ||
|
|
0a303be198 | ||
|
|
6edb2ebeaa | ||
|
|
4ad48b4326 | ||
|
|
23a3c4d074 | ||
|
|
83717771c0 | ||
|
|
d7915d372b | ||
|
|
2acce34d9e | ||
|
|
99cde7c3d6 | ||
|
|
50a8c2bf18 | ||
|
|
096f9716ef | ||
|
|
2f19a7c077 | ||
|
|
cd09666f1d | ||
|
|
b43cb22dc1 | ||
|
|
deef577c36 | ||
|
|
d5bdb81cbe | ||
|
|
56a7c29653 | ||
|
|
acdfeffd75 | ||
|
|
eeacfdb284 | ||
|
|
d867212790 | ||
|
|
98c90756f8 | ||
|
|
52e2698279 | ||
|
|
83b594a750 | ||
|
|
4ac03bd3e2 | ||
|
|
ec8f1f0589 | ||
|
|
133b364c78 | ||
|
|
b0def25348 | ||
|
|
7e13678e0b | ||
|
|
7a539a498f | ||
|
|
90ac8a16cc | ||
|
|
8f4e854a20 | ||
|
|
a062529e82 | ||
|
|
7a34d1f2ae | ||
|
|
a46126e015 | ||
|
|
1876231ac1 | ||
|
|
c6e156089d | ||
|
|
32d76c4ce8 | ||
|
|
2831cabec9 | ||
|
|
4cbde7e1b7 | ||
|
|
14e68031ac | ||
|
|
1a03205aba | ||
|
|
620ca115fb | ||
|
|
af16230cf9 | ||
|
|
0f947f6677 | ||
|
|
33d17f4b56 | ||
|
|
90863cd8f5 | ||
|
|
a2548ab5c2 | ||
|
|
d6070099d1 | ||
|
|
8a49b19d00 | ||
|
|
9d14f38965 | ||
|
|
f35b60ed4e | ||
|
|
7cc5f194e9 | ||
|
|
6dc052afa6 | ||
|
|
8878afe168 | ||
|
|
7a8daa72c6 | ||
|
|
f52a395e0d | ||
|
|
6e75527157 | ||
|
|
4f11c2c312 | ||
|
|
1a4d9d8c08 | ||
|
|
71cfd54166 | ||
|
|
4c807e1cf2 | ||
|
|
0846a3bf03 | ||
|
|
90de6df77c | ||
|
|
677fbd4368 | ||
|
|
42edfab50d | ||
|
|
e34fd28df7 | ||
|
|
de5fff4f5c | ||
|
|
b782ebceee | ||
|
|
a6578f4973 | ||
|
|
77dd809e8c | ||
|
|
60b3992ca5 | ||
|
|
49b923230f | ||
|
|
9e29410c0d | ||
|
|
f0989f4fb0 | ||
|
|
3947e36c98 | ||
|
|
03c97d31d3 | ||
|
|
e94b274ed0 | ||
|
|
883853bc5c | ||
|
|
1c6b184e94 | ||
|
|
8a99f184bf | ||
|
|
3ad2413e7a | ||
|
|
4818d90386 |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -40,8 +40,10 @@ Desktop.ini
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
|
||||
# Composer (si hay dependencias PHP)
|
||||
vendor/
|
||||
|
||||
# PHPUnit
|
||||
.phpunit.result.cache
|
||||
/tests/_output/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
@@ -65,7 +67,11 @@ vendor/
|
||||
# Planning and documentation
|
||||
_planeacion/
|
||||
|
||||
# Testing infrastructure (composer, phpunit, phpcs configs and dependencies)
|
||||
_testing-suite/
|
||||
|
||||
# Claude Code tools
|
||||
.playwright-mcp/
|
||||
.serena/
|
||||
.claude/
|
||||
nul
|
||||
|
||||
18
404.php
18
404.php
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#404-not-found
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -23,7 +23,7 @@ get_header();
|
||||
<!-- Error Header -->
|
||||
<header class="page-header">
|
||||
<h1 id="error-404-title" class="page-title">
|
||||
<?php esc_html_e( '404 - Page Not Found', 'apus-theme' ); ?>
|
||||
<?php esc_html_e( '404 - Page Not Found', 'roi-theme' ); ?>
|
||||
</h1>
|
||||
</header><!-- .page-header -->
|
||||
|
||||
@@ -31,25 +31,25 @@ get_header();
|
||||
<div class="page-content">
|
||||
|
||||
<p class="error-message">
|
||||
<?php esc_html_e( 'Oops! The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.', 'apus-theme' ); ?>
|
||||
<?php esc_html_e( 'Oops! The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.', 'roi-theme' ); ?>
|
||||
</p>
|
||||
|
||||
<!-- Helpful Actions -->
|
||||
<div class="error-actions">
|
||||
|
||||
<h2><?php esc_html_e( 'What can you do?', 'apus-theme' ); ?></h2>
|
||||
<h2><?php esc_html_e( 'What can you do?', 'roi-theme' ); ?></h2>
|
||||
|
||||
<ul class="error-suggestions" role="list">
|
||||
<li>
|
||||
<a href="<?php echo esc_url( home_url( '/' ) ); ?>">
|
||||
<?php esc_html_e( 'Go to the homepage', 'apus-theme' ); ?>
|
||||
<?php esc_html_e( 'Go to the homepage', 'roi-theme' ); ?>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<?php esc_html_e( 'Check the URL for typos', 'apus-theme' ); ?>
|
||||
<?php esc_html_e( 'Check the URL for typos', 'roi-theme' ); ?>
|
||||
</li>
|
||||
<li>
|
||||
<?php esc_html_e( 'Use the navigation menu above', 'apus-theme' ); ?>
|
||||
<?php esc_html_e( 'Use the navigation menu above', 'roi-theme' ); ?>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -65,7 +65,7 @@ get_header();
|
||||
if ( ! empty( $recent_posts ) ) :
|
||||
?>
|
||||
<div class="recent-posts-section">
|
||||
<h3><?php esc_html_e( 'Recent Posts', 'apus-theme' ); ?></h3>
|
||||
<h3><?php esc_html_e( 'Recent Posts', 'roi-theme' ); ?></h3>
|
||||
<ul class="recent-posts-list" role="list">
|
||||
<?php foreach ( $recent_posts as $recent ) : ?>
|
||||
<li>
|
||||
@@ -95,7 +95,7 @@ get_header();
|
||||
if ( ! empty( $categories ) ) :
|
||||
?>
|
||||
<div class="categories-section">
|
||||
<h3><?php esc_html_e( 'Browse by Category', 'apus-theme' ); ?></h3>
|
||||
<h3><?php esc_html_e( 'Browse by Category', 'roi-theme' ); ?></h3>
|
||||
<ul class="categories-list" role="list">
|
||||
<?php foreach ( $categories as $category ) : ?>
|
||||
<li>
|
||||
|
||||
0
Admin/.gitkeep
Normal file
0
Admin/.gitkeep
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\AdsensePlacement\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
final class AdsensePlacementFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'adsense-placement';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// VISIBILITY
|
||||
'adsense-placementEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'adsense-placementShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'adsense-placementShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'adsense-placementHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// ANALYTICS (Google Analytics)
|
||||
'adsense-placementAnalyticsEnabled' => ['group' => 'analytics', 'attribute' => 'analytics_enabled'],
|
||||
'adsense-placementGaTrackingId' => ['group' => 'analytics', 'attribute' => 'ga_tracking_id'],
|
||||
'adsense-placementGaAnonymizeIp' => ['group' => 'analytics', 'attribute' => 'ga_anonymize_ip'],
|
||||
|
||||
// CONTENT (Credentials)
|
||||
'adsense-placementPublisherId' => ['group' => 'content', 'attribute' => 'publisher_id'],
|
||||
'adsense-placementSlotDisplay' => ['group' => 'content', 'attribute' => 'slot_display'],
|
||||
'adsense-placementSlotAuto' => ['group' => 'content', 'attribute' => 'slot_auto'],
|
||||
'adsense-placementSlotAutorelaxed' => ['group' => 'content', 'attribute' => 'slot_autorelaxed'],
|
||||
'adsense-placementSlotInarticle' => ['group' => 'content', 'attribute' => 'slot_inarticle'],
|
||||
'adsense-placementSlotSkyscraper' => ['group' => 'content', 'attribute' => 'slot_skyscraper'],
|
||||
|
||||
// BEHAVIOR (Post locations + formats)
|
||||
'adsense-placementPostTopEnabled' => ['group' => 'behavior', 'attribute' => 'post_top_enabled'],
|
||||
'adsense-placementPostTopFormat' => ['group' => 'behavior', 'attribute' => 'post_top_format'],
|
||||
'adsense-placementPostContentEnabled' => ['group' => 'behavior', 'attribute' => 'post_content_enabled'],
|
||||
'adsense-placementPostContentRandomMode' => ['group' => 'behavior', 'attribute' => 'post_content_random_mode'],
|
||||
'adsense-placementPostContentMinAds' => ['group' => 'behavior', 'attribute' => 'post_content_min_ads'],
|
||||
'adsense-placementPostContentMaxAds' => ['group' => 'behavior', 'attribute' => 'post_content_max_ads'],
|
||||
'adsense-placementPostContentAfterParagraphs' => ['group' => 'behavior', 'attribute' => 'post_content_after_paragraphs'],
|
||||
'adsense-placementPostContentMinParagraphsBetween' => ['group' => 'behavior', 'attribute' => 'post_content_min_paragraphs_between'],
|
||||
'adsense-placementPostContentFormat' => ['group' => 'behavior', 'attribute' => 'post_content_format'],
|
||||
'adsense-placementPostBottomEnabled' => ['group' => 'behavior', 'attribute' => 'post_bottom_enabled'],
|
||||
'adsense-placementPostBottomFormat' => ['group' => 'behavior', 'attribute' => 'post_bottom_format'],
|
||||
'adsense-placementAfterRelatedEnabled' => ['group' => 'behavior', 'attribute' => 'after_related_enabled'],
|
||||
'adsense-placementAfterRelatedFormat' => ['group' => 'behavior', 'attribute' => 'after_related_format'],
|
||||
|
||||
// BEHAVIOR (Rail Ads)
|
||||
'adsense-placementRailAdsEnabled' => ['group' => 'behavior', 'attribute' => 'rail_ads_enabled'],
|
||||
'adsense-placementRailLeftEnabled' => ['group' => 'behavior', 'attribute' => 'rail_left_enabled'],
|
||||
'adsense-placementRailRightEnabled' => ['group' => 'behavior', 'attribute' => 'rail_right_enabled'],
|
||||
'adsense-placementRailFormat' => ['group' => 'behavior', 'attribute' => 'rail_format'],
|
||||
'adsense-placementRailTopOffset' => ['group' => 'behavior', 'attribute' => 'rail_top_offset'],
|
||||
|
||||
// LAYOUT (Archive/Global locations + formats)
|
||||
'adsense-placementArchiveTopEnabled' => ['group' => 'layout', 'attribute' => 'archive_top_enabled'],
|
||||
'adsense-placementArchiveBetweenEnabled' => ['group' => 'layout', 'attribute' => 'archive_between_enabled'],
|
||||
'adsense-placementArchiveBetweenEvery' => ['group' => 'layout', 'attribute' => 'archive_between_every'],
|
||||
'adsense-placementArchiveBottomEnabled' => ['group' => 'layout', 'attribute' => 'archive_bottom_enabled'],
|
||||
'adsense-placementArchiveFormat' => ['group' => 'layout', 'attribute' => 'archive_format'],
|
||||
'adsense-placementHeaderBelowEnabled' => ['group' => 'layout', 'attribute' => 'header_below_enabled'],
|
||||
'adsense-placementFooterAboveEnabled' => ['group' => 'layout', 'attribute' => 'footer_above_enabled'],
|
||||
'adsense-placementGlobalFormat' => ['group' => 'layout', 'attribute' => 'global_format'],
|
||||
|
||||
// FORMS (Exclusions + Delay)
|
||||
'adsense-placementExcludeCategories' => ['group' => 'forms', 'attribute' => 'exclude_categories'],
|
||||
'adsense-placementExcludePostTypes' => ['group' => 'forms', 'attribute' => 'exclude_post_types'],
|
||||
'adsense-placementExcludePostIds' => ['group' => 'forms', 'attribute' => 'exclude_post_ids'],
|
||||
'adsense-placementMinContentLength' => ['group' => 'forms', 'attribute' => 'min_content_length'],
|
||||
'adsense-placementDelayEnabled' => ['group' => 'forms', 'attribute' => 'delay_enabled'],
|
||||
'adsense-placementDelayTimeout' => ['group' => 'forms', 'attribute' => 'delay_timeout'],
|
||||
|
||||
// ANCHOR ADS
|
||||
'adsense-placementAnchorEnabled' => ['group' => 'anchor_ads', 'attribute' => 'anchor_enabled'],
|
||||
'adsense-placementAnchorPosition' => ['group' => 'anchor_ads', 'attribute' => 'anchor_position'],
|
||||
'adsense-placementAnchorHeight' => ['group' => 'anchor_ads', 'attribute' => 'anchor_height'],
|
||||
'adsense-placementAnchorCollapsibleEnabled' => ['group' => 'anchor_ads', 'attribute' => 'anchor_collapsible_enabled'],
|
||||
'adsense-placementAnchorShowOnMobile' => ['group' => 'anchor_ads', 'attribute' => 'anchor_show_on_mobile'],
|
||||
'adsense-placementAnchorShowOnWideScreens' => ['group' => 'anchor_ads', 'attribute' => 'anchor_show_on_wide_screens'],
|
||||
'adsense-placementAnchorRememberState' => ['group' => 'anchor_ads', 'attribute' => 'anchor_remember_state'],
|
||||
'adsense-placementAnchorRememberDuration' => ['group' => 'anchor_ads', 'attribute' => 'anchor_remember_duration'],
|
||||
|
||||
// VIGNETTE ADS
|
||||
'adsense-placementVignetteEnabled' => ['group' => 'vignette_ads', 'attribute' => 'vignette_enabled'],
|
||||
'adsense-placementVignetteTrigger' => ['group' => 'vignette_ads', 'attribute' => 'vignette_trigger'],
|
||||
'adsense-placementVignetteTriggerDelay' => ['group' => 'vignette_ads', 'attribute' => 'vignette_trigger_delay'],
|
||||
'adsense-placementVignetteSize' => ['group' => 'vignette_ads', 'attribute' => 'vignette_size'],
|
||||
'adsense-placementVignetteOverlayOpacity' => ['group' => 'vignette_ads', 'attribute' => 'vignette_overlay_opacity'],
|
||||
'adsense-placementVignetteShowOnMobile' => ['group' => 'vignette_ads', 'attribute' => 'vignette_show_on_mobile'],
|
||||
'adsense-placementVignetteShowOnDesktop' => ['group' => 'vignette_ads', 'attribute' => 'vignette_show_on_desktop'],
|
||||
'adsense-placementVignetteReshowEnabled' => ['group' => 'vignette_ads', 'attribute' => 'vignette_reshow_enabled'],
|
||||
'adsense-placementVignetteReshowTime' => ['group' => 'vignette_ads', 'attribute' => 'vignette_reshow_time'],
|
||||
'adsense-placementVignetteMaxPerSession' => ['group' => 'vignette_ads', 'attribute' => 'vignette_max_per_session'],
|
||||
|
||||
// SEARCH RESULTS (ROI APU Search)
|
||||
'adsense-placementSearchAdsEnabled' => ['group' => 'search_results', 'attribute' => 'search_ads_enabled'],
|
||||
'adsense-placementSearchTopAdEnabled' => ['group' => 'search_results', 'attribute' => 'search_top_ad_enabled'],
|
||||
'adsense-placementSearchTopAdFormat' => ['group' => 'search_results', 'attribute' => 'search_top_ad_format'],
|
||||
'adsense-placementSearchBetweenEnabled' => ['group' => 'search_results', 'attribute' => 'search_between_enabled'],
|
||||
'adsense-placementSearchBetweenMax' => ['group' => 'search_results', 'attribute' => 'search_between_max'],
|
||||
'adsense-placementSearchBetweenFormat' => ['group' => 'search_results', 'attribute' => 'search_between_format'],
|
||||
'adsense-placementSearchBetweenPosition' => ['group' => 'search_results', 'attribute' => 'search_between_position'],
|
||||
'adsense-placementSearchBetweenEvery' => ['group' => 'search_results', 'attribute' => 'search_between_every'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'adsense-placementVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'adsense-placementVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'adsense-placementVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'adsense-placementVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'adsense-placementVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'adsense-placementExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'adsense-placementExcludeCategoriesAdv' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'adsense-placementExcludePostIdsAdv' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'adsense-placementExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,985 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\AdsensePlacement\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para AdSense Placement y Google Analytics
|
||||
*
|
||||
* Panel reorganizado con:
|
||||
* - Diagrama visual de ubicaciones
|
||||
* - Secciones colapsables
|
||||
* - In-content ads configurables (1-8 random)
|
||||
*/
|
||||
final class AdsensePlacementFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
// HEADER CON GRADIENTE
|
||||
$html .= '<div class="rounded p-4 mb-4 shadow text-white" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-megaphone me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' AdSense y Analytics';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Configura Google AdSense y Analytics con ubicaciones visuales';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// LAYOUT 2 COLUMNAS
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// COLUMNA IZQUIERDA (7 cols)
|
||||
$html .= ' <div class="col-lg-7">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildDiagramSection();
|
||||
$html .= $this->buildPostLocationsGroup($componentId);
|
||||
$html .= $this->buildInContentAdsGroup($componentId);
|
||||
$html .= $this->buildExclusionsGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
|
||||
// COLUMNA DERECHA (5 cols)
|
||||
$html .= ' <div class="col-lg-5">';
|
||||
$html .= $this->buildCredentialsGroup($componentId);
|
||||
$html .= $this->buildAnalyticsGroup($componentId);
|
||||
$html .= $this->buildRailAdsGroup($componentId);
|
||||
$html .= $this->buildAnchorAdsGroup($componentId);
|
||||
$html .= $this->buildVignetteAdsGroup($componentId);
|
||||
$html .= $this->buildSearchResultsGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #28a745;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-power me-2" style="color: #28a745;"></i>';
|
||||
$html .= ' Activacion Global';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$enabled = $this->renderer->getFieldValue($cid, 'visibility', 'is_enabled', false);
|
||||
$html .= $this->buildSwitch($cid . 'Enabled', 'Activar AdSense', $enabled, 'bi-power');
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$showMobile = $this->renderer->getFieldValue($cid, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch($cid . 'ShowOnMobile', 'Mostrar en movil', $showMobile, 'bi-phone');
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$showDesktop = $this->renderer->getFieldValue($cid, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch($cid . 'ShowOnDesktop', 'Mostrar en escritorio', $showDesktop, 'bi-display');
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Opcion para ocultar anuncios a usuarios logueados
|
||||
$html .= '<div class="mt-3 p-2 rounded" style="background: #fff3cd;">';
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($cid, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= $this->buildSwitch($cid . 'HideForLoggedIn', 'Ocultar para usuarios logueados', $hideForLoggedIn, 'bi-person-lock');
|
||||
$html .= '<small class="text-muted d-block" style="margin-top: -8px; margin-left: 40px;">No mostrar anuncios a usuarios con sesion iniciada en WordPress</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
// =============================================
|
||||
// Visibilidad por tipo de pagina
|
||||
// Grupo especial: _page_visibility (Plan 99.11)
|
||||
// =============================================
|
||||
$html .= '<hr class="my-3">';
|
||||
$html .= '<p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-layout-text-window me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= '</p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($cid, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($cid, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($cid, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($cid, '_page_visibility', 'show_on_archives', true);
|
||||
$showOnSearch = $this->renderer->getFieldValue($cid, '_page_visibility', 'show_on_search', true);
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox($cid . 'VisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox($cid . 'VisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox($cid . 'VisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox($cid . 'VisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox($cid . 'VisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($cid, $cid);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagrama visual de ubicaciones de anuncios
|
||||
*/
|
||||
private function buildDiagramSection(): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #6f42c1;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-layout-text-window-reverse me-2" style="color: #6f42c1;"></i>';
|
||||
$html .= ' Mapa de Ubicaciones';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Diagrama visual del layout
|
||||
$html .= '<div class="border rounded p-3" style="background: #f8f9fa; font-family: monospace; font-size: 11px;">';
|
||||
|
||||
// Anchor Top
|
||||
$html .= '<div class="text-center p-2 mb-1 rounded" style="background: #d1ecf1; border: 2px solid #17a2b8;">';
|
||||
$html .= ' <i class="bi bi-pin-angle"></i> <strong>ANCHOR TOP</strong> (fijo, collapsible)';
|
||||
$html .= '</div>';
|
||||
|
||||
// Header
|
||||
$html .= '<div class="text-center p-2 mb-1 rounded" style="background: #e9ecef; border: 1px dashed #6c757d;">';
|
||||
$html .= ' <strong>HEADER</strong>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Hero / Featured Image
|
||||
$html .= '<div class="text-center p-2 mb-1 rounded" style="background: #d1e7dd; border: 1px solid #198754;">';
|
||||
$html .= ' <i class="bi bi-image"></i> Featured Image / Hero';
|
||||
$html .= '</div>';
|
||||
|
||||
// Ad: Post Top
|
||||
$html .= '<div class="text-center p-2 mb-1 rounded" style="background: #fff3cd; border: 2px solid #ffc107;">';
|
||||
$html .= ' <i class="bi bi-megaphone"></i> <strong>📍 POST-TOP</strong> (Despues de imagen)';
|
||||
$html .= '</div>';
|
||||
|
||||
// Content container
|
||||
$html .= '<div class="p-2 mb-1 rounded" style="background: #fff; border: 1px solid #dee2e6;">';
|
||||
$html .= ' <div class="mb-1 small text-muted text-center">📝 CONTENIDO DEL POST</div>';
|
||||
$html .= ' <div class="p-1 rounded mb-1" style="background: #e7f1ff; font-size: 10px;">Parrafo 1...</div>';
|
||||
$html .= ' <div class="p-1 rounded mb-1" style="background: #e7f1ff; font-size: 10px;">Parrafo 2...</div>';
|
||||
$html .= ' <div class="p-1 rounded mb-1" style="background: #e7f1ff; font-size: 10px;">Parrafo 3...</div>';
|
||||
|
||||
// In-content ad
|
||||
$html .= ' <div class="text-center p-1 mb-1 rounded" style="background: #fff3cd; border: 2px dashed #ffc107; font-size: 10px;">';
|
||||
$html .= ' <i class="bi bi-megaphone"></i> <strong>📍 IN-CONTENT #1</strong>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="p-1 rounded mb-1" style="background: #e7f1ff; font-size: 10px;">Parrafo 4...</div>';
|
||||
$html .= ' <div class="p-1 rounded mb-1" style="background: #e7f1ff; font-size: 10px;">Parrafo 5...</div>';
|
||||
$html .= ' <div class="p-1 rounded mb-1" style="background: #e7f1ff; font-size: 10px;">Parrafo 6...</div>';
|
||||
|
||||
// In-content ad 2
|
||||
$html .= ' <div class="text-center p-1 mb-1 rounded" style="background: #fff3cd; border: 2px dashed #ffc107; font-size: 10px;">';
|
||||
$html .= ' <i class="bi bi-megaphone"></i> <strong>📍 IN-CONTENT #2</strong> (random)';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="p-1 rounded" style="background: #e7f1ff; font-size: 10px;">Mas parrafos...</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Ad: Post Bottom
|
||||
$html .= '<div class="text-center p-2 mb-1 rounded" style="background: #fff3cd; border: 2px solid #ffc107;">';
|
||||
$html .= ' <i class="bi bi-megaphone"></i> <strong>📍 POST-BOTTOM</strong> (Despues del contenido)';
|
||||
$html .= '</div>';
|
||||
|
||||
// Related Posts
|
||||
$html .= '<div class="text-center p-2 mb-1 rounded" style="background: #cfe2ff; border: 1px solid #0d6efd;">';
|
||||
$html .= ' <i class="bi bi-grid-3x2"></i> Related Posts';
|
||||
$html .= '</div>';
|
||||
|
||||
// Ad: After Related
|
||||
$html .= '<div class="text-center p-2 mb-1 rounded" style="background: #fff3cd; border: 2px solid #ffc107;">';
|
||||
$html .= ' <i class="bi bi-megaphone"></i> <strong>📍 AFTER-RELATED</strong>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Footer
|
||||
$html .= '<div class="text-center p-2 mb-1 rounded" style="background: #e9ecef; border: 1px dashed #6c757d;">';
|
||||
$html .= ' <strong>FOOTER</strong>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Anchor Bottom
|
||||
$html .= '<div class="text-center p-2 rounded" style="background: #d1ecf1; border: 2px solid #17a2b8;">';
|
||||
$html .= ' <i class="bi bi-pin-angle"></i> <strong>ANCHOR BOTTOM</strong> (fijo, collapsible)';
|
||||
$html .= '</div>';
|
||||
|
||||
// Rail Ads (laterales)
|
||||
$html .= '<div class="mt-2 d-flex justify-content-between">';
|
||||
$html .= ' <div class="p-2 rounded text-center" style="width: 45%; background: #f8d7da; border: 2px solid #dc3545; font-size: 10px;">';
|
||||
$html .= ' <strong>📍 RAIL IZQ</strong><br><small>(160x600)</small>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="p-2 rounded text-center" style="width: 45%; background: #f8d7da; border: 2px solid #dc3545; font-size: 10px;">';
|
||||
$html .= ' <strong>📍 RAIL DER</strong><br><small>(160x600)</small>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Vignette Ad (modal)
|
||||
$html .= '<div class="mt-2 p-2 rounded text-center" style="background: #f3e5f5; border: 2px solid #9c27b0;">';
|
||||
$html .= ' <i class="bi bi-fullscreen"></i> <strong>VIGNETTE</strong> (modal pantalla completa)';
|
||||
$html .= ' <br><small class="text-muted">Aparece segun trigger configurado</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="mt-2 small text-muted">';
|
||||
$html .= ' <i class="bi bi-info-circle"></i> <span class="badge bg-warning text-dark">Amarillo</span> = Posts, ';
|
||||
$html .= ' <span class="badge bg-danger">Rojo</span> = Rails >1600px, ';
|
||||
$html .= ' <span class="badge" style="background:#17a2b8;color:white;">Cyan</span> = Anchors, ';
|
||||
$html .= ' <span class="badge" style="background:#9c27b0;color:white;">Morado</span> = Vignette';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPostLocationsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #ffc107;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-geo-alt me-2" style="color: #ffc107;"></i>';
|
||||
$html .= ' Ubicaciones en Posts';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// === POST-TOP ===
|
||||
$html .= '<div class="border rounded p-3 mb-3" style="background: #fffbeb;">';
|
||||
$html .= '<div class="d-flex align-items-center gap-2 mb-2">';
|
||||
$html .= ' <span class="badge bg-warning text-dark">POST-TOP</span>';
|
||||
$html .= ' <small class="text-muted">Despues de la imagen destacada</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$postTopEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'post_top_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'PostTopEnabled', 'Activar', $postTopEnabled);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$html .= $this->buildSelect($cid . 'PostTopFormat', 'Formato',
|
||||
$this->renderer->getFieldValue($cid, 'behavior', 'post_top_format', 'auto'),
|
||||
[
|
||||
'auto' => 'Auto (responsive)',
|
||||
'in-article' => 'In-Article (fluid)',
|
||||
'display' => 'Display (728x90)',
|
||||
'display-large' => 'Display Large (970x250)'
|
||||
]
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// === POST-BOTTOM ===
|
||||
$html .= '<div class="border rounded p-3 mb-3" style="background: #fffbeb;">';
|
||||
$html .= '<div class="d-flex align-items-center gap-2 mb-2">';
|
||||
$html .= ' <span class="badge bg-warning text-dark">POST-BOTTOM</span>';
|
||||
$html .= ' <small class="text-muted">Despues del contenido, antes de Related</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$postBottomEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'post_bottom_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'PostBottomEnabled', 'Activar', $postBottomEnabled);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$html .= $this->buildSelect($cid . 'PostBottomFormat', 'Formato',
|
||||
$this->renderer->getFieldValue($cid, 'behavior', 'post_bottom_format', 'auto'),
|
||||
['auto' => 'Auto', 'in-article' => 'In-Article', 'display' => 'Display']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// === AFTER-RELATED ===
|
||||
$html .= '<div class="border rounded p-3" style="background: #fffbeb;">';
|
||||
$html .= '<div class="d-flex align-items-center gap-2 mb-2">';
|
||||
$html .= ' <span class="badge bg-warning text-dark">AFTER-RELATED</span>';
|
||||
$html .= ' <small class="text-muted">Despues de Related Posts</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$afterRelatedEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'after_related_enabled', false);
|
||||
$html .= $this->buildSwitch($cid . 'AfterRelatedEnabled', 'Activar', $afterRelatedEnabled);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$html .= $this->buildSelect($cid . 'AfterRelatedFormat', 'Formato',
|
||||
$this->renderer->getFieldValue($cid, 'behavior', 'after_related_format', 'autorelaxed'),
|
||||
['autorelaxed' => 'Autorelaxed (feed)', 'auto' => 'Auto']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seccion especial para in-content ads con configuracion de 1-8 random
|
||||
*/
|
||||
private function buildInContentAdsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #0d6efd;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-body-text me-2" style="color: #0d6efd;"></i>';
|
||||
$html .= ' Anuncios Dentro del Contenido';
|
||||
$html .= ' <span class="badge bg-primary ms-2">1-8 ads</span>';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= '<div class="alert alert-info small mb-3">';
|
||||
$html .= ' <i class="bi bi-lightbulb me-1"></i>';
|
||||
$html .= ' <strong>Modo Random:</strong> Inserta entre 1 y 8 anuncios en posiciones aleatorias entre parrafos.';
|
||||
$html .= ' Mejor UX al variar la posicion en cada visita.';
|
||||
$html .= '</div>';
|
||||
|
||||
// Master switch
|
||||
$postContentEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_enabled', false);
|
||||
$html .= '<div class="mb-3">';
|
||||
$html .= $this->buildSwitch($cid . 'PostContentEnabled', 'Activar In-Content Ads', $postContentEnabled, 'bi-power');
|
||||
$html .= '</div>';
|
||||
|
||||
// Configuracion de cantidad
|
||||
$html .= '<div class="row g-2 mb-3">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$minAdsValue = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_min_ads', '1');
|
||||
$html .= $this->buildSelect($cid . 'PostContentMinAds', 'Minimo de anuncios',
|
||||
is_string($minAdsValue) ? $minAdsValue : '1',
|
||||
['1' => '1 anuncio', '2' => '2 anuncios', '3' => '3 anuncios', '4' => '4 anuncios']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$maxAdsValue = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_max_ads', '3');
|
||||
$html .= $this->buildSelect($cid . 'PostContentMaxAds', 'Maximo de anuncios',
|
||||
is_string($maxAdsValue) ? $maxAdsValue : '3',
|
||||
[
|
||||
'1' => '1 anuncio', '2' => '2 anuncios', '3' => '3 anuncios', '4' => '4 anuncios',
|
||||
'5' => '5 anuncios', '6' => '6 anuncios', '7' => '7 anuncios', '8' => '8 anuncios'
|
||||
]
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Configuracion de posicionamiento
|
||||
$html .= '<div class="row g-2 mb-3">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$afterPara = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_after_paragraphs', '3');
|
||||
$html .= $this->buildTextInput($cid . 'PostContentAfterParagraphs', 'Primer ad despues del parrafo #', (string)$afterPara, '3');
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$minBetweenValue = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_min_paragraphs_between', '4');
|
||||
$html .= $this->buildSelect($cid . 'PostContentMinParagraphsBetween', 'Parrafos entre ads',
|
||||
is_string($minBetweenValue) ? $minBetweenValue : '4',
|
||||
['2' => '2 parrafos', '3' => '3 parrafos', '4' => '4 parrafos', '5' => '5 parrafos', '6' => '6 parrafos']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Modo y formato
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$randomMode = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_random_mode', true);
|
||||
$html .= $this->buildSwitch($cid . 'PostContentRandomMode', 'Posiciones aleatorias', $randomMode, 'bi-shuffle');
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$formatValue = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_format', 'in-article');
|
||||
$html .= $this->buildSelect($cid . 'PostContentFormat', 'Formato de ads',
|
||||
is_string($formatValue) ? $formatValue : 'in-article',
|
||||
['in-article' => 'In-Article (fluid)', 'auto' => 'Auto (responsive)']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildCredentialsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-key me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Credenciales AdSense';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Publisher ID
|
||||
$pubId = $this->renderer->getFieldValue($cid, 'content', 'publisher_id', 'ca-pub-8476420265998726');
|
||||
$html .= $this->buildTextInput($cid . 'PublisherId', 'Publisher ID', $pubId, 'ca-pub-XXXXX');
|
||||
|
||||
$html .= '<hr class="my-3">';
|
||||
$html .= '<p class="small text-muted mb-2"><i class="bi bi-info-circle me-1"></i> Slots por tipo de anuncio:</p>';
|
||||
|
||||
// Slots con descripciones claras
|
||||
$html .= '<div class="mb-2">';
|
||||
$slotAuto = $this->renderer->getFieldValue($cid, 'content', 'slot_auto', '8471732096');
|
||||
$html .= $this->buildTextInput($cid . 'SlotAuto', '📱 Auto (responsive)', $slotAuto);
|
||||
$html .= '<div class="form-text small" style="margin-top:-10px;">Para: Post-Top, Post-Bottom, globales</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="mb-2">';
|
||||
$slotInArticle = $this->renderer->getFieldValue($cid, 'content', 'slot_inarticle', '7285187368');
|
||||
$html .= $this->buildTextInput($cid . 'SlotInarticle', '📝 In-Article (fluid)', $slotInArticle);
|
||||
$html .= '<div class="form-text small" style="margin-top:-10px;">Para: In-Content (dentro del texto)</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="mb-2">';
|
||||
$slotDisplay = $this->renderer->getFieldValue($cid, 'content', 'slot_display', '2873062302');
|
||||
$html .= $this->buildTextInput($cid . 'SlotDisplay', '🖥️ Display (fijo)', $slotDisplay);
|
||||
$html .= '<div class="form-text small" style="margin-top:-10px;">Para: 728x90, 970x250 (opcional)</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="mb-2">';
|
||||
$slotRelaxed = $this->renderer->getFieldValue($cid, 'content', 'slot_autorelaxed', '9205569855');
|
||||
$html .= $this->buildTextInput($cid . 'SlotAutorelaxed', '📋 Autorelaxed (feed)', $slotRelaxed);
|
||||
$html .= '<div class="form-text small" style="margin-top:-10px;">Para: After-Related, archives</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="mb-2">';
|
||||
$slotSkyscraper = $this->renderer->getFieldValue($cid, 'content', 'slot_skyscraper', '');
|
||||
$html .= $this->buildTextInput($cid . 'SlotSkyscraper', '🏢 Skyscraper (tall)', $slotSkyscraper);
|
||||
$html .= '<div class="form-text small" style="margin-top:-10px;">Para: Rail Ads laterales (160x600)</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildAnalyticsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #4285f4;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-graph-up me-2" style="color: #4285f4;"></i>';
|
||||
$html .= ' Google Analytics';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Switch: Analytics Enabled
|
||||
$analyticsEnabled = $this->renderer->getFieldValue($cid, 'analytics', 'analytics_enabled', false);
|
||||
$html .= $this->buildSwitch($cid . 'AnalyticsEnabled', 'Activar Analytics', $analyticsEnabled, 'bi-power');
|
||||
|
||||
// Tracking ID
|
||||
$gaTrackingId = $this->renderer->getFieldValue($cid, 'analytics', 'ga_tracking_id', '');
|
||||
$html .= $this->buildTextInput($cid . 'GaTrackingId', 'Google Analytics ID', $gaTrackingId, 'G-XXXXXXXXXX');
|
||||
$html .= '<div class="form-text small mb-2">Formato: G-XXXXXXXXXX (GA4) o UA-XXXXXXXX-X</div>';
|
||||
|
||||
// Anonymize IP
|
||||
$gaAnonymizeIp = $this->renderer->getFieldValue($cid, 'analytics', 'ga_anonymize_ip', true);
|
||||
$html .= $this->buildSwitch($cid . 'GaAnonymizeIp', 'Anonimizar IP (GDPR)', $gaAnonymizeIp, 'bi-shield-check');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildRailAdsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #dc3545;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-layout-sidebar me-2" style="color: #dc3545;"></i>';
|
||||
$html .= ' Rail Ads (Laterales)';
|
||||
$html .= ' <span class="badge bg-secondary ms-2">>1600px</span>';
|
||||
$html .= ' </h5>';
|
||||
$html .= ' <p class="small text-muted mb-3">Anuncios fijos en los margenes del viewport. Solo en pantallas muy anchas.</p>';
|
||||
|
||||
// Master switch
|
||||
$railEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'rail_ads_enabled', false);
|
||||
$html .= $this->buildSwitch($cid . 'RailAdsEnabled', 'Activar Rail Ads', $railEnabled, 'bi-power');
|
||||
|
||||
// Left/Right toggles
|
||||
$html .= '<div class="row g-2 mt-2">';
|
||||
$html .= ' <div class="col-6">';
|
||||
$leftEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'rail_left_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'RailLeftEnabled', 'Rail izquierdo', $leftEnabled);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6">';
|
||||
$rightEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'rail_right_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'RailRightEnabled', 'Rail derecho', $rightEnabled);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Format select - Solo altura (el ancho es responsive)
|
||||
$railFormat = $this->renderer->getFieldValue($cid, 'behavior', 'rail_format', 'h600');
|
||||
$html .= $this->buildSelect($cid . 'RailFormat', 'Altura del Rail',
|
||||
$railFormat,
|
||||
[
|
||||
'h250' => '250px (Compacto)',
|
||||
'h300' => '300px (Pequeno)',
|
||||
'h400' => '400px (Mediano)',
|
||||
'h500' => '500px',
|
||||
'h600' => '600px (Recomendado)',
|
||||
'h700' => '700px',
|
||||
'h800' => '800px (Grande)',
|
||||
'h1050' => '1050px (Extra grande)'
|
||||
]
|
||||
);
|
||||
$html .= '<small class="text-muted d-block mt-1 mb-2">El ancho se ajusta automaticamente al espacio disponible.</small>';
|
||||
|
||||
// Top offset - Select con opciones predefinidas
|
||||
$topOffset = $this->renderer->getFieldValue($cid, 'behavior', 'rail_top_offset', '300');
|
||||
$html .= $this->buildSelect($cid . 'RailTopOffset', 'Distancia desde arriba',
|
||||
$topOffset,
|
||||
[
|
||||
'150' => '150px (Cerca del header)',
|
||||
'200' => '200px',
|
||||
'300' => '300px (Recomendado)',
|
||||
'400' => '400px',
|
||||
'500' => '500px',
|
||||
'700' => '700px (Debajo del fold)'
|
||||
]
|
||||
);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seccion para Anchor Ads (anuncios fijos top/bottom)
|
||||
*/
|
||||
private function buildAnchorAdsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #17a2b8;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-pin-angle me-2" style="color: #17a2b8;"></i>';
|
||||
$html .= ' Anuncios Fijos (Anchor)';
|
||||
$html .= ' </h5>';
|
||||
$html .= ' <p class="small text-muted mb-3">Anuncios fijos en el borde superior o inferior de la pantalla.</p>';
|
||||
|
||||
// Master switch
|
||||
$anchorEnabled = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_enabled', false);
|
||||
$html .= $this->buildSwitch($cid . 'AnchorEnabled', 'Activar Anchor Ads', $anchorEnabled, 'bi-power');
|
||||
|
||||
// Posicion
|
||||
$anchorPosition = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_position', 'bottom');
|
||||
$html .= $this->buildSelect($cid . 'AnchorPosition', 'Posicion del anuncio',
|
||||
$anchorPosition,
|
||||
[
|
||||
'top' => 'Solo en la parte superior',
|
||||
'bottom' => 'Solo en la parte inferior',
|
||||
'both' => 'Superior e inferior'
|
||||
]
|
||||
);
|
||||
|
||||
// Altura
|
||||
$anchorHeight = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_height', '90');
|
||||
$html .= $this->buildSelect($cid . 'AnchorHeight', 'Altura del anchor',
|
||||
$anchorHeight,
|
||||
['50' => '50px', '90' => '90px', '100' => '100px', '120' => '120px']
|
||||
);
|
||||
|
||||
// Collapsible toggle
|
||||
$collapsible = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_collapsible_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'AnchorCollapsibleEnabled', 'Permitir minimizar', $collapsible, 'bi-arrows-collapse');
|
||||
$html .= '<small class="text-muted d-block" style="margin-top: -8px; margin-left: 40px;">Usuario puede minimizar en lugar de cerrar</small>';
|
||||
|
||||
// Pantallas
|
||||
$html .= '<div class="row g-2 mt-2">';
|
||||
$html .= ' <div class="col-6">';
|
||||
$showMobile = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_show_on_mobile', true);
|
||||
$html .= $this->buildSwitch($cid . 'AnchorShowOnMobile', 'Mostrar en movil', $showMobile, 'bi-phone');
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6">';
|
||||
$showWide = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_show_on_wide_screens', false);
|
||||
$html .= $this->buildSwitch($cid . 'AnchorShowOnWideScreens', 'Pantallas anchas', $showWide, 'bi-display');
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Recordar estado
|
||||
$html .= '<div class="mt-3 p-2 rounded" style="background: #e7f1ff;">';
|
||||
$rememberState = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_remember_state', true);
|
||||
$html .= $this->buildSwitch($cid . 'AnchorRememberState', 'Recordar cierre/colapso', $rememberState, 'bi-clock-history');
|
||||
|
||||
$rememberDuration = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_remember_duration', 'session');
|
||||
$html .= $this->buildSelect($cid . 'AnchorRememberDuration', 'Duracion',
|
||||
$rememberDuration,
|
||||
[
|
||||
'session' => 'Solo esta sesion',
|
||||
'1hour' => '1 hora',
|
||||
'1day' => '1 dia',
|
||||
'1week' => '1 semana'
|
||||
]
|
||||
);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seccion para Vignette Ads (pantalla completa)
|
||||
*/
|
||||
private function buildVignetteAdsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #9c27b0;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fullscreen me-2" style="color: #9c27b0;"></i>';
|
||||
$html .= ' Anuncios de Vineta';
|
||||
$html .= ' <span class="badge bg-secondary ms-2">Pantalla Completa</span>';
|
||||
$html .= ' </h5>';
|
||||
$html .= ' <p class="small text-muted mb-3">Anuncios que ocupan toda la pantalla, aparecen segun el trigger configurado.</p>';
|
||||
|
||||
// Master switch
|
||||
$vignetteEnabled = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_enabled', false);
|
||||
$html .= $this->buildSwitch($cid . 'VignetteEnabled', 'Activar Vignette Ads', $vignetteEnabled, 'bi-power');
|
||||
|
||||
// Trigger
|
||||
$vignetteTrigger = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_trigger', 'pageview');
|
||||
$html .= $this->buildSelect($cid . 'VignetteTrigger', 'Cuando mostrar',
|
||||
(string)$vignetteTrigger,
|
||||
[
|
||||
'pageview' => 'Al cargar la pagina',
|
||||
'scroll_50' => 'Al scrollear 50%',
|
||||
'scroll_75' => 'Al scrollear 75%',
|
||||
'exit_intent' => 'Al intentar salir',
|
||||
'time_delay' => 'Despues de X segundos'
|
||||
]
|
||||
);
|
||||
|
||||
// Delay inicial
|
||||
$triggerDelay = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_trigger_delay', '5');
|
||||
$html .= $this->buildTextInput($cid . 'VignetteTriggerDelay', 'Delay inicial (segundos)', (string)$triggerDelay, '5');
|
||||
|
||||
// Tamano y opacidad
|
||||
$html .= '<div class="row g-2 mt-2">';
|
||||
$html .= ' <div class="col-6">';
|
||||
$size = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_size', 'auto');
|
||||
$html .= $this->buildSelect($cid . 'VignetteSize', 'Tamano',
|
||||
(string)$size,
|
||||
[
|
||||
'auto' => 'Auto (recomendado)',
|
||||
'responsive' => 'Responsive (fluid)',
|
||||
'1280x720' => '1280x720 (HD 720p)',
|
||||
'960x540' => '960x540 (qHD)',
|
||||
'854x480' => '854x480 (480p)',
|
||||
'800x450' => '800x450 (16:9)',
|
||||
'640x360' => '640x360 (360p)',
|
||||
'560x315' => '560x315 (YouTube)',
|
||||
'300x250' => '300x250 (Rectangle)',
|
||||
'336x280' => '336x280 (Large Rectangle)',
|
||||
]
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6">';
|
||||
$opacity = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_overlay_opacity', '0.7');
|
||||
$html .= $this->buildSelect($cid . 'VignetteOverlayOpacity', 'Opacidad fondo',
|
||||
(string)$opacity,
|
||||
['0.5' => '50%', '0.6' => '60%', '0.7' => '70%', '0.8' => '80%', '0.9' => '90%']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Pantallas
|
||||
$html .= '<div class="row g-2 mt-2">';
|
||||
$html .= ' <div class="col-6">';
|
||||
$showMobile = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_show_on_mobile', true);
|
||||
$html .= $this->buildSwitch($cid . 'VignetteShowOnMobile', 'Mostrar en movil', $showMobile, 'bi-phone');
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6">';
|
||||
$showDesktop = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_show_on_desktop', true);
|
||||
$html .= $this->buildSwitch($cid . 'VignetteShowOnDesktop', 'Mostrar en desktop', $showDesktop, 'bi-display');
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Reaparicion
|
||||
$html .= '<div class="mt-3 p-2 rounded" style="background: #f3e5f5;">';
|
||||
$html .= '<p class="small fw-semibold mb-2"><i class="bi bi-arrow-repeat me-1"></i> Reaparicion</p>';
|
||||
|
||||
$reshowEnabled = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_reshow_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'VignetteReshowEnabled', 'Permitir reaparicion', $reshowEnabled);
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-6">';
|
||||
$reshowTime = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_reshow_time', '5');
|
||||
$html .= $this->buildSelect($cid . 'VignetteReshowTime', 'Tiempo (min)',
|
||||
(string)$reshowTime,
|
||||
['1' => '1 min', '2' => '2 min', '3' => '3 min', '4' => '4 min', '5' => '5 min', '10' => '10 min', '15' => '15 min', '30' => '30 min']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6">';
|
||||
$maxSession = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_max_per_session', '3');
|
||||
$html .= $this->buildSelect($cid . 'VignetteMaxPerSession', 'Max/sesion',
|
||||
(string)$maxSession,
|
||||
['1' => '1', '2' => '2', '3' => '3', '5' => '5', 'unlimited' => 'Sin limite']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seccion para anuncios en resultados de busqueda (ROI APU Search)
|
||||
*/
|
||||
private function buildSearchResultsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #fd7e14;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-search me-2" style="color: #fd7e14;"></i>';
|
||||
$html .= ' Resultados de Busqueda';
|
||||
$html .= ' <span class="badge bg-secondary ms-2">ROI APU Search</span>';
|
||||
$html .= ' </h5>';
|
||||
$html .= ' <p class="small text-muted mb-3">Insertar anuncios en los resultados del buscador de Analisis de Precios Unitarios.</p>';
|
||||
|
||||
// Master switch
|
||||
$searchAdsEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_ads_enabled', false);
|
||||
$html .= $this->buildSwitch($cid . 'SearchAdsEnabled', 'Activar ads en busqueda', $searchAdsEnabled, 'bi-power');
|
||||
|
||||
// Anuncio superior
|
||||
$html .= '<div class="border rounded p-3 mb-3" style="background: #fff8f0;">';
|
||||
$html .= '<div class="d-flex align-items-center gap-2 mb-2">';
|
||||
$html .= ' <span class="badge" style="background: #fd7e14;">ANUNCIO SUPERIOR</span>';
|
||||
$html .= ' <small class="text-muted">Debajo del campo de busqueda</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$topEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_top_ad_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'SearchTopAdEnabled', 'Activar', $topEnabled);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$topFormat = $this->renderer->getFieldValue($cid, 'search_results', 'search_top_ad_format', 'auto');
|
||||
$html .= $this->buildSelect($cid . 'SearchTopAdFormat', 'Formato',
|
||||
(string)$topFormat,
|
||||
['auto' => 'Auto (responsive)', 'display' => 'Display (fijo)', 'in-article' => 'In-Article (fluid)']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Anuncios entre resultados
|
||||
$html .= '<div class="border rounded p-3" style="background: #fff8f0;">';
|
||||
$html .= '<div class="d-flex align-items-center gap-2 mb-2">';
|
||||
$html .= ' <span class="badge" style="background: #fd7e14;">ENTRE RESULTADOS</span>';
|
||||
$html .= ' <small class="text-muted">Intercalados con los resultados</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2 mb-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$betweenEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'SearchBetweenEnabled', 'Activar', $betweenEnabled);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$betweenMax = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_max', '1');
|
||||
$html .= $this->buildSelect($cid . 'SearchBetweenMax', 'Maximo ads',
|
||||
(string)$betweenMax,
|
||||
['1' => '1 anuncio', '2' => '2 anuncios', '3' => '3 anuncios (max)']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2 mb-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$betweenFormat = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_format', 'in-article');
|
||||
$html .= $this->buildSelect($cid . 'SearchBetweenFormat', 'Formato',
|
||||
(string)$betweenFormat,
|
||||
['in-article' => 'In-Article (fluid)', 'auto' => 'Auto (responsive)', 'autorelaxed' => 'Autorelaxed (feed)']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$betweenPosition = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_position', 'random');
|
||||
$html .= $this->buildSelect($cid . 'SearchBetweenPosition', 'Posicion',
|
||||
(string)$betweenPosition,
|
||||
['random' => 'Aleatorio', 'fixed' => 'Fijo (cada N)', 'first_half' => 'Primera mitad']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$betweenEvery = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_every', '5');
|
||||
$html .= $this->buildSelect($cid . 'SearchBetweenEvery', 'Cada N resultados (si es fijo)',
|
||||
(string)$betweenEvery,
|
||||
['3' => 'Cada 3', '4' => 'Cada 4', '5' => 'Cada 5', '6' => 'Cada 6', '7' => 'Cada 7', '8' => 'Cada 8', '10' => 'Cada 10']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildExclusionsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #6c757d;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-slash-circle me-2" style="color: #6c757d;"></i>';
|
||||
$html .= ' Exclusiones y Rendimiento';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Accordion para exclusiones
|
||||
$html .= '<div class="accordion accordion-flush" id="exclusionsAccordion">';
|
||||
|
||||
// Exclusiones
|
||||
$html .= '<div class="accordion-item">';
|
||||
$html .= ' <h2 class="accordion-header">';
|
||||
$html .= ' <button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#exclusionsCollapse">';
|
||||
$html .= ' <i class="bi bi-funnel me-2"></i> Filtros de exclusion';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </h2>';
|
||||
$html .= ' <div id="exclusionsCollapse" class="accordion-collapse collapse" data-bs-parent="#exclusionsAccordion">';
|
||||
$html .= ' <div class="accordion-body">';
|
||||
|
||||
$excludeCats = $this->renderer->getFieldValue($cid, 'forms', 'exclude_categories', '');
|
||||
$html .= $this->buildTextarea($cid . 'ExcludeCategories', 'Excluir categorias (IDs)', $excludeCats, 'Ej: 5,12,23');
|
||||
|
||||
$excludeTypes = $this->renderer->getFieldValue($cid, 'forms', 'exclude_post_types', '');
|
||||
$html .= $this->buildTextarea($cid . 'ExcludePostTypes', 'Excluir tipos de post', $excludeTypes, 'Ej: page,attachment');
|
||||
|
||||
$excludeIds = $this->renderer->getFieldValue($cid, 'forms', 'exclude_post_ids', '');
|
||||
$html .= $this->buildTextarea($cid . 'ExcludePostIds', 'Excluir posts (IDs)', $excludeIds, 'Ej: 100,205,310');
|
||||
|
||||
$minLength = $this->renderer->getFieldValue($cid, 'forms', 'min_content_length', '500');
|
||||
$html .= $this->buildTextInput($cid . 'MinContentLength', 'Longitud minima de contenido', $minLength);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>'; // end accordion
|
||||
|
||||
// Delay settings (siempre visibles)
|
||||
$html .= '<hr class="my-3">';
|
||||
$html .= '<p class="small text-muted mb-2"><i class="bi bi-speedometer2 me-1"></i> Rendimiento:</p>';
|
||||
|
||||
$delayEnabled = $this->renderer->getFieldValue($cid, 'forms', 'delay_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'DelayEnabled', 'Retrasar carga (mejor PageSpeed)', $delayEnabled, 'bi-hourglass-split');
|
||||
|
||||
$delayTimeout = $this->renderer->getFieldValue($cid, 'forms', 'delay_timeout', '5000');
|
||||
$html .= $this->buildTextInput($cid . 'DelayTimeout', 'Timeout de delay (ms)', $delayTimeout);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
// === HELPERS ===
|
||||
|
||||
private function buildSwitch(string $id, string $label, $value, string $icon = ''): string
|
||||
{
|
||||
$checked = checked($value, true, false);
|
||||
$iconHtml = $icon ? '<i class="bi ' . $icon . ' me-1" style="color: #FF8600;"></i>' : '';
|
||||
|
||||
return sprintf(
|
||||
'<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="%s" %s>
|
||||
<label class="form-check-label small" for="%s">%s%s</label>
|
||||
</div>
|
||||
</div>',
|
||||
esc_attr($id), $checked, esc_attr($id), $iconHtml, esc_html($label)
|
||||
);
|
||||
}
|
||||
|
||||
private function buildTextInput(string $id, string $label, string $value, string $placeholder = ''): string
|
||||
{
|
||||
return sprintf(
|
||||
'<div class="mb-3">
|
||||
<label for="%s" class="form-label small fw-semibold">%s</label>
|
||||
<input type="text" class="form-control form-control-sm" id="%s" value="%s" placeholder="%s">
|
||||
</div>',
|
||||
esc_attr($id), esc_html($label), esc_attr($id), esc_attr($value), esc_attr($placeholder)
|
||||
);
|
||||
}
|
||||
|
||||
private function buildTextarea(string $id, string $label, string $value, string $placeholder = ''): string
|
||||
{
|
||||
return sprintf(
|
||||
'<div class="mb-3">
|
||||
<label for="%s" class="form-label small fw-semibold">%s</label>
|
||||
<textarea class="form-control form-control-sm" id="%s" rows="2" placeholder="%s">%s</textarea>
|
||||
</div>',
|
||||
esc_attr($id), esc_html($label), esc_attr($id), esc_attr($placeholder), esc_textarea($value)
|
||||
);
|
||||
}
|
||||
|
||||
private function buildSelect(string $id, string $label, string $value, array $options): string
|
||||
{
|
||||
$optionsHtml = '';
|
||||
foreach ($options as $optValue => $optLabel) {
|
||||
$selected = selected($value, $optValue, false);
|
||||
$optionsHtml .= sprintf(
|
||||
'<option value="%s" %s>%s</option>',
|
||||
esc_attr($optValue),
|
||||
$selected,
|
||||
esc_html($optLabel)
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<div class="mb-2">
|
||||
<label for="%s" class="form-label small fw-semibold">%s</label>
|
||||
<select class="form-select form-select-sm" id="%s">%s</select>
|
||||
</div>',
|
||||
esc_attr($id), esc_html($label), esc_attr($id), $optionsHtml
|
||||
);
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, $value): string
|
||||
{
|
||||
$checked = checked($value, true, false);
|
||||
|
||||
return sprintf(
|
||||
'<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="%s" %s>
|
||||
<label class="form-check-label small" for="%s">
|
||||
<i class="bi %s me-1" style="color: #6c757d;"></i>%s
|
||||
</label>
|
||||
</div>',
|
||||
esc_attr($id),
|
||||
$checked,
|
||||
esc_attr($id),
|
||||
esc_attr($icon),
|
||||
esc_html($label)
|
||||
);
|
||||
}
|
||||
}
|
||||
41
Admin/Application/UseCases/RenderDashboardUseCase.php
Normal file
41
Admin/Application/UseCases/RenderDashboardUseCase.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Application\UseCases;
|
||||
|
||||
use ROITheme\Admin\Domain\Contracts\DashboardRendererInterface;
|
||||
|
||||
/**
|
||||
* Caso de uso para renderizar el dashboard del panel de administración
|
||||
*
|
||||
* Application - Orquestación sin lógica de negocio ni WordPress
|
||||
*/
|
||||
final class RenderDashboardUseCase
|
||||
{
|
||||
/**
|
||||
* @param DashboardRendererInterface $renderer Renderizador del dashboard
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly DashboardRendererInterface $renderer
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejecuta el caso de uso
|
||||
*
|
||||
* @param string $viewType Tipo de vista a renderizar
|
||||
* @return string HTML renderizado
|
||||
* @throws \RuntimeException Si el renderer no soporta el tipo de vista
|
||||
*/
|
||||
public function execute(string $viewType = 'dashboard'): string
|
||||
{
|
||||
if (!$this->renderer->supports($viewType)) {
|
||||
throw new \RuntimeException(
|
||||
sprintf('Renderer does not support view type: %s', $viewType)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->renderer->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ContactForm\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Contact Form
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class ContactFormFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'contact-form';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'contactFormEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'contactFormShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'contactFormShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'contactFormVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'contactFormVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'contactFormVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'contactFormVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'contactFormVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'contactFormExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'contactFormExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'contactFormExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'contactFormExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'contactFormSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'],
|
||||
'contactFormSectionDescription' => ['group' => 'content', 'attribute' => 'section_description'],
|
||||
'contactFormSubmitButtonText' => ['group' => 'content', 'attribute' => 'submit_button_text'],
|
||||
'contactFormSubmitButtonIcon' => ['group' => 'content', 'attribute' => 'submit_button_icon'],
|
||||
|
||||
// Contact Info
|
||||
'contactFormShowContactInfo' => ['group' => 'contact_info', 'attribute' => 'show_contact_info'],
|
||||
'contactFormPhoneLabel' => ['group' => 'contact_info', 'attribute' => 'phone_label'],
|
||||
'contactFormPhoneValue' => ['group' => 'contact_info', 'attribute' => 'phone_value'],
|
||||
'contactFormEmailLabel' => ['group' => 'contact_info', 'attribute' => 'email_label'],
|
||||
'contactFormEmailValue' => ['group' => 'contact_info', 'attribute' => 'email_value'],
|
||||
'contactFormLocationLabel' => ['group' => 'contact_info', 'attribute' => 'location_label'],
|
||||
'contactFormLocationValue' => ['group' => 'contact_info', 'attribute' => 'location_value'],
|
||||
|
||||
// Form Labels
|
||||
'contactFormFullnamePlaceholder' => ['group' => 'form_labels', 'attribute' => 'fullname_placeholder'],
|
||||
'contactFormCompanyPlaceholder' => ['group' => 'form_labels', 'attribute' => 'company_placeholder'],
|
||||
'contactFormWhatsappPlaceholder' => ['group' => 'form_labels', 'attribute' => 'whatsapp_placeholder'],
|
||||
'contactFormEmailPlaceholder' => ['group' => 'form_labels', 'attribute' => 'email_placeholder'],
|
||||
'contactFormMessagePlaceholder' => ['group' => 'form_labels', 'attribute' => 'message_placeholder'],
|
||||
|
||||
// Integration
|
||||
'contactFormWebhookUrl' => ['group' => 'integration', 'attribute' => 'webhook_url'],
|
||||
'contactFormWebhookMethod' => ['group' => 'integration', 'attribute' => 'webhook_method'],
|
||||
'contactFormIncludePageUrl' => ['group' => 'integration', 'attribute' => 'include_page_url'],
|
||||
'contactFormIncludeTimestamp' => ['group' => 'integration', 'attribute' => 'include_timestamp'],
|
||||
|
||||
// Messages
|
||||
'contactFormSuccessMessage' => ['group' => 'messages', 'attribute' => 'success_message'],
|
||||
'contactFormErrorMessage' => ['group' => 'messages', 'attribute' => 'error_message'],
|
||||
'contactFormSendingMessage' => ['group' => 'messages', 'attribute' => 'sending_message'],
|
||||
'contactFormValidationRequired' => ['group' => 'messages', 'attribute' => 'validation_required'],
|
||||
'contactFormValidationEmail' => ['group' => 'messages', 'attribute' => 'validation_email'],
|
||||
|
||||
// Colors
|
||||
'contactFormSectionBgColor' => ['group' => 'colors', 'attribute' => 'section_bg_color'],
|
||||
'contactFormTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'contactFormDescriptionColor' => ['group' => 'colors', 'attribute' => 'description_color'],
|
||||
'contactFormIconColor' => ['group' => 'colors', 'attribute' => 'icon_color'],
|
||||
'contactFormInfoLabelColor' => ['group' => 'colors', 'attribute' => 'info_label_color'],
|
||||
'contactFormInfoValueColor' => ['group' => 'colors', 'attribute' => 'info_value_color'],
|
||||
'contactFormInputBorderColor' => ['group' => 'colors', 'attribute' => 'input_border_color'],
|
||||
'contactFormInputFocusBorder' => ['group' => 'colors', 'attribute' => 'input_focus_border'],
|
||||
'contactFormButtonBgColor' => ['group' => 'colors', 'attribute' => 'button_bg_color'],
|
||||
'contactFormButtonTextColor' => ['group' => 'colors', 'attribute' => 'button_text_color'],
|
||||
'contactFormButtonHoverBg' => ['group' => 'colors', 'attribute' => 'button_hover_bg'],
|
||||
'contactFormSuccessBgColor' => ['group' => 'colors', 'attribute' => 'success_bg_color'],
|
||||
'contactFormSuccessTextColor' => ['group' => 'colors', 'attribute' => 'success_text_color'],
|
||||
'contactFormErrorBgColor' => ['group' => 'colors', 'attribute' => 'error_bg_color'],
|
||||
'contactFormErrorTextColor' => ['group' => 'colors', 'attribute' => 'error_text_color'],
|
||||
|
||||
// Spacing
|
||||
'contactFormSectionPaddingY' => ['group' => 'spacing', 'attribute' => 'section_padding_y'],
|
||||
'contactFormSectionMarginTop' => ['group' => 'spacing', 'attribute' => 'section_margin_top'],
|
||||
'contactFormTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||
'contactFormDescriptionMarginBottom' => ['group' => 'spacing', 'attribute' => 'description_margin_bottom'],
|
||||
'contactFormFormGap' => ['group' => 'spacing', 'attribute' => 'form_gap'],
|
||||
|
||||
// Visual Effects
|
||||
'contactFormInputBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'input_border_radius'],
|
||||
'contactFormButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
|
||||
'contactFormButtonPadding' => ['group' => 'visual_effects', 'attribute' => 'button_padding'],
|
||||
'contactFormTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
'contactFormTextareaRows' => ['group' => 'visual_effects', 'attribute' => 'textarea_rows'],
|
||||
];
|
||||
}
|
||||
}
|
||||
652
Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php
Normal file
652
Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php
Normal file
@@ -0,0 +1,652 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ContactForm\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Contact Form
|
||||
*
|
||||
* RESPONSABILIDAD: Generar formulario de configuracion del Contact Form
|
||||
*
|
||||
* SEGURIDAD: El webhook_url se muestra como input type="password" para evitar
|
||||
* que sea visible accidentalmente en pantalla compartida.
|
||||
*
|
||||
* @package ROITheme\Admin\ContactForm\Infrastructure\Ui
|
||||
*/
|
||||
final class ContactFormFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildContactInfoGroup($componentId);
|
||||
$html .= $this->buildFormLabelsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildIntegrationGroup($componentId);
|
||||
$html .= $this->buildMessagesGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-envelope-paper me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Formulario de Contacto';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Seccion de contacto antes del footer con envio a webhook';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="contact-form">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('contactFormEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('contactFormShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('contactFormShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'contactForm');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$sectionTitle = $this->renderer->getFieldValue($componentId, 'content', 'section_title', '¿Tienes alguna pregunta?');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormSectionTitle" class="form-label small mb-1 fw-semibold">Titulo de seccion</label>';
|
||||
$html .= ' <input type="text" id="contactFormSectionTitle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionTitle) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$sectionDescription = $this->renderer->getFieldValue($componentId, 'content', 'section_description', 'Completa el formulario y nuestro equipo te responderá en menos de 24 horas.');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormSectionDescription" class="form-label small mb-1 fw-semibold">Descripcion</label>';
|
||||
$html .= ' <textarea id="contactFormSectionDescription" class="form-control form-control-sm" rows="2">';
|
||||
$html .= esc_textarea($sectionDescription);
|
||||
$html .= '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$submitButtonText = $this->renderer->getFieldValue($componentId, 'content', 'submit_button_text', 'Enviar Mensaje');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormSubmitButtonText" class="form-label small mb-1 fw-semibold">Texto boton enviar</label>';
|
||||
$html .= ' <input type="text" id="contactFormSubmitButtonText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($submitButtonText) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$submitButtonIcon = $this->renderer->getFieldValue($componentId, 'content', 'submit_button_icon', 'bi-send-fill');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="contactFormSubmitButtonIcon" class="form-label small mb-1 fw-semibold">Icono boton (Bootstrap Icons)</label>';
|
||||
$html .= ' <input type="text" id="contactFormSubmitButtonIcon" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($submitButtonIcon) . '" placeholder="bi-send-fill">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContactInfoGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-person-lines-fill me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Info de Contacto';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$showContactInfo = $this->renderer->getFieldValue($componentId, 'contact_info', 'show_contact_info', true);
|
||||
$html .= $this->buildSwitch('contactFormShowContactInfo', 'Mostrar info contacto', 'bi-eye', $showContactInfo);
|
||||
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">Telefono</p>';
|
||||
|
||||
$phoneLabel = $this->renderer->getFieldValue($componentId, 'contact_info', 'phone_label', 'Teléfono');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <input type="text" id="contactFormPhoneLabel" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($phoneLabel) . '" placeholder="Label">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$phoneValue = $this->renderer->getFieldValue($componentId, 'contact_info', 'phone_value', '+52 55 1234 5678');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <input type="text" id="contactFormPhoneValue" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($phoneValue) . '" placeholder="Numero">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <p class="small fw-semibold mb-2">Email</p>';
|
||||
|
||||
$emailLabel = $this->renderer->getFieldValue($componentId, 'contact_info', 'email_label', 'Email');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <input type="text" id="contactFormEmailLabel" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($emailLabel) . '" placeholder="Label">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$emailValue = $this->renderer->getFieldValue($componentId, 'contact_info', 'email_value', 'contacto@apumexico.com');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <input type="email" id="contactFormEmailValue" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($emailValue) . '" placeholder="Direccion">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <p class="small fw-semibold mb-2">Ubicacion</p>';
|
||||
|
||||
$locationLabel = $this->renderer->getFieldValue($componentId, 'contact_info', 'location_label', 'Ubicación');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <input type="text" id="contactFormLocationLabel" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($locationLabel) . '" placeholder="Label">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$locationValue = $this->renderer->getFieldValue($componentId, 'contact_info', 'location_value', 'Ciudad de México, México');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <input type="text" id="contactFormLocationValue" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($locationValue) . '" placeholder="Direccion">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildFormLabelsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-input-cursor-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Labels del Formulario';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$fullnamePlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'fullname_placeholder', 'Nombre completo *');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="contactFormFullnamePlaceholder" class="form-label small mb-1 fw-semibold">Placeholder nombre</label>';
|
||||
$html .= ' <input type="text" id="contactFormFullnamePlaceholder" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($fullnamePlaceholder) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$companyPlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'company_placeholder', 'Empresa');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="contactFormCompanyPlaceholder" class="form-label small mb-1 fw-semibold">Placeholder empresa</label>';
|
||||
$html .= ' <input type="text" id="contactFormCompanyPlaceholder" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($companyPlaceholder) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$whatsappPlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'whatsapp_placeholder', 'WhatsApp *');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="contactFormWhatsappPlaceholder" class="form-label small mb-1 fw-semibold">Placeholder WhatsApp</label>';
|
||||
$html .= ' <input type="text" id="contactFormWhatsappPlaceholder" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($whatsappPlaceholder) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$emailPlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'email_placeholder', 'Correo electrónico *');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="contactFormEmailPlaceholder" class="form-label small mb-1 fw-semibold">Placeholder email</label>';
|
||||
$html .= ' <input type="text" id="contactFormEmailPlaceholder" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($emailPlaceholder) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$messagePlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'message_placeholder', '¿En qué podemos ayudarte?');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="contactFormMessagePlaceholder" class="form-label small mb-1 fw-semibold">Placeholder mensaje</label>';
|
||||
$html .= ' <input type="text" id="contactFormMessagePlaceholder" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($messagePlaceholder) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildIntegrationGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-link-45deg me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Integracion Webhook';
|
||||
$html .= ' <span class="badge bg-warning text-dark ms-2">Privado</span>';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="alert alert-info py-2 small mb-3">';
|
||||
$html .= ' <i class="bi bi-shield-lock me-1"></i>';
|
||||
$html .= ' El webhook URL nunca se expone en el frontend. Los datos se envian de forma segura desde el servidor.';
|
||||
$html .= ' </div>';
|
||||
|
||||
$webhookUrl = $this->renderer->getFieldValue($componentId, 'integration', 'webhook_url', '');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormWebhookUrl" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-link me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' URL del Webhook';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea id="contactFormWebhookUrl" class="form-control form-control-sm" rows="2" ';
|
||||
$html .= ' placeholder="https://tu-webhook.com/endpoint">';
|
||||
$html .= esc_textarea($webhookUrl);
|
||||
$html .= '</textarea>';
|
||||
$html .= ' <small class="text-muted">Deja vacio si no deseas enviar a un webhook externo.</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$webhookMethod = $this->renderer->getFieldValue($componentId, 'integration', 'webhook_method', 'POST');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormWebhookMethod" class="form-label small mb-1 fw-semibold">Metodo HTTP</label>';
|
||||
$html .= ' <select id="contactFormWebhookMethod" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="POST"' . ($webhookMethod === 'POST' ? ' selected' : '') . '>POST</option>';
|
||||
$html .= ' <option value="GET"' . ($webhookMethod === 'GET' ? ' selected' : '') . '>GET</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$includePageUrl = $this->renderer->getFieldValue($componentId, 'integration', 'include_page_url', true);
|
||||
$html .= $this->buildSwitch('contactFormIncludePageUrl', 'Incluir URL de pagina', 'bi-link', $includePageUrl);
|
||||
|
||||
$includeTimestamp = $this->renderer->getFieldValue($componentId, 'integration', 'include_timestamp', true);
|
||||
$html .= $this->buildSwitch('contactFormIncludeTimestamp', 'Incluir timestamp', 'bi-clock', $includeTimestamp);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildMessagesGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-chat-quote me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mensajes';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$successMessage = $this->renderer->getFieldValue($componentId, 'messages', 'success_message', '¡Gracias por contactarnos! Te responderemos pronto.');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormSuccessMessage" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-check-circle me-1 text-success"></i>';
|
||||
$html .= ' Mensaje de exito';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea id="contactFormSuccessMessage" class="form-control form-control-sm" rows="2">';
|
||||
$html .= esc_textarea($successMessage);
|
||||
$html .= '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$errorMessage = $this->renderer->getFieldValue($componentId, 'messages', 'error_message', 'Hubo un error al enviar el mensaje. Por favor intenta de nuevo.');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormErrorMessage" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-x-circle me-1 text-danger"></i>';
|
||||
$html .= ' Mensaje de error';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea id="contactFormErrorMessage" class="form-control form-control-sm" rows="2">';
|
||||
$html .= esc_textarea($errorMessage);
|
||||
$html .= '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$sendingMessage = $this->renderer->getFieldValue($componentId, 'messages', 'sending_message', 'Enviando...');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormSendingMessage" class="form-label small mb-1 fw-semibold">Mensaje enviando</label>';
|
||||
$html .= ' <input type="text" id="contactFormSendingMessage" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sendingMessage) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$validationRequired = $this->renderer->getFieldValue($componentId, 'messages', 'validation_required', 'Este campo es obligatorio');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="contactFormValidationRequired" class="form-label small mb-1 fw-semibold">Error campo requerido</label>';
|
||||
$html .= ' <input type="text" id="contactFormValidationRequired" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($validationRequired) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$validationEmail = $this->renderer->getFieldValue($componentId, 'messages', 'validation_email', 'Por favor ingresa un email válido');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="contactFormValidationEmail" class="form-label small mb-1 fw-semibold">Error email invalido</label>';
|
||||
$html .= ' <input type="text" id="contactFormValidationEmail" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($validationEmail) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Seccion
|
||||
$html .= ' <p class="small fw-semibold mb-2">Seccion</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$sectionBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'section_bg_color', 'rgba(108, 117, 125, 0.25)');
|
||||
$html .= $this->buildColorPicker('contactFormSectionBgColor', 'Fondo seccion', $sectionBgColor);
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#212529');
|
||||
$html .= $this->buildColorPicker('contactFormTitleColor', 'Color titulo', $titleColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Boton
|
||||
$html .= ' <p class="small fw-semibold mb-2">Boton</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_bg_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('contactFormButtonBgColor', 'Fondo boton', $buttonBgColor);
|
||||
|
||||
$buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('contactFormButtonTextColor', 'Texto boton', $buttonTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_bg', '#e67a00');
|
||||
$html .= $this->buildColorPicker('contactFormButtonHoverBg', 'Hover boton', $buttonHoverBg);
|
||||
|
||||
$iconColor = $this->renderer->getFieldValue($componentId, 'colors', 'icon_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('contactFormIconColor', 'Iconos', $iconColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Mensajes
|
||||
$html .= ' <p class="small fw-semibold mb-2">Mensajes</p>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$successBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'success_bg_color', '#d1e7dd');
|
||||
$html .= $this->buildColorPicker('contactFormSuccessBgColor', 'Fondo exito', $successBgColor);
|
||||
|
||||
$errorBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'error_bg_color', '#f8d7da');
|
||||
$html .= $this->buildColorPicker('contactFormErrorBgColor', 'Fondo error', $errorBgColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$sectionPaddingY = $this->renderer->getFieldValue($componentId, 'spacing', 'section_padding_y', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormSectionPaddingY" class="form-label small mb-1 fw-semibold">Padding vertical</label>';
|
||||
$html .= ' <input type="text" id="contactFormSectionPaddingY" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionPaddingY) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$sectionMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_top', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormSectionMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
|
||||
$html .= ' <input type="text" id="contactFormSectionMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionMarginTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.75rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||
$html .= ' <input type="text" id="contactFormTitleMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$formGap = $this->renderer->getFieldValue($componentId, 'spacing', 'form_gap', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormFormGap" class="form-label small mb-1 fw-semibold">Espacio campos</label>';
|
||||
$html .= ' <input type="text" id="contactFormFormGap" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($formGap) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$inputBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'input_border_radius', '6px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormInputBorderRadius" class="form-label small mb-1 fw-semibold">Radio inputs</label>';
|
||||
$html .= ' <input type="text" id="contactFormInputBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($inputBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$buttonBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '6px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormButtonBorderRadius" class="form-label small mb-1 fw-semibold">Radio boton</label>';
|
||||
$html .= ' <input type="text" id="contactFormButtonBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonPadding = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_padding', '0.75rem 2rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormButtonPadding" class="form-label small mb-1 fw-semibold">Padding boton</label>';
|
||||
$html .= ' <input type="text" id="contactFormButtonPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormTransitionDuration" class="form-label small mb-1 fw-semibold">Duracion transicion</label>';
|
||||
$html .= ' <input type="text" id="contactFormTransitionDuration" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$textareaRows = $this->renderer->getFieldValue($componentId, 'visual_effects', 'textarea_rows', '4');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="contactFormTextareaRows" class="form-label small mb-1 fw-semibold">Filas textarea</label>';
|
||||
$html .= ' <input type="number" id="contactFormTextareaRows" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($textareaRows) . '" min="2" max="10">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
// Manejar colores rgba
|
||||
$colorValue = $value;
|
||||
if (strpos($value, 'rgba') === 0 || strpos($value, 'rgb') === 0) {
|
||||
// Para rgba usamos un color aproximado en el picker
|
||||
$colorValue = '#6c757d';
|
||||
}
|
||||
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($colorValue)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <input type="text" class="form-control" id="%sText" value="%s" style="font-size: 0.75rem;">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaBoxSidebar\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para CTA Box Sidebar
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*
|
||||
* UBICACION:
|
||||
* - Dentro del modulo CtaBoxSidebar (autocontenido)
|
||||
* - Eliminar modulo = eliminar mapper
|
||||
*/
|
||||
final class CtaBoxSidebarFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'cta-box-sidebar';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'ctaEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'ctaVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'ctaVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'ctaVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'ctaVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'ctaVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'ctaExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'ctaExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'ctaExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'ctaExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'ctaTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||
'ctaDescription' => ['group' => 'content', 'attribute' => 'description'],
|
||||
'ctaButtonText' => ['group' => 'content', 'attribute' => 'button_text'],
|
||||
'ctaButtonIcon' => ['group' => 'content', 'attribute' => 'button_icon'],
|
||||
'ctaButtonAction' => ['group' => 'content', 'attribute' => 'button_action'],
|
||||
'ctaButtonLink' => ['group' => 'content', 'attribute' => 'button_link'],
|
||||
|
||||
// Behavior
|
||||
'ctaTextAlign' => ['group' => 'behavior', 'attribute' => 'text_align'],
|
||||
|
||||
// Typography
|
||||
'ctaTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'],
|
||||
'ctaTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'],
|
||||
'ctaDescFontSize' => ['group' => 'typography', 'attribute' => 'description_font_size'],
|
||||
'ctaButtonFontSize' => ['group' => 'typography', 'attribute' => 'button_font_size'],
|
||||
'ctaButtonFontWeight' => ['group' => 'typography', 'attribute' => 'button_font_weight'],
|
||||
|
||||
// Colors
|
||||
'ctaBackgroundColor' => ['group' => 'colors', 'attribute' => 'background_color'],
|
||||
'ctaTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'ctaDescriptionColor' => ['group' => 'colors', 'attribute' => 'description_color'],
|
||||
'ctaButtonBgColor' => ['group' => 'colors', 'attribute' => 'button_background_color'],
|
||||
'ctaButtonTextColor' => ['group' => 'colors', 'attribute' => 'button_text_color'],
|
||||
'ctaButtonHoverBg' => ['group' => 'colors', 'attribute' => 'button_hover_background'],
|
||||
'ctaButtonHoverText' => ['group' => 'colors', 'attribute' => 'button_hover_text_color'],
|
||||
|
||||
// Spacing
|
||||
'ctaContainerPadding' => ['group' => 'spacing', 'attribute' => 'container_padding'],
|
||||
'ctaTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||
'ctaDescMarginBottom' => ['group' => 'spacing', 'attribute' => 'description_margin_bottom'],
|
||||
'ctaButtonPadding' => ['group' => 'spacing', 'attribute' => 'button_padding'],
|
||||
'ctaIconMarginRight' => ['group' => 'spacing', 'attribute' => 'icon_margin_right'],
|
||||
|
||||
// Visual Effects
|
||||
'ctaBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'ctaButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
|
||||
'ctaBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
'ctaTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaBoxSidebar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para el CTA Box Sidebar
|
||||
*
|
||||
* Responsabilidad:
|
||||
* - Generar HTML del formulario de configuracion
|
||||
* - Usar Design System (Bootstrap 5)
|
||||
* - Cargar valores desde BD via AdminDashboardRenderer
|
||||
*
|
||||
* @package ROITheme\Admin\CtaBoxSidebar\Infrastructure\Ui
|
||||
*/
|
||||
final class CtaBoxSidebarFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildBehaviorGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-megaphone me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de CTA Box Sidebar';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Caja de llamada a la accion en el sidebar';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="cta-box-sidebar">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// is_enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('ctaEnabled', 'Activar CTA box', 'bi-power', $enabled);
|
||||
|
||||
// show_on_desktop
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('ctaShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
// show_on_mobile
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
||||
$html .= $this->buildSwitch('ctaShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
// Obtener valores de _page_visibility (grupo especial)
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
// Grid 3 columnas según Design System
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'cta');
|
||||
|
||||
// Switch: Ocultar para usuarios logueados (Plan 99.16)
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($componentId, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaHideForLoggedIn" ';
|
||||
$html .= checked($hideForLoggedIn, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaHideForLoggedIn" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-person-lock me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Ocultar para usuarios logueados</strong>';
|
||||
$html .= ' <small class="text-muted d-block">No mostrar a usuarios con sesión iniciada</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// title
|
||||
$title = $this->renderer->getFieldValue($componentId, 'content', 'title', '¿Listo para potenciar tus proyectos?');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaTitle" class="form-label small mb-1 fw-semibold">Titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaTitle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($title) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// description
|
||||
$description = $this->renderer->getFieldValue($componentId, 'content', 'description', 'Accede a nuestra biblioteca completa de APUs y herramientas profesionales.');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaDescription" class="form-label small mb-1 fw-semibold">Descripcion</label>';
|
||||
$html .= ' <textarea id="ctaDescription" class="form-control form-control-sm" rows="2">' . esc_textarea($description) . '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_text
|
||||
$buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', 'Solicitar Demo');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaButtonText" class="form-label small mb-1 fw-semibold">Texto del boton</label>';
|
||||
$html .= ' <input type="text" id="ctaButtonText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonText) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_icon
|
||||
$buttonIcon = $this->renderer->getFieldValue($componentId, 'content', 'button_icon', 'bi bi-calendar-check');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaButtonIcon" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-stars me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Icono del boton';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="ctaButtonIcon" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonIcon) . '" placeholder="ej: bi bi-calendar-check">';
|
||||
$html .= ' <small class="text-muted">Clase de Bootstrap Icons</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_action
|
||||
$buttonAction = $this->renderer->getFieldValue($componentId, 'content', 'button_action', 'modal');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaButtonAction" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-cursor me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Accion del boton';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="ctaButtonAction" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="modal"' . ($buttonAction === 'modal' ? ' selected' : '') . '>Abrir modal</option>';
|
||||
$html .= ' <option value="link"' . ($buttonAction === 'link' ? ' selected' : '') . '>Ir a URL</option>';
|
||||
$html .= ' <option value="scroll"' . ($buttonAction === 'scroll' ? ' selected' : '') . '>Scroll a seccion</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_link
|
||||
$buttonLink = $this->renderer->getFieldValue($componentId, 'content', 'button_link', '#contactModal');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaButtonLink" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' URL/ID destino';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="ctaButtonLink" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonLink) . '" placeholder="ej: #contactModal o https://...">';
|
||||
$html .= ' <small class="text-muted">Para modal usa #nombreModal, para scroll usa #idSeccion</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildBehaviorGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-sliders me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Comportamiento';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// text_align
|
||||
$textAlign = $this->renderer->getFieldValue($componentId, 'behavior', 'text_align', 'center');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaTextAlign" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-text-center me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Alineacion del texto';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="ctaTextAlign" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="left"' . ($textAlign === 'left' ? ' selected' : '') . '>Izquierda</option>';
|
||||
$html .= ' <option value="center"' . ($textAlign === 'center' ? ' selected' : '') . '>Centro</option>';
|
||||
$html .= ' <option value="right"' . ($textAlign === 'right' ? ' selected' : '') . '>Derecha</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// title_font_size
|
||||
$titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '1.25rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaTitleFontSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaTitleFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// title_font_weight
|
||||
$titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '700');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaTitleFontWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaTitleFontWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// description_font_size
|
||||
$descFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'description_font_size', '0.9rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaDescFontSize" class="form-label small mb-1 fw-semibold">Tamano descripcion</label>';
|
||||
$html .= ' <input type="text" id="ctaDescFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($descFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_font_size
|
||||
$buttonFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'button_font_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaButtonFontSize" class="form-label small mb-1 fw-semibold">Tamano boton</label>';
|
||||
$html .= ' <input type="text" id="ctaButtonFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// button_font_weight
|
||||
$buttonFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'button_font_weight', '700');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaButtonFontWeight" class="form-label small mb-1 fw-semibold">Peso boton</label>';
|
||||
$html .= ' <input type="text" id="ctaButtonFontWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonFontWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Colores principales
|
||||
$html .= ' <p class="small fw-semibold mb-2">Contenedor</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('ctaBackgroundColor', 'Fondo', $bgColor);
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaTitleColor', 'Titulo', $titleColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$descColor = $this->renderer->getFieldValue($componentId, 'colors', 'description_color', 'rgba(255, 255, 255, 0.95)');
|
||||
$html .= $this->buildColorPicker('ctaDescriptionColor', 'Descripcion', $descColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores del boton
|
||||
$html .= ' <p class="small fw-semibold mb-2">Boton</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_background_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaButtonBgColor', 'Fondo', $buttonBgColor);
|
||||
|
||||
$buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('ctaButtonTextColor', 'Texto', $buttonTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores hover
|
||||
$html .= ' <p class="small fw-semibold mb-2">Boton Hover</p>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_background', '#0E2337');
|
||||
$html .= $this->buildColorPicker('ctaButtonHoverBg', 'Fondo hover', $buttonHoverBg);
|
||||
|
||||
$buttonHoverText = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_text_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaButtonHoverText', 'Texto hover', $buttonHoverText);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// container_padding
|
||||
$containerPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding', '24px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaContainerPadding" class="form-label small mb-1 fw-semibold">Padding contenedor</label>';
|
||||
$html .= ' <input type="text" id="ctaContainerPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// title_margin_bottom
|
||||
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaTitleMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// description_margin_bottom
|
||||
$descMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'description_margin_bottom', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaDescMarginBottom" class="form-label small mb-1 fw-semibold">Margen descripcion</label>';
|
||||
$html .= ' <input type="text" id="ctaDescMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($descMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_padding
|
||||
$buttonPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'button_padding', '0.75rem 1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaButtonPadding" class="form-label small mb-1 fw-semibold">Padding boton</label>';
|
||||
$html .= ' <input type="text" id="ctaButtonPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// icon_margin_right
|
||||
$iconMarginRight = $this->renderer->getFieldValue($componentId, 'spacing', 'icon_margin_right', '0.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaIconMarginRight" class="form-label small mb-1 fw-semibold">Margen icono</label>';
|
||||
$html .= ' <input type="text" id="ctaIconMarginRight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($iconMarginRight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// border_radius
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '8px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaBorderRadius" class="form-label small mb-1 fw-semibold">Radio contenedor</label>';
|
||||
$html .= ' <input type="text" id="ctaBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_border_radius
|
||||
$buttonBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '8px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaButtonBorderRadius" class="form-label small mb-1 fw-semibold">Radio boton</label>';
|
||||
$html .= ' <input type="text" id="ctaButtonBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// box_shadow
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 4px 12px rgba(255, 133, 0, 0.2)');
|
||||
$html .= ' <div class="col-12">';
|
||||
$html .= ' <label for="ctaBoxShadow" class="form-label small mb-1 fw-semibold">Sombra</label>';
|
||||
$html .= ' <input type="text" id="ctaBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mt-3 mb-0">';
|
||||
|
||||
// transition_duration
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaTransitionDuration" class="form-label small mb-1 fw-semibold">Duracion transicion</label>';
|
||||
$html .= ' <input type="text" id="ctaTransitionDuration" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, bool $checked): string
|
||||
{
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera un checkbox de visibilidad por tipo de pagina
|
||||
*
|
||||
* Sigue Design System: form-check-checkbox es obligatorio
|
||||
*/
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, bool $checked): string
|
||||
{
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaLetsTalk\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para CTA Lets Talk
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class CtaLetsTalkFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'cta-lets-talk';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'ctaLetsTalkEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaLetsTalkShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaLetsTalkShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaLetsTalkHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'ctaLetsTalkVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'ctaLetsTalkVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'ctaLetsTalkVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'ctaLetsTalkVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'ctaLetsTalkVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'letsTalkExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'letsTalkExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'letsTalkExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'letsTalkExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'ctaLetsTalkButtonText' => ['group' => 'content', 'attribute' => 'button_text'],
|
||||
'ctaLetsTalkShowIcon' => ['group' => 'content', 'attribute' => 'show_icon'],
|
||||
'ctaLetsTalkIconClass' => ['group' => 'content', 'attribute' => 'icon_class'],
|
||||
'ctaLetsTalkModalTarget' => ['group' => 'content', 'attribute' => 'modal_target'],
|
||||
'ctaLetsTalkAriaLabel' => ['group' => 'content', 'attribute' => 'aria_label'],
|
||||
|
||||
// Behavior
|
||||
'ctaLetsTalkEnableModal' => ['group' => 'behavior', 'attribute' => 'enable_modal'],
|
||||
'ctaLetsTalkCustomUrl' => ['group' => 'behavior', 'attribute' => 'custom_url'],
|
||||
'ctaLetsTalkOpenNewTab' => ['group' => 'behavior', 'attribute' => 'open_in_new_tab'],
|
||||
|
||||
// Typography
|
||||
'ctaLetsTalkFontSize' => ['group' => 'typography', 'attribute' => 'font_size'],
|
||||
'ctaLetsTalkFontWeight' => ['group' => 'typography', 'attribute' => 'font_weight'],
|
||||
'ctaLetsTalkTextTransform' => ['group' => 'typography', 'attribute' => 'text_transform'],
|
||||
|
||||
// Colors
|
||||
'ctaLetsTalkBgColor' => ['group' => 'colors', 'attribute' => 'background_color'],
|
||||
'ctaLetsTalkBgHoverColor' => ['group' => 'colors', 'attribute' => 'background_hover_color'],
|
||||
'ctaLetsTalkTextColor' => ['group' => 'colors', 'attribute' => 'text_color'],
|
||||
'ctaLetsTalkTextHoverColor' => ['group' => 'colors', 'attribute' => 'text_hover_color'],
|
||||
'ctaLetsTalkBorderColor' => ['group' => 'colors', 'attribute' => 'border_color'],
|
||||
|
||||
// Spacing
|
||||
'ctaLetsTalkPaddingTB' => ['group' => 'spacing', 'attribute' => 'padding_top_bottom'],
|
||||
'ctaLetsTalkPaddingLR' => ['group' => 'spacing', 'attribute' => 'padding_left_right'],
|
||||
'ctaLetsTalkMarginLeft' => ['group' => 'spacing', 'attribute' => 'margin_left'],
|
||||
'ctaLetsTalkIconSpacing' => ['group' => 'spacing', 'attribute' => 'icon_spacing'],
|
||||
|
||||
// Visual Effects
|
||||
'ctaLetsTalkBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'ctaLetsTalkBorderWidth' => ['group' => 'visual_effects', 'attribute' => 'border_width'],
|
||||
'ctaLetsTalkBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
'ctaLetsTalkTransition' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
];
|
||||
}
|
||||
}
|
||||
530
Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php
Normal file
530
Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php
Normal file
@@ -0,0 +1,530 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaLetsTalk\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* Class CtaLetsTalkFormBuilder
|
||||
*
|
||||
* Genera el formulario de administración para el componente CTA "Let's Talk".
|
||||
*
|
||||
* Responsabilidades:
|
||||
* - Renderizar formulario de configuración del botón CTA
|
||||
* - Organizar campos en grupos según el schema JSON
|
||||
* - Aplicar Design System (gradiente navy, borde orange)
|
||||
* - Usar Bootstrap 5 form controls
|
||||
*
|
||||
* @package ROITheme\Admin\CtaLetsTalk\Infrastructure\Ui
|
||||
*/
|
||||
final class CtaLetsTalkFormBuilder
|
||||
{
|
||||
private const COMPONENT_ID = 'cta-lets-talk';
|
||||
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
// Header
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
// Layout 2 columnas
|
||||
$html .= '<div class="row g-3">';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildBehaviorGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildVisualEffectsGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-lightning-charge-fill me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuración del Botón "Let\'s Talk"';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Personaliza el botón CTA principal del navbar';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="cta-lets-talk">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Switch: Enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkEnabled" name="visibility[is_enabled]" ';
|
||||
$html .= checked($enabled, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkEnabled">';
|
||||
$html .= ' <strong>Mostrar botón Let\'s Talk</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Desktop
|
||||
$showDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkShowDesktop" name="visibility[show_on_desktop]" ';
|
||||
$html .= checked($showDesktop, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkShowDesktop">';
|
||||
$html .= ' <strong>Mostrar en escritorio</strong> <span class="text-muted">(≥992px)</span>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Mobile
|
||||
$showMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkShowMobile" name="visibility[show_on_mobile]" ';
|
||||
$html .= checked($showMobile, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkShowMobile">';
|
||||
$html .= ' <strong>Mostrar en móvil</strong> <span class="text-muted">(<992px)</span>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'letsTalk');
|
||||
|
||||
// Switch: CSS Crítico
|
||||
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
||||
$html .= ' <div class="mb-2 mt-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkIsCritical" ';
|
||||
$html .= checked($isCritical, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkIsCritical" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-lightning-charge me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>CSS Crítico</strong>';
|
||||
$html .= ' <small class="text-muted d-block">Inyectar CSS en <head> para optimizar LCP</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Ocultar para usuarios logueados (Plan 99.16)
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($componentId, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkHideForLoggedIn" ';
|
||||
$html .= checked($hideForLoggedIn, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkHideForLoggedIn" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-person-lock me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Ocultar para usuarios logueados</strong>';
|
||||
$html .= ' <small class="text-muted d-block">No mostrar a usuarios con sesión iniciada</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-type me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Text: Button Text
|
||||
$buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', "Let's Talk");
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkButtonText" class="form-label small mb-1 fw-semibold">Texto del botón</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkButtonText" name="content[button_text]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonText) . '" maxlength="30" placeholder="Let\'s Talk">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show Icon
|
||||
$showIcon = $this->renderer->getFieldValue($componentId, 'content', 'show_icon', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkShowIcon" name="content[show_icon]" ';
|
||||
$html .= checked($showIcon, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkShowIcon">';
|
||||
$html .= ' <strong>Mostrar ícono</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Icon Class
|
||||
$iconClass = $this->renderer->getFieldValue($componentId, 'content', 'icon_class', 'bi-lightning-charge-fill');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkIconClass" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Clase del ícono <a href="https://icons.getbootstrap.com/" target="_blank" class="text-decoration-none"><i class="bi bi-box-arrow-up-right"></i></a>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkIconClass" name="content[icon_class]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($iconClass) . '" placeholder="bi-lightning-charge-fill">';
|
||||
$html .= ' <small class="text-muted">Usa clases de Bootstrap Icons (ej: bi-chat-dots)</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Modal Target
|
||||
$modalTarget = $this->renderer->getFieldValue($componentId, 'content', 'modal_target', '#contactModal');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkModalTarget" class="form-label small mb-1 fw-semibold">ID del modal</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkModalTarget" name="content[modal_target]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($modalTarget) . '" placeholder="#contactModal">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: ARIA Label
|
||||
$ariaLabel = $this->renderer->getFieldValue($componentId, 'content', 'aria_label', 'Abrir formulario de contacto');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkAriaLabel" class="form-label small mb-1 fw-semibold">Etiqueta ARIA (accesibilidad)</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkAriaLabel" name="content[aria_label]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($ariaLabel) . '" maxlength="100">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildBehaviorGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-mouse me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Comportamiento';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Switch: Enable Modal
|
||||
$enableModal = $this->renderer->getFieldValue($componentId, 'behavior', 'enable_modal', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkEnableModal" name="behavior[enable_modal]" ';
|
||||
$html .= checked($enableModal, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkEnableModal">';
|
||||
$html .= ' <strong>Abrir modal al hacer clic</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <small class="text-muted">Si está desactivado, usará la URL personalizada</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// URL: Custom URL
|
||||
$customUrl = $this->renderer->getFieldValue($componentId, 'behavior', 'custom_url', '');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkCustomUrl" class="form-label small mb-1 fw-semibold">URL personalizada</label>';
|
||||
$html .= ' <input type="url" id="ctaLetsTalkCustomUrl" name="behavior[custom_url]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($customUrl) . '" placeholder="https://ejemplo.com/contacto">';
|
||||
$html .= ' <small class="text-muted">Solo se usa si "Abrir modal" está desactivado</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Open in New Tab
|
||||
$openNewTab = $this->renderer->getFieldValue($componentId, 'behavior', 'open_in_new_tab', false);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkOpenNewTab" name="behavior[open_in_new_tab]" ';
|
||||
$html .= checked($openNewTab, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkOpenNewTab">';
|
||||
$html .= ' <strong>Abrir en nueva pestaña</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografía';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Text: Font Size
|
||||
$fontSize = $this->renderer->getFieldValue($componentId, 'typography', 'font_size', '1rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkFontSize" class="form-label small mb-1 fw-semibold">Tamaño de fuente</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkFontSize" name="typography[font_size]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($fontSize) . '" placeholder="1rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Font Weight
|
||||
$fontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'font_weight', '600');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkFontWeight" class="form-label small mb-1 fw-semibold">Peso de fuente</label>';
|
||||
$html .= ' <select id="ctaLetsTalkFontWeight" name="typography[font_weight]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="400" ' . selected($fontWeight, '400', false) . '>Normal (400)</option>';
|
||||
$html .= ' <option value="500" ' . selected($fontWeight, '500', false) . '>Medium (500)</option>';
|
||||
$html .= ' <option value="600" ' . selected($fontWeight, '600', false) . '>Semibold (600)</option>';
|
||||
$html .= ' <option value="700" ' . selected($fontWeight, '700', false) . '>Bold (700)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Text Transform
|
||||
$textTransform = $this->renderer->getFieldValue($componentId, 'typography', 'text_transform', 'none');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkTextTransform" class="form-label small mb-1 fw-semibold">Transformación de texto</label>';
|
||||
$html .= ' <select id="ctaLetsTalkTextTransform" name="typography[text_transform]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="none" ' . selected($textTransform, 'none', false) . '>Normal</option>';
|
||||
$html .= ' <option value="uppercase" ' . selected($textTransform, 'uppercase', false) . '>MAYÚSCULAS</option>';
|
||||
$html .= ' <option value="lowercase" ' . selected($textTransform, 'lowercase', false) . '>minúsculas</option>';
|
||||
$html .= ' <option value="capitalize" ' . selected($textTransform, 'capitalize', false) . '>Capitalizado</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Color: Background
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#FF8600');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkBgColor" class="form-label small mb-1 fw-semibold">Color de fondo</label>';
|
||||
$html .= ' <input type="color" id="ctaLetsTalkBgColor" name="colors[background_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($bgColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Color: Background Hover
|
||||
$bgHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_hover_color', '#FF6B35');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkBgHoverColor" class="form-label small mb-1 fw-semibold">Color de fondo (hover)</label>';
|
||||
$html .= ' <input type="color" id="ctaLetsTalkBgHoverColor" name="colors[background_hover_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($bgHoverColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Color: Text
|
||||
$textColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_color', '#FFFFFF');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkTextColor" class="form-label small mb-1 fw-semibold">Color del texto</label>';
|
||||
$html .= ' <input type="color" id="ctaLetsTalkTextColor" name="colors[text_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($textColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Color: Text Hover
|
||||
$textHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_hover_color', '#FFFFFF');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkTextHoverColor" class="form-label small mb-1 fw-semibold">Color del texto (hover)</label>';
|
||||
$html .= ' <input type="color" id="ctaLetsTalkTextHoverColor" name="colors[text_hover_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($textHoverColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Border Color (permite transparent)
|
||||
$borderColor = $this->renderer->getFieldValue($componentId, 'colors', 'border_color', 'transparent');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkBorderColor" class="form-label small mb-1 fw-semibold">Color del borde</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkBorderColor" name="colors[border_color]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderColor) . '" placeholder="transparent o #RRGGBB">';
|
||||
$html .= ' <small class="text-muted">Usa "transparent" para sin borde visible</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-angle-expand me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Text: Padding Top/Bottom
|
||||
$paddingTB = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_top_bottom', '0.5rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkPaddingTB" class="form-label small mb-1 fw-semibold">Padding vertical</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkPaddingTB" name="spacing[padding_top_bottom]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($paddingTB) . '" placeholder="0.5rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Padding Left/Right
|
||||
$paddingLR = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_left_right', '1.5rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkPaddingLR" class="form-label small mb-1 fw-semibold">Padding horizontal</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkPaddingLR" name="spacing[padding_left_right]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($paddingLR) . '" placeholder="1.5rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Margin Left
|
||||
$marginLeft = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_left', '1rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkMarginLeft" class="form-label small mb-1 fw-semibold">Margen izquierdo (desktop)</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkMarginLeft" name="spacing[margin_left]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginLeft) . '" placeholder="1rem">';
|
||||
$html .= ' <small class="text-muted">Separación del menú en pantallas ≥992px</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Icon Spacing
|
||||
$iconSpacing = $this->renderer->getFieldValue($componentId, 'spacing', 'icon_spacing', '0.5rem');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkIconSpacing" class="form-label small mb-1 fw-semibold">Espaciado del ícono</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkIconSpacing" name="spacing[icon_spacing]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($iconSpacing) . '" placeholder="0.5rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisualEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-stars me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Text: Border Radius
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '6px');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkBorderRadius" class="form-label small mb-1 fw-semibold">Radio de bordes</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkBorderRadius" name="visual_effects[border_radius]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '" placeholder="6px">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Border Width
|
||||
$borderWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_width', '0');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkBorderWidth" class="form-label small mb-1 fw-semibold">Grosor del borde</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkBorderWidth" name="visual_effects[border_width]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderWidth) . '" placeholder="0">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Box Shadow
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', 'none');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkBoxShadow" class="form-label small mb-1 fw-semibold">Sombra</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkBoxShadow" name="visual_effects[box_shadow]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '" placeholder="none">';
|
||||
$html .= ' <small class="text-muted">Ej: 0 2px 4px rgba(0,0,0,0.1)</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Transition Duration
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkTransition" class="form-label small mb-1 fw-semibold">Duración de transición</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkTransition" name="visual_effects[transition_duration]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '" placeholder="0.3s">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaPost\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para CTA Post
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class CtaPostFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'cta-post';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'ctaPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaPostHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'ctaPostVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'ctaPostVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'ctaPostVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'ctaPostVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'ctaPostVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'ctaPostExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'ctaPostExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'ctaPostExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'ctaPostExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'ctaPostTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||
'ctaPostDescription' => ['group' => 'content', 'attribute' => 'description'],
|
||||
'ctaPostButtonText' => ['group' => 'content', 'attribute' => 'button_text'],
|
||||
'ctaPostButtonUrl' => ['group' => 'content', 'attribute' => 'button_url'],
|
||||
'ctaPostButtonIcon' => ['group' => 'content', 'attribute' => 'button_icon'],
|
||||
|
||||
// Typography
|
||||
'ctaPostTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'],
|
||||
'ctaPostTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'],
|
||||
'ctaPostDescriptionFontSize' => ['group' => 'typography', 'attribute' => 'description_font_size'],
|
||||
'ctaPostButtonFontSize' => ['group' => 'typography', 'attribute' => 'button_font_size'],
|
||||
|
||||
// Colors
|
||||
'ctaPostGradientStart' => ['group' => 'colors', 'attribute' => 'gradient_start'],
|
||||
'ctaPostGradientEnd' => ['group' => 'colors', 'attribute' => 'gradient_end'],
|
||||
'ctaPostTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'ctaPostDescriptionColor' => ['group' => 'colors', 'attribute' => 'description_color'],
|
||||
'ctaPostButtonBgColor' => ['group' => 'colors', 'attribute' => 'button_bg_color'],
|
||||
'ctaPostButtonTextColor' => ['group' => 'colors', 'attribute' => 'button_text_color'],
|
||||
'ctaPostButtonHoverBg' => ['group' => 'colors', 'attribute' => 'button_hover_bg'],
|
||||
|
||||
// Spacing
|
||||
'ctaPostContainerMarginTop' => ['group' => 'spacing', 'attribute' => 'container_margin_top'],
|
||||
'ctaPostContainerMarginBottom' => ['group' => 'spacing', 'attribute' => 'container_margin_bottom'],
|
||||
'ctaPostContainerPadding' => ['group' => 'spacing', 'attribute' => 'container_padding'],
|
||||
'ctaPostTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||
'ctaPostButtonIconMargin' => ['group' => 'spacing', 'attribute' => 'button_icon_margin'],
|
||||
|
||||
// Visual Effects
|
||||
'ctaPostBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'ctaPostGradientAngle' => ['group' => 'visual_effects', 'attribute' => 'gradient_angle'],
|
||||
'ctaPostButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
|
||||
'ctaPostButtonPadding' => ['group' => 'visual_effects', 'attribute' => 'button_padding'],
|
||||
'ctaPostTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
'ctaPostBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
];
|
||||
}
|
||||
}
|
||||
505
Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php
Normal file
505
Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php
Normal file
@@ -0,0 +1,505 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaPost\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para CTA Post
|
||||
*
|
||||
* @package ROITheme\Admin\CtaPost\Infrastructure\Ui
|
||||
*/
|
||||
final class CtaPostFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de CTA Post';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' CTA promocional debajo del contenido del post';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="cta-post">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('ctaPostEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('ctaPostShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('ctaPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'ctaPost');
|
||||
|
||||
// Switch: Ocultar para usuarios logueados (Plan 99.16)
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($componentId, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaPostHideForLoggedIn" ';
|
||||
$html .= checked($hideForLoggedIn, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaPostHideForLoggedIn" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-person-lock me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Ocultar para usuarios logueados</strong>';
|
||||
$html .= ' <small class="text-muted d-block">No mostrar a usuarios con sesion iniciada</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Title
|
||||
$title = $this->renderer->getFieldValue($componentId, 'content', 'title', 'Accede a 200,000+ Analisis de Precios Unitarios');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaPostTitle" class="form-label small mb-1 fw-semibold">Titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaPostTitle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($title) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Description
|
||||
$description = $this->renderer->getFieldValue($componentId, 'content', 'description', '');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaPostDescription" class="form-label small mb-1 fw-semibold">Descripcion</label>';
|
||||
$html .= ' <textarea id="ctaPostDescription" class="form-control form-control-sm" rows="3">';
|
||||
$html .= esc_textarea($description);
|
||||
$html .= '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Button Text
|
||||
$buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', 'Ver Catalogo Completo');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaPostButtonText" class="form-label small mb-1 fw-semibold">Texto del boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonText) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Button URL
|
||||
$buttonUrl = $this->renderer->getFieldValue($componentId, 'content', 'button_url', '/catalogo');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaPostButtonUrl" class="form-label small mb-1 fw-semibold">URL del boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonUrl" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonUrl) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Button Icon
|
||||
$buttonIcon = $this->renderer->getFieldValue($componentId, 'content', 'button_icon', 'bi-arrow-right');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaPostButtonIcon" class="form-label small mb-1 fw-semibold">Icono del boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonIcon" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonIcon) . '" placeholder="bi-arrow-right">';
|
||||
$html .= ' <small class="text-muted">Clase de Bootstrap Icons</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostTitleFontSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaPostTitleFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '700');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostTitleFontWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaPostTitleFontWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$descFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'description_font_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostDescFontSize" class="form-label small mb-1 fw-semibold">Tamano descripcion</label>';
|
||||
$html .= ' <input type="text" id="ctaPostDescFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($descFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$buttonFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'button_font_size', '1.125rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostButtonFontSize" class="form-label small mb-1 fw-semibold">Tamano boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Gradiente
|
||||
$html .= ' <p class="small fw-semibold mb-2">Gradiente de fondo</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$gradientStart = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_start', '#FF8600');
|
||||
$html .= $this->buildColorPicker('ctaPostGradientStart', 'Inicio', $gradientStart);
|
||||
|
||||
$gradientEnd = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_end', '#FFB800');
|
||||
$html .= $this->buildColorPicker('ctaPostGradientEnd', 'Fin', $gradientEnd);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Textos
|
||||
$html .= ' <p class="small fw-semibold mb-2">Textos</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaPostTitleColor', 'Titulo', $titleColor);
|
||||
|
||||
$descColor = $this->renderer->getFieldValue($componentId, 'colors', 'description_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaPostDescColor', 'Descripcion', $descColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Boton
|
||||
$html .= ' <p class="small fw-semibold mb-2">Boton</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_bg_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaPostButtonBg', 'Fondo', $buttonBg);
|
||||
|
||||
$buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#212529');
|
||||
$html .= $this->buildColorPicker('ctaPostButtonTextColor', 'Texto', $buttonTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_bg', '#f8f9fa');
|
||||
$html .= $this->buildColorPicker('ctaPostButtonHoverBg', 'Hover', $buttonHoverBg);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_top', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
|
||||
$html .= ' <input type="text" id="ctaPostMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_bottom', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
|
||||
$html .= ' <input type="text" id="ctaPostMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$padding = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostPadding" class="form-label small mb-1 fw-semibold">Padding interno</label>';
|
||||
$html .= ' <input type="text" id="ctaPostPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($padding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleMargin = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostTitleMargin" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaPostTitleMargin" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleMargin) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '0.375rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostBorderRadius" class="form-label small mb-1 fw-semibold">Radio contenedor</label>';
|
||||
$html .= ' <input type="text" id="ctaPostBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$gradientAngle = $this->renderer->getFieldValue($componentId, 'visual_effects', 'gradient_angle', '135deg');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostGradientAngle" class="form-label small mb-1 fw-semibold">Angulo gradiente</label>';
|
||||
$html .= ' <input type="text" id="ctaPostGradientAngle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($gradientAngle) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '0.375rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostButtonRadius" class="form-label small mb-1 fw-semibold">Radio boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$buttonPadding = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_padding', '0.5rem 1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostButtonPadding" class="form-label small mb-1 fw-semibold">Padding boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$transition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostTransition" class="form-label small mb-1 fw-semibold">Transicion</label>';
|
||||
$html .= ' <input type="text" id="ctaPostTransition" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transition) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', 'none');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostBoxShadow" class="form-label small mb-1 fw-semibold">Sombra</label>';
|
||||
$html .= ' <input type="text" id="ctaPostBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Application\DTOs;
|
||||
|
||||
/**
|
||||
* DTO para solicitud de guardado de snippet
|
||||
*
|
||||
* Inmutable - una vez creado no puede modificarse.
|
||||
* Transporta datos desde Infrastructure (form) hacia Application (use case).
|
||||
*/
|
||||
final class SaveSnippetRequest
|
||||
{
|
||||
/**
|
||||
* @param string $id ID único del snippet (nuevo o existente)
|
||||
* @param string $name Nombre descriptivo
|
||||
* @param string $description Descripción opcional
|
||||
* @param string $css Código CSS
|
||||
* @param string $type Tipo de carga: 'critical' | 'deferred'
|
||||
* @param array<string> $pages Páginas donde aplicar: ['all'], ['home', 'posts'], etc.
|
||||
* @param bool $enabled Si el snippet está activo
|
||||
* @param int $order Orden de carga (menor = primero)
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $id,
|
||||
public readonly string $name,
|
||||
public readonly string $description,
|
||||
public readonly string $css,
|
||||
public readonly string $type,
|
||||
public readonly array $pages,
|
||||
public readonly bool $enabled,
|
||||
public readonly int $order
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Factory desde array (formulario o API)
|
||||
*
|
||||
* @param array $data Datos del formulario
|
||||
* @return self
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
id: $data['id'] ?? '',
|
||||
name: $data['name'] ?? '',
|
||||
description: $data['description'] ?? '',
|
||||
css: $data['css'] ?? '',
|
||||
type: $data['type'] ?? 'deferred',
|
||||
pages: $data['pages'] ?? ['all'],
|
||||
enabled: (bool)($data['enabled'] ?? true),
|
||||
order: (int)($data['order'] ?? 100)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte a array para persistencia
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'css' => $this->css,
|
||||
'type' => $this->type,
|
||||
'pages' => $this->pages,
|
||||
'enabled' => $this->enabled,
|
||||
'order' => $this->order,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Application\UseCases;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\CSSSnippetRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Caso de uso: Eliminar snippet CSS
|
||||
*
|
||||
* SRP: Solo responsable de orquestar la eliminación
|
||||
*/
|
||||
final class DeleteSnippetUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CSSSnippetRepositoryInterface $repository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Ejecuta la eliminación del snippet
|
||||
*
|
||||
* @param string $snippetId ID del snippet a eliminar
|
||||
* @return void
|
||||
*/
|
||||
public function execute(string $snippetId): void
|
||||
{
|
||||
$this->repository->delete($snippetId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Application\UseCases;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\CSSSnippetRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Caso de uso: Obtener todos los snippets (para Admin UI)
|
||||
*
|
||||
* SRP: Solo responsable de obtener lista completa
|
||||
*/
|
||||
final class GetAllSnippetsUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CSSSnippetRepositoryInterface $repository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Ejecuta la obtención de todos los snippets
|
||||
*
|
||||
* @return array<array> Lista de snippets ordenados por 'order'
|
||||
*/
|
||||
public function execute(): array
|
||||
{
|
||||
return $this->repository->getAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Application\UseCases;
|
||||
|
||||
use ROITheme\Admin\CustomCSSManager\Application\DTOs\SaveSnippetRequest;
|
||||
use ROITheme\Admin\CustomCSSManager\Domain\Entities\CSSSnippet;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSSnippetRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Caso de uso: Guardar snippet CSS
|
||||
*
|
||||
* SRP: Solo responsable de orquestar el guardado
|
||||
*/
|
||||
final class SaveSnippetUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CSSSnippetRepositoryInterface $repository
|
||||
) {}
|
||||
|
||||
public function execute(SaveSnippetRequest $request): void
|
||||
{
|
||||
// 1. Crear entidad desde DTO
|
||||
$snippet = CSSSnippet::fromArray($request->toArray());
|
||||
|
||||
// 2. Validar en dominio
|
||||
$snippet->validate();
|
||||
|
||||
// 3. Validar tamaño según tipo
|
||||
$snippet->css()->validateForLoadType($snippet->loadType());
|
||||
|
||||
// 4. Persistir
|
||||
$this->repository->save($snippet->toArray());
|
||||
}
|
||||
}
|
||||
115
Admin/CustomCSSManager/Domain/Entities/CSSSnippet.php
Normal file
115
Admin/CustomCSSManager/Domain/Entities/CSSSnippet.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Domain\Entities;
|
||||
|
||||
use ROITheme\Admin\CustomCSSManager\Domain\ValueObjects\SnippetId;
|
||||
use ROITheme\Admin\CustomCSSManager\Domain\ValueObjects\CSSCode;
|
||||
use ROITheme\Admin\CustomCSSManager\Domain\ValueObjects\LoadType;
|
||||
use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* Entidad de dominio para snippet CSS (contexto Admin)
|
||||
*
|
||||
* Responsabilidad: Reglas de negocio para ADMINISTRAR snippets
|
||||
*/
|
||||
final class CSSSnippet
|
||||
{
|
||||
private function __construct(
|
||||
private readonly SnippetId $id,
|
||||
private readonly string $name,
|
||||
private readonly string $description,
|
||||
private readonly CSSCode $css,
|
||||
private readonly LoadType $loadType,
|
||||
private readonly array $pages,
|
||||
private readonly bool $enabled,
|
||||
private readonly int $order
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Factory method desde array (BD)
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
SnippetId::fromString($data['id']),
|
||||
$data['name'],
|
||||
$data['description'] ?? '',
|
||||
CSSCode::fromString($data['css']),
|
||||
LoadType::fromString($data['type']),
|
||||
$data['pages'] ?? ['all'],
|
||||
$data['enabled'] ?? true,
|
||||
$data['order'] ?? 100
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida que el snippet pueda ser guardado
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function validate(): void
|
||||
{
|
||||
if (empty($this->name)) {
|
||||
throw new ValidationException('El nombre del snippet es requerido');
|
||||
}
|
||||
|
||||
if (strlen($this->name) > 100) {
|
||||
throw new ValidationException('El nombre no puede exceder 100 caracteres');
|
||||
}
|
||||
|
||||
// CSS ya validado en Value Object CSSCode
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte a array para persistencia
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id->value(),
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'css' => $this->css->value(),
|
||||
'type' => $this->loadType->value(),
|
||||
'pages' => $this->pages,
|
||||
'enabled' => $this->enabled,
|
||||
'order' => $this->order,
|
||||
];
|
||||
}
|
||||
|
||||
// Getters
|
||||
public function id(): SnippetId
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function css(): CSSCode
|
||||
{
|
||||
return $this->css;
|
||||
}
|
||||
|
||||
public function loadType(): LoadType
|
||||
{
|
||||
return $this->loadType;
|
||||
}
|
||||
|
||||
public function pages(): array
|
||||
{
|
||||
return $this->pages;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
public function order(): int
|
||||
{
|
||||
return $this->order;
|
||||
}
|
||||
}
|
||||
74
Admin/CustomCSSManager/Domain/ValueObjects/CSSCode.php
Normal file
74
Admin/CustomCSSManager/Domain/ValueObjects/CSSCode.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Domain\ValueObjects;
|
||||
|
||||
use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* Value Object para código CSS validado
|
||||
*/
|
||||
final class CSSCode
|
||||
{
|
||||
private const MAX_SIZE_CRITICAL = 14336; // 14KB para CSS crítico
|
||||
private const MAX_SIZE_DEFERRED = 102400; // 100KB para CSS diferido
|
||||
|
||||
private function __construct(
|
||||
private readonly string $value
|
||||
) {}
|
||||
|
||||
public static function fromString(string $css): self
|
||||
{
|
||||
$sanitized = self::sanitize($css);
|
||||
self::validate($sanitized);
|
||||
return new self($sanitized);
|
||||
}
|
||||
|
||||
private static function sanitize(string $css): string
|
||||
{
|
||||
// Eliminar etiquetas <style>
|
||||
$css = preg_replace('/<\/?style[^>]*>/i', '', $css);
|
||||
|
||||
// Eliminar comentarios HTML
|
||||
$css = preg_replace('/<!--.*?-->/s', '', $css);
|
||||
|
||||
return trim($css);
|
||||
}
|
||||
|
||||
private static function validate(string $css): void
|
||||
{
|
||||
// Detectar código potencialmente peligroso
|
||||
$dangerous = ['javascript:', 'expression(', '@import', 'behavior:'];
|
||||
foreach ($dangerous as $pattern) {
|
||||
if (stripos($css, $pattern) !== false) {
|
||||
throw new ValidationException("CSS contiene patrón no permitido: {$pattern}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function validateForLoadType(LoadType $loadType): void
|
||||
{
|
||||
$maxSize = $loadType->isCritical()
|
||||
? self::MAX_SIZE_CRITICAL
|
||||
: self::MAX_SIZE_DEFERRED;
|
||||
|
||||
if (strlen($this->value) > $maxSize) {
|
||||
throw new ValidationException(
|
||||
sprintf('CSS excede el tamaño máximo de %d bytes para tipo %s',
|
||||
$maxSize,
|
||||
$loadType->value()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function value(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->value);
|
||||
}
|
||||
}
|
||||
56
Admin/CustomCSSManager/Domain/ValueObjects/LoadType.php
Normal file
56
Admin/CustomCSSManager/Domain/ValueObjects/LoadType.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Domain\ValueObjects;
|
||||
|
||||
use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* Value Object para tipo de carga CSS
|
||||
*/
|
||||
final class LoadType
|
||||
{
|
||||
private const VALID_TYPES = ['critical', 'deferred'];
|
||||
|
||||
private function __construct(
|
||||
private readonly string $value
|
||||
) {}
|
||||
|
||||
public static function fromString(string $value): self
|
||||
{
|
||||
if (!in_array($value, self::VALID_TYPES, true)) {
|
||||
throw new ValidationException(
|
||||
sprintf('LoadType inválido: %s. Valores válidos: %s',
|
||||
$value,
|
||||
implode(', ', self::VALID_TYPES)
|
||||
)
|
||||
);
|
||||
}
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
public static function critical(): self
|
||||
{
|
||||
return new self('critical');
|
||||
}
|
||||
|
||||
public static function deferred(): self
|
||||
{
|
||||
return new self('deferred');
|
||||
}
|
||||
|
||||
public function isCritical(): bool
|
||||
{
|
||||
return $this->value === 'critical';
|
||||
}
|
||||
|
||||
public function isDeferred(): bool
|
||||
{
|
||||
return $this->value === 'deferred';
|
||||
}
|
||||
|
||||
public function value(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
121
Admin/CustomCSSManager/Domain/ValueObjects/SnippetId.php
Normal file
121
Admin/CustomCSSManager/Domain/ValueObjects/SnippetId.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Domain\ValueObjects;
|
||||
|
||||
use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* Value Object para ID único de snippet CSS
|
||||
*
|
||||
* Soporta dos formatos:
|
||||
* 1. Generado: css_[timestamp]_[random] (ej: "css_1701432000_a1b2c3")
|
||||
* 2. Legacy/Migración: kebab-case (ej: "cls-tables-apu", "generic-tables")
|
||||
*
|
||||
* Esto permite migrar snippets existentes sin romper IDs.
|
||||
*/
|
||||
final class SnippetId
|
||||
{
|
||||
private const PREFIX = 'css_';
|
||||
private const PATTERN_GENERATED = '/^css_[0-9]+_[a-z0-9]{6}$/';
|
||||
private const PATTERN_LEGACY = '/^[a-z0-9]+(-[a-z0-9]+)*$/';
|
||||
|
||||
private function __construct(
|
||||
private readonly string $value
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Crea SnippetId desde string existente (desde BD)
|
||||
*
|
||||
* Acepta tanto IDs generados (css_*) como IDs legacy (kebab-case).
|
||||
*
|
||||
* @param string $id ID existente
|
||||
* @return self
|
||||
* @throws ValidationException Si el formato es inválido
|
||||
*/
|
||||
public static function fromString(string $id): self
|
||||
{
|
||||
$id = trim($id);
|
||||
|
||||
if (empty($id)) {
|
||||
throw new ValidationException('El ID del snippet no puede estar vacío');
|
||||
}
|
||||
|
||||
if (strlen($id) > 50) {
|
||||
throw new ValidationException('El ID del snippet no puede exceder 50 caracteres');
|
||||
}
|
||||
|
||||
// Validar formato generado (css_*)
|
||||
if (str_starts_with($id, self::PREFIX)) {
|
||||
if (!preg_match(self::PATTERN_GENERATED, $id)) {
|
||||
throw new ValidationException(
|
||||
sprintf('Formato de ID generado inválido: %s. Esperado: css_[timestamp]_[random]', $id)
|
||||
);
|
||||
}
|
||||
return new self($id);
|
||||
}
|
||||
|
||||
// Validar formato legacy (kebab-case)
|
||||
if (!preg_match(self::PATTERN_LEGACY, $id)) {
|
||||
throw new ValidationException(
|
||||
sprintf('Formato de ID inválido: %s. Use kebab-case (ej: cls-tables-apu)', $id)
|
||||
);
|
||||
}
|
||||
|
||||
return new self($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera un nuevo SnippetId único
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function generate(): self
|
||||
{
|
||||
$timestamp = time();
|
||||
$random = bin2hex(random_bytes(3));
|
||||
|
||||
return new self(self::PREFIX . $timestamp . '_' . $random);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si es un ID generado (vs legacy)
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isGenerated(): bool
|
||||
{
|
||||
return str_starts_with($this->value, self::PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el valor del ID
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function value(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compara igualdad con otro SnippetId
|
||||
*
|
||||
* @param SnippetId $other
|
||||
* @return bool
|
||||
*/
|
||||
public function equals(SnippetId $other): bool
|
||||
{
|
||||
return $this->value === $other->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Representación string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Infrastructure\Persistence;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\CSSSnippetRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Repositorio WordPress para snippets CSS
|
||||
*
|
||||
* Almacena snippets como JSON en wp_roi_theme_component_settings
|
||||
*/
|
||||
final class WordPressSnippetRepository implements CSSSnippetRepositoryInterface
|
||||
{
|
||||
private const COMPONENT_NAME = 'custom-css-manager';
|
||||
private const GROUP_NAME = 'css_snippets';
|
||||
private const ATTRIBUTE_NAME = 'snippets_json';
|
||||
|
||||
public function __construct(
|
||||
private readonly \wpdb $wpdb
|
||||
) {}
|
||||
|
||||
public function getAll(): array
|
||||
{
|
||||
$tableName = $this->wpdb->prefix . 'roi_theme_component_settings';
|
||||
|
||||
$sql = $this->wpdb->prepare(
|
||||
"SELECT attribute_value FROM {$tableName}
|
||||
WHERE component_name = %s
|
||||
AND group_name = %s
|
||||
AND attribute_name = %s
|
||||
LIMIT 1",
|
||||
self::COMPONENT_NAME,
|
||||
self::GROUP_NAME,
|
||||
self::ATTRIBUTE_NAME
|
||||
);
|
||||
|
||||
$json = $this->wpdb->get_var($sql);
|
||||
|
||||
if (empty($json)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$snippets = json_decode($json, true);
|
||||
|
||||
return is_array($snippets) ? $snippets : [];
|
||||
}
|
||||
|
||||
public function getByLoadType(string $loadType): array
|
||||
{
|
||||
$all = $this->getAll();
|
||||
|
||||
$filtered = array_filter($all, function ($snippet) use ($loadType) {
|
||||
return ($snippet['type'] ?? '') === $loadType
|
||||
&& ($snippet['enabled'] ?? false) === true;
|
||||
});
|
||||
|
||||
// Reindexar para evitar keys dispersas [0,2,5] → [0,1,2]
|
||||
return array_values($filtered);
|
||||
}
|
||||
|
||||
public function getForPage(string $loadType, string $pageType): array
|
||||
{
|
||||
$snippets = $this->getByLoadType($loadType);
|
||||
|
||||
$filtered = array_filter($snippets, function ($snippet) use ($pageType) {
|
||||
$pages = $snippet['pages'] ?? ['all'];
|
||||
return in_array('all', $pages, true)
|
||||
|| in_array($pageType, $pages, true);
|
||||
});
|
||||
|
||||
// Reindexar para evitar keys dispersas
|
||||
return array_values($filtered);
|
||||
}
|
||||
|
||||
public function save(array $snippet): void
|
||||
{
|
||||
$all = $this->getAll();
|
||||
|
||||
// Actualizar o agregar
|
||||
$found = false;
|
||||
foreach ($all as &$existing) {
|
||||
if ($existing['id'] === $snippet['id']) {
|
||||
$existing = $snippet;
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
$all[] = $snippet;
|
||||
}
|
||||
|
||||
// Ordenar por 'order'
|
||||
usort($all, fn($a, $b) => ($a['order'] ?? 100) <=> ($b['order'] ?? 100));
|
||||
|
||||
$this->persist($all);
|
||||
}
|
||||
|
||||
public function delete(string $snippetId): void
|
||||
{
|
||||
$all = $this->getAll();
|
||||
|
||||
$filtered = array_filter($all, fn($s) => $s['id'] !== $snippetId);
|
||||
|
||||
$this->persist(array_values($filtered));
|
||||
}
|
||||
|
||||
/**
|
||||
* Persiste la lista de snippets en BD
|
||||
*
|
||||
* Usa update() + insert() para consistencia con patrón existente.
|
||||
* NOTA: NO usa replace() porque:
|
||||
* - Preserva ID autoincremental
|
||||
* - Preserva campos como is_editable, created_at
|
||||
*
|
||||
* @param array $snippets Lista de snippets a persistir
|
||||
*/
|
||||
private function persist(array $snippets): void
|
||||
{
|
||||
$tableName = $this->wpdb->prefix . 'roi_theme_component_settings';
|
||||
$json = wp_json_encode($snippets, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
// Verificar si el registro existe
|
||||
$exists = $this->wpdb->get_var($this->wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$tableName}
|
||||
WHERE component_name = %s
|
||||
AND group_name = %s
|
||||
AND attribute_name = %s",
|
||||
self::COMPONENT_NAME,
|
||||
self::GROUP_NAME,
|
||||
self::ATTRIBUTE_NAME
|
||||
));
|
||||
|
||||
if ($exists > 0) {
|
||||
// UPDATE existente (preserva id, created_at, is_editable)
|
||||
$this->wpdb->update(
|
||||
$tableName,
|
||||
['attribute_value' => $json],
|
||||
[
|
||||
'component_name' => self::COMPONENT_NAME,
|
||||
'group_name' => self::GROUP_NAME,
|
||||
'attribute_name' => self::ATTRIBUTE_NAME,
|
||||
],
|
||||
['%s'],
|
||||
['%s', '%s', '%s']
|
||||
);
|
||||
} else {
|
||||
// INSERT nuevo
|
||||
$this->wpdb->insert(
|
||||
$tableName,
|
||||
[
|
||||
'component_name' => self::COMPONENT_NAME,
|
||||
'group_name' => self::GROUP_NAME,
|
||||
'attribute_name' => self::ATTRIBUTE_NAME,
|
||||
'attribute_value' => $json,
|
||||
'is_editable' => 1,
|
||||
],
|
||||
['%s', '%s', '%s', '%s', '%d']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\CustomCSSManager\Infrastructure\Persistence\WordPressSnippetRepository;
|
||||
use ROITheme\Admin\CustomCSSManager\Application\UseCases\SaveSnippetUseCase;
|
||||
use ROITheme\Admin\CustomCSSManager\Application\UseCases\DeleteSnippetUseCase;
|
||||
use ROITheme\Admin\CustomCSSManager\Application\UseCases\GetAllSnippetsUseCase;
|
||||
use ROITheme\Admin\CustomCSSManager\Application\DTOs\SaveSnippetRequest;
|
||||
use ROITheme\Admin\CustomCSSManager\Domain\ValueObjects\SnippetId;
|
||||
use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* FormBuilder para gestión de CSS snippets en Admin Panel
|
||||
*
|
||||
* Sigue el patrón estándar de FormBuilders del tema:
|
||||
* - Constructor recibe AdminDashboardRenderer
|
||||
* - Método buildForm() genera el HTML del formulario
|
||||
*
|
||||
* Design System: Gradiente navy #0E2337 → #1e3a5f, accent #FF8600
|
||||
*/
|
||||
final class CustomCSSManagerFormBuilder
|
||||
{
|
||||
private const COMPONENT_ID = 'custom-css-manager';
|
||||
private const NONCE_ACTION = 'roi_custom_css_manager';
|
||||
|
||||
private WordPressSnippetRepository $repository;
|
||||
private GetAllSnippetsUseCase $getAllUseCase;
|
||||
private SaveSnippetUseCase $saveUseCase;
|
||||
private DeleteSnippetUseCase $deleteUseCase;
|
||||
|
||||
public function __construct(
|
||||
private readonly AdminDashboardRenderer $renderer
|
||||
) {
|
||||
// Crear repositorio y Use Cases internamente
|
||||
global $wpdb;
|
||||
$this->repository = new WordPressSnippetRepository($wpdb);
|
||||
$this->getAllUseCase = new GetAllSnippetsUseCase($this->repository);
|
||||
$this->saveUseCase = new SaveSnippetUseCase($this->repository);
|
||||
$this->deleteUseCase = new DeleteSnippetUseCase($this->repository);
|
||||
|
||||
// Registrar handler de formulario POST
|
||||
$this->registerFormHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra handler para procesar formularios POST
|
||||
*/
|
||||
private function registerFormHandler(): void
|
||||
{
|
||||
// Solo registrar una vez
|
||||
static $registered = false;
|
||||
if ($registered) {
|
||||
return;
|
||||
}
|
||||
$registered = true;
|
||||
|
||||
add_action('admin_init', function() {
|
||||
$this->handleFormSubmission();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesa envío de formulario
|
||||
*/
|
||||
public function handleFormSubmission(): void
|
||||
{
|
||||
if (!isset($_POST['roi_css_action'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar nonce
|
||||
if (!wp_verify_nonce($_POST['_wpnonce'] ?? '', self::NONCE_ACTION)) {
|
||||
wp_die('Nonce verification failed');
|
||||
}
|
||||
|
||||
// Verificar permisos
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_die('Insufficient permissions');
|
||||
}
|
||||
|
||||
$action = sanitize_text_field($_POST['roi_css_action']);
|
||||
|
||||
try {
|
||||
match ($action) {
|
||||
'save' => $this->processSave($_POST),
|
||||
'delete' => $this->processDelete($_POST),
|
||||
default => null,
|
||||
};
|
||||
|
||||
// Redirect con mensaje de éxito
|
||||
wp_redirect(add_query_arg('roi_message', 'success', wp_get_referer()));
|
||||
exit;
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
// Redirect con mensaje de error
|
||||
wp_redirect(add_query_arg([
|
||||
'roi_message' => 'error',
|
||||
'roi_error' => urlencode($e->getMessage())
|
||||
], wp_get_referer()));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesa guardado de snippet
|
||||
*/
|
||||
private function processSave(array $data): void
|
||||
{
|
||||
$id = sanitize_text_field($data['snippet_id'] ?? '');
|
||||
|
||||
// Generar ID si es nuevo
|
||||
if (empty($id)) {
|
||||
$id = SnippetId::generate()->value();
|
||||
}
|
||||
|
||||
$request = SaveSnippetRequest::fromArray([
|
||||
'id' => $id,
|
||||
'name' => sanitize_text_field($data['snippet_name'] ?? ''),
|
||||
'description' => sanitize_textarea_field($data['snippet_description'] ?? ''),
|
||||
'css' => wp_strip_all_tags($data['snippet_css'] ?? ''),
|
||||
'type' => sanitize_text_field($data['snippet_type'] ?? 'deferred'),
|
||||
'pages' => array_map('sanitize_text_field', $data['snippet_pages'] ?? ['all']),
|
||||
'enabled' => isset($data['snippet_enabled']),
|
||||
'order' => absint($data['snippet_order'] ?? 100),
|
||||
]);
|
||||
|
||||
$this->saveUseCase->execute($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesa eliminación de snippet
|
||||
*/
|
||||
private function processDelete(array $data): void
|
||||
{
|
||||
$id = sanitize_text_field($data['snippet_id'] ?? '');
|
||||
|
||||
if (empty($id)) {
|
||||
throw new ValidationException('ID de snippet requerido para eliminar');
|
||||
}
|
||||
|
||||
$this->deleteUseCase->execute($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye el formulario del componente
|
||||
*
|
||||
* @param string $componentId ID del componente (custom-css-manager)
|
||||
* @return string HTML del formulario
|
||||
*/
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$snippets = $this->getAllUseCase->execute();
|
||||
$message = $this->getFlashMessage();
|
||||
|
||||
$html = '';
|
||||
|
||||
// Header
|
||||
$html .= $this->buildHeader($componentId, count($snippets));
|
||||
|
||||
// Mensajes flash
|
||||
if ($message) {
|
||||
$html .= sprintf(
|
||||
'<div class="alert alert-%s m-3">%s</div>',
|
||||
esc_attr($message['type']),
|
||||
esc_html($message['text'])
|
||||
);
|
||||
}
|
||||
|
||||
// Lista de snippets existentes
|
||||
$html .= $this->buildSnippetsList($snippets);
|
||||
|
||||
// Formulario de creación/edición
|
||||
$html .= $this->buildSnippetForm();
|
||||
|
||||
// JavaScript
|
||||
$html .= $this->buildJavaScript();
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye el header del componente
|
||||
*/
|
||||
private function buildHeader(string $componentId, int $snippetCount): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-4" style="border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="card-header d-flex justify-content-between align-items-center" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%);">';
|
||||
$html .= ' <h3 class="mb-0 text-white"><i class="bi bi-file-earmark-code me-2"></i>Gestor de CSS Personalizado</h3>';
|
||||
$html .= ' <span class="badge bg-light text-dark">' . esc_html($snippetCount) . ' snippet(s)</span>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <p class="text-muted mb-0">Gestiona snippets de CSS personalizados. Los snippets críticos se cargan en el head, los diferidos en el footer.</p>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye la lista de snippets existentes
|
||||
*/
|
||||
private function buildSnippetsList(array $snippets): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-4" style="border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-list-ul me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Snippets Configurados';
|
||||
$html .= ' </h5>';
|
||||
|
||||
if (empty($snippets)) {
|
||||
$html .= ' <p class="text-muted">No hay snippets configurados.</p>';
|
||||
} else {
|
||||
$html .= ' <div class="table-responsive">';
|
||||
$html .= ' <table class="table table-hover">';
|
||||
$html .= ' <thead>';
|
||||
$html .= ' <tr>';
|
||||
$html .= ' <th>Nombre</th>';
|
||||
$html .= ' <th>Tipo</th>';
|
||||
$html .= ' <th>Páginas</th>';
|
||||
$html .= ' <th>Estado</th>';
|
||||
$html .= ' <th>Acciones</th>';
|
||||
$html .= ' </tr>';
|
||||
$html .= ' </thead>';
|
||||
$html .= ' <tbody>';
|
||||
foreach ($snippets as $snippet) {
|
||||
$html .= $this->renderSnippetRow($snippet);
|
||||
}
|
||||
$html .= ' </tbody>';
|
||||
$html .= ' </table>';
|
||||
$html .= ' </div>';
|
||||
}
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderiza una fila de snippet en la tabla
|
||||
*/
|
||||
private function renderSnippetRow(array $snippet): string
|
||||
{
|
||||
$id = esc_attr($snippet['id']);
|
||||
$name = esc_html($snippet['name']);
|
||||
$type = $snippet['type'] === 'critical' ? 'Crítico' : 'Diferido';
|
||||
$typeBadge = $snippet['type'] === 'critical' ? 'bg-danger' : 'bg-info';
|
||||
$pages = implode(', ', $snippet['pages'] ?? ['all']);
|
||||
$enabled = ($snippet['enabled'] ?? false) ? 'Activo' : 'Inactivo';
|
||||
$enabledBadge = ($snippet['enabled'] ?? false) ? 'bg-success' : 'bg-secondary';
|
||||
|
||||
// Usar data-attribute para JSON seguro
|
||||
$snippetJson = esc_attr(wp_json_encode($snippet, JSON_HEX_APOS | JSON_HEX_QUOT));
|
||||
$nonce = wp_create_nonce(self::NONCE_ACTION);
|
||||
|
||||
return <<<HTML
|
||||
<tr>
|
||||
<td><strong>{$name}</strong></td>
|
||||
<td><span class="badge {$typeBadge}">{$type}</span></td>
|
||||
<td><small>{$pages}</small></td>
|
||||
<td><span class="badge {$enabledBadge}">{$enabled}</span></td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary btn-edit-snippet"
|
||||
data-snippet="{$snippetJson}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<form method="post" class="d-inline"
|
||||
onsubmit="return confirm('¿Eliminar este snippet?');">
|
||||
<input type="hidden" name="_wpnonce" value="{$nonce}">
|
||||
<input type="hidden" name="roi_css_action" value="delete">
|
||||
<input type="hidden" name="snippet_id" value="{$id}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye el formulario de creación/edición de snippets
|
||||
*/
|
||||
private function buildSnippetForm(): string
|
||||
{
|
||||
$nonce = wp_create_nonce(self::NONCE_ACTION);
|
||||
|
||||
$html = '<div class="card shadow-sm mb-4" style="border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-plus-circle me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Agregar/Editar Snippet';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <form method="post" id="roi-snippet-form">';
|
||||
$html .= ' <input type="hidden" name="_wpnonce" value="' . esc_attr($nonce) . '">';
|
||||
$html .= ' <input type="hidden" name="roi_css_action" value="save">';
|
||||
$html .= ' <input type="hidden" name="snippet_id" id="snippet_id" value="">';
|
||||
|
||||
$html .= ' <div class="row g-3">';
|
||||
|
||||
// Nombre
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$html .= ' <label class="form-label">Nombre *</label>';
|
||||
$html .= ' <input type="text" name="snippet_name" id="snippet_name" class="form-control" required maxlength="100" placeholder="Ej: Estilos Tablas APU">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Tipo
|
||||
$html .= ' <div class="col-md-3">';
|
||||
$html .= ' <label class="form-label">Tipo *</label>';
|
||||
$html .= ' <select name="snippet_type" id="snippet_type" class="form-select">';
|
||||
$html .= ' <option value="critical">Crítico (head)</option>';
|
||||
$html .= ' <option value="deferred" selected>Diferido (footer)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Orden
|
||||
$html .= ' <div class="col-md-3">';
|
||||
$html .= ' <label class="form-label">Orden</label>';
|
||||
$html .= ' <input type="number" name="snippet_order" id="snippet_order" class="form-control" value="100" min="1" max="999">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Descripción
|
||||
$html .= ' <div class="col-12">';
|
||||
$html .= ' <label class="form-label">Descripción</label>';
|
||||
$html .= ' <input type="text" name="snippet_description" id="snippet_description" class="form-control" maxlength="255" placeholder="Descripción breve del propósito del CSS">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Páginas
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$html .= ' <label class="form-label">Aplicar en páginas</label>';
|
||||
$html .= ' <div class="d-flex flex-wrap gap-2">';
|
||||
foreach ($this->getPageOptions() as $value => $label) {
|
||||
$checked = $value === 'all' ? 'checked' : '';
|
||||
$html .= sprintf(
|
||||
'<div class="form-check"><input type="checkbox" name="snippet_pages[]" value="%s" id="page_%s" class="form-check-input" %s><label class="form-check-label" for="page_%s">%s</label></div>',
|
||||
esc_attr($value),
|
||||
esc_attr($value),
|
||||
$checked,
|
||||
esc_attr($value),
|
||||
esc_html($label)
|
||||
);
|
||||
}
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Estado
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$html .= ' <label class="form-label">Estado</label>';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input type="checkbox" name="snippet_enabled" id="snippet_enabled" class="form-check-input" checked>';
|
||||
$html .= ' <label class="form-check-label" for="snippet_enabled">Habilitado</label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Código CSS
|
||||
$html .= ' <div class="col-12">';
|
||||
$html .= ' <label class="form-label">Código CSS *</label>';
|
||||
$html .= ' <textarea name="snippet_css" id="snippet_css" class="form-control font-monospace" rows="10" required placeholder="/* Tu CSS aquí */"></textarea>';
|
||||
$html .= ' <small class="text-muted">Crítico: máx 14KB | Diferido: máx 100KB</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Botones
|
||||
$html .= ' <div class="col-12">';
|
||||
$html .= ' <button type="submit" class="btn text-white" style="background-color: #FF8600;">';
|
||||
$html .= ' <i class="bi bi-save me-1"></i> Guardar Snippet';
|
||||
$html .= ' </button>';
|
||||
$html .= ' <button type="button" class="btn btn-secondary" onclick="resetCssForm()">';
|
||||
$html .= ' <i class="bi bi-x-circle me-1"></i> Cancelar';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= ' </form>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera el JavaScript necesario para el formulario
|
||||
*/
|
||||
private function buildJavaScript(): string
|
||||
{
|
||||
return <<<JS
|
||||
<script>
|
||||
// Event delegation para botones de edición
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('.btn-edit-snippet').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
const snippet = JSON.parse(this.dataset.snippet);
|
||||
editCssSnippet(snippet);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function editCssSnippet(snippet) {
|
||||
document.getElementById('snippet_id').value = snippet.id;
|
||||
document.getElementById('snippet_name').value = snippet.name;
|
||||
document.getElementById('snippet_description').value = snippet.description || '';
|
||||
document.getElementById('snippet_type').value = snippet.type;
|
||||
document.getElementById('snippet_order').value = snippet.order || 100;
|
||||
document.getElementById('snippet_css').value = snippet.css;
|
||||
document.getElementById('snippet_enabled').checked = snippet.enabled;
|
||||
|
||||
// Actualizar checkboxes de páginas
|
||||
document.querySelectorAll('input[name="snippet_pages[]"]').forEach(cb => {
|
||||
cb.checked = (snippet.pages || ['all']).includes(cb.value);
|
||||
});
|
||||
|
||||
document.getElementById('snippet_name').focus();
|
||||
|
||||
// Scroll al formulario
|
||||
document.getElementById('roi-snippet-form').scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
function resetCssForm() {
|
||||
document.getElementById('roi-snippet-form').reset();
|
||||
document.getElementById('snippet_id').value = '';
|
||||
}
|
||||
</script>
|
||||
JS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opciones de páginas disponibles
|
||||
*/
|
||||
private function getPageOptions(): array
|
||||
{
|
||||
return [
|
||||
'all' => 'Todas',
|
||||
'home' => 'Inicio',
|
||||
'posts' => 'Posts',
|
||||
'pages' => 'Páginas',
|
||||
'archives' => 'Archivos',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene mensaje flash de la URL
|
||||
*/
|
||||
private function getFlashMessage(): ?array
|
||||
{
|
||||
$message = $_GET['roi_message'] ?? null;
|
||||
|
||||
if ($message === 'success') {
|
||||
return ['type' => 'success', 'text' => 'Snippet guardado correctamente'];
|
||||
}
|
||||
|
||||
if ($message === 'error') {
|
||||
$error = urldecode($_GET['roi_error'] ?? 'Error desconocido');
|
||||
return ['type' => 'danger', 'text' => $error];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
48
Admin/Domain/Contracts/ComponentTabInterface.php
Normal file
48
Admin/Domain/Contracts/ComponentTabInterface.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Contrato para tabs de componentes en el dashboard
|
||||
*
|
||||
* Domain - Lógica pura sin WordPress
|
||||
*/
|
||||
interface ComponentTabInterface
|
||||
{
|
||||
/**
|
||||
* Obtiene el ID del componente
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): string;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre visible del tab
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLabel(): string;
|
||||
|
||||
/**
|
||||
* Obtiene el ícono del tab
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getIcon(): string;
|
||||
|
||||
/**
|
||||
* Renderiza el contenido del tab
|
||||
*
|
||||
* @return string HTML del contenido
|
||||
*/
|
||||
public function renderContent(): string;
|
||||
|
||||
/**
|
||||
* Verifica si el tab está activo
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isActive(): bool;
|
||||
}
|
||||
28
Admin/Domain/Contracts/DashboardRendererInterface.php
Normal file
28
Admin/Domain/Contracts/DashboardRendererInterface.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Contrato para renderizar el dashboard del panel de administración
|
||||
*
|
||||
* Domain - Lógica pura sin WordPress
|
||||
*/
|
||||
interface DashboardRendererInterface
|
||||
{
|
||||
/**
|
||||
* Renderiza el dashboard completo
|
||||
*
|
||||
* @return string HTML del dashboard
|
||||
*/
|
||||
public function render(): string;
|
||||
|
||||
/**
|
||||
* Verifica si el renderizador soporta un tipo de vista
|
||||
*
|
||||
* @param string $viewType Tipo de vista (dashboard, settings, etc.)
|
||||
* @return bool
|
||||
*/
|
||||
public function supports(string $viewType): bool;
|
||||
}
|
||||
34
Admin/Domain/Contracts/MenuRegistrarInterface.php
Normal file
34
Admin/Domain/Contracts/MenuRegistrarInterface.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Contrato para registrar el menú de administración
|
||||
*
|
||||
* Domain - Lógica pura sin WordPress
|
||||
*/
|
||||
interface MenuRegistrarInterface
|
||||
{
|
||||
/**
|
||||
* Registra el menú en el sistema de administración
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void;
|
||||
|
||||
/**
|
||||
* Obtiene la capacidad requerida para acceder al menú
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCapability(): string;
|
||||
|
||||
/**
|
||||
* Obtiene el slug del menú
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSlug(): string;
|
||||
}
|
||||
85
Admin/Domain/ValueObjects/MenuItem.php
Normal file
85
Admin/Domain/ValueObjects/MenuItem.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Domain\ValueObjects;
|
||||
|
||||
/**
|
||||
* Value Object para representar un item del menú
|
||||
*
|
||||
* Domain - Objeto inmutable sin WordPress
|
||||
*/
|
||||
final class MenuItem
|
||||
{
|
||||
/**
|
||||
* @param string $pageTitle Título de la página
|
||||
* @param string $menuTitle Título del menú
|
||||
* @param string $capability Capacidad requerida
|
||||
* @param string $menuSlug Slug del menú
|
||||
* @param string $icon Ícono del menú
|
||||
* @param int $position Posición en el menú
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $pageTitle,
|
||||
private readonly string $menuTitle,
|
||||
private readonly string $capability,
|
||||
private readonly string $menuSlug,
|
||||
private readonly string $icon,
|
||||
private readonly int $position
|
||||
) {
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
private function validate(): void
|
||||
{
|
||||
if (empty($this->pageTitle)) {
|
||||
throw new \InvalidArgumentException('Page title cannot be empty');
|
||||
}
|
||||
|
||||
if (empty($this->menuTitle)) {
|
||||
throw new \InvalidArgumentException('Menu title cannot be empty');
|
||||
}
|
||||
|
||||
if (empty($this->capability)) {
|
||||
throw new \InvalidArgumentException('Capability cannot be empty');
|
||||
}
|
||||
|
||||
if (empty($this->menuSlug)) {
|
||||
throw new \InvalidArgumentException('Menu slug cannot be empty');
|
||||
}
|
||||
|
||||
if ($this->position < 0) {
|
||||
throw new \InvalidArgumentException('Position must be >= 0');
|
||||
}
|
||||
}
|
||||
|
||||
public function getPageTitle(): string
|
||||
{
|
||||
return $this->pageTitle;
|
||||
}
|
||||
|
||||
public function getMenuTitle(): string
|
||||
{
|
||||
return $this->menuTitle;
|
||||
}
|
||||
|
||||
public function getCapability(): string
|
||||
{
|
||||
return $this->capability;
|
||||
}
|
||||
|
||||
public function getMenuSlug(): string
|
||||
{
|
||||
return $this->menuSlug;
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\FeaturedImage\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Featured Image
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class FeaturedImageFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'featured-image';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'featuredImageEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'featuredImageShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'featuredImageShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'featuredImageVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'featuredImageVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'featuredImageVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'featuredImageVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'featuredImageVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'featuredImageExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'featuredImageExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'featuredImageExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'featuredImageExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'featuredImageSize' => ['group' => 'content', 'attribute' => 'image_size'],
|
||||
'featuredImageLazyLoading' => ['group' => 'content', 'attribute' => 'lazy_loading'],
|
||||
'featuredImageLinkToMedia' => ['group' => 'content', 'attribute' => 'link_to_media'],
|
||||
|
||||
// Spacing
|
||||
'featuredImageMarginTop' => ['group' => 'spacing', 'attribute' => 'margin_top'],
|
||||
'featuredImageMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'],
|
||||
|
||||
// Visual Effects
|
||||
'featuredImageBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'featuredImageBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
'featuredImageHoverEffect' => ['group' => 'visual_effects', 'attribute' => 'hover_effect'],
|
||||
'featuredImageHoverScale' => ['group' => 'visual_effects', 'attribute' => 'hover_scale'],
|
||||
'featuredImageTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\FeaturedImage\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
final class FeaturedImageFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-image me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Imagen Destacada';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Personaliza la imagen destacada de los posts';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="featured-image">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageEnabled" ';
|
||||
$html .= checked($enabled, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageEnabled">';
|
||||
$html .= ' <i class="bi bi-power me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar imagen destacada</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageShowOnDesktop" ';
|
||||
$html .= checked($showOnDesktop, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageShowOnDesktop">';
|
||||
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Desktop</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageShowOnMobile" ';
|
||||
$html .= checked($showOnMobile, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageShowOnMobile">';
|
||||
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Mobile</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'featuredImage');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-image me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$imageSize = $this->renderer->getFieldValue($componentId, 'content', 'image_size', 'roi-featured-large');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="featuredImageSize" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-aspect-ratio me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tamano de imagen';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="featuredImageSize" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="roi-featured-large" ' . selected($imageSize, 'roi-featured-large', false) . '>Grande (1200x600)</option>';
|
||||
$html .= ' <option value="roi-featured-medium" ' . selected($imageSize, 'roi-featured-medium', false) . '>Mediano (800x400)</option>';
|
||||
$html .= ' <option value="full" ' . selected($imageSize, 'full', false) . '>Original (tamano completo)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$lazyLoading = $this->renderer->getFieldValue($componentId, 'content', 'lazy_loading', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageLazyLoading" ';
|
||||
$html .= checked($lazyLoading, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageLazyLoading">';
|
||||
$html .= ' <i class="bi bi-lightning me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Carga diferida (lazy loading)</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <small class="text-muted">Mejora rendimiento cargando imagen cuando es visible</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$linkToMedia = $this->renderer->getFieldValue($componentId, 'content', 'link_to_media', false);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageLinkToMedia" ';
|
||||
$html .= checked($linkToMedia, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageLinkToMedia">';
|
||||
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Enlazar a imagen completa</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <small class="text-muted">Abre la imagen en tamano completo al hacer clic</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_top', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="featuredImageMarginTop" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Margen superior';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginTop) . '" placeholder="1rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '2rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="featuredImageMarginBottom" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Margen inferior';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginBottom) . '" placeholder="2rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '12px');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="featuredImageBorderRadius" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Radio de bordes';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '" placeholder="12px">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 8px 24px rgba(0, 0, 0, 0.1)');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="featuredImageBoxShadow" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Sombra';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$hoverEffect = $this->renderer->getFieldValue($componentId, 'visual_effects', 'hover_effect', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageHoverEffect" ';
|
||||
$html .= checked($hoverEffect, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageHoverEffect">';
|
||||
$html .= ' <i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Efecto hover</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <small class="text-muted">Aplica efecto de escala sutil al pasar el mouse</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$hoverScale = $this->renderer->getFieldValue($componentId, 'visual_effects', 'hover_scale', '1.02');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="featuredImageHoverScale" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Escala en hover';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageHoverScale" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($hoverScale) . '" placeholder="1.02">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="featuredImageTransitionDuration" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Duracion transicion';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageTransitionDuration" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '" placeholder="0.3s">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Footer\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Footer
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class FooterFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'footer';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'footerEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'footerShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'footerShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'footerVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'footerVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'footerVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'footerVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'footerVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'footerExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'footerExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'footerExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'footerExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Widget 1
|
||||
'footerWidget1Visible' => ['group' => 'widget_1', 'attribute' => 'widget_1_visible'],
|
||||
'footerWidget1Title' => ['group' => 'widget_1', 'attribute' => 'widget_1_title'],
|
||||
|
||||
// Widget 2
|
||||
'footerWidget2Visible' => ['group' => 'widget_2', 'attribute' => 'widget_2_visible'],
|
||||
'footerWidget2Title' => ['group' => 'widget_2', 'attribute' => 'widget_2_title'],
|
||||
|
||||
// Widget 3
|
||||
'footerWidget3Visible' => ['group' => 'widget_3', 'attribute' => 'widget_3_visible'],
|
||||
'footerWidget3Title' => ['group' => 'widget_3', 'attribute' => 'widget_3_title'],
|
||||
|
||||
// Newsletter
|
||||
'footerNewsletterVisible' => ['group' => 'newsletter', 'attribute' => 'newsletter_visible'],
|
||||
'footerNewsletterTitle' => ['group' => 'newsletter', 'attribute' => 'newsletter_title'],
|
||||
'footerNewsletterDescription' => ['group' => 'newsletter', 'attribute' => 'newsletter_description'],
|
||||
'footerNewsletterPlaceholder' => ['group' => 'newsletter', 'attribute' => 'newsletter_email_placeholder'],
|
||||
'footerNewsletterButtonText' => ['group' => 'newsletter', 'attribute' => 'newsletter_button_text'],
|
||||
'footerNewsletterWebhookUrl' => ['group' => 'newsletter', 'attribute' => 'newsletter_webhook_url'],
|
||||
'footerNewsletterSuccessMessage' => ['group' => 'newsletter', 'attribute' => 'newsletter_success_message'],
|
||||
'footerNewsletterErrorMessage' => ['group' => 'newsletter', 'attribute' => 'newsletter_error_message'],
|
||||
|
||||
// Footer Bottom
|
||||
'footerCopyrightText' => ['group' => 'footer_bottom', 'attribute' => 'copyright_text'],
|
||||
|
||||
// Colors
|
||||
'footerBgColor' => ['group' => 'colors', 'attribute' => 'bg_color'],
|
||||
'footerTextColor' => ['group' => 'colors', 'attribute' => 'text_color'],
|
||||
'footerTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'footerLinkColor' => ['group' => 'colors', 'attribute' => 'link_color'],
|
||||
'footerLinkHoverColor' => ['group' => 'colors', 'attribute' => 'link_hover_color'],
|
||||
'footerButtonBgColor' => ['group' => 'colors', 'attribute' => 'button_bg_color'],
|
||||
'footerButtonTextColor' => ['group' => 'colors', 'attribute' => 'button_text_color'],
|
||||
'footerButtonHoverBg' => ['group' => 'colors', 'attribute' => 'button_hover_bg'],
|
||||
|
||||
// Spacing
|
||||
'footerPaddingY' => ['group' => 'spacing', 'attribute' => 'padding_y'],
|
||||
'footerMarginTop' => ['group' => 'spacing', 'attribute' => 'margin_top'],
|
||||
|
||||
// Visual Effects
|
||||
'footerInputBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'input_border_radius'],
|
||||
'footerButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
|
||||
'footerTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
];
|
||||
}
|
||||
}
|
||||
470
Admin/Footer/Infrastructure/Ui/FooterFormBuilder.php
Normal file
470
Admin/Footer/Infrastructure/Ui/FooterFormBuilder.php
Normal file
@@ -0,0 +1,470 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Footer\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Footer
|
||||
*
|
||||
* RESPONSABILIDAD: Generar formulario de configuracion del Footer
|
||||
*
|
||||
* @package ROITheme\Admin\Footer\Infrastructure\Ui
|
||||
*/
|
||||
final class FooterFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildWidget1Group($componentId);
|
||||
$html .= $this->buildWidget2Group($componentId);
|
||||
$html .= $this->buildWidget3Group($componentId);
|
||||
$html .= $this->buildNewsletterGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildFooterBottomGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-layout-text-window-reverse me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Footer';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Footer con menus de navegacion y newsletter';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="footer">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('footerEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('footerShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('footerShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', true);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', true);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'footer');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildWidget1Group(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-list-ul me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Widget 1 (Menu)';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$visible = $this->renderer->getFieldValue($componentId, 'widget_1', 'widget_1_visible', true);
|
||||
$html .= $this->buildSwitch('footerWidget1Visible', 'Mostrar Widget 1', 'bi-eye', $visible);
|
||||
|
||||
$title = $this->renderer->getFieldValue($componentId, 'widget_1', 'widget_1_title', 'Recursos');
|
||||
$html .= $this->buildTextInput('footerWidget1Title', 'Titulo', 'bi-type', $title);
|
||||
|
||||
$html .= ' <div class="alert alert-info small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' El contenido se gestiona desde <strong>Apariencia > Menus > Footer Menu 1</strong>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildWidget2Group(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-list-ul me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Widget 2 (Menu)';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$visible = $this->renderer->getFieldValue($componentId, 'widget_2', 'widget_2_visible', true);
|
||||
$html .= $this->buildSwitch('footerWidget2Visible', 'Mostrar Widget 2', 'bi-eye', $visible);
|
||||
|
||||
$title = $this->renderer->getFieldValue($componentId, 'widget_2', 'widget_2_title', 'Soporte');
|
||||
$html .= $this->buildTextInput('footerWidget2Title', 'Titulo', 'bi-type', $title);
|
||||
|
||||
$html .= ' <div class="alert alert-info small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' El contenido se gestiona desde <strong>Apariencia > Menus > Footer Menu 2</strong>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildWidget3Group(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-list-ul me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Widget 3 (Menu)';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$visible = $this->renderer->getFieldValue($componentId, 'widget_3', 'widget_3_visible', true);
|
||||
$html .= $this->buildSwitch('footerWidget3Visible', 'Mostrar Widget 3', 'bi-eye', $visible);
|
||||
|
||||
$title = $this->renderer->getFieldValue($componentId, 'widget_3', 'widget_3_title', 'Empresa');
|
||||
$html .= $this->buildTextInput('footerWidget3Title', 'Titulo', 'bi-type', $title);
|
||||
|
||||
$html .= ' <div class="alert alert-info small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' El contenido se gestiona desde <strong>Apariencia > Menus > Footer Menu 3</strong>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildNewsletterGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-envelope-paper me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Newsletter';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$visible = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_visible', true);
|
||||
$html .= $this->buildSwitch('footerNewsletterVisible', 'Mostrar Newsletter', 'bi-eye', $visible);
|
||||
|
||||
$title = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_title', 'Suscribete al Newsletter');
|
||||
$html .= $this->buildTextInput('footerNewsletterTitle', 'Titulo', 'bi-type', $title);
|
||||
|
||||
$description = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_description', 'Recibe las ultimas actualizaciones.');
|
||||
$html .= $this->buildTextarea('footerNewsletterDescription', 'Descripcion', 'bi-text-paragraph', $description);
|
||||
|
||||
$placeholder = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_email_placeholder', 'Email');
|
||||
$html .= $this->buildTextInput('footerNewsletterPlaceholder', 'Placeholder email', 'bi-input-cursor', $placeholder);
|
||||
|
||||
$buttonText = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_button_text', 'Suscribirse');
|
||||
$html .= $this->buildTextInput('footerNewsletterButtonText', 'Texto boton', 'bi-cursor', $buttonText);
|
||||
|
||||
$webhookUrl = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_webhook_url', '');
|
||||
$html .= $this->buildTextarea('footerNewsletterWebhookUrl', 'URL del Webhook', 'bi-link-45deg', $webhookUrl);
|
||||
|
||||
$successMsg = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_success_message', 'Gracias por suscribirte!');
|
||||
$html .= $this->buildTextInput('footerNewsletterSuccessMessage', 'Mensaje exito', 'bi-check-circle', $successMsg);
|
||||
|
||||
$errorMsg = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_error_message', 'Error al suscribirse.');
|
||||
$html .= $this->buildTextInput('footerNewsletterErrorMessage', 'Mensaje error', 'bi-x-circle', $errorMsg);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildFooterBottomGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-c-circle me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Pie de Footer';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$copyright = $this->renderer->getFieldValue($componentId, 'footer_bottom', 'copyright_text', date('Y') . ' Todos los derechos reservados.');
|
||||
$html .= $this->buildTextInput('footerCopyrightText', 'Texto copyright', 'bi-c-circle', $copyright);
|
||||
|
||||
$html .= ' <div class="alert alert-secondary small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' El simbolo © se agrega automaticamente';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'bg_color', '#212529');
|
||||
$html .= $this->buildColorInput('footerBgColor', 'Fondo footer', $bgColor);
|
||||
|
||||
$textColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_color', '#ffffff');
|
||||
$html .= $this->buildColorInput('footerTextColor', 'Color texto', $textColor);
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#ffffff');
|
||||
$html .= $this->buildColorInput('footerTitleColor', 'Color titulos', $titleColor);
|
||||
|
||||
$linkColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_color', '#ffffff');
|
||||
$html .= $this->buildColorInput('footerLinkColor', 'Color links', $linkColor);
|
||||
|
||||
$linkHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_color', '#FF8600');
|
||||
$html .= $this->buildColorInput('footerLinkHoverColor', 'Color links hover', $linkHoverColor);
|
||||
|
||||
$buttonBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_bg_color', '#0d6efd');
|
||||
$html .= $this->buildColorInput('footerButtonBgColor', 'Fondo boton', $buttonBgColor);
|
||||
|
||||
$buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#ffffff');
|
||||
$html .= $this->buildColorInput('footerButtonTextColor', 'Texto boton', $buttonTextColor);
|
||||
|
||||
$buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_bg', '#0b5ed7');
|
||||
$html .= $this->buildColorInput('footerButtonHoverBg', 'Fondo boton hover', $buttonHoverBg);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-expand me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$paddingY = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_y', '3rem');
|
||||
$html .= $this->buildTextInput('footerPaddingY', 'Padding vertical', 'bi-arrows-vertical', $paddingY);
|
||||
|
||||
$marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_top', '0');
|
||||
$html .= $this->buildTextInput('footerMarginTop', 'Margen superior', 'bi-arrow-up', $marginTop);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-stars me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$inputRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'input_border_radius', '6px');
|
||||
$html .= $this->buildTextInput('footerInputBorderRadius', 'Radio input', 'bi-square', $inputRadius);
|
||||
|
||||
$buttonRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '6px');
|
||||
$html .= $this->buildTextInput('footerButtonBorderRadius', 'Radio boton', 'bi-square', $buttonRadius);
|
||||
|
||||
$transition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= $this->buildTextInput('footerTransitionDuration', 'Duracion transicion', 'bi-hourglass', $transition);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private function buildSwitch(string $id, string $label, string $icon, $value): string
|
||||
{
|
||||
$checked = $value === true || $value === '1' || $value === 1 ? 'checked' : '';
|
||||
|
||||
$html = ' <div class="form-check form-switch mb-2">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="' . esc_attr($id) . '" ' . $checked . '>';
|
||||
$html .= ' <label class="form-check-label small" for="' . esc_attr($id) . '">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTextInput(string $id, string $label, string $icon, mixed $value): string
|
||||
{
|
||||
$value = $this->normalizeStringValue($value);
|
||||
|
||||
$html = ' <div class="mb-3">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" class="form-control form-control-sm" id="' . esc_attr($id) . '" value="' . esc_attr($value) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPasswordInput(string $id, string $label, string $icon, mixed $value): string
|
||||
{
|
||||
$value = $this->normalizeStringValue($value);
|
||||
|
||||
$html = ' <div class="mb-3">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="password" class="form-control form-control-sm" id="' . esc_attr($id) . '" value="' . esc_attr($value) . '">';
|
||||
$html .= ' <div class="form-text small">URL oculta por seguridad</div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTextarea(string $id, string $label, string $icon, mixed $value): string
|
||||
{
|
||||
$value = $this->normalizeStringValue($value);
|
||||
|
||||
$html = ' <div class="mb-3">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea class="form-control form-control-sm" id="' . esc_attr($id) . '" rows="2">' . esc_textarea($value) . '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorInput(string $id, string $label, mixed $value): string
|
||||
{
|
||||
$value = $this->normalizeStringValue($value);
|
||||
|
||||
$html = ' <div class="mb-2 d-flex align-items-center gap-2">';
|
||||
$html .= ' <input type="color" class="form-control form-control-color" id="' . esc_attr($id) . '" value="' . esc_attr($value) . '" style="width: 40px; height: 30px;">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label mb-0 small">' . esc_html($label) . '</label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliza un valor a string para inputs de formulario
|
||||
*
|
||||
* El repositorio convierte '0' a false y '1' a true automáticamente,
|
||||
* pero para campos de texto necesitamos el valor original como string.
|
||||
*/
|
||||
private function normalizeStringValue(mixed $value): string
|
||||
{
|
||||
if ($value === false) {
|
||||
return '0';
|
||||
}
|
||||
if ($value === true) {
|
||||
return '1';
|
||||
}
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, $value): string
|
||||
{
|
||||
$checked = $value === true || $value === '1' || $value === 1 ? 'checked' : '';
|
||||
|
||||
$html = '<div class="form-check">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="' . esc_attr($id) . '" ' . $checked . '>';
|
||||
$html .= ' <label class="form-check-label small" for="' . esc_attr($id) . '">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
78
Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php
Normal file
78
Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Hero\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Hero Section
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class HeroFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'hero';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'heroEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'heroShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'heroShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'heroIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'heroVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'heroVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'heroVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'heroVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'heroVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'heroExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'heroExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'heroExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'heroExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'heroShowCategories' => ['group' => 'content', 'attribute' => 'show_categories'],
|
||||
'heroShowBadgeIcon' => ['group' => 'content', 'attribute' => 'show_badge_icon'],
|
||||
'heroBadgeIconClass' => ['group' => 'content', 'attribute' => 'badge_icon_class'],
|
||||
'heroTitleTag' => ['group' => 'content', 'attribute' => 'title_tag'],
|
||||
|
||||
// Colors
|
||||
'heroGradientStart' => ['group' => 'colors', 'attribute' => 'gradient_start'],
|
||||
'heroGradientEnd' => ['group' => 'colors', 'attribute' => 'gradient_end'],
|
||||
'heroTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'heroBadgeBgColor' => ['group' => 'colors', 'attribute' => 'badge_bg_color'],
|
||||
'heroBadgeTextColor' => ['group' => 'colors', 'attribute' => 'badge_text_color'],
|
||||
'heroBadgeIconColor' => ['group' => 'colors', 'attribute' => 'badge_icon_color'],
|
||||
'heroBadgeHoverBg' => ['group' => 'colors', 'attribute' => 'badge_hover_bg'],
|
||||
|
||||
// Typography
|
||||
'heroTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'],
|
||||
'heroTitleFontSizeMobile' => ['group' => 'typography', 'attribute' => 'title_font_size_mobile'],
|
||||
'heroTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'],
|
||||
'heroTitleLineHeight' => ['group' => 'typography', 'attribute' => 'title_line_height'],
|
||||
'heroBadgeFontSize' => ['group' => 'typography', 'attribute' => 'badge_font_size'],
|
||||
|
||||
// Spacing
|
||||
'heroPaddingVertical' => ['group' => 'spacing', 'attribute' => 'padding_vertical'],
|
||||
'heroMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'],
|
||||
'heroBadgePadding' => ['group' => 'spacing', 'attribute' => 'badge_padding'],
|
||||
'heroBadgeBorderRadius' => ['group' => 'spacing', 'attribute' => 'badge_border_radius'],
|
||||
|
||||
// Visual Effects
|
||||
'heroBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
'heroTitleTextShadow' => ['group' => 'visual_effects', 'attribute' => 'title_text_shadow'],
|
||||
'heroBadgeBackdropBlur' => ['group' => 'visual_effects', 'attribute' => 'badge_backdrop_blur'],
|
||||
];
|
||||
}
|
||||
}
|
||||
480
Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php
Normal file
480
Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php
Normal file
@@ -0,0 +1,480 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Hero\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
final class HeroFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-image me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuración de Hero Section';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Personaliza la sección hero con título y badges de categorías';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="hero">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="heroEnabled" ';
|
||||
$html .= checked($enabled, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="heroEnabled">';
|
||||
$html .= ' <i class="bi bi-power me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Activar Hero Section</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="heroShowOnDesktop" ';
|
||||
$html .= checked($showOnDesktop, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="heroShowOnDesktop">';
|
||||
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Desktop</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="heroShowOnMobile" ';
|
||||
$html .= checked($showOnMobile, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="heroShowOnMobile">';
|
||||
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Mobile</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'hero');
|
||||
|
||||
// Switch: CSS Crítico
|
||||
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="heroIsCritical" ';
|
||||
$html .= checked($isCritical, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="heroIsCritical">';
|
||||
$html .= ' <i class="bi bi-lightning-charge me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>CSS Crítico</strong>';
|
||||
$html .= ' <small class="text-muted d-block">Inyectar CSS en <head> para optimizar LCP</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$showCategories = $this->renderer->getFieldValue($componentId, 'content', 'show_categories', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="heroShowCategories" ';
|
||||
$html .= checked($showCategories, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="heroShowCategories">';
|
||||
$html .= ' <i class="bi bi-tags me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar badges de categorías</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showBadgeIcon = $this->renderer->getFieldValue($componentId, 'content', 'show_badge_icon', true);
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="heroShowBadgeIcon" ';
|
||||
$html .= checked($showBadgeIcon, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="heroShowBadgeIcon">';
|
||||
$html .= ' <i class="bi bi-star me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar ícono en badges</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$badgeIconClass = $this->renderer->getFieldValue($componentId, 'content', 'badge_icon_class', 'bi-folder-fill');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="heroBadgeIconClass" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-bootstrap me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Clase del ícono de badge';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBadgeIconClass" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($badgeIconClass) . '" placeholder="bi-folder-fill">';
|
||||
$html .= ' <small class="text-muted">Usa clases de Bootstrap Icons</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleTag = $this->renderer->getFieldValue($componentId, 'content', 'title_tag', 'h1');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="heroTitleTag" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-code me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Etiqueta HTML del título';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="heroTitleTag" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="h1" ' . selected($titleTag, 'h1', false) . '>H1 (recomendado para SEO)</option>';
|
||||
$html .= ' <option value="h2" ' . selected($titleTag, 'h2', false) . '>H2</option>';
|
||||
$html .= ' <option value="div" ' . selected($titleTag, 'div', false) . '>DIV (sin semántica)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
|
||||
$gradientStart = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_start', '#1e3a5f');
|
||||
$html .= $this->buildColorPicker('heroGradientStart', 'Degradado (inicio)', 'circle-half', $gradientStart);
|
||||
|
||||
$gradientEnd = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_end', '#2c5282');
|
||||
$html .= $this->buildColorPicker('heroGradientEnd', 'Degradado (fin)', 'circle-fill', $gradientEnd);
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#FFFFFF');
|
||||
$html .= $this->buildColorPicker('heroTitleColor', 'Color título', 'fonts', $titleColor);
|
||||
|
||||
$badgeBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'badge_bg_color', '#FFFFFF');
|
||||
$html .= $this->buildColorPicker('heroBadgeBgColor', 'Fondo badges', 'badge', $badgeBgColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$badgeTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'badge_text_color', '#FFFFFF');
|
||||
$html .= $this->buildColorPicker('heroBadgeTextColor', 'Texto badges', 'card-text', $badgeTextColor);
|
||||
|
||||
$badgeIconColor = $this->renderer->getFieldValue($componentId, 'colors', 'badge_icon_color', '#FFB800');
|
||||
$html .= $this->buildColorPicker('heroBadgeIconColor', 'Ícono badges', 'star-fill', $badgeIconColor);
|
||||
|
||||
$badgeHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'badge_hover_bg', '#FF8600');
|
||||
$html .= $this->buildColorPicker('heroBadgeHoverBg', 'Badges (hover)', 'hand-index', $badgeHoverBg);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-type me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografía';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
|
||||
$titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '2.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroTitleFontSize" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Tamaño desktop';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroTitleFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleFontSizeMobile = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size_mobile', '1.75rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroTitleFontSizeMobile" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Tamaño mobile';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroTitleFontSizeMobile" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontSizeMobile) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
|
||||
$titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '700');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroTitleFontWeight" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Peso del título';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="heroTitleFontWeight" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="400" ' . selected($titleFontWeight, '400', false) . '>Normal (400)</option>';
|
||||
$html .= ' <option value="500" ' . selected($titleFontWeight, '500', false) . '>Medium (500)</option>';
|
||||
$html .= ' <option value="600" ' . selected($titleFontWeight, '600', false) . '>Semibold (600)</option>';
|
||||
$html .= ' <option value="700" ' . selected($titleFontWeight, '700', false) . '>Bold (700)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleLineHeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_line_height', '1.4');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroTitleLineHeight" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Altura de línea';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroTitleLineHeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleLineHeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$badgeFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'badge_font_size', '0.813rem');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="heroBadgeFontSize" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Tamaño fuente badges';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBadgeFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($badgeFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
|
||||
$paddingVertical = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_vertical', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroPaddingVertical" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Padding vertical';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroPaddingVertical" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($paddingVertical) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroMarginBottom" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Margen inferior';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$badgePadding = $this->renderer->getFieldValue($componentId, 'spacing', 'badge_padding', '0.375rem 0.875rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroBadgePadding" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Padding badges';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBadgePadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($badgePadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$badgeBorderRadius = $this->renderer->getFieldValue($componentId, 'spacing', 'badge_border_radius', '20px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroBadgeBorderRadius" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Border radius badges';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBadgeBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($badgeBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 4px 16px rgba(30, 58, 95, 0.25)');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="heroBoxShadow" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Sombra del hero';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleTextShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'title_text_shadow', '1px 1px 2px rgba(0, 0, 0, 0.2)');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="heroTitleTextShadow" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Sombra del título';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroTitleTextShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleTextShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$badgeBackdropBlur = $this->renderer->getFieldValue($componentId, 'visual_effects', 'badge_backdrop_blur', '10px');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="heroBadgeBackdropBlur" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Blur de fondo badges';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBadgeBackdropBlur" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($badgeBackdropBlur) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $icon, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= ' <label for="' . $id . '" class="form-label small mb-1 fw-semibold" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-' . $icon . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . $label;
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="color" id="' . $id . '" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($value) . '" title="' . esc_attr($label) . '">';
|
||||
$html .= ' <small class="text-muted d-block mt-1" id="' . $id . 'Value">' . esc_html(strtoupper($value)) . '</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
76
Admin/Infrastructure/Api/WordPress/AdminMenuRegistrar.php
Normal file
76
Admin/Infrastructure/Api/WordPress/AdminMenuRegistrar.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Infrastructure\Api\WordPress;
|
||||
|
||||
use ROITheme\Admin\Domain\Contracts\MenuRegistrarInterface;
|
||||
use ROITheme\Admin\Domain\ValueObjects\MenuItem;
|
||||
use ROITheme\Admin\Application\UseCases\RenderDashboardUseCase;
|
||||
|
||||
/**
|
||||
* Registra el menú de administración en WordPress
|
||||
*
|
||||
* Infrastructure - Implementación específica de WordPress
|
||||
*/
|
||||
final class AdminMenuRegistrar implements MenuRegistrarInterface
|
||||
{
|
||||
private MenuItem $menuItem;
|
||||
|
||||
/**
|
||||
* @param MenuItem $menuItem Configuración del menú
|
||||
* @param RenderDashboardUseCase $renderUseCase Caso de uso para renderizar
|
||||
*/
|
||||
public function __construct(
|
||||
MenuItem $menuItem,
|
||||
private readonly RenderDashboardUseCase $renderUseCase
|
||||
) {
|
||||
$this->menuItem = $menuItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra el menú en WordPress
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
add_action('admin_menu', [$this, 'addMenuPage']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback para agregar la página al menú de WordPress
|
||||
*/
|
||||
public function addMenuPage(): void
|
||||
{
|
||||
add_menu_page(
|
||||
$this->menuItem->getPageTitle(),
|
||||
$this->menuItem->getMenuTitle(),
|
||||
$this->menuItem->getCapability(),
|
||||
$this->menuItem->getMenuSlug(),
|
||||
[$this, 'renderPage'],
|
||||
$this->menuItem->getIcon(),
|
||||
$this->menuItem->getPosition()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback para renderizar la página
|
||||
*/
|
||||
public function renderPage(): void
|
||||
{
|
||||
try {
|
||||
echo $this->renderUseCase->execute('dashboard');
|
||||
} catch (\Exception $e) {
|
||||
echo '<div class="error"><p>Error rendering dashboard: ' . esc_html($e->getMessage()) . '</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
public function getCapability(): string
|
||||
{
|
||||
return $this->menuItem->getCapability();
|
||||
}
|
||||
|
||||
public function getSlug(): string
|
||||
{
|
||||
return $this->menuItem->getMenuSlug();
|
||||
}
|
||||
}
|
||||
129
Admin/Infrastructure/Services/AdminAssetEnqueuer.php
Normal file
129
Admin/Infrastructure/Services/AdminAssetEnqueuer.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Infrastructure\Services;
|
||||
|
||||
/**
|
||||
* Servicio para enqueue de assets del panel de administración
|
||||
*
|
||||
* Infrastructure - WordPress specific
|
||||
*/
|
||||
final class AdminAssetEnqueuer
|
||||
{
|
||||
private const ADMIN_PAGE_SLUG = 'roi-theme-admin';
|
||||
|
||||
public function __construct(
|
||||
private readonly string $themeUri
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra los hooks de WordPress
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue de assets solo en la página del dashboard
|
||||
*
|
||||
* @param string $hook Hook name de WordPress
|
||||
*/
|
||||
public function enqueueAssets(string $hook): void
|
||||
{
|
||||
// Solo cargar en nuestra página de admin
|
||||
if (!$this->isAdminPage($hook)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->enqueueStyles();
|
||||
$this->enqueueScripts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si estamos en la página del dashboard
|
||||
*
|
||||
* @param string $hook Hook name
|
||||
* @return bool
|
||||
*/
|
||||
private function isAdminPage(string $hook): bool
|
||||
{
|
||||
return strpos($hook, self::ADMIN_PAGE_SLUG) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue de estilos CSS
|
||||
*/
|
||||
private function enqueueStyles(): void
|
||||
{
|
||||
// Bootstrap 5 CSS
|
||||
wp_enqueue_style(
|
||||
'bootstrap',
|
||||
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css',
|
||||
[],
|
||||
'5.3.2'
|
||||
);
|
||||
|
||||
// Bootstrap Icons
|
||||
wp_enqueue_style(
|
||||
'bootstrap-icons',
|
||||
'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css',
|
||||
[],
|
||||
'1.11.3'
|
||||
);
|
||||
|
||||
// Estilos del dashboard
|
||||
wp_enqueue_style(
|
||||
'roi-admin-dashboard',
|
||||
$this->themeUri . '/Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css',
|
||||
['bootstrap', 'bootstrap-icons'],
|
||||
filemtime(get_template_directory() . '/Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue de scripts JavaScript
|
||||
*/
|
||||
private function enqueueScripts(): void
|
||||
{
|
||||
// Bootstrap 5 JS Bundle (incluye Popper)
|
||||
// IMPORTANTE: Cargar en header (false) para que esté disponible antes del contenido
|
||||
wp_enqueue_script(
|
||||
'bootstrap',
|
||||
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js',
|
||||
[],
|
||||
'5.3.2',
|
||||
false // Load in header, not footer - required for Bootstrap tabs to work
|
||||
);
|
||||
|
||||
// Script del dashboard
|
||||
wp_enqueue_script(
|
||||
'roi-admin-dashboard',
|
||||
$this->themeUri . '/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js',
|
||||
['bootstrap'],
|
||||
filemtime(get_template_directory() . '/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js'),
|
||||
true
|
||||
);
|
||||
|
||||
// Script de toggle para exclusiones (Plan 99.11)
|
||||
wp_enqueue_script(
|
||||
'roi-exclusion-toggle',
|
||||
$this->themeUri . '/Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js',
|
||||
['roi-admin-dashboard'],
|
||||
filemtime(get_template_directory() . '/Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js'),
|
||||
true
|
||||
);
|
||||
|
||||
// Pasar variables al JavaScript
|
||||
wp_localize_script(
|
||||
'roi-admin-dashboard',
|
||||
'roiAdminDashboard',
|
||||
[
|
||||
'nonce' => wp_create_nonce('roi_admin_dashboard'),
|
||||
'ajaxurl' => admin_url('admin-ajax.php')
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
215
Admin/Infrastructure/Ui/AdminDashboardRenderer.php
Normal file
215
Admin/Infrastructure/Ui/AdminDashboardRenderer.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Domain\Contracts\DashboardRendererInterface;
|
||||
use ROITheme\Shared\Application\UseCases\GetComponentSettings\GetComponentSettingsUseCase;
|
||||
|
||||
/**
|
||||
* Renderiza el dashboard del panel de administración
|
||||
*
|
||||
* Infrastructure - Implementación con WordPress
|
||||
*/
|
||||
final class AdminDashboardRenderer implements DashboardRendererInterface
|
||||
{
|
||||
private const SUPPORTED_VIEWS = ['dashboard'];
|
||||
|
||||
/**
|
||||
* @param GetComponentSettingsUseCase|null $getComponentSettingsUseCase
|
||||
* @param ComponentGroupRegistry|null $groupRegistry Registro de grupos de componentes
|
||||
* @param array<string, mixed> $components Componentes disponibles
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ?GetComponentSettingsUseCase $getComponentSettingsUseCase = null,
|
||||
private readonly ?ComponentGroupRegistry $groupRegistry = null,
|
||||
private readonly array $components = []
|
||||
) {
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
ob_start();
|
||||
require __DIR__ . '/Views/dashboard.php';
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
public function supports(string $viewType): bool
|
||||
{
|
||||
return in_array($viewType, self::SUPPORTED_VIEWS, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene los componentes disponibles
|
||||
*
|
||||
* @return array<string, array<string, string>>
|
||||
*/
|
||||
public function getComponents(): array
|
||||
{
|
||||
return [
|
||||
'top-notification-bar' => [
|
||||
'id' => 'top-notification-bar',
|
||||
'label' => 'TopBar',
|
||||
'icon' => 'bi-megaphone-fill',
|
||||
],
|
||||
'navbar' => [
|
||||
'id' => 'navbar',
|
||||
'label' => 'Navbar',
|
||||
'icon' => 'bi-list',
|
||||
],
|
||||
'cta-lets-talk' => [
|
||||
'id' => 'cta-lets-talk',
|
||||
'label' => "Let's Talk",
|
||||
'icon' => 'bi-lightning-charge-fill',
|
||||
],
|
||||
'hero' => [
|
||||
'id' => 'hero',
|
||||
'label' => 'Hero Section',
|
||||
'icon' => 'bi-image',
|
||||
],
|
||||
'featured-image' => [
|
||||
'id' => 'featured-image',
|
||||
'label' => 'Featured Image',
|
||||
'icon' => 'bi-card-image',
|
||||
],
|
||||
'table-of-contents' => [
|
||||
'id' => 'table-of-contents',
|
||||
'label' => 'Table of Contents',
|
||||
'icon' => 'bi-list-nested',
|
||||
],
|
||||
'cta-box-sidebar' => [
|
||||
'id' => 'cta-box-sidebar',
|
||||
'label' => 'CTA Sidebar',
|
||||
'icon' => 'bi-megaphone',
|
||||
],
|
||||
'social-share' => [
|
||||
'id' => 'social-share',
|
||||
'label' => 'Social Share',
|
||||
'icon' => 'bi-share',
|
||||
],
|
||||
'cta-post' => [
|
||||
'id' => 'cta-post',
|
||||
'label' => 'CTA Post',
|
||||
'icon' => 'bi-megaphone-fill',
|
||||
],
|
||||
'related-post' => [
|
||||
'id' => 'related-post',
|
||||
'label' => 'Related Posts',
|
||||
'icon' => 'bi-grid-3x3-gap',
|
||||
],
|
||||
'contact-form' => [
|
||||
'id' => 'contact-form',
|
||||
'label' => 'Contact Form',
|
||||
'icon' => 'bi-envelope-paper',
|
||||
],
|
||||
'footer' => [
|
||||
'id' => 'footer',
|
||||
'label' => 'Footer',
|
||||
'icon' => 'bi-layout-text-window-reverse',
|
||||
],
|
||||
'theme-settings' => [
|
||||
'id' => 'theme-settings',
|
||||
'label' => 'Theme Settings',
|
||||
'icon' => 'bi-gear',
|
||||
],
|
||||
'adsense-placement' => [
|
||||
'id' => 'adsense-placement',
|
||||
'label' => 'AdSense',
|
||||
'icon' => 'bi-megaphone',
|
||||
],
|
||||
'custom-css-manager' => [
|
||||
'id' => 'custom-css-manager',
|
||||
'label' => 'CSS Personalizado',
|
||||
'icon' => 'bi-file-earmark-code',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene las configuraciones de un componente
|
||||
*
|
||||
* @param string $componentName Nombre del componente
|
||||
* @return array<string, array<string, mixed>> Configuraciones agrupadas por grupo
|
||||
*/
|
||||
public function getComponentSettings(string $componentName): array
|
||||
{
|
||||
if ($this->getComponentSettingsUseCase === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->getComponentSettingsUseCase->execute($componentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el valor de un campo de configuración
|
||||
*
|
||||
* @param string $componentName Nombre del componente
|
||||
* @param string $groupName Nombre del grupo
|
||||
* @param string $attributeName Nombre del atributo
|
||||
* @param mixed $default Valor por defecto si no existe
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFieldValue(string $componentName, string $groupName, string $attributeName, mixed $default = null): mixed
|
||||
{
|
||||
$settings = $this->getComponentSettings($componentName);
|
||||
|
||||
return $settings[$groupName][$attributeName] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la clase del FormBuilder para un componente
|
||||
*
|
||||
* @param string $componentId ID del componente en kebab-case (ej: 'top-notification-bar')
|
||||
* @return string Namespace completo del FormBuilder
|
||||
*/
|
||||
public function getFormBuilderClass(string $componentId): string
|
||||
{
|
||||
// Mapeo especial para componentes con acrónimos (CSS, API, etc.)
|
||||
$specialMappings = [
|
||||
'custom-css-manager' => 'CustomCSSManager',
|
||||
];
|
||||
|
||||
// Usar mapeo especial si existe, sino convertir kebab-case a PascalCase
|
||||
if (isset($specialMappings[$componentId])) {
|
||||
$className = $specialMappings[$componentId];
|
||||
} else {
|
||||
// 'top-notification-bar' → 'TopNotificationBar'
|
||||
$className = str_replace('-', '', ucwords($componentId, '-'));
|
||||
}
|
||||
|
||||
// Construir namespace completo
|
||||
// ROITheme\Admin\TopNotificationBar\Infrastructure\Ui\TopNotificationBarFormBuilder
|
||||
return "ROITheme\\Admin\\{$className}\\Infrastructure\\Ui\\{$className}FormBuilder";
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene los grupos de componentes
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public function getComponentGroups(): array
|
||||
{
|
||||
if ($this->groupRegistry === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->groupRegistry->getGroups();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el grupo al que pertenece un componente
|
||||
*
|
||||
* @param string $componentId ID del componente
|
||||
* @return string|null ID del grupo o null
|
||||
*/
|
||||
public function getGroupForComponent(string $componentId): ?string
|
||||
{
|
||||
if ($this->groupRegistry === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->groupRegistry->getGroupForComponent($componentId);
|
||||
}
|
||||
|
||||
}
|
||||
630
Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css
Normal file
630
Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css
Normal file
@@ -0,0 +1,630 @@
|
||||
/**
|
||||
* ROI Theme Admin Dashboard - Improved Version
|
||||
* Basado en improved-panel.css del Design System
|
||||
*/
|
||||
|
||||
/* ================================================
|
||||
CSS VARIABLES - DESIGN SYSTEM
|
||||
================================================ */
|
||||
|
||||
:root {
|
||||
/* Navy Colors */
|
||||
--roi-navy-dark: #0E2337;
|
||||
--roi-navy-primary: #1e3a5f;
|
||||
--roi-navy-light: #2c5282;
|
||||
|
||||
/* Orange Colors */
|
||||
--roi-orange-primary: #FF8600;
|
||||
--roi-orange-hover: #FF6B35;
|
||||
--roi-orange-light: #FFB800;
|
||||
|
||||
/* Neutral Colors */
|
||||
--roi-neutral-50: #f8f9fa;
|
||||
--roi-neutral-100: #e9ecef;
|
||||
--roi-neutral-600: #495057;
|
||||
--roi-neutral-700: #6c757d;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
--shadow-xl: 0 12px 32px rgba(0, 0, 0, 0.15);
|
||||
--shadow-orange: 0 6px 20px rgba(255, 134, 0, 0.15);
|
||||
|
||||
/* Transitions */
|
||||
--transition-base: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-fast: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
WORDPRESS ADMIN OVERRIDES
|
||||
================================================ */
|
||||
|
||||
/* Sobrescribir max-width de .card de WordPress */
|
||||
.wrap.roi-admin-panel .card {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
/* Fix para switches de Bootstrap */
|
||||
.wrap.roi-admin-panel .form-switch .form-check-input {
|
||||
all: unset !important;
|
||||
width: 2em !important;
|
||||
height: 1em !important;
|
||||
margin-left: -2.5em !important;
|
||||
margin-right: 0.5em !important;
|
||||
background-color: #dee2e6 !important;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='white'/%3e%3c/svg%3e") !important;
|
||||
background-position: left center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: contain !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.25) !important;
|
||||
border-radius: 2em !important;
|
||||
transition: background-position 0.15s ease-in-out !important;
|
||||
cursor: pointer !important;
|
||||
flex-shrink: 0 !important;
|
||||
appearance: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
-moz-appearance: none !important;
|
||||
}
|
||||
|
||||
.wrap.roi-admin-panel .form-switch .form-check-input:checked {
|
||||
background-color: #0d6efd !important;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='white'/%3e%3c/svg%3e") !important;
|
||||
background-position: right center !important;
|
||||
border-color: #0d6efd !important;
|
||||
}
|
||||
|
||||
.wrap.roi-admin-panel .form-switch .form-check-input::before,
|
||||
.wrap.roi-admin-panel .form-switch .form-check-input::after {
|
||||
display: none !important;
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
.wrap.roi-admin-panel .form-switch .form-check-input:focus {
|
||||
outline: 0 !important;
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25) !important;
|
||||
}
|
||||
|
||||
.wrap.roi-admin-panel .form-check {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.wrap.roi-admin-panel .form-check-label {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
margin-bottom: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
TABS NAVIGATION (Legacy)
|
||||
================================================ */
|
||||
|
||||
.nav-tabs-admin {
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-item {
|
||||
margin-right: 0.1rem;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-link {
|
||||
color: #6c757d;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
padding: 0.3rem 0.3rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.83rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-link i.bi {
|
||||
margin-right: 0.2rem !important;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-link:hover {
|
||||
color: #FF8600;
|
||||
border-bottom-color: #FFB800;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-link.active {
|
||||
color: #FF8600;
|
||||
border-bottom-color: #FF8600;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Tab Content */
|
||||
.tab-content {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
HEADER MEJORADO
|
||||
================================================ */
|
||||
|
||||
.roi-home-header {
|
||||
position: relative;
|
||||
padding: 2.5rem 2rem;
|
||||
background: var(--roi-navy-dark);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: none;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
/* Patrón de fondo sutil */
|
||||
.roi-home-header__pattern {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 50%, rgba(255, 134, 0, 0.05) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 50%, rgba(44, 82, 130, 0.08) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.roi-home-header__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.roi-home-header__icon-wrapper {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--roi-orange-primary), var(--roi-orange-light));
|
||||
border-radius: 16px;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4px 16px rgba(255, 134, 0, 0.3);
|
||||
}
|
||||
|
||||
.roi-home-header__icon {
|
||||
font-size: 2rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.roi-home-header__text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.roi-home-header__title {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin: 0 0 0.5rem 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.roi-home-header__subtitle {
|
||||
font-size: 0.95rem;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
GRID DE GRUPOS
|
||||
================================================ */
|
||||
|
||||
.roi-groups-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.roi-groups-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
GROUP CARDS MEJORADOS
|
||||
================================================ */
|
||||
|
||||
.roi-group-card {
|
||||
position: relative;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--roi-neutral-100);
|
||||
transition: var(--transition-base);
|
||||
animation: fadeInUp 0.4s cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||
}
|
||||
|
||||
/* Efecto glow en hover */
|
||||
.roi-group-card__glow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 0 0 1px var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
.roi-group-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
border-color: var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
.roi-group-card:hover .roi-group-card__glow {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Header del grupo */
|
||||
.roi-group-card__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1.25rem;
|
||||
border-bottom: 2px solid var(--roi-neutral-50);
|
||||
}
|
||||
|
||||
.roi-group-card__icon-wrapper {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 134, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
flex-shrink: 0;
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
|
||||
.roi-group-card:hover .roi-group-card__icon-wrapper {
|
||||
background: rgba(255, 134, 0, 0.15);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.roi-group-card__icon {
|
||||
font-size: 1.75rem;
|
||||
color: var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
.roi-group-card__header-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.roi-group-card__title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.35rem 0;
|
||||
color: var(--roi-navy-primary);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.roi-group-card__description {
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
color: var(--roi-neutral-700);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
COMPONENTS GRID
|
||||
================================================ */
|
||||
|
||||
.roi-components-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
MINI CARDS MEJORADOS
|
||||
================================================ */
|
||||
|
||||
.roi-component-minicard {
|
||||
position: relative;
|
||||
background: white;
|
||||
border: 1px solid var(--roi-neutral-100);
|
||||
border-radius: 10px;
|
||||
padding: 1.25rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-base);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.roi-component-minicard::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--roi-orange-primary), var(--roi-orange-light));
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.roi-component-minicard:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.roi-component-minicard:hover {
|
||||
transform: translateY(-3px) scale(1.02);
|
||||
box-shadow: var(--shadow-orange);
|
||||
border-color: var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
.roi-component-minicard:active {
|
||||
transform: translateY(-2px) scale(0.98);
|
||||
}
|
||||
|
||||
.roi-component-minicard:focus {
|
||||
outline: 2px solid var(--roi-orange-primary);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
/* Icono del mini card */
|
||||
.roi-component-minicard__icon-bg {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 134, 0, 0.08);
|
||||
border-radius: 10px;
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
|
||||
.roi-component-minicard:hover .roi-component-minicard__icon-bg {
|
||||
background: rgba(255, 134, 0, 0.15);
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
.roi-component-minicard__icon {
|
||||
font-size: 1.5rem;
|
||||
color: var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
.roi-component-minicard__label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--roi-navy-dark);
|
||||
line-height: 1.3;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.roi-component-minicard:hover .roi-component-minicard__label {
|
||||
color: var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
BREADCRUMB
|
||||
================================================ */
|
||||
|
||||
.roi-breadcrumb {
|
||||
padding: 1rem 1.5rem;
|
||||
background: var(--roi-neutral-50);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid var(--roi-orange-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.roi-breadcrumb__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.roi-breadcrumb__separator {
|
||||
color: var(--roi-neutral-700);
|
||||
}
|
||||
|
||||
.roi-breadcrumb__group {
|
||||
color: var(--roi-neutral-700);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.roi-breadcrumb__current {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--roi-navy-primary);
|
||||
}
|
||||
|
||||
/* Botón Volver */
|
||||
.roi-back-to-home {
|
||||
border-color: var(--roi-navy-primary);
|
||||
color: var(--roi-navy-primary);
|
||||
}
|
||||
|
||||
.roi-back-to-home:hover {
|
||||
background-color: var(--roi-navy-primary);
|
||||
border-color: var(--roi-navy-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
COMPONENT FORM CONTAINER
|
||||
================================================ */
|
||||
|
||||
.roi-component-form-container {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
ANIMATIONS
|
||||
================================================ */
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
#roi-home-view,
|
||||
#roi-component-view {
|
||||
animation: fadeIn 0.4s ease-in;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
UTILITY CLASSES
|
||||
================================================ */
|
||||
|
||||
.roi-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
RESPONSIVE
|
||||
================================================ */
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.roi-group-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.roi-home-header {
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.roi-home-header__icon-wrapper {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.roi-home-header__icon {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.roi-home-header__title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-tabs-admin {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-link {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.roi-home-header__content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.roi-components-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.roi-component-minicard {
|
||||
padding: 1rem 0.75rem;
|
||||
}
|
||||
|
||||
.roi-component-minicard__icon-bg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.roi-component-minicard__icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.roi-group-card__icon-wrapper {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.roi-group-card__icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-tabs-admin {
|
||||
overflow-x: auto;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-item {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.roi-groups-grid {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.roi-home-header {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.roi-home-header__title {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.roi-home-header__subtitle {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.roi-group-card {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.roi-group-card__title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.roi-components-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.roi-component-minicard {
|
||||
flex-direction: row;
|
||||
text-align: left;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
521
Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js
Normal file
521
Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js
Normal file
@@ -0,0 +1,521 @@
|
||||
/**
|
||||
* JavaScript para el Dashboard del Panel de Administración ROI Theme
|
||||
* Vanilla JavaScript - No frameworks
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Inicializa el dashboard cuando el DOM está listo
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Nueva navegación por Cards/Grupos
|
||||
initializeCardNavigation();
|
||||
|
||||
// Funcionalidad existente (solo si hay tabs visibles)
|
||||
if (document.querySelector('.nav-tabs-admin')) {
|
||||
initializeTabs();
|
||||
}
|
||||
|
||||
initializeFormValidation();
|
||||
initializeButtons();
|
||||
initializeColorPickers();
|
||||
});
|
||||
|
||||
/**
|
||||
* Inicializa la navegación por Cards/Grupos (App-Style)
|
||||
*/
|
||||
function initializeCardNavigation() {
|
||||
// Verificar que estamos en el panel correcto
|
||||
const adminPanel = document.querySelector('.roi-admin-panel');
|
||||
if (!adminPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delegación de eventos para mini-cards
|
||||
document.addEventListener('click', function(e) {
|
||||
const minicard = e.target.closest('.roi-component-minicard');
|
||||
if (minicard) {
|
||||
e.preventDefault();
|
||||
const componentId = minicard.getAttribute('data-component-id');
|
||||
if (componentId) {
|
||||
navigateToComponent(componentId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Botón volver al home
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.roi-back-to-home')) {
|
||||
e.preventDefault();
|
||||
navigateToHome();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navega a un componente específico
|
||||
*
|
||||
* @param {string} componentId ID del componente en kebab-case
|
||||
*/
|
||||
function navigateToComponent(componentId) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('component', componentId);
|
||||
// Eliminar el parámetro admin-tab si existe (legacy)
|
||||
url.searchParams.delete('admin-tab');
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navega de vuelta al home (vista de grupos)
|
||||
*/
|
||||
function navigateToHome() {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('component');
|
||||
url.searchParams.delete('admin-tab');
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa el sistema de tabs con persistencia en URL
|
||||
*/
|
||||
function initializeTabs() {
|
||||
const tabButtons = document.querySelectorAll('[data-bs-toggle="tab"]');
|
||||
|
||||
// Leer parametro admin-tab de la URL al cargar
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const activeTabParam = urlParams.get('admin-tab');
|
||||
|
||||
if (activeTabParam) {
|
||||
// Buscar el boton del tab correspondiente
|
||||
const targetButton = document.querySelector('[data-bs-target="#' + activeTabParam + 'Tab"]');
|
||||
if (targetButton) {
|
||||
// Activar el tab usando Bootstrap API
|
||||
const tab = new bootstrap.Tab(targetButton);
|
||||
tab.show();
|
||||
}
|
||||
}
|
||||
|
||||
// Escuchar cambios de tab para actualizar URL
|
||||
tabButtons.forEach(function(tabButton) {
|
||||
tabButton.addEventListener('shown.bs.tab', function(e) {
|
||||
// Obtener el ID del componente desde data-bs-target
|
||||
const target = e.target.getAttribute('data-bs-target');
|
||||
const componentId = target.replace('#', '').replace('Tab', '');
|
||||
|
||||
// Actualizar URL sin recargar pagina
|
||||
updateUrlWithTab(componentId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza la URL con el parametro admin-tab sin recargar la pagina
|
||||
*
|
||||
* @param {string} tabId ID del tab activo
|
||||
*/
|
||||
function updateUrlWithTab(tabId) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('admin-tab', tabId);
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el ID del tab activo actualmente
|
||||
*
|
||||
* @returns {string|null} ID del componente activo o null
|
||||
*/
|
||||
function getActiveTabId() {
|
||||
const activeTab = document.querySelector('.tab-pane.active');
|
||||
if (activeTab) {
|
||||
return activeTab.id.replace('Tab', '');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa validación de formularios
|
||||
*/
|
||||
function initializeFormValidation() {
|
||||
const forms = document.querySelectorAll('.roi-component-config form');
|
||||
|
||||
forms.forEach(function(form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (!validateForm(form)) {
|
||||
e.preventDefault();
|
||||
showError('Por favor, corrige los errores en el formulario.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida un formulario
|
||||
*
|
||||
* @param {HTMLFormElement} form Formulario a validar
|
||||
* @returns {boolean} True si es válido
|
||||
*/
|
||||
function validateForm(form) {
|
||||
let isValid = true;
|
||||
const requiredFields = form.querySelectorAll('[required]');
|
||||
|
||||
requiredFields.forEach(function(field) {
|
||||
if (!field.value.trim()) {
|
||||
field.classList.add('error');
|
||||
isValid = false;
|
||||
} else {
|
||||
field.classList.remove('error');
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra un mensaje de error
|
||||
*
|
||||
* @param {string} message Mensaje a mostrar
|
||||
*/
|
||||
function showError(message) {
|
||||
const notice = document.createElement('div');
|
||||
notice.className = 'notice notice-error is-dismissible';
|
||||
notice.innerHTML = '<p>' + escapeHtml(message) + '</p>';
|
||||
|
||||
const h1 = document.querySelector('.roi-admin-dashboard h1');
|
||||
if (h1 && h1.nextElementSibling) {
|
||||
h1.nextElementSibling.after(notice);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapa HTML para prevenir XSS
|
||||
*
|
||||
* @param {string} text Texto a escapar
|
||||
* @returns {string} Texto escapado
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa los botones del panel
|
||||
*/
|
||||
function initializeButtons() {
|
||||
// Botón Guardar Cambios
|
||||
const saveButton = document.getElementById('saveSettings');
|
||||
if (saveButton) {
|
||||
saveButton.addEventListener('click', handleSaveSettings);
|
||||
}
|
||||
|
||||
// Botón Cancelar
|
||||
const cancelButton = document.getElementById('cancelChanges');
|
||||
if (cancelButton) {
|
||||
cancelButton.addEventListener('click', handleCancelChanges);
|
||||
}
|
||||
|
||||
// Botones Restaurar valores por defecto (dinámico para todos los componentes)
|
||||
const resetButtons = document.querySelectorAll('.btn-reset-defaults[data-component]');
|
||||
resetButtons.forEach(function(button) {
|
||||
button.addEventListener('click', function(e) {
|
||||
const componentId = this.getAttribute('data-component');
|
||||
handleResetDefaults(e, componentId, this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarda los cambios del formulario
|
||||
*/
|
||||
function handleSaveSettings(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Obtener el tab activo
|
||||
const activeTab = document.querySelector('.tab-pane.active');
|
||||
if (!activeTab) {
|
||||
showNotice('error', 'No hay ningún componente seleccionado.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Obtener el ID del componente desde el tab
|
||||
const componentId = activeTab.id.replace('Tab', '');
|
||||
|
||||
// Recopilar todos los campos del formulario activo
|
||||
const formData = collectFormData(activeTab);
|
||||
|
||||
// Mostrar loading en el botón
|
||||
const saveButton = document.getElementById('saveSettings');
|
||||
const originalText = saveButton.innerHTML;
|
||||
saveButton.disabled = true;
|
||||
saveButton.innerHTML = '<i class="bi bi-hourglass-split me-1"></i> Guardando...';
|
||||
|
||||
// Enviar por AJAX
|
||||
fetch(ajaxurl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
action: 'roi_save_component_settings',
|
||||
nonce: roiAdminDashboard.nonce,
|
||||
component: componentId,
|
||||
settings: JSON.stringify(formData)
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotice('success', data.data.message || 'Cambios guardados correctamente.');
|
||||
} else {
|
||||
showNotice('error', data.data.message || 'Error al guardar los cambios.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotice('error', 'Error de conexión al guardar los cambios.');
|
||||
})
|
||||
.finally(() => {
|
||||
saveButton.disabled = false;
|
||||
saveButton.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancela los cambios y recarga la página
|
||||
*/
|
||||
function handleCancelChanges(e) {
|
||||
e.preventDefault();
|
||||
showConfirmModal(
|
||||
'Cancelar cambios',
|
||||
'¿Descartar todos los cambios no guardados?',
|
||||
function() {
|
||||
location.reload();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restaura los valores por defecto de un componente
|
||||
*
|
||||
* @param {Event} e Evento del click
|
||||
* @param {string} componentId ID del componente a resetear
|
||||
* @param {HTMLElement} resetButton Elemento del botón que disparó el evento
|
||||
*/
|
||||
function handleResetDefaults(e, componentId, resetButton) {
|
||||
e.preventDefault();
|
||||
|
||||
showConfirmModal(
|
||||
'Restaurar valores por defecto',
|
||||
'¿Restaurar todos los valores a los valores por defecto? Esta acción no se puede deshacer.',
|
||||
function() {
|
||||
// Mostrar loading en el botón
|
||||
const originalText = resetButton.innerHTML;
|
||||
resetButton.disabled = true;
|
||||
resetButton.innerHTML = '<i class="bi bi-hourglass-split me-1"></i> Restaurando...';
|
||||
|
||||
// Enviar por AJAX
|
||||
fetch(ajaxurl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
action: 'roi_reset_component_defaults',
|
||||
nonce: roiAdminDashboard.nonce,
|
||||
component: componentId
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotice('success', data.data.message || 'Valores restaurados correctamente.');
|
||||
// Recargar preservando el tab activo
|
||||
setTimeout(() => {
|
||||
const activeTabId = getActiveTabId();
|
||||
if (activeTabId) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('admin-tab', activeTabId);
|
||||
window.location.href = url.toString();
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
}, 1500);
|
||||
} else {
|
||||
showNotice('error', data.data.message || 'Error al restaurar los valores.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotice('error', 'Error de conexión al restaurar los valores.');
|
||||
})
|
||||
.finally(() => {
|
||||
resetButton.disabled = false;
|
||||
resetButton.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recopila los datos del formulario del tab activo
|
||||
*/
|
||||
function collectFormData(container) {
|
||||
const formData = {};
|
||||
|
||||
// Inputs de texto, textarea, select, color, number, email, password
|
||||
const textInputs = container.querySelectorAll('input[type="text"], input[type="url"], input[type="color"], input[type="number"], input[type="email"], input[type="password"], textarea, select');
|
||||
textInputs.forEach(input => {
|
||||
if (input.id) {
|
||||
formData[input.id] = input.value;
|
||||
}
|
||||
});
|
||||
|
||||
// Checkboxes (switches)
|
||||
const checkboxes = container.querySelectorAll('input[type="checkbox"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
if (checkbox.id) {
|
||||
formData[checkbox.id] = checkbox.checked;
|
||||
}
|
||||
});
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra un toast de Bootstrap
|
||||
*/
|
||||
function showNotice(type, message) {
|
||||
// Mapear tipos
|
||||
const typeMap = {
|
||||
'success': { bg: 'success', icon: 'bi-check-circle-fill', text: 'Éxito' },
|
||||
'error': { bg: 'danger', icon: 'bi-x-circle-fill', text: 'Error' },
|
||||
'warning': { bg: 'warning', icon: 'bi-exclamation-triangle-fill', text: 'Advertencia' },
|
||||
'info': { bg: 'info', icon: 'bi-info-circle-fill', text: 'Información' }
|
||||
};
|
||||
|
||||
const config = typeMap[type] || typeMap['info'];
|
||||
|
||||
// Crear container de toasts si no existe
|
||||
let toastContainer = document.getElementById('roiToastContainer');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.id = 'roiToastContainer';
|
||||
toastContainer.className = 'toast-container position-fixed start-50 translate-middle-x';
|
||||
toastContainer.style.top = '60px';
|
||||
toastContainer.style.zIndex = '999999';
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
// Crear toast
|
||||
const toastId = 'toast-' + Date.now();
|
||||
const toastHTML = `
|
||||
<div id="${toastId}" class="toast align-items-center text-white bg-${config.bg} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<i class="bi ${config.icon} me-2"></i>
|
||||
<strong>${escapeHtml(message)}</strong>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toastContainer.insertAdjacentHTML('beforeend', toastHTML);
|
||||
|
||||
// Mostrar toast
|
||||
const toastElement = document.getElementById(toastId);
|
||||
const toast = new bootstrap.Toast(toastElement, {
|
||||
autohide: true,
|
||||
delay: 5000
|
||||
});
|
||||
|
||||
toast.show();
|
||||
|
||||
// Eliminar del DOM después de ocultarse
|
||||
toastElement.addEventListener('hidden.bs.toast', function() {
|
||||
toastElement.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra un modal de confirmación de Bootstrap
|
||||
*/
|
||||
function showConfirmModal(title, message, onConfirm) {
|
||||
// Crear modal si no existe
|
||||
let modal = document.getElementById('roiConfirmModal');
|
||||
if (!modal) {
|
||||
const modalHTML = `
|
||||
<div class="modal fade" id="roiConfirmModal" tabindex="-1" aria-labelledby="roiConfirmModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-bottom: none;">
|
||||
<h5 class="modal-title text-white" id="roiConfirmModalLabel">
|
||||
<i class="bi bi-question-circle me-2" style="color: #FF8600;"></i>
|
||||
<span id="roiConfirmModalTitle">Confirmar</span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="roiConfirmModalBody" style="padding: 2rem;">
|
||||
Mensaje de confirmación
|
||||
</div>
|
||||
<div class="modal-footer" style="border-top: 1px solid #dee2e6; padding: 1rem 1.5rem;">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
||||
<i class="bi bi-x-circle me-1"></i>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="button" class="btn text-white" id="roiConfirmModalConfirm" style="background-color: #FF8600; border-color: #FF8600;">
|
||||
<i class="bi bi-check-circle me-1"></i>
|
||||
Confirmar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||
modal = document.getElementById('roiConfirmModal');
|
||||
}
|
||||
|
||||
// Actualizar contenido
|
||||
document.getElementById('roiConfirmModalTitle').textContent = title;
|
||||
document.getElementById('roiConfirmModalBody').textContent = message;
|
||||
|
||||
// Configurar callback
|
||||
const confirmButton = document.getElementById('roiConfirmModalConfirm');
|
||||
const newConfirmButton = confirmButton.cloneNode(true);
|
||||
confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton);
|
||||
|
||||
newConfirmButton.addEventListener('click', function() {
|
||||
const bsModal = bootstrap.Modal.getInstance(modal);
|
||||
bsModal.hide();
|
||||
if (typeof onConfirm === 'function') {
|
||||
onConfirm();
|
||||
}
|
||||
});
|
||||
|
||||
// Mostrar modal
|
||||
const bsModal = new bootstrap.Modal(modal);
|
||||
bsModal.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa los color pickers para mostrar el valor HEX
|
||||
*/
|
||||
function initializeColorPickers() {
|
||||
const colorPickers = document.querySelectorAll('input[type="color"]');
|
||||
|
||||
colorPickers.forEach(picker => {
|
||||
// Elemento donde se muestra el valor HEX
|
||||
const valueDisplay = document.getElementById(picker.id + 'Value');
|
||||
|
||||
if (valueDisplay) {
|
||||
picker.addEventListener('input', function() {
|
||||
valueDisplay.textContent = this.value.toUpperCase();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
104
Admin/Infrastructure/Ui/ComponentGroupRegistry.php
Normal file
104
Admin/Infrastructure/Ui/ComponentGroupRegistry.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Infrastructure\Ui;
|
||||
|
||||
/**
|
||||
* Registro de grupos de componentes para el admin panel
|
||||
*
|
||||
* Responsabilidad única: Gestionar la configuración de grupos
|
||||
* y la asignación de componentes a grupos.
|
||||
*
|
||||
* @package ROITheme\Admin\Infrastructure\Ui
|
||||
*/
|
||||
final class ComponentGroupRegistry
|
||||
{
|
||||
/**
|
||||
* Obtiene los grupos de componentes con sus configuraciones
|
||||
*
|
||||
* Los grupos son extensibles via filtro WordPress para permitir
|
||||
* que plugins agreguen componentes a grupos existentes o creen nuevos.
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public function getGroups(): array
|
||||
{
|
||||
// Design System: Todos los grupos usan el mismo gradiente Navy (#0E2337 → #1e3a5f)
|
||||
// No se requiere propiedad 'color' ya que está definido en CSS
|
||||
$defaultGroups = [
|
||||
'header-navigation' => [
|
||||
'label' => __('Header & Navegación', 'roi-theme'),
|
||||
'icon' => 'bi-layout-text-window',
|
||||
'description' => __('Barras superiores, menú y pie de página', 'roi-theme'),
|
||||
'components' => ['top-notification-bar', 'navbar', 'footer']
|
||||
],
|
||||
'main-content' => [
|
||||
'label' => __('Contenido Principal', 'roi-theme'),
|
||||
'icon' => 'bi-file-richtext',
|
||||
'description' => __('Secciones principales de páginas y posts', 'roi-theme'),
|
||||
'components' => ['hero', 'featured-image', 'table-of-contents', 'related-post']
|
||||
],
|
||||
'ctas-conversion' => [
|
||||
'label' => __('CTAs & Conversión', 'roi-theme'),
|
||||
'icon' => 'bi-lightning-charge',
|
||||
'description' => __('Llamadas a la acción y elementos de conversión', 'roi-theme'),
|
||||
'components' => ['cta-lets-talk', 'cta-box-sidebar', 'cta-post']
|
||||
],
|
||||
'engagement' => [
|
||||
'label' => __('Engagement', 'roi-theme'),
|
||||
'icon' => 'bi-share',
|
||||
'description' => __('Interacción social y compartir', 'roi-theme'),
|
||||
'components' => ['social-share']
|
||||
],
|
||||
'forms' => [
|
||||
'label' => __('Formularios', 'roi-theme'),
|
||||
'icon' => 'bi-envelope-paper',
|
||||
'description' => __('Formularios de contacto y captura', 'roi-theme'),
|
||||
'components' => ['contact-form']
|
||||
],
|
||||
'settings' => [
|
||||
'label' => __('Configuración', 'roi-theme'),
|
||||
'icon' => 'bi-gear',
|
||||
'description' => __('Ajustes globales del tema y monetización', 'roi-theme'),
|
||||
'components' => ['theme-settings', 'adsense-placement', 'custom-css-manager']
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Filtro para extender o modificar los grupos de componentes
|
||||
*
|
||||
* @param array<string, array<string, mixed>> $groups Grupos por defecto
|
||||
* @return array<string, array<string, mixed>> Grupos modificados
|
||||
*/
|
||||
return apply_filters('roi_theme_component_groups', $defaultGroups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el grupo al que pertenece un componente
|
||||
*
|
||||
* @param string $componentId ID del componente en kebab-case
|
||||
* @return string|null ID del grupo o null si no pertenece a ninguno
|
||||
*/
|
||||
public function getGroupForComponent(string $componentId): ?string
|
||||
{
|
||||
foreach ($this->getGroups() as $groupId => $group) {
|
||||
if (in_array($componentId, $group['components'], true)) {
|
||||
return $groupId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la información de un grupo específico
|
||||
*
|
||||
* @param string $groupId ID del grupo
|
||||
* @return array<string, mixed>|null Datos del grupo o null
|
||||
*/
|
||||
public function getGroup(string $groupId): ?array
|
||||
{
|
||||
$groups = $this->getGroups();
|
||||
return $groups[$groupId] ?? null;
|
||||
}
|
||||
}
|
||||
48
Admin/Infrastructure/Ui/Views/dashboard.php
Normal file
48
Admin/Infrastructure/Ui/Views/dashboard.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/**
|
||||
* ROI Theme - Panel de Administración Principal
|
||||
*
|
||||
* Nueva UI con sistema de Cards/Grupos (App-Style Navigation)
|
||||
*
|
||||
* @var AdminDashboardRenderer $this
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Prevenir acceso directo
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$components = $this->getComponents();
|
||||
$groups = $this->getComponentGroups();
|
||||
|
||||
// =====================================================
|
||||
// SANITIZACIÓN OBLIGATORIA según estándares WordPress
|
||||
// =====================================================
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Solo lectura de parámetro para UI
|
||||
$activeComponent = null;
|
||||
if (isset($_GET['component'])) {
|
||||
$requestedComponent = sanitize_text_field(wp_unslash($_GET['component']));
|
||||
// Validar que el componente exista
|
||||
if (array_key_exists($requestedComponent, $components)) {
|
||||
$activeComponent = $requestedComponent;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="wrap roi-admin-panel">
|
||||
|
||||
<?php if ($activeComponent !== null): ?>
|
||||
<!-- =====================================================
|
||||
Vista de Componente Individual
|
||||
===================================================== -->
|
||||
<?php include __DIR__ . '/partials/component-view.php'; ?>
|
||||
<?php else: ?>
|
||||
<!-- =====================================================
|
||||
Vista Home: Grupos y Cards
|
||||
===================================================== -->
|
||||
<?php include __DIR__ . '/partials/groups-home.php'; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
</div><!-- /wrap -->
|
||||
48
Admin/Infrastructure/Ui/Views/partials/breadcrumb.php
Normal file
48
Admin/Infrastructure/Ui/Views/partials/breadcrumb.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/**
|
||||
* Breadcrumb de navegación
|
||||
*
|
||||
* @var AdminDashboardRenderer $this
|
||||
* @var string $activeComponent ID del componente activo
|
||||
* @var array<string, array<string, mixed>> $groups Grupos de componentes
|
||||
* @var array<string, array<string, string>> $components Componentes disponibles
|
||||
* @var array<string, mixed>|null $group Grupo del componente activo
|
||||
* @var array<string, string>|null $component Datos del componente activo
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<nav class="roi-breadcrumb mb-4" aria-label="<?php echo esc_attr__('Navegación', 'roi-theme'); ?>">
|
||||
<div class="d-flex align-items-center flex-wrap gap-2">
|
||||
<!-- Botón Volver -->
|
||||
<button type="button" class="roi-back-to-home btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>
|
||||
<?php echo esc_html__('Volver', 'roi-theme'); ?>
|
||||
</button>
|
||||
|
||||
<!-- Separador -->
|
||||
<span class="roi-breadcrumb__separator text-muted">/</span>
|
||||
|
||||
<!-- Grupo -->
|
||||
<?php if ($group): ?>
|
||||
<span class="roi-breadcrumb__group">
|
||||
<i class="bi <?php echo esc_attr($group['icon']); ?> me-1" style="color: <?php echo esc_attr($group['color']); ?>;"></i>
|
||||
<?php echo esc_html($group['label']); ?>
|
||||
</span>
|
||||
<span class="roi-breadcrumb__separator text-muted">/</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Componente actual -->
|
||||
<?php if ($component): ?>
|
||||
<span class="roi-breadcrumb__current fw-semibold" aria-current="page" style="color: #FF8600;">
|
||||
<i class="bi <?php echo esc_attr($component['icon']); ?> me-1"></i>
|
||||
<?php echo esc_html($component['label']); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</nav>
|
||||
74
Admin/Infrastructure/Ui/Views/partials/component-view.php
Normal file
74
Admin/Infrastructure/Ui/Views/partials/component-view.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/**
|
||||
* Vista de Componente Individual con Breadcrumb
|
||||
*
|
||||
* @var AdminDashboardRenderer $this
|
||||
* @var string $activeComponent ID del componente activo
|
||||
* @var array<string, array<string, mixed>> $groups Grupos de componentes
|
||||
* @var array<string, array<string, string>> $components Componentes disponibles
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$component = $components[$activeComponent] ?? null;
|
||||
$groupId = $this->getGroupForComponent($activeComponent);
|
||||
$group = $groupId && isset($groups[$groupId]) ? $groups[$groupId] : null;
|
||||
?>
|
||||
|
||||
<div id="roi-component-view">
|
||||
<!-- Breadcrumb Navigation -->
|
||||
<?php include __DIR__ . '/breadcrumb.php'; ?>
|
||||
|
||||
<!-- Component Form Container -->
|
||||
<!-- IMPORTANTE: El tab-pane con clase .active es necesario para que el JS
|
||||
de handleSaveSettings() pueda encontrar los campos del formulario -->
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active"
|
||||
id="<?php echo esc_attr($activeComponent); ?>Tab"
|
||||
role="tabpanel">
|
||||
|
||||
<div class="roi-component-form-container">
|
||||
<?php
|
||||
// Renderizar FormBuilder del componente
|
||||
$formBuilderClass = $this->getFormBuilderClass($activeComponent);
|
||||
if (class_exists($formBuilderClass)) {
|
||||
$formBuilder = new $formBuilderClass($this);
|
||||
echo $formBuilder->buildForm($activeComponent);
|
||||
} else {
|
||||
?>
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<?php
|
||||
echo esc_html(
|
||||
sprintf(
|
||||
/* translators: %s: FormBuilder class name */
|
||||
__('FormBuilder no encontrado: %s', 'roi-theme'),
|
||||
$formBuilderClass
|
||||
)
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botones Globales Save/Cancel -->
|
||||
<div class="d-flex justify-content-end gap-2 p-3 rounded border mt-4" style="background-color: #f8f9fa; border-color: #e9ecef !important;">
|
||||
<button type="button" class="btn btn-outline-secondary" id="cancelChanges">
|
||||
<i class="bi bi-x-circle me-1"></i>
|
||||
<?php echo esc_html__('Cancelar', 'roi-theme'); ?>
|
||||
</button>
|
||||
<button type="button" id="saveSettings" class="btn fw-semibold text-white" style="background-color: #FF8600; border-color: #FF8600;">
|
||||
<i class="bi bi-check-circle me-1"></i>
|
||||
<?php echo esc_html__('Guardar Cambios', 'roi-theme'); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
92
Admin/Infrastructure/Ui/Views/partials/groups-home.php
Normal file
92
Admin/Infrastructure/Ui/Views/partials/groups-home.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
/**
|
||||
* Vista Home: Grupos de componentes con mini-cards (Improved Version)
|
||||
*
|
||||
* @var AdminDashboardRenderer $this
|
||||
* @var array<string, array<string, mixed>> $groups Grupos de componentes
|
||||
* @var array<string, array<string, string>> $components Componentes disponibles
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<div id="roi-home-view">
|
||||
<!-- Header Mejorado -->
|
||||
<div class="roi-home-header">
|
||||
<div class="roi-home-header__pattern"></div>
|
||||
<div class="roi-home-header__content">
|
||||
<div class="roi-home-header__icon-wrapper">
|
||||
<i class="bi bi-grid-3x3-gap-fill roi-home-header__icon"></i>
|
||||
</div>
|
||||
<div class="roi-home-header__text">
|
||||
<h1 class="roi-home-header__title">
|
||||
<?php echo esc_html__('Panel de Administración ROI Theme', 'roi-theme'); ?>
|
||||
</h1>
|
||||
<p class="roi-home-header__subtitle">
|
||||
<?php echo esc_html__('Selecciona un componente para configurarlo y personalizarlo', 'roi-theme'); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid de Grupos Mejorado -->
|
||||
<div class="roi-groups-grid">
|
||||
<?php
|
||||
$delay = 0;
|
||||
foreach ($groups as $groupId => $group):
|
||||
?>
|
||||
<div class="roi-group-card"
|
||||
data-group-id="<?php echo esc_attr($groupId); ?>"
|
||||
style="animation-delay: <?php echo esc_attr($delay . 's'); ?>">
|
||||
<div class="roi-group-card__glow"></div>
|
||||
|
||||
<div class="roi-group-card__header">
|
||||
<div class="roi-group-card__icon-wrapper">
|
||||
<i class="bi <?php echo esc_attr($group['icon']); ?> roi-group-card__icon"></i>
|
||||
</div>
|
||||
<div class="roi-group-card__header-text">
|
||||
<h3 class="roi-group-card__title">
|
||||
<?php echo esc_html($group['label']); ?>
|
||||
</h3>
|
||||
<p class="roi-group-card__description">
|
||||
<?php echo esc_html($group['description']); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="roi-components-grid">
|
||||
<?php foreach ($group['components'] as $componentId): ?>
|
||||
<?php if (isset($components[$componentId])): ?>
|
||||
<?php $component = $components[$componentId]; ?>
|
||||
<button type="button"
|
||||
class="roi-component-minicard"
|
||||
data-component-id="<?php echo esc_attr($componentId); ?>"
|
||||
aria-label="<?php echo esc_attr(
|
||||
sprintf(
|
||||
/* translators: %s: component label */
|
||||
__('Configurar %s', 'roi-theme'),
|
||||
$component['label']
|
||||
)
|
||||
); ?>">
|
||||
<div class="roi-component-minicard__icon-bg">
|
||||
<i class="bi <?php echo esc_attr($component['icon']); ?> roi-component-minicard__icon"></i>
|
||||
</div>
|
||||
<span class="roi-component-minicard__label">
|
||||
<?php echo esc_html($component['label']); ?>
|
||||
</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?php
|
||||
$delay += 0.1;
|
||||
endforeach;
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Navbar\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Navbar
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class NavbarFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'navbar';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'navbarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'navbarShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'navbarShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'navbarSticky' => ['group' => 'visibility', 'attribute' => 'sticky_enabled'],
|
||||
'navbarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'navbarVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'navbarVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'navbarVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'navbarVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'navbarVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'navbarExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'navbarExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'navbarExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'navbarExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Layout
|
||||
'navbarContainerType' => ['group' => 'layout', 'attribute' => 'container_type'],
|
||||
'navbarPaddingVertical' => ['group' => 'layout', 'attribute' => 'padding_vertical'],
|
||||
'navbarZIndex' => ['group' => 'layout', 'attribute' => 'z_index'],
|
||||
|
||||
// Behavior
|
||||
'navbarMenuLocation' => ['group' => 'behavior', 'attribute' => 'menu_location'],
|
||||
'navbarCustomMenuId' => ['group' => 'behavior', 'attribute' => 'custom_menu_id'],
|
||||
'navbarEnableDropdowns' => ['group' => 'behavior', 'attribute' => 'enable_dropdowns'],
|
||||
'navbarMobileBreakpoint' => ['group' => 'behavior', 'attribute' => 'mobile_breakpoint'],
|
||||
|
||||
// Media (Logo/Marca)
|
||||
'navbarShowBrand' => ['group' => 'media', 'attribute' => 'show_brand'],
|
||||
'navbarUseLogo' => ['group' => 'media', 'attribute' => 'use_logo'],
|
||||
'navbarLogoUrl' => ['group' => 'media', 'attribute' => 'logo_url'],
|
||||
'navbarLogoHeight' => ['group' => 'media', 'attribute' => 'logo_height'],
|
||||
'navbarBrandText' => ['group' => 'media', 'attribute' => 'brand_text'],
|
||||
'navbarBrandFontSize' => ['group' => 'media', 'attribute' => 'brand_font_size'],
|
||||
'navbarBrandColor' => ['group' => 'media', 'attribute' => 'brand_color'],
|
||||
'navbarBrandHoverColor' => ['group' => 'media', 'attribute' => 'brand_hover_color'],
|
||||
|
||||
// Links
|
||||
'linksTextColor' => ['group' => 'links', 'attribute' => 'text_color'],
|
||||
'linksHoverColor' => ['group' => 'links', 'attribute' => 'hover_color'],
|
||||
'linksActiveColor' => ['group' => 'links', 'attribute' => 'active_color'],
|
||||
'linksFontSize' => ['group' => 'links', 'attribute' => 'font_size'],
|
||||
'linksFontWeight' => ['group' => 'links', 'attribute' => 'font_weight'],
|
||||
'linksPadding' => ['group' => 'links', 'attribute' => 'padding'],
|
||||
'linksBorderRadius' => ['group' => 'links', 'attribute' => 'border_radius'],
|
||||
'linksShowUnderline' => ['group' => 'links', 'attribute' => 'show_underline_effect'],
|
||||
'linksUnderlineColor' => ['group' => 'links', 'attribute' => 'underline_color'],
|
||||
|
||||
// Visual Effects (Dropdown)
|
||||
'dropdownBgColor' => ['group' => 'visual_effects', 'attribute' => 'background_color'],
|
||||
'dropdownBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'dropdownShadow' => ['group' => 'visual_effects', 'attribute' => 'shadow'],
|
||||
'dropdownItemColor' => ['group' => 'visual_effects', 'attribute' => 'item_color'],
|
||||
'dropdownItemHoverBg' => ['group' => 'visual_effects', 'attribute' => 'item_hover_background'],
|
||||
'dropdownItemPadding' => ['group' => 'visual_effects', 'attribute' => 'item_padding'],
|
||||
'dropdownMaxHeight' => ['group' => 'visual_effects', 'attribute' => 'dropdown_max_height'],
|
||||
|
||||
// Colors (Navbar styles)
|
||||
'navbarBgColor' => ['group' => 'colors', 'attribute' => 'background_color'],
|
||||
'navbarBoxShadow' => ['group' => 'colors', 'attribute' => 'box_shadow'],
|
||||
];
|
||||
}
|
||||
}
|
||||
582
Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php
Normal file
582
Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php
Normal file
@@ -0,0 +1,582 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Navbar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
final class NavbarFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
// Header
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
// Layout 2 columnas
|
||||
$html .= '<div class="row g-3">';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildLayoutGroup($componentId);
|
||||
$html .= $this->buildBehaviorGroup($componentId);
|
||||
$html .= $this->buildMediaGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildLinksGroup($componentId);
|
||||
$html .= $this->buildVisualEffectsGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-menu-button-wide me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuración de Navbar';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Personaliza el menú de navegación principal del sitio';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="navbar">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Activación y Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Switch: Enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarEnabled" name="visibility[is_enabled]" ';
|
||||
$html .= checked($enabled, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarEnabled">';
|
||||
$html .= ' <strong>Activar Navbar</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Mobile
|
||||
$showMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarShowMobile" name="visibility[show_on_mobile]" ';
|
||||
$html .= checked($showMobile, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarShowMobile">';
|
||||
$html .= ' <strong>Mostrar en Mobile</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Desktop
|
||||
$showDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarShowDesktop" name="visibility[show_on_desktop]" ';
|
||||
$html .= checked($showDesktop, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarShowDesktop">';
|
||||
$html .= ' <strong>Mostrar en Desktop</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', true);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', true);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'navbar');
|
||||
|
||||
// Switch: Sticky
|
||||
$sticky = $this->renderer->getFieldValue($componentId, 'visibility', 'sticky_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarSticky" name="visibility[sticky_enabled]" ';
|
||||
$html .= checked($sticky, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarSticky">';
|
||||
$html .= ' <strong>Navbar fijo (sticky)</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: CSS Crítico
|
||||
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarIsCritical" name="visibility[is_critical]" ';
|
||||
$html .= checked($isCritical, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarIsCritical">';
|
||||
$html .= ' <strong>CSS Crítico</strong>';
|
||||
$html .= ' <small class="text-muted d-block">Inyectar CSS en <head> para optimizar LCP</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildLayoutGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-layout-sidebar me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Layout y Estructura';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Container Type
|
||||
$containerType = $this->renderer->getFieldValue($componentId, 'layout', 'container_type', 'container');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarContainerType" class="form-label small mb-1 fw-semibold">Tipo de contenedor</label>';
|
||||
$html .= ' <select id="navbarContainerType" name="layout[container_type]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="container" ' . selected($containerType, 'container', false) . '>Container (ancho fijo)</option>';
|
||||
$html .= ' <option value="container-fluid" ' . selected($containerType, 'container-fluid', false) . '>Container Fluid (ancho completo)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Padding Vertical
|
||||
$paddingVertical = $this->renderer->getFieldValue($componentId, 'layout', 'padding_vertical', '0.75rem 0');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarPaddingVertical" class="form-label small mb-1 fw-semibold">Padding vertical</label>';
|
||||
$html .= ' <input type="text" id="navbarPaddingVertical" name="layout[padding_vertical]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($paddingVertical) . '" placeholder="0.75rem 0">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Z-index
|
||||
$zIndex = $this->renderer->getFieldValue($componentId, 'layout', 'z_index', '1030');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="navbarZIndex" class="form-label small mb-1 fw-semibold">Z-index</label>';
|
||||
$html .= ' <input type="text" id="navbarZIndex" name="layout[z_index]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($zIndex) . '" placeholder="1030">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildBehaviorGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-list me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuración del Menú';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Menu Location
|
||||
$menuLocation = $this->renderer->getFieldValue($componentId, 'behavior', 'menu_location', 'primary');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarMenuLocation" class="form-label small mb-1 fw-semibold">Ubicación del menú</label>';
|
||||
$html .= ' <select id="navbarMenuLocation" name="behavior[menu_location]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="primary" ' . selected($menuLocation, 'primary', false) . '>Menú Principal</option>';
|
||||
$html .= ' <option value="secondary" ' . selected($menuLocation, 'secondary', false) . '>Menú Secundario</option>';
|
||||
$html .= ' <option value="custom" ' . selected($menuLocation, 'custom', false) . '>Menú personalizado</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Custom Menu ID
|
||||
$customMenuId = $this->renderer->getFieldValue($componentId, 'behavior', 'custom_menu_id', '0');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarCustomMenuId" class="form-label small mb-1 fw-semibold">ID del menú personalizado</label>';
|
||||
$html .= ' <input type="text" id="navbarCustomMenuId" name="behavior[custom_menu_id]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($customMenuId) . '" placeholder="0">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Enable Dropdowns
|
||||
$enableDropdowns = $this->renderer->getFieldValue($componentId, 'behavior', 'enable_dropdowns', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarEnableDropdowns" name="behavior[enable_dropdowns]" ';
|
||||
$html .= checked($enableDropdowns, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarEnableDropdowns">';
|
||||
$html .= ' <strong>Habilitar submenús desplegables</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Mobile Breakpoint
|
||||
$mobileBreakpoint = $this->renderer->getFieldValue($componentId, 'behavior', 'mobile_breakpoint', 'lg');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="navbarMobileBreakpoint" class="form-label small mb-1 fw-semibold">Breakpoint para menú móvil</label>';
|
||||
$html .= ' <select id="navbarMobileBreakpoint" name="behavior[mobile_breakpoint]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="sm" ' . selected($mobileBreakpoint, 'sm', false) . '>Small (576px)</option>';
|
||||
$html .= ' <option value="md" ' . selected($mobileBreakpoint, 'md', false) . '>Medium (768px)</option>';
|
||||
$html .= ' <option value="lg" ' . selected($mobileBreakpoint, 'lg', false) . '>Large (992px)</option>';
|
||||
$html .= ' <option value="xl" ' . selected($mobileBreakpoint, 'xl', false) . '>Extra Large (1200px)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildMediaGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-image me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Logo/Marca';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Show Brand
|
||||
$showBrand = $this->renderer->getFieldValue($componentId, 'media', 'show_brand', false);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarShowBrand" name="media[show_brand]" ';
|
||||
$html .= checked($showBrand, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarShowBrand">';
|
||||
$html .= ' <strong>Mostrar logo/marca</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Use Logo
|
||||
$useLogo = $this->renderer->getFieldValue($componentId, 'media', 'use_logo', false);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarUseLogo" name="media[use_logo]" ';
|
||||
$html .= checked($useLogo, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarUseLogo">';
|
||||
$html .= ' <strong>Usar logo (imagen)</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Logo URL
|
||||
$logoUrl = $this->renderer->getFieldValue($componentId, 'media', 'logo_url', '');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarLogoUrl" class="form-label small mb-1 fw-semibold">URL del logo</label>';
|
||||
$html .= ' <input type="text" id="navbarLogoUrl" name="media[logo_url]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($logoUrl) . '" placeholder="https://...">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Logo Height
|
||||
$logoHeight = $this->renderer->getFieldValue($componentId, 'media', 'logo_height', '40px');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarLogoHeight" class="form-label small mb-1 fw-semibold">Altura del logo</label>';
|
||||
$html .= ' <input type="text" id="navbarLogoHeight" name="media[logo_height]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($logoHeight) . '" placeholder="40px">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Brand Text
|
||||
$brandText = $this->renderer->getFieldValue($componentId, 'media', 'brand_text', 'Mi Sitio');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarBrandText" class="form-label small mb-1 fw-semibold">Texto de la marca</label>';
|
||||
$html .= ' <input type="text" id="navbarBrandText" name="media[brand_text]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($brandText) . '" maxlength="50">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Brand Font Size
|
||||
$brandFontSize = $this->renderer->getFieldValue($componentId, 'media', 'brand_font_size', '1.5rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarBrandFontSize" class="form-label small mb-1 fw-semibold">Tamaño de fuente</label>';
|
||||
$html .= ' <input type="text" id="navbarBrandFontSize" name="media[brand_font_size]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($brandFontSize) . '" placeholder="1.5rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Brand Color
|
||||
$brandColor = $this->renderer->getFieldValue($componentId, 'media', 'brand_color', '#FFFFFF');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarBrandColor" class="form-label small mb-1 fw-semibold">Color de la marca</label>';
|
||||
$html .= ' <input type="color" id="navbarBrandColor" name="media[brand_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($brandColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Brand Hover Color
|
||||
$brandHoverColor = $this->renderer->getFieldValue($componentId, 'media', 'brand_hover_color', '#FF8600');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="navbarBrandHoverColor" class="form-label small mb-1 fw-semibold">Color hover de la marca</label>';
|
||||
$html .= ' <input type="color" id="navbarBrandHoverColor" name="media[brand_hover_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($brandHoverColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildLinksGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-link-45deg me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Estilos de Enlaces';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Text Color
|
||||
$textColor = $this->renderer->getFieldValue($componentId, 'links', 'text_color', '#FFFFFF');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksTextColor" class="form-label small mb-1 fw-semibold">Color del texto</label>';
|
||||
$html .= ' <input type="color" id="linksTextColor" name="links[text_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($textColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Hover Color
|
||||
$hoverColor = $this->renderer->getFieldValue($componentId, 'links', 'hover_color', '#FF8600');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksHoverColor" class="form-label small mb-1 fw-semibold">Color hover</label>';
|
||||
$html .= ' <input type="color" id="linksHoverColor" name="links[hover_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($hoverColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Active Color
|
||||
$activeColor = $this->renderer->getFieldValue($componentId, 'links', 'active_color', '#FF8600');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksActiveColor" class="form-label small mb-1 fw-semibold">Color del item activo</label>';
|
||||
$html .= ' <input type="color" id="linksActiveColor" name="links[active_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($activeColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Font Size
|
||||
$fontSize = $this->renderer->getFieldValue($componentId, 'links', 'font_size', '0.9rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksFontSize" class="form-label small mb-1 fw-semibold">Tamaño de fuente</label>';
|
||||
$html .= ' <input type="text" id="linksFontSize" name="links[font_size]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($fontSize) . '" placeholder="0.9rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Font Weight
|
||||
$fontWeight = $this->renderer->getFieldValue($componentId, 'links', 'font_weight', '500');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksFontWeight" class="form-label small mb-1 fw-semibold">Grosor de fuente</label>';
|
||||
$html .= ' <input type="text" id="linksFontWeight" name="links[font_weight]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($fontWeight) . '" placeholder="500">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Padding
|
||||
$padding = $this->renderer->getFieldValue($componentId, 'links', 'padding', '0.5rem 0.65rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksPadding" class="form-label small mb-1 fw-semibold">Padding de enlaces</label>';
|
||||
$html .= ' <input type="text" id="linksPadding" name="links[padding]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($padding) . '" placeholder="0.5rem 0.65rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Border Radius
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'links', 'border_radius', '4px');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksBorderRadius" class="form-label small mb-1 fw-semibold">Border radius hover</label>';
|
||||
$html .= ' <input type="text" id="linksBorderRadius" name="links[border_radius]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '" placeholder="4px">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Show Underline Effect
|
||||
$showUnderline = $this->renderer->getFieldValue($componentId, 'links', 'show_underline_effect', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="linksShowUnderline" name="links[show_underline_effect]" ';
|
||||
$html .= checked($showUnderline, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="linksShowUnderline">';
|
||||
$html .= ' <strong>Mostrar efecto de subrayado</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Underline Color
|
||||
$underlineColor = $this->renderer->getFieldValue($componentId, 'links', 'underline_color', '#FF8600');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="linksUnderlineColor" class="form-label small mb-1 fw-semibold">Color del subrayado</label>';
|
||||
$html .= ' <input type="color" id="linksUnderlineColor" name="links[underline_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($underlineColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisualEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-chevron-down me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Estilos de Dropdown';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Background Color
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'visual_effects', 'background_color', '#FFFFFF');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownBgColor" class="form-label small mb-1 fw-semibold">Fondo de dropdown</label>';
|
||||
$html .= ' <input type="color" id="dropdownBgColor" name="visual_effects[background_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($bgColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Border Radius
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '8px');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownBorderRadius" class="form-label small mb-1 fw-semibold">Border radius</label>';
|
||||
$html .= ' <input type="text" id="dropdownBorderRadius" name="visual_effects[border_radius]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '" placeholder="8px">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Shadow
|
||||
$shadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'shadow', '0 8px 24px rgba(0, 0, 0, 0.12)');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownShadow" class="form-label small mb-1 fw-semibold">Sombra del dropdown</label>';
|
||||
$html .= ' <input type="text" id="dropdownShadow" name="visual_effects[shadow]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($shadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Item Color
|
||||
$itemColor = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_color', '#495057');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownItemColor" class="form-label small mb-1 fw-semibold">Color de items</label>';
|
||||
$html .= ' <input type="color" id="dropdownItemColor" name="visual_effects[item_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($itemColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Item Hover Background
|
||||
$itemHoverBg = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_hover_background', 'rgba(255, 133, 0, 0.1)');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownItemHoverBg" class="form-label small mb-1 fw-semibold">Fondo hover de items</label>';
|
||||
$html .= ' <input type="text" id="dropdownItemHoverBg" name="visual_effects[item_hover_background]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($itemHoverBg) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Item Padding
|
||||
$itemPadding = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_padding', '0.625rem 1.25rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownItemPadding" class="form-label small mb-1 fw-semibold">Padding de items</label>';
|
||||
$html .= ' <input type="text" id="dropdownItemPadding" name="visual_effects[item_padding]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($itemPadding) . '" placeholder="0.625rem 1.25rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Dropdown Max Height
|
||||
$dropdownMaxHeight = $this->renderer->getFieldValue($componentId, 'visual_effects', 'dropdown_max_height', '300px');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="dropdownMaxHeight" class="form-label small mb-1 fw-semibold">Altura máxima del dropdown</label>';
|
||||
$html .= ' <input type="text" id="dropdownMaxHeight" name="visual_effects[dropdown_max_height]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($dropdownMaxHeight) . '" placeholder="300px">';
|
||||
$html .= ' <small class="text-muted">Si se excede, aparece scroll vertical</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Estilos del Navbar';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Background Color
|
||||
$navbarBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#1e3a5f');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarBgColor" class="form-label small mb-1 fw-semibold">Color de fondo</label>';
|
||||
$html .= ' <input type="color" id="navbarBgColor" name="colors[background_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($navbarBgColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Box Shadow
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'colors', 'box_shadow', '0 4px 12px rgba(30, 58, 95, 0.15)');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="navbarBoxShadow" class="form-label small mb-1 fw-semibold">Sombra del navbar</label>';
|
||||
$html .= ' <input type="text" id="navbarBoxShadow" name="colors[box_shadow]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
544
Admin/Navbar/Infrastructure/Ui/navbar-design-preview.html
Normal file
544
Admin/Navbar/Infrastructure/Ui/navbar-design-preview.html
Normal file
@@ -0,0 +1,544 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Navbar - Preview de Diseño</title>
|
||||
|
||||
<!-- Bootstrap 5 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background-color: #f0f0f1;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ============================================================
|
||||
TAB: NAVBAR CONFIGURATION
|
||||
============================================================ -->
|
||||
<div class="tab-pane fade show active" id="navbarTab" role="tabpanel">
|
||||
|
||||
<!-- ========================================
|
||||
PATRÓN 1: HEADER CON GRADIENTE
|
||||
======================================== -->
|
||||
<div class="rounded p-4 mb-4 shadow text-white" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h3 class="h4 mb-1 fw-bold">
|
||||
<i class="bi bi-list-ul me-2" style="color: #FF8600;"></i>
|
||||
Configuración de Navbar
|
||||
</h3>
|
||||
<p class="mb-0 small" style="opacity: 0.85;">
|
||||
Personaliza el menú de navegación principal del sitio
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-light" id="resetNavbarDefaults">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
||||
Restaurar valores por defecto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
PATRÓN 2: LAYOUT 2 COLUMNAS
|
||||
======================================== -->
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
<!-- ========================================
|
||||
GRUPO 1: ACTIVACIÓN Y VISIBILIDAD (OBLIGATORIO)
|
||||
PATRÓN 3: CARD CON BORDER-LEFT NAVY
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>
|
||||
Activación y Visibilidad
|
||||
</h5>
|
||||
|
||||
<!-- ⚠️ PATRÓN 4: SWITCHES VERTICALES CON ICONOS (3 OBLIGATORIOS) -->
|
||||
|
||||
<!-- Switch 1: Enabled (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarEnabled" checked>
|
||||
<label class="form-check-label small" for="navbarEnabled" style="color: #495057;">
|
||||
<i class="bi bi-power me-1" style="color: #FF8600;"></i>
|
||||
<strong>Activar Navbar</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Switch 2: Show on Mobile (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarShowOnMobile" checked>
|
||||
<label class="form-check-label small" for="navbarShowOnMobile" style="color: #495057;">
|
||||
<i class="bi bi-phone me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar en Mobile</strong> <span class="text-muted">(<768px)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Switch 3: Show on Desktop (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarShowOnDesktop" checked>
|
||||
<label class="form-check-label small" for="navbarShowOnDesktop" style="color: #495057;">
|
||||
<i class="bi bi-display me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar en Desktop</strong> <span class="text-muted">(≥768px)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campo adicional del schema: show_on_pages (select) -->
|
||||
<div class="mb-2 mt-3">
|
||||
<label for="navbarShowOnPages" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>
|
||||
Mostrar en
|
||||
</label>
|
||||
<select id="navbarShowOnPages" class="form-select form-select-sm">
|
||||
<option value="all" selected>Todas las páginas</option>
|
||||
<option value="home">Solo página de inicio</option>
|
||||
<option value="posts">Solo posts individuales</option>
|
||||
<option value="pages">Solo páginas</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Switch 5: Sticky Enabled -->
|
||||
<div class="mb-0 mt-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarStickyEnabled" checked>
|
||||
<label class="form-check-label small" for="navbarStickyEnabled" style="color: #495057;">
|
||||
<i class="bi bi-pin-angle me-1" style="color: #FF8600;"></i>
|
||||
<strong>Navbar fijo (sticky)</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 2: LAYOUT Y ESTRUCTURA
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-columns-gap me-2" style="color: #FF8600;"></i>
|
||||
Layout y Estructura
|
||||
</h5>
|
||||
|
||||
<!-- container_type (select) -->
|
||||
<div class="mb-2">
|
||||
<label for="navbarContainerType" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-box me-1" style="color: #FF8600;"></i>
|
||||
Tipo de contenedor
|
||||
</label>
|
||||
<select id="navbarContainerType" class="form-select form-select-sm">
|
||||
<option value="container" selected>Container (ancho fijo)</option>
|
||||
<option value="container-fluid">Container Fluid (ancho completo)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- padding_vertical + z_index (compactados) -->
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="navbarPaddingVertical" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-arrows-vertical me-1" style="color: #FF8600;"></i>
|
||||
Padding vertical
|
||||
</label>
|
||||
<input type="text" id="navbarPaddingVertical" class="form-control form-control-sm" value="0.75rem 0">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarZIndex" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-layers me-1" style="color: #FF8600;"></i>
|
||||
Z-index
|
||||
</label>
|
||||
<input type="number" id="navbarZIndex" class="form-control form-control-sm" value="1030" min="1" max="9999">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 3: CONFIGURACIÓN DEL MENÚ
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-gear me-2" style="color: #FF8600;"></i>
|
||||
Configuración del Menú
|
||||
</h5>
|
||||
|
||||
<!-- menu_location + custom_menu_id (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarMenuLocation" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-pin-map me-1" style="color: #FF8600;"></i>
|
||||
Ubicación del menú
|
||||
</label>
|
||||
<select id="navbarMenuLocation" class="form-select form-select-sm">
|
||||
<option value="primary" selected>Menú Principal</option>
|
||||
<option value="secondary">Menú Secundario</option>
|
||||
<option value="custom">Menú personalizado</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarCustomMenuId" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-hash me-1" style="color: #FF8600;"></i>
|
||||
ID del menú
|
||||
</label>
|
||||
<input type="number" id="navbarCustomMenuId" class="form-control form-control-sm" value="0" min="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- enable_dropdowns (switch) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarEnableDropdowns" checked>
|
||||
<label class="form-check-label small" for="navbarEnableDropdowns" style="color: #495057;">
|
||||
<i class="bi bi-chevron-down me-1" style="color: #FF8600;"></i>
|
||||
<strong>Habilitar submenús desplegables</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- mobile_breakpoint (select) -->
|
||||
<div class="mb-0">
|
||||
<label for="navbarMobileBreakpoint" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-phone-landscape me-1" style="color: #FF8600;"></i>
|
||||
Breakpoint para menú móvil
|
||||
</label>
|
||||
<select id="navbarMobileBreakpoint" class="form-select form-select-sm">
|
||||
<option value="sm">Small (576px)</option>
|
||||
<option value="md">Medium (768px)</option>
|
||||
<option value="lg" selected>Large (992px)</option>
|
||||
<option value="xl">Extra Large (1200px)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 4: LOGO/MARCA
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-award me-2" style="color: #FF8600;"></i>
|
||||
Logo/Marca
|
||||
</h5>
|
||||
|
||||
<!-- show_brand (switch) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarShowBrand">
|
||||
<label class="form-check-label small" for="navbarShowBrand" style="color: #495057;">
|
||||
<i class="bi bi-eye me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar logo/marca</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- use_logo (switch) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarUseLogo">
|
||||
<label class="form-check-label small" for="navbarUseLogo" style="color: #495057;">
|
||||
<i class="bi bi-image me-1" style="color: #FF8600;"></i>
|
||||
<strong>Usar logo (imagen)</strong>
|
||||
</label>
|
||||
</div>
|
||||
<small class="text-muted d-block ms-4 mt-1">Usa una imagen en lugar de texto</small>
|
||||
</div>
|
||||
|
||||
<!-- logo_url + logo_height (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-8">
|
||||
<label for="navbarLogoUrl" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>
|
||||
URL del logo
|
||||
</label>
|
||||
<input type="url" id="navbarLogoUrl" class="form-control form-control-sm" placeholder="https://...">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label for="navbarLogoHeight" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-arrows-vertical me-1" style="color: #FF8600;"></i>
|
||||
Altura
|
||||
</label>
|
||||
<input type="text" id="navbarLogoHeight" class="form-control form-control-sm" value="40px">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- brand_text -->
|
||||
<div class="mb-2">
|
||||
<label for="navbarBrandText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Texto de la marca
|
||||
</label>
|
||||
<input type="text" id="navbarBrandText" class="form-control form-control-sm" value="Mi Sitio" maxlength="50">
|
||||
<small class="text-muted">Se muestra si no hay logo</small>
|
||||
</div>
|
||||
|
||||
<!-- brand_font_size + brand_color (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarBrandFontSize" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-type me-1" style="color: #FF8600;"></i>
|
||||
Tamaño fuente
|
||||
</label>
|
||||
<input type="text" id="navbarBrandFontSize" class="form-control form-control-sm" value="1.5rem">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarBrandColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-palette me-1" style="color: #FF8600;"></i>
|
||||
Color
|
||||
</label>
|
||||
<input type="color" id="navbarBrandColor" class="form-control form-control-color w-100" value="#FFFFFF">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- brand_hover_color -->
|
||||
<div class="mb-0">
|
||||
<label for="navbarBrandHoverColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>
|
||||
Color hover
|
||||
</label>
|
||||
<input type="color" id="navbarBrandHoverColor" class="form-control form-control-color w-100" value="#FF8600">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<!-- ========================================
|
||||
GRUPO 5: ESTILOS DEL NAVBAR
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-paint-bucket me-2" style="color: #FF8600;"></i>
|
||||
Estilos del Navbar
|
||||
</h5>
|
||||
|
||||
<!-- background_color -->
|
||||
<div class="mb-2">
|
||||
<label for="navbarBackgroundColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-palette me-1" style="color: #FF8600;"></i>
|
||||
Color de fondo
|
||||
</label>
|
||||
<input type="color" id="navbarBackgroundColor" class="form-control form-control-color w-100" value="#1e3a5f">
|
||||
<small class="text-muted d-block mt-1" id="navbarBackgroundColorValue">#1E3A5F</small>
|
||||
</div>
|
||||
|
||||
<!-- box_shadow -->
|
||||
<div class="mb-0">
|
||||
<label for="navbarBoxShadow" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-droplet me-1" style="color: #FF8600;"></i>
|
||||
Sombra del navbar
|
||||
</label>
|
||||
<input type="text" id="navbarBoxShadow" class="form-control form-control-sm" value="0 4px 12px rgba(30, 58, 95, 0.15)">
|
||||
<small class="text-muted">Sombra CSS (ej: 0 4px 12px rgba(0,0,0,0.15))</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 6: ESTILOS DE ENLACES
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-link-45deg me-2" style="color: #FF8600;"></i>
|
||||
Estilos de Enlaces
|
||||
</h5>
|
||||
|
||||
<!-- COLOR PICKERS EN GRID 3 COLORES -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-4">
|
||||
<label for="navbarTextColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Color texto
|
||||
</label>
|
||||
<input type="color" id="navbarTextColor" class="form-control form-control-color w-100" value="#FFFFFF">
|
||||
<small class="text-muted d-block mt-1" id="navbarTextColorValue">#FFFFFF</small>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label for="navbarHoverColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>
|
||||
Color hover
|
||||
</label>
|
||||
<input type="color" id="navbarHoverColor" class="form-control form-control-color w-100" value="#FF8600">
|
||||
<small class="text-muted d-block mt-1" id="navbarHoverColorValue">#FF8600</small>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label for="navbarActiveColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-check-circle me-1" style="color: #FF8600;"></i>
|
||||
Color activo
|
||||
</label>
|
||||
<input type="color" id="navbarActiveColor" class="form-control form-control-color w-100" value="#FF8600">
|
||||
<small class="text-muted d-block mt-1" id="navbarActiveColorValue">#FF8600</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- font_size + font_weight (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarFontSize" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-type me-1" style="color: #FF8600;"></i>
|
||||
Tamaño fuente
|
||||
</label>
|
||||
<input type="text" id="navbarFontSize" class="form-control form-control-sm" value="0.9rem">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarFontWeight" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Grosor fuente
|
||||
</label>
|
||||
<input type="number" id="navbarFontWeight" class="form-control form-control-sm" value="500" min="100" max="900" step="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- padding + border_radius (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarLinkPadding" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-bounding-box me-1" style="color: #FF8600;"></i>
|
||||
Padding
|
||||
</label>
|
||||
<input type="text" id="navbarLinkPadding" class="form-control form-control-sm" value="0.5rem 0.65rem">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarBorderRadius" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-square me-1" style="color: #FF8600;"></i>
|
||||
Border radius
|
||||
</label>
|
||||
<input type="text" id="navbarBorderRadius" class="form-control form-control-sm" value="4px">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- show_underline_effect (switch) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarShowUnderlineEffect" checked>
|
||||
<label class="form-check-label small" for="navbarShowUnderlineEffect" style="color: #495057;">
|
||||
<i class="bi bi-dash-lg me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar efecto de subrayado</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- underline_color -->
|
||||
<div class="mb-0">
|
||||
<label for="navbarUnderlineColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-palette me-1" style="color: #FF8600;"></i>
|
||||
Color del subrayado
|
||||
</label>
|
||||
<input type="color" id="navbarUnderlineColor" class="form-control form-control-color w-100" value="#FF8600">
|
||||
<small class="text-muted d-block mt-1" id="navbarUnderlineColorValue">#FF8600</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 7: ESTILOS DE DROPDOWN
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-chevron-down me-2" style="color: #FF8600;"></i>
|
||||
Estilos de Dropdown
|
||||
</h5>
|
||||
|
||||
<!-- background_color -->
|
||||
<div class="mb-2">
|
||||
<label for="navbarDropdownBackground" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
|
||||
Fondo dropdown
|
||||
</label>
|
||||
<input type="color" id="navbarDropdownBackground" class="form-control form-control-color w-100" value="#FFFFFF">
|
||||
<small class="text-muted d-block mt-1" id="navbarDropdownBackgroundValue">#FFFFFF</small>
|
||||
</div>
|
||||
|
||||
<!-- border_radius + shadow (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarDropdownBorderRadius" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-square me-1" style="color: #FF8600;"></i>
|
||||
Border radius
|
||||
</label>
|
||||
<input type="text" id="navbarDropdownBorderRadius" class="form-control form-control-sm" value="8px">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarDropdownShadow" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-droplet me-1" style="color: #FF8600;"></i>
|
||||
Sombra
|
||||
</label>
|
||||
<input type="text" id="navbarDropdownShadow" class="form-control form-control-sm" value="0 8px 24px rgba(0,0,0,0.12)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- item_color + item_hover_background -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarDropdownItemColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Color items
|
||||
</label>
|
||||
<input type="color" id="navbarDropdownItemColor" class="form-control form-control-color w-100" value="#495057">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarDropdownItemHoverBg" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
|
||||
Fondo hover
|
||||
</label>
|
||||
<input type="color" id="navbarDropdownItemHoverBg" class="form-control form-control-color w-100" value="#FFF5EB">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- item_padding -->
|
||||
<div class="mb-0">
|
||||
<label for="navbarDropdownItemPadding" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-bounding-box me-1" style="color: #FF8600;"></i>
|
||||
Padding de items
|
||||
</label>
|
||||
<input type="text" id="navbarDropdownItemPadding" class="form-control form-control-sm" value="0.625rem 1.25rem">
|
||||
<small class="text-muted">Espaciado interno de items (ej: 0.625rem 1.25rem)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /tab-pane -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Actualizar valores HEX de color pickers
|
||||
document.querySelectorAll('input[type="color"]').forEach(picker => {
|
||||
const valueDisplay = document.getElementById(picker.id + 'Value');
|
||||
if (valueDisplay) {
|
||||
picker.addEventListener('input', function() {
|
||||
valueDisplay.textContent = this.value.toUpperCase();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Simular reset button
|
||||
document.getElementById('resetNavbarDefaults').addEventListener('click', function() {
|
||||
if (confirm('¿Restaurar todos los valores a los valores por defecto?')) {
|
||||
alert('En producción, esto restauraría los valores del schema JSON');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\RelatedPost\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Related Post
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*
|
||||
* NOTA: Este componente NO tenia mapeos en AdminAjaxHandler
|
||||
* (era el unico componente roto - 35 campos no se guardaban)
|
||||
*/
|
||||
final class RelatedPostFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'related-post';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'relatedPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'relatedPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'relatedPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'relatedPostVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'relatedPostVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'relatedPostVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'relatedPostVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'relatedPostVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'relatedPostExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'relatedPostExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'relatedPostExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'relatedPostExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'relatedPostSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'],
|
||||
'relatedPostPerPage' => ['group' => 'content', 'attribute' => 'posts_per_page'],
|
||||
'relatedPostOrderby' => ['group' => 'content', 'attribute' => 'orderby'],
|
||||
'relatedPostOrder' => ['group' => 'content', 'attribute' => 'order'],
|
||||
'relatedPostShowPagination' => ['group' => 'content', 'attribute' => 'show_pagination'],
|
||||
|
||||
// Layout
|
||||
'relatedPostColsDesktop' => ['group' => 'layout', 'attribute' => 'columns_desktop'],
|
||||
'relatedPostColsTablet' => ['group' => 'layout', 'attribute' => 'columns_tablet'],
|
||||
'relatedPostColsMobile' => ['group' => 'layout', 'attribute' => 'columns_mobile'],
|
||||
|
||||
// Typography
|
||||
'relatedPostSectionTitleSize' => ['group' => 'typography', 'attribute' => 'section_title_size'],
|
||||
'relatedPostSectionTitleWeight' => ['group' => 'typography', 'attribute' => 'section_title_weight'],
|
||||
'relatedPostCardTitleSize' => ['group' => 'typography', 'attribute' => 'card_title_size'],
|
||||
'relatedPostCardTitleWeight' => ['group' => 'typography', 'attribute' => 'card_title_weight'],
|
||||
|
||||
// Colors
|
||||
'relatedPostSectionTitleColor' => ['group' => 'colors', 'attribute' => 'section_title_color'],
|
||||
'relatedPostCardBgColor' => ['group' => 'colors', 'attribute' => 'card_bg_color'],
|
||||
'relatedPostCardTitleColor' => ['group' => 'colors', 'attribute' => 'card_title_color'],
|
||||
'relatedPostCardHoverBgColor' => ['group' => 'colors', 'attribute' => 'card_hover_bg_color'],
|
||||
'relatedPostPaginationBgColor' => ['group' => 'colors', 'attribute' => 'pagination_bg_color'],
|
||||
'relatedPostPaginationTextColor' => ['group' => 'colors', 'attribute' => 'pagination_text_color'],
|
||||
'relatedPostPaginationActiveBg' => ['group' => 'colors', 'attribute' => 'pagination_active_bg'],
|
||||
'relatedPostPaginationActiveText' => ['group' => 'colors', 'attribute' => 'pagination_active_text'],
|
||||
|
||||
// Spacing
|
||||
'relatedPostSectionMarginTop' => ['group' => 'spacing', 'attribute' => 'section_margin_top'],
|
||||
'relatedPostSectionMarginBottom' => ['group' => 'spacing', 'attribute' => 'section_margin_bottom'],
|
||||
'relatedPostTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||
'relatedPostGridGap' => ['group' => 'spacing', 'attribute' => 'grid_gap'],
|
||||
'relatedPostCardPadding' => ['group' => 'spacing', 'attribute' => 'card_padding'],
|
||||
'relatedPostPaginationMarginTop' => ['group' => 'spacing', 'attribute' => 'pagination_margin_top'],
|
||||
|
||||
// Visual Effects
|
||||
'relatedPostCardBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'card_border_radius'],
|
||||
'relatedPostCardShadow' => ['group' => 'visual_effects', 'attribute' => 'card_shadow'],
|
||||
'relatedPostCardHoverShadow' => ['group' => 'visual_effects', 'attribute' => 'card_hover_shadow'],
|
||||
'relatedPostCardTransition' => ['group' => 'visual_effects', 'attribute' => 'card_transition'],
|
||||
];
|
||||
}
|
||||
}
|
||||
552
Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php
Normal file
552
Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php
Normal file
@@ -0,0 +1,552 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\RelatedPost\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Related Posts
|
||||
*
|
||||
* @package ROITheme\Admin\RelatedPost\Infrastructure\Ui
|
||||
*/
|
||||
final class RelatedPostFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildLayoutGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-grid-3x3-gap me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Posts Relacionados';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Seccion de posts relacionados con grid de cards';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="related-post">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('relatedPostEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('relatedPostShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('relatedPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'relatedPost');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Section Title
|
||||
$sectionTitle = $this->renderer->getFieldValue($componentId, 'content', 'section_title', 'Descubre Mas Contenido');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostSectionTitle" class="form-label small mb-1 fw-semibold">Titulo de seccion</label>';
|
||||
$html .= ' <input type="text" id="relatedPostSectionTitle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionTitle) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Posts per page
|
||||
$postsPerPage = $this->renderer->getFieldValue($componentId, 'content', 'posts_per_page', '12');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostPerPage" class="form-label small mb-1 fw-semibold">Posts por pagina</label>';
|
||||
$html .= ' <input type="number" id="relatedPostPerPage" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($postsPerPage) . '" min="1" max="50">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Order by
|
||||
$orderby = $this->renderer->getFieldValue($componentId, 'content', 'orderby', 'rand');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostOrderby" class="form-label small mb-1 fw-semibold">Ordenar por</label>';
|
||||
$html .= ' <select id="relatedPostOrderby" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="rand"' . ($orderby === 'rand' ? ' selected' : '') . '>Aleatorio</option>';
|
||||
$html .= ' <option value="date"' . ($orderby === 'date' ? ' selected' : '') . '>Fecha</option>';
|
||||
$html .= ' <option value="title"' . ($orderby === 'title' ? ' selected' : '') . '>Titulo</option>';
|
||||
$html .= ' <option value="comment_count"' . ($orderby === 'comment_count' ? ' selected' : '') . '>Comentarios</option>';
|
||||
$html .= ' <option value="menu_order"' . ($orderby === 'menu_order' ? ' selected' : '') . '>Orden de menu</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Order direction
|
||||
$order = $this->renderer->getFieldValue($componentId, 'content', 'order', 'DESC');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostOrder" class="form-label small mb-1 fw-semibold">Direccion</label>';
|
||||
$html .= ' <select id="relatedPostOrder" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="DESC"' . ($order === 'DESC' ? ' selected' : '') . '>Descendente</option>';
|
||||
$html .= ' <option value="ASC"' . ($order === 'ASC' ? ' selected' : '') . '>Ascendente</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Show pagination
|
||||
$showPagination = $this->renderer->getFieldValue($componentId, 'content', 'show_pagination', true);
|
||||
$html .= $this->buildSwitch('relatedPostShowPagination', 'Mostrar paginacion', 'bi-three-dots', $showPagination);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildLayoutGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-grid me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Disposicion';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Columns desktop
|
||||
$colsDesktop = $this->renderer->getFieldValue($componentId, 'layout', 'columns_desktop', '3');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostColsDesktop" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Columnas escritorio';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="relatedPostColsDesktop" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="2"' . ($colsDesktop === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||
$html .= ' <option value="3"' . ($colsDesktop === '3' ? ' selected' : '') . '>3 columnas</option>';
|
||||
$html .= ' <option value="4"' . ($colsDesktop === '4' ? ' selected' : '') . '>4 columnas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Columns tablet
|
||||
$colsTablet = $this->renderer->getFieldValue($componentId, 'layout', 'columns_tablet', '2');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostColsTablet" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-tablet me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Columnas tablet';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="relatedPostColsTablet" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="1"' . ($colsTablet === '1' ? ' selected' : '') . '>1 columna</option>';
|
||||
$html .= ' <option value="2"' . ($colsTablet === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||
$html .= ' <option value="3"' . ($colsTablet === '3' ? ' selected' : '') . '>3 columnas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Columns mobile
|
||||
$colsMobile = $this->renderer->getFieldValue($componentId, 'layout', 'columns_mobile', '1');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="relatedPostColsMobile" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Columnas movil';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="relatedPostColsMobile" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="1"' . ($colsMobile === '1' ? ' selected' : '') . '>1 columna</option>';
|
||||
$html .= ' <option value="2"' . ($colsMobile === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$sectionTitleSize = $this->renderer->getFieldValue($componentId, 'typography', 'section_title_size', '1.75rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostSectionTitleSize" class="form-label small mb-1 fw-semibold">Tamano titulo seccion</label>';
|
||||
$html .= ' <input type="text" id="relatedPostSectionTitleSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionTitleSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$sectionTitleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'section_title_weight', '500');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostSectionTitleWeight" class="form-label small mb-1 fw-semibold">Peso titulo seccion</label>';
|
||||
$html .= ' <input type="text" id="relatedPostSectionTitleWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionTitleWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$cardTitleSize = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostCardTitleSize" class="form-label small mb-1 fw-semibold">Tamano titulo card</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardTitleSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardTitleSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$cardTitleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_weight', '500');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostCardTitleWeight" class="form-label small mb-1 fw-semibold">Peso titulo card</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardTitleWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardTitleWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Seccion
|
||||
$html .= ' <p class="small fw-semibold mb-2">Seccion</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$sectionTitleColor = $this->renderer->getFieldValue($componentId, 'colors', 'section_title_color', '#212529');
|
||||
$html .= $this->buildColorPicker('relatedPostSectionTitleColor', 'Titulo seccion', $sectionTitleColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Cards
|
||||
$html .= ' <p class="small fw-semibold mb-2">Cards</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_bg_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('relatedPostCardBgColor', 'Fondo card', $cardBgColor);
|
||||
|
||||
$cardTitleColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_title_color', '#212529');
|
||||
$html .= $this->buildColorPicker('relatedPostCardTitleColor', 'Titulo card', $cardTitleColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardHoverBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_hover_bg_color', '#f8f9fa');
|
||||
$html .= $this->buildColorPicker('relatedPostCardHoverBgColor', 'Fondo hover', $cardHoverBgColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Paginacion
|
||||
$html .= ' <p class="small fw-semibold mb-2">Paginacion</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$paginationBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_bg_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('relatedPostPaginationBgColor', 'Fondo', $paginationBgColor);
|
||||
|
||||
$paginationTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_text_color', '#0d6efd');
|
||||
$html .= $this->buildColorPicker('relatedPostPaginationTextColor', 'Texto', $paginationTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$paginationActiveBg = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_active_bg', '#0d6efd');
|
||||
$html .= $this->buildColorPicker('relatedPostPaginationActiveBg', 'Activo fondo', $paginationActiveBg);
|
||||
|
||||
$paginationActiveText = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_active_text', '#ffffff');
|
||||
$html .= $this->buildColorPicker('relatedPostPaginationActiveText', 'Activo texto', $paginationActiveText);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$sectionMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_top', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostSectionMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
|
||||
$html .= ' <input type="text" id="relatedPostSectionMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionMarginTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$sectionMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_bottom', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostSectionMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
|
||||
$html .= ' <input type="text" id="relatedPostSectionMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||
$html .= ' <input type="text" id="relatedPostTitleMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$gridGap = $this->renderer->getFieldValue($componentId, 'spacing', 'grid_gap', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostGridGap" class="form-label small mb-1 fw-semibold">Espacio cards</label>';
|
||||
$html .= ' <input type="text" id="relatedPostGridGap" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($gridGap) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$cardPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'card_padding', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostCardPadding" class="form-label small mb-1 fw-semibold">Padding card</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$paginationMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'pagination_margin_top', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostPaginationMarginTop" class="form-label small mb-1 fw-semibold">Margen paginacion</label>';
|
||||
$html .= ' <input type="text" id="relatedPostPaginationMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($paginationMarginTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_border_radius', '0.375rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostCardBorderRadius" class="form-label small mb-1 fw-semibold">Radio borde card</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$cardTransition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_transition', '0.3s ease');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostCardTransition" class="form-label small mb-1 fw-semibold">Transicion</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardTransition" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardTransition) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="mb-3">';
|
||||
$cardShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_shadow', '0 .125rem .25rem rgba(0,0,0,.075)');
|
||||
$html .= ' <label for="relatedPostCardShadow" class="form-label small mb-1 fw-semibold">Sombra card</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="mb-0">';
|
||||
$cardHoverShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_hover_shadow', '0 .5rem 1rem rgba(0,0,0,.15)');
|
||||
$html .= ' <label for="relatedPostCardHoverShadow" class="form-label small mb-1 fw-semibold">Sombra hover</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardHoverShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardHoverShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
38
Admin/Shared/Domain/Contracts/FieldMapperInterface.php
Normal file
38
Admin/Shared/Domain/Contracts/FieldMapperInterface.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Contrato para mapeo de campos de formulario a atributos de BD
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Definir el mapeo de field IDs a grupos/atributos
|
||||
* - Cada modulo implementa su propio mapper
|
||||
*
|
||||
* PRINCIPIOS:
|
||||
* - ISP: Interfaz pequena (2 metodos)
|
||||
* - DIP: Capas superiores dependen de esta abstraccion
|
||||
*/
|
||||
interface FieldMapperInterface
|
||||
{
|
||||
/**
|
||||
* Retorna el nombre del componente que mapea
|
||||
*
|
||||
* @return string Nombre en kebab-case (ej: 'cta-box-sidebar')
|
||||
*/
|
||||
public function getComponentName(): string;
|
||||
|
||||
/**
|
||||
* Retorna el mapeo de field IDs a grupo/atributo
|
||||
*
|
||||
* @return array<string, array{group: string, attribute: string}>
|
||||
*
|
||||
* Ejemplo:
|
||||
* [
|
||||
* 'ctaTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||
* 'ctaEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
* ]
|
||||
*/
|
||||
public function getFieldMapping(): array;
|
||||
}
|
||||
158
Admin/Shared/Infrastructure/Api/WordPress/AdminAjaxHandler.php
Normal file
158
Admin/Shared/Infrastructure/Api/WordPress/AdminAjaxHandler.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\Api\WordPress;
|
||||
|
||||
use ROITheme\Shared\Application\UseCases\SaveComponentSettings\SaveComponentSettingsUseCase;
|
||||
use ROITheme\Admin\Shared\Infrastructure\FieldMapping\FieldMapperRegistry;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Services\ExclusionFieldProcessor;
|
||||
|
||||
/**
|
||||
* Handler para peticiones AJAX del panel de administracion
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Manejar HTTP (request/response)
|
||||
* - Delegar mapeo a FieldMapperRegistry
|
||||
* - NO contiene logica de mapeo
|
||||
*
|
||||
* PRINCIPIOS:
|
||||
* - SRP: Solo maneja HTTP
|
||||
* - OCP: Nuevos componentes no requieren modificar esta clase
|
||||
* - DIP: Depende de abstracciones (FieldMapperRegistry)
|
||||
*/
|
||||
final class AdminAjaxHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ?SaveComponentSettingsUseCase $saveComponentSettingsUseCase = null,
|
||||
private readonly ?FieldMapperRegistry $fieldMapperRegistry = null
|
||||
) {}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
add_action('wp_ajax_roi_save_component_settings', [$this, 'saveComponentSettings']);
|
||||
add_action('wp_ajax_roi_reset_component_defaults', [$this, 'resetComponentDefaults']);
|
||||
}
|
||||
|
||||
public function saveComponentSettings(): void
|
||||
{
|
||||
check_ajax_referer('roi_admin_dashboard', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error(['message' => 'No tienes permisos para realizar esta accion.']);
|
||||
}
|
||||
|
||||
$component = sanitize_text_field($_POST['component'] ?? '');
|
||||
$settings = json_decode(stripslashes($_POST['settings'] ?? '{}'), true);
|
||||
|
||||
if (empty($component) || empty($settings)) {
|
||||
wp_send_json_error(['message' => 'Datos incompletos.']);
|
||||
}
|
||||
|
||||
// Obtener mapper del modulo correspondiente
|
||||
if ($this->fieldMapperRegistry === null || !$this->fieldMapperRegistry->hasMapper($component)) {
|
||||
wp_send_json_error([
|
||||
'message' => "No existe mapper para el componente: {$component}"
|
||||
]);
|
||||
}
|
||||
|
||||
$mapper = $this->fieldMapperRegistry->getMapper($component);
|
||||
$fieldMapping = $mapper->getFieldMapping();
|
||||
|
||||
// Mapear settings usando el mapper del modulo
|
||||
$mappedSettings = $this->mapSettings($settings, $fieldMapping);
|
||||
|
||||
// Guardar usando Use Case
|
||||
if ($this->saveComponentSettingsUseCase !== null) {
|
||||
$updated = $this->saveComponentSettingsUseCase->execute($component, $mappedSettings);
|
||||
wp_send_json_success([
|
||||
'message' => sprintf('Se guardaron %d campos correctamente.', $updated)
|
||||
]);
|
||||
} else {
|
||||
wp_send_json_error(['message' => 'Error: Use Case no disponible.']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapea settings de field IDs a grupos/atributos
|
||||
*
|
||||
* Soporta tipos especiales para campos de exclusion:
|
||||
* - json_array: Convierte "a, b, c" a ["a", "b", "c"]
|
||||
* - json_array_int: Convierte "1, 2, 3" a [1, 2, 3]
|
||||
* - json_array_lines: Convierte lineas a array
|
||||
*/
|
||||
private function mapSettings(array $settings, array $fieldMapping): array
|
||||
{
|
||||
$mappedSettings = [];
|
||||
$fieldProcessor = new ExclusionFieldProcessor();
|
||||
|
||||
foreach ($settings as $fieldId => $value) {
|
||||
if (!isset($fieldMapping[$fieldId])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mapping = $fieldMapping[$fieldId];
|
||||
$groupName = $mapping['group'];
|
||||
$attributeName = $mapping['attribute'];
|
||||
$type = $mapping['type'] ?? null;
|
||||
|
||||
if (!isset($mappedSettings[$groupName])) {
|
||||
$mappedSettings[$groupName] = [];
|
||||
}
|
||||
|
||||
// Procesar valor segun tipo
|
||||
if ($type !== null && is_string($value)) {
|
||||
$value = $fieldProcessor->process($value, $type);
|
||||
}
|
||||
|
||||
$mappedSettings[$groupName][$attributeName] = $value;
|
||||
}
|
||||
|
||||
return $mappedSettings;
|
||||
}
|
||||
|
||||
public function resetComponentDefaults(): void
|
||||
{
|
||||
// Verificar nonce
|
||||
check_ajax_referer('roi_admin_dashboard', 'nonce');
|
||||
|
||||
// Verificar permisos
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error([
|
||||
'message' => 'No tienes permisos para realizar esta accion.'
|
||||
]);
|
||||
}
|
||||
|
||||
// Obtener componente
|
||||
$component = sanitize_text_field($_POST['component'] ?? '');
|
||||
|
||||
if (empty($component)) {
|
||||
wp_send_json_error([
|
||||
'message' => 'Componente no especificado.'
|
||||
]);
|
||||
}
|
||||
|
||||
// Ruta al schema JSON
|
||||
$schemaPath = get_template_directory() . '/Schemas/' . $component . '.json';
|
||||
|
||||
if (!file_exists($schemaPath)) {
|
||||
wp_send_json_error([
|
||||
'message' => 'Schema del componente no encontrado.'
|
||||
]);
|
||||
}
|
||||
|
||||
// Usar repositorio para restaurar valores
|
||||
if ($this->saveComponentSettingsUseCase !== null) {
|
||||
global $wpdb;
|
||||
$repository = new \ROITheme\Shared\Infrastructure\Persistence\WordPress\WordPressComponentSettingsRepository($wpdb);
|
||||
$updated = $repository->resetToDefaults($component, $schemaPath);
|
||||
|
||||
wp_send_json_success([
|
||||
'message' => sprintf('Se restauraron %d campos a sus valores por defecto.', $updated)
|
||||
]);
|
||||
} else {
|
||||
wp_send_json_error([
|
||||
'message' => 'Error: Repositorio no disponible.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Provider para auto-registro de Field Mappers
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Descubrir automaticamente FieldMappers en cada modulo
|
||||
* - Registrarlos en el FieldMapperRegistry
|
||||
*
|
||||
* BENEFICIO:
|
||||
* - Agregar nuevo componente = crear FieldMapper (sin tocar functions.php)
|
||||
* - Eliminar componente = borrar carpeta (limpieza automatica)
|
||||
*/
|
||||
final class FieldMapperProvider
|
||||
{
|
||||
private const MODULES = [
|
||||
'TopNotificationBar',
|
||||
'Navbar',
|
||||
'CtaLetsTalk',
|
||||
'Hero',
|
||||
'FeaturedImage',
|
||||
'TableOfContents',
|
||||
'CtaBoxSidebar',
|
||||
'SocialShare',
|
||||
'CtaPost',
|
||||
'RelatedPost',
|
||||
'ContactForm',
|
||||
'Footer',
|
||||
'ThemeSettings',
|
||||
'AdsensePlacement',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly FieldMapperRegistry $registry
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Registra todos los FieldMappers disponibles
|
||||
*/
|
||||
public function registerAll(): void
|
||||
{
|
||||
foreach (self::MODULES as $module) {
|
||||
$this->registerIfExists($module);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra un mapper si existe la clase
|
||||
*/
|
||||
private function registerIfExists(string $module): void
|
||||
{
|
||||
$className = sprintf(
|
||||
'ROITheme\\Admin\\%s\\Infrastructure\\FieldMapping\\%sFieldMapper',
|
||||
$module,
|
||||
$module
|
||||
);
|
||||
|
||||
if (class_exists($className)) {
|
||||
$mapper = new $className();
|
||||
if ($mapper instanceof FieldMapperInterface) {
|
||||
$this->registry->register($mapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Registro central de Field Mappers
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Registrar mappers de cada modulo
|
||||
* - Resolver mapper por nombre de componente
|
||||
*
|
||||
* PRINCIPIOS:
|
||||
* - OCP: Nuevos mappers se registran sin modificar esta clase
|
||||
* - SRP: Solo gestiona el registro, no contiene mapeos
|
||||
*/
|
||||
final class FieldMapperRegistry
|
||||
{
|
||||
/** @var array<string, FieldMapperInterface> */
|
||||
private array $mappers = [];
|
||||
|
||||
/**
|
||||
* Registra un mapper
|
||||
*/
|
||||
public function register(FieldMapperInterface $mapper): void
|
||||
{
|
||||
$this->mappers[$mapper->getComponentName()] = $mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene un mapper por nombre de componente
|
||||
*
|
||||
* @throws \InvalidArgumentException Si no existe mapper para el componente
|
||||
*/
|
||||
public function getMapper(string $componentName): FieldMapperInterface
|
||||
{
|
||||
if (!isset($this->mappers[$componentName])) {
|
||||
throw new \InvalidArgumentException(
|
||||
"No field mapper registered for component: {$componentName}"
|
||||
);
|
||||
}
|
||||
|
||||
return $this->mappers[$componentName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si existe mapper para un componente
|
||||
*/
|
||||
public function hasMapper(string $componentName): bool
|
||||
{
|
||||
return isset($this->mappers[$componentName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene todos los mappers registrados
|
||||
*
|
||||
* @return array<string, FieldMapperInterface>
|
||||
*/
|
||||
public function getAllMappers(): array
|
||||
{
|
||||
return $this->mappers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\Services;
|
||||
|
||||
/**
|
||||
* Servicio para procesar campos de exclusion antes de guardar en BD
|
||||
*
|
||||
* Convierte formatos de UI a JSON para almacenamiento.
|
||||
*
|
||||
* v1.1: Extraido de AdminAjaxHandler (SRP)
|
||||
*
|
||||
* @package ROITheme\Admin\Shared\Infrastructure\Services
|
||||
*/
|
||||
final class ExclusionFieldProcessor
|
||||
{
|
||||
/**
|
||||
* Procesa un valor de campo de exclusion segun su tipo
|
||||
*
|
||||
* @param string $value Valor del campo (desde UI)
|
||||
* @param string $type Tipo de campo: json_array, json_array_int, json_array_lines
|
||||
* @return string JSON string para almacenar en BD
|
||||
*/
|
||||
public function process(string $value, string $type): string
|
||||
{
|
||||
return match ($type) {
|
||||
'json_array' => $this->processJsonArray($value),
|
||||
'json_array_int' => $this->processJsonArrayInt($value),
|
||||
'json_array_lines' => $this->processJsonArrayLines($value),
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* "a, b, c" -> ["a", "b", "c"]
|
||||
*/
|
||||
private function processJsonArray(string $value): string
|
||||
{
|
||||
$items = array_map('trim', explode(',', $value));
|
||||
$items = array_filter($items, fn($item) => $item !== '');
|
||||
return json_encode(array_values($items), JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* "1, 2, 3" -> [1, 2, 3]
|
||||
*/
|
||||
private function processJsonArrayInt(string $value): string
|
||||
{
|
||||
$items = array_map('trim', explode(',', $value));
|
||||
$items = array_filter($items, 'is_numeric');
|
||||
$items = array_map('intval', $items);
|
||||
return json_encode(array_values($items));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lineas separadas -> array
|
||||
*/
|
||||
private function processJsonArrayLines(string $value): string
|
||||
{
|
||||
$items = preg_split('/\r\n|\r|\n/', $value);
|
||||
$items = array_map('trim', $items);
|
||||
$items = array_filter($items, fn($item) => $item !== '');
|
||||
return json_encode(array_values($items), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
}
|
||||
31
Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js
Normal file
31
Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Toggle para mostrar/ocultar reglas de exclusion en FormBuilders
|
||||
*
|
||||
* Escucha cambios en checkboxes con ID que termine en "ExclusionsEnabled"
|
||||
* y muestra/oculta el contenedor de reglas correspondiente.
|
||||
*
|
||||
* @package ROITheme\Admin
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
function initExclusionToggles() {
|
||||
document.querySelectorAll('[id$="ExclusionsEnabled"]').forEach(function(checkbox) {
|
||||
// Handler para cambios
|
||||
checkbox.addEventListener('change', function() {
|
||||
const prefix = this.id.replace('ExclusionsEnabled', '');
|
||||
const rulesContainer = document.getElementById(prefix + 'ExclusionRules');
|
||||
if (rulesContainer) {
|
||||
rulesContainer.style.display = this.checked ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Inicializar cuando DOM este listo
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initExclusionToggles);
|
||||
} else {
|
||||
initExclusionToggles();
|
||||
}
|
||||
})();
|
||||
260
Admin/Shared/Infrastructure/Ui/ExclusionFormPartial.php
Normal file
260
Admin/Shared/Infrastructure/Ui/ExclusionFormPartial.php
Normal file
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
/**
|
||||
* Componente UI parcial reutilizable para reglas de exclusion
|
||||
*
|
||||
* Genera el HTML para la seccion de exclusiones en FormBuilders.
|
||||
* Debe ser incluido despues de la seccion de visibilidad por tipo de pagina.
|
||||
*
|
||||
* Uso en FormBuilder:
|
||||
* ```php
|
||||
* $exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
* $html .= $exclusionPartial->render($componentId, 'prefijo');
|
||||
* ```
|
||||
*
|
||||
* @package ROITheme\Admin\Shared\Infrastructure\Ui
|
||||
*/
|
||||
final class ExclusionFormPartial
|
||||
{
|
||||
private const GROUP_NAME = '_exclusions';
|
||||
|
||||
public function __construct(
|
||||
private readonly AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Renderiza la seccion de exclusiones
|
||||
*
|
||||
* @param string $componentId ID del componente (kebab-case)
|
||||
* @param string $prefix Prefijo para IDs de campos (ej: 'cta' genera 'ctaExclusionsEnabled')
|
||||
* @return string HTML de la seccion
|
||||
*/
|
||||
public function render(string $componentId, string $prefix): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildExclusionHeader();
|
||||
$html .= $this->buildExclusionToggle($componentId, $prefix);
|
||||
$html .= $this->buildExclusionRules($componentId, $prefix);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildExclusionHeader(): string
|
||||
{
|
||||
$html = '<hr class="my-3">';
|
||||
$html .= '<p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-funnel me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Reglas de exclusion avanzadas';
|
||||
$html .= '</p>';
|
||||
$html .= '<p class="small text-muted mb-2">';
|
||||
$html .= ' Excluir este componente de categorias, posts o URLs especificos.';
|
||||
$html .= '</p>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildExclusionToggle(string $componentId, string $prefix): string
|
||||
{
|
||||
$enabled = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclusions_enabled',
|
||||
false
|
||||
);
|
||||
$checked = $this->toBool($enabled);
|
||||
|
||||
$id = $prefix . 'ExclusionsEnabled';
|
||||
|
||||
$html = '<div class="mb-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= ' <i class="bi bi-filter-circle me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Activar reglas de exclusion</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildExclusionRules(string $componentId, string $prefix): string
|
||||
{
|
||||
$enabled = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclusions_enabled',
|
||||
false
|
||||
);
|
||||
$display = $this->toBool($enabled) ? 'block' : 'none';
|
||||
|
||||
$html = sprintf(
|
||||
'<div id="%sExclusionRules" style="display: %s;">',
|
||||
esc_attr($prefix),
|
||||
$display
|
||||
);
|
||||
|
||||
$html .= $this->buildCategoryField($componentId, $prefix);
|
||||
$html .= $this->buildPostIdsField($componentId, $prefix);
|
||||
$html .= $this->buildUrlPatternsField($componentId, $prefix);
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildCategoryField(string $componentId, string $prefix): string
|
||||
{
|
||||
$value = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclude_categories',
|
||||
'[]'
|
||||
);
|
||||
$categories = $this->jsonToCommaList($value);
|
||||
|
||||
$id = $prefix . 'ExcludeCategories';
|
||||
|
||||
$html = '<div class="mb-3">';
|
||||
$html .= sprintf(
|
||||
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= ' <i class="bi bi-folder me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Excluir en categorias';
|
||||
$html .= ' </label>';
|
||||
$html .= sprintf(
|
||||
' <input type="text" id="%s" class="form-control form-control-sm" value="%s" placeholder="noticias, eventos, tutoriales">',
|
||||
esc_attr($id),
|
||||
esc_attr($categories)
|
||||
);
|
||||
$html .= ' <small class="text-muted">Slugs de categorias separados por comas</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPostIdsField(string $componentId, string $prefix): string
|
||||
{
|
||||
$value = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclude_post_ids',
|
||||
'[]'
|
||||
);
|
||||
$postIds = $this->jsonToCommaList($value);
|
||||
|
||||
$id = $prefix . 'ExcludePostIds';
|
||||
|
||||
$html = '<div class="mb-3">';
|
||||
$html .= sprintf(
|
||||
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= ' <i class="bi bi-hash me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Excluir en posts/paginas';
|
||||
$html .= ' </label>';
|
||||
$html .= sprintf(
|
||||
' <input type="text" id="%s" class="form-control form-control-sm" value="%s" placeholder="123, 456, 789">',
|
||||
esc_attr($id),
|
||||
esc_attr($postIds)
|
||||
);
|
||||
$html .= ' <small class="text-muted">IDs de posts o paginas separados por comas</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildUrlPatternsField(string $componentId, string $prefix): string
|
||||
{
|
||||
$value = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclude_url_patterns',
|
||||
'[]'
|
||||
);
|
||||
$patterns = $this->jsonToLineList($value);
|
||||
|
||||
$id = $prefix . 'ExcludeUrlPatterns';
|
||||
|
||||
$html = '<div class="mb-0">';
|
||||
$html .= sprintf(
|
||||
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Excluir por patrones URL';
|
||||
$html .= ' </label>';
|
||||
$html .= sprintf(
|
||||
' <textarea id="%s" class="form-control form-control-sm" rows="3" placeholder="/privado/ /landing-especial/ /^\/categoria\/\d+$/">%s</textarea>',
|
||||
esc_attr($id),
|
||||
esc_textarea($patterns)
|
||||
);
|
||||
$html .= ' <small class="text-muted">Un patron por linea. Soporta texto simple o regex (ej: /^\/blog\/\d+$/)</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte JSON array o array a lista separada por comas
|
||||
*
|
||||
* @param string|array $value Valor desde BD (puede ser JSON string o array ya deserializado)
|
||||
*/
|
||||
private function jsonToCommaList(string|array $value): string
|
||||
{
|
||||
// Si ya es array, usarlo directamente
|
||||
if (is_array($value)) {
|
||||
return empty($value) ? '' : implode(', ', $value);
|
||||
}
|
||||
|
||||
// Si es string, intentar decodificar JSON
|
||||
$decoded = json_decode($value, true);
|
||||
|
||||
if (!is_array($decoded) || empty($decoded)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return implode(', ', $decoded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte JSON array o array a lista separada por lineas
|
||||
*
|
||||
* @param string|array $value Valor desde BD (puede ser JSON string o array ya deserializado)
|
||||
*/
|
||||
private function jsonToLineList(string|array $value): string
|
||||
{
|
||||
// Si ya es array, usarlo directamente
|
||||
if (is_array($value)) {
|
||||
return empty($value) ? '' : implode("\n", $value);
|
||||
}
|
||||
|
||||
// Si es string, intentar decodificar JSON
|
||||
$decoded = json_decode($value, true);
|
||||
|
||||
if (!is_array($decoded) || empty($decoded)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return implode("\n", $decoded);
|
||||
}
|
||||
|
||||
private function toBool(mixed $value): bool
|
||||
{
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\SocialShare\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Social Share
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class SocialShareFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'social-share';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'socialShareEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'socialShareShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'socialShareShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'socialShareVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'socialShareVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'socialShareVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'socialShareVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'socialShareVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'socialShareExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'socialShareExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'socialShareExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'socialShareExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'socialShareShowLabel' => ['group' => 'content', 'attribute' => 'show_label'],
|
||||
'socialShareLabelText' => ['group' => 'content', 'attribute' => 'label_text'],
|
||||
|
||||
// Networks
|
||||
'socialShareFacebook' => ['group' => 'networks', 'attribute' => 'show_facebook'],
|
||||
'socialShareFacebookUrl' => ['group' => 'networks', 'attribute' => 'facebook_url'],
|
||||
'socialShareInstagram' => ['group' => 'networks', 'attribute' => 'show_instagram'],
|
||||
'socialShareInstagramUrl' => ['group' => 'networks', 'attribute' => 'instagram_url'],
|
||||
'socialShareLinkedin' => ['group' => 'networks', 'attribute' => 'show_linkedin'],
|
||||
'socialShareLinkedinUrl' => ['group' => 'networks', 'attribute' => 'linkedin_url'],
|
||||
'socialShareWhatsapp' => ['group' => 'networks', 'attribute' => 'show_whatsapp'],
|
||||
'socialShareWhatsappNumber' => ['group' => 'networks', 'attribute' => 'whatsapp_number'],
|
||||
'socialShareTwitter' => ['group' => 'networks', 'attribute' => 'show_twitter'],
|
||||
'socialShareTwitterUrl' => ['group' => 'networks', 'attribute' => 'twitter_url'],
|
||||
'socialShareEmail' => ['group' => 'networks', 'attribute' => 'show_email'],
|
||||
'socialShareEmailAddress' => ['group' => 'networks', 'attribute' => 'email_address'],
|
||||
|
||||
// Colors
|
||||
'socialShareLabelColor' => ['group' => 'colors', 'attribute' => 'label_color'],
|
||||
'socialShareBorderTopColor' => ['group' => 'colors', 'attribute' => 'border_top_color'],
|
||||
'socialShareButtonBg' => ['group' => 'colors', 'attribute' => 'button_background'],
|
||||
'socialShareFacebookColor' => ['group' => 'colors', 'attribute' => 'facebook_color'],
|
||||
'socialShareInstagramColor' => ['group' => 'colors', 'attribute' => 'instagram_color'],
|
||||
'socialShareLinkedinColor' => ['group' => 'colors', 'attribute' => 'linkedin_color'],
|
||||
'socialShareWhatsappColor' => ['group' => 'colors', 'attribute' => 'whatsapp_color'],
|
||||
'socialShareTwitterColor' => ['group' => 'colors', 'attribute' => 'twitter_color'],
|
||||
'socialShareEmailColor' => ['group' => 'colors', 'attribute' => 'email_color'],
|
||||
|
||||
// Typography
|
||||
'socialShareLabelFontSize' => ['group' => 'typography', 'attribute' => 'label_font_size'],
|
||||
'socialShareIconFontSize' => ['group' => 'typography', 'attribute' => 'icon_font_size'],
|
||||
|
||||
// Spacing
|
||||
'socialShareMarginTop' => ['group' => 'spacing', 'attribute' => 'container_margin_top'],
|
||||
'socialShareMarginBottom' => ['group' => 'spacing', 'attribute' => 'container_margin_bottom'],
|
||||
'socialSharePaddingTop' => ['group' => 'spacing', 'attribute' => 'container_padding_top'],
|
||||
'socialSharePaddingBottom' => ['group' => 'spacing', 'attribute' => 'container_padding_bottom'],
|
||||
'socialShareLabelMarginBottom' => ['group' => 'spacing', 'attribute' => 'label_margin_bottom'],
|
||||
'socialShareButtonsGap' => ['group' => 'spacing', 'attribute' => 'buttons_gap'],
|
||||
'socialShareButtonPadding' => ['group' => 'spacing', 'attribute' => 'button_padding'],
|
||||
|
||||
// Visual Effects
|
||||
'socialShareBorderTopWidth' => ['group' => 'visual_effects', 'attribute' => 'border_top_width'],
|
||||
'socialShareButtonBorderWidth' => ['group' => 'visual_effects', 'attribute' => 'button_border_width'],
|
||||
'socialShareButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
|
||||
'socialShareTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
'socialShareHoverBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'hover_box_shadow'],
|
||||
];
|
||||
}
|
||||
}
|
||||
579
Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php
Normal file
579
Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php
Normal file
@@ -0,0 +1,579 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\SocialShare\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Social Share
|
||||
*
|
||||
* Responsabilidad:
|
||||
* - Generar HTML del formulario de configuracion
|
||||
* - Usar Design System (Bootstrap 5)
|
||||
* - Cargar valores desde BD via AdminDashboardRenderer
|
||||
*
|
||||
* @package ROITheme\Admin\SocialShare\Infrastructure\Ui
|
||||
*/
|
||||
final class SocialShareFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildNetworksGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-share me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Compartir en Redes';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Botones para compartir contenido en redes sociales';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="social-share">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// is_enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('socialShareEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
// show_on_desktop
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('socialShareShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
// show_on_mobile
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('socialShareShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'socialShare');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// show_label
|
||||
$showLabel = $this->renderer->getFieldValue($componentId, 'content', 'show_label', true);
|
||||
$html .= $this->buildSwitch('socialShareShowLabel', 'Mostrar etiqueta', 'bi-tag', $showLabel);
|
||||
|
||||
// label_text
|
||||
$labelText = $this->renderer->getFieldValue($componentId, 'content', 'label_text', 'Compartir:');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="socialShareLabelText" class="form-label small mb-1 fw-semibold">Texto etiqueta</label>';
|
||||
$html .= ' <input type="text" id="socialShareLabelText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($labelText) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildNetworksGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-globe me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Redes Sociales';
|
||||
$html .= ' </h5>';
|
||||
$html .= ' <p class="small text-muted mb-3">Configura las redes sociales y sus URLs</p>';
|
||||
|
||||
// Facebook
|
||||
$showFacebook = $this->renderer->getFieldValue($componentId, 'networks', 'show_facebook', true);
|
||||
$facebookUrl = $this->renderer->getFieldValue($componentId, 'networks', 'facebook_url', '');
|
||||
$html .= $this->buildNetworkField('socialShareFacebook', 'socialShareFacebookUrl', 'Facebook', 'bi-facebook', $showFacebook, $facebookUrl, 'https://facebook.com/tu-pagina');
|
||||
|
||||
// Instagram
|
||||
$showInstagram = $this->renderer->getFieldValue($componentId, 'networks', 'show_instagram', true);
|
||||
$instagramUrl = $this->renderer->getFieldValue($componentId, 'networks', 'instagram_url', '');
|
||||
$html .= $this->buildNetworkField('socialShareInstagram', 'socialShareInstagramUrl', 'Instagram', 'bi-instagram', $showInstagram, $instagramUrl, 'https://instagram.com/tu-perfil');
|
||||
|
||||
// LinkedIn
|
||||
$showLinkedin = $this->renderer->getFieldValue($componentId, 'networks', 'show_linkedin', true);
|
||||
$linkedinUrl = $this->renderer->getFieldValue($componentId, 'networks', 'linkedin_url', '');
|
||||
$html .= $this->buildNetworkField('socialShareLinkedin', 'socialShareLinkedinUrl', 'LinkedIn', 'bi-linkedin', $showLinkedin, $linkedinUrl, 'https://linkedin.com/in/tu-perfil');
|
||||
|
||||
// WhatsApp
|
||||
$showWhatsapp = $this->renderer->getFieldValue($componentId, 'networks', 'show_whatsapp', true);
|
||||
$whatsappNumber = $this->renderer->getFieldValue($componentId, 'networks', 'whatsapp_number', '');
|
||||
$html .= $this->buildNetworkField('socialShareWhatsapp', 'socialShareWhatsappNumber', 'WhatsApp', 'bi-whatsapp', $showWhatsapp, $whatsappNumber, '521234567890');
|
||||
|
||||
// X (Twitter)
|
||||
$showTwitter = $this->renderer->getFieldValue($componentId, 'networks', 'show_twitter', true);
|
||||
$twitterUrl = $this->renderer->getFieldValue($componentId, 'networks', 'twitter_url', '');
|
||||
$html .= $this->buildNetworkField('socialShareTwitter', 'socialShareTwitterUrl', 'X (Twitter)', 'bi-twitter-x', $showTwitter, $twitterUrl, 'https://x.com/tu-perfil');
|
||||
|
||||
// Email
|
||||
$showEmail = $this->renderer->getFieldValue($componentId, 'networks', 'show_email', true);
|
||||
$emailAddress = $this->renderer->getFieldValue($componentId, 'networks', 'email_address', '');
|
||||
$html .= $this->buildNetworkField('socialShareEmail', 'socialShareEmailAddress', 'Email', 'bi-envelope', $showEmail, $emailAddress, 'contacto@tudominio.com');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildNetworkField(string $switchId, string $urlId, string $label, string $icon, mixed $checked, string $urlValue, string $placeholder): string
|
||||
{
|
||||
// Normalizar valor booleano desde BD
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-3 p-2 rounded" style="background-color: #f8f9fa;">';
|
||||
|
||||
// Switch
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($switchId),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small fw-semibold" for="%s">',
|
||||
esc_attr($switchId)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// URL Input
|
||||
$html .= sprintf(
|
||||
' <input type="text" id="%s" class="form-control form-control-sm mt-2" value="%s" placeholder="%s">',
|
||||
esc_attr($urlId),
|
||||
esc_attr($urlValue),
|
||||
esc_attr($placeholder)
|
||||
);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Colores generales
|
||||
$html .= ' <p class="small fw-semibold mb-2">General</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$labelColor = $this->renderer->getFieldValue($componentId, 'colors', 'label_color', '#6c757d');
|
||||
$html .= $this->buildColorPicker('socialShareLabelColor', 'Etiqueta', $labelColor);
|
||||
|
||||
$borderTopColor = $this->renderer->getFieldValue($componentId, 'colors', 'border_top_color', '#dee2e6');
|
||||
$html .= $this->buildColorPicker('socialShareBorderTopColor', 'Borde superior', $borderTopColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonBackground = $this->renderer->getFieldValue($componentId, 'colors', 'button_background', '#ffffff');
|
||||
$html .= $this->buildColorPicker('socialShareButtonBg', 'Fondo botones', $buttonBackground);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores por red social
|
||||
$html .= ' <p class="small fw-semibold mb-2">Redes Sociales</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$facebookColor = $this->renderer->getFieldValue($componentId, 'colors', 'facebook_color', '#0d6efd');
|
||||
$html .= $this->buildColorPicker('socialShareFacebookColor', 'Facebook', $facebookColor);
|
||||
|
||||
$instagramColor = $this->renderer->getFieldValue($componentId, 'colors', 'instagram_color', '#dc3545');
|
||||
$html .= $this->buildColorPicker('socialShareInstagramColor', 'Instagram', $instagramColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$linkedinColor = $this->renderer->getFieldValue($componentId, 'colors', 'linkedin_color', '#0dcaf0');
|
||||
$html .= $this->buildColorPicker('socialShareLinkedinColor', 'LinkedIn', $linkedinColor);
|
||||
|
||||
$whatsappColor = $this->renderer->getFieldValue($componentId, 'colors', 'whatsapp_color', '#198754');
|
||||
$html .= $this->buildColorPicker('socialShareWhatsappColor', 'WhatsApp', $whatsappColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$twitterColor = $this->renderer->getFieldValue($componentId, 'colors', 'twitter_color', '#212529');
|
||||
$html .= $this->buildColorPicker('socialShareTwitterColor', 'X (Twitter)', $twitterColor);
|
||||
|
||||
$emailColor = $this->renderer->getFieldValue($componentId, 'colors', 'email_color', '#6c757d');
|
||||
$html .= $this->buildColorPicker('socialShareEmailColor', 'Email', $emailColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// label_font_size
|
||||
$labelFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'label_font_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareLabelFontSize" class="form-label small mb-1 fw-semibold">Tamano etiqueta</label>';
|
||||
$html .= ' <input type="text" id="socialShareLabelFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($labelFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// icon_font_size
|
||||
$iconFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'icon_font_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareIconFontSize" class="form-label small mb-1 fw-semibold">Tamano iconos</label>';
|
||||
$html .= ' <input type="text" id="socialShareIconFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($iconFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// container_margin_top
|
||||
$containerMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_top', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
|
||||
$html .= ' <input type="text" id="socialShareMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerMarginTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// container_margin_bottom
|
||||
$containerMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_bottom', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
|
||||
$html .= ' <input type="text" id="socialShareMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// container_padding_top
|
||||
$containerPaddingTop = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding_top', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialSharePaddingTop" class="form-label small mb-1 fw-semibold">Padding superior</label>';
|
||||
$html .= ' <input type="text" id="socialSharePaddingTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerPaddingTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// container_padding_bottom
|
||||
$containerPaddingBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding_bottom', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialSharePaddingBottom" class="form-label small mb-1 fw-semibold">Padding inferior</label>';
|
||||
$html .= ' <input type="text" id="socialSharePaddingBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerPaddingBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// label_margin_bottom
|
||||
$labelMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'label_margin_bottom', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareLabelMarginBottom" class="form-label small mb-1 fw-semibold">Margen etiqueta</label>';
|
||||
$html .= ' <input type="text" id="socialShareLabelMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($labelMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// buttons_gap
|
||||
$buttonsGap = $this->renderer->getFieldValue($componentId, 'spacing', 'buttons_gap', '0.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareButtonsGap" class="form-label small mb-1 fw-semibold">Espacio botones</label>';
|
||||
$html .= ' <input type="text" id="socialShareButtonsGap" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonsGap) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// button_padding
|
||||
$buttonPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'button_padding', '0.25rem 0.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareButtonPadding" class="form-label small mb-1 fw-semibold">Padding botones</label>';
|
||||
$html .= ' <input type="text" id="socialShareButtonPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// border_top_width
|
||||
$borderTopWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_top_width', '1px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareBorderTopWidth" class="form-label small mb-1 fw-semibold">Grosor borde sup.</label>';
|
||||
$html .= ' <input type="text" id="socialShareBorderTopWidth" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderTopWidth) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_border_width
|
||||
$buttonBorderWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_width', '2px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareButtonBorderWidth" class="form-label small mb-1 fw-semibold">Grosor borde btn</label>';
|
||||
$html .= ' <input type="text" id="socialShareButtonBorderWidth" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonBorderWidth) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// button_border_radius
|
||||
$buttonBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '0.375rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareButtonBorderRadius" class="form-label small mb-1 fw-semibold">Radio botones</label>';
|
||||
$html .= ' <input type="text" id="socialShareButtonBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// transition_duration
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareTransitionDuration" class="form-label small mb-1 fw-semibold">Duracion transicion</label>';
|
||||
$html .= ' <input type="text" id="socialShareTransitionDuration" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// hover_box_shadow
|
||||
$hoverBoxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'hover_box_shadow', '0 4px 12px rgba(0, 0, 0, 0.15)');
|
||||
$html .= ' <div class="col-12">';
|
||||
$html .= ' <label for="socialShareHoverBoxShadow" class="form-label small mb-1 fw-semibold">Sombra hover</label>';
|
||||
$html .= ' <input type="text" id="socialShareHoverBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($hoverBoxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
// Normalizar valor booleano desde BD
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\TableOfContents\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Table of Contents
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class TableOfContentsFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'table-of-contents';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'tocEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'tocShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'tocShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'tocVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'tocVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'tocVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'tocVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'tocVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'tocExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'tocExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'tocExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'tocExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'tocTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||
'tocAutoGenerate' => ['group' => 'content', 'attribute' => 'auto_generate'],
|
||||
'tocHeadingLevels' => ['group' => 'content', 'attribute' => 'heading_levels'],
|
||||
'tocSmoothScroll' => ['group' => 'content', 'attribute' => 'smooth_scroll'],
|
||||
|
||||
// Typography
|
||||
'tocTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'],
|
||||
'tocTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'],
|
||||
'tocLinkFontSize' => ['group' => 'typography', 'attribute' => 'link_font_size'],
|
||||
'tocLinkLineHeight' => ['group' => 'typography', 'attribute' => 'link_line_height'],
|
||||
'tocLevelThreeFontSize' => ['group' => 'typography', 'attribute' => 'level_three_font_size'],
|
||||
'tocLevelFourFontSize' => ['group' => 'typography', 'attribute' => 'level_four_font_size'],
|
||||
|
||||
// Colors
|
||||
'tocBackgroundColor' => ['group' => 'colors', 'attribute' => 'background_color'],
|
||||
'tocBorderColor' => ['group' => 'colors', 'attribute' => 'border_color'],
|
||||
'tocTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'tocTitleBorderColor' => ['group' => 'colors', 'attribute' => 'title_border_color'],
|
||||
'tocLinkColor' => ['group' => 'colors', 'attribute' => 'link_color'],
|
||||
'tocLinkHoverColor' => ['group' => 'colors', 'attribute' => 'link_hover_color'],
|
||||
'tocLinkHoverBackground' => ['group' => 'colors', 'attribute' => 'link_hover_background'],
|
||||
'tocActiveBorderColor' => ['group' => 'colors', 'attribute' => 'active_border_color'],
|
||||
'tocActiveBackgroundColor' => ['group' => 'colors', 'attribute' => 'active_background_color'],
|
||||
'tocActiveTextColor' => ['group' => 'colors', 'attribute' => 'active_text_color'],
|
||||
'tocScrollbarTrackColor' => ['group' => 'colors', 'attribute' => 'scrollbar_track_color'],
|
||||
'tocScrollbarThumbColor' => ['group' => 'colors', 'attribute' => 'scrollbar_thumb_color'],
|
||||
|
||||
// Spacing
|
||||
'tocContainerPadding' => ['group' => 'spacing', 'attribute' => 'container_padding'],
|
||||
'tocMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'],
|
||||
'tocTitlePaddingBottom' => ['group' => 'spacing', 'attribute' => 'title_padding_bottom'],
|
||||
'tocTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||
'tocItemMarginBottom' => ['group' => 'spacing', 'attribute' => 'item_margin_bottom'],
|
||||
'tocLinkPadding' => ['group' => 'spacing', 'attribute' => 'link_padding'],
|
||||
'tocLevelThreePaddingLeft' => ['group' => 'spacing', 'attribute' => 'level_three_padding_left'],
|
||||
'tocLevelFourPaddingLeft' => ['group' => 'spacing', 'attribute' => 'level_four_padding_left'],
|
||||
'tocScrollbarWidth' => ['group' => 'spacing', 'attribute' => 'scrollbar_width'],
|
||||
|
||||
// Visual Effects
|
||||
'tocBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'tocBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
'tocBorderWidth' => ['group' => 'visual_effects', 'attribute' => 'border_width'],
|
||||
'tocLinkBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'link_border_radius'],
|
||||
'tocActiveBorderLeftWidth' => ['group' => 'visual_effects', 'attribute' => 'active_border_left_width'],
|
||||
'tocTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
'tocScrollbarBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'scrollbar_border_radius'],
|
||||
|
||||
// Behavior
|
||||
'tocIsSticky' => ['group' => 'behavior', 'attribute' => 'is_sticky'],
|
||||
'tocScrollOffset' => ['group' => 'behavior', 'attribute' => 'scroll_offset'],
|
||||
'tocMaxHeight' => ['group' => 'behavior', 'attribute' => 'max_height'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,638 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\TableOfContents\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para la Tabla de Contenido
|
||||
*
|
||||
* Responsabilidad:
|
||||
* - Generar HTML del formulario de configuracion
|
||||
* - Usar Design System (Bootstrap 5)
|
||||
* - Cargar valores desde BD via AdminDashboardRenderer
|
||||
*
|
||||
* @package ROITheme\Admin\TableOfContents\Infrastructure\Ui
|
||||
*/
|
||||
final class TableOfContentsFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildBehaviorGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-list-nested me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Tabla de Contenido';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Navegacion automatica con ScrollSpy';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="table-of-contents">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// is_enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('tocEnabled', 'Activar tabla de contenido', 'bi-power', $enabled);
|
||||
|
||||
// show_on_desktop
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('tocShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
// show_on_mobile
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
||||
$html .= $this->buildSwitch('tocShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'toc');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// title
|
||||
$title = $this->renderer->getFieldValue($componentId, 'content', 'title', 'Tabla de Contenido');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="tocTitle" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-type me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Titulo';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="tocTitle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($title) . '" placeholder="Tabla de Contenido">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// auto_generate
|
||||
$autoGenerate = $this->renderer->getFieldValue($componentId, 'content', 'auto_generate', true);
|
||||
$html .= $this->buildSwitch('tocAutoGenerate', 'Generar automaticamente', 'bi-magic', $autoGenerate);
|
||||
$html .= ' <small class="text-muted d-block mb-3">Genera TOC desde los encabezados del contenido</small>';
|
||||
|
||||
// heading_levels
|
||||
$headingLevels = $this->renderer->getFieldValue($componentId, 'content', 'heading_levels', 'h2,h3');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="tocHeadingLevels" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-list-ol me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Niveles de encabezados';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="tocHeadingLevels" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($headingLevels) . '" placeholder="h2,h3">';
|
||||
$html .= ' <small class="text-muted">Separados por coma: h2,h3,h4</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// smooth_scroll
|
||||
$smoothScroll = $this->renderer->getFieldValue($componentId, 'content', 'smooth_scroll', true);
|
||||
$html .= $this->buildSwitch('tocSmoothScroll', 'Scroll suave', 'bi-arrow-down-circle', $smoothScroll);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildBehaviorGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-gear me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Comportamiento';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// is_sticky
|
||||
$isSticky = $this->renderer->getFieldValue($componentId, 'behavior', 'is_sticky', true);
|
||||
$html .= $this->buildSwitch('tocIsSticky', 'Sticky (fijo al scroll)', 'bi-pin', $isSticky);
|
||||
|
||||
// scroll_offset
|
||||
$scrollOffset = $this->renderer->getFieldValue($componentId, 'behavior', 'scroll_offset', '100');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="tocScrollOffset" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-arrows-vertical me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Offset de scroll (px)';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="tocScrollOffset" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($scrollOffset) . '" placeholder="100">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// max_height
|
||||
$maxHeight = $this->renderer->getFieldValue($componentId, 'behavior', 'max_height', 'calc(100vh - 71px - 10px - 250px - 15px - 15px)');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="tocMaxHeight" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-arrows-expand me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Altura maxima';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="tocMaxHeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($maxHeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// title_font_size
|
||||
$titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocTitleFontSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
|
||||
$html .= ' <input type="text" id="tocTitleFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontSize) . '" placeholder="1rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// title_font_weight
|
||||
$titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '600');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocTitleFontWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
|
||||
$html .= ' <input type="text" id="tocTitleFontWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontWeight) . '" placeholder="600">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// link_font_size
|
||||
$linkFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'link_font_size', '0.9rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLinkFontSize" class="form-label small mb-1 fw-semibold">Tamano enlaces</label>';
|
||||
$html .= ' <input type="text" id="tocLinkFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($linkFontSize) . '" placeholder="0.9rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// link_line_height
|
||||
$linkLineHeight = $this->renderer->getFieldValue($componentId, 'typography', 'link_line_height', '1.3');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLinkLineHeight" class="form-label small mb-1 fw-semibold">Altura linea</label>';
|
||||
$html .= ' <input type="text" id="tocLinkLineHeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($linkLineHeight) . '" placeholder="1.3">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// level_three_font_size
|
||||
$level3FontSize = $this->renderer->getFieldValue($componentId, 'typography', 'level_three_font_size', '0.85rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLevelThreeFontSize" class="form-label small mb-1 fw-semibold">Tamano H3</label>';
|
||||
$html .= ' <input type="text" id="tocLevelThreeFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($level3FontSize) . '" placeholder="0.85rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// level_four_font_size
|
||||
$level4FontSize = $this->renderer->getFieldValue($componentId, 'typography', 'level_four_font_size', '0.8rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLevelFourFontSize" class="form-label small mb-1 fw-semibold">Tamano H4</label>';
|
||||
$html .= ' <input type="text" id="tocLevelFourFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($level4FontSize) . '" placeholder="0.8rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Colores principales
|
||||
$html .= ' <p class="small fw-semibold mb-2">Contenedor</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('tocBackgroundColor', 'Fondo', $bgColor);
|
||||
|
||||
$borderColor = $this->renderer->getFieldValue($componentId, 'colors', 'border_color', '#E6E9ED');
|
||||
$html .= $this->buildColorPicker('tocBorderColor', 'Borde', $borderColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores del titulo
|
||||
$html .= ' <p class="small fw-semibold mb-2">Titulo</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('tocTitleColor', 'Color', $titleColor);
|
||||
|
||||
$titleBorderColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_border_color', '#E6E9ED');
|
||||
$html .= $this->buildColorPicker('tocTitleBorderColor', 'Borde', $titleBorderColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores de enlaces
|
||||
$html .= ' <p class="small fw-semibold mb-2">Enlaces</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$linkColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_color', '#6B7280');
|
||||
$html .= $this->buildColorPicker('tocLinkColor', 'Normal', $linkColor);
|
||||
|
||||
$linkHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('tocLinkHoverColor', 'Hover', $linkHoverColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$linkHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_background', '#F9FAFB');
|
||||
$html .= $this->buildColorPicker('tocLinkHoverBackground', 'Fondo hover', $linkHoverBg);
|
||||
|
||||
$activeBorderColor = $this->renderer->getFieldValue($componentId, 'colors', 'active_border_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('tocActiveBorderColor', 'Borde activo', $activeBorderColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores de activo
|
||||
$html .= ' <p class="small fw-semibold mb-2">Estado Activo</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$activeBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'active_background_color', '#F9FAFB');
|
||||
$html .= $this->buildColorPicker('tocActiveBackgroundColor', 'Fondo', $activeBgColor);
|
||||
|
||||
$activeTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'active_text_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('tocActiveTextColor', 'Texto', $activeTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores de scrollbar
|
||||
$html .= ' <p class="small fw-semibold mb-2">Scrollbar</p>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$scrollbarTrack = $this->renderer->getFieldValue($componentId, 'colors', 'scrollbar_track_color', '#F9FAFB');
|
||||
$html .= $this->buildColorPicker('tocScrollbarTrackColor', 'Pista', $scrollbarTrack);
|
||||
|
||||
$scrollbarThumb = $this->renderer->getFieldValue($componentId, 'colors', 'scrollbar_thumb_color', '#6B7280');
|
||||
$html .= $this->buildColorPicker('tocScrollbarThumbColor', 'Thumb', $scrollbarThumb);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// container_padding
|
||||
$containerPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding', '12px 16px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocContainerPadding" class="form-label small mb-1 fw-semibold">Padding contenedor</label>';
|
||||
$html .= ' <input type="text" id="tocContainerPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// margin_bottom
|
||||
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '13px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
|
||||
$html .= ' <input type="text" id="tocMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// title_padding_bottom
|
||||
$titlePaddingBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_padding_bottom', '8px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocTitlePaddingBottom" class="form-label small mb-1 fw-semibold">Padding titulo</label>';
|
||||
$html .= ' <input type="text" id="tocTitlePaddingBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titlePaddingBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// title_margin_bottom
|
||||
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.75rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||
$html .= ' <input type="text" id="tocTitleMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// item_margin_bottom
|
||||
$itemMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'item_margin_bottom', '0.15rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocItemMarginBottom" class="form-label small mb-1 fw-semibold">Margen items</label>';
|
||||
$html .= ' <input type="text" id="tocItemMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($itemMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// link_padding
|
||||
$linkPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'link_padding', '0.3rem 0.85rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLinkPadding" class="form-label small mb-1 fw-semibold">Padding enlaces</label>';
|
||||
$html .= ' <input type="text" id="tocLinkPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($linkPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// level_three_padding_left
|
||||
$level3PaddingLeft = $this->renderer->getFieldValue($componentId, 'spacing', 'level_three_padding_left', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLevelThreePaddingLeft" class="form-label small mb-1 fw-semibold">Padding H3</label>';
|
||||
$html .= ' <input type="text" id="tocLevelThreePaddingLeft" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($level3PaddingLeft) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// level_four_padding_left
|
||||
$level4PaddingLeft = $this->renderer->getFieldValue($componentId, 'spacing', 'level_four_padding_left', '2rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLevelFourPaddingLeft" class="form-label small mb-1 fw-semibold">Padding H4</label>';
|
||||
$html .= ' <input type="text" id="tocLevelFourPaddingLeft" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($level4PaddingLeft) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// border_radius
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '8px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocBorderRadius" class="form-label small mb-1 fw-semibold">Radio borde</label>';
|
||||
$html .= ' <input type="text" id="tocBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// border_width
|
||||
$borderWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_width', '1px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocBorderWidth" class="form-label small mb-1 fw-semibold">Grosor borde</label>';
|
||||
$html .= ' <input type="text" id="tocBorderWidth" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderWidth) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// box_shadow
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 2px 8px rgba(0, 0, 0, 0.08)');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="tocBoxShadow" class="form-label small mb-1 fw-semibold">Sombra</label>';
|
||||
$html .= ' <input type="text" id="tocBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// link_border_radius
|
||||
$linkBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'link_border_radius', '4px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLinkBorderRadius" class="form-label small mb-1 fw-semibold">Radio enlaces</label>';
|
||||
$html .= ' <input type="text" id="tocLinkBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($linkBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// active_border_left_width
|
||||
$activeBorderLeftWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'active_border_left_width', '3px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocActiveBorderLeftWidth" class="form-label small mb-1 fw-semibold">Borde activo</label>';
|
||||
$html .= ' <input type="text" id="tocActiveBorderLeftWidth" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($activeBorderLeftWidth) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// transition_duration
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocTransitionDuration" class="form-label small mb-1 fw-semibold">Transicion</label>';
|
||||
$html .= ' <input type="text" id="tocTransitionDuration" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// scrollbar_border_radius
|
||||
$scrollbarBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'scrollbar_border_radius', '3px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocScrollbarBorderRadius" class="form-label small mb-1 fw-semibold">Radio scrollbar</label>';
|
||||
$html .= ' <input type="text" id="tocScrollbarBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($scrollbarBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, bool $checked): string
|
||||
{
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ThemeSettings\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Theme Settings
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*
|
||||
* NOTA: Logo/branding se gestiona desde el componente navbar
|
||||
*/
|
||||
final class ThemeSettingsFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'theme-settings';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Layout
|
||||
'themeSettingsContainerMaxWidth' => ['group' => 'layout', 'attribute' => 'container_max_width'],
|
||||
'themeSettingsContentColumnWidth' => ['group' => 'layout', 'attribute' => 'content_column_width'],
|
||||
|
||||
// Custom Code
|
||||
'themeSettingsCustomCss' => ['group' => 'custom_code', 'attribute' => 'custom_css'],
|
||||
'themeSettingsCustomJsHeader' => ['group' => 'custom_code', 'attribute' => 'custom_js_header'],
|
||||
'themeSettingsCustomJsFooter' => ['group' => 'custom_code', 'attribute' => 'custom_js_footer'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ThemeSettings\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
/**
|
||||
* FormBuilder para Theme Settings
|
||||
*
|
||||
* RESPONSABILIDAD: Generar formulario de configuraciones globales del tema
|
||||
* (JavaScript personalizado)
|
||||
*
|
||||
* NOTA: CSS personalizado se gestiona desde CustomCSSManager (TIPO 3)
|
||||
* Analytics y AdSense se gestionan desde el componente adsense-placement
|
||||
*
|
||||
* @package ROITheme\Admin\ThemeSettings\Infrastructure\Ui
|
||||
*/
|
||||
final class ThemeSettingsFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
// Layout Group
|
||||
$html .= $this->buildLayoutGroup($componentId);
|
||||
|
||||
// JavaScript Personalizado (solo 1 card)
|
||||
$html .= $this->buildJsGroup($componentId);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildLayoutGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-4" style="border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-layout-wtf me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Layout y Contenedor';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-3">';
|
||||
|
||||
// Container Max Width
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$containerWidth = $this->renderer->getFieldValue($componentId, 'layout', 'container_max_width', '1320');
|
||||
$containerWidthStr = is_string($containerWidth) ? $containerWidth : '1320';
|
||||
$html .= $this->buildSelect(
|
||||
'themeSettingsContainerMaxWidth',
|
||||
'Ancho maximo del contenedor',
|
||||
$containerWidthStr,
|
||||
[
|
||||
'1140' => '1140px (Bootstrap md)',
|
||||
'1200' => '1200px (Compacto)',
|
||||
'1320' => '1320px (Bootstrap xxl - Default)',
|
||||
'1400' => '1400px (Amplio)',
|
||||
'100%' => '100% (Fluido)'
|
||||
],
|
||||
'Valores menores dejan mas espacio para Rail Ads laterales'
|
||||
);
|
||||
$html .= ' </div>';
|
||||
|
||||
// Content Column Width
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$columnWidth = $this->renderer->getFieldValue($componentId, 'layout', 'content_column_width', 'col-lg-9');
|
||||
$columnWidthStr = is_string($columnWidth) ? $columnWidth : 'col-lg-9';
|
||||
$html .= $this->buildSelect(
|
||||
'themeSettingsContentColumnWidth',
|
||||
'Ancho columna de contenido',
|
||||
$columnWidthStr,
|
||||
[
|
||||
'col-lg-8' => '8 columnas (66.67%)',
|
||||
'col-lg-9' => '9 columnas (75% - Default)',
|
||||
'col-lg-10' => '10 columnas (83.33%)',
|
||||
'col-lg-12' => '12 columnas (100% sin sidebar)'
|
||||
],
|
||||
'Proporcion de la columna principal vs sidebar'
|
||||
);
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="alert alert-info small mb-0 mt-3">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' Reduce el ancho del contenedor para dar mas espacio a los Rail Ads en pantallas grandes.';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSelect(string $id, string $label, string $value, array $options, string $helpText = ''): string
|
||||
{
|
||||
$html = ' <div class="mb-3">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select class="form-select form-select-sm" id="' . esc_attr($id) . '">';
|
||||
foreach ($options as $optionValue => $optionLabel) {
|
||||
$selected = ($value === (string) $optionValue) ? ' selected' : '';
|
||||
$html .= ' <option value="' . esc_attr($optionValue) . '"' . $selected . '>' . esc_html($optionLabel) . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
if (!empty($helpText)) {
|
||||
$html .= ' <div class="form-text small">' . esc_html($helpText) . '</div>';
|
||||
}
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-gear me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuraciones Globales del Tema';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Layout y JavaScript Personalizado';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="theme-settings">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildJsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-filetype-js me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' JavaScript Personalizado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$customJsHeader = $this->renderer->getFieldValue($componentId, 'custom_code', 'custom_js_header', '');
|
||||
$html .= $this->buildTextareaCode(
|
||||
'themeSettingsCustomJsHeader',
|
||||
'JavaScript en Header',
|
||||
$customJsHeader,
|
||||
'Se inyecta en wp_head. No incluir etiquetas <script>',
|
||||
5
|
||||
);
|
||||
|
||||
$customJsFooter = $this->renderer->getFieldValue($componentId, 'custom_code', 'custom_js_footer', '');
|
||||
$html .= $this->buildTextareaCode(
|
||||
'themeSettingsCustomJsFooter',
|
||||
'JavaScript en Footer',
|
||||
$customJsFooter,
|
||||
'Se inyecta en wp_footer. No incluir etiquetas <script>',
|
||||
5
|
||||
);
|
||||
|
||||
$html .= ' <div class="alert alert-danger small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-exclamation-octagon me-1"></i>';
|
||||
$html .= ' <strong>Advertencia:</strong> El codigo JS puede afectar el rendimiento y seguridad del sitio.';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTextareaCode(string $id, string $label, mixed $value, string $helpText = '', int $rows = 4): string
|
||||
{
|
||||
$value = $this->normalizeStringValue($value);
|
||||
|
||||
$html = ' <div class="mb-3">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea class="form-control form-control-sm font-monospace" id="' . esc_attr($id) . '" rows="' . $rows . '" style="font-size: 0.85em;">' . esc_textarea($value) . '</textarea>';
|
||||
if (!empty($helpText)) {
|
||||
$html .= ' <div class="form-text small">' . $helpText . '</div>';
|
||||
}
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliza un valor a string para inputs de formulario
|
||||
*/
|
||||
private function normalizeStringValue(mixed $value): string
|
||||
{
|
||||
if ($value === false) {
|
||||
return '0';
|
||||
}
|
||||
if ($value === true) {
|
||||
return '1';
|
||||
}
|
||||
return (string) $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\TopNotificationBar\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Top Notification Bar
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class TopNotificationBarFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'top-notification-bar';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'topBarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'topBarShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'topBarShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'topBarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
|
||||
'topBarHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'topBarVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'topBarVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'topBarVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'topBarVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'topBarVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'topBarExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'topBarExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'topBarExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'topBarExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'topBarIconClass' => ['group' => 'content', 'attribute' => 'icon_class'],
|
||||
'topBarLabelText' => ['group' => 'content', 'attribute' => 'label_text'],
|
||||
'topBarMessageText' => ['group' => 'content', 'attribute' => 'message_text'],
|
||||
'topBarLinkText' => ['group' => 'content', 'attribute' => 'link_text'],
|
||||
'topBarLinkUrl' => ['group' => 'content', 'attribute' => 'link_url'],
|
||||
|
||||
// Colors
|
||||
'topBarBackgroundColor' => ['group' => 'colors', 'attribute' => 'background_color'],
|
||||
'topBarTextColor' => ['group' => 'colors', 'attribute' => 'text_color'],
|
||||
'topBarLabelColor' => ['group' => 'colors', 'attribute' => 'label_color'],
|
||||
'topBarIconColor' => ['group' => 'colors', 'attribute' => 'icon_color'],
|
||||
'topBarLinkColor' => ['group' => 'colors', 'attribute' => 'link_color'],
|
||||
'topBarLinkHoverColor' => ['group' => 'colors', 'attribute' => 'link_hover_color'],
|
||||
|
||||
// Spacing
|
||||
'topBarFontSize' => ['group' => 'spacing', 'attribute' => 'font_size'],
|
||||
'topBarPadding' => ['group' => 'spacing', 'attribute' => 'padding'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\TopNotificationBar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
final class TopNotificationBarFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
// Header
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
// Layout 2 columnas
|
||||
$html .= '<div class="row g-3">';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildTypographyAndSpacingGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuración de TopBar';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Personaliza la barra de notificación superior del sitio';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="top-notification-bar">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Activación y Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Switch: Enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="topBarEnabled" ';
|
||||
$html .= checked($enabled, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="topBarEnabled" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-power me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Activar TopBar</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Mobile
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="topBarShowOnMobile" ';
|
||||
$html .= checked($showOnMobile, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="topBarShowOnMobile" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Mobile</strong> <span class="text-muted">(<768px)</span>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Desktop
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="topBarShowOnDesktop" ';
|
||||
$html .= checked($showOnDesktop, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="topBarShowOnDesktop" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Desktop</strong> <span class="text-muted">(≥768px)</span>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'topBar');
|
||||
|
||||
// Switch: CSS Crítico
|
||||
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
||||
$html .= ' <div class="mb-2 mt-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="topBarIsCritical" ';
|
||||
$html .= checked($isCritical, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="topBarIsCritical" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-lightning-charge me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>CSS Crítico</strong>';
|
||||
$html .= ' <small class="text-muted d-block">Inyectar CSS en <head> para optimizar LCP</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Ocultar para usuarios logueados (Plan 99.16)
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($componentId, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="topBarHideForLoggedIn" ';
|
||||
$html .= checked($hideForLoggedIn, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="topBarHideForLoggedIn" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-person-lock me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Ocultar para usuarios logueados</strong>';
|
||||
$html .= ' <small class="text-muted d-block">No mostrar a usuarios con sesión iniciada</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-chat-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// icon_class + label_text (row)
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarIconClass" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-star-fill me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Clase del ícono';
|
||||
$html .= ' </label>';
|
||||
$iconClass = $this->renderer->getFieldValue($componentId, 'content', 'icon_class', 'bi-megaphone-fill');
|
||||
$html .= ' <input type="text" id="topBarIconClass" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($iconClass) . '" placeholder="bi-...">';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarLabelText" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-tag me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Etiqueta';
|
||||
$html .= ' </label>';
|
||||
$labelText = $this->renderer->getFieldValue($componentId, 'content', 'label_text', 'Nuevo:');
|
||||
$html .= ' <input type="text" id="topBarLabelText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($labelText) . '" maxlength="30">';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// message_text (textarea)
|
||||
$messageText = $this->renderer->getFieldValue($componentId, 'content', 'message_text',
|
||||
'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="topBarMessageText" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-chat-dots me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mensaje';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea id="topBarMessageText" class="form-control form-control-sm" rows="3" maxlength="200">';
|
||||
$html .= esc_textarea($messageText);
|
||||
$html .= ' </textarea>';
|
||||
$html .= ' <small class="text-muted">Máximo 200 caracteres</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// link_text + link_url (row)
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarLinkText" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Texto del enlace';
|
||||
$html .= ' </label>';
|
||||
$linkText = $this->renderer->getFieldValue($componentId, 'content', 'link_text', 'Ver Catálogo');
|
||||
$html .= ' <input type="text" id="topBarLinkText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($linkText) . '" maxlength="50">';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarLinkUrl" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-box-arrow-up-right me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' URL';
|
||||
$html .= ' </label>';
|
||||
$linkUrl = $this->renderer->getFieldValue($componentId, 'content', 'link_url', '#');
|
||||
$html .= ' <input type="url" id="topBarLinkUrl" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_url($linkUrl) . '" placeholder="https://...">';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Grid 2x3 de color pickers
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
|
||||
// Background Color
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('topBarBackgroundColor', 'Color de fondo', 'paint-bucket', $bgColor);
|
||||
|
||||
// Text Color
|
||||
$textColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_color', '#FFFFFF');
|
||||
$html .= $this->buildColorPicker('topBarTextColor', 'Color de texto', 'fonts', $textColor);
|
||||
|
||||
// Label Color
|
||||
$labelColor = $this->renderer->getFieldValue($componentId, 'colors', 'label_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('topBarLabelColor', 'Color etiqueta', 'tag-fill', $labelColor);
|
||||
|
||||
// Icon Color
|
||||
$iconColor = $this->renderer->getFieldValue($componentId, 'colors', 'icon_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('topBarIconColor', 'Color ícono', 'star', $iconColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Row 2 de color pickers
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// Link Color
|
||||
$linkColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_color', '#FFFFFF');
|
||||
$html .= $this->buildColorPicker('topBarLinkColor', 'Color enlace', 'link', $linkColor);
|
||||
|
||||
// Link Hover Color
|
||||
$linkHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('topBarLinkHoverColor', 'Color enlace (hover)', 'hand-index', $linkHoverColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyAndSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-fullscreen me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografía y Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// Font Size
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarFontSize" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-type me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tamaño de fuente';
|
||||
$html .= ' </label>';
|
||||
$fontSize = $this->renderer->getFieldValue($componentId, 'spacing', 'font_size', '0.9rem');
|
||||
$html .= ' <input type="text" id="topBarFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($fontSize) . '">';
|
||||
$html .= ' <small class="text-muted">Ej: 0.9rem, 14px</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Padding
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarPadding" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-bounding-box me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Padding vertical';
|
||||
$html .= ' </label>';
|
||||
$padding = $this->renderer->getFieldValue($componentId, 'spacing', 'padding', '0.5rem 0');
|
||||
$html .= ' <input type="text" id="topBarPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($padding) . '">';
|
||||
$html .= ' <small class="text-muted">Ej: 0.5rem 0</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $icon, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= ' <label for="' . $id . '" class="form-label small mb-1 fw-semibold" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-' . $icon . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . $label;
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="color" id="' . $id . '" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($value) . '" title="' . esc_attr($label) . '">';
|
||||
$html .= ' <small class="text-muted d-block mt-1" id="' . $id . 'Value">' . esc_html(strtoupper($value)) . '</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TopBar - Preview de Diseño</title>
|
||||
|
||||
<!-- Bootstrap 5 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background-color: #f0f0f1;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ============================================================
|
||||
TAB: TOP NOTIFICATION BAR CONFIGURATION
|
||||
============================================================ -->
|
||||
<div class="tab-pane fade show active" id="topBarTab" role="tabpanel">
|
||||
|
||||
<!-- ========================================
|
||||
PATRÓN 1: HEADER CON GRADIENTE
|
||||
======================================== -->
|
||||
<div class="rounded p-4 mb-4 shadow text-white" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h3 class="h4 mb-1 fw-bold">
|
||||
<i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>
|
||||
Configuración de TopBar
|
||||
</h3>
|
||||
<p class="mb-0 small" style="opacity: 0.85;">
|
||||
Personaliza la barra de notificación superior del sitio
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-light" id="resetTopBarDefaults">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
||||
Restaurar valores por defecto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
PATRÓN 2: LAYOUT 2 COLUMNAS
|
||||
======================================== -->
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
<!-- ========================================
|
||||
GRUPO 1: ACTIVACIÓN Y VISIBILIDAD (OBLIGATORIO)
|
||||
PATRÓN 3: CARD CON BORDER-LEFT NAVY
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>
|
||||
Activación y Visibilidad
|
||||
</h5>
|
||||
|
||||
<!-- ⚠️ PATRÓN 4: SWITCHES VERTICALES CON ICONOS (3 OBLIGATORIOS) -->
|
||||
|
||||
<!-- Switch 1: Enabled (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="topBarEnabled" checked>
|
||||
<label class="form-check-label small" for="topBarEnabled" style="color: #495057;">
|
||||
<i class="bi bi-power me-1" style="color: #FF8600;"></i>
|
||||
<strong>Activar TopBar</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Switch 2: Show on Mobile (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="topBarShowOnMobile" checked>
|
||||
<label class="form-check-label small" for="topBarShowOnMobile" style="color: #495057;">
|
||||
<i class="bi bi-phone me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar en Mobile</strong> <span class="text-muted">(<768px)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Switch 3: Show on Desktop (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="topBarShowOnDesktop" checked>
|
||||
<label class="form-check-label small" for="topBarShowOnDesktop" style="color: #495057;">
|
||||
<i class="bi bi-display me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar en Desktop</strong> <span class="text-muted">(≥768px)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campo adicional del schema: show_on_pages (select) -->
|
||||
<div class="mb-0 mt-3">
|
||||
<label for="topBarShowOnPages" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>
|
||||
Mostrar en
|
||||
</label>
|
||||
<select id="topBarShowOnPages" class="form-select form-select-sm">
|
||||
<option value="all" selected>Todas las páginas</option>
|
||||
<option value="home">Solo página de inicio</option>
|
||||
<option value="posts">Solo posts individuales</option>
|
||||
<option value="pages">Solo páginas</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 2: CONTENIDO
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-chat-text me-2" style="color: #FF8600;"></i>
|
||||
Contenido
|
||||
</h5>
|
||||
|
||||
<!-- icon_class + label_text (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="topBarIconClass" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-star-fill me-1" style="color: #FF8600;"></i>
|
||||
Clase del ícono
|
||||
</label>
|
||||
<input type="text" id="topBarIconClass" class="form-control form-control-sm" value="bi-megaphone-fill" placeholder="bi-...">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarLabelText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-tag me-1" style="color: #FF8600;"></i>
|
||||
Etiqueta
|
||||
</label>
|
||||
<input type="text" id="topBarLabelText" class="form-control form-control-sm" value="Nuevo:" maxlength="30">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- message_text (textarea full width) -->
|
||||
<div class="mb-2">
|
||||
<label for="topBarMessageText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-chat-dots me-1" style="color: #FF8600;"></i>
|
||||
Mensaje
|
||||
</label>
|
||||
<textarea id="topBarMessageText" class="form-control form-control-sm" rows="3" maxlength="200">Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.</textarea>
|
||||
<small class="text-muted">Máximo 200 caracteres</small>
|
||||
</div>
|
||||
|
||||
<!-- link_text + link_url (compactados) -->
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="topBarLinkText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>
|
||||
Texto del enlace
|
||||
</label>
|
||||
<input type="text" id="topBarLinkText" class="form-control form-control-sm" value="Ver Catálogo" maxlength="50">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarLinkUrl" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-box-arrow-up-right me-1" style="color: #FF8600;"></i>
|
||||
URL
|
||||
</label>
|
||||
<input type="url" id="topBarLinkUrl" class="form-control form-control-sm" value="#" placeholder="https://...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<!-- ========================================
|
||||
GRUPO 3: ESTILOS - COLORES
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
|
||||
Estilos - Colores
|
||||
</h5>
|
||||
|
||||
<!-- PATRÓN 5: COLOR PICKERS EN GRID 2X2 -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="topBarBackgroundColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
|
||||
Color de fondo
|
||||
</label>
|
||||
<input type="color" id="topBarBackgroundColor" class="form-control form-control-color w-100" value="#0E2337" title="Color de fondo">
|
||||
<small class="text-muted d-block mt-1" id="topBarBackgroundColorValue">#0E2337</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarTextColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Color de texto
|
||||
</label>
|
||||
<input type="color" id="topBarTextColor" class="form-control form-control-color w-100" value="#FFFFFF" title="Color de texto">
|
||||
<small class="text-muted d-block mt-1" id="topBarTextColorValue">#FFFFFF</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarLabelColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-tag-fill me-1" style="color: #FF8600;"></i>
|
||||
Color etiqueta
|
||||
</label>
|
||||
<input type="color" id="topBarLabelColor" class="form-control form-control-color w-100" value="#FF8600" title="Color etiqueta">
|
||||
<small class="text-muted d-block mt-1" id="topBarLabelColorValue">#FF8600</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarIconColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-star me-1" style="color: #FF8600;"></i>
|
||||
Color ícono
|
||||
</label>
|
||||
<input type="color" id="topBarIconColor" class="form-control form-control-color w-100" value="#FF8600" title="Color ícono">
|
||||
<small class="text-muted d-block mt-1" id="topBarIconColorValue">#FF8600</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="topBarLinkColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-link me-1" style="color: #FF8600;"></i>
|
||||
Color enlace
|
||||
</label>
|
||||
<input type="color" id="topBarLinkColor" class="form-control form-control-color w-100" value="#FFFFFF" title="Color enlace">
|
||||
<small class="text-muted d-block mt-1" id="topBarLinkColorValue">#FFFFFF</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarLinkHoverColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>
|
||||
Color enlace (hover)
|
||||
</label>
|
||||
<input type="color" id="topBarLinkHoverColor" class="form-control form-control-color w-100" value="#FF8600" title="Color enlace hover">
|
||||
<small class="text-muted d-block mt-1" id="topBarLinkHoverColorValue">#FF8600</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 4: ESTILOS - TAMAÑOS
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-arrows-fullscreen me-2" style="color: #FF8600;"></i>
|
||||
Estilos - Tamaños
|
||||
</h5>
|
||||
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="topBarFontSize" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-type me-1" style="color: #FF8600;"></i>
|
||||
Tamaño de fuente
|
||||
</label>
|
||||
<input type="text" id="topBarFontSize" class="form-control form-control-sm" value="0.9rem">
|
||||
<small class="text-muted">Ej: 0.9rem, 14px</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarPadding" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-bounding-box me-1" style="color: #FF8600;"></i>
|
||||
Padding vertical
|
||||
</label>
|
||||
<input type="text" id="topBarPadding" class="form-control form-control-sm" value="0.5rem 0">
|
||||
<small class="text-muted">Ej: 0.5rem 0</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /tab-pane -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1
Assets/CriticalCSS/responsive.critical.css
Normal file
1
Assets/CriticalCSS/responsive.critical.css
Normal file
@@ -0,0 +1 @@
|
||||
@media (max-width:575.98px){:root{--bs-gutter-x:1rem}body{font-size:14px}h1{font-size:24px}h2{font-size:20px}h3{font-size:18px}.container-fluid{padding:0 10px}.navbar{padding:0.5rem 0}.navbar-brand{font-size:18px}main{padding:0.5rem}.sidebar{margin-top:2rem}table{font-size:12px;margin-bottom:1rem;overflow-x:auto}.table-responsive{margin-bottom:1rem}.btn{padding:0.375rem 0.75rem;font-size:14px}.btn-lg{padding:0.5rem 1rem;font-size:16px}.card{margin-bottom:1rem}.form-group{margin-bottom:1rem}.form-control{padding:0.375rem 0.75rem;font-size:16px}.modal-dialog{margin:0.5rem}.modal-content{border-radius:4px}img{max-width:100%;height:auto}ul,ol{padding-left:1.5rem}.mt-1,.my-1{margin-top:0.25rem !important}.mb-1,.my-1{margin-bottom:0.25rem !important}.p-1{padding:0.25rem !important}}@media (min-width:576px){body{font-size:14px}h1{font-size:28px}h2{font-size:22px}h3{font-size:18px}}@media (min-width:768px){body{font-size:15px}h1{font-size:32px}h2{font-size:26px}h3{font-size:20px}.row-md-2{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem}.navbar{padding:1rem 0}.main-content{display:grid;grid-template-columns:1fr 300px;gap:2rem}.main-content.no-sidebar{grid-template-columns:1fr}}@media (min-width:992px){body{font-size:16px}h1{font-size:36px}h2{font-size:28px}h3{font-size:22px}.row-lg-3{display:grid;grid-template-columns:repeat(3,1fr);gap:2rem}.main-content{display:grid;grid-template-columns:1fr 300px;gap:2rem}.main-content.with-left-sidebar{grid-template-columns:250px 1fr 300px}.content-wrapper{max-width:1200px;margin:0 auto}}
|
||||
1
Assets/CriticalCSS/variables.critical.css
Normal file
1
Assets/CriticalCSS/variables.critical.css
Normal file
@@ -0,0 +1 @@
|
||||
:root{--color-navy-dark:#0E2337;--color-navy-primary:#1e3a5f;--color-navy-light:#2c5282;--color-blue-primary:#1e3a5f;--color-blue-secondary:#2c5282;--color-blue-light:#1a73e8;--color-cyan-primary:#61c7cd;--color-cyan-dark:#4db8c4;--color-cyan-darker:#4fb3b9;--color-orange-primary:#FF8600;--color-orange-secondary:#FFB800;--color-orange-light:#FFB800;--color-orange-button:#FF6B35;--color-orange-button-end:#FF8C42;--color-orange-hover:#FF6B35;--color-neutral-50:#f8f9fa;--color-neutral-100:#e9ecef;--color-neutral-600:#495057;--color-neutral-700:#6c757d;--color-slate-gray:#4C5C6B;--color-gray-50:#f8f9fa;--color-gray-100:#f7fafc;--color-gray-200:#e9ecef;--color-gray-300:#dee2e6;--color-gray-400:#cbd5e0;--color-gray-500:#a0aec0;--color-gray-600:#6c757d;--color-gray-700:#495057;--color-gray-800:#333;--color-gray-900:#212529;--color-gray-dark:#1a1a1a;--color-white:#ffffff;--color-black:#000000;--font-family-base:'Poppins',sans-serif;--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--font-size-base:1rem;--font-size-sm:0.875rem;--font-size-lg:1.125rem;--font-size-xl:1.25rem;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--line-height-base:1.5;--line-height-tight:1.25;--line-height-loose:1.8;--spacing-xs:0.25rem;--spacing-sm:0.5rem;--spacing-md:1rem;--spacing-lg:1.5rem;--spacing-xl:2rem;--spacing-2xl:3rem;--spacing-3xl:4rem;--border-width:1px;--border-width-thick:2px;--border-width-thicker:3px;--border-width-lateral:4px;--border-radius-sm:4px;--border-radius-md:8px;--border-radius-lg:12px;--border-radius-xl:16px;--border-color-light:var(--color-gray-200);--border-color-default:var(--color-gray-300);--shadow-xs:0 1px 2px rgba(0,0,0,0.05);--shadow-sm:0 2px 4px rgba(0,0,0,0.1);--shadow-md:0 4px 12px rgba(0,0,0,0.15);--shadow-lg:0 8px 24px rgba(0,0,0,0.2);--shadow-xl:0 12px 32px rgba(0,0,0,0.25);--shadow-2xl:0 20px 60px rgba(0,0,0,0.3);--shadow-navbar:0 2px 4px rgba(0,0,0,0.15);--shadow-navbar-scrolled:0 4px 12px rgba(0,0,0,0.25);--shadow-dropdown:0 8px 24px rgba(0,0,0,0.12);--shadow-cta:0 8px 24px rgba(255,133,0,0.3);--shadow-cta-hover:0 12px 32px rgba(255,133,0,0.4);--shadow-button:0 4px 12px rgba(255,107,53,0.3);--shadow-related-posts:0 12px 32px rgba(26,115,232,0.15);--shadow-pagination:0 4px 12px rgba(26,115,232,0.3);--transition-fast:0.15s ease;--transition-base:0.3s ease;--transition-slow:0.5s ease;--transition-cubic:cubic-bezier(0.4,0,0.2,1);--z-dropdown:1000;--z-sticky:1020;--z-navbar:1030;--z-modal-backdrop:1040;--z-modal:1050;--z-popover:1060;--z-tooltip:1070;--gradient-hero:linear-gradient(135deg,var(--color-blue-primary) 0%,var(--color-blue-secondary) 100%);--gradient-cta:linear-gradient(135deg,var(--color-orange-primary) 0%,var(--color-orange-secondary) 100%);--gradient-button-lets-talk:linear-gradient(135deg,var(--color-orange-button) 0%,var(--color-orange-button-end) 100%);--gradient-pagination:linear-gradient(135deg,var(--color-blue-primary) 0%,var(--color-blue-secondary) 100%);--gradient-underline:linear-gradient(90deg,var(--color-cyan-primary) 0%,var(--color-cyan-dark) 100%);--gradient-border-related:linear-gradient(180deg,var(--color-blue-primary) 0%,var(--color-blue-light) 100%);--opacity-disabled:0.5;--opacity-hover:0.8;--opacity-backdrop:0.5;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--breakpoint-xxl:1400px}
|
||||
821
Assets/Css/critical-bootstrap.css
Normal file
821
Assets/Css/critical-bootstrap.css
Normal file
@@ -0,0 +1,821 @@
|
||||
/**
|
||||
* Critical Bootstrap CSS Subset (TIPO 2)
|
||||
*
|
||||
* Contiene SOLO clases de Bootstrap 5.3.2 usadas en componentes above-the-fold.
|
||||
* NO contiene CSS personalizado (ese va en critical-custom-temp.css - TIPO 3).
|
||||
*
|
||||
* Componentes Bootstrap incluidos:
|
||||
* - Fonts (@font-face Poppins)
|
||||
* - Variables CSS (:root)
|
||||
* - Resets (box-sizing, body)
|
||||
* - Container system
|
||||
* - Grid system (row, col-*)
|
||||
* - Flexbox utilities (d-flex, justify-content-*, align-items-*)
|
||||
* - Spacing utilities (m-*, p-*)
|
||||
* - Text utilities
|
||||
* - Navbar component
|
||||
* - Collapse/Dropdown components
|
||||
* - Button component
|
||||
* - Alert component
|
||||
* - Typography base (h1-h6, p)
|
||||
* - Responsive breakpoints
|
||||
*
|
||||
* Hook: wp_head priority 0
|
||||
* Output: <style id="roi-critical-bootstrap">
|
||||
*
|
||||
* @version 5.3.2-subset
|
||||
* @see Inc/enqueue-scripts.php - Bootstrap diferido
|
||||
* @see Shared/Infrastructure/Services/CriticalBootstrapService.php
|
||||
* @see Assets/Css/critical-custom-temp.css - CSS personalizado (TIPO 3)
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
CRITICAL FONTS (Poppins - LCP optimization)
|
||||
|
||||
font-display: swap + preload = fuente carga rapido y siempre se muestra
|
||||
size-adjust: 100.6% = fallback casi identico a Poppins (minimiza CLS)
|
||||
========================================================================== */
|
||||
@font-face {
|
||||
font-family: 'Poppins Fallback';
|
||||
src: local('Arial'), local('Helvetica Neue'), local('sans-serif');
|
||||
size-adjust: 106%;
|
||||
ascent-override: 105%;
|
||||
descent-override: 35%;
|
||||
line-gap-override: 10%;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('/wp-content/themes/roi-theme/Assets/Fonts/poppins-v24-latin-regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('/wp-content/themes/roi-theme/Assets/Fonts/poppins-v24-latin-600.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('/wp-content/themes/roi-theme/Assets/Fonts/poppins-v24-latin-700.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Fonts */
|
||||
--font-primary: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||
--bs-body-font-family: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||
|
||||
/* Theme Colors (críticos para above-the-fold) */
|
||||
--color-navy-dark: #0E2337;
|
||||
--color-navy-medium: #1e3a5f;
|
||||
--color-orange-primary: #FF8600;
|
||||
--color-orange-hover: #e67a00;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-white: #fff;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
|
||||
/* Spacing */
|
||||
--bs-gutter-x: 1.5rem;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
BOX SIZING & RESETS (Bootstrap Reboot crítico)
|
||||
========================================================================== */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif);
|
||||
font-size: var(--bs-body-font-size, 1rem);
|
||||
font-weight: var(--bs-body-font-weight, 400);
|
||||
line-height: var(--bs-body-line-height, 1.5);
|
||||
color: var(--bs-body-color, #212529);
|
||||
background-color: var(--bs-body-bg, #fff);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--bs-link-color, #0d6efd);
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: var(--bs-link-hover-color, #0a58ca);
|
||||
}
|
||||
|
||||
img, svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
CONTAINER (Layout crítico)
|
||||
========================================================================== */
|
||||
.container,
|
||||
.container-fluid {
|
||||
--bs-gutter-x: 1.5rem;
|
||||
--bs-gutter-y: 0;
|
||||
width: 100%;
|
||||
padding-right: calc(var(--bs-gutter-x) * 0.5);
|
||||
padding-left: calc(var(--bs-gutter-x) * 0.5);
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.container { max-width: 540px; }
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.container { max-width: 720px; }
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.container { max-width: 960px; }
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container { max-width: 1140px; }
|
||||
}
|
||||
@media (min-width: 1400px) {
|
||||
.container { max-width: 1320px; }
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
GRID SYSTEM (Layout crítico - Previene CLS)
|
||||
========================================================================== */
|
||||
.row {
|
||||
--bs-gutter-x: 1.5rem;
|
||||
--bs-gutter-y: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: calc(-1 * var(--bs-gutter-y));
|
||||
margin-right: calc(-0.5 * var(--bs-gutter-x));
|
||||
margin-left: calc(-0.5 * var(--bs-gutter-x));
|
||||
}
|
||||
.row > * {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding-right: calc(var(--bs-gutter-x) * 0.5);
|
||||
padding-left: calc(var(--bs-gutter-x) * 0.5);
|
||||
margin-top: var(--bs-gutter-y);
|
||||
}
|
||||
|
||||
.col { flex: 1 0 0%; }
|
||||
.col-auto { flex: 0 0 auto; width: auto; }
|
||||
.col-1 { flex: 0 0 auto; width: 8.33333333%; }
|
||||
.col-2 { flex: 0 0 auto; width: 16.66666667%; }
|
||||
.col-3 { flex: 0 0 auto; width: 25%; }
|
||||
.col-4 { flex: 0 0 auto; width: 33.33333333%; }
|
||||
.col-5 { flex: 0 0 auto; width: 41.66666667%; }
|
||||
.col-6 { flex: 0 0 auto; width: 50%; }
|
||||
.col-7 { flex: 0 0 auto; width: 58.33333333%; }
|
||||
.col-8 { flex: 0 0 auto; width: 66.66666667%; }
|
||||
.col-9 { flex: 0 0 auto; width: 75%; }
|
||||
.col-10 { flex: 0 0 auto; width: 83.33333333%; }
|
||||
.col-11 { flex: 0 0 auto; width: 91.66666667%; }
|
||||
.col-12 { flex: 0 0 auto; width: 100%; }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.col-md-1 { flex: 0 0 auto; width: 8.33333333%; }
|
||||
.col-md-2 { flex: 0 0 auto; width: 16.66666667%; }
|
||||
.col-md-3 { flex: 0 0 auto; width: 25%; }
|
||||
.col-md-4 { flex: 0 0 auto; width: 33.33333333%; }
|
||||
.col-md-5 { flex: 0 0 auto; width: 41.66666667%; }
|
||||
.col-md-6 { flex: 0 0 auto; width: 50%; }
|
||||
.col-md-7 { flex: 0 0 auto; width: 58.33333333%; }
|
||||
.col-md-8 { flex: 0 0 auto; width: 66.66666667%; }
|
||||
.col-md-9 { flex: 0 0 auto; width: 75%; }
|
||||
.col-md-10 { flex: 0 0 auto; width: 83.33333333%; }
|
||||
.col-md-11 { flex: 0 0 auto; width: 91.66666667%; }
|
||||
.col-md-12 { flex: 0 0 auto; width: 100%; }
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.col-lg-1 { flex: 0 0 auto; width: 8.33333333%; }
|
||||
.col-lg-2 { flex: 0 0 auto; width: 16.66666667%; }
|
||||
.col-lg-3 { flex: 0 0 auto; width: 25%; }
|
||||
.col-lg-4 { flex: 0 0 auto; width: 33.33333333%; }
|
||||
.col-lg-5 { flex: 0 0 auto; width: 41.66666667%; }
|
||||
.col-lg-6 { flex: 0 0 auto; width: 50%; }
|
||||
.col-lg-7 { flex: 0 0 auto; width: 58.33333333%; }
|
||||
.col-lg-8 { flex: 0 0 auto; width: 66.66666667%; }
|
||||
.col-lg-9 { flex: 0 0 auto; width: 75%; }
|
||||
.col-lg-10 { flex: 0 0 auto; width: 83.33333333%; }
|
||||
.col-lg-11 { flex: 0 0 auto; width: 91.66666667%; }
|
||||
.col-lg-12 { flex: 0 0 auto; width: 100%; }
|
||||
}
|
||||
|
||||
/* Gutter utilities */
|
||||
.g-0, .gx-0 { --bs-gutter-x: 0; }
|
||||
.g-0, .gy-0 { --bs-gutter-y: 0; }
|
||||
.g-3, .gx-3 { --bs-gutter-x: 1rem; }
|
||||
.g-3, .gy-3 { --bs-gutter-y: 1rem; }
|
||||
|
||||
/* ==========================================================================
|
||||
FLEXBOX UTILITIES (Layout crítico)
|
||||
========================================================================== */
|
||||
.d-flex {
|
||||
display: flex !important;
|
||||
}
|
||||
.d-none {
|
||||
display: none !important;
|
||||
}
|
||||
.d-block {
|
||||
display: block !important;
|
||||
}
|
||||
.d-inline-block {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
/* Responsive Display Utilities - Previene CLS en TopNotificationBar */
|
||||
@media (min-width: 992px) {
|
||||
.d-lg-none {
|
||||
display: none !important;
|
||||
}
|
||||
.d-lg-block {
|
||||
display: block !important;
|
||||
}
|
||||
.d-lg-flex {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap !important;
|
||||
}
|
||||
.flex-column {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.justify-content-center {
|
||||
justify-content: center !important;
|
||||
}
|
||||
.justify-content-between {
|
||||
justify-content: space-between !important;
|
||||
}
|
||||
.justify-content-start {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
.justify-content-end {
|
||||
justify-content: flex-end !important;
|
||||
}
|
||||
|
||||
.align-items-center {
|
||||
align-items: center !important;
|
||||
}
|
||||
.align-items-start {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
.align-items-end {
|
||||
align-items: flex-end !important;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
.gap-3 {
|
||||
gap: 1rem !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
SPACING UTILITIES (Margin/Padding críticos)
|
||||
========================================================================== */
|
||||
.m-0 { margin: 0 !important; }
|
||||
.m-auto { margin: auto !important; }
|
||||
|
||||
.mb-0 { margin-bottom: 0 !important; }
|
||||
.mb-1 { margin-bottom: 0.25rem !important; }
|
||||
.mb-2 { margin-bottom: 0.5rem !important; }
|
||||
.mb-3 { margin-bottom: 1rem !important; }
|
||||
.mb-4 { margin-bottom: 1.5rem !important; }
|
||||
|
||||
.mt-0 { margin-top: 0 !important; }
|
||||
.mt-2 { margin-top: 0.5rem !important; }
|
||||
.mt-3 { margin-top: 1rem !important; }
|
||||
|
||||
.me-1 { margin-right: 0.25rem !important; }
|
||||
.me-2 { margin-right: 0.5rem !important; }
|
||||
.me-3 { margin-right: 1rem !important; }
|
||||
|
||||
.ms-2 { margin-left: 0.5rem !important; }
|
||||
.ms-3 { margin-left: 1rem !important; }
|
||||
|
||||
.mx-auto { margin-left: auto !important; margin-right: auto !important; }
|
||||
|
||||
.p-0 { padding: 0 !important; }
|
||||
.p-2 { padding: 0.5rem !important; }
|
||||
.p-3 { padding: 1rem !important; }
|
||||
|
||||
.py-2 { padding-top: 0.5rem !important; padding-bottom: 0.5rem !important; }
|
||||
.py-3 { padding-top: 1rem !important; padding-bottom: 1rem !important; }
|
||||
.py-4 { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; }
|
||||
|
||||
.px-2 { padding-left: 0.5rem !important; padding-right: 0.5rem !important; }
|
||||
.px-3 { padding-left: 1rem !important; padding-right: 1rem !important; }
|
||||
.px-4 { padding-left: 1.5rem !important; padding-right: 1.5rem !important; }
|
||||
|
||||
/* ==========================================================================
|
||||
SIZING UTILITIES (Width/Height críticos)
|
||||
========================================================================== */
|
||||
.w-100 { width: 100% !important; }
|
||||
.w-auto { width: auto !important; }
|
||||
.h-100 { height: 100% !important; }
|
||||
.h-auto { height: auto !important; }
|
||||
|
||||
/* ==========================================================================
|
||||
TEXT UTILITIES (Críticos para layout)
|
||||
========================================================================== */
|
||||
.text-center { text-align: center !important; }
|
||||
.text-start { text-align: left !important; }
|
||||
.text-end { text-align: right !important; }
|
||||
.text-white { color: #fff !important; }
|
||||
.text-muted { color: var(--bs-secondary-color, #6c757d) !important; }
|
||||
|
||||
.fw-normal { font-weight: 400 !important; }
|
||||
.fw-medium { font-weight: 500 !important; }
|
||||
.fw-semibold { font-weight: 600 !important; }
|
||||
.fw-bold { font-weight: 700 !important; }
|
||||
|
||||
.fs-5 { font-size: 1.25rem !important; }
|
||||
.fs-6 { font-size: 1rem !important; }
|
||||
|
||||
.small { font-size: 0.875em !important; }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.text-md-start { text-align: left !important; }
|
||||
.text-md-center { text-align: center !important; }
|
||||
.text-md-end { text-align: right !important; }
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
NAVBAR COMPONENT (Crítico - Above the fold)
|
||||
========================================================================== */
|
||||
.navbar {
|
||||
--bs-navbar-padding-x: 0;
|
||||
--bs-navbar-padding-y: 0.5rem;
|
||||
--bs-navbar-color: rgba(255, 255, 255, 0.55);
|
||||
--bs-navbar-hover-color: rgba(255, 255, 255, 0.75);
|
||||
--bs-navbar-disabled-color: rgba(255, 255, 255, 0.25);
|
||||
--bs-navbar-active-color: #fff;
|
||||
--bs-navbar-brand-padding-y: 0.3125rem;
|
||||
--bs-navbar-brand-margin-end: 1rem;
|
||||
--bs-navbar-brand-font-size: 1.25rem;
|
||||
--bs-navbar-brand-color: #fff;
|
||||
--bs-navbar-brand-hover-color: #fff;
|
||||
--bs-navbar-nav-link-padding-x: 0.5rem;
|
||||
--bs-navbar-toggler-padding-y: 0.25rem;
|
||||
--bs-navbar-toggler-padding-x: 0.75rem;
|
||||
--bs-navbar-toggler-font-size: 1.25rem;
|
||||
--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||
--bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1);
|
||||
--bs-navbar-toggler-border-radius: var(--bs-border-radius, 0.375rem);
|
||||
--bs-navbar-toggler-focus-width: 0.25rem;
|
||||
--bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;
|
||||
/* position: controlado por CriticalCSSService según sticky_enabled */
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--bs-navbar-padding-y) var(--bs-navbar-padding-x);
|
||||
}
|
||||
|
||||
.navbar > .container,
|
||||
.navbar > .container-fluid {
|
||||
display: flex;
|
||||
flex-wrap: inherit;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
padding-top: var(--bs-navbar-brand-padding-y);
|
||||
padding-bottom: var(--bs-navbar-brand-padding-y);
|
||||
margin-right: var(--bs-navbar-brand-margin-end);
|
||||
font-size: var(--bs-navbar-brand-font-size);
|
||||
color: var(--bs-navbar-brand-color);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.navbar-brand:hover,
|
||||
.navbar-brand:focus {
|
||||
color: var(--bs-navbar-brand-hover-color);
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
--bs-nav-link-padding-x: 0;
|
||||
--bs-nav-link-padding-y: 0.5rem;
|
||||
--bs-nav-link-color: var(--bs-navbar-color);
|
||||
--bs-nav-link-hover-color: var(--bs-navbar-hover-color);
|
||||
--bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: 0;
|
||||
margin-bottom: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
color: var(--bs-nav-link-color);
|
||||
}
|
||||
.navbar-nav .nav-link:hover,
|
||||
.navbar-nav .nav-link:focus {
|
||||
color: var(--bs-nav-link-hover-color);
|
||||
}
|
||||
.navbar-nav .nav-link.active {
|
||||
color: var(--bs-navbar-active-color);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: block;
|
||||
padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);
|
||||
font-size: var(--bs-nav-link-font-size);
|
||||
font-weight: var(--bs-nav-link-font-weight);
|
||||
color: var(--bs-nav-link-color);
|
||||
text-decoration: none;
|
||||
background: 0 0;
|
||||
border: 0;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.navbar-toggler {
|
||||
padding: var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);
|
||||
font-size: var(--bs-navbar-toggler-font-size);
|
||||
line-height: 1;
|
||||
color: var(--bs-navbar-color);
|
||||
background-color: transparent;
|
||||
border: var(--bs-border-width, 1px) solid var(--bs-navbar-toggler-border-color);
|
||||
border-radius: var(--bs-navbar-toggler-border-radius);
|
||||
transition: var(--bs-navbar-toggler-transition);
|
||||
}
|
||||
.navbar-toggler:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.navbar-toggler:focus {
|
||||
text-decoration: none;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 var(--bs-navbar-toggler-focus-width);
|
||||
}
|
||||
|
||||
.navbar-toggler-icon {
|
||||
display: inline-block;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
vertical-align: middle;
|
||||
background-image: var(--bs-navbar-toggler-icon-bg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
.navbar-dark,
|
||||
.navbar[data-bs-theme="dark"] {
|
||||
--bs-navbar-color: rgba(255, 255, 255, 0.55);
|
||||
--bs-navbar-hover-color: rgba(255, 255, 255, 0.75);
|
||||
--bs-navbar-disabled-color: rgba(255, 255, 255, 0.25);
|
||||
--bs-navbar-active-color: #fff;
|
||||
--bs-navbar-brand-color: #fff;
|
||||
--bs-navbar-brand-hover-color: #fff;
|
||||
--bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1);
|
||||
--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
COLLAPSE COMPONENT (Navbar mobile)
|
||||
========================================================================== */
|
||||
.collapse:not(.show) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
flex-basis: 100%;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
DROPDOWN COMPONENT (Navbar submenus)
|
||||
========================================================================== */
|
||||
.dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dropdown-toggle::after {
|
||||
display: inline-block;
|
||||
margin-left: 0.255em;
|
||||
vertical-align: 0.255em;
|
||||
content: "";
|
||||
border-top: 0.3em solid;
|
||||
border-right: 0.3em solid transparent;
|
||||
border-bottom: 0;
|
||||
border-left: 0.3em solid transparent;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
--bs-dropdown-zindex: 1000;
|
||||
--bs-dropdown-min-width: 10rem;
|
||||
--bs-dropdown-padding-x: 0;
|
||||
--bs-dropdown-padding-y: 0.5rem;
|
||||
--bs-dropdown-spacer: 0.125rem;
|
||||
--bs-dropdown-font-size: 1rem;
|
||||
--bs-dropdown-color: var(--bs-body-color, #212529);
|
||||
--bs-dropdown-bg: var(--bs-body-bg, #fff);
|
||||
--bs-dropdown-border-color: var(--bs-border-color-translucent, rgba(0,0,0,.175));
|
||||
--bs-dropdown-border-radius: var(--bs-border-radius, 0.375rem);
|
||||
--bs-dropdown-border-width: var(--bs-border-width, 1px);
|
||||
--bs-dropdown-inner-border-radius: calc(var(--bs-border-radius, 0.375rem) - var(--bs-border-width, 1px));
|
||||
--bs-dropdown-divider-bg: var(--bs-border-color-translucent, rgba(0,0,0,.175));
|
||||
--bs-dropdown-divider-margin-y: 0.5rem;
|
||||
--bs-dropdown-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-dropdown-link-color: var(--bs-body-color, #212529);
|
||||
--bs-dropdown-link-hover-color: var(--bs-body-color, #212529);
|
||||
--bs-dropdown-link-hover-bg: var(--bs-tertiary-bg, #f8f9fa);
|
||||
--bs-dropdown-link-active-color: #fff;
|
||||
--bs-dropdown-link-active-bg: #0d6efd;
|
||||
--bs-dropdown-link-disabled-color: var(--bs-tertiary-color, #adb5bd);
|
||||
--bs-dropdown-item-padding-x: 1rem;
|
||||
--bs-dropdown-item-padding-y: 0.25rem;
|
||||
--bs-dropdown-header-color: #6c757d;
|
||||
--bs-dropdown-header-padding-x: 1rem;
|
||||
--bs-dropdown-header-padding-y: 0.5rem;
|
||||
position: absolute;
|
||||
z-index: var(--bs-dropdown-zindex);
|
||||
display: none;
|
||||
min-width: var(--bs-dropdown-min-width);
|
||||
padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);
|
||||
margin: 0;
|
||||
font-size: var(--bs-dropdown-font-size);
|
||||
color: var(--bs-dropdown-color);
|
||||
text-align: left;
|
||||
list-style: none;
|
||||
background-color: var(--bs-dropdown-bg);
|
||||
background-clip: padding-box;
|
||||
border: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);
|
||||
border-radius: var(--bs-dropdown-border-radius);
|
||||
}
|
||||
|
||||
.dropdown-menu.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);
|
||||
clear: both;
|
||||
font-weight: 400;
|
||||
color: var(--bs-dropdown-link-color);
|
||||
text-align: inherit;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
.dropdown-item:hover,
|
||||
.dropdown-item:focus {
|
||||
color: var(--bs-dropdown-link-hover-color);
|
||||
background-color: var(--bs-dropdown-link-hover-bg);
|
||||
}
|
||||
.dropdown-item.active,
|
||||
.dropdown-item:active {
|
||||
color: var(--bs-dropdown-link-active-color);
|
||||
text-decoration: none;
|
||||
background-color: var(--bs-dropdown-link-active-bg);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
TEXT UTILITIES
|
||||
========================================================================== */
|
||||
.text-decoration-underline {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
.text-decoration-none {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
IMAGE UTILITIES
|
||||
========================================================================== */
|
||||
.img-fluid {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
ALERT COMPONENT (Above-the-fold notifications)
|
||||
========================================================================== */
|
||||
.alert {
|
||||
--bs-alert-padding-x: 1rem;
|
||||
--bs-alert-padding-y: 1rem;
|
||||
--bs-alert-margin-bottom: 1rem;
|
||||
--bs-alert-border-radius: 0.375rem;
|
||||
position: relative;
|
||||
padding: var(--bs-alert-padding-y) var(--bs-alert-padding-x);
|
||||
margin-bottom: var(--bs-alert-margin-bottom);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--bs-alert-border-radius);
|
||||
}
|
||||
.alert-warning {
|
||||
--bs-alert-color: #664d03;
|
||||
--bs-alert-bg: #fff3cd;
|
||||
--bs-alert-border-color: #ffecb5;
|
||||
color: var(--bs-alert-color);
|
||||
background-color: var(--bs-alert-bg);
|
||||
border-color: var(--bs-alert-border-color);
|
||||
}
|
||||
.alert-info {
|
||||
--bs-alert-color: #055160;
|
||||
--bs-alert-bg: #cff4fc;
|
||||
--bs-alert-border-color: #b6effb;
|
||||
color: var(--bs-alert-color);
|
||||
background-color: var(--bs-alert-bg);
|
||||
border-color: var(--bs-alert-border-color);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
BUTTON COMPONENT (Above-the-fold - Navbar CTA)
|
||||
========================================================================== */
|
||||
.btn {
|
||||
--bs-btn-padding-x: 0.75rem;
|
||||
--bs-btn-padding-y: 0.375rem;
|
||||
--bs-btn-font-size: 1rem;
|
||||
--bs-btn-font-weight: 400;
|
||||
--bs-btn-line-height: 1.5;
|
||||
--bs-btn-color: var(--bs-body-color);
|
||||
--bs-btn-bg: transparent;
|
||||
--bs-btn-border-width: var(--bs-border-width, 1px);
|
||||
--bs-btn-border-color: transparent;
|
||||
--bs-btn-border-radius: var(--bs-border-radius, 0.375rem);
|
||||
--bs-btn-hover-border-color: transparent;
|
||||
--bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
--bs-btn-disabled-opacity: 0.65;
|
||||
--bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), 0.5);
|
||||
display: inline-block;
|
||||
padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x);
|
||||
font-family: var(--bs-btn-font-family);
|
||||
font-size: var(--bs-btn-font-size);
|
||||
font-weight: var(--bs-btn-font-weight);
|
||||
line-height: var(--bs-btn-line-height);
|
||||
color: var(--bs-btn-color);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border: var(--bs-btn-border-width) solid var(--bs-btn-border-color);
|
||||
border-radius: var(--bs-btn-border-radius);
|
||||
background-color: var(--bs-btn-bg);
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
.btn:hover {
|
||||
color: var(--bs-btn-hover-color);
|
||||
background-color: var(--bs-btn-hover-bg);
|
||||
border-color: var(--bs-btn-hover-border-color);
|
||||
}
|
||||
.btn:focus-visible {
|
||||
color: var(--bs-btn-hover-color);
|
||||
background-color: var(--bs-btn-hover-bg);
|
||||
border-color: var(--bs-btn-hover-border-color);
|
||||
outline: 0;
|
||||
box-shadow: var(--bs-btn-focus-box-shadow);
|
||||
}
|
||||
.btn:disabled, .btn.disabled {
|
||||
pointer-events: none;
|
||||
opacity: var(--bs-btn-disabled-opacity);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
BUTTON CLOSE (Dismiss notification)
|
||||
========================================================================== */
|
||||
.btn-close {
|
||||
--bs-btn-close-color: #000;
|
||||
--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");
|
||||
--bs-btn-close-opacity: 0.5;
|
||||
--bs-btn-close-hover-opacity: 0.75;
|
||||
--bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
--bs-btn-close-focus-opacity: 1;
|
||||
--bs-btn-close-disabled-opacity: 0.25;
|
||||
--bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);
|
||||
box-sizing: content-box;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
padding: 0.25em 0.25em;
|
||||
color: var(--bs-btn-close-color);
|
||||
background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat;
|
||||
border: 0;
|
||||
border-radius: 0.375rem;
|
||||
opacity: var(--bs-btn-close-opacity);
|
||||
}
|
||||
.btn-close:hover {
|
||||
color: var(--bs-btn-close-color);
|
||||
text-decoration: none;
|
||||
opacity: var(--bs-btn-close-hover-opacity);
|
||||
}
|
||||
.btn-close:focus {
|
||||
outline: 0;
|
||||
box-shadow: var(--bs-btn-close-focus-shadow);
|
||||
opacity: var(--bs-btn-close-focus-opacity);
|
||||
}
|
||||
|
||||
.btn-close-white {
|
||||
filter: var(--bs-btn-close-white-filter);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
RESPONSIVE BREAKPOINTS (navbar-expand-lg)
|
||||
========================================================================== */
|
||||
@media (min-width: 992px) {
|
||||
.navbar-expand-lg {
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.navbar-expand-lg .navbar-nav {
|
||||
flex-direction: row;
|
||||
}
|
||||
.navbar-expand-lg .navbar-nav .dropdown-menu {
|
||||
position: absolute;
|
||||
}
|
||||
.navbar-expand-lg .navbar-nav .nav-link {
|
||||
padding-right: var(--bs-navbar-nav-link-padding-x);
|
||||
padding-left: var(--bs-navbar-nav-link-padding-x);
|
||||
}
|
||||
.navbar-expand-lg .navbar-collapse {
|
||||
display: flex !important;
|
||||
flex-basis: auto;
|
||||
}
|
||||
.navbar-expand-lg .navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.d-lg-block { display: block !important; }
|
||||
.d-lg-none { display: none !important; }
|
||||
.mb-lg-0 { margin-bottom: 0 !important; }
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.navbar-expand-lg > .container,
|
||||
.navbar-expand-lg > .container-fluid {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
RESPONSIVE DISPLAY UTILITIES (md breakpoint)
|
||||
========================================================================== */
|
||||
@media (min-width: 768px) {
|
||||
.d-md-block { display: block !important; }
|
||||
.d-md-none { display: none !important; }
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
TYPOGRAPHY BASE (Critical)
|
||||
========================================================================== */
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
h1 { font-size: calc(1.375rem + 1.5vw); }
|
||||
h2 { font-size: calc(1.325rem + 0.9vw); }
|
||||
h3 { font-size: calc(1.3rem + 0.6vw); }
|
||||
h4 { font-size: calc(1.275rem + 0.3vw); }
|
||||
h5 { font-size: 1.25rem; }
|
||||
h6 { font-size: 1rem; }
|
||||
@media (min-width: 1200px) {
|
||||
h1 { font-size: 2.5rem; }
|
||||
h2 { font-size: 2rem; }
|
||||
h3 { font-size: 1.75rem; }
|
||||
h4 { font-size: 1.5rem; }
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* screen reader utilities, and minimum touch targets.
|
||||
* Compliant with WCAG 2.1 Level AA standards.
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -662,7 +662,7 @@ select:valid {
|
||||
*/
|
||||
|
||||
/* Links del TOC con focus visible */
|
||||
.apus-toc a:focus,
|
||||
.roi-toc a:focus,
|
||||
.toc-link:focus {
|
||||
outline: 3px solid #0066cc;
|
||||
outline-offset: 2px;
|
||||
@@ -671,7 +671,7 @@ select:valid {
|
||||
}
|
||||
|
||||
/* Item activo del TOC */
|
||||
.apus-toc a.active,
|
||||
.roi-toc a.active,
|
||||
.toc-link.active {
|
||||
font-weight: bold;
|
||||
border-left: 4px solid #0066cc;
|
||||
@@ -679,11 +679,11 @@ select:valid {
|
||||
}
|
||||
|
||||
/* Botón toggle del TOC con ARIA */
|
||||
.apus-toc-toggle[aria-expanded="true"]::before {
|
||||
.roi-toc-toggle[aria-expanded="true"]::before {
|
||||
content: "▼ ";
|
||||
}
|
||||
|
||||
.apus-toc-toggle[aria-expanded="false"]::before {
|
||||
.roi-toc-toggle[aria-expanded="false"]::before {
|
||||
content: "▶ ";
|
||||
}
|
||||
|
||||
1
Assets/Css/css-global-accessibility.min.css
vendored
Normal file
1
Assets/Css/css-global-accessibility.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
||||
* Animation Styles
|
||||
*
|
||||
* CSS animations and keyframes for the theme
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* NOTA: Todos los estilos de badges están en style.css según template original.
|
||||
* Este archivo se mantiene vacío para evitar duplicaciones.
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Sistema de Tipografías - APUS Theme
|
||||
* Sistema de Tipografías - ROI Theme
|
||||
*
|
||||
* RESPONSABILIDAD: SOLO definición de fuentes y variables tipográficas
|
||||
* - Declaraciones @font-face (comentadas - usar Google Fonts)
|
||||
@@ -11,7 +11,7 @@
|
||||
* - Estilos de elementos HTML (van en style.css)
|
||||
* - Variables de colores o espaciados (van en variables.css)
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -24,11 +24,12 @@
|
||||
--font-system: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
|
||||
|
||||
/* Fuente primaria - Poppins según template y documentación */
|
||||
--font-primary: 'Poppins', sans-serif;
|
||||
/* Fuente primaria - Poppins con fallback ajustado (Fase 4.3 PageSpeed)
|
||||
'Poppins Fallback' tiene size-adjust para reducir CLS durante font swap */
|
||||
--font-primary: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||
|
||||
/* Fuente para encabezados - Poppins según template */
|
||||
--font-headings: 'Poppins', sans-serif;
|
||||
/* Fuente para encabezados - Poppins con fallback ajustado */
|
||||
--font-headings: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||
|
||||
/* Fuente para código (monospace) */
|
||||
--font-mono: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono',
|
||||
@@ -45,26 +46,41 @@
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
POPPINS (Opcional - Solo si se activa)
|
||||
POPPINS (Self-hosted)
|
||||
============================================
|
||||
|
||||
Las siguientes declaraciones @font-face solo
|
||||
se cargan cuando el usuario activa "Use Custom Fonts"
|
||||
en Apariencia > Personalizar > Tipografía.
|
||||
Fuentes Poppins alojadas localmente para:
|
||||
- Eliminar dependencia de Google Fonts
|
||||
- Mejorar rendimiento (sin requests externos)
|
||||
- Cumplimiento GDPR (sin tracking de Google)
|
||||
|
||||
Para activar Poppins:
|
||||
1. Descargar archivos WOFF2 de Google Fonts
|
||||
2. Colocar en assets/fonts/poppins/
|
||||
3. Descomentar las declaraciones @font-face
|
||||
4. Activar en Customizer
|
||||
Pesos incluidos: 400, 500, 600, 700
|
||||
Formato: WOFF2 (mejor compresión)
|
||||
|
||||
Fase 4.3 PageSpeed: Fallback con size-adjust para reducir CLS
|
||||
- size-adjust: 100.6% ajustado para coincidir mejor con Poppins
|
||||
- font-display: swap + preload = carga rapida sin salto visual
|
||||
- Preload en CriticalCSSInjector P:-2 acelera descarga de fuentes
|
||||
|
||||
NOTA: El valor 100.6% fue calibrado empiricamente.
|
||||
- 106% causaba un salto visual notable (navbar se "achicaba")
|
||||
- 100.6% minimiza el CLS manteniendo legibilidad del fallback
|
||||
|
||||
============================================ */
|
||||
|
||||
/*
|
||||
/* Fallback font con metricas ajustadas para Poppins */
|
||||
@font-face {
|
||||
font-family: 'Poppins Fallback';
|
||||
src: local('Arial'), local('Helvetica Neue'), local('Helvetica'), local('sans-serif');
|
||||
size-adjust: 106%;
|
||||
ascent-override: 105%;
|
||||
descent-override: 35%;
|
||||
line-gap-override: 10%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../fonts/poppins/Poppins-Regular.woff2') format('woff2'),
|
||||
url('../fonts/poppins/Poppins-Regular.woff') format('woff');
|
||||
src: url('../Fonts/poppins-v24-latin-regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -72,8 +88,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../fonts/poppins/Poppins-Medium.woff2') format('woff2'),
|
||||
url('../fonts/poppins/Poppins-Medium.woff') format('woff');
|
||||
src: url('../Fonts/poppins-v24-latin-500.woff2') format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -81,8 +96,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../fonts/poppins/Poppins-SemiBold.woff2') format('woff2'),
|
||||
url('../fonts/poppins/Poppins-SemiBold.woff') format('woff');
|
||||
src: url('../Fonts/poppins-v24-latin-600.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -90,21 +104,11 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../fonts/poppins/Poppins-Bold.woff2') format('woff2'),
|
||||
url('../fonts/poppins/Poppins-Bold.woff') format('woff');
|
||||
src: url('../Fonts/poppins-v24-latin-700.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
*/
|
||||
|
||||
/* Cuando Poppins esté activo, se aplica con clase .use-custom-fonts */
|
||||
/*
|
||||
.use-custom-fonts {
|
||||
--font-primary: 'Poppins', var(--font-system);
|
||||
--font-headings: 'Poppins', var(--font-system);
|
||||
}
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
UTILIDADES DE FUENTES
|
||||
@@ -4,7 +4,7 @@
|
||||
* Estilos para tablas genéricas en post-content (NO tablas APU)
|
||||
* Aplica 10 estilos diferentes automáticamente a las primeras 11 tablas
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -63,13 +63,14 @@
|
||||
|
||||
/* ========================================
|
||||
STYLE 2: Orange Header with Light Background
|
||||
Fase 4.4 Accesibilidad: Texto oscuro para contraste WCAG AA (4.5:1)
|
||||
======================================== */
|
||||
|
||||
.post-content table:not(.analisis table):nth-of-type(3) thead tr:first-child th,
|
||||
.post-content table:not(.analisis table):nth-of-type(3) tbody tr:first-child td,
|
||||
.post-content table:not(.analisis table):nth-of-type(3) tr:first-child td {
|
||||
background: var(--color-orange-primary);
|
||||
color: #ffffff !important;
|
||||
color: var(--color-navy-dark) !important;
|
||||
border: none !important;
|
||||
box-shadow: 0 2px 8px rgba(255, 133, 0, 0.3);
|
||||
}
|
||||
@@ -126,13 +127,14 @@
|
||||
|
||||
/* ========================================
|
||||
STYLE 5: Orange Gradient Header
|
||||
Fase 4.4 Accesibilidad: Texto oscuro para contraste WCAG AA (4.5:1)
|
||||
======================================== */
|
||||
|
||||
.post-content table:not(.analisis table):nth-of-type(6) thead tr:first-child th,
|
||||
.post-content table:not(.analisis table):nth-of-type(6) tbody tr:first-child td,
|
||||
.post-content table:not(.analisis table):nth-of-type(6) tr:first-child td {
|
||||
background: linear-gradient(135deg, var(--color-orange-primary) 0%, var(--color-orange-light) 100%);
|
||||
color: #ffffff !important;
|
||||
color: var(--color-navy-dark) !important;
|
||||
border: none !important;
|
||||
box-shadow: 0 2px 8px rgba(255, 133, 0, 0.35);
|
||||
}
|
||||
@@ -168,13 +170,14 @@
|
||||
|
||||
/* ========================================
|
||||
STYLE 7: Light Orange Background
|
||||
Fase 4.4 Accesibilidad: Texto oscuro para contraste WCAG AA (4.5:1)
|
||||
======================================== */
|
||||
|
||||
.post-content table:not(.analisis table):nth-of-type(8) thead tr:first-child th,
|
||||
.post-content table:not(.analisis table):nth-of-type(8) tbody tr:first-child td,
|
||||
.post-content table:not(.analisis table):nth-of-type(8) tr:first-child td {
|
||||
background: var(--color-orange-primary);
|
||||
color: #ffffff !important;
|
||||
color: var(--color-navy-dark) !important;
|
||||
border: none !important;
|
||||
border-bottom: 3px solid var(--color-navy-primary) !important;
|
||||
}
|
||||
@@ -235,6 +238,7 @@
|
||||
|
||||
/* ========================================
|
||||
STYLE 10: Bold Orange Border
|
||||
Fase 4.4 Accesibilidad: Texto oscuro para contraste WCAG AA (4.5:1)
|
||||
======================================== */
|
||||
|
||||
.post-content table:not(.analisis table):nth-of-type(11) {
|
||||
@@ -245,7 +249,7 @@
|
||||
.post-content table:not(.analisis table):nth-of-type(11) tbody tr:first-child td,
|
||||
.post-content table:not(.analisis table):nth-of-type(11) tr:first-child td {
|
||||
background: linear-gradient(135deg, var(--color-orange-hover) 0%, var(--color-orange-primary) 100%);
|
||||
color: #ffffff !important;
|
||||
color: var(--color-navy-dark) !important;
|
||||
border: none !important;
|
||||
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.4);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* Estilos personalizados para paginación
|
||||
* Template ref: css/style.css líneas 180-207
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -37,9 +37,7 @@
|
||||
color: var(--color-orange-primary);
|
||||
background-color: rgba(255, 133, 0, 0.1);
|
||||
border-color: var(--color-orange-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(255, 133, 0, 0.15);
|
||||
z-index: 2;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.page-link:focus {
|
||||
@@ -53,17 +51,8 @@
|
||||
/* Active page */
|
||||
.page-item.active .page-link {
|
||||
color: #ffffff;
|
||||
background: var(--color-orange-primary);
|
||||
background-color: var(--color-orange-primary);
|
||||
border-color: var(--color-orange-primary);
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4px 12px rgba(255, 133, 0, 0.3);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.page-item.active .page-link:hover {
|
||||
background: var(--color-orange-light);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(255, 133, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
@@ -2,7 +2,7 @@
|
||||
* Print Styles
|
||||
*
|
||||
* Optimized styling for printing
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Responsive Design Styles
|
||||
*
|
||||
* Media queries and responsive adjustments
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -246,31 +246,12 @@
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1140px;
|
||||
}
|
||||
|
||||
.container-lg {
|
||||
max-width: 1280px;
|
||||
}
|
||||
|
||||
.container-xl {
|
||||
max-width: 1400px;
|
||||
}
|
||||
}
|
||||
|
||||
/* XXL devices (1400px and up) */
|
||||
@media (min-width: 1400px) {
|
||||
.container {
|
||||
max-width: 1320px;
|
||||
}
|
||||
|
||||
.container-xl {
|
||||
max-width: 1500px;
|
||||
}
|
||||
|
||||
/* Container width uses CSS variable from Theme Settings */
|
||||
.container,
|
||||
.container-lg,
|
||||
.container-xl,
|
||||
.container-xxl {
|
||||
max-width: 1700px;
|
||||
max-width: var(--roi-container-width, 1320px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* IMPORTANTE: Bootstrap 5 ya provee la mayoría de utilities (display, flex, spacing, etc.)
|
||||
* Este archivo solo contiene utilities adicionales no incluidas en Bootstrap
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -88,3 +88,43 @@
|
||||
.transition-none {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
COMPONENT VISIBILITY FAILSAFE (Plan 99.15)
|
||||
|
||||
CSS failsafe: Oculta wrappers de componentes
|
||||
cuando body tiene clases roi-hide-*
|
||||
|
||||
Estas clases se agregan via BodyClassHooksRegistrar
|
||||
cuando los componentes están deshabilitados/excluidos.
|
||||
======================================== */
|
||||
|
||||
/* Navbar hidden */
|
||||
body.roi-hide-navbar .navbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Table of Contents hidden */
|
||||
body.roi-hide-toc .roi-toc-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* CTA Sidebar hidden */
|
||||
body.roi-hide-cta-sidebar .roi-cta-box {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Generic sidebar hidden */
|
||||
body.roi-hide-sidebar .sidebar-sticky {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* When ALL sidebar components are hidden, expand main column */
|
||||
body.roi-sidebar-empty .col-lg-9 {
|
||||
flex: 0 0 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
body.roi-sidebar-empty .col-lg-3 {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
* - Clases utilitarias (van en utilities.css o style.css)
|
||||
* - Estilos aplicados (SOLO variables en :root)
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Estilos para videos embebidos (YouTube, Vimeo, etc.) en post-content
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
border-spacing: 0;
|
||||
/* CRITICO: table-layout fixed previene CLS
|
||||
El navegador calcula anchos basado en primera fila,
|
||||
no recalcula cuando carga más contenido */
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
/* Eliminar todos los bordes */
|
||||
@@ -153,12 +157,13 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Columna 3: Unidad - centrada */
|
||||
/* Columna 3: Unidad - centrada
|
||||
Fase 4.4 Accesibilidad: Color #495057 (ratio 7.0:1) en lugar de #6c757d */
|
||||
.analisis table td:nth-child(3),
|
||||
.analisis table td.c3,
|
||||
.desglose table td.c3 {
|
||||
text-align: center !important;
|
||||
color: #6c757d;
|
||||
color: #495057;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
@@ -214,16 +219,17 @@
|
||||
/* ========================================
|
||||
FILAS DE SUBTOTALES
|
||||
(Suma de Material, Suma de Mano de Obra, etc)
|
||||
Fase 4.4 Accesibilidad: Color oscuro para contraste WCAG AA (4.5:1)
|
||||
======================================== */
|
||||
.analisis table tr.subtotal-row,
|
||||
.desglose table tr.subtotal-row {
|
||||
background-color: rgba(255, 133, 0, 0.1) !important;
|
||||
background-color: rgba(255, 133, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.analisis table tr.subtotal-row td,
|
||||
.desglose table tr.subtotal-row td {
|
||||
font-weight: 700;
|
||||
color: var(--color-orange-primary);
|
||||
color: #1e3a5f;
|
||||
padding: 0.875rem 1rem;
|
||||
border: none !important;
|
||||
}
|
||||
@@ -235,7 +241,7 @@
|
||||
.analisis table tr.subtotal-row td.c6,
|
||||
.analisis table tr.subtotal-row td:nth-child(6) {
|
||||
font-size: 1.05rem;
|
||||
color: var(--color-orange-primary);
|
||||
color: #1e3a5f;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* APUS Theme - Main Stylesheet
|
||||
* ROI Theme - Main Stylesheet
|
||||
*
|
||||
* RESPONSABILIDAD: Estilos principales del tema
|
||||
* - Variables CSS específicas del tema (:root en este archivo)
|
||||
@@ -12,7 +12,7 @@
|
||||
* - variables.css: SOLO variables de colores/espaciados/etc
|
||||
* - style.css: Aplica variables a elementos HTML (este archivo)
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
IMPORTANTE: Este archivo style.css es para estilos GLOBALES del tema únicamente.
|
||||
|
||||
El CSS de componentes individuales DEBE ir en archivos separados en:
|
||||
wp-content/themes/apus-theme/assets/css/[nombre-componente].css
|
||||
wp-content/themes/roi-theme/assets/css/[nombre-componente].css
|
||||
|
||||
Ejemplos de componentes con archivos individuales:
|
||||
- CTA Box Sidebar → cta-box-sidebar.css
|
||||
@@ -43,7 +43,7 @@
|
||||
========================================
|
||||
|
||||
El CSS de Share Buttons DEBE estar en:
|
||||
wp-content/themes/apus-theme/assets/css/social-share.css
|
||||
wp-content/themes/roi-theme/assets/css/social-share.css
|
||||
|
||||
Este archivo ya existe y está correctamente enqueued.
|
||||
Ver: inc/enqueue-scripts.php líneas 405-421
|
||||
@@ -55,7 +55,7 @@
|
||||
========================================
|
||||
|
||||
El CSS de CTA A/B Testing DEBE estar en:
|
||||
wp-content/themes/apus-theme/assets/css/cta.css
|
||||
wp-content/themes/roi-theme/assets/css/cta.css
|
||||
|
||||
Este archivo ya existe y está correctamente enqueued.
|
||||
Ver: inc/enqueue-scripts.php líneas 443-477
|
||||
@@ -67,7 +67,7 @@
|
||||
========================================
|
||||
|
||||
El CSS de Related Posts DEBE estar en:
|
||||
wp-content/themes/apus-theme/assets/css/related-posts.css
|
||||
wp-content/themes/roi-theme/assets/css/related-posts.css
|
||||
|
||||
Este archivo ya existe y está correctamente enqueued.
|
||||
Ver: inc/enqueue-scripts.php líneas 148-156
|
||||
@@ -79,7 +79,7 @@
|
||||
========================================
|
||||
|
||||
El CSS de Pagination DEBE estar en:
|
||||
wp-content/themes/apus-theme/assets/css/pagination.css
|
||||
wp-content/themes/roi-theme/assets/css/pagination.css
|
||||
|
||||
Este archivo ya existe y está correctamente enqueued.
|
||||
Ver: inc/enqueue-scripts.php líneas 129-136
|
||||
@@ -91,7 +91,7 @@
|
||||
========================================
|
||||
|
||||
El CSS de Footer Contact Form DEBE estar en:
|
||||
wp-content/themes/apus-theme/assets/css/footer-contact.css
|
||||
wp-content/themes/roi-theme/assets/css/footer-contact.css
|
||||
|
||||
Este archivo ya existe y está correctamente enqueued.
|
||||
Ver: inc/enqueue-scripts.php líneas 506-517
|
||||
@@ -146,7 +146,7 @@
|
||||
--color-text: #212529; /* Contrast ratio 15.52:1 against white */
|
||||
--color-bg: #ffffff;
|
||||
|
||||
/* APU Template Colors (from apus-theme-template/css/style.css) */
|
||||
/* APU Template Colors (from roi-theme-template/css/style.css) */
|
||||
--color-navy-dark: #0E2337;
|
||||
--color-navy-primary: #1e3a5f;
|
||||
--color-navy-light: #2c5282;
|
||||
@@ -341,6 +341,11 @@ img {
|
||||
.content-wrapper {
|
||||
grid-template-columns: 2fr 1fr;
|
||||
}
|
||||
|
||||
/* Full width when no sidebar */
|
||||
.no-sidebar .content-wrapper {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
#primary {
|
||||
1
Assets/Css/style.min.css
vendored
Normal file
1
Assets/Css/style.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
Assets/Fonts/poppins-v24-latin-500.woff2
Normal file
BIN
Assets/Fonts/poppins-v24-latin-500.woff2
Normal file
Binary file not shown.
BIN
Assets/Fonts/poppins-v24-latin-600.woff2
Normal file
BIN
Assets/Fonts/poppins-v24-latin-600.woff2
Normal file
Binary file not shown.
BIN
Assets/Fonts/poppins-v24-latin-700.woff2
Normal file
BIN
Assets/Fonts/poppins-v24-latin-700.woff2
Normal file
Binary file not shown.
BIN
Assets/Fonts/poppins-v24-latin-regular.woff2
Normal file
BIN
Assets/Fonts/poppins-v24-latin-regular.woff2
Normal file
Binary file not shown.
@@ -4,7 +4,7 @@
|
||||
* Mejoras de accesibilidad para navegación por teclado, gestión de focus,
|
||||
* y cumplimiento de WCAG 2.1 Level AA.
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Este script retrasa la carga de Google AdSense hasta que haya interacción
|
||||
* del usuario o se cumpla un timeout, mejorando el rendimiento de carga inicial.
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -182,12 +182,74 @@
|
||||
}, CONFIG.timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activa slots de AdSense insertados dinamicamente
|
||||
* Escucha el evento 'roi-adsense-activate' disparado por otros scripts
|
||||
*/
|
||||
function setupDynamicAdsListener() {
|
||||
window.addEventListener('roi-adsense-activate', function() {
|
||||
debugLog('Evento roi-adsense-activate recibido');
|
||||
|
||||
// Si AdSense aun no ha cargado, forzar carga ahora
|
||||
if (!adsenseLoaded) {
|
||||
debugLog('AdSense no cargado, forzando carga...');
|
||||
loadAdSense();
|
||||
return;
|
||||
}
|
||||
|
||||
// AdSense ya cargado - activar nuevos slots
|
||||
debugLog('Activando nuevos slots dinamicos...');
|
||||
activateDynamicSlots();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Activa slots de AdSense que fueron insertados despues de la carga inicial
|
||||
*/
|
||||
function activateDynamicSlots() {
|
||||
// Buscar scripts de push que aun no han sido ejecutados
|
||||
var pendingPushScripts = document.querySelectorAll('script[data-adsense-push][type="text/plain"]');
|
||||
|
||||
if (pendingPushScripts.length === 0) {
|
||||
debugLog('No hay slots pendientes por activar');
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('Activando ' + pendingPushScripts.length + ' slot(s) dinamico(s)');
|
||||
|
||||
// Asegurar que adsbygoogle existe
|
||||
window.adsbygoogle = window.adsbygoogle || [];
|
||||
|
||||
pendingPushScripts.forEach(function(oldScript) {
|
||||
try {
|
||||
// Crear nuevo script ejecutable
|
||||
var newScript = document.createElement('script');
|
||||
newScript.type = 'text/javascript';
|
||||
newScript.innerHTML = oldScript.innerHTML;
|
||||
|
||||
// Reemplazar el placeholder con el script real
|
||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||
} catch (e) {
|
||||
debugLog('Error activando slot: ' + e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa el cargador retrasado de AdSense
|
||||
*/
|
||||
function init() {
|
||||
// =========================================================================
|
||||
// NUEVO: Siempre configurar listener para ads dinamicos
|
||||
// IMPORTANTE: Esto debe ejecutarse ANTES del early return
|
||||
// porque los ads dinamicos pueden necesitar activarse aunque
|
||||
// el delay global este deshabilitado
|
||||
// =========================================================================
|
||||
setupDynamicAdsListener();
|
||||
debugLog('Listener para ads dinamicos configurado');
|
||||
|
||||
// Verificar si el retardo de AdSense está habilitado
|
||||
if (!window.apusAdsenseDelayed) {
|
||||
if (!window.roiAdsenseDelayed) {
|
||||
debugLog('Retardo de AdSense no habilitado');
|
||||
return;
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
* - Google Analytics 4 tracking
|
||||
* - Estados de loading y mensajes de feedback
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
// Configuración del webhook
|
||||
const WEBHOOK_URL = 'https://hook.us2.make.com/iq8p4q9w50a12crlb58d4h1o6lwu4f47';
|
||||
const FORM_SOURCE = 'APU Website - Footer Contact Form';
|
||||
const FORM_SOURCE = 'ROI Website - Footer Contact Form';
|
||||
|
||||
// Expresiones regulares para validación
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
@@ -8,7 +8,7 @@
|
||||
* - Accessibility features (keyboard navigation, ARIA attributes)
|
||||
* - Body scroll locking when mobile menu is open
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
135
Assets/Js/lazy-css-loader.js
Normal file
135
Assets/Js/lazy-css-loader.js
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* TIPO 5: Lazy CSS Loader
|
||||
*
|
||||
* Carga CSS no critico despues del evento load usando:
|
||||
* - requestIdleCallback para CSS de baja prioridad
|
||||
* - Event listeners para CSS condicional
|
||||
*
|
||||
* @package ROITheme
|
||||
* @since 1.0.20
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Configuracion de CSS lazy (inyectada desde PHP)
|
||||
var config = window.roiLazyCSSConfig || {
|
||||
baseUrl: '',
|
||||
version: '1.0.0',
|
||||
idleTimeout: 2000,
|
||||
cssFiles: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Carga un archivo CSS de forma asincrona
|
||||
*
|
||||
* @param {string} href URL del archivo CSS
|
||||
* @param {string} id ID del elemento link
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function loadCSS(href, id) {
|
||||
// Evitar duplicados
|
||||
if (document.getElementById(id)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
var link = document.createElement('link');
|
||||
link.id = id;
|
||||
link.rel = 'stylesheet';
|
||||
link.href = href;
|
||||
link.onload = resolve;
|
||||
link.onerror = reject;
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Carga CSS cuando el navegador esta idle
|
||||
*
|
||||
* @param {Array} files Lista de archivos a cargar
|
||||
*/
|
||||
function loadOnIdle(files) {
|
||||
var load = function() {
|
||||
files.forEach(function(file) {
|
||||
loadCSS(
|
||||
config.baseUrl + file.path + '?ver=' + config.version,
|
||||
'roi-lazy-' + file.id
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(load, { timeout: config.idleTimeout });
|
||||
} else {
|
||||
// Fallback para Safari
|
||||
setTimeout(load, config.idleTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Carga CSS de print solo cuando se va a imprimir
|
||||
*
|
||||
* @param {Object} file Archivo de print CSS
|
||||
*/
|
||||
function setupPrintCSS(file) {
|
||||
var loaded = false;
|
||||
|
||||
var load = function() {
|
||||
if (loaded) return;
|
||||
loaded = true;
|
||||
loadCSS(
|
||||
config.baseUrl + file.path + '?ver=' + config.version,
|
||||
'roi-lazy-print'
|
||||
);
|
||||
};
|
||||
|
||||
// Evento antes de imprimir
|
||||
window.addEventListener('beforeprint', load);
|
||||
|
||||
// Fallback: detectar Ctrl+P / Cmd+P
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'p') {
|
||||
load();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializacion
|
||||
*/
|
||||
function init() {
|
||||
var idleFiles = [];
|
||||
var printFile = null;
|
||||
|
||||
config.cssFiles.forEach(function(file) {
|
||||
switch (file.trigger) {
|
||||
case 'idle':
|
||||
idleFiles.push(file);
|
||||
break;
|
||||
case 'print':
|
||||
printFile = file;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Cargar CSS idle despues de que la pagina este lista
|
||||
if (idleFiles.length > 0) {
|
||||
if (document.readyState === 'complete') {
|
||||
loadOnIdle(idleFiles);
|
||||
} else {
|
||||
window.addEventListener('load', function() {
|
||||
loadOnIdle(idleFiles);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Configurar CSS de print
|
||||
if (printFile) {
|
||||
setupPrintCSS(printFile);
|
||||
}
|
||||
}
|
||||
|
||||
// Iniciar
|
||||
init();
|
||||
|
||||
})();
|
||||
27
Assets/Js/main.js
Normal file
27
Assets/Js/main.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* ROI THEME - MAIN JAVASCRIPT
|
||||
*
|
||||
* OPTIMIZACIÓN TBT Fase 2.3 (2025-11-27):
|
||||
* - Eliminado ~300 líneas de código muerto
|
||||
* - Removido: loadContactModal (modalContainer no existe)
|
||||
* - Removido: initContactForm (contactForm no existe)
|
||||
* - Removido: footerContactForm handler (ID incorrecto)
|
||||
* - Removido: TOC ScrollSpy duplicado (.toc-container no existe)
|
||||
* - Removido: smooth scroll duplicado (Bootstrap lo maneja)
|
||||
* - Removido: console.log de debug
|
||||
*
|
||||
* Código activo: Solo efecto scroll del navbar
|
||||
* Reducción: ~315 líneas → ~25 líneas
|
||||
*/
|
||||
|
||||
// Navbar scroll effect - adds 'scrolled' class when user scrolls
|
||||
window.addEventListener('scroll', function() {
|
||||
const navbar = document.querySelector('.navbar');
|
||||
if (navbar) {
|
||||
if (window.scrollY > 50) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -4,7 +4,7 @@
|
||||
* Carga dinámica del modal, validaciones y envío a webhook
|
||||
* Compatible con Bootstrap 5.3.2
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -69,9 +69,9 @@
|
||||
}
|
||||
|
||||
// Obtener la URL del tema desde WordPress
|
||||
const themeUrl = typeof apusTheme !== 'undefined' && apusTheme.themeUrl
|
||||
? apusTheme.themeUrl
|
||||
: '/wp-content/themes/apus-theme';
|
||||
const themeUrl = typeof roiheme !== 'undefined' && rroieme.themeUrl
|
||||
? roiheme.themeUrl
|
||||
: '/wp-content/themes/roitheme';
|
||||
|
||||
// Cargar el HTML del modal
|
||||
fetch(themeUrl + '/modal-contact.html')
|
||||
@@ -5,7 +5,7 @@
|
||||
* Maneja el cierre de la barra de notificación y el almacenamiento
|
||||
* de la preferencia del usuario mediante cookies.
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
}
|
||||
|
||||
// Guardar cookie por 7 días
|
||||
setCookie('apus_notification_dismissed', '1', 7);
|
||||
setCookie('roinotification_dismissed', '1', 7);
|
||||
|
||||
// Disparar evento personalizado para otros scripts
|
||||
const event = new CustomEvent('notificationBarClosed', {
|
||||
4085
Assets/Vendor/Bootstrap/Css/bootstrap-grid.css
vendored
Normal file
4085
Assets/Vendor/Bootstrap/Css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Assets/Vendor/Bootstrap/Css/bootstrap-grid.css.map
vendored
Normal file
1
Assets/Vendor/Bootstrap/Css/bootstrap-grid.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user