Compare commits
171 Commits
migration/
...
f4b45b7e17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -40,9 +40,6 @@ Desktop.ini
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
|
||||
# Composer (si hay dependencias PHP)
|
||||
vendor/
|
||||
composer.lock
|
||||
|
||||
# PHPUnit
|
||||
.phpunit.result.cache
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<?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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,924 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\AdsensePlacement\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
/**
|
||||
* 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>';
|
||||
|
||||
$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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,19 @@ final class ContactFormFieldMapper implements FieldMapperInterface
|
||||
'contactFormEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'contactFormShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'contactFormShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'contactFormShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// 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'],
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
@@ -93,18 +94,46 @@ final class ContactFormFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('contactFormShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="contactFormShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="contactFormShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// 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>';
|
||||
@@ -598,4 +627,26 @@ final class ContactFormFormBuilder
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,19 @@ final class CtaBoxSidebarFieldMapper implements FieldMapperInterface
|
||||
'ctaEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// 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'],
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
@@ -94,19 +95,48 @@ final class CtaBoxSidebarFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
||||
$html .= $this->buildSwitch('ctaShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// show_on_pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="ctaShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="ctaShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// 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');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
@@ -515,4 +545,29 @@ final class CtaBoxSidebarFormBuilder
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,19 @@ final class CtaLetsTalkFieldMapper implements FieldMapperInterface
|
||||
'ctaLetsTalkEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaLetsTalkShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaLetsTalkShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaLetsTalkShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// 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'],
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
@@ -120,17 +121,46 @@ final class CtaLetsTalkFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Show on Pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkShowOnPages" class="form-label small mb-1 fw-semibold">Mostrar en</label>';
|
||||
$html .= ' <select id="ctaLetsTalkShowOnPages" name="visibility[show_on_pages]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// 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');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
@@ -447,4 +477,26 @@ final class CtaLetsTalkFormBuilder
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,19 @@ final class CtaPostFieldMapper implements FieldMapperInterface
|
||||
'ctaPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaPostShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// 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'],
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
@@ -85,18 +86,46 @@ final class CtaPostFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('ctaPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="ctaPostShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="ctaPostShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// 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');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
@@ -437,4 +466,26 @@ final class CtaPostFormBuilder
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,19 @@ final class FeaturedImageFieldMapper implements FieldMapperInterface
|
||||
'featuredImageEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'featuredImageShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'featuredImageShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'featuredImageShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// 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'],
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
{
|
||||
@@ -100,18 +101,46 @@ final class FeaturedImageFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="featuredImageShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="featuredImageShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las paginas</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// 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>';
|
||||
@@ -119,6 +148,28 @@ final class FeaturedImageFormBuilder
|
||||
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;">';
|
||||
|
||||
@@ -27,6 +27,19 @@ final class FooterFieldMapper implements FieldMapperInterface
|
||||
'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'],
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
@@ -90,6 +91,47 @@ final class FooterFormBuilder
|
||||
$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>';
|
||||
|
||||
@@ -410,4 +452,19 @@ final class FooterFormBuilder
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,20 @@ final class HeroFieldMapper implements FieldMapperInterface
|
||||
'heroEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'heroShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'heroShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'heroShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
'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'],
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
{
|
||||
@@ -102,18 +103,59 @@ final class HeroFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
// =============================================
|
||||
// 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 .= ' <label for="heroShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$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 .= ' <select id="heroShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
@@ -413,4 +455,26 @@ final class HeroFormBuilder
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Infrastructure\Api\Wordpress;
|
||||
namespace ROITheme\Admin\Infrastructure\Api\WordPress;
|
||||
|
||||
use ROITheme\Admin\Domain\Contracts\MenuRegistrarInterface;
|
||||
use ROITheme\Admin\Domain\ValueObjects\MenuItem;
|
||||
@@ -107,6 +107,15 @@ final class AdminAssetEnqueuer
|
||||
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',
|
||||
|
||||
@@ -18,10 +18,12 @@ final class AdminDashboardRenderer implements DashboardRendererInterface
|
||||
|
||||
/**
|
||||
* @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 = []
|
||||
) {
|
||||
}
|
||||
@@ -111,6 +113,16 @@ final class AdminDashboardRenderer implements DashboardRendererInterface
|
||||
'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',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -153,13 +165,51 @@ final class AdminDashboardRenderer implements DashboardRendererInterface
|
||||
*/
|
||||
public function getFormBuilderClass(string $componentId): string
|
||||
{
|
||||
// Convertir kebab-case a PascalCase
|
||||
// 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +1,53 @@
|
||||
/**
|
||||
* Estilos para el Dashboard del Panel de Administración ROI Theme
|
||||
* Siguiendo especificaciones del Design System
|
||||
* 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 - resetear completamente estilos de WordPress */
|
||||
/* Fix para switches de Bootstrap */
|
||||
.wrap.roi-admin-panel .form-switch .form-check-input {
|
||||
all: unset !important;
|
||||
/* Restaurar estilos necesarios de Bootstrap */
|
||||
width: 2em !important;
|
||||
height: 1em !important;
|
||||
margin-left: -2.5em !important;
|
||||
@@ -49,7 +85,6 @@
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25) !important;
|
||||
}
|
||||
|
||||
/* Alinear verticalmente los labels con los switches */
|
||||
.wrap.roi-admin-panel .form-check {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
@@ -62,7 +97,10 @@
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
/* Tabs Navigation */
|
||||
/* ================================================
|
||||
TABS NAVIGATION (Legacy)
|
||||
================================================ */
|
||||
|
||||
.nav-tabs-admin {
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
}
|
||||
@@ -77,7 +115,7 @@
|
||||
border-bottom: 3px solid transparent;
|
||||
padding: 0.3rem 0.3rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.83rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@@ -113,8 +151,394 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
/* ================================================
|
||||
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;
|
||||
}
|
||||
@@ -125,7 +549,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
@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;
|
||||
@@ -135,3 +592,39 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,73 @@
|
||||
* 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
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
/**
|
||||
* ROI Theme - Panel de Administración Principal
|
||||
*
|
||||
* Nueva UI con sistema de Cards/Grupos (App-Style Navigation)
|
||||
*
|
||||
* @var AdminDashboardRenderer $this
|
||||
*/
|
||||
|
||||
@@ -13,76 +15,34 @@ if (!defined('ABSPATH')) {
|
||||
}
|
||||
|
||||
$components = $this->getComponents();
|
||||
$groups = $this->getComponentGroups();
|
||||
|
||||
// Determinar tab activo: desde URL o primer componente
|
||||
$activeComponentId = array_key_first($components);
|
||||
|
||||
// Leer parametro admin-tab de la URL con sanitizacion
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Solo lectura de parametro para UI
|
||||
if (isset($_GET['admin-tab'])) {
|
||||
$requestedTab = sanitize_text_field(wp_unslash($_GET['admin-tab']));
|
||||
// =====================================================
|
||||
// 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($requestedTab, $components)) {
|
||||
$activeComponentId = $requestedTab;
|
||||
if (array_key_exists($requestedComponent, $components)) {
|
||||
$activeComponent = $requestedComponent;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="wrap roi-admin-panel">
|
||||
<!-- Navigation Tabs -->
|
||||
<ul class="nav nav-tabs nav-tabs-admin mb-0" role="tablist">
|
||||
<?php foreach ($components as $componentId => $component): ?>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link <?php echo $componentId === $activeComponentId ? 'active' : ''; ?>"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#<?php echo esc_attr($componentId); ?>Tab"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="<?php echo esc_attr($componentId); ?>Tab"
|
||||
aria-selected="<?php echo $componentId === $activeComponentId ? 'true' : 'false'; ?>">
|
||||
<i class="bi <?php echo esc_attr($component['icon']); ?> me-1"></i>
|
||||
<?php echo esc_html($component['label']); ?>
|
||||
</button>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content mt-3">
|
||||
<?php foreach ($components as $componentId => $component):
|
||||
$isActive = ($componentId === $activeComponentId);
|
||||
$componentSettings = $this->getComponentSettings($componentId);
|
||||
?>
|
||||
<!-- Tab: <?php echo esc_html($component['label']); ?> -->
|
||||
<div class="tab-pane fade <?php echo $isActive ? 'show active' : ''; ?>"
|
||||
id="<?php echo esc_attr($componentId); ?>Tab"
|
||||
role="tabpanel">
|
||||
|
||||
<?php
|
||||
// Renderizar FormBuilder del componente
|
||||
$formBuilderClass = $this->getFormBuilderClass($componentId);
|
||||
if (class_exists($formBuilderClass)) {
|
||||
$formBuilder = new $formBuilderClass($this);
|
||||
echo $formBuilder->buildForm($componentId);
|
||||
} else {
|
||||
echo '<p class="text-danger">FormBuilder no encontrado: ' . esc_html($formBuilderClass) . '</p>';
|
||||
}
|
||||
?>
|
||||
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</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>
|
||||
Cancelar
|
||||
</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>
|
||||
Guardar Cambios
|
||||
</button>
|
||||
</div>
|
||||
<?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>
|
||||
@@ -26,8 +26,21 @@ final class NavbarFieldMapper implements FieldMapperInterface
|
||||
'navbarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'navbarShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'navbarShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'navbarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
'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'],
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
{
|
||||
@@ -105,21 +106,50 @@ final class NavbarFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Show on Pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarShowOnPages" class="form-label small mb-1 fw-semibold">Mostrar en</label>';
|
||||
$html .= ' <select id="navbarShowOnPages" name="visibility[show_on_pages]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// 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-0">';
|
||||
$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) . '>';
|
||||
@@ -129,6 +159,19 @@ final class NavbarFormBuilder
|
||||
$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>';
|
||||
|
||||
@@ -514,4 +557,26 @@ final class NavbarFormBuilder
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,19 @@ final class RelatedPostFieldMapper implements FieldMapperInterface
|
||||
'relatedPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'relatedPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'relatedPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'relatedPostShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// 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'],
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
@@ -86,18 +87,46 @@ final class RelatedPostFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('relatedPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="relatedPostShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="relatedPostShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// 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>';
|
||||
@@ -498,4 +527,26 @@ final class RelatedPostFormBuilder
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\Api\Wordpress;
|
||||
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
|
||||
@@ -73,10 +74,16 @@ final class AdminAjaxHandler
|
||||
|
||||
/**
|
||||
* 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])) {
|
||||
@@ -86,11 +93,17 @@ final class AdminAjaxHandler
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -130,7 +143,7 @@ final class AdminAjaxHandler
|
||||
// Usar repositorio para restaurar valores
|
||||
if ($this->saveComponentSettingsUseCase !== null) {
|
||||
global $wpdb;
|
||||
$repository = new \ROITheme\Shared\Infrastructure\Persistence\Wordpress\WordPressComponentSettingsRepository($wpdb);
|
||||
$repository = new \ROITheme\Shared\Infrastructure\Persistence\WordPress\WordPressComponentSettingsRepository($wpdb);
|
||||
$updated = $repository->resetToDefaults($component, $schemaPath);
|
||||
|
||||
wp_send_json_success([
|
||||
@@ -32,6 +32,7 @@ final class FieldMapperProvider
|
||||
'ContactForm',
|
||||
'Footer',
|
||||
'ThemeSettings',
|
||||
'AdsensePlacement',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,19 @@ final class SocialShareFieldMapper implements FieldMapperInterface
|
||||
'socialShareEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'socialShareShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'socialShareShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'socialShareShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// 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'],
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
@@ -94,19 +95,46 @@ final class SocialShareFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('socialShareShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// show_on_pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="socialShareShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="socialShareShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// 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>';
|
||||
@@ -526,4 +554,26 @@ final class SocialShareFormBuilder
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,19 @@ final class TableOfContentsFieldMapper implements FieldMapperInterface
|
||||
'tocEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'tocShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'tocShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'tocShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// 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'],
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
@@ -94,19 +95,46 @@ final class TableOfContentsFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
||||
$html .= $this->buildSwitch('tocShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// show_on_pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="tocShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="tocShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las paginas</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// 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>';
|
||||
@@ -585,4 +613,26 @@ final class TableOfContentsFormBuilder
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,9 @@ final class ThemeSettingsFieldMapper implements FieldMapperInterface
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Analytics
|
||||
'themeSettingsGaTrackingId' => ['group' => 'analytics', 'attribute' => 'ga_tracking_id'],
|
||||
'themeSettingsGaAnonymizeIp' => ['group' => 'analytics', 'attribute' => 'ga_anonymize_ip'],
|
||||
|
||||
// AdSense
|
||||
'themeSettingsAdsensePublisherId' => ['group' => 'adsense', 'attribute' => 'adsense_publisher_id'],
|
||||
'themeSettingsAdsenseAutoAds' => ['group' => 'adsense', 'attribute' => 'adsense_auto_ads'],
|
||||
// Layout
|
||||
'themeSettingsContainerMaxWidth' => ['group' => 'layout', 'attribute' => 'container_max_width'],
|
||||
'themeSettingsContentColumnWidth' => ['group' => 'layout', 'attribute' => 'content_column_width'],
|
||||
|
||||
// Custom Code
|
||||
'themeSettingsCustomCss' => ['group' => 'custom_code', 'attribute' => 'custom_css'],
|
||||
|
||||
@@ -9,9 +9,10 @@ use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
* FormBuilder para Theme Settings
|
||||
*
|
||||
* RESPONSABILIDAD: Generar formulario de configuraciones globales del tema
|
||||
* (analytics, adsense, codigo personalizado)
|
||||
* (JavaScript personalizado)
|
||||
*
|
||||
* NOTA: Logo/branding se gestiona desde el componente navbar
|
||||
* 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
|
||||
*/
|
||||
@@ -27,20 +28,92 @@ final class ThemeSettingsFormBuilder
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
// Layout Group
|
||||
$html .= $this->buildLayoutGroup($componentId);
|
||||
|
||||
// Columna izquierda - Analytics + AdSense
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildAnalyticsGroup($componentId);
|
||||
$html .= $this->buildAdSenseGroup($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>';
|
||||
|
||||
// Columna derecha - Custom Code
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildCustomCodeGroup($componentId);
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
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;
|
||||
}
|
||||
@@ -56,7 +129,7 @@ final class ThemeSettingsFormBuilder
|
||||
$html .= ' Configuraciones Globales del Tema';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Analytics, AdSense y Codigo Personalizado';
|
||||
$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">';
|
||||
@@ -69,83 +142,36 @@ final class ThemeSettingsFormBuilder
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildAnalyticsGroup(string $componentId): string
|
||||
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-graph-up me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Analytics';
|
||||
$html .= ' <i class="bi bi-filetype-js me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' JavaScript Personalizado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$gaTrackingId = $this->renderer->getFieldValue($componentId, 'analytics', 'ga_tracking_id', '');
|
||||
$html .= $this->buildTextInput('themeSettingsGaTrackingId', 'Google Analytics ID', 'bi-bar-chart', $gaTrackingId);
|
||||
|
||||
$html .= ' <div class="form-text small mb-2">Formato: G-XXXXXXXXXX o UA-XXXXXXXX-X</div>';
|
||||
|
||||
$gaAnonymizeIp = $this->renderer->getFieldValue($componentId, 'analytics', 'ga_anonymize_ip', true);
|
||||
$html .= $this->buildSwitch('themeSettingsGaAnonymizeIp', 'Anonimizar IP (GDPR)', 'bi-shield-check', $gaAnonymizeIp);
|
||||
|
||||
$html .= ' <div class="alert alert-warning small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-exclamation-triangle me-1"></i>';
|
||||
$html .= ' Recomendado activar para cumplir con GDPR/RGPD';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildAdSenseGroup(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-badge-ad me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Google AdSense';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$publisherId = $this->renderer->getFieldValue($componentId, 'adsense', 'adsense_publisher_id', '');
|
||||
$html .= $this->buildTextInput('themeSettingsAdsensePublisherId', 'Publisher ID', 'bi-key', $publisherId);
|
||||
|
||||
$html .= ' <div class="form-text small mb-2">Formato: ca-pub-1234567890123456</div>';
|
||||
|
||||
$autoAds = $this->renderer->getFieldValue($componentId, 'adsense', 'adsense_auto_ads', false);
|
||||
$html .= $this->buildSwitch('themeSettingsAdsenseAutoAds', 'Activar Auto Ads', 'bi-magic', $autoAds);
|
||||
|
||||
$html .= ' <div class="alert alert-info small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' Auto Ads permite que Google coloque anuncios automaticamente en las mejores ubicaciones.';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildCustomCodeGroup(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-code-slash me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Codigo Personalizado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$customCss = $this->renderer->getFieldValue($componentId, 'custom_code', 'custom_css', '');
|
||||
$html .= $this->buildTextareaCode('themeSettingsCustomCss', 'CSS Personalizado', 'bi-filetype-css', $customCss, 'Se inyecta en wp_head. No incluir etiquetas <style>');
|
||||
|
||||
$customJsHeader = $this->renderer->getFieldValue($componentId, 'custom_code', 'custom_js_header', '');
|
||||
$html .= $this->buildTextareaCode('themeSettingsCustomJsHeader', 'JavaScript en Header', 'bi-filetype-js', $customJsHeader, 'Se inyecta en wp_head. No incluir etiquetas <script>');
|
||||
$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', 'bi-filetype-js', $customJsFooter, 'Se inyecta en wp_footer. No incluir etiquetas <script>');
|
||||
$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 personalizado puede afectar el rendimiento y seguridad del sitio.';
|
||||
$html .= ' <strong>Advertencia:</strong> El codigo JS puede afectar el rendimiento y seguridad del sitio.';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
@@ -154,47 +180,15 @@ final class ThemeSettingsFormBuilder
|
||||
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
|
||||
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 .= ' <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 buildTextareaCode(string $id, string $label, string $icon, mixed $value, string $helpText = ''): 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 font-monospace" id="' . esc_attr($id) . '" rows="4" style="font-size: 0.85em;">' . esc_textarea($value) . '</textarea>';
|
||||
$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>';
|
||||
}
|
||||
|
||||
@@ -26,7 +26,20 @@ final class TopNotificationBarFieldMapper implements FieldMapperInterface
|
||||
'topBarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'topBarShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'topBarShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'topBarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
'topBarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
|
||||
|
||||
// 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'],
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
{
|
||||
@@ -105,19 +106,59 @@ final class TopNotificationBarFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Show on Pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
||||
// =============================================
|
||||
// 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-0 mt-3">';
|
||||
$html .= ' <label for="topBarShowOnPages" class="form-label small mb-1 fw-semibold" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$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 .= ' <select id="topBarShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
@@ -305,4 +346,26 @@ final class TopNotificationBarFormBuilder
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
|
||||
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
@@ -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',
|
||||
@@ -56,11 +57,30 @@
|
||||
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-v24-latin-regular.woff2') format('woff2');
|
||||
src: url('../Fonts/poppins-v24-latin-regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -68,7 +88,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../fonts/poppins-v24-latin-500.woff2') format('woff2');
|
||||
src: url('../Fonts/poppins-v24-latin-500.woff2') format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -76,7 +96,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../fonts/poppins-v24-latin-600.woff2') format('woff2');
|
||||
src: url('../Fonts/poppins-v24-latin-600.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -84,7 +104,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../fonts/poppins-v24-latin-700.woff2') format('woff2');
|
||||
src: url('../Fonts/poppins-v24-latin-700.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
@@ -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
@@ -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.roidsenseDelayed) {
|
||||
if (!window.roiAdsenseDelayed) {
|
||||
debugLog('Retardo de AdSense no habilitado');
|
||||
return;
|
||||
}
|
||||
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');
|
||||
}
|
||||
}
|
||||
});
|
||||
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
6
Assets/Vendor/Bootstrap/Css/bootstrap-grid.min.css
vendored
Normal file
6
Assets/Vendor/Bootstrap/Css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Assets/Vendor/Bootstrap/Css/bootstrap-grid.min.css.map
vendored
Normal file
1
Assets/Vendor/Bootstrap/Css/bootstrap-grid.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4084
Assets/Vendor/Bootstrap/Css/bootstrap-grid.rtl.css
vendored
Normal file
4084
Assets/Vendor/Bootstrap/Css/bootstrap-grid.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Assets/Vendor/Bootstrap/Css/bootstrap-grid.rtl.css.map
vendored
Normal file
1
Assets/Vendor/Bootstrap/Css/bootstrap-grid.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
Assets/Vendor/Bootstrap/Css/bootstrap-grid.rtl.min.css
vendored
Normal file
6
Assets/Vendor/Bootstrap/Css/bootstrap-grid.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Assets/Vendor/Bootstrap/Css/bootstrap-grid.rtl.min.css.map
vendored
Normal file
1
Assets/Vendor/Bootstrap/Css/bootstrap-grid.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
597
Assets/Vendor/Bootstrap/Css/bootstrap-reboot.css
vendored
Normal file
597
Assets/Vendor/Bootstrap/Css/bootstrap-reboot.css
vendored
Normal file
@@ -0,0 +1,597 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2023 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
1
Assets/Vendor/Bootstrap/Css/bootstrap-reboot.css.map
vendored
Normal file
1
Assets/Vendor/Bootstrap/Css/bootstrap-reboot.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
Assets/Vendor/Bootstrap/Css/bootstrap-reboot.min.css
vendored
Normal file
6
Assets/Vendor/Bootstrap/Css/bootstrap-reboot.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Assets/Vendor/Bootstrap/Css/bootstrap-reboot.min.css.map
vendored
Normal file
1
Assets/Vendor/Bootstrap/Css/bootstrap-reboot.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
594
Assets/Vendor/Bootstrap/Css/bootstrap-reboot.rtl.css
vendored
Normal file
594
Assets/Vendor/Bootstrap/Css/bootstrap-reboot.rtl.css
vendored
Normal file
@@ -0,0 +1,594 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2023 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user