Compare commits
234 Commits
backup-est
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85f3387fd2 | ||
|
|
ff5ba25505 | ||
|
|
eab974d14c | ||
|
|
b509b1a2b4 | ||
|
|
83d113d669 | ||
|
|
0c1908e7d1 | ||
|
|
5333531be4 | ||
|
|
fb68f2023c | ||
|
|
79e91f59ee | ||
|
|
c23dc22d76 | ||
|
|
b79569c5e7 | ||
|
|
6be292e085 | ||
|
|
885276aad1 | ||
|
|
1e6a076904 | ||
|
|
a33c43a104 | ||
|
|
78d2ba57b9 | ||
|
|
1c0750604b | ||
|
|
bf304f08fc | ||
|
|
30b30b065b | ||
|
|
b2d5cdfb57 | ||
|
|
b40e5b671a | ||
|
|
61c67acca5 | ||
|
|
ffe6ea8e65 | ||
|
|
36d5cf56de | ||
|
|
23339e3349 | ||
|
|
caa6413bc6 | ||
|
|
ea695010f3 | ||
|
|
e4c79d3f26 | ||
|
|
f4b45b7e17 | ||
|
|
c28fedd6e7 | ||
|
|
14138e7762 | ||
|
|
8735962f52 | ||
|
|
7fb5eda108 | ||
|
|
4cdc4db397 | ||
|
|
c732b5af05 | ||
|
|
29a69617e4 | ||
|
|
9e37ea93eb | ||
|
|
7472dbad11 | ||
|
|
ce66eeba6d | ||
|
|
565c275c16 | ||
|
|
faf5fc6db2 | ||
|
|
de66b77fe3 | ||
|
|
73e5ac4acd | ||
|
|
78ec902688 | ||
|
|
d8fa5cb609 | ||
|
|
e01605ec37 | ||
|
|
e1923b630d | ||
|
|
625d99d698 | ||
|
|
9f0ae9fcb6 | ||
|
|
647f177a35 | ||
|
|
49eff2223c | ||
|
|
c302c653c3 | ||
|
|
9cb0dd1491 | ||
|
|
423aae062c | ||
|
|
972c3c5de9 | ||
|
|
cc4de0eda7 | ||
|
|
80fc41afad | ||
|
|
0b34317cc6 | ||
|
|
0ea874876e | ||
|
|
fb74ccbdc2 | ||
|
|
9f5cc92ec6 | ||
|
|
c6450211a7 | ||
|
|
3c8e5982ba | ||
|
|
7667b7f02a | ||
|
|
c4dcdad14b | ||
|
|
d648e7ff4c | ||
|
|
842f529816 | ||
|
|
3b9a1cb299 | ||
|
|
c0172467b3 | ||
|
|
ee28baafd8 | ||
|
|
d145d4dfde | ||
|
|
8710895db5 | ||
|
|
163b8c6c2a | ||
|
|
0239191dfc | ||
|
|
3bf40787ad | ||
|
|
bc85854453 | ||
|
|
4e99fa5310 | ||
|
|
13e17a7b12 | ||
|
|
c7e8f14d83 | ||
|
|
0fba2d567c | ||
|
|
31d4a41fc9 | ||
|
|
9afdd6ee1d | ||
|
|
b4071bf598 | ||
|
|
62a0f17b21 | ||
|
|
5d4523e49a | ||
|
|
19b6c38fbf | ||
|
|
8a9c62e17e | ||
|
|
b7ae8cac21 | ||
|
|
371af1f7e5 | ||
|
|
a01ebf303e | ||
|
|
8361e14862 | ||
|
|
77a59d0db8 | ||
|
|
6004420620 | ||
|
|
d5a2fd2702 | ||
|
|
ce0179a134 | ||
|
|
38d7099bcd | ||
|
|
4f25297f14 | ||
|
|
6d03076032 | ||
|
|
f5089724c6 | ||
|
|
956819cf14 | ||
|
|
46ad8340c3 | ||
|
|
4294a7c07b | ||
|
|
8aba07fdbf | ||
|
|
13beaf7b06 | ||
|
|
1f0ce58b22 | ||
|
|
7edddada89 | ||
|
|
b96a13427e | ||
|
|
4d5cc1a58c | ||
|
|
e3d17db5ea | ||
|
|
a281448bf8 | ||
|
|
8c3fea964d | ||
|
|
cec8b8dccd | ||
|
|
e8ead33311 | ||
|
|
9e8ffdb26f | ||
|
|
ec64ea38ea | ||
|
|
e7fc0f1408 | ||
|
|
f4e3a61df8 | ||
|
|
961f663107 | ||
|
|
21ac98c969 | ||
|
|
de4f808a1a | ||
|
|
c9c6a5ac7b | ||
|
|
6b6ebd3c6d | ||
|
|
070ee7398c | ||
|
|
ce19345f78 | ||
|
|
1b9910165b | ||
|
|
6e2ef67dc4 | ||
|
|
72ef7580fc | ||
|
|
122bcd4750 | ||
|
|
0dfe3fcd2c | ||
|
|
2fa112ab7f | ||
|
|
55f061df67 | ||
|
|
1a069a1336 | ||
|
|
cfcc38c0f7 | ||
|
|
c564ee7a2a | ||
|
|
4119f2e86d | ||
|
|
e52df682ae | ||
|
|
58a4cc2c56 | ||
|
|
b9b21c390a | ||
|
|
22e9273b4f | ||
|
|
79b48ad94f | ||
|
|
82abdf047a | ||
|
|
b70e11be62 | ||
|
|
3279b7df2b | ||
|
|
a3fa5fe22e | ||
|
|
84441af9c0 | ||
|
|
651e8124d4 | ||
|
|
a10831e2c2 | ||
|
|
d7c42f26ef | ||
|
|
4dbf73f226 | ||
|
|
f4bd013271 | ||
|
|
371995d151 | ||
|
|
1c901ecdf9 | ||
|
|
281c05fa33 | ||
|
|
0a303be198 | ||
|
|
6edb2ebeaa | ||
|
|
4ad48b4326 | ||
|
|
23a3c4d074 | ||
|
|
83717771c0 | ||
|
|
d7915d372b | ||
|
|
2acce34d9e | ||
|
|
99cde7c3d6 | ||
|
|
50a8c2bf18 | ||
|
|
096f9716ef | ||
|
|
2f19a7c077 | ||
|
|
cd09666f1d | ||
|
|
b43cb22dc1 | ||
|
|
deef577c36 | ||
|
|
d5bdb81cbe | ||
|
|
56a7c29653 | ||
|
|
acdfeffd75 | ||
|
|
eeacfdb284 | ||
|
|
d867212790 | ||
|
|
98c90756f8 | ||
|
|
52e2698279 | ||
|
|
83b594a750 | ||
|
|
4ac03bd3e2 | ||
|
|
ec8f1f0589 | ||
|
|
133b364c78 | ||
|
|
b0def25348 | ||
|
|
7e13678e0b | ||
|
|
7a539a498f | ||
|
|
90ac8a16cc | ||
|
|
8f4e854a20 | ||
|
|
a062529e82 | ||
|
|
7a34d1f2ae | ||
|
|
a46126e015 | ||
|
|
1876231ac1 | ||
|
|
c6e156089d | ||
|
|
32d76c4ce8 | ||
|
|
2831cabec9 | ||
|
|
4cbde7e1b7 | ||
|
|
14e68031ac | ||
|
|
1a03205aba | ||
|
|
620ca115fb | ||
|
|
af16230cf9 | ||
|
|
0f947f6677 | ||
|
|
33d17f4b56 | ||
|
|
90863cd8f5 | ||
|
|
a2548ab5c2 | ||
|
|
d6070099d1 | ||
|
|
8a49b19d00 | ||
|
|
9d14f38965 | ||
|
|
f35b60ed4e | ||
|
|
7cc5f194e9 | ||
|
|
6dc052afa6 | ||
|
|
8878afe168 | ||
|
|
7a8daa72c6 | ||
|
|
f52a395e0d | ||
|
|
6e75527157 | ||
|
|
4f11c2c312 | ||
|
|
1a4d9d8c08 | ||
|
|
71cfd54166 | ||
|
|
4c807e1cf2 | ||
|
|
0846a3bf03 | ||
|
|
90de6df77c | ||
|
|
677fbd4368 | ||
|
|
42edfab50d | ||
|
|
e34fd28df7 | ||
|
|
de5fff4f5c | ||
|
|
b782ebceee | ||
|
|
a6578f4973 | ||
|
|
77dd809e8c | ||
|
|
60b3992ca5 | ||
|
|
49b923230f | ||
|
|
9e29410c0d | ||
|
|
f0989f4fb0 | ||
|
|
3947e36c98 | ||
|
|
03c97d31d3 | ||
|
|
e94b274ed0 | ||
|
|
883853bc5c | ||
|
|
1c6b184e94 | ||
|
|
8a99f184bf | ||
|
|
3ad2413e7a | ||
|
|
4818d90386 |
17
.claude/commands/openspec/apply.md
Normal file
17
.claude/commands/openspec/apply.md
Normal file
@@ -0,0 +1,17 @@
|
||||
Implement the OpenSpec change: $ARGUMENTS
|
||||
|
||||
**Guardrails**
|
||||
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||
- Keep changes tightly scoped to the requested outcome.
|
||||
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||
|
||||
**Steps**
|
||||
Track these steps as TODOs and complete them one by one.
|
||||
1. Read `changes/<id>/proposal.md`, `design.md` (if present), and `tasks.md` to confirm scope and acceptance criteria.
|
||||
2. Work through tasks sequentially, keeping edits minimal and focused on the requested change.
|
||||
3. Confirm completion before updating statuses—make sure every item in `tasks.md` is finished.
|
||||
4. Update the checklist after all work is done so each task is marked `- [x]` and reflects reality.
|
||||
5. Reference `openspec list` or `openspec show <item>` when additional context is required.
|
||||
|
||||
**Reference**
|
||||
- Use `openspec show <id> --json --deltas-only` if you need additional context from the proposal while implementing.
|
||||
21
.claude/commands/openspec/archive.md
Normal file
21
.claude/commands/openspec/archive.md
Normal file
@@ -0,0 +1,21 @@
|
||||
Archive the completed OpenSpec change: $ARGUMENTS
|
||||
|
||||
**Guardrails**
|
||||
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||
- Keep changes tightly scoped to the requested outcome.
|
||||
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||
|
||||
**Steps**
|
||||
1. Determine the change ID to archive:
|
||||
- If this prompt already includes a specific change ID (for example inside a `<ChangeId>` block populated by slash-command arguments), use that value after trimming whitespace.
|
||||
- If the conversation references a change loosely (for example by title or summary), run `openspec list` to surface likely IDs, share the relevant candidates, and confirm which one the user intends.
|
||||
- Otherwise, review the conversation, run `openspec list`, and ask the user which change to archive; wait for a confirmed change ID before proceeding.
|
||||
- If you still cannot identify a single change ID, stop and tell the user you cannot archive anything yet.
|
||||
2. Validate the change ID by running `openspec list` (or `openspec show <id>`) and stop if the change is missing, already archived, or otherwise not ready to archive.
|
||||
3. Run `openspec archive <id> --yes` so the CLI moves the change and applies spec updates without prompts (use `--skip-specs` only for tooling-only work).
|
||||
4. Review the command output to confirm the target specs were updated and the change landed in `changes/archive/`.
|
||||
5. Validate with `openspec validate --strict` and inspect with `openspec show <id>` if anything looks off.
|
||||
|
||||
**Reference**
|
||||
- Use `openspec list` to confirm change IDs before archiving.
|
||||
- Inspect refreshed specs with `openspec list --specs` and address any validation issues before handing off.
|
||||
22
.claude/commands/openspec/proposal.md
Normal file
22
.claude/commands/openspec/proposal.md
Normal file
@@ -0,0 +1,22 @@
|
||||
Create an OpenSpec change proposal for: $ARGUMENTS
|
||||
|
||||
**Guardrails**
|
||||
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||
- Keep changes tightly scoped to the requested outcome.
|
||||
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||
- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.
|
||||
- Do not write any code during the proposal stage. Only create design documents (proposal.md, tasks.md, design.md, and spec deltas). Implementation happens in the apply stage after approval.
|
||||
|
||||
**Steps**
|
||||
1. Review `openspec/project.md`, run `openspec list` and `openspec list --specs`, and inspect related code or docs (e.g., via `rg`/`ls`) to ground the proposal in current behaviour; note any gaps that require clarification.
|
||||
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, and `design.md` (when needed) under `openspec/changes/<id>/`.
|
||||
3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
|
||||
4. Capture architectural reasoning in `design.md` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs.
|
||||
5. Draft spec deltas in `changes/<id>/specs/<capability>/spec.md` (one folder per capability) using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement and cross-reference related capabilities when relevant.
|
||||
6. Draft `tasks.md` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work.
|
||||
7. Validate with `openspec validate <id> --strict` and resolve every issue before sharing the proposal.
|
||||
|
||||
**Reference**
|
||||
- Use `openspec show <id> --json --deltas-only` or `openspec show <spec> --type spec` to inspect details when validation fails.
|
||||
- Search existing requirements with `rg -n "Requirement:|Scenario:" openspec/specs` before writing new ones.
|
||||
- Explore the codebase with `rg <keyword>`, `ls`, or direct file reads so proposals align with current implementation realities.
|
||||
43
.commitlintrc.json
Normal file
43
.commitlintrc.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"extends": ["@commitlint/config-conventional"],
|
||||
"rules": {
|
||||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"feat",
|
||||
"fix",
|
||||
"docs",
|
||||
"style",
|
||||
"refactor",
|
||||
"perf",
|
||||
"test",
|
||||
"build",
|
||||
"ci",
|
||||
"chore",
|
||||
"revert"
|
||||
]
|
||||
],
|
||||
"scope-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"theme",
|
||||
"templates",
|
||||
"assets",
|
||||
"css",
|
||||
"js",
|
||||
"php",
|
||||
"admin",
|
||||
"api",
|
||||
"schema",
|
||||
"seo",
|
||||
"config",
|
||||
"deps"
|
||||
]
|
||||
],
|
||||
"subject-case": [2, "always", "lower-case"],
|
||||
"subject-max-length": [2, "always", 72],
|
||||
"body-max-line-length": [2, "always", 72]
|
||||
}
|
||||
}
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -40,8 +40,10 @@ Desktop.ini
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
|
||||
# Composer (si hay dependencias PHP)
|
||||
vendor/
|
||||
|
||||
# PHPUnit
|
||||
.phpunit.result.cache
|
||||
/tests/_output/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
@@ -65,7 +67,10 @@ vendor/
|
||||
# Planning and documentation
|
||||
_planeacion/
|
||||
|
||||
# Testing infrastructure (composer, phpunit, phpcs configs and dependencies)
|
||||
_testing-suite/
|
||||
|
||||
# Claude Code tools
|
||||
.playwright-mcp/
|
||||
.serena/
|
||||
.claude/
|
||||
nul
|
||||
|
||||
1
.husky/commit-msg
Normal file
1
.husky/commit-msg
Normal file
@@ -0,0 +1 @@
|
||||
npx --no -- commitlint --edit $1
|
||||
18
404.php
18
404.php
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* @link https://developer.wordpress.org/themes/basics/template-hierarchy/#404-not-found
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -23,7 +23,7 @@ get_header();
|
||||
<!-- Error Header -->
|
||||
<header class="page-header">
|
||||
<h1 id="error-404-title" class="page-title">
|
||||
<?php esc_html_e( '404 - Page Not Found', 'apus-theme' ); ?>
|
||||
<?php esc_html_e( '404 - Page Not Found', 'roi-theme' ); ?>
|
||||
</h1>
|
||||
</header><!-- .page-header -->
|
||||
|
||||
@@ -31,25 +31,25 @@ get_header();
|
||||
<div class="page-content">
|
||||
|
||||
<p class="error-message">
|
||||
<?php esc_html_e( 'Oops! The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.', 'apus-theme' ); ?>
|
||||
<?php esc_html_e( 'Oops! The page you are looking for might have been removed, had its name changed, or is temporarily unavailable.', 'roi-theme' ); ?>
|
||||
</p>
|
||||
|
||||
<!-- Helpful Actions -->
|
||||
<div class="error-actions">
|
||||
|
||||
<h2><?php esc_html_e( 'What can you do?', 'apus-theme' ); ?></h2>
|
||||
<h2><?php esc_html_e( 'What can you do?', 'roi-theme' ); ?></h2>
|
||||
|
||||
<ul class="error-suggestions" role="list">
|
||||
<li>
|
||||
<a href="<?php echo esc_url( home_url( '/' ) ); ?>">
|
||||
<?php esc_html_e( 'Go to the homepage', 'apus-theme' ); ?>
|
||||
<?php esc_html_e( 'Go to the homepage', 'roi-theme' ); ?>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<?php esc_html_e( 'Check the URL for typos', 'apus-theme' ); ?>
|
||||
<?php esc_html_e( 'Check the URL for typos', 'roi-theme' ); ?>
|
||||
</li>
|
||||
<li>
|
||||
<?php esc_html_e( 'Use the navigation menu above', 'apus-theme' ); ?>
|
||||
<?php esc_html_e( 'Use the navigation menu above', 'roi-theme' ); ?>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -65,7 +65,7 @@ get_header();
|
||||
if ( ! empty( $recent_posts ) ) :
|
||||
?>
|
||||
<div class="recent-posts-section">
|
||||
<h3><?php esc_html_e( 'Recent Posts', 'apus-theme' ); ?></h3>
|
||||
<h3><?php esc_html_e( 'Recent Posts', 'roi-theme' ); ?></h3>
|
||||
<ul class="recent-posts-list" role="list">
|
||||
<?php foreach ( $recent_posts as $recent ) : ?>
|
||||
<li>
|
||||
@@ -95,7 +95,7 @@ get_header();
|
||||
if ( ! empty( $categories ) ) :
|
||||
?>
|
||||
<div class="categories-section">
|
||||
<h3><?php esc_html_e( 'Browse by Category', 'apus-theme' ); ?></h3>
|
||||
<h3><?php esc_html_e( 'Browse by Category', 'roi-theme' ); ?></h3>
|
||||
<ul class="categories-list" role="list">
|
||||
<?php foreach ( $categories as $category ) : ?>
|
||||
<li>
|
||||
|
||||
18
AGENTS.md
Normal file
18
AGENTS.md
Normal file
@@ -0,0 +1,18 @@
|
||||
<!-- OPENSPEC:START -->
|
||||
# OpenSpec Instructions
|
||||
|
||||
These instructions are for AI assistants working in this project.
|
||||
|
||||
Always open `@/openspec/AGENTS.md` when the request:
|
||||
- Mentions planning or proposals (words like proposal, spec, change, plan)
|
||||
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
|
||||
- Sounds ambiguous and you need the authoritative spec before coding
|
||||
|
||||
Use `@/openspec/AGENTS.md` to learn:
|
||||
- How to create and apply change proposals
|
||||
- Spec format and conventions
|
||||
- Project structure and guidelines
|
||||
|
||||
Keep this managed block so 'openspec update' can refresh the instructions.
|
||||
|
||||
<!-- OPENSPEC:END -->
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\AdsensePlacement\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
final class AdsensePlacementFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'adsense-placement';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// VISIBILITY
|
||||
'adsense-placementEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'adsense-placementShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'adsense-placementShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'adsense-placementHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// ANALYTICS (Google Analytics)
|
||||
'adsense-placementAnalyticsEnabled' => ['group' => 'analytics', 'attribute' => 'analytics_enabled'],
|
||||
'adsense-placementGaTrackingId' => ['group' => 'analytics', 'attribute' => 'ga_tracking_id'],
|
||||
'adsense-placementGaAnonymizeIp' => ['group' => 'analytics', 'attribute' => 'ga_anonymize_ip'],
|
||||
|
||||
// CONTENT (Credentials)
|
||||
'adsense-placementPublisherId' => ['group' => 'content', 'attribute' => 'publisher_id'],
|
||||
'adsense-placementSlotDisplay' => ['group' => 'content', 'attribute' => 'slot_display'],
|
||||
'adsense-placementSlotAuto' => ['group' => 'content', 'attribute' => 'slot_auto'],
|
||||
'adsense-placementSlotAutorelaxed' => ['group' => 'content', 'attribute' => 'slot_autorelaxed'],
|
||||
'adsense-placementSlotInarticle' => ['group' => 'content', 'attribute' => 'slot_inarticle'],
|
||||
'adsense-placementSlotSkyscraper' => ['group' => 'content', 'attribute' => 'slot_skyscraper'],
|
||||
|
||||
// BEHAVIOR (Post locations + formats)
|
||||
'adsense-placementPostTopEnabled' => ['group' => 'behavior', 'attribute' => 'post_top_enabled'],
|
||||
'adsense-placementPostTopFormat' => ['group' => 'behavior', 'attribute' => 'post_top_format'],
|
||||
'adsense-placementPostContentEnabled' => ['group' => 'behavior', 'attribute' => 'post_content_enabled'],
|
||||
'adsense-placementPostContentRandomMode' => ['group' => 'behavior', 'attribute' => 'post_content_random_mode'],
|
||||
'adsense-placementPostContentMinAds' => ['group' => 'behavior', 'attribute' => 'post_content_min_ads'],
|
||||
'adsense-placementPostContentMaxAds' => ['group' => 'behavior', 'attribute' => 'post_content_max_ads'],
|
||||
'adsense-placementPostContentAfterParagraphs' => ['group' => 'behavior', 'attribute' => 'post_content_after_paragraphs'],
|
||||
'adsense-placementPostContentMinParagraphsBetween' => ['group' => 'behavior', 'attribute' => 'post_content_min_paragraphs_between'],
|
||||
'adsense-placementPostContentFormat' => ['group' => 'behavior', 'attribute' => 'post_content_format'],
|
||||
'adsense-placementPostBottomEnabled' => ['group' => 'behavior', 'attribute' => 'post_bottom_enabled'],
|
||||
'adsense-placementPostBottomFormat' => ['group' => 'behavior', 'attribute' => 'post_bottom_format'],
|
||||
'adsense-placementAfterRelatedEnabled' => ['group' => 'behavior', 'attribute' => 'after_related_enabled'],
|
||||
'adsense-placementAfterRelatedFormat' => ['group' => 'behavior', 'attribute' => 'after_related_format'],
|
||||
|
||||
// BEHAVIOR (Rail Ads)
|
||||
'adsense-placementRailAdsEnabled' => ['group' => 'behavior', 'attribute' => 'rail_ads_enabled'],
|
||||
'adsense-placementRailLeftEnabled' => ['group' => 'behavior', 'attribute' => 'rail_left_enabled'],
|
||||
'adsense-placementRailRightEnabled' => ['group' => 'behavior', 'attribute' => 'rail_right_enabled'],
|
||||
'adsense-placementRailFormat' => ['group' => 'behavior', 'attribute' => 'rail_format'],
|
||||
'adsense-placementRailTopOffset' => ['group' => 'behavior', 'attribute' => 'rail_top_offset'],
|
||||
|
||||
// LAYOUT (Archive/Global locations + formats)
|
||||
'adsense-placementArchiveTopEnabled' => ['group' => 'layout', 'attribute' => 'archive_top_enabled'],
|
||||
'adsense-placementArchiveBetweenEnabled' => ['group' => 'layout', 'attribute' => 'archive_between_enabled'],
|
||||
'adsense-placementArchiveBetweenEvery' => ['group' => 'layout', 'attribute' => 'archive_between_every'],
|
||||
'adsense-placementArchiveBottomEnabled' => ['group' => 'layout', 'attribute' => 'archive_bottom_enabled'],
|
||||
'adsense-placementArchiveFormat' => ['group' => 'layout', 'attribute' => 'archive_format'],
|
||||
'adsense-placementHeaderBelowEnabled' => ['group' => 'layout', 'attribute' => 'header_below_enabled'],
|
||||
'adsense-placementFooterAboveEnabled' => ['group' => 'layout', 'attribute' => 'footer_above_enabled'],
|
||||
'adsense-placementGlobalFormat' => ['group' => 'layout', 'attribute' => 'global_format'],
|
||||
|
||||
// FORMS (Exclusions + Delay)
|
||||
'adsense-placementExcludeCategories' => ['group' => 'forms', 'attribute' => 'exclude_categories'],
|
||||
'adsense-placementExcludePostTypes' => ['group' => 'forms', 'attribute' => 'exclude_post_types'],
|
||||
'adsense-placementExcludePostIds' => ['group' => 'forms', 'attribute' => 'exclude_post_ids'],
|
||||
'adsense-placementMinContentLength' => ['group' => 'forms', 'attribute' => 'min_content_length'],
|
||||
'adsense-placementDelayEnabled' => ['group' => 'forms', 'attribute' => 'delay_enabled'],
|
||||
'adsense-placementDelayTimeout' => ['group' => 'forms', 'attribute' => 'delay_timeout'],
|
||||
|
||||
// ANCHOR ADS
|
||||
'adsense-placementAnchorEnabled' => ['group' => 'anchor_ads', 'attribute' => 'anchor_enabled'],
|
||||
'adsense-placementAnchorPosition' => ['group' => 'anchor_ads', 'attribute' => 'anchor_position'],
|
||||
'adsense-placementAnchorHeight' => ['group' => 'anchor_ads', 'attribute' => 'anchor_height'],
|
||||
'adsense-placementAnchorCollapsibleEnabled' => ['group' => 'anchor_ads', 'attribute' => 'anchor_collapsible_enabled'],
|
||||
'adsense-placementAnchorShowOnMobile' => ['group' => 'anchor_ads', 'attribute' => 'anchor_show_on_mobile'],
|
||||
'adsense-placementAnchorShowOnWideScreens' => ['group' => 'anchor_ads', 'attribute' => 'anchor_show_on_wide_screens'],
|
||||
'adsense-placementAnchorRememberState' => ['group' => 'anchor_ads', 'attribute' => 'anchor_remember_state'],
|
||||
'adsense-placementAnchorRememberDuration' => ['group' => 'anchor_ads', 'attribute' => 'anchor_remember_duration'],
|
||||
|
||||
// VIGNETTE ADS
|
||||
'adsense-placementVignetteEnabled' => ['group' => 'vignette_ads', 'attribute' => 'vignette_enabled'],
|
||||
'adsense-placementVignetteTrigger' => ['group' => 'vignette_ads', 'attribute' => 'vignette_trigger'],
|
||||
'adsense-placementVignetteTriggerDelay' => ['group' => 'vignette_ads', 'attribute' => 'vignette_trigger_delay'],
|
||||
'adsense-placementVignetteSize' => ['group' => 'vignette_ads', 'attribute' => 'vignette_size'],
|
||||
'adsense-placementVignetteOverlayOpacity' => ['group' => 'vignette_ads', 'attribute' => 'vignette_overlay_opacity'],
|
||||
'adsense-placementVignetteShowOnMobile' => ['group' => 'vignette_ads', 'attribute' => 'vignette_show_on_mobile'],
|
||||
'adsense-placementVignetteShowOnDesktop' => ['group' => 'vignette_ads', 'attribute' => 'vignette_show_on_desktop'],
|
||||
'adsense-placementVignetteReshowEnabled' => ['group' => 'vignette_ads', 'attribute' => 'vignette_reshow_enabled'],
|
||||
'adsense-placementVignetteReshowTime' => ['group' => 'vignette_ads', 'attribute' => 'vignette_reshow_time'],
|
||||
'adsense-placementVignetteMaxPerSession' => ['group' => 'vignette_ads', 'attribute' => 'vignette_max_per_session'],
|
||||
|
||||
// SEARCH RESULTS (ROI APU Search)
|
||||
'adsense-placementSearchAdsEnabled' => ['group' => 'search_results', 'attribute' => 'search_ads_enabled'],
|
||||
'adsense-placementSearchTopAdEnabled' => ['group' => 'search_results', 'attribute' => 'search_top_ad_enabled'],
|
||||
'adsense-placementSearchTopAdFormat' => ['group' => 'search_results', 'attribute' => 'search_top_ad_format'],
|
||||
'adsense-placementSearchBetweenEnabled' => ['group' => 'search_results', 'attribute' => 'search_between_enabled'],
|
||||
'adsense-placementSearchBetweenMax' => ['group' => 'search_results', 'attribute' => 'search_between_max'],
|
||||
'adsense-placementSearchBetweenFormat' => ['group' => 'search_results', 'attribute' => 'search_between_format'],
|
||||
'adsense-placementSearchBetweenPosition' => ['group' => 'search_results', 'attribute' => 'search_between_position'],
|
||||
'adsense-placementSearchBetweenEvery' => ['group' => 'search_results', 'attribute' => 'search_between_every'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'adsense-placementVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'adsense-placementVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'adsense-placementVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'adsense-placementVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'adsense-placementVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'adsense-placementExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'adsense-placementExcludeCategoriesAdv' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'adsense-placementExcludePostIdsAdv' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'adsense-placementExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,985 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\AdsensePlacement\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para AdSense Placement y Google Analytics
|
||||
*
|
||||
* Panel reorganizado con:
|
||||
* - Diagrama visual de ubicaciones
|
||||
* - Secciones colapsables
|
||||
* - In-content ads configurables (1-8 random)
|
||||
*/
|
||||
final class AdsensePlacementFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
// HEADER CON GRADIENTE
|
||||
$html .= '<div class="rounded p-4 mb-4 shadow text-white" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-megaphone me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' AdSense y Analytics';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Configura Google AdSense y Analytics con ubicaciones visuales';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// LAYOUT 2 COLUMNAS
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// COLUMNA IZQUIERDA (7 cols)
|
||||
$html .= ' <div class="col-lg-7">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildDiagramSection();
|
||||
$html .= $this->buildPostLocationsGroup($componentId);
|
||||
$html .= $this->buildInContentAdsGroup($componentId);
|
||||
$html .= $this->buildExclusionsGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
|
||||
// COLUMNA DERECHA (5 cols)
|
||||
$html .= ' <div class="col-lg-5">';
|
||||
$html .= $this->buildCredentialsGroup($componentId);
|
||||
$html .= $this->buildAnalyticsGroup($componentId);
|
||||
$html .= $this->buildRailAdsGroup($componentId);
|
||||
$html .= $this->buildAnchorAdsGroup($componentId);
|
||||
$html .= $this->buildVignetteAdsGroup($componentId);
|
||||
$html .= $this->buildSearchResultsGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #28a745;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-power me-2" style="color: #28a745;"></i>';
|
||||
$html .= ' Activacion Global';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$enabled = $this->renderer->getFieldValue($cid, 'visibility', 'is_enabled', false);
|
||||
$html .= $this->buildSwitch($cid . 'Enabled', 'Activar AdSense', $enabled, 'bi-power');
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$showMobile = $this->renderer->getFieldValue($cid, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch($cid . 'ShowOnMobile', 'Mostrar en movil', $showMobile, 'bi-phone');
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$showDesktop = $this->renderer->getFieldValue($cid, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch($cid . 'ShowOnDesktop', 'Mostrar en escritorio', $showDesktop, 'bi-display');
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Opcion para ocultar anuncios a usuarios logueados
|
||||
$html .= '<div class="mt-3 p-2 rounded" style="background: #fff3cd;">';
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($cid, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= $this->buildSwitch($cid . 'HideForLoggedIn', 'Ocultar para usuarios logueados', $hideForLoggedIn, 'bi-person-lock');
|
||||
$html .= '<small class="text-muted d-block" style="margin-top: -8px; margin-left: 40px;">No mostrar anuncios a usuarios con sesion iniciada en WordPress</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
// =============================================
|
||||
// Visibilidad por tipo de pagina
|
||||
// Grupo especial: _page_visibility (Plan 99.11)
|
||||
// =============================================
|
||||
$html .= '<hr class="my-3">';
|
||||
$html .= '<p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-layout-text-window me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= '</p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($cid, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($cid, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($cid, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($cid, '_page_visibility', 'show_on_archives', true);
|
||||
$showOnSearch = $this->renderer->getFieldValue($cid, '_page_visibility', 'show_on_search', true);
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox($cid . 'VisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox($cid . 'VisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox($cid . 'VisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox($cid . 'VisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox($cid . 'VisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($cid, $cid);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagrama visual de ubicaciones de anuncios
|
||||
*/
|
||||
private function buildDiagramSection(): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #6f42c1;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-layout-text-window-reverse me-2" style="color: #6f42c1;"></i>';
|
||||
$html .= ' Mapa de Ubicaciones';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Diagrama visual del layout
|
||||
$html .= '<div class="border rounded p-3" style="background: #f8f9fa; font-family: monospace; font-size: 11px;">';
|
||||
|
||||
// Anchor Top
|
||||
$html .= '<div class="text-center p-2 mb-1 rounded" style="background: #d1ecf1; border: 2px solid #17a2b8;">';
|
||||
$html .= ' <i class="bi bi-pin-angle"></i> <strong>ANCHOR TOP</strong> (fijo, collapsible)';
|
||||
$html .= '</div>';
|
||||
|
||||
// Header
|
||||
$html .= '<div class="text-center p-2 mb-1 rounded" style="background: #e9ecef; border: 1px dashed #6c757d;">';
|
||||
$html .= ' <strong>HEADER</strong>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Hero / Featured Image
|
||||
$html .= '<div class="text-center p-2 mb-1 rounded" style="background: #d1e7dd; border: 1px solid #198754;">';
|
||||
$html .= ' <i class="bi bi-image"></i> Featured Image / Hero';
|
||||
$html .= '</div>';
|
||||
|
||||
// Ad: Post Top
|
||||
$html .= '<div class="text-center p-2 mb-1 rounded" style="background: #fff3cd; border: 2px solid #ffc107;">';
|
||||
$html .= ' <i class="bi bi-megaphone"></i> <strong>📍 POST-TOP</strong> (Despues de imagen)';
|
||||
$html .= '</div>';
|
||||
|
||||
// Content container
|
||||
$html .= '<div class="p-2 mb-1 rounded" style="background: #fff; border: 1px solid #dee2e6;">';
|
||||
$html .= ' <div class="mb-1 small text-muted text-center">📝 CONTENIDO DEL POST</div>';
|
||||
$html .= ' <div class="p-1 rounded mb-1" style="background: #e7f1ff; font-size: 10px;">Parrafo 1...</div>';
|
||||
$html .= ' <div class="p-1 rounded mb-1" style="background: #e7f1ff; font-size: 10px;">Parrafo 2...</div>';
|
||||
$html .= ' <div class="p-1 rounded mb-1" style="background: #e7f1ff; font-size: 10px;">Parrafo 3...</div>';
|
||||
|
||||
// In-content ad
|
||||
$html .= ' <div class="text-center p-1 mb-1 rounded" style="background: #fff3cd; border: 2px dashed #ffc107; font-size: 10px;">';
|
||||
$html .= ' <i class="bi bi-megaphone"></i> <strong>📍 IN-CONTENT #1</strong>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="p-1 rounded mb-1" style="background: #e7f1ff; font-size: 10px;">Parrafo 4...</div>';
|
||||
$html .= ' <div class="p-1 rounded mb-1" style="background: #e7f1ff; font-size: 10px;">Parrafo 5...</div>';
|
||||
$html .= ' <div class="p-1 rounded mb-1" style="background: #e7f1ff; font-size: 10px;">Parrafo 6...</div>';
|
||||
|
||||
// In-content ad 2
|
||||
$html .= ' <div class="text-center p-1 mb-1 rounded" style="background: #fff3cd; border: 2px dashed #ffc107; font-size: 10px;">';
|
||||
$html .= ' <i class="bi bi-megaphone"></i> <strong>📍 IN-CONTENT #2</strong> (random)';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="p-1 rounded" style="background: #e7f1ff; font-size: 10px;">Mas parrafos...</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Ad: Post Bottom
|
||||
$html .= '<div class="text-center p-2 mb-1 rounded" style="background: #fff3cd; border: 2px solid #ffc107;">';
|
||||
$html .= ' <i class="bi bi-megaphone"></i> <strong>📍 POST-BOTTOM</strong> (Despues del contenido)';
|
||||
$html .= '</div>';
|
||||
|
||||
// Related Posts
|
||||
$html .= '<div class="text-center p-2 mb-1 rounded" style="background: #cfe2ff; border: 1px solid #0d6efd;">';
|
||||
$html .= ' <i class="bi bi-grid-3x2"></i> Related Posts';
|
||||
$html .= '</div>';
|
||||
|
||||
// Ad: After Related
|
||||
$html .= '<div class="text-center p-2 mb-1 rounded" style="background: #fff3cd; border: 2px solid #ffc107;">';
|
||||
$html .= ' <i class="bi bi-megaphone"></i> <strong>📍 AFTER-RELATED</strong>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Footer
|
||||
$html .= '<div class="text-center p-2 mb-1 rounded" style="background: #e9ecef; border: 1px dashed #6c757d;">';
|
||||
$html .= ' <strong>FOOTER</strong>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Anchor Bottom
|
||||
$html .= '<div class="text-center p-2 rounded" style="background: #d1ecf1; border: 2px solid #17a2b8;">';
|
||||
$html .= ' <i class="bi bi-pin-angle"></i> <strong>ANCHOR BOTTOM</strong> (fijo, collapsible)';
|
||||
$html .= '</div>';
|
||||
|
||||
// Rail Ads (laterales)
|
||||
$html .= '<div class="mt-2 d-flex justify-content-between">';
|
||||
$html .= ' <div class="p-2 rounded text-center" style="width: 45%; background: #f8d7da; border: 2px solid #dc3545; font-size: 10px;">';
|
||||
$html .= ' <strong>📍 RAIL IZQ</strong><br><small>(160x600)</small>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="p-2 rounded text-center" style="width: 45%; background: #f8d7da; border: 2px solid #dc3545; font-size: 10px;">';
|
||||
$html .= ' <strong>📍 RAIL DER</strong><br><small>(160x600)</small>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Vignette Ad (modal)
|
||||
$html .= '<div class="mt-2 p-2 rounded text-center" style="background: #f3e5f5; border: 2px solid #9c27b0;">';
|
||||
$html .= ' <i class="bi bi-fullscreen"></i> <strong>VIGNETTE</strong> (modal pantalla completa)';
|
||||
$html .= ' <br><small class="text-muted">Aparece segun trigger configurado</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="mt-2 small text-muted">';
|
||||
$html .= ' <i class="bi bi-info-circle"></i> <span class="badge bg-warning text-dark">Amarillo</span> = Posts, ';
|
||||
$html .= ' <span class="badge bg-danger">Rojo</span> = Rails >1600px, ';
|
||||
$html .= ' <span class="badge" style="background:#17a2b8;color:white;">Cyan</span> = Anchors, ';
|
||||
$html .= ' <span class="badge" style="background:#9c27b0;color:white;">Morado</span> = Vignette';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPostLocationsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #ffc107;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-geo-alt me-2" style="color: #ffc107;"></i>';
|
||||
$html .= ' Ubicaciones en Posts';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// === POST-TOP ===
|
||||
$html .= '<div class="border rounded p-3 mb-3" style="background: #fffbeb;">';
|
||||
$html .= '<div class="d-flex align-items-center gap-2 mb-2">';
|
||||
$html .= ' <span class="badge bg-warning text-dark">POST-TOP</span>';
|
||||
$html .= ' <small class="text-muted">Despues de la imagen destacada</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$postTopEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'post_top_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'PostTopEnabled', 'Activar', $postTopEnabled);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$html .= $this->buildSelect($cid . 'PostTopFormat', 'Formato',
|
||||
$this->renderer->getFieldValue($cid, 'behavior', 'post_top_format', 'auto'),
|
||||
[
|
||||
'auto' => 'Auto (responsive)',
|
||||
'in-article' => 'In-Article (fluid)',
|
||||
'display' => 'Display (728x90)',
|
||||
'display-large' => 'Display Large (970x250)'
|
||||
]
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// === POST-BOTTOM ===
|
||||
$html .= '<div class="border rounded p-3 mb-3" style="background: #fffbeb;">';
|
||||
$html .= '<div class="d-flex align-items-center gap-2 mb-2">';
|
||||
$html .= ' <span class="badge bg-warning text-dark">POST-BOTTOM</span>';
|
||||
$html .= ' <small class="text-muted">Despues del contenido, antes de Related</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$postBottomEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'post_bottom_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'PostBottomEnabled', 'Activar', $postBottomEnabled);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$html .= $this->buildSelect($cid . 'PostBottomFormat', 'Formato',
|
||||
$this->renderer->getFieldValue($cid, 'behavior', 'post_bottom_format', 'auto'),
|
||||
['auto' => 'Auto', 'in-article' => 'In-Article', 'display' => 'Display']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// === AFTER-RELATED ===
|
||||
$html .= '<div class="border rounded p-3" style="background: #fffbeb;">';
|
||||
$html .= '<div class="d-flex align-items-center gap-2 mb-2">';
|
||||
$html .= ' <span class="badge bg-warning text-dark">AFTER-RELATED</span>';
|
||||
$html .= ' <small class="text-muted">Despues de Related Posts</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$afterRelatedEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'after_related_enabled', false);
|
||||
$html .= $this->buildSwitch($cid . 'AfterRelatedEnabled', 'Activar', $afterRelatedEnabled);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$html .= $this->buildSelect($cid . 'AfterRelatedFormat', 'Formato',
|
||||
$this->renderer->getFieldValue($cid, 'behavior', 'after_related_format', 'autorelaxed'),
|
||||
['autorelaxed' => 'Autorelaxed (feed)', 'auto' => 'Auto']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seccion especial para in-content ads con configuracion de 1-8 random
|
||||
*/
|
||||
private function buildInContentAdsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #0d6efd;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-body-text me-2" style="color: #0d6efd;"></i>';
|
||||
$html .= ' Anuncios Dentro del Contenido';
|
||||
$html .= ' <span class="badge bg-primary ms-2">1-8 ads</span>';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= '<div class="alert alert-info small mb-3">';
|
||||
$html .= ' <i class="bi bi-lightbulb me-1"></i>';
|
||||
$html .= ' <strong>Modo Random:</strong> Inserta entre 1 y 8 anuncios en posiciones aleatorias entre parrafos.';
|
||||
$html .= ' Mejor UX al variar la posicion en cada visita.';
|
||||
$html .= '</div>';
|
||||
|
||||
// Master switch
|
||||
$postContentEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_enabled', false);
|
||||
$html .= '<div class="mb-3">';
|
||||
$html .= $this->buildSwitch($cid . 'PostContentEnabled', 'Activar In-Content Ads', $postContentEnabled, 'bi-power');
|
||||
$html .= '</div>';
|
||||
|
||||
// Configuracion de cantidad
|
||||
$html .= '<div class="row g-2 mb-3">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$minAdsValue = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_min_ads', '1');
|
||||
$html .= $this->buildSelect($cid . 'PostContentMinAds', 'Minimo de anuncios',
|
||||
is_string($minAdsValue) ? $minAdsValue : '1',
|
||||
['1' => '1 anuncio', '2' => '2 anuncios', '3' => '3 anuncios', '4' => '4 anuncios']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$maxAdsValue = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_max_ads', '3');
|
||||
$html .= $this->buildSelect($cid . 'PostContentMaxAds', 'Maximo de anuncios',
|
||||
is_string($maxAdsValue) ? $maxAdsValue : '3',
|
||||
[
|
||||
'1' => '1 anuncio', '2' => '2 anuncios', '3' => '3 anuncios', '4' => '4 anuncios',
|
||||
'5' => '5 anuncios', '6' => '6 anuncios', '7' => '7 anuncios', '8' => '8 anuncios'
|
||||
]
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Configuracion de posicionamiento
|
||||
$html .= '<div class="row g-2 mb-3">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$afterPara = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_after_paragraphs', '3');
|
||||
$html .= $this->buildTextInput($cid . 'PostContentAfterParagraphs', 'Primer ad despues del parrafo #', (string)$afterPara, '3');
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$minBetweenValue = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_min_paragraphs_between', '4');
|
||||
$html .= $this->buildSelect($cid . 'PostContentMinParagraphsBetween', 'Parrafos entre ads',
|
||||
is_string($minBetweenValue) ? $minBetweenValue : '4',
|
||||
['2' => '2 parrafos', '3' => '3 parrafos', '4' => '4 parrafos', '5' => '5 parrafos', '6' => '6 parrafos']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Modo y formato
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$randomMode = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_random_mode', true);
|
||||
$html .= $this->buildSwitch($cid . 'PostContentRandomMode', 'Posiciones aleatorias', $randomMode, 'bi-shuffle');
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$formatValue = $this->renderer->getFieldValue($cid, 'behavior', 'post_content_format', 'in-article');
|
||||
$html .= $this->buildSelect($cid . 'PostContentFormat', 'Formato de ads',
|
||||
is_string($formatValue) ? $formatValue : 'in-article',
|
||||
['in-article' => 'In-Article (fluid)', 'auto' => 'Auto (responsive)']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildCredentialsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-key me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Credenciales AdSense';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Publisher ID
|
||||
$pubId = $this->renderer->getFieldValue($cid, 'content', 'publisher_id', 'ca-pub-8476420265998726');
|
||||
$html .= $this->buildTextInput($cid . 'PublisherId', 'Publisher ID', $pubId, 'ca-pub-XXXXX');
|
||||
|
||||
$html .= '<hr class="my-3">';
|
||||
$html .= '<p class="small text-muted mb-2"><i class="bi bi-info-circle me-1"></i> Slots por tipo de anuncio:</p>';
|
||||
|
||||
// Slots con descripciones claras
|
||||
$html .= '<div class="mb-2">';
|
||||
$slotAuto = $this->renderer->getFieldValue($cid, 'content', 'slot_auto', '8471732096');
|
||||
$html .= $this->buildTextInput($cid . 'SlotAuto', '📱 Auto (responsive)', $slotAuto);
|
||||
$html .= '<div class="form-text small" style="margin-top:-10px;">Para: Post-Top, Post-Bottom, globales</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="mb-2">';
|
||||
$slotInArticle = $this->renderer->getFieldValue($cid, 'content', 'slot_inarticle', '7285187368');
|
||||
$html .= $this->buildTextInput($cid . 'SlotInarticle', '📝 In-Article (fluid)', $slotInArticle);
|
||||
$html .= '<div class="form-text small" style="margin-top:-10px;">Para: In-Content (dentro del texto)</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="mb-2">';
|
||||
$slotDisplay = $this->renderer->getFieldValue($cid, 'content', 'slot_display', '2873062302');
|
||||
$html .= $this->buildTextInput($cid . 'SlotDisplay', '🖥️ Display (fijo)', $slotDisplay);
|
||||
$html .= '<div class="form-text small" style="margin-top:-10px;">Para: 728x90, 970x250 (opcional)</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="mb-2">';
|
||||
$slotRelaxed = $this->renderer->getFieldValue($cid, 'content', 'slot_autorelaxed', '9205569855');
|
||||
$html .= $this->buildTextInput($cid . 'SlotAutorelaxed', '📋 Autorelaxed (feed)', $slotRelaxed);
|
||||
$html .= '<div class="form-text small" style="margin-top:-10px;">Para: After-Related, archives</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="mb-2">';
|
||||
$slotSkyscraper = $this->renderer->getFieldValue($cid, 'content', 'slot_skyscraper', '');
|
||||
$html .= $this->buildTextInput($cid . 'SlotSkyscraper', '🏢 Skyscraper (tall)', $slotSkyscraper);
|
||||
$html .= '<div class="form-text small" style="margin-top:-10px;">Para: Rail Ads laterales (160x600)</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildAnalyticsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #4285f4;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-graph-up me-2" style="color: #4285f4;"></i>';
|
||||
$html .= ' Google Analytics';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Switch: Analytics Enabled
|
||||
$analyticsEnabled = $this->renderer->getFieldValue($cid, 'analytics', 'analytics_enabled', false);
|
||||
$html .= $this->buildSwitch($cid . 'AnalyticsEnabled', 'Activar Analytics', $analyticsEnabled, 'bi-power');
|
||||
|
||||
// Tracking ID
|
||||
$gaTrackingId = $this->renderer->getFieldValue($cid, 'analytics', 'ga_tracking_id', '');
|
||||
$html .= $this->buildTextInput($cid . 'GaTrackingId', 'Google Analytics ID', $gaTrackingId, 'G-XXXXXXXXXX');
|
||||
$html .= '<div class="form-text small mb-2">Formato: G-XXXXXXXXXX (GA4) o UA-XXXXXXXX-X</div>';
|
||||
|
||||
// Anonymize IP
|
||||
$gaAnonymizeIp = $this->renderer->getFieldValue($cid, 'analytics', 'ga_anonymize_ip', true);
|
||||
$html .= $this->buildSwitch($cid . 'GaAnonymizeIp', 'Anonimizar IP (GDPR)', $gaAnonymizeIp, 'bi-shield-check');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildRailAdsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #dc3545;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-layout-sidebar me-2" style="color: #dc3545;"></i>';
|
||||
$html .= ' Rail Ads (Laterales)';
|
||||
$html .= ' <span class="badge bg-secondary ms-2">>1600px</span>';
|
||||
$html .= ' </h5>';
|
||||
$html .= ' <p class="small text-muted mb-3">Anuncios fijos en los margenes del viewport. Solo en pantallas muy anchas.</p>';
|
||||
|
||||
// Master switch
|
||||
$railEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'rail_ads_enabled', false);
|
||||
$html .= $this->buildSwitch($cid . 'RailAdsEnabled', 'Activar Rail Ads', $railEnabled, 'bi-power');
|
||||
|
||||
// Left/Right toggles
|
||||
$html .= '<div class="row g-2 mt-2">';
|
||||
$html .= ' <div class="col-6">';
|
||||
$leftEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'rail_left_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'RailLeftEnabled', 'Rail izquierdo', $leftEnabled);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6">';
|
||||
$rightEnabled = $this->renderer->getFieldValue($cid, 'behavior', 'rail_right_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'RailRightEnabled', 'Rail derecho', $rightEnabled);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Format select - Solo altura (el ancho es responsive)
|
||||
$railFormat = $this->renderer->getFieldValue($cid, 'behavior', 'rail_format', 'h600');
|
||||
$html .= $this->buildSelect($cid . 'RailFormat', 'Altura del Rail',
|
||||
$railFormat,
|
||||
[
|
||||
'h250' => '250px (Compacto)',
|
||||
'h300' => '300px (Pequeno)',
|
||||
'h400' => '400px (Mediano)',
|
||||
'h500' => '500px',
|
||||
'h600' => '600px (Recomendado)',
|
||||
'h700' => '700px',
|
||||
'h800' => '800px (Grande)',
|
||||
'h1050' => '1050px (Extra grande)'
|
||||
]
|
||||
);
|
||||
$html .= '<small class="text-muted d-block mt-1 mb-2">El ancho se ajusta automaticamente al espacio disponible.</small>';
|
||||
|
||||
// Top offset - Select con opciones predefinidas
|
||||
$topOffset = $this->renderer->getFieldValue($cid, 'behavior', 'rail_top_offset', '300');
|
||||
$html .= $this->buildSelect($cid . 'RailTopOffset', 'Distancia desde arriba',
|
||||
$topOffset,
|
||||
[
|
||||
'150' => '150px (Cerca del header)',
|
||||
'200' => '200px',
|
||||
'300' => '300px (Recomendado)',
|
||||
'400' => '400px',
|
||||
'500' => '500px',
|
||||
'700' => '700px (Debajo del fold)'
|
||||
]
|
||||
);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seccion para Anchor Ads (anuncios fijos top/bottom)
|
||||
*/
|
||||
private function buildAnchorAdsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #17a2b8;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-pin-angle me-2" style="color: #17a2b8;"></i>';
|
||||
$html .= ' Anuncios Fijos (Anchor)';
|
||||
$html .= ' </h5>';
|
||||
$html .= ' <p class="small text-muted mb-3">Anuncios fijos en el borde superior o inferior de la pantalla.</p>';
|
||||
|
||||
// Master switch
|
||||
$anchorEnabled = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_enabled', false);
|
||||
$html .= $this->buildSwitch($cid . 'AnchorEnabled', 'Activar Anchor Ads', $anchorEnabled, 'bi-power');
|
||||
|
||||
// Posicion
|
||||
$anchorPosition = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_position', 'bottom');
|
||||
$html .= $this->buildSelect($cid . 'AnchorPosition', 'Posicion del anuncio',
|
||||
$anchorPosition,
|
||||
[
|
||||
'top' => 'Solo en la parte superior',
|
||||
'bottom' => 'Solo en la parte inferior',
|
||||
'both' => 'Superior e inferior'
|
||||
]
|
||||
);
|
||||
|
||||
// Altura
|
||||
$anchorHeight = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_height', '90');
|
||||
$html .= $this->buildSelect($cid . 'AnchorHeight', 'Altura del anchor',
|
||||
$anchorHeight,
|
||||
['50' => '50px', '90' => '90px', '100' => '100px', '120' => '120px']
|
||||
);
|
||||
|
||||
// Collapsible toggle
|
||||
$collapsible = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_collapsible_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'AnchorCollapsibleEnabled', 'Permitir minimizar', $collapsible, 'bi-arrows-collapse');
|
||||
$html .= '<small class="text-muted d-block" style="margin-top: -8px; margin-left: 40px;">Usuario puede minimizar en lugar de cerrar</small>';
|
||||
|
||||
// Pantallas
|
||||
$html .= '<div class="row g-2 mt-2">';
|
||||
$html .= ' <div class="col-6">';
|
||||
$showMobile = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_show_on_mobile', true);
|
||||
$html .= $this->buildSwitch($cid . 'AnchorShowOnMobile', 'Mostrar en movil', $showMobile, 'bi-phone');
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6">';
|
||||
$showWide = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_show_on_wide_screens', false);
|
||||
$html .= $this->buildSwitch($cid . 'AnchorShowOnWideScreens', 'Pantallas anchas', $showWide, 'bi-display');
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Recordar estado
|
||||
$html .= '<div class="mt-3 p-2 rounded" style="background: #e7f1ff;">';
|
||||
$rememberState = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_remember_state', true);
|
||||
$html .= $this->buildSwitch($cid . 'AnchorRememberState', 'Recordar cierre/colapso', $rememberState, 'bi-clock-history');
|
||||
|
||||
$rememberDuration = $this->renderer->getFieldValue($cid, 'anchor_ads', 'anchor_remember_duration', 'session');
|
||||
$html .= $this->buildSelect($cid . 'AnchorRememberDuration', 'Duracion',
|
||||
$rememberDuration,
|
||||
[
|
||||
'session' => 'Solo esta sesion',
|
||||
'1hour' => '1 hora',
|
||||
'1day' => '1 dia',
|
||||
'1week' => '1 semana'
|
||||
]
|
||||
);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seccion para Vignette Ads (pantalla completa)
|
||||
*/
|
||||
private function buildVignetteAdsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #9c27b0;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fullscreen me-2" style="color: #9c27b0;"></i>';
|
||||
$html .= ' Anuncios de Vineta';
|
||||
$html .= ' <span class="badge bg-secondary ms-2">Pantalla Completa</span>';
|
||||
$html .= ' </h5>';
|
||||
$html .= ' <p class="small text-muted mb-3">Anuncios que ocupan toda la pantalla, aparecen segun el trigger configurado.</p>';
|
||||
|
||||
// Master switch
|
||||
$vignetteEnabled = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_enabled', false);
|
||||
$html .= $this->buildSwitch($cid . 'VignetteEnabled', 'Activar Vignette Ads', $vignetteEnabled, 'bi-power');
|
||||
|
||||
// Trigger
|
||||
$vignetteTrigger = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_trigger', 'pageview');
|
||||
$html .= $this->buildSelect($cid . 'VignetteTrigger', 'Cuando mostrar',
|
||||
(string)$vignetteTrigger,
|
||||
[
|
||||
'pageview' => 'Al cargar la pagina',
|
||||
'scroll_50' => 'Al scrollear 50%',
|
||||
'scroll_75' => 'Al scrollear 75%',
|
||||
'exit_intent' => 'Al intentar salir',
|
||||
'time_delay' => 'Despues de X segundos'
|
||||
]
|
||||
);
|
||||
|
||||
// Delay inicial
|
||||
$triggerDelay = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_trigger_delay', '5');
|
||||
$html .= $this->buildTextInput($cid . 'VignetteTriggerDelay', 'Delay inicial (segundos)', (string)$triggerDelay, '5');
|
||||
|
||||
// Tamano y opacidad
|
||||
$html .= '<div class="row g-2 mt-2">';
|
||||
$html .= ' <div class="col-6">';
|
||||
$size = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_size', 'auto');
|
||||
$html .= $this->buildSelect($cid . 'VignetteSize', 'Tamano',
|
||||
(string)$size,
|
||||
[
|
||||
'auto' => 'Auto (recomendado)',
|
||||
'responsive' => 'Responsive (fluid)',
|
||||
'1280x720' => '1280x720 (HD 720p)',
|
||||
'960x540' => '960x540 (qHD)',
|
||||
'854x480' => '854x480 (480p)',
|
||||
'800x450' => '800x450 (16:9)',
|
||||
'640x360' => '640x360 (360p)',
|
||||
'560x315' => '560x315 (YouTube)',
|
||||
'300x250' => '300x250 (Rectangle)',
|
||||
'336x280' => '336x280 (Large Rectangle)',
|
||||
]
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6">';
|
||||
$opacity = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_overlay_opacity', '0.7');
|
||||
$html .= $this->buildSelect($cid . 'VignetteOverlayOpacity', 'Opacidad fondo',
|
||||
(string)$opacity,
|
||||
['0.5' => '50%', '0.6' => '60%', '0.7' => '70%', '0.8' => '80%', '0.9' => '90%']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Pantallas
|
||||
$html .= '<div class="row g-2 mt-2">';
|
||||
$html .= ' <div class="col-6">';
|
||||
$showMobile = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_show_on_mobile', true);
|
||||
$html .= $this->buildSwitch($cid . 'VignetteShowOnMobile', 'Mostrar en movil', $showMobile, 'bi-phone');
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6">';
|
||||
$showDesktop = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_show_on_desktop', true);
|
||||
$html .= $this->buildSwitch($cid . 'VignetteShowOnDesktop', 'Mostrar en desktop', $showDesktop, 'bi-display');
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Reaparicion
|
||||
$html .= '<div class="mt-3 p-2 rounded" style="background: #f3e5f5;">';
|
||||
$html .= '<p class="small fw-semibold mb-2"><i class="bi bi-arrow-repeat me-1"></i> Reaparicion</p>';
|
||||
|
||||
$reshowEnabled = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_reshow_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'VignetteReshowEnabled', 'Permitir reaparicion', $reshowEnabled);
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-6">';
|
||||
$reshowTime = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_reshow_time', '5');
|
||||
$html .= $this->buildSelect($cid . 'VignetteReshowTime', 'Tiempo (min)',
|
||||
(string)$reshowTime,
|
||||
['1' => '1 min', '2' => '2 min', '3' => '3 min', '4' => '4 min', '5' => '5 min', '10' => '10 min', '15' => '15 min', '30' => '30 min']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6">';
|
||||
$maxSession = $this->renderer->getFieldValue($cid, 'vignette_ads', 'vignette_max_per_session', '3');
|
||||
$html .= $this->buildSelect($cid . 'VignetteMaxPerSession', 'Max/sesion',
|
||||
(string)$maxSession,
|
||||
['1' => '1', '2' => '2', '3' => '3', '5' => '5', 'unlimited' => 'Sin limite']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seccion para anuncios en resultados de busqueda (ROI APU Search)
|
||||
*/
|
||||
private function buildSearchResultsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #fd7e14;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-search me-2" style="color: #fd7e14;"></i>';
|
||||
$html .= ' Resultados de Busqueda';
|
||||
$html .= ' <span class="badge bg-secondary ms-2">ROI APU Search</span>';
|
||||
$html .= ' </h5>';
|
||||
$html .= ' <p class="small text-muted mb-3">Insertar anuncios en los resultados del buscador de Analisis de Precios Unitarios.</p>';
|
||||
|
||||
// Master switch
|
||||
$searchAdsEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_ads_enabled', false);
|
||||
$html .= $this->buildSwitch($cid . 'SearchAdsEnabled', 'Activar ads en busqueda', $searchAdsEnabled, 'bi-power');
|
||||
|
||||
// Anuncio superior
|
||||
$html .= '<div class="border rounded p-3 mb-3" style="background: #fff8f0;">';
|
||||
$html .= '<div class="d-flex align-items-center gap-2 mb-2">';
|
||||
$html .= ' <span class="badge" style="background: #fd7e14;">ANUNCIO SUPERIOR</span>';
|
||||
$html .= ' <small class="text-muted">Debajo del campo de busqueda</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$topEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_top_ad_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'SearchTopAdEnabled', 'Activar', $topEnabled);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$topFormat = $this->renderer->getFieldValue($cid, 'search_results', 'search_top_ad_format', 'auto');
|
||||
$html .= $this->buildSelect($cid . 'SearchTopAdFormat', 'Formato',
|
||||
(string)$topFormat,
|
||||
['auto' => 'Auto (responsive)', 'display' => 'Display (fijo)', 'in-article' => 'In-Article (fluid)']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Anuncios entre resultados
|
||||
$html .= '<div class="border rounded p-3" style="background: #fff8f0;">';
|
||||
$html .= '<div class="d-flex align-items-center gap-2 mb-2">';
|
||||
$html .= ' <span class="badge" style="background: #fd7e14;">ENTRE RESULTADOS</span>';
|
||||
$html .= ' <small class="text-muted">Intercalados con los resultados</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2 mb-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$betweenEnabled = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'SearchBetweenEnabled', 'Activar', $betweenEnabled);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$betweenMax = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_max', '1');
|
||||
$html .= $this->buildSelect($cid . 'SearchBetweenMax', 'Maximo ads',
|
||||
(string)$betweenMax,
|
||||
['1' => '1 anuncio', '2' => '2 anuncios', '3' => '3 anuncios (max)']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2 mb-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$betweenFormat = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_format', 'in-article');
|
||||
$html .= $this->buildSelect($cid . 'SearchBetweenFormat', 'Formato',
|
||||
(string)$betweenFormat,
|
||||
['in-article' => 'In-Article (fluid)', 'auto' => 'Auto (responsive)', 'autorelaxed' => 'Autorelaxed (feed)']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$betweenPosition = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_position', 'random');
|
||||
$html .= $this->buildSelect($cid . 'SearchBetweenPosition', 'Posicion',
|
||||
(string)$betweenPosition,
|
||||
['random' => 'Aleatorio', 'fixed' => 'Fijo (cada N)', 'first_half' => 'Primera mitad']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="row g-2">';
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$betweenEvery = $this->renderer->getFieldValue($cid, 'search_results', 'search_between_every', '5');
|
||||
$html .= $this->buildSelect($cid . 'SearchBetweenEvery', 'Cada N resultados (si es fijo)',
|
||||
(string)$betweenEvery,
|
||||
['3' => 'Cada 3', '4' => 'Cada 4', '5' => 'Cada 5', '6' => 'Cada 6', '7' => 'Cada 7', '8' => 'Cada 8', '10' => 'Cada 10']
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildExclusionsGroup(string $cid): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #6c757d;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-slash-circle me-2" style="color: #6c757d;"></i>';
|
||||
$html .= ' Exclusiones y Rendimiento';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Accordion para exclusiones
|
||||
$html .= '<div class="accordion accordion-flush" id="exclusionsAccordion">';
|
||||
|
||||
// Exclusiones
|
||||
$html .= '<div class="accordion-item">';
|
||||
$html .= ' <h2 class="accordion-header">';
|
||||
$html .= ' <button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#exclusionsCollapse">';
|
||||
$html .= ' <i class="bi bi-funnel me-2"></i> Filtros de exclusion';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </h2>';
|
||||
$html .= ' <div id="exclusionsCollapse" class="accordion-collapse collapse" data-bs-parent="#exclusionsAccordion">';
|
||||
$html .= ' <div class="accordion-body">';
|
||||
|
||||
$excludeCats = $this->renderer->getFieldValue($cid, 'forms', 'exclude_categories', '');
|
||||
$html .= $this->buildTextarea($cid . 'ExcludeCategories', 'Excluir categorias (IDs)', $excludeCats, 'Ej: 5,12,23');
|
||||
|
||||
$excludeTypes = $this->renderer->getFieldValue($cid, 'forms', 'exclude_post_types', '');
|
||||
$html .= $this->buildTextarea($cid . 'ExcludePostTypes', 'Excluir tipos de post', $excludeTypes, 'Ej: page,attachment');
|
||||
|
||||
$excludeIds = $this->renderer->getFieldValue($cid, 'forms', 'exclude_post_ids', '');
|
||||
$html .= $this->buildTextarea($cid . 'ExcludePostIds', 'Excluir posts (IDs)', $excludeIds, 'Ej: 100,205,310');
|
||||
|
||||
$minLength = $this->renderer->getFieldValue($cid, 'forms', 'min_content_length', '500');
|
||||
$html .= $this->buildTextInput($cid . 'MinContentLength', 'Longitud minima de contenido', $minLength);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>'; // end accordion
|
||||
|
||||
// Delay settings (siempre visibles)
|
||||
$html .= '<hr class="my-3">';
|
||||
$html .= '<p class="small text-muted mb-2"><i class="bi bi-speedometer2 me-1"></i> Rendimiento:</p>';
|
||||
|
||||
$delayEnabled = $this->renderer->getFieldValue($cid, 'forms', 'delay_enabled', true);
|
||||
$html .= $this->buildSwitch($cid . 'DelayEnabled', 'Retrasar carga (mejor PageSpeed)', $delayEnabled, 'bi-hourglass-split');
|
||||
|
||||
$delayTimeout = $this->renderer->getFieldValue($cid, 'forms', 'delay_timeout', '5000');
|
||||
$html .= $this->buildTextInput($cid . 'DelayTimeout', 'Timeout de delay (ms)', $delayTimeout);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
// === HELPERS ===
|
||||
|
||||
private function buildSwitch(string $id, string $label, $value, string $icon = ''): string
|
||||
{
|
||||
$checked = checked($value, true, false);
|
||||
$iconHtml = $icon ? '<i class="bi ' . $icon . ' me-1" style="color: #FF8600;"></i>' : '';
|
||||
|
||||
return sprintf(
|
||||
'<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="%s" %s>
|
||||
<label class="form-check-label small" for="%s">%s%s</label>
|
||||
</div>
|
||||
</div>',
|
||||
esc_attr($id), $checked, esc_attr($id), $iconHtml, esc_html($label)
|
||||
);
|
||||
}
|
||||
|
||||
private function buildTextInput(string $id, string $label, string $value, string $placeholder = ''): string
|
||||
{
|
||||
return sprintf(
|
||||
'<div class="mb-3">
|
||||
<label for="%s" class="form-label small fw-semibold">%s</label>
|
||||
<input type="text" class="form-control form-control-sm" id="%s" value="%s" placeholder="%s">
|
||||
</div>',
|
||||
esc_attr($id), esc_html($label), esc_attr($id), esc_attr($value), esc_attr($placeholder)
|
||||
);
|
||||
}
|
||||
|
||||
private function buildTextarea(string $id, string $label, string $value, string $placeholder = ''): string
|
||||
{
|
||||
return sprintf(
|
||||
'<div class="mb-3">
|
||||
<label for="%s" class="form-label small fw-semibold">%s</label>
|
||||
<textarea class="form-control form-control-sm" id="%s" rows="2" placeholder="%s">%s</textarea>
|
||||
</div>',
|
||||
esc_attr($id), esc_html($label), esc_attr($id), esc_attr($placeholder), esc_textarea($value)
|
||||
);
|
||||
}
|
||||
|
||||
private function buildSelect(string $id, string $label, string $value, array $options): string
|
||||
{
|
||||
$optionsHtml = '';
|
||||
foreach ($options as $optValue => $optLabel) {
|
||||
$selected = selected($value, $optValue, false);
|
||||
$optionsHtml .= sprintf(
|
||||
'<option value="%s" %s>%s</option>',
|
||||
esc_attr($optValue),
|
||||
$selected,
|
||||
esc_html($optLabel)
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<div class="mb-2">
|
||||
<label for="%s" class="form-label small fw-semibold">%s</label>
|
||||
<select class="form-select form-select-sm" id="%s">%s</select>
|
||||
</div>',
|
||||
esc_attr($id), esc_html($label), esc_attr($id), $optionsHtml
|
||||
);
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, $value): string
|
||||
{
|
||||
$checked = checked($value, true, false);
|
||||
|
||||
return sprintf(
|
||||
'<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="%s" %s>
|
||||
<label class="form-check-label small" for="%s">
|
||||
<i class="bi %s me-1" style="color: #6c757d;"></i>%s
|
||||
</label>
|
||||
</div>',
|
||||
esc_attr($id),
|
||||
$checked,
|
||||
esc_attr($id),
|
||||
esc_attr($icon),
|
||||
esc_html($label)
|
||||
);
|
||||
}
|
||||
}
|
||||
41
Admin/Application/UseCases/RenderDashboardUseCase.php
Normal file
41
Admin/Application/UseCases/RenderDashboardUseCase.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Application\UseCases;
|
||||
|
||||
use ROITheme\Admin\Domain\Contracts\DashboardRendererInterface;
|
||||
|
||||
/**
|
||||
* Caso de uso para renderizar el dashboard del panel de administración
|
||||
*
|
||||
* Application - Orquestación sin lógica de negocio ni WordPress
|
||||
*/
|
||||
final class RenderDashboardUseCase
|
||||
{
|
||||
/**
|
||||
* @param DashboardRendererInterface $renderer Renderizador del dashboard
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly DashboardRendererInterface $renderer
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejecuta el caso de uso
|
||||
*
|
||||
* @param string $viewType Tipo de vista a renderizar
|
||||
* @return string HTML renderizado
|
||||
* @throws \RuntimeException Si el renderer no soporta el tipo de vista
|
||||
*/
|
||||
public function execute(string $viewType = 'dashboard'): string
|
||||
{
|
||||
if (!$this->renderer->supports($viewType)) {
|
||||
throw new \RuntimeException(
|
||||
sprintf('Renderer does not support view type: %s', $viewType)
|
||||
);
|
||||
}
|
||||
|
||||
return $this->renderer->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ArchiveHeader\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Archive Header
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class ArchiveHeaderFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'archive-header';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'archiveHeaderEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'archiveHeaderShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'archiveHeaderShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'archiveHeaderVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'archiveHeaderVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'archiveHeaderVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'archiveHeaderVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'archiveHeaderVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions)
|
||||
'archiveHeaderExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'archiveHeaderExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'archiveHeaderExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'archiveHeaderExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'archiveHeaderBlogTitle' => ['group' => 'content', 'attribute' => 'blog_title'],
|
||||
'archiveHeaderShowPostCount' => ['group' => 'content', 'attribute' => 'show_post_count'],
|
||||
'archiveHeaderShowDescription' => ['group' => 'content', 'attribute' => 'show_description'],
|
||||
'archiveHeaderCategoryPrefix' => ['group' => 'content', 'attribute' => 'category_prefix'],
|
||||
'archiveHeaderTagPrefix' => ['group' => 'content', 'attribute' => 'tag_prefix'],
|
||||
'archiveHeaderAuthorPrefix' => ['group' => 'content', 'attribute' => 'author_prefix'],
|
||||
'archiveHeaderDatePrefix' => ['group' => 'content', 'attribute' => 'date_prefix'],
|
||||
'archiveHeaderSearchPrefix' => ['group' => 'content', 'attribute' => 'search_prefix'],
|
||||
'archiveHeaderCountSingular' => ['group' => 'content', 'attribute' => 'posts_count_singular'],
|
||||
'archiveHeaderCountPlural' => ['group' => 'content', 'attribute' => 'posts_count_plural'],
|
||||
|
||||
// Typography
|
||||
'archiveHeaderHeadingLevel' => ['group' => 'typography', 'attribute' => 'heading_level'],
|
||||
'archiveHeaderTitleSize' => ['group' => 'typography', 'attribute' => 'title_size'],
|
||||
'archiveHeaderTitleWeight' => ['group' => 'typography', 'attribute' => 'title_weight'],
|
||||
'archiveHeaderDescriptionSize' => ['group' => 'typography', 'attribute' => 'description_size'],
|
||||
'archiveHeaderCountSize' => ['group' => 'typography', 'attribute' => 'count_size'],
|
||||
|
||||
// Colors
|
||||
'archiveHeaderTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'archiveHeaderPrefixColor' => ['group' => 'colors', 'attribute' => 'prefix_color'],
|
||||
'archiveHeaderDescriptionColor' => ['group' => 'colors', 'attribute' => 'description_color'],
|
||||
'archiveHeaderCountBgColor' => ['group' => 'colors', 'attribute' => 'count_bg_color'],
|
||||
'archiveHeaderCountTextColor' => ['group' => 'colors', 'attribute' => 'count_text_color'],
|
||||
|
||||
// Spacing
|
||||
'archiveHeaderMarginTop' => ['group' => 'spacing', 'attribute' => 'margin_top'],
|
||||
'archiveHeaderMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'],
|
||||
'archiveHeaderPadding' => ['group' => 'spacing', 'attribute' => 'padding'],
|
||||
'archiveHeaderTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||
'archiveHeaderCountPadding' => ['group' => 'spacing', 'attribute' => 'count_padding'],
|
||||
|
||||
// Behavior
|
||||
'archiveHeaderIsSticky' => ['group' => 'behavior', 'attribute' => 'is_sticky'],
|
||||
'archiveHeaderStickyOffset' => ['group' => 'behavior', 'attribute' => 'sticky_offset'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ArchiveHeader\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Archive Header
|
||||
*
|
||||
* @package ROITheme\Admin\ArchiveHeader\Infrastructure\Ui
|
||||
*/
|
||||
final class ArchiveHeaderFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildBehaviorGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-layout-text-window me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Cabecera de Archivo';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Cabecera dinamica para paginas de listados (blog, categorias, tags, autor, fecha, busqueda)';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="archive-header">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('archiveHeaderEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('archiveHeaderShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('archiveHeaderShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// Page visibility checkboxes
|
||||
$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', false);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', false);
|
||||
$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('archiveHeaderVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('archiveHeaderVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Exclusions
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'archiveHeader');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Blog Title
|
||||
$blogTitle = $this->renderer->getFieldValue($componentId, 'content', 'blog_title', 'Blog');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="archiveHeaderBlogTitle" class="form-label small mb-1 fw-semibold">Titulo del blog</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderBlogTitle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($blogTitle) . '">';
|
||||
$html .= ' <small class="text-muted">Mostrado en la pagina principal del blog</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switches
|
||||
$showPostCount = $this->renderer->getFieldValue($componentId, 'content', 'show_post_count', true);
|
||||
$html .= $this->buildSwitch('archiveHeaderShowPostCount', 'Mostrar contador de posts', 'bi-hash', $showPostCount);
|
||||
|
||||
$showDescription = $this->renderer->getFieldValue($componentId, 'content', 'show_description', true);
|
||||
$html .= $this->buildSwitch('archiveHeaderShowDescription', 'Mostrar descripcion', 'bi-text-paragraph', $showDescription);
|
||||
|
||||
// Prefixes section
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-tag me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Prefijos de titulo';
|
||||
$html .= ' </p>';
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
|
||||
$categoryPrefix = $this->renderer->getFieldValue($componentId, 'content', 'category_prefix', 'Categoria:');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderCategoryPrefix" class="form-label small mb-1">Categoria</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderCategoryPrefix" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($categoryPrefix) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$tagPrefix = $this->renderer->getFieldValue($componentId, 'content', 'tag_prefix', 'Etiqueta:');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderTagPrefix" class="form-label small mb-1">Etiqueta</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderTagPrefix" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($tagPrefix) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$authorPrefix = $this->renderer->getFieldValue($componentId, 'content', 'author_prefix', 'Articulos de:');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderAuthorPrefix" class="form-label small mb-1">Autor</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderAuthorPrefix" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($authorPrefix) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$datePrefix = $this->renderer->getFieldValue($componentId, 'content', 'date_prefix', 'Archivo:');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderDatePrefix" class="form-label small mb-1">Fecha</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderDatePrefix" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($datePrefix) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$searchPrefix = $this->renderer->getFieldValue($componentId, 'content', 'search_prefix', 'Resultados para:');
|
||||
$html .= ' <div class="col-12">';
|
||||
$html .= ' <label for="archiveHeaderSearchPrefix" class="form-label small mb-1">Busqueda</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderSearchPrefix" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($searchPrefix) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Post count texts
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-123 me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Textos del contador';
|
||||
$html .= ' </p>';
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
|
||||
$countSingular = $this->renderer->getFieldValue($componentId, 'content', 'posts_count_singular', 'publicacion');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderCountSingular" class="form-label small mb-1">Singular</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderCountSingular" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($countSingular) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$countPlural = $this->renderer->getFieldValue($componentId, 'content', 'posts_count_plural', 'publicaciones');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderCountPlural" class="form-label small mb-1">Plural</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderCountPlural" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($countPlural) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildBehaviorGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-gear me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Comportamiento';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$isSticky = $this->renderer->getFieldValue($componentId, 'behavior', 'is_sticky', false);
|
||||
$html .= $this->buildSwitch('archiveHeaderIsSticky', 'Header fijo al hacer scroll', 'bi-pin-angle', $isSticky);
|
||||
|
||||
$stickyOffset = $this->renderer->getFieldValue($componentId, 'behavior', 'sticky_offset', '0');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="archiveHeaderStickyOffset" class="form-label small mb-1 fw-semibold">Offset sticky</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderStickyOffset" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($stickyOffset) . '">';
|
||||
$html .= ' <small class="text-muted">Distancia desde el top cuando es sticky (ej: 60px)</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Heading level
|
||||
$headingLevel = $this->renderer->getFieldValue($componentId, 'typography', 'heading_level', 'h1');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="archiveHeaderHeadingLevel" class="form-label small mb-1 fw-semibold">Nivel de encabezado</label>';
|
||||
$html .= ' <select id="archiveHeaderHeadingLevel" class="form-select form-select-sm">';
|
||||
foreach (['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as $level) {
|
||||
$selected = $headingLevel === $level ? ' selected' : '';
|
||||
$html .= sprintf(' <option value="%s"%s>%s</option>', $level, $selected, strtoupper($level));
|
||||
}
|
||||
$html .= ' </select>';
|
||||
$html .= ' <small class="text-muted">Importante para SEO y accesibilidad</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$titleSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_size', '2rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderTitleSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderTitleSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_weight', '700');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderTitleWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderTitleWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$descriptionSize = $this->renderer->getFieldValue($componentId, 'typography', 'description_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderDescriptionSize" class="form-label small mb-1 fw-semibold">Tamano descripcion</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderDescriptionSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($descriptionSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$countSize = $this->renderer->getFieldValue($componentId, 'typography', 'count_size', '0.875rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderCountSize" class="form-label small mb-1 fw-semibold">Tamano contador</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderCountSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($countSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('archiveHeaderTitleColor', 'Titulo', $titleColor);
|
||||
|
||||
$prefixColor = $this->renderer->getFieldValue($componentId, 'colors', 'prefix_color', '#6b7280');
|
||||
$html .= $this->buildColorPicker('archiveHeaderPrefixColor', 'Prefijo', $prefixColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$descriptionColor = $this->renderer->getFieldValue($componentId, 'colors', 'description_color', '#6b7280');
|
||||
$html .= $this->buildColorPicker('archiveHeaderDescriptionColor', 'Descripcion', $descriptionColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <p class="small fw-semibold mb-2">Contador de posts</p>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$countBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'count_bg_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('archiveHeaderCountBgColor', 'Fondo', $countBgColor);
|
||||
|
||||
$countTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'count_text_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('archiveHeaderCountTextColor', 'Texto', $countTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_top', '2rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '2rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$padding = $this->renderer->getFieldValue($componentId, 'spacing', 'padding', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderPadding" class="form-label small mb-1 fw-semibold">Padding</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($padding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="archiveHeaderTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderTitleMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="mb-0">';
|
||||
$countPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'count_padding', '0.25rem 0.75rem');
|
||||
$html .= ' <label for="archiveHeaderCountPadding" class="form-label small mb-1 fw-semibold">Padding contador</label>';
|
||||
$html .= ' <input type="text" id="archiveHeaderCountPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($countPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ContactForm\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Contact Form
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class ContactFormFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'contact-form';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'contactFormEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'contactFormShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'contactFormShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'contactFormVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'contactFormVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'contactFormVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'contactFormVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'contactFormVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'contactFormExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'contactFormExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'contactFormExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'contactFormExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'contactFormSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'],
|
||||
'contactFormSectionDescription' => ['group' => 'content', 'attribute' => 'section_description'],
|
||||
'contactFormSubmitButtonText' => ['group' => 'content', 'attribute' => 'submit_button_text'],
|
||||
'contactFormSubmitButtonIcon' => ['group' => 'content', 'attribute' => 'submit_button_icon'],
|
||||
|
||||
// Contact Info
|
||||
'contactFormShowContactInfo' => ['group' => 'contact_info', 'attribute' => 'show_contact_info'],
|
||||
'contactFormPhoneLabel' => ['group' => 'contact_info', 'attribute' => 'phone_label'],
|
||||
'contactFormPhoneValue' => ['group' => 'contact_info', 'attribute' => 'phone_value'],
|
||||
'contactFormEmailLabel' => ['group' => 'contact_info', 'attribute' => 'email_label'],
|
||||
'contactFormEmailValue' => ['group' => 'contact_info', 'attribute' => 'email_value'],
|
||||
'contactFormLocationLabel' => ['group' => 'contact_info', 'attribute' => 'location_label'],
|
||||
'contactFormLocationValue' => ['group' => 'contact_info', 'attribute' => 'location_value'],
|
||||
|
||||
// Form Labels
|
||||
'contactFormFullnamePlaceholder' => ['group' => 'form_labels', 'attribute' => 'fullname_placeholder'],
|
||||
'contactFormCompanyPlaceholder' => ['group' => 'form_labels', 'attribute' => 'company_placeholder'],
|
||||
'contactFormWhatsappPlaceholder' => ['group' => 'form_labels', 'attribute' => 'whatsapp_placeholder'],
|
||||
'contactFormEmailPlaceholder' => ['group' => 'form_labels', 'attribute' => 'email_placeholder'],
|
||||
'contactFormMessagePlaceholder' => ['group' => 'form_labels', 'attribute' => 'message_placeholder'],
|
||||
|
||||
// Integration
|
||||
'contactFormWebhookUrl' => ['group' => 'integration', 'attribute' => 'webhook_url'],
|
||||
'contactFormWebhookMethod' => ['group' => 'integration', 'attribute' => 'webhook_method'],
|
||||
'contactFormIncludePageUrl' => ['group' => 'integration', 'attribute' => 'include_page_url'],
|
||||
'contactFormIncludeTimestamp' => ['group' => 'integration', 'attribute' => 'include_timestamp'],
|
||||
|
||||
// Messages
|
||||
'contactFormSuccessMessage' => ['group' => 'messages', 'attribute' => 'success_message'],
|
||||
'contactFormErrorMessage' => ['group' => 'messages', 'attribute' => 'error_message'],
|
||||
'contactFormSendingMessage' => ['group' => 'messages', 'attribute' => 'sending_message'],
|
||||
'contactFormValidationRequired' => ['group' => 'messages', 'attribute' => 'validation_required'],
|
||||
'contactFormValidationEmail' => ['group' => 'messages', 'attribute' => 'validation_email'],
|
||||
|
||||
// Colors
|
||||
'contactFormSectionBgColor' => ['group' => 'colors', 'attribute' => 'section_bg_color'],
|
||||
'contactFormTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'contactFormDescriptionColor' => ['group' => 'colors', 'attribute' => 'description_color'],
|
||||
'contactFormIconColor' => ['group' => 'colors', 'attribute' => 'icon_color'],
|
||||
'contactFormInfoLabelColor' => ['group' => 'colors', 'attribute' => 'info_label_color'],
|
||||
'contactFormInfoValueColor' => ['group' => 'colors', 'attribute' => 'info_value_color'],
|
||||
'contactFormInputBorderColor' => ['group' => 'colors', 'attribute' => 'input_border_color'],
|
||||
'contactFormInputFocusBorder' => ['group' => 'colors', 'attribute' => 'input_focus_border'],
|
||||
'contactFormButtonBgColor' => ['group' => 'colors', 'attribute' => 'button_bg_color'],
|
||||
'contactFormButtonTextColor' => ['group' => 'colors', 'attribute' => 'button_text_color'],
|
||||
'contactFormButtonHoverBg' => ['group' => 'colors', 'attribute' => 'button_hover_bg'],
|
||||
'contactFormSuccessBgColor' => ['group' => 'colors', 'attribute' => 'success_bg_color'],
|
||||
'contactFormSuccessTextColor' => ['group' => 'colors', 'attribute' => 'success_text_color'],
|
||||
'contactFormErrorBgColor' => ['group' => 'colors', 'attribute' => 'error_bg_color'],
|
||||
'contactFormErrorTextColor' => ['group' => 'colors', 'attribute' => 'error_text_color'],
|
||||
|
||||
// Spacing
|
||||
'contactFormSectionPaddingY' => ['group' => 'spacing', 'attribute' => 'section_padding_y'],
|
||||
'contactFormSectionMarginTop' => ['group' => 'spacing', 'attribute' => 'section_margin_top'],
|
||||
'contactFormTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||
'contactFormDescriptionMarginBottom' => ['group' => 'spacing', 'attribute' => 'description_margin_bottom'],
|
||||
'contactFormFormGap' => ['group' => 'spacing', 'attribute' => 'form_gap'],
|
||||
|
||||
// Visual Effects
|
||||
'contactFormInputBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'input_border_radius'],
|
||||
'contactFormButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
|
||||
'contactFormButtonPadding' => ['group' => 'visual_effects', 'attribute' => 'button_padding'],
|
||||
'contactFormTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
'contactFormTextareaRows' => ['group' => 'visual_effects', 'attribute' => 'textarea_rows'],
|
||||
];
|
||||
}
|
||||
}
|
||||
652
Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php
Normal file
652
Admin/ContactForm/Infrastructure/Ui/ContactFormFormBuilder.php
Normal file
@@ -0,0 +1,652 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ContactForm\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Contact Form
|
||||
*
|
||||
* RESPONSABILIDAD: Generar formulario de configuracion del Contact Form
|
||||
*
|
||||
* SEGURIDAD: El webhook_url se muestra como input type="password" para evitar
|
||||
* que sea visible accidentalmente en pantalla compartida.
|
||||
*
|
||||
* @package ROITheme\Admin\ContactForm\Infrastructure\Ui
|
||||
*/
|
||||
final class ContactFormFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildContactInfoGroup($componentId);
|
||||
$html .= $this->buildFormLabelsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildIntegrationGroup($componentId);
|
||||
$html .= $this->buildMessagesGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-envelope-paper me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Formulario de Contacto';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Seccion de contacto antes del footer con envio a webhook';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="contact-form">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('contactFormEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('contactFormShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('contactFormShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'contactForm');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$sectionTitle = $this->renderer->getFieldValue($componentId, 'content', 'section_title', '¿Tienes alguna pregunta?');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormSectionTitle" class="form-label small mb-1 fw-semibold">Titulo de seccion</label>';
|
||||
$html .= ' <input type="text" id="contactFormSectionTitle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionTitle) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$sectionDescription = $this->renderer->getFieldValue($componentId, 'content', 'section_description', 'Completa el formulario y nuestro equipo te responderá en menos de 24 horas.');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormSectionDescription" class="form-label small mb-1 fw-semibold">Descripcion</label>';
|
||||
$html .= ' <textarea id="contactFormSectionDescription" class="form-control form-control-sm" rows="2">';
|
||||
$html .= esc_textarea($sectionDescription);
|
||||
$html .= '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$submitButtonText = $this->renderer->getFieldValue($componentId, 'content', 'submit_button_text', 'Enviar Mensaje');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormSubmitButtonText" class="form-label small mb-1 fw-semibold">Texto boton enviar</label>';
|
||||
$html .= ' <input type="text" id="contactFormSubmitButtonText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($submitButtonText) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$submitButtonIcon = $this->renderer->getFieldValue($componentId, 'content', 'submit_button_icon', 'bi-send-fill');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="contactFormSubmitButtonIcon" class="form-label small mb-1 fw-semibold">Icono boton (Bootstrap Icons)</label>';
|
||||
$html .= ' <input type="text" id="contactFormSubmitButtonIcon" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($submitButtonIcon) . '" placeholder="bi-send-fill">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContactInfoGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-person-lines-fill me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Info de Contacto';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$showContactInfo = $this->renderer->getFieldValue($componentId, 'contact_info', 'show_contact_info', true);
|
||||
$html .= $this->buildSwitch('contactFormShowContactInfo', 'Mostrar info contacto', 'bi-eye', $showContactInfo);
|
||||
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">Telefono</p>';
|
||||
|
||||
$phoneLabel = $this->renderer->getFieldValue($componentId, 'contact_info', 'phone_label', 'Teléfono');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <input type="text" id="contactFormPhoneLabel" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($phoneLabel) . '" placeholder="Label">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$phoneValue = $this->renderer->getFieldValue($componentId, 'contact_info', 'phone_value', '+52 55 1234 5678');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <input type="text" id="contactFormPhoneValue" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($phoneValue) . '" placeholder="Numero">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <p class="small fw-semibold mb-2">Email</p>';
|
||||
|
||||
$emailLabel = $this->renderer->getFieldValue($componentId, 'contact_info', 'email_label', 'Email');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <input type="text" id="contactFormEmailLabel" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($emailLabel) . '" placeholder="Label">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$emailValue = $this->renderer->getFieldValue($componentId, 'contact_info', 'email_value', 'contacto@apumexico.com');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <input type="email" id="contactFormEmailValue" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($emailValue) . '" placeholder="Direccion">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <p class="small fw-semibold mb-2">Ubicacion</p>';
|
||||
|
||||
$locationLabel = $this->renderer->getFieldValue($componentId, 'contact_info', 'location_label', 'Ubicación');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <input type="text" id="contactFormLocationLabel" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($locationLabel) . '" placeholder="Label">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$locationValue = $this->renderer->getFieldValue($componentId, 'contact_info', 'location_value', 'Ciudad de México, México');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <input type="text" id="contactFormLocationValue" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($locationValue) . '" placeholder="Direccion">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildFormLabelsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-input-cursor-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Labels del Formulario';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$fullnamePlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'fullname_placeholder', 'Nombre completo *');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="contactFormFullnamePlaceholder" class="form-label small mb-1 fw-semibold">Placeholder nombre</label>';
|
||||
$html .= ' <input type="text" id="contactFormFullnamePlaceholder" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($fullnamePlaceholder) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$companyPlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'company_placeholder', 'Empresa');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="contactFormCompanyPlaceholder" class="form-label small mb-1 fw-semibold">Placeholder empresa</label>';
|
||||
$html .= ' <input type="text" id="contactFormCompanyPlaceholder" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($companyPlaceholder) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$whatsappPlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'whatsapp_placeholder', 'WhatsApp *');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="contactFormWhatsappPlaceholder" class="form-label small mb-1 fw-semibold">Placeholder WhatsApp</label>';
|
||||
$html .= ' <input type="text" id="contactFormWhatsappPlaceholder" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($whatsappPlaceholder) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$emailPlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'email_placeholder', 'Correo electrónico *');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="contactFormEmailPlaceholder" class="form-label small mb-1 fw-semibold">Placeholder email</label>';
|
||||
$html .= ' <input type="text" id="contactFormEmailPlaceholder" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($emailPlaceholder) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$messagePlaceholder = $this->renderer->getFieldValue($componentId, 'form_labels', 'message_placeholder', '¿En qué podemos ayudarte?');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="contactFormMessagePlaceholder" class="form-label small mb-1 fw-semibold">Placeholder mensaje</label>';
|
||||
$html .= ' <input type="text" id="contactFormMessagePlaceholder" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($messagePlaceholder) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildIntegrationGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-link-45deg me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Integracion Webhook';
|
||||
$html .= ' <span class="badge bg-warning text-dark ms-2">Privado</span>';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="alert alert-info py-2 small mb-3">';
|
||||
$html .= ' <i class="bi bi-shield-lock me-1"></i>';
|
||||
$html .= ' El webhook URL nunca se expone en el frontend. Los datos se envian de forma segura desde el servidor.';
|
||||
$html .= ' </div>';
|
||||
|
||||
$webhookUrl = $this->renderer->getFieldValue($componentId, 'integration', 'webhook_url', '');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormWebhookUrl" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-link me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' URL del Webhook';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea id="contactFormWebhookUrl" class="form-control form-control-sm" rows="2" ';
|
||||
$html .= ' placeholder="https://tu-webhook.com/endpoint">';
|
||||
$html .= esc_textarea($webhookUrl);
|
||||
$html .= '</textarea>';
|
||||
$html .= ' <small class="text-muted">Deja vacio si no deseas enviar a un webhook externo.</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$webhookMethod = $this->renderer->getFieldValue($componentId, 'integration', 'webhook_method', 'POST');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormWebhookMethod" class="form-label small mb-1 fw-semibold">Metodo HTTP</label>';
|
||||
$html .= ' <select id="contactFormWebhookMethod" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="POST"' . ($webhookMethod === 'POST' ? ' selected' : '') . '>POST</option>';
|
||||
$html .= ' <option value="GET"' . ($webhookMethod === 'GET' ? ' selected' : '') . '>GET</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$includePageUrl = $this->renderer->getFieldValue($componentId, 'integration', 'include_page_url', true);
|
||||
$html .= $this->buildSwitch('contactFormIncludePageUrl', 'Incluir URL de pagina', 'bi-link', $includePageUrl);
|
||||
|
||||
$includeTimestamp = $this->renderer->getFieldValue($componentId, 'integration', 'include_timestamp', true);
|
||||
$html .= $this->buildSwitch('contactFormIncludeTimestamp', 'Incluir timestamp', 'bi-clock', $includeTimestamp);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildMessagesGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-chat-quote me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mensajes';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$successMessage = $this->renderer->getFieldValue($componentId, 'messages', 'success_message', '¡Gracias por contactarnos! Te responderemos pronto.');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormSuccessMessage" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-check-circle me-1 text-success"></i>';
|
||||
$html .= ' Mensaje de exito';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea id="contactFormSuccessMessage" class="form-control form-control-sm" rows="2">';
|
||||
$html .= esc_textarea($successMessage);
|
||||
$html .= '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$errorMessage = $this->renderer->getFieldValue($componentId, 'messages', 'error_message', 'Hubo un error al enviar el mensaje. Por favor intenta de nuevo.');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormErrorMessage" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-x-circle me-1 text-danger"></i>';
|
||||
$html .= ' Mensaje de error';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea id="contactFormErrorMessage" class="form-control form-control-sm" rows="2">';
|
||||
$html .= esc_textarea($errorMessage);
|
||||
$html .= '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$sendingMessage = $this->renderer->getFieldValue($componentId, 'messages', 'sending_message', 'Enviando...');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="contactFormSendingMessage" class="form-label small mb-1 fw-semibold">Mensaje enviando</label>';
|
||||
$html .= ' <input type="text" id="contactFormSendingMessage" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sendingMessage) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$validationRequired = $this->renderer->getFieldValue($componentId, 'messages', 'validation_required', 'Este campo es obligatorio');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="contactFormValidationRequired" class="form-label small mb-1 fw-semibold">Error campo requerido</label>';
|
||||
$html .= ' <input type="text" id="contactFormValidationRequired" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($validationRequired) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$validationEmail = $this->renderer->getFieldValue($componentId, 'messages', 'validation_email', 'Por favor ingresa un email válido');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="contactFormValidationEmail" class="form-label small mb-1 fw-semibold">Error email invalido</label>';
|
||||
$html .= ' <input type="text" id="contactFormValidationEmail" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($validationEmail) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Seccion
|
||||
$html .= ' <p class="small fw-semibold mb-2">Seccion</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$sectionBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'section_bg_color', 'rgba(108, 117, 125, 0.25)');
|
||||
$html .= $this->buildColorPicker('contactFormSectionBgColor', 'Fondo seccion', $sectionBgColor);
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#212529');
|
||||
$html .= $this->buildColorPicker('contactFormTitleColor', 'Color titulo', $titleColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Boton
|
||||
$html .= ' <p class="small fw-semibold mb-2">Boton</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_bg_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('contactFormButtonBgColor', 'Fondo boton', $buttonBgColor);
|
||||
|
||||
$buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('contactFormButtonTextColor', 'Texto boton', $buttonTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_bg', '#e67a00');
|
||||
$html .= $this->buildColorPicker('contactFormButtonHoverBg', 'Hover boton', $buttonHoverBg);
|
||||
|
||||
$iconColor = $this->renderer->getFieldValue($componentId, 'colors', 'icon_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('contactFormIconColor', 'Iconos', $iconColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Mensajes
|
||||
$html .= ' <p class="small fw-semibold mb-2">Mensajes</p>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$successBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'success_bg_color', '#d1e7dd');
|
||||
$html .= $this->buildColorPicker('contactFormSuccessBgColor', 'Fondo exito', $successBgColor);
|
||||
|
||||
$errorBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'error_bg_color', '#f8d7da');
|
||||
$html .= $this->buildColorPicker('contactFormErrorBgColor', 'Fondo error', $errorBgColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$sectionPaddingY = $this->renderer->getFieldValue($componentId, 'spacing', 'section_padding_y', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormSectionPaddingY" class="form-label small mb-1 fw-semibold">Padding vertical</label>';
|
||||
$html .= ' <input type="text" id="contactFormSectionPaddingY" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionPaddingY) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$sectionMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_top', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormSectionMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
|
||||
$html .= ' <input type="text" id="contactFormSectionMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionMarginTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.75rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||
$html .= ' <input type="text" id="contactFormTitleMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$formGap = $this->renderer->getFieldValue($componentId, 'spacing', 'form_gap', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormFormGap" class="form-label small mb-1 fw-semibold">Espacio campos</label>';
|
||||
$html .= ' <input type="text" id="contactFormFormGap" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($formGap) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$inputBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'input_border_radius', '6px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormInputBorderRadius" class="form-label small mb-1 fw-semibold">Radio inputs</label>';
|
||||
$html .= ' <input type="text" id="contactFormInputBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($inputBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$buttonBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '6px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormButtonBorderRadius" class="form-label small mb-1 fw-semibold">Radio boton</label>';
|
||||
$html .= ' <input type="text" id="contactFormButtonBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonPadding = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_padding', '0.75rem 2rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormButtonPadding" class="form-label small mb-1 fw-semibold">Padding boton</label>';
|
||||
$html .= ' <input type="text" id="contactFormButtonPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="contactFormTransitionDuration" class="form-label small mb-1 fw-semibold">Duracion transicion</label>';
|
||||
$html .= ' <input type="text" id="contactFormTransitionDuration" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$textareaRows = $this->renderer->getFieldValue($componentId, 'visual_effects', 'textarea_rows', '4');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="contactFormTextareaRows" class="form-label small mb-1 fw-semibold">Filas textarea</label>';
|
||||
$html .= ' <input type="number" id="contactFormTextareaRows" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($textareaRows) . '" min="2" max="10">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
// Manejar colores rgba
|
||||
$colorValue = $value;
|
||||
if (strpos($value, 'rgba') === 0 || strpos($value, 'rgb') === 0) {
|
||||
// Para rgba usamos un color aproximado en el picker
|
||||
$colorValue = '#6c757d';
|
||||
}
|
||||
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($colorValue)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <input type="text" class="form-control" id="%sText" value="%s" style="font-size: 0.75rem;">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaBoxSidebar\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para CTA Box Sidebar
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*
|
||||
* UBICACION:
|
||||
* - Dentro del modulo CtaBoxSidebar (autocontenido)
|
||||
* - Eliminar modulo = eliminar mapper
|
||||
*/
|
||||
final class CtaBoxSidebarFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'cta-box-sidebar';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'ctaEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'ctaVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'ctaVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'ctaVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'ctaVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'ctaVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'ctaExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'ctaExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'ctaExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'ctaExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'ctaTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||
'ctaDescription' => ['group' => 'content', 'attribute' => 'description'],
|
||||
'ctaButtonText' => ['group' => 'content', 'attribute' => 'button_text'],
|
||||
'ctaButtonIcon' => ['group' => 'content', 'attribute' => 'button_icon'],
|
||||
'ctaButtonAction' => ['group' => 'content', 'attribute' => 'button_action'],
|
||||
'ctaButtonLink' => ['group' => 'content', 'attribute' => 'button_link'],
|
||||
|
||||
// Behavior
|
||||
'ctaTextAlign' => ['group' => 'behavior', 'attribute' => 'text_align'],
|
||||
|
||||
// Typography
|
||||
'ctaTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'],
|
||||
'ctaTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'],
|
||||
'ctaDescFontSize' => ['group' => 'typography', 'attribute' => 'description_font_size'],
|
||||
'ctaButtonFontSize' => ['group' => 'typography', 'attribute' => 'button_font_size'],
|
||||
'ctaButtonFontWeight' => ['group' => 'typography', 'attribute' => 'button_font_weight'],
|
||||
|
||||
// Colors
|
||||
'ctaBackgroundColor' => ['group' => 'colors', 'attribute' => 'background_color'],
|
||||
'ctaTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'ctaDescriptionColor' => ['group' => 'colors', 'attribute' => 'description_color'],
|
||||
'ctaButtonBgColor' => ['group' => 'colors', 'attribute' => 'button_background_color'],
|
||||
'ctaButtonTextColor' => ['group' => 'colors', 'attribute' => 'button_text_color'],
|
||||
'ctaButtonHoverBg' => ['group' => 'colors', 'attribute' => 'button_hover_background'],
|
||||
'ctaButtonHoverText' => ['group' => 'colors', 'attribute' => 'button_hover_text_color'],
|
||||
|
||||
// Spacing
|
||||
'ctaContainerPadding' => ['group' => 'spacing', 'attribute' => 'container_padding'],
|
||||
'ctaTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||
'ctaDescMarginBottom' => ['group' => 'spacing', 'attribute' => 'description_margin_bottom'],
|
||||
'ctaButtonPadding' => ['group' => 'spacing', 'attribute' => 'button_padding'],
|
||||
'ctaIconMarginRight' => ['group' => 'spacing', 'attribute' => 'icon_margin_right'],
|
||||
|
||||
// Visual Effects
|
||||
'ctaBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'ctaButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
|
||||
'ctaBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
'ctaTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaBoxSidebar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para el CTA Box Sidebar
|
||||
*
|
||||
* Responsabilidad:
|
||||
* - Generar HTML del formulario de configuracion
|
||||
* - Usar Design System (Bootstrap 5)
|
||||
* - Cargar valores desde BD via AdminDashboardRenderer
|
||||
*
|
||||
* @package ROITheme\Admin\CtaBoxSidebar\Infrastructure\Ui
|
||||
*/
|
||||
final class CtaBoxSidebarFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildBehaviorGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-megaphone me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de CTA Box Sidebar';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Caja de llamada a la accion en el sidebar';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="cta-box-sidebar">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// is_enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('ctaEnabled', 'Activar CTA box', 'bi-power', $enabled);
|
||||
|
||||
// show_on_desktop
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('ctaShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
// show_on_mobile
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
||||
$html .= $this->buildSwitch('ctaShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
// Obtener valores de _page_visibility (grupo especial)
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', true);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
// Grid 3 columnas según Design System
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'cta');
|
||||
|
||||
// Switch: Ocultar para usuarios logueados (Plan 99.16)
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($componentId, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaHideForLoggedIn" ';
|
||||
$html .= checked($hideForLoggedIn, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaHideForLoggedIn" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-person-lock me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Ocultar para usuarios logueados</strong>';
|
||||
$html .= ' <small class="text-muted d-block">No mostrar a usuarios con sesión iniciada</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// title
|
||||
$title = $this->renderer->getFieldValue($componentId, 'content', 'title', '¿Listo para potenciar tus proyectos?');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaTitle" class="form-label small mb-1 fw-semibold">Titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaTitle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($title) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// description
|
||||
$description = $this->renderer->getFieldValue($componentId, 'content', 'description', 'Accede a nuestra biblioteca completa de APUs y herramientas profesionales.');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaDescription" class="form-label small mb-1 fw-semibold">Descripcion</label>';
|
||||
$html .= ' <textarea id="ctaDescription" class="form-control form-control-sm" rows="2">' . esc_textarea($description) . '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_text
|
||||
$buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', 'Solicitar Demo');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaButtonText" class="form-label small mb-1 fw-semibold">Texto del boton</label>';
|
||||
$html .= ' <input type="text" id="ctaButtonText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonText) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_icon
|
||||
$buttonIcon = $this->renderer->getFieldValue($componentId, 'content', 'button_icon', 'bi bi-calendar-check');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaButtonIcon" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-stars me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Icono del boton';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="ctaButtonIcon" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonIcon) . '" placeholder="ej: bi bi-calendar-check">';
|
||||
$html .= ' <small class="text-muted">Clase de Bootstrap Icons</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_action
|
||||
$buttonAction = $this->renderer->getFieldValue($componentId, 'content', 'button_action', 'modal');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaButtonAction" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-cursor me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Accion del boton';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="ctaButtonAction" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="modal"' . ($buttonAction === 'modal' ? ' selected' : '') . '>Abrir modal</option>';
|
||||
$html .= ' <option value="link"' . ($buttonAction === 'link' ? ' selected' : '') . '>Ir a URL</option>';
|
||||
$html .= ' <option value="scroll"' . ($buttonAction === 'scroll' ? ' selected' : '') . '>Scroll a seccion</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_link
|
||||
$buttonLink = $this->renderer->getFieldValue($componentId, 'content', 'button_link', '#contactModal');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaButtonLink" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' URL/ID destino';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="ctaButtonLink" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonLink) . '" placeholder="ej: #contactModal o https://...">';
|
||||
$html .= ' <small class="text-muted">Para modal usa #nombreModal, para scroll usa #idSeccion</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildBehaviorGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-sliders me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Comportamiento';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// text_align
|
||||
$textAlign = $this->renderer->getFieldValue($componentId, 'behavior', 'text_align', 'center');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaTextAlign" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-text-center me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Alineacion del texto';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="ctaTextAlign" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="left"' . ($textAlign === 'left' ? ' selected' : '') . '>Izquierda</option>';
|
||||
$html .= ' <option value="center"' . ($textAlign === 'center' ? ' selected' : '') . '>Centro</option>';
|
||||
$html .= ' <option value="right"' . ($textAlign === 'right' ? ' selected' : '') . '>Derecha</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// title_font_size
|
||||
$titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '1.25rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaTitleFontSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaTitleFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// title_font_weight
|
||||
$titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '700');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaTitleFontWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaTitleFontWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// description_font_size
|
||||
$descFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'description_font_size', '0.9rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaDescFontSize" class="form-label small mb-1 fw-semibold">Tamano descripcion</label>';
|
||||
$html .= ' <input type="text" id="ctaDescFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($descFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_font_size
|
||||
$buttonFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'button_font_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaButtonFontSize" class="form-label small mb-1 fw-semibold">Tamano boton</label>';
|
||||
$html .= ' <input type="text" id="ctaButtonFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// button_font_weight
|
||||
$buttonFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'button_font_weight', '700');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaButtonFontWeight" class="form-label small mb-1 fw-semibold">Peso boton</label>';
|
||||
$html .= ' <input type="text" id="ctaButtonFontWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonFontWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Colores principales
|
||||
$html .= ' <p class="small fw-semibold mb-2">Contenedor</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('ctaBackgroundColor', 'Fondo', $bgColor);
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaTitleColor', 'Titulo', $titleColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$descColor = $this->renderer->getFieldValue($componentId, 'colors', 'description_color', 'rgba(255, 255, 255, 0.95)');
|
||||
$html .= $this->buildColorPicker('ctaDescriptionColor', 'Descripcion', $descColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores del boton
|
||||
$html .= ' <p class="small fw-semibold mb-2">Boton</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_background_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaButtonBgColor', 'Fondo', $buttonBgColor);
|
||||
|
||||
$buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('ctaButtonTextColor', 'Texto', $buttonTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores hover
|
||||
$html .= ' <p class="small fw-semibold mb-2">Boton Hover</p>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_background', '#0E2337');
|
||||
$html .= $this->buildColorPicker('ctaButtonHoverBg', 'Fondo hover', $buttonHoverBg);
|
||||
|
||||
$buttonHoverText = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_text_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaButtonHoverText', 'Texto hover', $buttonHoverText);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// container_padding
|
||||
$containerPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding', '24px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaContainerPadding" class="form-label small mb-1 fw-semibold">Padding contenedor</label>';
|
||||
$html .= ' <input type="text" id="ctaContainerPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// title_margin_bottom
|
||||
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaTitleMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// description_margin_bottom
|
||||
$descMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'description_margin_bottom', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaDescMarginBottom" class="form-label small mb-1 fw-semibold">Margen descripcion</label>';
|
||||
$html .= ' <input type="text" id="ctaDescMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($descMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_padding
|
||||
$buttonPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'button_padding', '0.75rem 1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaButtonPadding" class="form-label small mb-1 fw-semibold">Padding boton</label>';
|
||||
$html .= ' <input type="text" id="ctaButtonPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// icon_margin_right
|
||||
$iconMarginRight = $this->renderer->getFieldValue($componentId, 'spacing', 'icon_margin_right', '0.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaIconMarginRight" class="form-label small mb-1 fw-semibold">Margen icono</label>';
|
||||
$html .= ' <input type="text" id="ctaIconMarginRight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($iconMarginRight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// border_radius
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '8px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaBorderRadius" class="form-label small mb-1 fw-semibold">Radio contenedor</label>';
|
||||
$html .= ' <input type="text" id="ctaBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_border_radius
|
||||
$buttonBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '8px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaButtonBorderRadius" class="form-label small mb-1 fw-semibold">Radio boton</label>';
|
||||
$html .= ' <input type="text" id="ctaButtonBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// box_shadow
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 4px 12px rgba(255, 133, 0, 0.2)');
|
||||
$html .= ' <div class="col-12">';
|
||||
$html .= ' <label for="ctaBoxShadow" class="form-label small mb-1 fw-semibold">Sombra</label>';
|
||||
$html .= ' <input type="text" id="ctaBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mt-3 mb-0">';
|
||||
|
||||
// transition_duration
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaTransitionDuration" class="form-label small mb-1 fw-semibold">Duracion transicion</label>';
|
||||
$html .= ' <input type="text" id="ctaTransitionDuration" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, bool $checked): string
|
||||
{
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera un checkbox de visibilidad por tipo de pagina
|
||||
*
|
||||
* Sigue Design System: form-check-checkbox es obligatorio
|
||||
*/
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, bool $checked): string
|
||||
{
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaLetsTalk\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para CTA Lets Talk
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class CtaLetsTalkFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'cta-lets-talk';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'ctaLetsTalkEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaLetsTalkShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaLetsTalkShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaLetsTalkHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'ctaLetsTalkVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'ctaLetsTalkVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'ctaLetsTalkVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'ctaLetsTalkVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'ctaLetsTalkVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'letsTalkExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'letsTalkExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'letsTalkExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'letsTalkExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'ctaLetsTalkButtonText' => ['group' => 'content', 'attribute' => 'button_text'],
|
||||
'ctaLetsTalkShowIcon' => ['group' => 'content', 'attribute' => 'show_icon'],
|
||||
'ctaLetsTalkIconClass' => ['group' => 'content', 'attribute' => 'icon_class'],
|
||||
'ctaLetsTalkModalTarget' => ['group' => 'content', 'attribute' => 'modal_target'],
|
||||
'ctaLetsTalkAriaLabel' => ['group' => 'content', 'attribute' => 'aria_label'],
|
||||
|
||||
// Behavior
|
||||
'ctaLetsTalkEnableModal' => ['group' => 'behavior', 'attribute' => 'enable_modal'],
|
||||
'ctaLetsTalkCustomUrl' => ['group' => 'behavior', 'attribute' => 'custom_url'],
|
||||
'ctaLetsTalkOpenNewTab' => ['group' => 'behavior', 'attribute' => 'open_in_new_tab'],
|
||||
|
||||
// Typography
|
||||
'ctaLetsTalkFontSize' => ['group' => 'typography', 'attribute' => 'font_size'],
|
||||
'ctaLetsTalkFontWeight' => ['group' => 'typography', 'attribute' => 'font_weight'],
|
||||
'ctaLetsTalkTextTransform' => ['group' => 'typography', 'attribute' => 'text_transform'],
|
||||
|
||||
// Colors
|
||||
'ctaLetsTalkBgColor' => ['group' => 'colors', 'attribute' => 'background_color'],
|
||||
'ctaLetsTalkBgHoverColor' => ['group' => 'colors', 'attribute' => 'background_hover_color'],
|
||||
'ctaLetsTalkTextColor' => ['group' => 'colors', 'attribute' => 'text_color'],
|
||||
'ctaLetsTalkTextHoverColor' => ['group' => 'colors', 'attribute' => 'text_hover_color'],
|
||||
'ctaLetsTalkBorderColor' => ['group' => 'colors', 'attribute' => 'border_color'],
|
||||
|
||||
// Spacing
|
||||
'ctaLetsTalkPaddingTB' => ['group' => 'spacing', 'attribute' => 'padding_top_bottom'],
|
||||
'ctaLetsTalkPaddingLR' => ['group' => 'spacing', 'attribute' => 'padding_left_right'],
|
||||
'ctaLetsTalkMarginLeft' => ['group' => 'spacing', 'attribute' => 'margin_left'],
|
||||
'ctaLetsTalkIconSpacing' => ['group' => 'spacing', 'attribute' => 'icon_spacing'],
|
||||
|
||||
// Visual Effects
|
||||
'ctaLetsTalkBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'ctaLetsTalkBorderWidth' => ['group' => 'visual_effects', 'attribute' => 'border_width'],
|
||||
'ctaLetsTalkBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
'ctaLetsTalkTransition' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
];
|
||||
}
|
||||
}
|
||||
530
Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php
Normal file
530
Admin/CtaLetsTalk/Infrastructure/Ui/CtaLetsTalkFormBuilder.php
Normal file
@@ -0,0 +1,530 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaLetsTalk\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* Class CtaLetsTalkFormBuilder
|
||||
*
|
||||
* Genera el formulario de administración para el componente CTA "Let's Talk".
|
||||
*
|
||||
* Responsabilidades:
|
||||
* - Renderizar formulario de configuración del botón CTA
|
||||
* - Organizar campos en grupos según el schema JSON
|
||||
* - Aplicar Design System (gradiente navy, borde orange)
|
||||
* - Usar Bootstrap 5 form controls
|
||||
*
|
||||
* @package ROITheme\Admin\CtaLetsTalk\Infrastructure\Ui
|
||||
*/
|
||||
final class CtaLetsTalkFormBuilder
|
||||
{
|
||||
private const COMPONENT_ID = 'cta-lets-talk';
|
||||
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
// Header
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
// Layout 2 columnas
|
||||
$html .= '<div class="row g-3">';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildBehaviorGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildVisualEffectsGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-lightning-charge-fill me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuración del Botón "Let\'s Talk"';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Personaliza el botón CTA principal del navbar';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="cta-lets-talk">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Switch: Enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkEnabled" name="visibility[is_enabled]" ';
|
||||
$html .= checked($enabled, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkEnabled">';
|
||||
$html .= ' <strong>Mostrar botón Let\'s Talk</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Desktop
|
||||
$showDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkShowDesktop" name="visibility[show_on_desktop]" ';
|
||||
$html .= checked($showDesktop, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkShowDesktop">';
|
||||
$html .= ' <strong>Mostrar en escritorio</strong> <span class="text-muted">(≥992px)</span>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Mobile
|
||||
$showMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkShowMobile" name="visibility[show_on_mobile]" ';
|
||||
$html .= checked($showMobile, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkShowMobile">';
|
||||
$html .= ' <strong>Mostrar en móvil</strong> <span class="text-muted">(<992px)</span>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaLetsTalkVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'letsTalk');
|
||||
|
||||
// Switch: CSS Crítico
|
||||
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
||||
$html .= ' <div class="mb-2 mt-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkIsCritical" ';
|
||||
$html .= checked($isCritical, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkIsCritical" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-lightning-charge me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>CSS Crítico</strong>';
|
||||
$html .= ' <small class="text-muted d-block">Inyectar CSS en <head> para optimizar LCP</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Ocultar para usuarios logueados (Plan 99.16)
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($componentId, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkHideForLoggedIn" ';
|
||||
$html .= checked($hideForLoggedIn, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkHideForLoggedIn" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-person-lock me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Ocultar para usuarios logueados</strong>';
|
||||
$html .= ' <small class="text-muted d-block">No mostrar a usuarios con sesión iniciada</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-type me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Text: Button Text
|
||||
$buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', "Let's Talk");
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkButtonText" class="form-label small mb-1 fw-semibold">Texto del botón</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkButtonText" name="content[button_text]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonText) . '" maxlength="30" placeholder="Let\'s Talk">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show Icon
|
||||
$showIcon = $this->renderer->getFieldValue($componentId, 'content', 'show_icon', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkShowIcon" name="content[show_icon]" ';
|
||||
$html .= checked($showIcon, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkShowIcon">';
|
||||
$html .= ' <strong>Mostrar ícono</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Icon Class
|
||||
$iconClass = $this->renderer->getFieldValue($componentId, 'content', 'icon_class', 'bi-lightning-charge-fill');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkIconClass" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Clase del ícono <a href="https://icons.getbootstrap.com/" target="_blank" class="text-decoration-none"><i class="bi bi-box-arrow-up-right"></i></a>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkIconClass" name="content[icon_class]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($iconClass) . '" placeholder="bi-lightning-charge-fill">';
|
||||
$html .= ' <small class="text-muted">Usa clases de Bootstrap Icons (ej: bi-chat-dots)</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Modal Target
|
||||
$modalTarget = $this->renderer->getFieldValue($componentId, 'content', 'modal_target', '#contactModal');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkModalTarget" class="form-label small mb-1 fw-semibold">ID del modal</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkModalTarget" name="content[modal_target]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($modalTarget) . '" placeholder="#contactModal">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: ARIA Label
|
||||
$ariaLabel = $this->renderer->getFieldValue($componentId, 'content', 'aria_label', 'Abrir formulario de contacto');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkAriaLabel" class="form-label small mb-1 fw-semibold">Etiqueta ARIA (accesibilidad)</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkAriaLabel" name="content[aria_label]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($ariaLabel) . '" maxlength="100">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildBehaviorGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-mouse me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Comportamiento';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Switch: Enable Modal
|
||||
$enableModal = $this->renderer->getFieldValue($componentId, 'behavior', 'enable_modal', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkEnableModal" name="behavior[enable_modal]" ';
|
||||
$html .= checked($enableModal, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkEnableModal">';
|
||||
$html .= ' <strong>Abrir modal al hacer clic</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <small class="text-muted">Si está desactivado, usará la URL personalizada</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// URL: Custom URL
|
||||
$customUrl = $this->renderer->getFieldValue($componentId, 'behavior', 'custom_url', '');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkCustomUrl" class="form-label small mb-1 fw-semibold">URL personalizada</label>';
|
||||
$html .= ' <input type="url" id="ctaLetsTalkCustomUrl" name="behavior[custom_url]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($customUrl) . '" placeholder="https://ejemplo.com/contacto">';
|
||||
$html .= ' <small class="text-muted">Solo se usa si "Abrir modal" está desactivado</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Open in New Tab
|
||||
$openNewTab = $this->renderer->getFieldValue($componentId, 'behavior', 'open_in_new_tab', false);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkOpenNewTab" name="behavior[open_in_new_tab]" ';
|
||||
$html .= checked($openNewTab, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkOpenNewTab">';
|
||||
$html .= ' <strong>Abrir en nueva pestaña</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografía';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Text: Font Size
|
||||
$fontSize = $this->renderer->getFieldValue($componentId, 'typography', 'font_size', '1rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkFontSize" class="form-label small mb-1 fw-semibold">Tamaño de fuente</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkFontSize" name="typography[font_size]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($fontSize) . '" placeholder="1rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Font Weight
|
||||
$fontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'font_weight', '600');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkFontWeight" class="form-label small mb-1 fw-semibold">Peso de fuente</label>';
|
||||
$html .= ' <select id="ctaLetsTalkFontWeight" name="typography[font_weight]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="400" ' . selected($fontWeight, '400', false) . '>Normal (400)</option>';
|
||||
$html .= ' <option value="500" ' . selected($fontWeight, '500', false) . '>Medium (500)</option>';
|
||||
$html .= ' <option value="600" ' . selected($fontWeight, '600', false) . '>Semibold (600)</option>';
|
||||
$html .= ' <option value="700" ' . selected($fontWeight, '700', false) . '>Bold (700)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Text Transform
|
||||
$textTransform = $this->renderer->getFieldValue($componentId, 'typography', 'text_transform', 'none');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkTextTransform" class="form-label small mb-1 fw-semibold">Transformación de texto</label>';
|
||||
$html .= ' <select id="ctaLetsTalkTextTransform" name="typography[text_transform]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="none" ' . selected($textTransform, 'none', false) . '>Normal</option>';
|
||||
$html .= ' <option value="uppercase" ' . selected($textTransform, 'uppercase', false) . '>MAYÚSCULAS</option>';
|
||||
$html .= ' <option value="lowercase" ' . selected($textTransform, 'lowercase', false) . '>minúsculas</option>';
|
||||
$html .= ' <option value="capitalize" ' . selected($textTransform, 'capitalize', false) . '>Capitalizado</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Color: Background
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#FF8600');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkBgColor" class="form-label small mb-1 fw-semibold">Color de fondo</label>';
|
||||
$html .= ' <input type="color" id="ctaLetsTalkBgColor" name="colors[background_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($bgColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Color: Background Hover
|
||||
$bgHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_hover_color', '#FF6B35');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkBgHoverColor" class="form-label small mb-1 fw-semibold">Color de fondo (hover)</label>';
|
||||
$html .= ' <input type="color" id="ctaLetsTalkBgHoverColor" name="colors[background_hover_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($bgHoverColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Color: Text
|
||||
$textColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_color', '#FFFFFF');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkTextColor" class="form-label small mb-1 fw-semibold">Color del texto</label>';
|
||||
$html .= ' <input type="color" id="ctaLetsTalkTextColor" name="colors[text_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($textColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Color: Text Hover
|
||||
$textHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_hover_color', '#FFFFFF');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkTextHoverColor" class="form-label small mb-1 fw-semibold">Color del texto (hover)</label>';
|
||||
$html .= ' <input type="color" id="ctaLetsTalkTextHoverColor" name="colors[text_hover_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($textHoverColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Border Color (permite transparent)
|
||||
$borderColor = $this->renderer->getFieldValue($componentId, 'colors', 'border_color', 'transparent');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkBorderColor" class="form-label small mb-1 fw-semibold">Color del borde</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkBorderColor" name="colors[border_color]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderColor) . '" placeholder="transparent o #RRGGBB">';
|
||||
$html .= ' <small class="text-muted">Usa "transparent" para sin borde visible</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-angle-expand me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Text: Padding Top/Bottom
|
||||
$paddingTB = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_top_bottom', '0.5rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkPaddingTB" class="form-label small mb-1 fw-semibold">Padding vertical</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkPaddingTB" name="spacing[padding_top_bottom]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($paddingTB) . '" placeholder="0.5rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Padding Left/Right
|
||||
$paddingLR = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_left_right', '1.5rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkPaddingLR" class="form-label small mb-1 fw-semibold">Padding horizontal</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkPaddingLR" name="spacing[padding_left_right]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($paddingLR) . '" placeholder="1.5rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Margin Left
|
||||
$marginLeft = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_left', '1rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkMarginLeft" class="form-label small mb-1 fw-semibold">Margen izquierdo (desktop)</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkMarginLeft" name="spacing[margin_left]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginLeft) . '" placeholder="1rem">';
|
||||
$html .= ' <small class="text-muted">Separación del menú en pantallas ≥992px</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Icon Spacing
|
||||
$iconSpacing = $this->renderer->getFieldValue($componentId, 'spacing', 'icon_spacing', '0.5rem');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkIconSpacing" class="form-label small mb-1 fw-semibold">Espaciado del ícono</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkIconSpacing" name="spacing[icon_spacing]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($iconSpacing) . '" placeholder="0.5rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisualEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-stars me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Text: Border Radius
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '6px');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkBorderRadius" class="form-label small mb-1 fw-semibold">Radio de bordes</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkBorderRadius" name="visual_effects[border_radius]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '" placeholder="6px">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Border Width
|
||||
$borderWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_width', '0');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkBorderWidth" class="form-label small mb-1 fw-semibold">Grosor del borde</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkBorderWidth" name="visual_effects[border_width]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderWidth) . '" placeholder="0">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Box Shadow
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', 'none');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="ctaLetsTalkBoxShadow" class="form-label small mb-1 fw-semibold">Sombra</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkBoxShadow" name="visual_effects[box_shadow]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '" placeholder="none">';
|
||||
$html .= ' <small class="text-muted">Ej: 0 2px 4px rgba(0,0,0,0.1)</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Text: Transition Duration
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaLetsTalkTransition" class="form-label small mb-1 fw-semibold">Duración de transición</label>';
|
||||
$html .= ' <input type="text" id="ctaLetsTalkTransition" name="visual_effects[transition_duration]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '" placeholder="0.3s">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaPost\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para CTA Post
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class CtaPostFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'cta-post';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'ctaPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaPostHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'ctaPostVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'ctaPostVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'ctaPostVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'ctaPostVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'ctaPostVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'ctaPostExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'ctaPostExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'ctaPostExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'ctaPostExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'ctaPostTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||
'ctaPostDescription' => ['group' => 'content', 'attribute' => 'description'],
|
||||
'ctaPostButtonText' => ['group' => 'content', 'attribute' => 'button_text'],
|
||||
'ctaPostButtonUrl' => ['group' => 'content', 'attribute' => 'button_url'],
|
||||
'ctaPostButtonIcon' => ['group' => 'content', 'attribute' => 'button_icon'],
|
||||
|
||||
// Typography
|
||||
'ctaPostTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'],
|
||||
'ctaPostTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'],
|
||||
'ctaPostDescriptionFontSize' => ['group' => 'typography', 'attribute' => 'description_font_size'],
|
||||
'ctaPostButtonFontSize' => ['group' => 'typography', 'attribute' => 'button_font_size'],
|
||||
|
||||
// Colors
|
||||
'ctaPostGradientStart' => ['group' => 'colors', 'attribute' => 'gradient_start'],
|
||||
'ctaPostGradientEnd' => ['group' => 'colors', 'attribute' => 'gradient_end'],
|
||||
'ctaPostTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'ctaPostDescriptionColor' => ['group' => 'colors', 'attribute' => 'description_color'],
|
||||
'ctaPostButtonBgColor' => ['group' => 'colors', 'attribute' => 'button_bg_color'],
|
||||
'ctaPostButtonTextColor' => ['group' => 'colors', 'attribute' => 'button_text_color'],
|
||||
'ctaPostButtonHoverBg' => ['group' => 'colors', 'attribute' => 'button_hover_bg'],
|
||||
|
||||
// Spacing
|
||||
'ctaPostContainerMarginTop' => ['group' => 'spacing', 'attribute' => 'container_margin_top'],
|
||||
'ctaPostContainerMarginBottom' => ['group' => 'spacing', 'attribute' => 'container_margin_bottom'],
|
||||
'ctaPostContainerPadding' => ['group' => 'spacing', 'attribute' => 'container_padding'],
|
||||
'ctaPostTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||
'ctaPostButtonIconMargin' => ['group' => 'spacing', 'attribute' => 'button_icon_margin'],
|
||||
|
||||
// Visual Effects
|
||||
'ctaPostBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'ctaPostGradientAngle' => ['group' => 'visual_effects', 'attribute' => 'gradient_angle'],
|
||||
'ctaPostButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
|
||||
'ctaPostButtonPadding' => ['group' => 'visual_effects', 'attribute' => 'button_padding'],
|
||||
'ctaPostTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
'ctaPostBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
];
|
||||
}
|
||||
}
|
||||
505
Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php
Normal file
505
Admin/CtaPost/Infrastructure/Ui/CtaPostFormBuilder.php
Normal file
@@ -0,0 +1,505 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CtaPost\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para CTA Post
|
||||
*
|
||||
* @package ROITheme\Admin\CtaPost\Infrastructure\Ui
|
||||
*/
|
||||
final class CtaPostFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de CTA Post';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' CTA promocional debajo del contenido del post';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="cta-post">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('ctaPostEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('ctaPostShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('ctaPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('ctaPostVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'ctaPost');
|
||||
|
||||
// Switch: Ocultar para usuarios logueados (Plan 99.16)
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($componentId, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaPostHideForLoggedIn" ';
|
||||
$html .= checked($hideForLoggedIn, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaPostHideForLoggedIn" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-person-lock me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Ocultar para usuarios logueados</strong>';
|
||||
$html .= ' <small class="text-muted d-block">No mostrar a usuarios con sesion iniciada</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Title
|
||||
$title = $this->renderer->getFieldValue($componentId, 'content', 'title', 'Accede a 200,000+ Analisis de Precios Unitarios');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaPostTitle" class="form-label small mb-1 fw-semibold">Titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaPostTitle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($title) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Description
|
||||
$description = $this->renderer->getFieldValue($componentId, 'content', 'description', '');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaPostDescription" class="form-label small mb-1 fw-semibold">Descripcion</label>';
|
||||
$html .= ' <textarea id="ctaPostDescription" class="form-control form-control-sm" rows="3">';
|
||||
$html .= esc_textarea($description);
|
||||
$html .= '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Button Text
|
||||
$buttonText = $this->renderer->getFieldValue($componentId, 'content', 'button_text', 'Ver Catalogo Completo');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaPostButtonText" class="form-label small mb-1 fw-semibold">Texto del boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonText) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Button URL
|
||||
$buttonUrl = $this->renderer->getFieldValue($componentId, 'content', 'button_url', '/catalogo');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="ctaPostButtonUrl" class="form-label small mb-1 fw-semibold">URL del boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonUrl" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonUrl) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Button Icon
|
||||
$buttonIcon = $this->renderer->getFieldValue($componentId, 'content', 'button_icon', 'bi-arrow-right');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="ctaPostButtonIcon" class="form-label small mb-1 fw-semibold">Icono del boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonIcon" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonIcon) . '" placeholder="bi-arrow-right">';
|
||||
$html .= ' <small class="text-muted">Clase de Bootstrap Icons</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostTitleFontSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaPostTitleFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '700');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostTitleFontWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaPostTitleFontWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$descFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'description_font_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostDescFontSize" class="form-label small mb-1 fw-semibold">Tamano descripcion</label>';
|
||||
$html .= ' <input type="text" id="ctaPostDescFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($descFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$buttonFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'button_font_size', '1.125rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostButtonFontSize" class="form-label small mb-1 fw-semibold">Tamano boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Gradiente
|
||||
$html .= ' <p class="small fw-semibold mb-2">Gradiente de fondo</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$gradientStart = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_start', '#FF8600');
|
||||
$html .= $this->buildColorPicker('ctaPostGradientStart', 'Inicio', $gradientStart);
|
||||
|
||||
$gradientEnd = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_end', '#FFB800');
|
||||
$html .= $this->buildColorPicker('ctaPostGradientEnd', 'Fin', $gradientEnd);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Textos
|
||||
$html .= ' <p class="small fw-semibold mb-2">Textos</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaPostTitleColor', 'Titulo', $titleColor);
|
||||
|
||||
$descColor = $this->renderer->getFieldValue($componentId, 'colors', 'description_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaPostDescColor', 'Descripcion', $descColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Boton
|
||||
$html .= ' <p class="small fw-semibold mb-2">Boton</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_bg_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('ctaPostButtonBg', 'Fondo', $buttonBg);
|
||||
|
||||
$buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#212529');
|
||||
$html .= $this->buildColorPicker('ctaPostButtonTextColor', 'Texto', $buttonTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_bg', '#f8f9fa');
|
||||
$html .= $this->buildColorPicker('ctaPostButtonHoverBg', 'Hover', $buttonHoverBg);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_top', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
|
||||
$html .= ' <input type="text" id="ctaPostMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_bottom', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
|
||||
$html .= ' <input type="text" id="ctaPostMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$padding = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostPadding" class="form-label small mb-1 fw-semibold">Padding interno</label>';
|
||||
$html .= ' <input type="text" id="ctaPostPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($padding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleMargin = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostTitleMargin" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||
$html .= ' <input type="text" id="ctaPostTitleMargin" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleMargin) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '0.375rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostBorderRadius" class="form-label small mb-1 fw-semibold">Radio contenedor</label>';
|
||||
$html .= ' <input type="text" id="ctaPostBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$gradientAngle = $this->renderer->getFieldValue($componentId, 'visual_effects', 'gradient_angle', '135deg');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostGradientAngle" class="form-label small mb-1 fw-semibold">Angulo gradiente</label>';
|
||||
$html .= ' <input type="text" id="ctaPostGradientAngle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($gradientAngle) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '0.375rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostButtonRadius" class="form-label small mb-1 fw-semibold">Radio boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$buttonPadding = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_padding', '0.5rem 1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostButtonPadding" class="form-label small mb-1 fw-semibold">Padding boton</label>';
|
||||
$html .= ' <input type="text" id="ctaPostButtonPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$transition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostTransition" class="form-label small mb-1 fw-semibold">Transicion</label>';
|
||||
$html .= ' <input type="text" id="ctaPostTransition" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transition) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', 'none');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="ctaPostBoxShadow" class="form-label small mb-1 fw-semibold">Sombra</label>';
|
||||
$html .= ' <input type="text" id="ctaPostBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Application\DTOs;
|
||||
|
||||
/**
|
||||
* DTO para solicitud de guardado de snippet
|
||||
*
|
||||
* Inmutable - una vez creado no puede modificarse.
|
||||
* Transporta datos desde Infrastructure (form) hacia Application (use case).
|
||||
*/
|
||||
final class SaveSnippetRequest
|
||||
{
|
||||
/**
|
||||
* @param string $id ID único del snippet (nuevo o existente)
|
||||
* @param string $name Nombre descriptivo
|
||||
* @param string $description Descripción opcional
|
||||
* @param string $css Código CSS
|
||||
* @param string $type Tipo de carga: 'critical' | 'deferred'
|
||||
* @param array<string> $pages Páginas donde aplicar: ['all'], ['home', 'posts'], etc.
|
||||
* @param bool $enabled Si el snippet está activo
|
||||
* @param int $order Orden de carga (menor = primero)
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $id,
|
||||
public readonly string $name,
|
||||
public readonly string $description,
|
||||
public readonly string $css,
|
||||
public readonly string $type,
|
||||
public readonly array $pages,
|
||||
public readonly bool $enabled,
|
||||
public readonly int $order
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Factory desde array (formulario o API)
|
||||
*
|
||||
* @param array $data Datos del formulario
|
||||
* @return self
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
id: $data['id'] ?? '',
|
||||
name: $data['name'] ?? '',
|
||||
description: $data['description'] ?? '',
|
||||
css: $data['css'] ?? '',
|
||||
type: $data['type'] ?? 'deferred',
|
||||
pages: $data['pages'] ?? ['all'],
|
||||
enabled: (bool)($data['enabled'] ?? true),
|
||||
order: (int)($data['order'] ?? 100)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte a array para persistencia
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'css' => $this->css,
|
||||
'type' => $this->type,
|
||||
'pages' => $this->pages,
|
||||
'enabled' => $this->enabled,
|
||||
'order' => $this->order,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Application\UseCases;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\CSSSnippetRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Caso de uso: Eliminar snippet CSS
|
||||
*
|
||||
* SRP: Solo responsable de orquestar la eliminación
|
||||
*/
|
||||
final class DeleteSnippetUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CSSSnippetRepositoryInterface $repository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Ejecuta la eliminación del snippet
|
||||
*
|
||||
* @param string $snippetId ID del snippet a eliminar
|
||||
* @return void
|
||||
*/
|
||||
public function execute(string $snippetId): void
|
||||
{
|
||||
$this->repository->delete($snippetId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Application\UseCases;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\CSSSnippetRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Caso de uso: Obtener todos los snippets (para Admin UI)
|
||||
*
|
||||
* SRP: Solo responsable de obtener lista completa
|
||||
*/
|
||||
final class GetAllSnippetsUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CSSSnippetRepositoryInterface $repository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Ejecuta la obtención de todos los snippets
|
||||
*
|
||||
* @return array<array> Lista de snippets ordenados por 'order'
|
||||
*/
|
||||
public function execute(): array
|
||||
{
|
||||
return $this->repository->getAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Application\UseCases;
|
||||
|
||||
use ROITheme\Admin\CustomCSSManager\Application\DTOs\SaveSnippetRequest;
|
||||
use ROITheme\Admin\CustomCSSManager\Domain\Entities\CSSSnippet;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSSnippetRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Caso de uso: Guardar snippet CSS
|
||||
*
|
||||
* SRP: Solo responsable de orquestar el guardado
|
||||
*/
|
||||
final class SaveSnippetUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CSSSnippetRepositoryInterface $repository
|
||||
) {}
|
||||
|
||||
public function execute(SaveSnippetRequest $request): void
|
||||
{
|
||||
// 1. Crear entidad desde DTO
|
||||
$snippet = CSSSnippet::fromArray($request->toArray());
|
||||
|
||||
// 2. Validar en dominio
|
||||
$snippet->validate();
|
||||
|
||||
// 3. Validar tamaño según tipo
|
||||
$snippet->css()->validateForLoadType($snippet->loadType());
|
||||
|
||||
// 4. Persistir
|
||||
$this->repository->save($snippet->toArray());
|
||||
}
|
||||
}
|
||||
115
Admin/CustomCSSManager/Domain/Entities/CSSSnippet.php
Normal file
115
Admin/CustomCSSManager/Domain/Entities/CSSSnippet.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Domain\Entities;
|
||||
|
||||
use ROITheme\Admin\CustomCSSManager\Domain\ValueObjects\SnippetId;
|
||||
use ROITheme\Admin\CustomCSSManager\Domain\ValueObjects\CSSCode;
|
||||
use ROITheme\Admin\CustomCSSManager\Domain\ValueObjects\LoadType;
|
||||
use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* Entidad de dominio para snippet CSS (contexto Admin)
|
||||
*
|
||||
* Responsabilidad: Reglas de negocio para ADMINISTRAR snippets
|
||||
*/
|
||||
final class CSSSnippet
|
||||
{
|
||||
private function __construct(
|
||||
private readonly SnippetId $id,
|
||||
private readonly string $name,
|
||||
private readonly string $description,
|
||||
private readonly CSSCode $css,
|
||||
private readonly LoadType $loadType,
|
||||
private readonly array $pages,
|
||||
private readonly bool $enabled,
|
||||
private readonly int $order
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Factory method desde array (BD)
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
SnippetId::fromString($data['id']),
|
||||
$data['name'],
|
||||
$data['description'] ?? '',
|
||||
CSSCode::fromString($data['css']),
|
||||
LoadType::fromString($data['type']),
|
||||
$data['pages'] ?? ['all'],
|
||||
$data['enabled'] ?? true,
|
||||
$data['order'] ?? 100
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida que el snippet pueda ser guardado
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function validate(): void
|
||||
{
|
||||
if (empty($this->name)) {
|
||||
throw new ValidationException('El nombre del snippet es requerido');
|
||||
}
|
||||
|
||||
if (strlen($this->name) > 100) {
|
||||
throw new ValidationException('El nombre no puede exceder 100 caracteres');
|
||||
}
|
||||
|
||||
// CSS ya validado en Value Object CSSCode
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte a array para persistencia
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id->value(),
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'css' => $this->css->value(),
|
||||
'type' => $this->loadType->value(),
|
||||
'pages' => $this->pages,
|
||||
'enabled' => $this->enabled,
|
||||
'order' => $this->order,
|
||||
];
|
||||
}
|
||||
|
||||
// Getters
|
||||
public function id(): SnippetId
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function css(): CSSCode
|
||||
{
|
||||
return $this->css;
|
||||
}
|
||||
|
||||
public function loadType(): LoadType
|
||||
{
|
||||
return $this->loadType;
|
||||
}
|
||||
|
||||
public function pages(): array
|
||||
{
|
||||
return $this->pages;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
public function order(): int
|
||||
{
|
||||
return $this->order;
|
||||
}
|
||||
}
|
||||
74
Admin/CustomCSSManager/Domain/ValueObjects/CSSCode.php
Normal file
74
Admin/CustomCSSManager/Domain/ValueObjects/CSSCode.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Domain\ValueObjects;
|
||||
|
||||
use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* Value Object para código CSS validado
|
||||
*/
|
||||
final class CSSCode
|
||||
{
|
||||
private const MAX_SIZE_CRITICAL = 14336; // 14KB para CSS crítico
|
||||
private const MAX_SIZE_DEFERRED = 102400; // 100KB para CSS diferido
|
||||
|
||||
private function __construct(
|
||||
private readonly string $value
|
||||
) {}
|
||||
|
||||
public static function fromString(string $css): self
|
||||
{
|
||||
$sanitized = self::sanitize($css);
|
||||
self::validate($sanitized);
|
||||
return new self($sanitized);
|
||||
}
|
||||
|
||||
private static function sanitize(string $css): string
|
||||
{
|
||||
// Eliminar etiquetas <style>
|
||||
$css = preg_replace('/<\/?style[^>]*>/i', '', $css);
|
||||
|
||||
// Eliminar comentarios HTML
|
||||
$css = preg_replace('/<!--.*?-->/s', '', $css);
|
||||
|
||||
return trim($css);
|
||||
}
|
||||
|
||||
private static function validate(string $css): void
|
||||
{
|
||||
// Detectar código potencialmente peligroso
|
||||
$dangerous = ['javascript:', 'expression(', '@import', 'behavior:'];
|
||||
foreach ($dangerous as $pattern) {
|
||||
if (stripos($css, $pattern) !== false) {
|
||||
throw new ValidationException("CSS contiene patrón no permitido: {$pattern}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function validateForLoadType(LoadType $loadType): void
|
||||
{
|
||||
$maxSize = $loadType->isCritical()
|
||||
? self::MAX_SIZE_CRITICAL
|
||||
: self::MAX_SIZE_DEFERRED;
|
||||
|
||||
if (strlen($this->value) > $maxSize) {
|
||||
throw new ValidationException(
|
||||
sprintf('CSS excede el tamaño máximo de %d bytes para tipo %s',
|
||||
$maxSize,
|
||||
$loadType->value()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function value(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->value);
|
||||
}
|
||||
}
|
||||
56
Admin/CustomCSSManager/Domain/ValueObjects/LoadType.php
Normal file
56
Admin/CustomCSSManager/Domain/ValueObjects/LoadType.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Domain\ValueObjects;
|
||||
|
||||
use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* Value Object para tipo de carga CSS
|
||||
*/
|
||||
final class LoadType
|
||||
{
|
||||
private const VALID_TYPES = ['critical', 'deferred'];
|
||||
|
||||
private function __construct(
|
||||
private readonly string $value
|
||||
) {}
|
||||
|
||||
public static function fromString(string $value): self
|
||||
{
|
||||
if (!in_array($value, self::VALID_TYPES, true)) {
|
||||
throw new ValidationException(
|
||||
sprintf('LoadType inválido: %s. Valores válidos: %s',
|
||||
$value,
|
||||
implode(', ', self::VALID_TYPES)
|
||||
)
|
||||
);
|
||||
}
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
public static function critical(): self
|
||||
{
|
||||
return new self('critical');
|
||||
}
|
||||
|
||||
public static function deferred(): self
|
||||
{
|
||||
return new self('deferred');
|
||||
}
|
||||
|
||||
public function isCritical(): bool
|
||||
{
|
||||
return $this->value === 'critical';
|
||||
}
|
||||
|
||||
public function isDeferred(): bool
|
||||
{
|
||||
return $this->value === 'deferred';
|
||||
}
|
||||
|
||||
public function value(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
124
Admin/CustomCSSManager/Domain/ValueObjects/SnippetId.php
Normal file
124
Admin/CustomCSSManager/Domain/ValueObjects/SnippetId.php
Normal file
@@ -0,0 +1,124 @@
|
||||
<?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 tres formatos:
|
||||
* 1. Generado: css_[timestamp]_[random] (ej: "css_1701432000_a1b2c3")
|
||||
* 2. Legacy con prefijo: css_[descriptive]_[number] (ej: "css_tablas_apu_1764624826")
|
||||
* 3. Legacy 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_PREFIX = '/^css_[a-z0-9_]+$/';
|
||||
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)) {
|
||||
// Acepta formato nuevo (css_timestamp_random) o legacy (css_descriptivo_numero)
|
||||
if (!preg_match(self::PATTERN_GENERATED, $id) && !preg_match(self::PATTERN_LEGACY_PREFIX, $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,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Infrastructure\Bootstrap;
|
||||
|
||||
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\DTOs\SaveSnippetRequest;
|
||||
use ROITheme\Admin\CustomCSSManager\Domain\ValueObjects\SnippetId;
|
||||
use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* Bootstrap para CustomCSSManager
|
||||
*
|
||||
* Registra el handler de formulario POST en admin_init
|
||||
* ANTES de que se envíen headers HTTP
|
||||
*/
|
||||
final class CustomCSSManagerBootstrap
|
||||
{
|
||||
private const NONCE_ACTION = 'roi_custom_css_manager';
|
||||
|
||||
public static function init(): void
|
||||
{
|
||||
add_action('admin_init', [self::class, 'handleFormSubmission']);
|
||||
}
|
||||
|
||||
public static function handleFormSubmission(): void
|
||||
{
|
||||
if (!isset($_POST['roi_css_action'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar que estamos en la página correcta
|
||||
$page = $_GET['page'] ?? '';
|
||||
$component = $_GET['component'] ?? '';
|
||||
if ($page !== 'roi-theme-admin' || $component !== 'custom-css-manager') {
|
||||
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');
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$repository = new WordPressSnippetRepository($wpdb);
|
||||
$saveUseCase = new SaveSnippetUseCase($repository);
|
||||
$deleteUseCase = new DeleteSnippetUseCase($repository);
|
||||
|
||||
$action = sanitize_text_field($_POST['roi_css_action']);
|
||||
|
||||
try {
|
||||
match ($action) {
|
||||
'save' => self::processSave($_POST, $saveUseCase),
|
||||
'delete' => self::processDelete($_POST, $deleteUseCase),
|
||||
default => null,
|
||||
};
|
||||
|
||||
// Redirect con mensaje de éxito
|
||||
$redirect_url = admin_url('admin.php?page=roi-theme-admin&component=custom-css-manager&roi_message=success');
|
||||
wp_redirect($redirect_url);
|
||||
exit;
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
$redirect_url = admin_url('admin.php?page=roi-theme-admin&component=custom-css-manager&roi_message=error&roi_error=' . urlencode($e->getMessage()));
|
||||
wp_redirect($redirect_url);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
private static function processSave(array $data, SaveSnippetUseCase $useCase): void
|
||||
{
|
||||
$id = sanitize_text_field($data['snippet_id'] ?? '');
|
||||
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),
|
||||
]);
|
||||
|
||||
$useCase->execute($request);
|
||||
}
|
||||
|
||||
private static function processDelete(array $data, DeleteSnippetUseCase $useCase): void
|
||||
{
|
||||
$id = sanitize_text_field($data['snippet_id'] ?? '');
|
||||
if (empty($id)) {
|
||||
throw new ValidationException('ID de snippet requerido para eliminar');
|
||||
}
|
||||
$useCase->execute($id);
|
||||
}
|
||||
}
|
||||
@@ -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,421 @@
|
||||
<?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\GetAllSnippetsUseCase;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* NOTA: El handler de formulario POST está en CustomCSSManagerBootstrap
|
||||
* para que se ejecute en admin_init ANTES de que se envíen headers HTTP.
|
||||
*
|
||||
* 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;
|
||||
|
||||
public function __construct(
|
||||
private readonly AdminDashboardRenderer $renderer
|
||||
) {
|
||||
// Crear repositorio y Use Case para listar snippets
|
||||
global $wpdb;
|
||||
$this->repository = new WordPressSnippetRepository($wpdb);
|
||||
$this->getAllUseCase = new GetAllSnippetsUseCase($this->repository);
|
||||
// NOTA: El handler POST está en CustomCSSManagerBootstrap (admin_init)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
|
||||
// Toast para mensajes (usa el sistema existente de admin-dashboard.js)
|
||||
if ($message) {
|
||||
$html .= $this->buildToastTrigger($message);
|
||||
}
|
||||
|
||||
// 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-check-circle me-1"></i> Guardar Cambios';
|
||||
$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' => 'Cambios guardados correctamente'];
|
||||
}
|
||||
|
||||
if ($message === 'error') {
|
||||
$error = urldecode($_GET['roi_error'] ?? 'Error desconocido');
|
||||
return ['type' => 'error', 'text' => $error];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera script para mostrar Toast
|
||||
*/
|
||||
private function buildToastTrigger(array $message): string
|
||||
{
|
||||
$type = esc_js($message['type']);
|
||||
$text = esc_js($message['text']);
|
||||
|
||||
// Mapear tipo a configuración de Bootstrap
|
||||
$typeMap = [
|
||||
'success' => ['bg' => 'success', 'icon' => 'bi-check-circle-fill'],
|
||||
'error' => ['bg' => 'danger', 'icon' => 'bi-x-circle-fill'],
|
||||
];
|
||||
$config = $typeMap[$type] ?? $typeMap['success'];
|
||||
$bg = $config['bg'];
|
||||
$icon = $config['icon'];
|
||||
|
||||
return <<<HTML
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Crear container de toasts si no existe
|
||||
let toastContainer = document.getElementById('roiToastContainer');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.id = 'roiToastContainer';
|
||||
toastContainer.className = 'toast-container position-fixed start-50 translate-middle-x';
|
||||
toastContainer.style.top = '60px';
|
||||
toastContainer.style.zIndex = '999999';
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
// Crear toast
|
||||
const toastId = 'toast-' + Date.now();
|
||||
const toastHTML = `
|
||||
<div id="\${toastId}" class="toast align-items-center text-white bg-{$bg} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<i class="bi {$icon} me-2"></i>
|
||||
<strong>{$text}</strong>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toastContainer.insertAdjacentHTML('beforeend', toastHTML);
|
||||
|
||||
// Mostrar toast
|
||||
const toastElement = document.getElementById(toastId);
|
||||
const toast = new bootstrap.Toast(toastElement, {
|
||||
autohide: true,
|
||||
delay: 5000
|
||||
});
|
||||
toast.show();
|
||||
|
||||
// Eliminar del DOM después de ocultarse
|
||||
toastElement.addEventListener('hidden.bs.toast', function() {
|
||||
toastElement.remove();
|
||||
});
|
||||
|
||||
// Limpiar parámetros de URL sin recargar
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('roi_message');
|
||||
url.searchParams.delete('roi_error');
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
});
|
||||
</script>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
48
Admin/Domain/Contracts/ComponentTabInterface.php
Normal file
48
Admin/Domain/Contracts/ComponentTabInterface.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Contrato para tabs de componentes en el dashboard
|
||||
*
|
||||
* Domain - Lógica pura sin WordPress
|
||||
*/
|
||||
interface ComponentTabInterface
|
||||
{
|
||||
/**
|
||||
* Obtiene el ID del componente
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId(): string;
|
||||
|
||||
/**
|
||||
* Obtiene el nombre visible del tab
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLabel(): string;
|
||||
|
||||
/**
|
||||
* Obtiene el ícono del tab
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getIcon(): string;
|
||||
|
||||
/**
|
||||
* Renderiza el contenido del tab
|
||||
*
|
||||
* @return string HTML del contenido
|
||||
*/
|
||||
public function renderContent(): string;
|
||||
|
||||
/**
|
||||
* Verifica si el tab está activo
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isActive(): bool;
|
||||
}
|
||||
28
Admin/Domain/Contracts/DashboardRendererInterface.php
Normal file
28
Admin/Domain/Contracts/DashboardRendererInterface.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Contrato para renderizar el dashboard del panel de administración
|
||||
*
|
||||
* Domain - Lógica pura sin WordPress
|
||||
*/
|
||||
interface DashboardRendererInterface
|
||||
{
|
||||
/**
|
||||
* Renderiza el dashboard completo
|
||||
*
|
||||
* @return string HTML del dashboard
|
||||
*/
|
||||
public function render(): string;
|
||||
|
||||
/**
|
||||
* Verifica si el renderizador soporta un tipo de vista
|
||||
*
|
||||
* @param string $viewType Tipo de vista (dashboard, settings, etc.)
|
||||
* @return bool
|
||||
*/
|
||||
public function supports(string $viewType): bool;
|
||||
}
|
||||
34
Admin/Domain/Contracts/MenuRegistrarInterface.php
Normal file
34
Admin/Domain/Contracts/MenuRegistrarInterface.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Contrato para registrar el menú de administración
|
||||
*
|
||||
* Domain - Lógica pura sin WordPress
|
||||
*/
|
||||
interface MenuRegistrarInterface
|
||||
{
|
||||
/**
|
||||
* Registra el menú en el sistema de administración
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void;
|
||||
|
||||
/**
|
||||
* Obtiene la capacidad requerida para acceder al menú
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCapability(): string;
|
||||
|
||||
/**
|
||||
* Obtiene el slug del menú
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSlug(): string;
|
||||
}
|
||||
85
Admin/Domain/ValueObjects/MenuItem.php
Normal file
85
Admin/Domain/ValueObjects/MenuItem.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Domain\ValueObjects;
|
||||
|
||||
/**
|
||||
* Value Object para representar un item del menú
|
||||
*
|
||||
* Domain - Objeto inmutable sin WordPress
|
||||
*/
|
||||
final class MenuItem
|
||||
{
|
||||
/**
|
||||
* @param string $pageTitle Título de la página
|
||||
* @param string $menuTitle Título del menú
|
||||
* @param string $capability Capacidad requerida
|
||||
* @param string $menuSlug Slug del menú
|
||||
* @param string $icon Ícono del menú
|
||||
* @param int $position Posición en el menú
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $pageTitle,
|
||||
private readonly string $menuTitle,
|
||||
private readonly string $capability,
|
||||
private readonly string $menuSlug,
|
||||
private readonly string $icon,
|
||||
private readonly int $position
|
||||
) {
|
||||
$this->validate();
|
||||
}
|
||||
|
||||
private function validate(): void
|
||||
{
|
||||
if (empty($this->pageTitle)) {
|
||||
throw new \InvalidArgumentException('Page title cannot be empty');
|
||||
}
|
||||
|
||||
if (empty($this->menuTitle)) {
|
||||
throw new \InvalidArgumentException('Menu title cannot be empty');
|
||||
}
|
||||
|
||||
if (empty($this->capability)) {
|
||||
throw new \InvalidArgumentException('Capability cannot be empty');
|
||||
}
|
||||
|
||||
if (empty($this->menuSlug)) {
|
||||
throw new \InvalidArgumentException('Menu slug cannot be empty');
|
||||
}
|
||||
|
||||
if ($this->position < 0) {
|
||||
throw new \InvalidArgumentException('Position must be >= 0');
|
||||
}
|
||||
}
|
||||
|
||||
public function getPageTitle(): string
|
||||
{
|
||||
return $this->pageTitle;
|
||||
}
|
||||
|
||||
public function getMenuTitle(): string
|
||||
{
|
||||
return $this->menuTitle;
|
||||
}
|
||||
|
||||
public function getCapability(): string
|
||||
{
|
||||
return $this->capability;
|
||||
}
|
||||
|
||||
public function getMenuSlug(): string
|
||||
{
|
||||
return $this->menuSlug;
|
||||
}
|
||||
|
||||
public function getIcon(): string
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function getPosition(): int
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\FeaturedImage\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Featured Image
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class FeaturedImageFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'featured-image';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'featuredImageEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'featuredImageShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'featuredImageShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'featuredImageVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'featuredImageVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'featuredImageVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'featuredImageVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'featuredImageVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'featuredImageExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'featuredImageExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'featuredImageExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'featuredImageExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'featuredImageSize' => ['group' => 'content', 'attribute' => 'image_size'],
|
||||
'featuredImageLazyLoading' => ['group' => 'content', 'attribute' => 'lazy_loading'],
|
||||
'featuredImageLinkToMedia' => ['group' => 'content', 'attribute' => 'link_to_media'],
|
||||
|
||||
// Spacing
|
||||
'featuredImageMarginTop' => ['group' => 'spacing', 'attribute' => 'margin_top'],
|
||||
'featuredImageMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'],
|
||||
|
||||
// Visual Effects
|
||||
'featuredImageBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'featuredImageBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
'featuredImageHoverEffect' => ['group' => 'visual_effects', 'attribute' => 'hover_effect'],
|
||||
'featuredImageHoverScale' => ['group' => 'visual_effects', 'attribute' => 'hover_scale'],
|
||||
'featuredImageTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\FeaturedImage\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
final class FeaturedImageFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-image me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Imagen Destacada';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Personaliza la imagen destacada de los posts';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="featured-image">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageEnabled" ';
|
||||
$html .= checked($enabled, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageEnabled">';
|
||||
$html .= ' <i class="bi bi-power me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar imagen destacada</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageShowOnDesktop" ';
|
||||
$html .= checked($showOnDesktop, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageShowOnDesktop">';
|
||||
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Desktop</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageShowOnMobile" ';
|
||||
$html .= checked($showOnMobile, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageShowOnMobile">';
|
||||
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Mobile</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'featuredImage');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-image me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$imageSize = $this->renderer->getFieldValue($componentId, 'content', 'image_size', 'roi-featured-large');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="featuredImageSize" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-aspect-ratio me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tamano de imagen';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="featuredImageSize" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="roi-featured-large" ' . selected($imageSize, 'roi-featured-large', false) . '>Grande (1200x600)</option>';
|
||||
$html .= ' <option value="roi-featured-medium" ' . selected($imageSize, 'roi-featured-medium', false) . '>Mediano (800x400)</option>';
|
||||
$html .= ' <option value="full" ' . selected($imageSize, 'full', false) . '>Original (tamano completo)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$lazyLoading = $this->renderer->getFieldValue($componentId, 'content', 'lazy_loading', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageLazyLoading" ';
|
||||
$html .= checked($lazyLoading, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageLazyLoading">';
|
||||
$html .= ' <i class="bi bi-lightning me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Carga diferida (lazy loading)</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <small class="text-muted">Mejora rendimiento cargando imagen cuando es visible</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$linkToMedia = $this->renderer->getFieldValue($componentId, 'content', 'link_to_media', false);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageLinkToMedia" ';
|
||||
$html .= checked($linkToMedia, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageLinkToMedia">';
|
||||
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Enlazar a imagen completa</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <small class="text-muted">Abre la imagen en tamano completo al hacer clic</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_top', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="featuredImageMarginTop" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Margen superior';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginTop) . '" placeholder="1rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '2rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="featuredImageMarginBottom" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Margen inferior';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginBottom) . '" placeholder="2rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '12px');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="featuredImageBorderRadius" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Radio de bordes';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '" placeholder="12px">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 8px 24px rgba(0, 0, 0, 0.1)');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="featuredImageBoxShadow" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Sombra';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$hoverEffect = $this->renderer->getFieldValue($componentId, 'visual_effects', 'hover_effect', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="featuredImageHoverEffect" ';
|
||||
$html .= checked($hoverEffect, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="featuredImageHoverEffect">';
|
||||
$html .= ' <i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Efecto hover</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <small class="text-muted">Aplica efecto de escala sutil al pasar el mouse</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$hoverScale = $this->renderer->getFieldValue($componentId, 'visual_effects', 'hover_scale', '1.02');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="featuredImageHoverScale" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Escala en hover';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageHoverScale" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($hoverScale) . '" placeholder="1.02">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="featuredImageTransitionDuration" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Duracion transicion';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="featuredImageTransitionDuration" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '" placeholder="0.3s">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Footer\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Footer
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class FooterFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'footer';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'footerEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'footerShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'footerShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'footerVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'footerVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'footerVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'footerVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'footerVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'footerExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'footerExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'footerExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'footerExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Widget 1
|
||||
'footerWidget1Visible' => ['group' => 'widget_1', 'attribute' => 'widget_1_visible'],
|
||||
'footerWidget1Title' => ['group' => 'widget_1', 'attribute' => 'widget_1_title'],
|
||||
|
||||
// Widget 2
|
||||
'footerWidget2Visible' => ['group' => 'widget_2', 'attribute' => 'widget_2_visible'],
|
||||
'footerWidget2Title' => ['group' => 'widget_2', 'attribute' => 'widget_2_title'],
|
||||
|
||||
// Widget 3
|
||||
'footerWidget3Visible' => ['group' => 'widget_3', 'attribute' => 'widget_3_visible'],
|
||||
'footerWidget3Title' => ['group' => 'widget_3', 'attribute' => 'widget_3_title'],
|
||||
|
||||
// Newsletter
|
||||
'footerNewsletterVisible' => ['group' => 'newsletter', 'attribute' => 'newsletter_visible'],
|
||||
'footerNewsletterTitle' => ['group' => 'newsletter', 'attribute' => 'newsletter_title'],
|
||||
'footerNewsletterDescription' => ['group' => 'newsletter', 'attribute' => 'newsletter_description'],
|
||||
'footerNewsletterPlaceholder' => ['group' => 'newsletter', 'attribute' => 'newsletter_email_placeholder'],
|
||||
'footerNewsletterButtonText' => ['group' => 'newsletter', 'attribute' => 'newsletter_button_text'],
|
||||
'footerNewsletterWebhookUrl' => ['group' => 'newsletter', 'attribute' => 'newsletter_webhook_url'],
|
||||
'footerNewsletterSuccessMessage' => ['group' => 'newsletter', 'attribute' => 'newsletter_success_message'],
|
||||
'footerNewsletterErrorMessage' => ['group' => 'newsletter', 'attribute' => 'newsletter_error_message'],
|
||||
|
||||
// Footer Bottom
|
||||
'footerCopyrightText' => ['group' => 'footer_bottom', 'attribute' => 'copyright_text'],
|
||||
|
||||
// Colors
|
||||
'footerBgColor' => ['group' => 'colors', 'attribute' => 'bg_color'],
|
||||
'footerTextColor' => ['group' => 'colors', 'attribute' => 'text_color'],
|
||||
'footerTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'footerLinkColor' => ['group' => 'colors', 'attribute' => 'link_color'],
|
||||
'footerLinkHoverColor' => ['group' => 'colors', 'attribute' => 'link_hover_color'],
|
||||
'footerButtonBgColor' => ['group' => 'colors', 'attribute' => 'button_bg_color'],
|
||||
'footerButtonTextColor' => ['group' => 'colors', 'attribute' => 'button_text_color'],
|
||||
'footerButtonHoverBg' => ['group' => 'colors', 'attribute' => 'button_hover_bg'],
|
||||
|
||||
// Spacing
|
||||
'footerPaddingY' => ['group' => 'spacing', 'attribute' => 'padding_y'],
|
||||
'footerMarginTop' => ['group' => 'spacing', 'attribute' => 'margin_top'],
|
||||
|
||||
// Visual Effects
|
||||
'footerInputBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'input_border_radius'],
|
||||
'footerButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
|
||||
'footerTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
];
|
||||
}
|
||||
}
|
||||
470
Admin/Footer/Infrastructure/Ui/FooterFormBuilder.php
Normal file
470
Admin/Footer/Infrastructure/Ui/FooterFormBuilder.php
Normal file
@@ -0,0 +1,470 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Footer\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Footer
|
||||
*
|
||||
* RESPONSABILIDAD: Generar formulario de configuracion del Footer
|
||||
*
|
||||
* @package ROITheme\Admin\Footer\Infrastructure\Ui
|
||||
*/
|
||||
final class FooterFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildWidget1Group($componentId);
|
||||
$html .= $this->buildWidget2Group($componentId);
|
||||
$html .= $this->buildWidget3Group($componentId);
|
||||
$html .= $this->buildNewsletterGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildFooterBottomGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-layout-text-window-reverse me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Footer';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Footer con menus de navegacion y newsletter';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="footer">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('footerEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('footerShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('footerShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', true);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', true);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'footer');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildWidget1Group(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-list-ul me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Widget 1 (Menu)';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$visible = $this->renderer->getFieldValue($componentId, 'widget_1', 'widget_1_visible', true);
|
||||
$html .= $this->buildSwitch('footerWidget1Visible', 'Mostrar Widget 1', 'bi-eye', $visible);
|
||||
|
||||
$title = $this->renderer->getFieldValue($componentId, 'widget_1', 'widget_1_title', 'Recursos');
|
||||
$html .= $this->buildTextInput('footerWidget1Title', 'Titulo', 'bi-type', $title);
|
||||
|
||||
$html .= ' <div class="alert alert-info small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' El contenido se gestiona desde <strong>Apariencia > Menus > Footer Menu 1</strong>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildWidget2Group(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-list-ul me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Widget 2 (Menu)';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$visible = $this->renderer->getFieldValue($componentId, 'widget_2', 'widget_2_visible', true);
|
||||
$html .= $this->buildSwitch('footerWidget2Visible', 'Mostrar Widget 2', 'bi-eye', $visible);
|
||||
|
||||
$title = $this->renderer->getFieldValue($componentId, 'widget_2', 'widget_2_title', 'Soporte');
|
||||
$html .= $this->buildTextInput('footerWidget2Title', 'Titulo', 'bi-type', $title);
|
||||
|
||||
$html .= ' <div class="alert alert-info small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' El contenido se gestiona desde <strong>Apariencia > Menus > Footer Menu 2</strong>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildWidget3Group(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-list-ul me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Widget 3 (Menu)';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$visible = $this->renderer->getFieldValue($componentId, 'widget_3', 'widget_3_visible', true);
|
||||
$html .= $this->buildSwitch('footerWidget3Visible', 'Mostrar Widget 3', 'bi-eye', $visible);
|
||||
|
||||
$title = $this->renderer->getFieldValue($componentId, 'widget_3', 'widget_3_title', 'Empresa');
|
||||
$html .= $this->buildTextInput('footerWidget3Title', 'Titulo', 'bi-type', $title);
|
||||
|
||||
$html .= ' <div class="alert alert-info small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' El contenido se gestiona desde <strong>Apariencia > Menus > Footer Menu 3</strong>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildNewsletterGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-envelope-paper me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Newsletter';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$visible = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_visible', true);
|
||||
$html .= $this->buildSwitch('footerNewsletterVisible', 'Mostrar Newsletter', 'bi-eye', $visible);
|
||||
|
||||
$title = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_title', 'Suscribete al Newsletter');
|
||||
$html .= $this->buildTextInput('footerNewsletterTitle', 'Titulo', 'bi-type', $title);
|
||||
|
||||
$description = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_description', 'Recibe las ultimas actualizaciones.');
|
||||
$html .= $this->buildTextarea('footerNewsletterDescription', 'Descripcion', 'bi-text-paragraph', $description);
|
||||
|
||||
$placeholder = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_email_placeholder', 'Email');
|
||||
$html .= $this->buildTextInput('footerNewsletterPlaceholder', 'Placeholder email', 'bi-input-cursor', $placeholder);
|
||||
|
||||
$buttonText = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_button_text', 'Suscribirse');
|
||||
$html .= $this->buildTextInput('footerNewsletterButtonText', 'Texto boton', 'bi-cursor', $buttonText);
|
||||
|
||||
$webhookUrl = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_webhook_url', '');
|
||||
$html .= $this->buildTextarea('footerNewsletterWebhookUrl', 'URL del Webhook', 'bi-link-45deg', $webhookUrl);
|
||||
|
||||
$successMsg = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_success_message', 'Gracias por suscribirte!');
|
||||
$html .= $this->buildTextInput('footerNewsletterSuccessMessage', 'Mensaje exito', 'bi-check-circle', $successMsg);
|
||||
|
||||
$errorMsg = $this->renderer->getFieldValue($componentId, 'newsletter', 'newsletter_error_message', 'Error al suscribirse.');
|
||||
$html .= $this->buildTextInput('footerNewsletterErrorMessage', 'Mensaje error', 'bi-x-circle', $errorMsg);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildFooterBottomGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-c-circle me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Pie de Footer';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$copyright = $this->renderer->getFieldValue($componentId, 'footer_bottom', 'copyright_text', date('Y') . ' Todos los derechos reservados.');
|
||||
$html .= $this->buildTextInput('footerCopyrightText', 'Texto copyright', 'bi-c-circle', $copyright);
|
||||
|
||||
$html .= ' <div class="alert alert-secondary small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' El simbolo © se agrega automaticamente';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'bg_color', '#212529');
|
||||
$html .= $this->buildColorInput('footerBgColor', 'Fondo footer', $bgColor);
|
||||
|
||||
$textColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_color', '#ffffff');
|
||||
$html .= $this->buildColorInput('footerTextColor', 'Color texto', $textColor);
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#ffffff');
|
||||
$html .= $this->buildColorInput('footerTitleColor', 'Color titulos', $titleColor);
|
||||
|
||||
$linkColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_color', '#ffffff');
|
||||
$html .= $this->buildColorInput('footerLinkColor', 'Color links', $linkColor);
|
||||
|
||||
$linkHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_color', '#FF8600');
|
||||
$html .= $this->buildColorInput('footerLinkHoverColor', 'Color links hover', $linkHoverColor);
|
||||
|
||||
$buttonBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_bg_color', '#0d6efd');
|
||||
$html .= $this->buildColorInput('footerButtonBgColor', 'Fondo boton', $buttonBgColor);
|
||||
|
||||
$buttonTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'button_text_color', '#ffffff');
|
||||
$html .= $this->buildColorInput('footerButtonTextColor', 'Texto boton', $buttonTextColor);
|
||||
|
||||
$buttonHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'button_hover_bg', '#0b5ed7');
|
||||
$html .= $this->buildColorInput('footerButtonHoverBg', 'Fondo boton hover', $buttonHoverBg);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-expand me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$paddingY = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_y', '3rem');
|
||||
$html .= $this->buildTextInput('footerPaddingY', 'Padding vertical', 'bi-arrows-vertical', $paddingY);
|
||||
|
||||
$marginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_top', '0');
|
||||
$html .= $this->buildTextInput('footerMarginTop', 'Margen superior', 'bi-arrow-up', $marginTop);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-stars me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$inputRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'input_border_radius', '6px');
|
||||
$html .= $this->buildTextInput('footerInputBorderRadius', 'Radio input', 'bi-square', $inputRadius);
|
||||
|
||||
$buttonRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '6px');
|
||||
$html .= $this->buildTextInput('footerButtonBorderRadius', 'Radio boton', 'bi-square', $buttonRadius);
|
||||
|
||||
$transition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= $this->buildTextInput('footerTransitionDuration', 'Duracion transicion', 'bi-hourglass', $transition);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private function buildSwitch(string $id, string $label, string $icon, $value): string
|
||||
{
|
||||
$checked = $value === true || $value === '1' || $value === 1 ? 'checked' : '';
|
||||
|
||||
$html = ' <div class="form-check form-switch mb-2">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="' . esc_attr($id) . '" ' . $checked . '>';
|
||||
$html .= ' <label class="form-check-label small" for="' . esc_attr($id) . '">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTextInput(string $id, string $label, string $icon, mixed $value): string
|
||||
{
|
||||
$value = $this->normalizeStringValue($value);
|
||||
|
||||
$html = ' <div class="mb-3">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" class="form-control form-control-sm" id="' . esc_attr($id) . '" value="' . esc_attr($value) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPasswordInput(string $id, string $label, string $icon, mixed $value): string
|
||||
{
|
||||
$value = $this->normalizeStringValue($value);
|
||||
|
||||
$html = ' <div class="mb-3">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="password" class="form-control form-control-sm" id="' . esc_attr($id) . '" value="' . esc_attr($value) . '">';
|
||||
$html .= ' <div class="form-text small">URL oculta por seguridad</div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTextarea(string $id, string $label, string $icon, mixed $value): string
|
||||
{
|
||||
$value = $this->normalizeStringValue($value);
|
||||
|
||||
$html = ' <div class="mb-3">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea class="form-control form-control-sm" id="' . esc_attr($id) . '" rows="2">' . esc_textarea($value) . '</textarea>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorInput(string $id, string $label, mixed $value): string
|
||||
{
|
||||
$value = $this->normalizeStringValue($value);
|
||||
|
||||
$html = ' <div class="mb-2 d-flex align-items-center gap-2">';
|
||||
$html .= ' <input type="color" class="form-control form-control-color" id="' . esc_attr($id) . '" value="' . esc_attr($value) . '" style="width: 40px; height: 30px;">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label mb-0 small">' . esc_html($label) . '</label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliza un valor a string para inputs de formulario
|
||||
*
|
||||
* El repositorio convierte '0' a false y '1' a true automáticamente,
|
||||
* pero para campos de texto necesitamos el valor original como string.
|
||||
*/
|
||||
private function normalizeStringValue(mixed $value): string
|
||||
{
|
||||
if ($value === false) {
|
||||
return '0';
|
||||
}
|
||||
if ($value === true) {
|
||||
return '1';
|
||||
}
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, $value): string
|
||||
{
|
||||
$checked = $value === true || $value === '1' || $value === 1 ? 'checked' : '';
|
||||
|
||||
$html = '<div class="form-check">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="' . esc_attr($id) . '" ' . $checked . '>';
|
||||
$html .= ' <label class="form-check-label small" for="' . esc_attr($id) . '">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
78
Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php
Normal file
78
Admin/Hero/Infrastructure/FieldMapping/HeroFieldMapper.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Hero\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Hero Section
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class HeroFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'hero';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'heroEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'heroShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'heroShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'heroIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'heroVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'heroVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'heroVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'heroVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'heroVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'heroExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'heroExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'heroExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'heroExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'heroShowCategories' => ['group' => 'content', 'attribute' => 'show_categories'],
|
||||
'heroShowBadgeIcon' => ['group' => 'content', 'attribute' => 'show_badge_icon'],
|
||||
'heroBadgeIconClass' => ['group' => 'content', 'attribute' => 'badge_icon_class'],
|
||||
'heroTitleTag' => ['group' => 'content', 'attribute' => 'title_tag'],
|
||||
|
||||
// Colors
|
||||
'heroGradientStart' => ['group' => 'colors', 'attribute' => 'gradient_start'],
|
||||
'heroGradientEnd' => ['group' => 'colors', 'attribute' => 'gradient_end'],
|
||||
'heroTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'heroBadgeBgColor' => ['group' => 'colors', 'attribute' => 'badge_bg_color'],
|
||||
'heroBadgeTextColor' => ['group' => 'colors', 'attribute' => 'badge_text_color'],
|
||||
'heroBadgeIconColor' => ['group' => 'colors', 'attribute' => 'badge_icon_color'],
|
||||
'heroBadgeHoverBg' => ['group' => 'colors', 'attribute' => 'badge_hover_bg'],
|
||||
|
||||
// Typography
|
||||
'heroTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'],
|
||||
'heroTitleFontSizeMobile' => ['group' => 'typography', 'attribute' => 'title_font_size_mobile'],
|
||||
'heroTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'],
|
||||
'heroTitleLineHeight' => ['group' => 'typography', 'attribute' => 'title_line_height'],
|
||||
'heroBadgeFontSize' => ['group' => 'typography', 'attribute' => 'badge_font_size'],
|
||||
|
||||
// Spacing
|
||||
'heroPaddingVertical' => ['group' => 'spacing', 'attribute' => 'padding_vertical'],
|
||||
'heroMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'],
|
||||
'heroBadgePadding' => ['group' => 'spacing', 'attribute' => 'badge_padding'],
|
||||
'heroBadgeBorderRadius' => ['group' => 'spacing', 'attribute' => 'badge_border_radius'],
|
||||
|
||||
// Visual Effects
|
||||
'heroBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
'heroTitleTextShadow' => ['group' => 'visual_effects', 'attribute' => 'title_text_shadow'],
|
||||
'heroBadgeBackdropBlur' => ['group' => 'visual_effects', 'attribute' => 'badge_backdrop_blur'],
|
||||
];
|
||||
}
|
||||
}
|
||||
480
Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php
Normal file
480
Admin/Hero/Infrastructure/Ui/HeroFormBuilder.php
Normal file
@@ -0,0 +1,480 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Hero\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
final class HeroFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-image me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuración de Hero Section';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Personaliza la sección hero con título y badges de categorías';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="hero">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="heroEnabled" ';
|
||||
$html .= checked($enabled, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="heroEnabled">';
|
||||
$html .= ' <i class="bi bi-power me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Activar Hero Section</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="heroShowOnDesktop" ';
|
||||
$html .= checked($showOnDesktop, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="heroShowOnDesktop">';
|
||||
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Desktop</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="heroShowOnMobile" ';
|
||||
$html .= checked($showOnMobile, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="heroShowOnMobile">';
|
||||
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Mobile</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'hero');
|
||||
|
||||
// Switch: CSS Crítico
|
||||
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="heroIsCritical" ';
|
||||
$html .= checked($isCritical, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="heroIsCritical">';
|
||||
$html .= ' <i class="bi bi-lightning-charge me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>CSS Crítico</strong>';
|
||||
$html .= ' <small class="text-muted d-block">Inyectar CSS en <head> para optimizar LCP</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$showCategories = $this->renderer->getFieldValue($componentId, 'content', 'show_categories', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="heroShowCategories" ';
|
||||
$html .= checked($showCategories, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="heroShowCategories">';
|
||||
$html .= ' <i class="bi bi-tags me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar badges de categorías</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showBadgeIcon = $this->renderer->getFieldValue($componentId, 'content', 'show_badge_icon', true);
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="heroShowBadgeIcon" ';
|
||||
$html .= checked($showBadgeIcon, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="heroShowBadgeIcon">';
|
||||
$html .= ' <i class="bi bi-star me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar ícono en badges</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$badgeIconClass = $this->renderer->getFieldValue($componentId, 'content', 'badge_icon_class', 'bi-folder-fill');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="heroBadgeIconClass" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-bootstrap me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Clase del ícono de badge';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBadgeIconClass" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($badgeIconClass) . '" placeholder="bi-folder-fill">';
|
||||
$html .= ' <small class="text-muted">Usa clases de Bootstrap Icons</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleTag = $this->renderer->getFieldValue($componentId, 'content', 'title_tag', 'h1');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="heroTitleTag" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-code me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Etiqueta HTML del título';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="heroTitleTag" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="h1" ' . selected($titleTag, 'h1', false) . '>H1 (recomendado para SEO)</option>';
|
||||
$html .= ' <option value="h2" ' . selected($titleTag, 'h2', false) . '>H2</option>';
|
||||
$html .= ' <option value="div" ' . selected($titleTag, 'div', false) . '>DIV (sin semántica)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
|
||||
$gradientStart = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_start', '#1e3a5f');
|
||||
$html .= $this->buildColorPicker('heroGradientStart', 'Degradado (inicio)', 'circle-half', $gradientStart);
|
||||
|
||||
$gradientEnd = $this->renderer->getFieldValue($componentId, 'colors', 'gradient_end', '#2c5282');
|
||||
$html .= $this->buildColorPicker('heroGradientEnd', 'Degradado (fin)', 'circle-fill', $gradientEnd);
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#FFFFFF');
|
||||
$html .= $this->buildColorPicker('heroTitleColor', 'Color título', 'fonts', $titleColor);
|
||||
|
||||
$badgeBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'badge_bg_color', '#FFFFFF');
|
||||
$html .= $this->buildColorPicker('heroBadgeBgColor', 'Fondo badges', 'badge', $badgeBgColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$badgeTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'badge_text_color', '#FFFFFF');
|
||||
$html .= $this->buildColorPicker('heroBadgeTextColor', 'Texto badges', 'card-text', $badgeTextColor);
|
||||
|
||||
$badgeIconColor = $this->renderer->getFieldValue($componentId, 'colors', 'badge_icon_color', '#FFB800');
|
||||
$html .= $this->buildColorPicker('heroBadgeIconColor', 'Ícono badges', 'star-fill', $badgeIconColor);
|
||||
|
||||
$badgeHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'badge_hover_bg', '#FF8600');
|
||||
$html .= $this->buildColorPicker('heroBadgeHoverBg', 'Badges (hover)', 'hand-index', $badgeHoverBg);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-type me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografía';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
|
||||
$titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '2.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroTitleFontSize" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Tamaño desktop';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroTitleFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleFontSizeMobile = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size_mobile', '1.75rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroTitleFontSizeMobile" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Tamaño mobile';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroTitleFontSizeMobile" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontSizeMobile) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
|
||||
$titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '700');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroTitleFontWeight" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Peso del título';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="heroTitleFontWeight" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="400" ' . selected($titleFontWeight, '400', false) . '>Normal (400)</option>';
|
||||
$html .= ' <option value="500" ' . selected($titleFontWeight, '500', false) . '>Medium (500)</option>';
|
||||
$html .= ' <option value="600" ' . selected($titleFontWeight, '600', false) . '>Semibold (600)</option>';
|
||||
$html .= ' <option value="700" ' . selected($titleFontWeight, '700', false) . '>Bold (700)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleLineHeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_line_height', '1.4');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroTitleLineHeight" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Altura de línea';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroTitleLineHeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleLineHeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$badgeFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'badge_font_size', '0.813rem');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="heroBadgeFontSize" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Tamaño fuente badges';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBadgeFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($badgeFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
|
||||
$paddingVertical = $this->renderer->getFieldValue($componentId, 'spacing', 'padding_vertical', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroPaddingVertical" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Padding vertical';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroPaddingVertical" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($paddingVertical) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroMarginBottom" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Margen inferior';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$badgePadding = $this->renderer->getFieldValue($componentId, 'spacing', 'badge_padding', '0.375rem 0.875rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroBadgePadding" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Padding badges';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBadgePadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($badgePadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$badgeBorderRadius = $this->renderer->getFieldValue($componentId, 'spacing', 'badge_border_radius', '20px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="heroBadgeBorderRadius" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Border radius badges';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBadgeBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($badgeBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 4px 16px rgba(30, 58, 95, 0.25)');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="heroBoxShadow" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Sombra del hero';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$titleTextShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'title_text_shadow', '1px 1px 2px rgba(0, 0, 0, 0.2)');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="heroTitleTextShadow" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Sombra del título';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroTitleTextShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleTextShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$badgeBackdropBlur = $this->renderer->getFieldValue($componentId, 'visual_effects', 'badge_backdrop_blur', '10px');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="heroBadgeBackdropBlur" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' Blur de fondo badges';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="heroBadgeBackdropBlur" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($badgeBackdropBlur) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $icon, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= ' <label for="' . $id . '" class="form-label small mb-1 fw-semibold" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-' . $icon . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . $label;
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="color" id="' . $id . '" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($value) . '" title="' . esc_attr($label) . '">';
|
||||
$html .= ' <small class="text-muted d-block mt-1" id="' . $id . 'Value">' . esc_html(strtoupper($value)) . '</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
76
Admin/Infrastructure/Api/WordPress/AdminMenuRegistrar.php
Normal file
76
Admin/Infrastructure/Api/WordPress/AdminMenuRegistrar.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Infrastructure\Api\WordPress;
|
||||
|
||||
use ROITheme\Admin\Domain\Contracts\MenuRegistrarInterface;
|
||||
use ROITheme\Admin\Domain\ValueObjects\MenuItem;
|
||||
use ROITheme\Admin\Application\UseCases\RenderDashboardUseCase;
|
||||
|
||||
/**
|
||||
* Registra el menú de administración en WordPress
|
||||
*
|
||||
* Infrastructure - Implementación específica de WordPress
|
||||
*/
|
||||
final class AdminMenuRegistrar implements MenuRegistrarInterface
|
||||
{
|
||||
private MenuItem $menuItem;
|
||||
|
||||
/**
|
||||
* @param MenuItem $menuItem Configuración del menú
|
||||
* @param RenderDashboardUseCase $renderUseCase Caso de uso para renderizar
|
||||
*/
|
||||
public function __construct(
|
||||
MenuItem $menuItem,
|
||||
private readonly RenderDashboardUseCase $renderUseCase
|
||||
) {
|
||||
$this->menuItem = $menuItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra el menú en WordPress
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
add_action('admin_menu', [$this, 'addMenuPage']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback para agregar la página al menú de WordPress
|
||||
*/
|
||||
public function addMenuPage(): void
|
||||
{
|
||||
add_menu_page(
|
||||
$this->menuItem->getPageTitle(),
|
||||
$this->menuItem->getMenuTitle(),
|
||||
$this->menuItem->getCapability(),
|
||||
$this->menuItem->getMenuSlug(),
|
||||
[$this, 'renderPage'],
|
||||
$this->menuItem->getIcon(),
|
||||
$this->menuItem->getPosition()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback para renderizar la página
|
||||
*/
|
||||
public function renderPage(): void
|
||||
{
|
||||
try {
|
||||
echo $this->renderUseCase->execute('dashboard');
|
||||
} catch (\Exception $e) {
|
||||
echo '<div class="error"><p>Error rendering dashboard: ' . esc_html($e->getMessage()) . '</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
public function getCapability(): string
|
||||
{
|
||||
return $this->menuItem->getCapability();
|
||||
}
|
||||
|
||||
public function getSlug(): string
|
||||
{
|
||||
return $this->menuItem->getMenuSlug();
|
||||
}
|
||||
}
|
||||
129
Admin/Infrastructure/Services/AdminAssetEnqueuer.php
Normal file
129
Admin/Infrastructure/Services/AdminAssetEnqueuer.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Infrastructure\Services;
|
||||
|
||||
/**
|
||||
* Servicio para enqueue de assets del panel de administración
|
||||
*
|
||||
* Infrastructure - WordPress specific
|
||||
*/
|
||||
final class AdminAssetEnqueuer
|
||||
{
|
||||
private const ADMIN_PAGE_SLUG = 'roi-theme-admin';
|
||||
|
||||
public function __construct(
|
||||
private readonly string $themeUri
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra los hooks de WordPress
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue de assets solo en la página del dashboard
|
||||
*
|
||||
* @param string $hook Hook name de WordPress
|
||||
*/
|
||||
public function enqueueAssets(string $hook): void
|
||||
{
|
||||
// Solo cargar en nuestra página de admin
|
||||
if (!$this->isAdminPage($hook)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->enqueueStyles();
|
||||
$this->enqueueScripts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si estamos en la página del dashboard
|
||||
*
|
||||
* @param string $hook Hook name
|
||||
* @return bool
|
||||
*/
|
||||
private function isAdminPage(string $hook): bool
|
||||
{
|
||||
return strpos($hook, self::ADMIN_PAGE_SLUG) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue de estilos CSS
|
||||
*/
|
||||
private function enqueueStyles(): void
|
||||
{
|
||||
// Bootstrap 5 CSS
|
||||
wp_enqueue_style(
|
||||
'bootstrap',
|
||||
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css',
|
||||
[],
|
||||
'5.3.2'
|
||||
);
|
||||
|
||||
// Bootstrap Icons
|
||||
wp_enqueue_style(
|
||||
'bootstrap-icons',
|
||||
'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css',
|
||||
[],
|
||||
'1.11.3'
|
||||
);
|
||||
|
||||
// Estilos del dashboard
|
||||
wp_enqueue_style(
|
||||
'roi-admin-dashboard',
|
||||
$this->themeUri . '/Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css',
|
||||
['bootstrap', 'bootstrap-icons'],
|
||||
filemtime(get_template_directory() . '/Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue de scripts JavaScript
|
||||
*/
|
||||
private function enqueueScripts(): void
|
||||
{
|
||||
// Bootstrap 5 JS Bundle (incluye Popper)
|
||||
// IMPORTANTE: Cargar en header (false) para que esté disponible antes del contenido
|
||||
wp_enqueue_script(
|
||||
'bootstrap',
|
||||
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js',
|
||||
[],
|
||||
'5.3.2',
|
||||
false // Load in header, not footer - required for Bootstrap tabs to work
|
||||
);
|
||||
|
||||
// Script del dashboard
|
||||
wp_enqueue_script(
|
||||
'roi-admin-dashboard',
|
||||
$this->themeUri . '/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js',
|
||||
['bootstrap'],
|
||||
filemtime(get_template_directory() . '/Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js'),
|
||||
true
|
||||
);
|
||||
|
||||
// Script de toggle para exclusiones (Plan 99.11)
|
||||
wp_enqueue_script(
|
||||
'roi-exclusion-toggle',
|
||||
$this->themeUri . '/Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js',
|
||||
['roi-admin-dashboard'],
|
||||
filemtime(get_template_directory() . '/Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js'),
|
||||
true
|
||||
);
|
||||
|
||||
// Pasar variables al JavaScript
|
||||
wp_localize_script(
|
||||
'roi-admin-dashboard',
|
||||
'roiAdminDashboard',
|
||||
[
|
||||
'nonce' => wp_create_nonce('roi_admin_dashboard'),
|
||||
'ajaxurl' => admin_url('admin-ajax.php')
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
225
Admin/Infrastructure/Ui/AdminDashboardRenderer.php
Normal file
225
Admin/Infrastructure/Ui/AdminDashboardRenderer.php
Normal file
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Domain\Contracts\DashboardRendererInterface;
|
||||
use ROITheme\Shared\Application\UseCases\GetComponentSettings\GetComponentSettingsUseCase;
|
||||
|
||||
/**
|
||||
* Renderiza el dashboard del panel de administración
|
||||
*
|
||||
* Infrastructure - Implementación con WordPress
|
||||
*/
|
||||
final class AdminDashboardRenderer implements DashboardRendererInterface
|
||||
{
|
||||
private const SUPPORTED_VIEWS = ['dashboard'];
|
||||
|
||||
/**
|
||||
* @param GetComponentSettingsUseCase|null $getComponentSettingsUseCase
|
||||
* @param ComponentGroupRegistry|null $groupRegistry Registro de grupos de componentes
|
||||
* @param array<string, mixed> $components Componentes disponibles
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ?GetComponentSettingsUseCase $getComponentSettingsUseCase = null,
|
||||
private readonly ?ComponentGroupRegistry $groupRegistry = null,
|
||||
private readonly array $components = []
|
||||
) {
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
ob_start();
|
||||
require __DIR__ . '/Views/dashboard.php';
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
public function supports(string $viewType): bool
|
||||
{
|
||||
return in_array($viewType, self::SUPPORTED_VIEWS, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene los componentes disponibles
|
||||
*
|
||||
* @return array<string, array<string, string>>
|
||||
*/
|
||||
public function getComponents(): array
|
||||
{
|
||||
return [
|
||||
'top-notification-bar' => [
|
||||
'id' => 'top-notification-bar',
|
||||
'label' => 'TopBar',
|
||||
'icon' => 'bi-megaphone-fill',
|
||||
],
|
||||
'navbar' => [
|
||||
'id' => 'navbar',
|
||||
'label' => 'Navbar',
|
||||
'icon' => 'bi-list',
|
||||
],
|
||||
'cta-lets-talk' => [
|
||||
'id' => 'cta-lets-talk',
|
||||
'label' => "Let's Talk",
|
||||
'icon' => 'bi-lightning-charge-fill',
|
||||
],
|
||||
'hero' => [
|
||||
'id' => 'hero',
|
||||
'label' => 'Hero Section',
|
||||
'icon' => 'bi-image',
|
||||
],
|
||||
'featured-image' => [
|
||||
'id' => 'featured-image',
|
||||
'label' => 'Featured Image',
|
||||
'icon' => 'bi-card-image',
|
||||
],
|
||||
'table-of-contents' => [
|
||||
'id' => 'table-of-contents',
|
||||
'label' => 'Table of Contents',
|
||||
'icon' => 'bi-list-nested',
|
||||
],
|
||||
'cta-box-sidebar' => [
|
||||
'id' => 'cta-box-sidebar',
|
||||
'label' => 'CTA Sidebar',
|
||||
'icon' => 'bi-megaphone',
|
||||
],
|
||||
'social-share' => [
|
||||
'id' => 'social-share',
|
||||
'label' => 'Social Share',
|
||||
'icon' => 'bi-share',
|
||||
],
|
||||
'cta-post' => [
|
||||
'id' => 'cta-post',
|
||||
'label' => 'CTA Post',
|
||||
'icon' => 'bi-megaphone-fill',
|
||||
],
|
||||
'related-post' => [
|
||||
'id' => 'related-post',
|
||||
'label' => 'Related Posts',
|
||||
'icon' => 'bi-grid-3x3-gap',
|
||||
],
|
||||
'archive-header' => [
|
||||
'id' => 'archive-header',
|
||||
'label' => 'Archive Header',
|
||||
'icon' => 'bi-layout-text-window',
|
||||
],
|
||||
'post-grid' => [
|
||||
'id' => 'post-grid',
|
||||
'label' => 'Post Grid',
|
||||
'icon' => 'bi-grid-3x3',
|
||||
],
|
||||
'contact-form' => [
|
||||
'id' => 'contact-form',
|
||||
'label' => 'Contact Form',
|
||||
'icon' => 'bi-envelope-paper',
|
||||
],
|
||||
'footer' => [
|
||||
'id' => 'footer',
|
||||
'label' => 'Footer',
|
||||
'icon' => 'bi-layout-text-window-reverse',
|
||||
],
|
||||
'theme-settings' => [
|
||||
'id' => 'theme-settings',
|
||||
'label' => 'Theme Settings',
|
||||
'icon' => 'bi-gear',
|
||||
],
|
||||
'adsense-placement' => [
|
||||
'id' => 'adsense-placement',
|
||||
'label' => 'AdSense',
|
||||
'icon' => 'bi-megaphone',
|
||||
],
|
||||
'custom-css-manager' => [
|
||||
'id' => 'custom-css-manager',
|
||||
'label' => 'CSS Personalizado',
|
||||
'icon' => 'bi-file-earmark-code',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene las configuraciones de un componente
|
||||
*
|
||||
* @param string $componentName Nombre del componente
|
||||
* @return array<string, array<string, mixed>> Configuraciones agrupadas por grupo
|
||||
*/
|
||||
public function getComponentSettings(string $componentName): array
|
||||
{
|
||||
if ($this->getComponentSettingsUseCase === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->getComponentSettingsUseCase->execute($componentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el valor de un campo de configuración
|
||||
*
|
||||
* @param string $componentName Nombre del componente
|
||||
* @param string $groupName Nombre del grupo
|
||||
* @param string $attributeName Nombre del atributo
|
||||
* @param mixed $default Valor por defecto si no existe
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFieldValue(string $componentName, string $groupName, string $attributeName, mixed $default = null): mixed
|
||||
{
|
||||
$settings = $this->getComponentSettings($componentName);
|
||||
|
||||
return $settings[$groupName][$attributeName] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la clase del FormBuilder para un componente
|
||||
*
|
||||
* @param string $componentId ID del componente en kebab-case (ej: 'top-notification-bar')
|
||||
* @return string Namespace completo del FormBuilder
|
||||
*/
|
||||
public function getFormBuilderClass(string $componentId): string
|
||||
{
|
||||
// Mapeo especial para componentes con acrónimos (CSS, API, etc.)
|
||||
$specialMappings = [
|
||||
'custom-css-manager' => 'CustomCSSManager',
|
||||
];
|
||||
|
||||
// Usar mapeo especial si existe, sino convertir kebab-case a PascalCase
|
||||
if (isset($specialMappings[$componentId])) {
|
||||
$className = $specialMappings[$componentId];
|
||||
} else {
|
||||
// 'top-notification-bar' → 'TopNotificationBar'
|
||||
$className = str_replace('-', '', ucwords($componentId, '-'));
|
||||
}
|
||||
|
||||
// Construir namespace completo
|
||||
// ROITheme\Admin\TopNotificationBar\Infrastructure\Ui\TopNotificationBarFormBuilder
|
||||
return "ROITheme\\Admin\\{$className}\\Infrastructure\\Ui\\{$className}FormBuilder";
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene los grupos de componentes
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public function getComponentGroups(): array
|
||||
{
|
||||
if ($this->groupRegistry === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->groupRegistry->getGroups();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el grupo al que pertenece un componente
|
||||
*
|
||||
* @param string $componentId ID del componente
|
||||
* @return string|null ID del grupo o null
|
||||
*/
|
||||
public function getGroupForComponent(string $componentId): ?string
|
||||
{
|
||||
if ($this->groupRegistry === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->groupRegistry->getGroupForComponent($componentId);
|
||||
}
|
||||
|
||||
}
|
||||
630
Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css
Normal file
630
Admin/Infrastructure/Ui/Assets/Css/admin-dashboard.css
Normal file
@@ -0,0 +1,630 @@
|
||||
/**
|
||||
* ROI Theme Admin Dashboard - Improved Version
|
||||
* Basado en improved-panel.css del Design System
|
||||
*/
|
||||
|
||||
/* ================================================
|
||||
CSS VARIABLES - DESIGN SYSTEM
|
||||
================================================ */
|
||||
|
||||
:root {
|
||||
/* Navy Colors */
|
||||
--roi-navy-dark: #0E2337;
|
||||
--roi-navy-primary: #1e3a5f;
|
||||
--roi-navy-light: #2c5282;
|
||||
|
||||
/* Orange Colors */
|
||||
--roi-orange-primary: #FF8600;
|
||||
--roi-orange-hover: #FF6B35;
|
||||
--roi-orange-light: #FFB800;
|
||||
|
||||
/* Neutral Colors */
|
||||
--roi-neutral-50: #f8f9fa;
|
||||
--roi-neutral-100: #e9ecef;
|
||||
--roi-neutral-600: #495057;
|
||||
--roi-neutral-700: #6c757d;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
--shadow-xl: 0 12px 32px rgba(0, 0, 0, 0.15);
|
||||
--shadow-orange: 0 6px 20px rgba(255, 134, 0, 0.15);
|
||||
|
||||
/* Transitions */
|
||||
--transition-base: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-fast: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
WORDPRESS ADMIN OVERRIDES
|
||||
================================================ */
|
||||
|
||||
/* Sobrescribir max-width de .card de WordPress */
|
||||
.wrap.roi-admin-panel .card {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
/* Fix para switches de Bootstrap */
|
||||
.wrap.roi-admin-panel .form-switch .form-check-input {
|
||||
all: unset !important;
|
||||
width: 2em !important;
|
||||
height: 1em !important;
|
||||
margin-left: -2.5em !important;
|
||||
margin-right: 0.5em !important;
|
||||
background-color: #dee2e6 !important;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='white'/%3e%3c/svg%3e") !important;
|
||||
background-position: left center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: contain !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.25) !important;
|
||||
border-radius: 2em !important;
|
||||
transition: background-position 0.15s ease-in-out !important;
|
||||
cursor: pointer !important;
|
||||
flex-shrink: 0 !important;
|
||||
appearance: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
-moz-appearance: none !important;
|
||||
}
|
||||
|
||||
.wrap.roi-admin-panel .form-switch .form-check-input:checked {
|
||||
background-color: #0d6efd !important;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='white'/%3e%3c/svg%3e") !important;
|
||||
background-position: right center !important;
|
||||
border-color: #0d6efd !important;
|
||||
}
|
||||
|
||||
.wrap.roi-admin-panel .form-switch .form-check-input::before,
|
||||
.wrap.roi-admin-panel .form-switch .form-check-input::after {
|
||||
display: none !important;
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
.wrap.roi-admin-panel .form-switch .form-check-input:focus {
|
||||
outline: 0 !important;
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25) !important;
|
||||
}
|
||||
|
||||
.wrap.roi-admin-panel .form-check {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.wrap.roi-admin-panel .form-check-label {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
margin-bottom: 0 !important;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
TABS NAVIGATION (Legacy)
|
||||
================================================ */
|
||||
|
||||
.nav-tabs-admin {
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-item {
|
||||
margin-right: 0.1rem;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-link {
|
||||
color: #6c757d;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
padding: 0.3rem 0.3rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.83rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-link i.bi {
|
||||
margin-right: 0.2rem !important;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-link:hover {
|
||||
color: #FF8600;
|
||||
border-bottom-color: #FFB800;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-link.active {
|
||||
color: #FF8600;
|
||||
border-bottom-color: #FF8600;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Tab Content */
|
||||
.tab-content {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
HEADER MEJORADO
|
||||
================================================ */
|
||||
|
||||
.roi-home-header {
|
||||
position: relative;
|
||||
padding: 2.5rem 2rem;
|
||||
background: var(--roi-navy-dark);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: none;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
/* Patrón de fondo sutil */
|
||||
.roi-home-header__pattern {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 50%, rgba(255, 134, 0, 0.05) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 50%, rgba(44, 82, 130, 0.08) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.roi-home-header__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.roi-home-header__icon-wrapper {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--roi-orange-primary), var(--roi-orange-light));
|
||||
border-radius: 16px;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4px 16px rgba(255, 134, 0, 0.3);
|
||||
}
|
||||
|
||||
.roi-home-header__icon {
|
||||
font-size: 2rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.roi-home-header__text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.roi-home-header__title {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin: 0 0 0.5rem 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.roi-home-header__subtitle {
|
||||
font-size: 0.95rem;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
GRID DE GRUPOS
|
||||
================================================ */
|
||||
|
||||
.roi-groups-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.roi-groups-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
GROUP CARDS MEJORADOS
|
||||
================================================ */
|
||||
|
||||
.roi-group-card {
|
||||
position: relative;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--roi-neutral-100);
|
||||
transition: var(--transition-base);
|
||||
animation: fadeInUp 0.4s cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||
}
|
||||
|
||||
/* Efecto glow en hover */
|
||||
.roi-group-card__glow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 0 0 1px var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
.roi-group-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
border-color: var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
.roi-group-card:hover .roi-group-card__glow {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Header del grupo */
|
||||
.roi-group-card__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1.25rem;
|
||||
border-bottom: 2px solid var(--roi-neutral-50);
|
||||
}
|
||||
|
||||
.roi-group-card__icon-wrapper {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 134, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
flex-shrink: 0;
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
|
||||
.roi-group-card:hover .roi-group-card__icon-wrapper {
|
||||
background: rgba(255, 134, 0, 0.15);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.roi-group-card__icon {
|
||||
font-size: 1.75rem;
|
||||
color: var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
.roi-group-card__header-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.roi-group-card__title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.35rem 0;
|
||||
color: var(--roi-navy-primary);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.roi-group-card__description {
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
color: var(--roi-neutral-700);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
COMPONENTS GRID
|
||||
================================================ */
|
||||
|
||||
.roi-components-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
MINI CARDS MEJORADOS
|
||||
================================================ */
|
||||
|
||||
.roi-component-minicard {
|
||||
position: relative;
|
||||
background: white;
|
||||
border: 1px solid var(--roi-neutral-100);
|
||||
border-radius: 10px;
|
||||
padding: 1.25rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-base);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.roi-component-minicard::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--roi-orange-primary), var(--roi-orange-light));
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.roi-component-minicard:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.roi-component-minicard:hover {
|
||||
transform: translateY(-3px) scale(1.02);
|
||||
box-shadow: var(--shadow-orange);
|
||||
border-color: var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
.roi-component-minicard:active {
|
||||
transform: translateY(-2px) scale(0.98);
|
||||
}
|
||||
|
||||
.roi-component-minicard:focus {
|
||||
outline: 2px solid var(--roi-orange-primary);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
/* Icono del mini card */
|
||||
.roi-component-minicard__icon-bg {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 134, 0, 0.08);
|
||||
border-radius: 10px;
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
|
||||
.roi-component-minicard:hover .roi-component-minicard__icon-bg {
|
||||
background: rgba(255, 134, 0, 0.15);
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
.roi-component-minicard__icon {
|
||||
font-size: 1.5rem;
|
||||
color: var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
.roi-component-minicard__label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--roi-navy-dark);
|
||||
line-height: 1.3;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.roi-component-minicard:hover .roi-component-minicard__label {
|
||||
color: var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
BREADCRUMB
|
||||
================================================ */
|
||||
|
||||
.roi-breadcrumb {
|
||||
padding: 1rem 1.5rem;
|
||||
background: var(--roi-neutral-50);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid var(--roi-orange-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.roi-breadcrumb__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.roi-breadcrumb__separator {
|
||||
color: var(--roi-neutral-700);
|
||||
}
|
||||
|
||||
.roi-breadcrumb__group {
|
||||
color: var(--roi-neutral-700);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.roi-breadcrumb__current {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--roi-navy-primary);
|
||||
}
|
||||
|
||||
/* Botón Volver */
|
||||
.roi-back-to-home {
|
||||
border-color: var(--roi-navy-primary);
|
||||
color: var(--roi-navy-primary);
|
||||
}
|
||||
|
||||
.roi-back-to-home:hover {
|
||||
background-color: var(--roi-navy-primary);
|
||||
border-color: var(--roi-navy-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
COMPONENT FORM CONTAINER
|
||||
================================================ */
|
||||
|
||||
.roi-component-form-container {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
ANIMATIONS
|
||||
================================================ */
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
#roi-home-view,
|
||||
#roi-component-view {
|
||||
animation: fadeIn 0.4s ease-in;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
UTILITY CLASSES
|
||||
================================================ */
|
||||
|
||||
.roi-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
RESPONSIVE
|
||||
================================================ */
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.roi-group-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.roi-home-header {
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.roi-home-header__icon-wrapper {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.roi-home-header__icon {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.roi-home-header__title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-tabs-admin {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-link {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.roi-home-header__content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.roi-components-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.roi-component-minicard {
|
||||
padding: 1rem 0.75rem;
|
||||
}
|
||||
|
||||
.roi-component-minicard__icon-bg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.roi-component-minicard__icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.roi-group-card__icon-wrapper {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.roi-group-card__icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-tabs-admin {
|
||||
overflow-x: auto;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.nav-tabs-admin .nav-item {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.roi-groups-grid {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.roi-home-header {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.roi-home-header__title {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.roi-home-header__subtitle {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.roi-group-card {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.roi-group-card__title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.roi-components-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.roi-component-minicard {
|
||||
flex-direction: row;
|
||||
text-align: left;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
521
Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js
Normal file
521
Admin/Infrastructure/Ui/Assets/Js/admin-dashboard.js
Normal file
@@ -0,0 +1,521 @@
|
||||
/**
|
||||
* JavaScript para el Dashboard del Panel de Administración ROI Theme
|
||||
* Vanilla JavaScript - No frameworks
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Inicializa el dashboard cuando el DOM está listo
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Nueva navegación por Cards/Grupos
|
||||
initializeCardNavigation();
|
||||
|
||||
// Funcionalidad existente (solo si hay tabs visibles)
|
||||
if (document.querySelector('.nav-tabs-admin')) {
|
||||
initializeTabs();
|
||||
}
|
||||
|
||||
initializeFormValidation();
|
||||
initializeButtons();
|
||||
initializeColorPickers();
|
||||
});
|
||||
|
||||
/**
|
||||
* Inicializa la navegación por Cards/Grupos (App-Style)
|
||||
*/
|
||||
function initializeCardNavigation() {
|
||||
// Verificar que estamos en el panel correcto
|
||||
const adminPanel = document.querySelector('.roi-admin-panel');
|
||||
if (!adminPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delegación de eventos para mini-cards
|
||||
document.addEventListener('click', function(e) {
|
||||
const minicard = e.target.closest('.roi-component-minicard');
|
||||
if (minicard) {
|
||||
e.preventDefault();
|
||||
const componentId = minicard.getAttribute('data-component-id');
|
||||
if (componentId) {
|
||||
navigateToComponent(componentId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Botón volver al home
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.roi-back-to-home')) {
|
||||
e.preventDefault();
|
||||
navigateToHome();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navega a un componente específico
|
||||
*
|
||||
* @param {string} componentId ID del componente en kebab-case
|
||||
*/
|
||||
function navigateToComponent(componentId) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('component', componentId);
|
||||
// Eliminar el parámetro admin-tab si existe (legacy)
|
||||
url.searchParams.delete('admin-tab');
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navega de vuelta al home (vista de grupos)
|
||||
*/
|
||||
function navigateToHome() {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('component');
|
||||
url.searchParams.delete('admin-tab');
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa el sistema de tabs con persistencia en URL
|
||||
*/
|
||||
function initializeTabs() {
|
||||
const tabButtons = document.querySelectorAll('[data-bs-toggle="tab"]');
|
||||
|
||||
// Leer parametro admin-tab de la URL al cargar
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const activeTabParam = urlParams.get('admin-tab');
|
||||
|
||||
if (activeTabParam) {
|
||||
// Buscar el boton del tab correspondiente
|
||||
const targetButton = document.querySelector('[data-bs-target="#' + activeTabParam + 'Tab"]');
|
||||
if (targetButton) {
|
||||
// Activar el tab usando Bootstrap API
|
||||
const tab = new bootstrap.Tab(targetButton);
|
||||
tab.show();
|
||||
}
|
||||
}
|
||||
|
||||
// Escuchar cambios de tab para actualizar URL
|
||||
tabButtons.forEach(function(tabButton) {
|
||||
tabButton.addEventListener('shown.bs.tab', function(e) {
|
||||
// Obtener el ID del componente desde data-bs-target
|
||||
const target = e.target.getAttribute('data-bs-target');
|
||||
const componentId = target.replace('#', '').replace('Tab', '');
|
||||
|
||||
// Actualizar URL sin recargar pagina
|
||||
updateUrlWithTab(componentId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza la URL con el parametro admin-tab sin recargar la pagina
|
||||
*
|
||||
* @param {string} tabId ID del tab activo
|
||||
*/
|
||||
function updateUrlWithTab(tabId) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('admin-tab', tabId);
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el ID del tab activo actualmente
|
||||
*
|
||||
* @returns {string|null} ID del componente activo o null
|
||||
*/
|
||||
function getActiveTabId() {
|
||||
const activeTab = document.querySelector('.tab-pane.active');
|
||||
if (activeTab) {
|
||||
return activeTab.id.replace('Tab', '');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa validación de formularios
|
||||
*/
|
||||
function initializeFormValidation() {
|
||||
const forms = document.querySelectorAll('.roi-component-config form');
|
||||
|
||||
forms.forEach(function(form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (!validateForm(form)) {
|
||||
e.preventDefault();
|
||||
showError('Por favor, corrige los errores en el formulario.');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida un formulario
|
||||
*
|
||||
* @param {HTMLFormElement} form Formulario a validar
|
||||
* @returns {boolean} True si es válido
|
||||
*/
|
||||
function validateForm(form) {
|
||||
let isValid = true;
|
||||
const requiredFields = form.querySelectorAll('[required]');
|
||||
|
||||
requiredFields.forEach(function(field) {
|
||||
if (!field.value.trim()) {
|
||||
field.classList.add('error');
|
||||
isValid = false;
|
||||
} else {
|
||||
field.classList.remove('error');
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra un mensaje de error
|
||||
*
|
||||
* @param {string} message Mensaje a mostrar
|
||||
*/
|
||||
function showError(message) {
|
||||
const notice = document.createElement('div');
|
||||
notice.className = 'notice notice-error is-dismissible';
|
||||
notice.innerHTML = '<p>' + escapeHtml(message) + '</p>';
|
||||
|
||||
const h1 = document.querySelector('.roi-admin-dashboard h1');
|
||||
if (h1 && h1.nextElementSibling) {
|
||||
h1.nextElementSibling.after(notice);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapa HTML para prevenir XSS
|
||||
*
|
||||
* @param {string} text Texto a escapar
|
||||
* @returns {string} Texto escapado
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa los botones del panel
|
||||
*/
|
||||
function initializeButtons() {
|
||||
// Botón Guardar Cambios
|
||||
const saveButton = document.getElementById('saveSettings');
|
||||
if (saveButton) {
|
||||
saveButton.addEventListener('click', handleSaveSettings);
|
||||
}
|
||||
|
||||
// Botón Cancelar
|
||||
const cancelButton = document.getElementById('cancelChanges');
|
||||
if (cancelButton) {
|
||||
cancelButton.addEventListener('click', handleCancelChanges);
|
||||
}
|
||||
|
||||
// Botones Restaurar valores por defecto (dinámico para todos los componentes)
|
||||
const resetButtons = document.querySelectorAll('.btn-reset-defaults[data-component]');
|
||||
resetButtons.forEach(function(button) {
|
||||
button.addEventListener('click', function(e) {
|
||||
const componentId = this.getAttribute('data-component');
|
||||
handleResetDefaults(e, componentId, this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarda los cambios del formulario
|
||||
*/
|
||||
function handleSaveSettings(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Obtener el tab activo
|
||||
const activeTab = document.querySelector('.tab-pane.active');
|
||||
if (!activeTab) {
|
||||
showNotice('error', 'No hay ningún componente seleccionado.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Obtener el ID del componente desde el tab
|
||||
const componentId = activeTab.id.replace('Tab', '');
|
||||
|
||||
// Recopilar todos los campos del formulario activo
|
||||
const formData = collectFormData(activeTab);
|
||||
|
||||
// Mostrar loading en el botón
|
||||
const saveButton = document.getElementById('saveSettings');
|
||||
const originalText = saveButton.innerHTML;
|
||||
saveButton.disabled = true;
|
||||
saveButton.innerHTML = '<i class="bi bi-hourglass-split me-1"></i> Guardando...';
|
||||
|
||||
// Enviar por AJAX
|
||||
fetch(ajaxurl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
action: 'roi_save_component_settings',
|
||||
nonce: roiAdminDashboard.nonce,
|
||||
component: componentId,
|
||||
settings: JSON.stringify(formData)
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotice('success', data.data.message || 'Cambios guardados correctamente.');
|
||||
} else {
|
||||
showNotice('error', data.data.message || 'Error al guardar los cambios.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotice('error', 'Error de conexión al guardar los cambios.');
|
||||
})
|
||||
.finally(() => {
|
||||
saveButton.disabled = false;
|
||||
saveButton.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancela los cambios y recarga la página
|
||||
*/
|
||||
function handleCancelChanges(e) {
|
||||
e.preventDefault();
|
||||
showConfirmModal(
|
||||
'Cancelar cambios',
|
||||
'¿Descartar todos los cambios no guardados?',
|
||||
function() {
|
||||
location.reload();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restaura los valores por defecto de un componente
|
||||
*
|
||||
* @param {Event} e Evento del click
|
||||
* @param {string} componentId ID del componente a resetear
|
||||
* @param {HTMLElement} resetButton Elemento del botón que disparó el evento
|
||||
*/
|
||||
function handleResetDefaults(e, componentId, resetButton) {
|
||||
e.preventDefault();
|
||||
|
||||
showConfirmModal(
|
||||
'Restaurar valores por defecto',
|
||||
'¿Restaurar todos los valores a los valores por defecto? Esta acción no se puede deshacer.',
|
||||
function() {
|
||||
// Mostrar loading en el botón
|
||||
const originalText = resetButton.innerHTML;
|
||||
resetButton.disabled = true;
|
||||
resetButton.innerHTML = '<i class="bi bi-hourglass-split me-1"></i> Restaurando...';
|
||||
|
||||
// Enviar por AJAX
|
||||
fetch(ajaxurl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
action: 'roi_reset_component_defaults',
|
||||
nonce: roiAdminDashboard.nonce,
|
||||
component: componentId
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotice('success', data.data.message || 'Valores restaurados correctamente.');
|
||||
// Recargar preservando el tab activo
|
||||
setTimeout(() => {
|
||||
const activeTabId = getActiveTabId();
|
||||
if (activeTabId) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('admin-tab', activeTabId);
|
||||
window.location.href = url.toString();
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
}, 1500);
|
||||
} else {
|
||||
showNotice('error', data.data.message || 'Error al restaurar los valores.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotice('error', 'Error de conexión al restaurar los valores.');
|
||||
})
|
||||
.finally(() => {
|
||||
resetButton.disabled = false;
|
||||
resetButton.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recopila los datos del formulario del tab activo
|
||||
*/
|
||||
function collectFormData(container) {
|
||||
const formData = {};
|
||||
|
||||
// Inputs de texto, textarea, select, color, number, email, password
|
||||
const textInputs = container.querySelectorAll('input[type="text"], input[type="url"], input[type="color"], input[type="number"], input[type="email"], input[type="password"], textarea, select');
|
||||
textInputs.forEach(input => {
|
||||
if (input.id) {
|
||||
formData[input.id] = input.value;
|
||||
}
|
||||
});
|
||||
|
||||
// Checkboxes (switches)
|
||||
const checkboxes = container.querySelectorAll('input[type="checkbox"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
if (checkbox.id) {
|
||||
formData[checkbox.id] = checkbox.checked;
|
||||
}
|
||||
});
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra un toast de Bootstrap
|
||||
*/
|
||||
function showNotice(type, message) {
|
||||
// Mapear tipos
|
||||
const typeMap = {
|
||||
'success': { bg: 'success', icon: 'bi-check-circle-fill', text: 'Éxito' },
|
||||
'error': { bg: 'danger', icon: 'bi-x-circle-fill', text: 'Error' },
|
||||
'warning': { bg: 'warning', icon: 'bi-exclamation-triangle-fill', text: 'Advertencia' },
|
||||
'info': { bg: 'info', icon: 'bi-info-circle-fill', text: 'Información' }
|
||||
};
|
||||
|
||||
const config = typeMap[type] || typeMap['info'];
|
||||
|
||||
// Crear container de toasts si no existe
|
||||
let toastContainer = document.getElementById('roiToastContainer');
|
||||
if (!toastContainer) {
|
||||
toastContainer = document.createElement('div');
|
||||
toastContainer.id = 'roiToastContainer';
|
||||
toastContainer.className = 'toast-container position-fixed start-50 translate-middle-x';
|
||||
toastContainer.style.top = '60px';
|
||||
toastContainer.style.zIndex = '999999';
|
||||
document.body.appendChild(toastContainer);
|
||||
}
|
||||
|
||||
// Crear toast
|
||||
const toastId = 'toast-' + Date.now();
|
||||
const toastHTML = `
|
||||
<div id="${toastId}" class="toast align-items-center text-white bg-${config.bg} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<i class="bi ${config.icon} me-2"></i>
|
||||
<strong>${escapeHtml(message)}</strong>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
toastContainer.insertAdjacentHTML('beforeend', toastHTML);
|
||||
|
||||
// Mostrar toast
|
||||
const toastElement = document.getElementById(toastId);
|
||||
const toast = new bootstrap.Toast(toastElement, {
|
||||
autohide: true,
|
||||
delay: 5000
|
||||
});
|
||||
|
||||
toast.show();
|
||||
|
||||
// Eliminar del DOM después de ocultarse
|
||||
toastElement.addEventListener('hidden.bs.toast', function() {
|
||||
toastElement.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Muestra un modal de confirmación de Bootstrap
|
||||
*/
|
||||
function showConfirmModal(title, message, onConfirm) {
|
||||
// Crear modal si no existe
|
||||
let modal = document.getElementById('roiConfirmModal');
|
||||
if (!modal) {
|
||||
const modalHTML = `
|
||||
<div class="modal fade" id="roiConfirmModal" tabindex="-1" aria-labelledby="roiConfirmModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-bottom: none;">
|
||||
<h5 class="modal-title text-white" id="roiConfirmModalLabel">
|
||||
<i class="bi bi-question-circle me-2" style="color: #FF8600;"></i>
|
||||
<span id="roiConfirmModalTitle">Confirmar</span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="roiConfirmModalBody" style="padding: 2rem;">
|
||||
Mensaje de confirmación
|
||||
</div>
|
||||
<div class="modal-footer" style="border-top: 1px solid #dee2e6; padding: 1rem 1.5rem;">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
||||
<i class="bi bi-x-circle me-1"></i>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="button" class="btn text-white" id="roiConfirmModalConfirm" style="background-color: #FF8600; border-color: #FF8600;">
|
||||
<i class="bi bi-check-circle me-1"></i>
|
||||
Confirmar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||
modal = document.getElementById('roiConfirmModal');
|
||||
}
|
||||
|
||||
// Actualizar contenido
|
||||
document.getElementById('roiConfirmModalTitle').textContent = title;
|
||||
document.getElementById('roiConfirmModalBody').textContent = message;
|
||||
|
||||
// Configurar callback
|
||||
const confirmButton = document.getElementById('roiConfirmModalConfirm');
|
||||
const newConfirmButton = confirmButton.cloneNode(true);
|
||||
confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton);
|
||||
|
||||
newConfirmButton.addEventListener('click', function() {
|
||||
const bsModal = bootstrap.Modal.getInstance(modal);
|
||||
bsModal.hide();
|
||||
if (typeof onConfirm === 'function') {
|
||||
onConfirm();
|
||||
}
|
||||
});
|
||||
|
||||
// Mostrar modal
|
||||
const bsModal = new bootstrap.Modal(modal);
|
||||
bsModal.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa los color pickers para mostrar el valor HEX
|
||||
*/
|
||||
function initializeColorPickers() {
|
||||
const colorPickers = document.querySelectorAll('input[type="color"]');
|
||||
|
||||
colorPickers.forEach(picker => {
|
||||
// Elemento donde se muestra el valor HEX
|
||||
const valueDisplay = document.getElementById(picker.id + 'Value');
|
||||
|
||||
if (valueDisplay) {
|
||||
picker.addEventListener('input', function() {
|
||||
valueDisplay.textContent = this.value.toUpperCase();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
104
Admin/Infrastructure/Ui/ComponentGroupRegistry.php
Normal file
104
Admin/Infrastructure/Ui/ComponentGroupRegistry.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Infrastructure\Ui;
|
||||
|
||||
/**
|
||||
* Registro de grupos de componentes para el admin panel
|
||||
*
|
||||
* Responsabilidad única: Gestionar la configuración de grupos
|
||||
* y la asignación de componentes a grupos.
|
||||
*
|
||||
* @package ROITheme\Admin\Infrastructure\Ui
|
||||
*/
|
||||
final class ComponentGroupRegistry
|
||||
{
|
||||
/**
|
||||
* Obtiene los grupos de componentes con sus configuraciones
|
||||
*
|
||||
* Los grupos son extensibles via filtro WordPress para permitir
|
||||
* que plugins agreguen componentes a grupos existentes o creen nuevos.
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public function getGroups(): array
|
||||
{
|
||||
// Design System: Todos los grupos usan el mismo gradiente Navy (#0E2337 → #1e3a5f)
|
||||
// No se requiere propiedad 'color' ya que está definido en CSS
|
||||
$defaultGroups = [
|
||||
'header-navigation' => [
|
||||
'label' => __('Header & Navegación', 'roi-theme'),
|
||||
'icon' => 'bi-layout-text-window',
|
||||
'description' => __('Barras superiores, menú y pie de página', 'roi-theme'),
|
||||
'components' => ['top-notification-bar', 'navbar', 'footer']
|
||||
],
|
||||
'main-content' => [
|
||||
'label' => __('Contenido Principal', 'roi-theme'),
|
||||
'icon' => 'bi-file-richtext',
|
||||
'description' => __('Secciones principales de páginas y posts', 'roi-theme'),
|
||||
'components' => ['hero', 'featured-image', 'table-of-contents', 'related-post', 'archive-header', 'post-grid']
|
||||
],
|
||||
'ctas-conversion' => [
|
||||
'label' => __('CTAs & Conversión', 'roi-theme'),
|
||||
'icon' => 'bi-lightning-charge',
|
||||
'description' => __('Llamadas a la acción y elementos de conversión', 'roi-theme'),
|
||||
'components' => ['cta-lets-talk', 'cta-box-sidebar', 'cta-post']
|
||||
],
|
||||
'engagement' => [
|
||||
'label' => __('Engagement', 'roi-theme'),
|
||||
'icon' => 'bi-share',
|
||||
'description' => __('Interacción social y compartir', 'roi-theme'),
|
||||
'components' => ['social-share']
|
||||
],
|
||||
'forms' => [
|
||||
'label' => __('Formularios', 'roi-theme'),
|
||||
'icon' => 'bi-envelope-paper',
|
||||
'description' => __('Formularios de contacto y captura', 'roi-theme'),
|
||||
'components' => ['contact-form']
|
||||
],
|
||||
'settings' => [
|
||||
'label' => __('Configuración', 'roi-theme'),
|
||||
'icon' => 'bi-gear',
|
||||
'description' => __('Ajustes globales del tema y monetización', 'roi-theme'),
|
||||
'components' => ['theme-settings', 'adsense-placement', 'custom-css-manager']
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Filtro para extender o modificar los grupos de componentes
|
||||
*
|
||||
* @param array<string, array<string, mixed>> $groups Grupos por defecto
|
||||
* @return array<string, array<string, mixed>> Grupos modificados
|
||||
*/
|
||||
return apply_filters('roi_theme_component_groups', $defaultGroups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el grupo al que pertenece un componente
|
||||
*
|
||||
* @param string $componentId ID del componente en kebab-case
|
||||
* @return string|null ID del grupo o null si no pertenece a ninguno
|
||||
*/
|
||||
public function getGroupForComponent(string $componentId): ?string
|
||||
{
|
||||
foreach ($this->getGroups() as $groupId => $group) {
|
||||
if (in_array($componentId, $group['components'], true)) {
|
||||
return $groupId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene la información de un grupo específico
|
||||
*
|
||||
* @param string $groupId ID del grupo
|
||||
* @return array<string, mixed>|null Datos del grupo o null
|
||||
*/
|
||||
public function getGroup(string $groupId): ?array
|
||||
{
|
||||
$groups = $this->getGroups();
|
||||
return $groups[$groupId] ?? null;
|
||||
}
|
||||
}
|
||||
48
Admin/Infrastructure/Ui/Views/dashboard.php
Normal file
48
Admin/Infrastructure/Ui/Views/dashboard.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/**
|
||||
* ROI Theme - Panel de Administración Principal
|
||||
*
|
||||
* Nueva UI con sistema de Cards/Grupos (App-Style Navigation)
|
||||
*
|
||||
* @var AdminDashboardRenderer $this
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Prevenir acceso directo
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$components = $this->getComponents();
|
||||
$groups = $this->getComponentGroups();
|
||||
|
||||
// =====================================================
|
||||
// SANITIZACIÓN OBLIGATORIA según estándares WordPress
|
||||
// =====================================================
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Solo lectura de parámetro para UI
|
||||
$activeComponent = null;
|
||||
if (isset($_GET['component'])) {
|
||||
$requestedComponent = sanitize_text_field(wp_unslash($_GET['component']));
|
||||
// Validar que el componente exista
|
||||
if (array_key_exists($requestedComponent, $components)) {
|
||||
$activeComponent = $requestedComponent;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="wrap roi-admin-panel">
|
||||
|
||||
<?php if ($activeComponent !== null): ?>
|
||||
<!-- =====================================================
|
||||
Vista de Componente Individual
|
||||
===================================================== -->
|
||||
<?php include __DIR__ . '/partials/component-view.php'; ?>
|
||||
<?php else: ?>
|
||||
<!-- =====================================================
|
||||
Vista Home: Grupos y Cards
|
||||
===================================================== -->
|
||||
<?php include __DIR__ . '/partials/groups-home.php'; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
</div><!-- /wrap -->
|
||||
48
Admin/Infrastructure/Ui/Views/partials/breadcrumb.php
Normal file
48
Admin/Infrastructure/Ui/Views/partials/breadcrumb.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/**
|
||||
* Breadcrumb de navegación
|
||||
*
|
||||
* @var AdminDashboardRenderer $this
|
||||
* @var string $activeComponent ID del componente activo
|
||||
* @var array<string, array<string, mixed>> $groups Grupos de componentes
|
||||
* @var array<string, array<string, string>> $components Componentes disponibles
|
||||
* @var array<string, mixed>|null $group Grupo del componente activo
|
||||
* @var array<string, string>|null $component Datos del componente activo
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<nav class="roi-breadcrumb mb-4" aria-label="<?php echo esc_attr__('Navegación', 'roi-theme'); ?>">
|
||||
<div class="d-flex align-items-center flex-wrap gap-2">
|
||||
<!-- Botón Volver -->
|
||||
<button type="button" class="roi-back-to-home btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>
|
||||
<?php echo esc_html__('Volver', 'roi-theme'); ?>
|
||||
</button>
|
||||
|
||||
<!-- Separador -->
|
||||
<span class="roi-breadcrumb__separator text-muted">/</span>
|
||||
|
||||
<!-- Grupo -->
|
||||
<?php if ($group): ?>
|
||||
<span class="roi-breadcrumb__group">
|
||||
<i class="bi <?php echo esc_attr($group['icon']); ?> me-1" style="color: <?php echo esc_attr($group['color']); ?>;"></i>
|
||||
<?php echo esc_html($group['label']); ?>
|
||||
</span>
|
||||
<span class="roi-breadcrumb__separator text-muted">/</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Componente actual -->
|
||||
<?php if ($component): ?>
|
||||
<span class="roi-breadcrumb__current fw-semibold" aria-current="page" style="color: #FF8600;">
|
||||
<i class="bi <?php echo esc_attr($component['icon']); ?> me-1"></i>
|
||||
<?php echo esc_html($component['label']); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</nav>
|
||||
81
Admin/Infrastructure/Ui/Views/partials/component-view.php
Normal file
81
Admin/Infrastructure/Ui/Views/partials/component-view.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?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>
|
||||
|
||||
<?php
|
||||
// Componentes con sistema de guardado propio (CRUD de entidades)
|
||||
$componentsWithOwnSaveSystem = ['custom-css-manager'];
|
||||
|
||||
if (!in_array($activeComponent, $componentsWithOwnSaveSystem, true)):
|
||||
?>
|
||||
<!-- 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>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
92
Admin/Infrastructure/Ui/Views/partials/groups-home.php
Normal file
92
Admin/Infrastructure/Ui/Views/partials/groups-home.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
/**
|
||||
* Vista Home: Grupos de componentes con mini-cards (Improved Version)
|
||||
*
|
||||
* @var AdminDashboardRenderer $this
|
||||
* @var array<string, array<string, mixed>> $groups Grupos de componentes
|
||||
* @var array<string, array<string, string>> $components Componentes disponibles
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<div id="roi-home-view">
|
||||
<!-- Header Mejorado -->
|
||||
<div class="roi-home-header">
|
||||
<div class="roi-home-header__pattern"></div>
|
||||
<div class="roi-home-header__content">
|
||||
<div class="roi-home-header__icon-wrapper">
|
||||
<i class="bi bi-grid-3x3-gap-fill roi-home-header__icon"></i>
|
||||
</div>
|
||||
<div class="roi-home-header__text">
|
||||
<h1 class="roi-home-header__title">
|
||||
<?php echo esc_html__('Panel de Administración ROI Theme', 'roi-theme'); ?>
|
||||
</h1>
|
||||
<p class="roi-home-header__subtitle">
|
||||
<?php echo esc_html__('Selecciona un componente para configurarlo y personalizarlo', 'roi-theme'); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grid de Grupos Mejorado -->
|
||||
<div class="roi-groups-grid">
|
||||
<?php
|
||||
$delay = 0;
|
||||
foreach ($groups as $groupId => $group):
|
||||
?>
|
||||
<div class="roi-group-card"
|
||||
data-group-id="<?php echo esc_attr($groupId); ?>"
|
||||
style="animation-delay: <?php echo esc_attr($delay . 's'); ?>">
|
||||
<div class="roi-group-card__glow"></div>
|
||||
|
||||
<div class="roi-group-card__header">
|
||||
<div class="roi-group-card__icon-wrapper">
|
||||
<i class="bi <?php echo esc_attr($group['icon']); ?> roi-group-card__icon"></i>
|
||||
</div>
|
||||
<div class="roi-group-card__header-text">
|
||||
<h3 class="roi-group-card__title">
|
||||
<?php echo esc_html($group['label']); ?>
|
||||
</h3>
|
||||
<p class="roi-group-card__description">
|
||||
<?php echo esc_html($group['description']); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="roi-components-grid">
|
||||
<?php foreach ($group['components'] as $componentId): ?>
|
||||
<?php if (isset($components[$componentId])): ?>
|
||||
<?php $component = $components[$componentId]; ?>
|
||||
<button type="button"
|
||||
class="roi-component-minicard"
|
||||
data-component-id="<?php echo esc_attr($componentId); ?>"
|
||||
aria-label="<?php echo esc_attr(
|
||||
sprintf(
|
||||
/* translators: %s: component label */
|
||||
__('Configurar %s', 'roi-theme'),
|
||||
$component['label']
|
||||
)
|
||||
); ?>">
|
||||
<div class="roi-component-minicard__icon-bg">
|
||||
<i class="bi <?php echo esc_attr($component['icon']); ?> roi-component-minicard__icon"></i>
|
||||
</div>
|
||||
<span class="roi-component-minicard__label">
|
||||
<?php echo esc_html($component['label']); ?>
|
||||
</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<?php
|
||||
$delay += 0.1;
|
||||
endforeach;
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Navbar\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Navbar
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class NavbarFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'navbar';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'navbarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'navbarShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'navbarShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'navbarSticky' => ['group' => 'visibility', 'attribute' => 'sticky_enabled'],
|
||||
'navbarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'navbarVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'navbarVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'navbarVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'navbarVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'navbarVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'navbarExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'navbarExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'navbarExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'navbarExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Layout
|
||||
'navbarContainerType' => ['group' => 'layout', 'attribute' => 'container_type'],
|
||||
'navbarPaddingVertical' => ['group' => 'layout', 'attribute' => 'padding_vertical'],
|
||||
'navbarZIndex' => ['group' => 'layout', 'attribute' => 'z_index'],
|
||||
|
||||
// Behavior
|
||||
'navbarMenuLocation' => ['group' => 'behavior', 'attribute' => 'menu_location'],
|
||||
'navbarCustomMenuId' => ['group' => 'behavior', 'attribute' => 'custom_menu_id'],
|
||||
'navbarEnableDropdowns' => ['group' => 'behavior', 'attribute' => 'enable_dropdowns'],
|
||||
'navbarMobileBreakpoint' => ['group' => 'behavior', 'attribute' => 'mobile_breakpoint'],
|
||||
|
||||
// Media (Logo/Marca)
|
||||
'navbarShowBrand' => ['group' => 'media', 'attribute' => 'show_brand'],
|
||||
'navbarUseLogo' => ['group' => 'media', 'attribute' => 'use_logo'],
|
||||
'navbarLogoUrl' => ['group' => 'media', 'attribute' => 'logo_url'],
|
||||
'navbarLogoHeight' => ['group' => 'media', 'attribute' => 'logo_height'],
|
||||
'navbarBrandText' => ['group' => 'media', 'attribute' => 'brand_text'],
|
||||
'navbarBrandFontSize' => ['group' => 'media', 'attribute' => 'brand_font_size'],
|
||||
'navbarBrandColor' => ['group' => 'media', 'attribute' => 'brand_color'],
|
||||
'navbarBrandHoverColor' => ['group' => 'media', 'attribute' => 'brand_hover_color'],
|
||||
|
||||
// Links
|
||||
'linksTextColor' => ['group' => 'links', 'attribute' => 'text_color'],
|
||||
'linksHoverColor' => ['group' => 'links', 'attribute' => 'hover_color'],
|
||||
'linksActiveColor' => ['group' => 'links', 'attribute' => 'active_color'],
|
||||
'linksFontSize' => ['group' => 'links', 'attribute' => 'font_size'],
|
||||
'linksFontWeight' => ['group' => 'links', 'attribute' => 'font_weight'],
|
||||
'linksPadding' => ['group' => 'links', 'attribute' => 'padding'],
|
||||
'linksBorderRadius' => ['group' => 'links', 'attribute' => 'border_radius'],
|
||||
'linksShowUnderline' => ['group' => 'links', 'attribute' => 'show_underline_effect'],
|
||||
'linksUnderlineColor' => ['group' => 'links', 'attribute' => 'underline_color'],
|
||||
|
||||
// Visual Effects (Dropdown)
|
||||
'dropdownBgColor' => ['group' => 'visual_effects', 'attribute' => 'background_color'],
|
||||
'dropdownBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'dropdownShadow' => ['group' => 'visual_effects', 'attribute' => 'shadow'],
|
||||
'dropdownItemColor' => ['group' => 'visual_effects', 'attribute' => 'item_color'],
|
||||
'dropdownItemHoverBg' => ['group' => 'visual_effects', 'attribute' => 'item_hover_background'],
|
||||
'dropdownItemPadding' => ['group' => 'visual_effects', 'attribute' => 'item_padding'],
|
||||
'dropdownMaxHeight' => ['group' => 'visual_effects', 'attribute' => 'dropdown_max_height'],
|
||||
|
||||
// Colors (Navbar styles)
|
||||
'navbarBgColor' => ['group' => 'colors', 'attribute' => 'background_color'],
|
||||
'navbarBoxShadow' => ['group' => 'colors', 'attribute' => 'box_shadow'],
|
||||
];
|
||||
}
|
||||
}
|
||||
582
Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php
Normal file
582
Admin/Navbar/Infrastructure/Ui/NavbarFormBuilder.php
Normal file
@@ -0,0 +1,582 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Navbar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
final class NavbarFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
// Header
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
// Layout 2 columnas
|
||||
$html .= '<div class="row g-3">';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildLayoutGroup($componentId);
|
||||
$html .= $this->buildBehaviorGroup($componentId);
|
||||
$html .= $this->buildMediaGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildLinksGroup($componentId);
|
||||
$html .= $this->buildVisualEffectsGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-menu-button-wide me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuración de Navbar';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Personaliza el menú de navegación principal del sitio';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="navbar">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Activación y Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Switch: Enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarEnabled" name="visibility[is_enabled]" ';
|
||||
$html .= checked($enabled, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarEnabled">';
|
||||
$html .= ' <strong>Activar Navbar</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Mobile
|
||||
$showMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarShowMobile" name="visibility[show_on_mobile]" ';
|
||||
$html .= checked($showMobile, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarShowMobile">';
|
||||
$html .= ' <strong>Mostrar en Mobile</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Desktop
|
||||
$showDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarShowDesktop" name="visibility[show_on_desktop]" ';
|
||||
$html .= checked($showDesktop, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarShowDesktop">';
|
||||
$html .= ' <strong>Mostrar en Desktop</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', true);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', true);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'navbar');
|
||||
|
||||
// Switch: Sticky
|
||||
$sticky = $this->renderer->getFieldValue($componentId, 'visibility', 'sticky_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarSticky" name="visibility[sticky_enabled]" ';
|
||||
$html .= checked($sticky, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarSticky">';
|
||||
$html .= ' <strong>Navbar fijo (sticky)</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: CSS Crítico
|
||||
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarIsCritical" name="visibility[is_critical]" ';
|
||||
$html .= checked($isCritical, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarIsCritical">';
|
||||
$html .= ' <strong>CSS Crítico</strong>';
|
||||
$html .= ' <small class="text-muted d-block">Inyectar CSS en <head> para optimizar LCP</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildLayoutGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-layout-sidebar me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Layout y Estructura';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Container Type
|
||||
$containerType = $this->renderer->getFieldValue($componentId, 'layout', 'container_type', 'container');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarContainerType" class="form-label small mb-1 fw-semibold">Tipo de contenedor</label>';
|
||||
$html .= ' <select id="navbarContainerType" name="layout[container_type]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="container" ' . selected($containerType, 'container', false) . '>Container (ancho fijo)</option>';
|
||||
$html .= ' <option value="container-fluid" ' . selected($containerType, 'container-fluid', false) . '>Container Fluid (ancho completo)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Padding Vertical
|
||||
$paddingVertical = $this->renderer->getFieldValue($componentId, 'layout', 'padding_vertical', '0.75rem 0');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarPaddingVertical" class="form-label small mb-1 fw-semibold">Padding vertical</label>';
|
||||
$html .= ' <input type="text" id="navbarPaddingVertical" name="layout[padding_vertical]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($paddingVertical) . '" placeholder="0.75rem 0">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Z-index
|
||||
$zIndex = $this->renderer->getFieldValue($componentId, 'layout', 'z_index', '1030');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="navbarZIndex" class="form-label small mb-1 fw-semibold">Z-index</label>';
|
||||
$html .= ' <input type="text" id="navbarZIndex" name="layout[z_index]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($zIndex) . '" placeholder="1030">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildBehaviorGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-list me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuración del Menú';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Menu Location
|
||||
$menuLocation = $this->renderer->getFieldValue($componentId, 'behavior', 'menu_location', 'primary');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarMenuLocation" class="form-label small mb-1 fw-semibold">Ubicación del menú</label>';
|
||||
$html .= ' <select id="navbarMenuLocation" name="behavior[menu_location]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="primary" ' . selected($menuLocation, 'primary', false) . '>Menú Principal</option>';
|
||||
$html .= ' <option value="secondary" ' . selected($menuLocation, 'secondary', false) . '>Menú Secundario</option>';
|
||||
$html .= ' <option value="custom" ' . selected($menuLocation, 'custom', false) . '>Menú personalizado</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Custom Menu ID
|
||||
$customMenuId = $this->renderer->getFieldValue($componentId, 'behavior', 'custom_menu_id', '0');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarCustomMenuId" class="form-label small mb-1 fw-semibold">ID del menú personalizado</label>';
|
||||
$html .= ' <input type="text" id="navbarCustomMenuId" name="behavior[custom_menu_id]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($customMenuId) . '" placeholder="0">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Enable Dropdowns
|
||||
$enableDropdowns = $this->renderer->getFieldValue($componentId, 'behavior', 'enable_dropdowns', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarEnableDropdowns" name="behavior[enable_dropdowns]" ';
|
||||
$html .= checked($enableDropdowns, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarEnableDropdowns">';
|
||||
$html .= ' <strong>Habilitar submenús desplegables</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Mobile Breakpoint
|
||||
$mobileBreakpoint = $this->renderer->getFieldValue($componentId, 'behavior', 'mobile_breakpoint', 'lg');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="navbarMobileBreakpoint" class="form-label small mb-1 fw-semibold">Breakpoint para menú móvil</label>';
|
||||
$html .= ' <select id="navbarMobileBreakpoint" name="behavior[mobile_breakpoint]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="sm" ' . selected($mobileBreakpoint, 'sm', false) . '>Small (576px)</option>';
|
||||
$html .= ' <option value="md" ' . selected($mobileBreakpoint, 'md', false) . '>Medium (768px)</option>';
|
||||
$html .= ' <option value="lg" ' . selected($mobileBreakpoint, 'lg', false) . '>Large (992px)</option>';
|
||||
$html .= ' <option value="xl" ' . selected($mobileBreakpoint, 'xl', false) . '>Extra Large (1200px)</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildMediaGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-image me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Logo/Marca';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Show Brand
|
||||
$showBrand = $this->renderer->getFieldValue($componentId, 'media', 'show_brand', false);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarShowBrand" name="media[show_brand]" ';
|
||||
$html .= checked($showBrand, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarShowBrand">';
|
||||
$html .= ' <strong>Mostrar logo/marca</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Use Logo
|
||||
$useLogo = $this->renderer->getFieldValue($componentId, 'media', 'use_logo', false);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarUseLogo" name="media[use_logo]" ';
|
||||
$html .= checked($useLogo, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarUseLogo">';
|
||||
$html .= ' <strong>Usar logo (imagen)</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Logo URL
|
||||
$logoUrl = $this->renderer->getFieldValue($componentId, 'media', 'logo_url', '');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarLogoUrl" class="form-label small mb-1 fw-semibold">URL del logo</label>';
|
||||
$html .= ' <input type="text" id="navbarLogoUrl" name="media[logo_url]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($logoUrl) . '" placeholder="https://...">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Logo Height
|
||||
$logoHeight = $this->renderer->getFieldValue($componentId, 'media', 'logo_height', '40px');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarLogoHeight" class="form-label small mb-1 fw-semibold">Altura del logo</label>';
|
||||
$html .= ' <input type="text" id="navbarLogoHeight" name="media[logo_height]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($logoHeight) . '" placeholder="40px">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Brand Text
|
||||
$brandText = $this->renderer->getFieldValue($componentId, 'media', 'brand_text', 'Mi Sitio');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarBrandText" class="form-label small mb-1 fw-semibold">Texto de la marca</label>';
|
||||
$html .= ' <input type="text" id="navbarBrandText" name="media[brand_text]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($brandText) . '" maxlength="50">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Brand Font Size
|
||||
$brandFontSize = $this->renderer->getFieldValue($componentId, 'media', 'brand_font_size', '1.5rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarBrandFontSize" class="form-label small mb-1 fw-semibold">Tamaño de fuente</label>';
|
||||
$html .= ' <input type="text" id="navbarBrandFontSize" name="media[brand_font_size]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($brandFontSize) . '" placeholder="1.5rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Brand Color
|
||||
$brandColor = $this->renderer->getFieldValue($componentId, 'media', 'brand_color', '#FFFFFF');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarBrandColor" class="form-label small mb-1 fw-semibold">Color de la marca</label>';
|
||||
$html .= ' <input type="color" id="navbarBrandColor" name="media[brand_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($brandColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Brand Hover Color
|
||||
$brandHoverColor = $this->renderer->getFieldValue($componentId, 'media', 'brand_hover_color', '#FF8600');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="navbarBrandHoverColor" class="form-label small mb-1 fw-semibold">Color hover de la marca</label>';
|
||||
$html .= ' <input type="color" id="navbarBrandHoverColor" name="media[brand_hover_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($brandHoverColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildLinksGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-link-45deg me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Estilos de Enlaces';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Text Color
|
||||
$textColor = $this->renderer->getFieldValue($componentId, 'links', 'text_color', '#FFFFFF');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksTextColor" class="form-label small mb-1 fw-semibold">Color del texto</label>';
|
||||
$html .= ' <input type="color" id="linksTextColor" name="links[text_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($textColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Hover Color
|
||||
$hoverColor = $this->renderer->getFieldValue($componentId, 'links', 'hover_color', '#FF8600');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksHoverColor" class="form-label small mb-1 fw-semibold">Color hover</label>';
|
||||
$html .= ' <input type="color" id="linksHoverColor" name="links[hover_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($hoverColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Active Color
|
||||
$activeColor = $this->renderer->getFieldValue($componentId, 'links', 'active_color', '#FF8600');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksActiveColor" class="form-label small mb-1 fw-semibold">Color del item activo</label>';
|
||||
$html .= ' <input type="color" id="linksActiveColor" name="links[active_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($activeColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Font Size
|
||||
$fontSize = $this->renderer->getFieldValue($componentId, 'links', 'font_size', '0.9rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksFontSize" class="form-label small mb-1 fw-semibold">Tamaño de fuente</label>';
|
||||
$html .= ' <input type="text" id="linksFontSize" name="links[font_size]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($fontSize) . '" placeholder="0.9rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Font Weight
|
||||
$fontWeight = $this->renderer->getFieldValue($componentId, 'links', 'font_weight', '500');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksFontWeight" class="form-label small mb-1 fw-semibold">Grosor de fuente</label>';
|
||||
$html .= ' <input type="text" id="linksFontWeight" name="links[font_weight]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($fontWeight) . '" placeholder="500">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Padding
|
||||
$padding = $this->renderer->getFieldValue($componentId, 'links', 'padding', '0.5rem 0.65rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksPadding" class="form-label small mb-1 fw-semibold">Padding de enlaces</label>';
|
||||
$html .= ' <input type="text" id="linksPadding" name="links[padding]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($padding) . '" placeholder="0.5rem 0.65rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Border Radius
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'links', 'border_radius', '4px');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="linksBorderRadius" class="form-label small mb-1 fw-semibold">Border radius hover</label>';
|
||||
$html .= ' <input type="text" id="linksBorderRadius" name="links[border_radius]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '" placeholder="4px">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Show Underline Effect
|
||||
$showUnderline = $this->renderer->getFieldValue($componentId, 'links', 'show_underline_effect', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="linksShowUnderline" name="links[show_underline_effect]" ';
|
||||
$html .= checked($showUnderline, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="linksShowUnderline">';
|
||||
$html .= ' <strong>Mostrar efecto de subrayado</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Underline Color
|
||||
$underlineColor = $this->renderer->getFieldValue($componentId, 'links', 'underline_color', '#FF8600');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="linksUnderlineColor" class="form-label small mb-1 fw-semibold">Color del subrayado</label>';
|
||||
$html .= ' <input type="color" id="linksUnderlineColor" name="links[underline_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($underlineColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisualEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-chevron-down me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Estilos de Dropdown';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Background Color
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'visual_effects', 'background_color', '#FFFFFF');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownBgColor" class="form-label small mb-1 fw-semibold">Fondo de dropdown</label>';
|
||||
$html .= ' <input type="color" id="dropdownBgColor" name="visual_effects[background_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($bgColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Border Radius
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '8px');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownBorderRadius" class="form-label small mb-1 fw-semibold">Border radius</label>';
|
||||
$html .= ' <input type="text" id="dropdownBorderRadius" name="visual_effects[border_radius]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '" placeholder="8px">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Shadow
|
||||
$shadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'shadow', '0 8px 24px rgba(0, 0, 0, 0.12)');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownShadow" class="form-label small mb-1 fw-semibold">Sombra del dropdown</label>';
|
||||
$html .= ' <input type="text" id="dropdownShadow" name="visual_effects[shadow]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($shadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Item Color
|
||||
$itemColor = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_color', '#495057');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownItemColor" class="form-label small mb-1 fw-semibold">Color de items</label>';
|
||||
$html .= ' <input type="color" id="dropdownItemColor" name="visual_effects[item_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($itemColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Item Hover Background
|
||||
$itemHoverBg = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_hover_background', 'rgba(255, 133, 0, 0.1)');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownItemHoverBg" class="form-label small mb-1 fw-semibold">Fondo hover de items</label>';
|
||||
$html .= ' <input type="text" id="dropdownItemHoverBg" name="visual_effects[item_hover_background]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($itemHoverBg) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Item Padding
|
||||
$itemPadding = $this->renderer->getFieldValue($componentId, 'visual_effects', 'item_padding', '0.625rem 1.25rem');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="dropdownItemPadding" class="form-label small mb-1 fw-semibold">Padding de items</label>';
|
||||
$html .= ' <input type="text" id="dropdownItemPadding" name="visual_effects[item_padding]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($itemPadding) . '" placeholder="0.625rem 1.25rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Dropdown Max Height
|
||||
$dropdownMaxHeight = $this->renderer->getFieldValue($componentId, 'visual_effects', 'dropdown_max_height', '300px');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="dropdownMaxHeight" class="form-label small mb-1 fw-semibold">Altura máxima del dropdown</label>';
|
||||
$html .= ' <input type="text" id="dropdownMaxHeight" name="visual_effects[dropdown_max_height]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($dropdownMaxHeight) . '" placeholder="300px">';
|
||||
$html .= ' <small class="text-muted">Si se excede, aparece scroll vertical</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Estilos del Navbar';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Background Color
|
||||
$navbarBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#1e3a5f');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarBgColor" class="form-label small mb-1 fw-semibold">Color de fondo</label>';
|
||||
$html .= ' <input type="color" id="navbarBgColor" name="colors[background_color]" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($navbarBgColor) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Box Shadow
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'colors', 'box_shadow', '0 4px 12px rgba(30, 58, 95, 0.15)');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="navbarBoxShadow" class="form-label small mb-1 fw-semibold">Sombra del navbar</label>';
|
||||
$html .= ' <input type="text" id="navbarBoxShadow" name="colors[box_shadow]" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
544
Admin/Navbar/Infrastructure/Ui/navbar-design-preview.html
Normal file
544
Admin/Navbar/Infrastructure/Ui/navbar-design-preview.html
Normal file
@@ -0,0 +1,544 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Navbar - Preview de Diseño</title>
|
||||
|
||||
<!-- Bootstrap 5 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background-color: #f0f0f1;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ============================================================
|
||||
TAB: NAVBAR CONFIGURATION
|
||||
============================================================ -->
|
||||
<div class="tab-pane fade show active" id="navbarTab" role="tabpanel">
|
||||
|
||||
<!-- ========================================
|
||||
PATRÓN 1: HEADER CON GRADIENTE
|
||||
======================================== -->
|
||||
<div class="rounded p-4 mb-4 shadow text-white" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h3 class="h4 mb-1 fw-bold">
|
||||
<i class="bi bi-list-ul me-2" style="color: #FF8600;"></i>
|
||||
Configuración de Navbar
|
||||
</h3>
|
||||
<p class="mb-0 small" style="opacity: 0.85;">
|
||||
Personaliza el menú de navegación principal del sitio
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-light" id="resetNavbarDefaults">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
||||
Restaurar valores por defecto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
PATRÓN 2: LAYOUT 2 COLUMNAS
|
||||
======================================== -->
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
<!-- ========================================
|
||||
GRUPO 1: ACTIVACIÓN Y VISIBILIDAD (OBLIGATORIO)
|
||||
PATRÓN 3: CARD CON BORDER-LEFT NAVY
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>
|
||||
Activación y Visibilidad
|
||||
</h5>
|
||||
|
||||
<!-- ⚠️ PATRÓN 4: SWITCHES VERTICALES CON ICONOS (3 OBLIGATORIOS) -->
|
||||
|
||||
<!-- Switch 1: Enabled (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarEnabled" checked>
|
||||
<label class="form-check-label small" for="navbarEnabled" style="color: #495057;">
|
||||
<i class="bi bi-power me-1" style="color: #FF8600;"></i>
|
||||
<strong>Activar Navbar</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Switch 2: Show on Mobile (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarShowOnMobile" checked>
|
||||
<label class="form-check-label small" for="navbarShowOnMobile" style="color: #495057;">
|
||||
<i class="bi bi-phone me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar en Mobile</strong> <span class="text-muted">(<768px)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Switch 3: Show on Desktop (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarShowOnDesktop" checked>
|
||||
<label class="form-check-label small" for="navbarShowOnDesktop" style="color: #495057;">
|
||||
<i class="bi bi-display me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar en Desktop</strong> <span class="text-muted">(≥768px)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campo adicional del schema: show_on_pages (select) -->
|
||||
<div class="mb-2 mt-3">
|
||||
<label for="navbarShowOnPages" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>
|
||||
Mostrar en
|
||||
</label>
|
||||
<select id="navbarShowOnPages" class="form-select form-select-sm">
|
||||
<option value="all" selected>Todas las páginas</option>
|
||||
<option value="home">Solo página de inicio</option>
|
||||
<option value="posts">Solo posts individuales</option>
|
||||
<option value="pages">Solo páginas</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Switch 5: Sticky Enabled -->
|
||||
<div class="mb-0 mt-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarStickyEnabled" checked>
|
||||
<label class="form-check-label small" for="navbarStickyEnabled" style="color: #495057;">
|
||||
<i class="bi bi-pin-angle me-1" style="color: #FF8600;"></i>
|
||||
<strong>Navbar fijo (sticky)</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 2: LAYOUT Y ESTRUCTURA
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-columns-gap me-2" style="color: #FF8600;"></i>
|
||||
Layout y Estructura
|
||||
</h5>
|
||||
|
||||
<!-- container_type (select) -->
|
||||
<div class="mb-2">
|
||||
<label for="navbarContainerType" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-box me-1" style="color: #FF8600;"></i>
|
||||
Tipo de contenedor
|
||||
</label>
|
||||
<select id="navbarContainerType" class="form-select form-select-sm">
|
||||
<option value="container" selected>Container (ancho fijo)</option>
|
||||
<option value="container-fluid">Container Fluid (ancho completo)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- padding_vertical + z_index (compactados) -->
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="navbarPaddingVertical" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-arrows-vertical me-1" style="color: #FF8600;"></i>
|
||||
Padding vertical
|
||||
</label>
|
||||
<input type="text" id="navbarPaddingVertical" class="form-control form-control-sm" value="0.75rem 0">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarZIndex" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-layers me-1" style="color: #FF8600;"></i>
|
||||
Z-index
|
||||
</label>
|
||||
<input type="number" id="navbarZIndex" class="form-control form-control-sm" value="1030" min="1" max="9999">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 3: CONFIGURACIÓN DEL MENÚ
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-gear me-2" style="color: #FF8600;"></i>
|
||||
Configuración del Menú
|
||||
</h5>
|
||||
|
||||
<!-- menu_location + custom_menu_id (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarMenuLocation" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-pin-map me-1" style="color: #FF8600;"></i>
|
||||
Ubicación del menú
|
||||
</label>
|
||||
<select id="navbarMenuLocation" class="form-select form-select-sm">
|
||||
<option value="primary" selected>Menú Principal</option>
|
||||
<option value="secondary">Menú Secundario</option>
|
||||
<option value="custom">Menú personalizado</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarCustomMenuId" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-hash me-1" style="color: #FF8600;"></i>
|
||||
ID del menú
|
||||
</label>
|
||||
<input type="number" id="navbarCustomMenuId" class="form-control form-control-sm" value="0" min="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- enable_dropdowns (switch) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarEnableDropdowns" checked>
|
||||
<label class="form-check-label small" for="navbarEnableDropdowns" style="color: #495057;">
|
||||
<i class="bi bi-chevron-down me-1" style="color: #FF8600;"></i>
|
||||
<strong>Habilitar submenús desplegables</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- mobile_breakpoint (select) -->
|
||||
<div class="mb-0">
|
||||
<label for="navbarMobileBreakpoint" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-phone-landscape me-1" style="color: #FF8600;"></i>
|
||||
Breakpoint para menú móvil
|
||||
</label>
|
||||
<select id="navbarMobileBreakpoint" class="form-select form-select-sm">
|
||||
<option value="sm">Small (576px)</option>
|
||||
<option value="md">Medium (768px)</option>
|
||||
<option value="lg" selected>Large (992px)</option>
|
||||
<option value="xl">Extra Large (1200px)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 4: LOGO/MARCA
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-award me-2" style="color: #FF8600;"></i>
|
||||
Logo/Marca
|
||||
</h5>
|
||||
|
||||
<!-- show_brand (switch) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarShowBrand">
|
||||
<label class="form-check-label small" for="navbarShowBrand" style="color: #495057;">
|
||||
<i class="bi bi-eye me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar logo/marca</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- use_logo (switch) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarUseLogo">
|
||||
<label class="form-check-label small" for="navbarUseLogo" style="color: #495057;">
|
||||
<i class="bi bi-image me-1" style="color: #FF8600;"></i>
|
||||
<strong>Usar logo (imagen)</strong>
|
||||
</label>
|
||||
</div>
|
||||
<small class="text-muted d-block ms-4 mt-1">Usa una imagen en lugar de texto</small>
|
||||
</div>
|
||||
|
||||
<!-- logo_url + logo_height (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-8">
|
||||
<label for="navbarLogoUrl" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>
|
||||
URL del logo
|
||||
</label>
|
||||
<input type="url" id="navbarLogoUrl" class="form-control form-control-sm" placeholder="https://...">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label for="navbarLogoHeight" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-arrows-vertical me-1" style="color: #FF8600;"></i>
|
||||
Altura
|
||||
</label>
|
||||
<input type="text" id="navbarLogoHeight" class="form-control form-control-sm" value="40px">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- brand_text -->
|
||||
<div class="mb-2">
|
||||
<label for="navbarBrandText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Texto de la marca
|
||||
</label>
|
||||
<input type="text" id="navbarBrandText" class="form-control form-control-sm" value="Mi Sitio" maxlength="50">
|
||||
<small class="text-muted">Se muestra si no hay logo</small>
|
||||
</div>
|
||||
|
||||
<!-- brand_font_size + brand_color (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarBrandFontSize" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-type me-1" style="color: #FF8600;"></i>
|
||||
Tamaño fuente
|
||||
</label>
|
||||
<input type="text" id="navbarBrandFontSize" class="form-control form-control-sm" value="1.5rem">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarBrandColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-palette me-1" style="color: #FF8600;"></i>
|
||||
Color
|
||||
</label>
|
||||
<input type="color" id="navbarBrandColor" class="form-control form-control-color w-100" value="#FFFFFF">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- brand_hover_color -->
|
||||
<div class="mb-0">
|
||||
<label for="navbarBrandHoverColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>
|
||||
Color hover
|
||||
</label>
|
||||
<input type="color" id="navbarBrandHoverColor" class="form-control form-control-color w-100" value="#FF8600">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<!-- ========================================
|
||||
GRUPO 5: ESTILOS DEL NAVBAR
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-paint-bucket me-2" style="color: #FF8600;"></i>
|
||||
Estilos del Navbar
|
||||
</h5>
|
||||
|
||||
<!-- background_color -->
|
||||
<div class="mb-2">
|
||||
<label for="navbarBackgroundColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-palette me-1" style="color: #FF8600;"></i>
|
||||
Color de fondo
|
||||
</label>
|
||||
<input type="color" id="navbarBackgroundColor" class="form-control form-control-color w-100" value="#1e3a5f">
|
||||
<small class="text-muted d-block mt-1" id="navbarBackgroundColorValue">#1E3A5F</small>
|
||||
</div>
|
||||
|
||||
<!-- box_shadow -->
|
||||
<div class="mb-0">
|
||||
<label for="navbarBoxShadow" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-droplet me-1" style="color: #FF8600;"></i>
|
||||
Sombra del navbar
|
||||
</label>
|
||||
<input type="text" id="navbarBoxShadow" class="form-control form-control-sm" value="0 4px 12px rgba(30, 58, 95, 0.15)">
|
||||
<small class="text-muted">Sombra CSS (ej: 0 4px 12px rgba(0,0,0,0.15))</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 6: ESTILOS DE ENLACES
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-link-45deg me-2" style="color: #FF8600;"></i>
|
||||
Estilos de Enlaces
|
||||
</h5>
|
||||
|
||||
<!-- COLOR PICKERS EN GRID 3 COLORES -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-4">
|
||||
<label for="navbarTextColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Color texto
|
||||
</label>
|
||||
<input type="color" id="navbarTextColor" class="form-control form-control-color w-100" value="#FFFFFF">
|
||||
<small class="text-muted d-block mt-1" id="navbarTextColorValue">#FFFFFF</small>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label for="navbarHoverColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>
|
||||
Color hover
|
||||
</label>
|
||||
<input type="color" id="navbarHoverColor" class="form-control form-control-color w-100" value="#FF8600">
|
||||
<small class="text-muted d-block mt-1" id="navbarHoverColorValue">#FF8600</small>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label for="navbarActiveColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-check-circle me-1" style="color: #FF8600;"></i>
|
||||
Color activo
|
||||
</label>
|
||||
<input type="color" id="navbarActiveColor" class="form-control form-control-color w-100" value="#FF8600">
|
||||
<small class="text-muted d-block mt-1" id="navbarActiveColorValue">#FF8600</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- font_size + font_weight (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarFontSize" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-type me-1" style="color: #FF8600;"></i>
|
||||
Tamaño fuente
|
||||
</label>
|
||||
<input type="text" id="navbarFontSize" class="form-control form-control-sm" value="0.9rem">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarFontWeight" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Grosor fuente
|
||||
</label>
|
||||
<input type="number" id="navbarFontWeight" class="form-control form-control-sm" value="500" min="100" max="900" step="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- padding + border_radius (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarLinkPadding" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-bounding-box me-1" style="color: #FF8600;"></i>
|
||||
Padding
|
||||
</label>
|
||||
<input type="text" id="navbarLinkPadding" class="form-control form-control-sm" value="0.5rem 0.65rem">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarBorderRadius" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-square me-1" style="color: #FF8600;"></i>
|
||||
Border radius
|
||||
</label>
|
||||
<input type="text" id="navbarBorderRadius" class="form-control form-control-sm" value="4px">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- show_underline_effect (switch) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="navbarShowUnderlineEffect" checked>
|
||||
<label class="form-check-label small" for="navbarShowUnderlineEffect" style="color: #495057;">
|
||||
<i class="bi bi-dash-lg me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar efecto de subrayado</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- underline_color -->
|
||||
<div class="mb-0">
|
||||
<label for="navbarUnderlineColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-palette me-1" style="color: #FF8600;"></i>
|
||||
Color del subrayado
|
||||
</label>
|
||||
<input type="color" id="navbarUnderlineColor" class="form-control form-control-color w-100" value="#FF8600">
|
||||
<small class="text-muted d-block mt-1" id="navbarUnderlineColorValue">#FF8600</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 7: ESTILOS DE DROPDOWN
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-chevron-down me-2" style="color: #FF8600;"></i>
|
||||
Estilos de Dropdown
|
||||
</h5>
|
||||
|
||||
<!-- background_color -->
|
||||
<div class="mb-2">
|
||||
<label for="navbarDropdownBackground" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
|
||||
Fondo dropdown
|
||||
</label>
|
||||
<input type="color" id="navbarDropdownBackground" class="form-control form-control-color w-100" value="#FFFFFF">
|
||||
<small class="text-muted d-block mt-1" id="navbarDropdownBackgroundValue">#FFFFFF</small>
|
||||
</div>
|
||||
|
||||
<!-- border_radius + shadow (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarDropdownBorderRadius" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-square me-1" style="color: #FF8600;"></i>
|
||||
Border radius
|
||||
</label>
|
||||
<input type="text" id="navbarDropdownBorderRadius" class="form-control form-control-sm" value="8px">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarDropdownShadow" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-droplet me-1" style="color: #FF8600;"></i>
|
||||
Sombra
|
||||
</label>
|
||||
<input type="text" id="navbarDropdownShadow" class="form-control form-control-sm" value="0 8px 24px rgba(0,0,0,0.12)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- item_color + item_hover_background -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="navbarDropdownItemColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Color items
|
||||
</label>
|
||||
<input type="color" id="navbarDropdownItemColor" class="form-control form-control-color w-100" value="#495057">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="navbarDropdownItemHoverBg" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
|
||||
Fondo hover
|
||||
</label>
|
||||
<input type="color" id="navbarDropdownItemHoverBg" class="form-control form-control-color w-100" value="#FFF5EB">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- item_padding -->
|
||||
<div class="mb-0">
|
||||
<label for="navbarDropdownItemPadding" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-bounding-box me-1" style="color: #FF8600;"></i>
|
||||
Padding de items
|
||||
</label>
|
||||
<input type="text" id="navbarDropdownItemPadding" class="form-control form-control-sm" value="0.625rem 1.25rem">
|
||||
<small class="text-muted">Espaciado interno de items (ej: 0.625rem 1.25rem)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /tab-pane -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Actualizar valores HEX de color pickers
|
||||
document.querySelectorAll('input[type="color"]').forEach(picker => {
|
||||
const valueDisplay = document.getElementById(picker.id + 'Value');
|
||||
if (valueDisplay) {
|
||||
picker.addEventListener('input', function() {
|
||||
valueDisplay.textContent = this.value.toUpperCase();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Simular reset button
|
||||
document.getElementById('resetNavbarDefaults').addEventListener('click', function() {
|
||||
if (confirm('¿Restaurar todos los valores a los valores por defecto?')) {
|
||||
alert('En producción, esto restauraría los valores del schema JSON');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\PostGrid\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Post Grid
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class PostGridFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'post-grid';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'postGridEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'postGridShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'postGridShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'postGridVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'postGridVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'postGridVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'postGridVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'postGridVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions)
|
||||
'postGridExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'postGridExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'postGridExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'postGridExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'postGridShowThumbnail' => ['group' => 'content', 'attribute' => 'show_thumbnail'],
|
||||
'postGridShowExcerpt' => ['group' => 'content', 'attribute' => 'show_excerpt'],
|
||||
'postGridShowMeta' => ['group' => 'content', 'attribute' => 'show_meta'],
|
||||
'postGridShowCategories' => ['group' => 'content', 'attribute' => 'show_categories'],
|
||||
'postGridExcerptLength' => ['group' => 'content', 'attribute' => 'excerpt_length'],
|
||||
'postGridReadMoreText' => ['group' => 'content', 'attribute' => 'read_more_text'],
|
||||
'postGridNoPostsMessage' => ['group' => 'content', 'attribute' => 'no_posts_message'],
|
||||
|
||||
// Layout
|
||||
'postGridColumnsDesktop' => ['group' => 'layout', 'attribute' => 'columns_desktop'],
|
||||
'postGridColumnsTablet' => ['group' => 'layout', 'attribute' => 'columns_tablet'],
|
||||
'postGridColumnsMobile' => ['group' => 'layout', 'attribute' => 'columns_mobile'],
|
||||
'postGridImagePosition' => ['group' => 'layout', 'attribute' => 'image_position'],
|
||||
|
||||
// Media
|
||||
'postGridFallbackImage' => ['group' => 'media', 'attribute' => 'fallback_image'],
|
||||
'postGridFallbackImageAlt' => ['group' => 'media', 'attribute' => 'fallback_image_alt'],
|
||||
|
||||
// Typography
|
||||
'postGridHeadingLevel' => ['group' => 'typography', 'attribute' => 'heading_level'],
|
||||
'postGridCardTitleSize' => ['group' => 'typography', 'attribute' => 'card_title_size'],
|
||||
'postGridCardTitleWeight' => ['group' => 'typography', 'attribute' => 'card_title_weight'],
|
||||
'postGridExcerptSize' => ['group' => 'typography', 'attribute' => 'excerpt_size'],
|
||||
'postGridMetaSize' => ['group' => 'typography', 'attribute' => 'meta_size'],
|
||||
|
||||
// Colors
|
||||
'postGridCardBgColor' => ['group' => 'colors', 'attribute' => 'card_bg_color'],
|
||||
'postGridCardTitleColor' => ['group' => 'colors', 'attribute' => 'card_title_color'],
|
||||
'postGridCardHoverBgColor' => ['group' => 'colors', 'attribute' => 'card_hover_bg_color'],
|
||||
'postGridCardBorderColor' => ['group' => 'colors', 'attribute' => 'card_border_color'],
|
||||
'postGridCardHoverBorderColor' => ['group' => 'colors', 'attribute' => 'card_hover_border_color'],
|
||||
'postGridExcerptColor' => ['group' => 'colors', 'attribute' => 'excerpt_color'],
|
||||
'postGridMetaColor' => ['group' => 'colors', 'attribute' => 'meta_color'],
|
||||
'postGridCategoryBgColor' => ['group' => 'colors', 'attribute' => 'category_bg_color'],
|
||||
'postGridCategoryTextColor' => ['group' => 'colors', 'attribute' => 'category_text_color'],
|
||||
'postGridPaginationColor' => ['group' => 'colors', 'attribute' => 'pagination_color'],
|
||||
'postGridPaginationActiveBg' => ['group' => 'colors', 'attribute' => 'pagination_active_bg'],
|
||||
'postGridPaginationActiveColor' => ['group' => 'colors', 'attribute' => 'pagination_active_color'],
|
||||
|
||||
// Spacing
|
||||
'postGridGapHorizontal' => ['group' => 'spacing', 'attribute' => 'gap_horizontal'],
|
||||
'postGridGapVertical' => ['group' => 'spacing', 'attribute' => 'gap_vertical'],
|
||||
'postGridCardPadding' => ['group' => 'spacing', 'attribute' => 'card_padding'],
|
||||
'postGridSectionMarginTop' => ['group' => 'spacing', 'attribute' => 'section_margin_top'],
|
||||
'postGridSectionMarginBottom' => ['group' => 'spacing', 'attribute' => 'section_margin_bottom'],
|
||||
|
||||
// Visual Effects
|
||||
'postGridCardBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'card_border_radius'],
|
||||
'postGridCardShadow' => ['group' => 'visual_effects', 'attribute' => 'card_shadow'],
|
||||
'postGridCardHoverShadow' => ['group' => 'visual_effects', 'attribute' => 'card_hover_shadow'],
|
||||
'postGridCardTransition' => ['group' => 'visual_effects', 'attribute' => 'card_transition'],
|
||||
'postGridImageBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'image_border_radius'],
|
||||
];
|
||||
}
|
||||
}
|
||||
781
Admin/PostGrid/Infrastructure/Ui/PostGridFormBuilder.php
Normal file
781
Admin/PostGrid/Infrastructure/Ui/PostGridFormBuilder.php
Normal file
@@ -0,0 +1,781 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\PostGrid\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* PostGridFormBuilder - Genera formulario admin para Post Grid
|
||||
*
|
||||
* Sigue el mismo patron visual que RelatedPostFormBuilder:
|
||||
* - Header con gradiente navy
|
||||
* - Layout de 2 columnas
|
||||
* - Cards con borde izquierdo
|
||||
* - Inputs compactos (form-control-sm)
|
||||
*
|
||||
* @package ROITheme\Admin\PostGrid\Infrastructure\Ui
|
||||
*/
|
||||
final class PostGridFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader();
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildShortcodeGuide();
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildMediaGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildLayoutGroup($componentId);
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-grid-3x3-gap me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Post Grid';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Grid de posts para listados, archivos y resultados de busqueda';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="post-grid">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('postGridEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('postGridShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('postGridShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
$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', false);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', false);
|
||||
$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('postGridVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('postGridVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('postGridVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('postGridVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('postGridVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Reglas de exclusion
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'postGrid');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Switches de contenido
|
||||
$showThumbnail = $this->renderer->getFieldValue($componentId, 'content', 'show_thumbnail', true);
|
||||
$html .= $this->buildSwitch('postGridShowThumbnail', 'Mostrar imagen destacada', 'bi-image', $showThumbnail);
|
||||
|
||||
$showExcerpt = $this->renderer->getFieldValue($componentId, 'content', 'show_excerpt', true);
|
||||
$html .= $this->buildSwitch('postGridShowExcerpt', 'Mostrar extracto', 'bi-text-paragraph', $showExcerpt);
|
||||
|
||||
$showMeta = $this->renderer->getFieldValue($componentId, 'content', 'show_meta', true);
|
||||
$html .= $this->buildSwitch('postGridShowMeta', 'Mostrar metadatos', 'bi-info-circle', $showMeta);
|
||||
|
||||
$showCategories = $this->renderer->getFieldValue($componentId, 'content', 'show_categories', true);
|
||||
$html .= $this->buildSwitch('postGridShowCategories', 'Mostrar categorias', 'bi-folder', $showCategories);
|
||||
|
||||
$html .= ' <hr class="my-3">';
|
||||
|
||||
// Excerpt length
|
||||
$excerptLength = $this->renderer->getFieldValue($componentId, 'content', 'excerpt_length', '20');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="postGridExcerptLength" class="form-label small mb-1 fw-semibold">Longitud del extracto</label>';
|
||||
$html .= ' <select id="postGridExcerptLength" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="10"' . ($excerptLength === '10' ? ' selected' : '') . '>10 palabras</option>';
|
||||
$html .= ' <option value="15"' . ($excerptLength === '15' ? ' selected' : '') . '>15 palabras</option>';
|
||||
$html .= ' <option value="20"' . ($excerptLength === '20' ? ' selected' : '') . '>20 palabras</option>';
|
||||
$html .= ' <option value="25"' . ($excerptLength === '25' ? ' selected' : '') . '>25 palabras</option>';
|
||||
$html .= ' <option value="30"' . ($excerptLength === '30' ? ' selected' : '') . '>30 palabras</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Read more text
|
||||
$readMoreText = $this->renderer->getFieldValue($componentId, 'content', 'read_more_text', 'Leer mas');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="postGridReadMoreText" class="form-label small mb-1 fw-semibold">Texto de leer mas</label>';
|
||||
$html .= ' <input type="text" id="postGridReadMoreText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($readMoreText) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// No posts message
|
||||
$noPostsMessage = $this->renderer->getFieldValue($componentId, 'content', 'no_posts_message', 'No se encontraron publicaciones');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="postGridNoPostsMessage" class="form-label small mb-1 fw-semibold">Mensaje sin posts</label>';
|
||||
$html .= ' <input type="text" id="postGridNoPostsMessage" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($noPostsMessage) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildLayoutGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-grid me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Disposicion';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Columns desktop
|
||||
$colsDesktop = $this->renderer->getFieldValue($componentId, 'layout', 'columns_desktop', '3');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="postGridColumnsDesktop" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Columnas escritorio';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridColumnsDesktop" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="2"' . ($colsDesktop === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||
$html .= ' <option value="3"' . ($colsDesktop === '3' ? ' selected' : '') . '>3 columnas</option>';
|
||||
$html .= ' <option value="4"' . ($colsDesktop === '4' ? ' selected' : '') . '>4 columnas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Columns tablet
|
||||
$colsTablet = $this->renderer->getFieldValue($componentId, 'layout', 'columns_tablet', '2');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="postGridColumnsTablet" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-tablet me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Columnas tablet';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridColumnsTablet" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="1"' . ($colsTablet === '1' ? ' selected' : '') . '>1 columna</option>';
|
||||
$html .= ' <option value="2"' . ($colsTablet === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||
$html .= ' <option value="3"' . ($colsTablet === '3' ? ' selected' : '') . '>3 columnas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Columns mobile
|
||||
$colsMobile = $this->renderer->getFieldValue($componentId, 'layout', 'columns_mobile', '1');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="postGridColumnsMobile" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Columnas movil';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridColumnsMobile" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="1"' . ($colsMobile === '1' ? ' selected' : '') . '>1 columna</option>';
|
||||
$html .= ' <option value="2"' . ($colsMobile === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Image position
|
||||
$imagePosition = $this->renderer->getFieldValue($componentId, 'layout', 'image_position', 'top');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="postGridImagePosition" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-aspect-ratio me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Posicion de imagen';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridImagePosition" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="top"' . ($imagePosition === 'top' ? ' selected' : '') . '>Arriba</option>';
|
||||
$html .= ' <option value="left"' . ($imagePosition === 'left' ? ' selected' : '') . '>Izquierda</option>';
|
||||
$html .= ' <option value="none"' . ($imagePosition === 'none' ? ' selected' : '') . '>Sin imagen</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildMediaGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-image me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Medios';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Fallback image
|
||||
$fallbackImage = $this->renderer->getFieldValue($componentId, 'media', 'fallback_image', '');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="postGridFallbackImage" class="form-label small mb-1 fw-semibold">URL imagen por defecto</label>';
|
||||
$html .= ' <input type="url" id="postGridFallbackImage" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_url($fallbackImage) . '" placeholder="https://...">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Fallback image alt
|
||||
$fallbackImageAlt = $this->renderer->getFieldValue($componentId, 'media', 'fallback_image_alt', 'Imagen por defecto');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="postGridFallbackImageAlt" class="form-label small mb-1 fw-semibold">Texto alternativo</label>';
|
||||
$html .= ' <input type="text" id="postGridFallbackImageAlt" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($fallbackImageAlt) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Heading level
|
||||
$headingLevel = $this->renderer->getFieldValue($componentId, 'typography', 'heading_level', 'h3');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="postGridHeadingLevel" class="form-label small mb-1 fw-semibold">Nivel de encabezado</label>';
|
||||
$html .= ' <select id="postGridHeadingLevel" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="h2"' . ($headingLevel === 'h2' ? ' selected' : '') . '>H2</option>';
|
||||
$html .= ' <option value="h3"' . ($headingLevel === 'h3' ? ' selected' : '') . '>H3</option>';
|
||||
$html .= ' <option value="h4"' . ($headingLevel === 'h4' ? ' selected' : '') . '>H4</option>';
|
||||
$html .= ' <option value="h5"' . ($headingLevel === 'h5' ? ' selected' : '') . '>H5</option>';
|
||||
$html .= ' <option value="h6"' . ($headingLevel === 'h6' ? ' selected' : '') . '>H6</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardTitleSize = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_size', '1.1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridCardTitleSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
|
||||
$html .= ' <input type="text" id="postGridCardTitleSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardTitleSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$cardTitleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_weight', '600');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridCardTitleWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
|
||||
$html .= ' <input type="text" id="postGridCardTitleWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardTitleWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$excerptSize = $this->renderer->getFieldValue($componentId, 'typography', 'excerpt_size', '0.9rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridExcerptSize" class="form-label small mb-1 fw-semibold">Tamano extracto</label>';
|
||||
$html .= ' <input type="text" id="postGridExcerptSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($excerptSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$metaSize = $this->renderer->getFieldValue($componentId, 'typography', 'meta_size', '0.8rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridMetaSize" class="form-label small mb-1 fw-semibold">Tamano metadatos</label>';
|
||||
$html .= ' <input type="text" id="postGridMetaSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($metaSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Cards
|
||||
$html .= ' <p class="small fw-semibold mb-2">Cards</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_bg_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('postGridCardBgColor', 'Fondo', $cardBgColor);
|
||||
|
||||
$cardTitleColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_title_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('postGridCardTitleColor', 'Titulo', $cardTitleColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardHoverBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_hover_bg_color', '#f9fafb');
|
||||
$html .= $this->buildColorPicker('postGridCardHoverBgColor', 'Fondo hover', $cardHoverBgColor);
|
||||
|
||||
$cardBorderColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_border_color', '#e5e7eb');
|
||||
$html .= $this->buildColorPicker('postGridCardBorderColor', 'Borde', $cardBorderColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardHoverBorderColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_hover_border_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('postGridCardHoverBorderColor', 'Borde hover', $cardHoverBorderColor);
|
||||
|
||||
$excerptColor = $this->renderer->getFieldValue($componentId, 'colors', 'excerpt_color', '#6b7280');
|
||||
$html .= $this->buildColorPicker('postGridExcerptColor', 'Extracto', $excerptColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$metaColor = $this->renderer->getFieldValue($componentId, 'colors', 'meta_color', '#9ca3af');
|
||||
$html .= $this->buildColorPicker('postGridMetaColor', 'Metadatos', $metaColor);
|
||||
|
||||
$categoryBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'category_bg_color', '#FFF5EB');
|
||||
$html .= $this->buildColorPicker('postGridCategoryBgColor', 'Fondo cat.', $categoryBgColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$categoryTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'category_text_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('postGridCategoryTextColor', 'Texto cat.', $categoryTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Paginacion
|
||||
$html .= ' <p class="small fw-semibold mb-2">Paginacion</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$paginationColor = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('postGridPaginationColor', 'Color', $paginationColor);
|
||||
|
||||
$paginationActiveBg = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_active_bg', '#FF8600');
|
||||
$html .= $this->buildColorPicker('postGridPaginationActiveBg', 'Activo fondo', $paginationActiveBg);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$paginationActiveColor = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_active_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('postGridPaginationActiveColor', 'Activo texto', $paginationActiveColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Separación entre cards
|
||||
$html .= ' <p class="small text-muted mb-2">Separacion entre cards:</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// Gap horizontal (entre columnas)
|
||||
$gapHorizontal = $this->renderer->getFieldValue($componentId, 'spacing', 'gap_horizontal', '24px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridGapHorizontal" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-arrows-expand me-1" style="color: #FF8600;"></i>Horizontal';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridGapHorizontal" class="form-select form-select-sm">';
|
||||
$gapOptions = ['0px', '8px', '12px', '16px', '20px', '24px', '32px', '40px', '48px'];
|
||||
foreach ($gapOptions as $opt) {
|
||||
$selected = ($gapHorizontal === $opt) ? ' selected' : '';
|
||||
$html .= ' <option value="' . $opt . '"' . $selected . '>' . $opt . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Gap vertical (entre filas)
|
||||
$gapVertical = $this->renderer->getFieldValue($componentId, 'spacing', 'gap_vertical', '24px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridGapVertical" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-arrows-collapse me-1" style="color: #FF8600;"></i>Vertical';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridGapVertical" class="form-select form-select-sm">';
|
||||
foreach ($gapOptions as $opt) {
|
||||
$selected = ($gapVertical === $opt) ? ' selected' : '';
|
||||
$html .= ' <option value="' . $opt . '"' . $selected . '>' . $opt . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Padding interno de cada card
|
||||
$html .= ' <p class="small text-muted mb-2">Padding interno de card:</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'card_padding', '20px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridCardPadding" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-box me-1" style="color: #FF8600;"></i>Padding';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridCardPadding" class="form-select form-select-sm">';
|
||||
$paddingOptions = ['0px', '8px', '12px', '16px', '20px', '24px', '32px'];
|
||||
foreach ($paddingOptions as $opt) {
|
||||
$selected = ($cardPadding === $opt) ? ' selected' : '';
|
||||
$html .= ' <option value="' . $opt . '"' . $selected . '>' . $opt . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6"></div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Margenes de la seccion
|
||||
$html .= ' <p class="small text-muted mb-2">Margenes de la seccion:</p>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$sectionMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_top', '0px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridSectionMarginTop" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-arrow-up me-1" style="color: #FF8600;"></i>Arriba';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridSectionMarginTop" class="form-select form-select-sm">';
|
||||
$marginOptions = ['0px', '8px', '16px', '24px', '32px', '48px', '64px'];
|
||||
foreach ($marginOptions as $opt) {
|
||||
$selected = ($sectionMarginTop === $opt) ? ' selected' : '';
|
||||
$html .= ' <option value="' . $opt . '"' . $selected . '>' . $opt . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$sectionMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_bottom', '32px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridSectionMarginBottom" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-arrow-down me-1" style="color: #FF8600;"></i>Abajo';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="postGridSectionMarginBottom" class="form-select form-select-sm">';
|
||||
foreach ($marginOptions as $opt) {
|
||||
$selected = ($sectionMarginBottom === $opt) ? ' selected' : '';
|
||||
$html .= ' <option value="' . $opt . '"' . $selected . '>' . $opt . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_border_radius', '0.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridCardBorderRadius" class="form-label small mb-1 fw-semibold">Radio borde</label>';
|
||||
$html .= ' <input type="text" id="postGridCardBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$imageBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'image_border_radius', '0.375rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="postGridImageBorderRadius" class="form-label small mb-1 fw-semibold">Radio imagen</label>';
|
||||
$html .= ' <input type="text" id="postGridImageBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($imageBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="mb-3">';
|
||||
$cardTransition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_transition', 'all 0.3s ease');
|
||||
$html .= ' <label for="postGridCardTransition" class="form-label small mb-1 fw-semibold">Transicion</label>';
|
||||
$html .= ' <input type="text" id="postGridCardTransition" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardTransition) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="mb-3">';
|
||||
$cardShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_shadow', '0 1px 3px rgba(0,0,0,0.1)');
|
||||
$html .= ' <label for="postGridCardShadow" class="form-label small mb-1 fw-semibold">Sombra normal</label>';
|
||||
$html .= ' <input type="text" id="postGridCardShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="mb-0">';
|
||||
$cardHoverShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_hover_shadow', '0 4px 12px rgba(0,0,0,0.15)');
|
||||
$html .= ' <label for="postGridCardHoverShadow" class="form-label small mb-1 fw-semibold">Sombra hover</label>';
|
||||
$html .= ' <input type="text" id="postGridCardHoverShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardHoverShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildShortcodeGuide(): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-code-square me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Shortcode [roi_post_grid]';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <p class="small text-muted mb-3">';
|
||||
$html .= ' Usa este shortcode para insertar grids de posts en cualquier pagina o entrada. ';
|
||||
$html .= ' Los estilos se heredan de la configuracion de este componente.';
|
||||
$html .= ' </p>';
|
||||
|
||||
// Uso basico
|
||||
$html .= ' <p class="small fw-semibold mb-1">';
|
||||
$html .= ' <i class="bi bi-1-circle me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Uso basico (9 posts, 3 columnas)';
|
||||
$html .= ' </p>';
|
||||
$html .= ' <div class="bg-dark text-light rounded p-2 mb-3" style="font-family: monospace; font-size: 0.8rem;">';
|
||||
$html .= ' <code class="text-warning">[roi_post_grid]</code>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Por categoria
|
||||
$html .= ' <p class="small fw-semibold mb-1">';
|
||||
$html .= ' <i class="bi bi-2-circle me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Filtrar por categoria';
|
||||
$html .= ' </p>';
|
||||
$html .= ' <div class="bg-dark text-light rounded p-2 mb-3" style="font-family: monospace; font-size: 0.8rem;">';
|
||||
$html .= ' <code class="text-warning">[roi_post_grid category="precios-unitarios"]</code>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Personalizar cantidad y columnas
|
||||
$html .= ' <p class="small fw-semibold mb-1">';
|
||||
$html .= ' <i class="bi bi-3-circle me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' 6 posts en 2 columnas';
|
||||
$html .= ' </p>';
|
||||
$html .= ' <div class="bg-dark text-light rounded p-2 mb-3" style="font-family: monospace; font-size: 0.8rem;">';
|
||||
$html .= ' <code class="text-warning">[roi_post_grid posts_per_page="6" columns="2"]</code>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Con paginacion
|
||||
$html .= ' <p class="small fw-semibold mb-1">';
|
||||
$html .= ' <i class="bi bi-4-circle me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Con paginacion';
|
||||
$html .= ' </p>';
|
||||
$html .= ' <div class="bg-dark text-light rounded p-2 mb-3" style="font-family: monospace; font-size: 0.8rem;">';
|
||||
$html .= ' <code class="text-warning">[roi_post_grid posts_per_page="12" show_pagination="true"]</code>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Filtrar por tag
|
||||
$html .= ' <p class="small fw-semibold mb-1">';
|
||||
$html .= ' <i class="bi bi-5-circle me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Filtrar por etiqueta';
|
||||
$html .= ' </p>';
|
||||
$html .= ' <div class="bg-dark text-light rounded p-2 mb-3" style="font-family: monospace; font-size: 0.8rem;">';
|
||||
$html .= ' <code class="text-warning">[roi_post_grid tag="tutorial"]</code>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Ejemplo completo
|
||||
$html .= ' <p class="small fw-semibold mb-1">';
|
||||
$html .= ' <i class="bi bi-6-circle me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Ejemplo completo';
|
||||
$html .= ' </p>';
|
||||
$html .= ' <div class="bg-dark text-light rounded p-2 mb-3" style="font-family: monospace; font-size: 0.75rem;">';
|
||||
$html .= ' <code class="text-warning">[roi_post_grid category="cursos" posts_per_page="6" columns="3" show_meta="false" show_categories="true"]</code>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Tabla de atributos
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-list-check me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Atributos disponibles';
|
||||
$html .= ' </p>';
|
||||
$html .= ' <div class="table-responsive">';
|
||||
$html .= ' <table class="table table-sm table-bordered small mb-0">';
|
||||
$html .= ' <thead class="table-light">';
|
||||
$html .= ' <tr><th>Atributo</th><th>Default</th><th>Descripcion</th></tr>';
|
||||
$html .= ' </thead>';
|
||||
$html .= ' <tbody>';
|
||||
$html .= ' <tr><td><code>posts_per_page</code></td><td>9</td><td>Cantidad de posts</td></tr>';
|
||||
$html .= ' <tr><td><code>columns</code></td><td>3</td><td>Columnas (1-4)</td></tr>';
|
||||
$html .= ' <tr><td><code>category</code></td><td>-</td><td>Slug de categoria</td></tr>';
|
||||
$html .= ' <tr><td><code>exclude_category</code></td><td>-</td><td>Excluir categoria</td></tr>';
|
||||
$html .= ' <tr><td><code>tag</code></td><td>-</td><td>Slug de etiqueta</td></tr>';
|
||||
$html .= ' <tr><td><code>author</code></td><td>-</td><td>ID o username</td></tr>';
|
||||
$html .= ' <tr><td><code>orderby</code></td><td>date</td><td>date, title, rand</td></tr>';
|
||||
$html .= ' <tr><td><code>order</code></td><td>DESC</td><td>DESC o ASC</td></tr>';
|
||||
$html .= ' <tr><td><code>show_pagination</code></td><td>false</td><td>Mostrar paginacion</td></tr>';
|
||||
$html .= ' <tr><td><code>show_thumbnail</code></td><td>true</td><td>Mostrar imagen</td></tr>';
|
||||
$html .= ' <tr><td><code>show_excerpt</code></td><td>true</td><td>Mostrar extracto</td></tr>';
|
||||
$html .= ' <tr><td><code>show_meta</code></td><td>true</td><td>Fecha y autor</td></tr>';
|
||||
$html .= ' <tr><td><code>show_categories</code></td><td>true</td><td>Badges categoria</td></tr>';
|
||||
$html .= ' <tr><td><code>excerpt_length</code></td><td>20</td><td>Palabras extracto</td></tr>';
|
||||
$html .= ' <tr><td><code>exclude_posts</code></td><td>-</td><td>IDs separados por coma</td></tr>';
|
||||
$html .= ' <tr><td><code>offset</code></td><td>0</td><td>Saltar N posts</td></tr>';
|
||||
$html .= ' <tr><td><code>id</code></td><td>-</td><td>ID unico (multiples grids)</td></tr>';
|
||||
$html .= ' <tr><td><code>class</code></td><td>-</td><td>Clase CSS adicional</td></tr>';
|
||||
$html .= ' </tbody>';
|
||||
$html .= ' </table>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\RelatedPost\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Related Post
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*
|
||||
* NOTA: Este componente NO tenia mapeos en AdminAjaxHandler
|
||||
* (era el unico componente roto - 35 campos no se guardaban)
|
||||
*/
|
||||
final class RelatedPostFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'related-post';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'relatedPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'relatedPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'relatedPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'relatedPostVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'relatedPostVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'relatedPostVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'relatedPostVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'relatedPostVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'relatedPostExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'relatedPostExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'relatedPostExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'relatedPostExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'relatedPostSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'],
|
||||
'relatedPostPerPage' => ['group' => 'content', 'attribute' => 'posts_per_page'],
|
||||
'relatedPostOrderby' => ['group' => 'content', 'attribute' => 'orderby'],
|
||||
'relatedPostOrder' => ['group' => 'content', 'attribute' => 'order'],
|
||||
'relatedPostShowPagination' => ['group' => 'content', 'attribute' => 'show_pagination'],
|
||||
|
||||
// Layout
|
||||
'relatedPostColsDesktop' => ['group' => 'layout', 'attribute' => 'columns_desktop'],
|
||||
'relatedPostColsTablet' => ['group' => 'layout', 'attribute' => 'columns_tablet'],
|
||||
'relatedPostColsMobile' => ['group' => 'layout', 'attribute' => 'columns_mobile'],
|
||||
|
||||
// Typography
|
||||
'relatedPostSectionTitleSize' => ['group' => 'typography', 'attribute' => 'section_title_size'],
|
||||
'relatedPostSectionTitleWeight' => ['group' => 'typography', 'attribute' => 'section_title_weight'],
|
||||
'relatedPostCardTitleSize' => ['group' => 'typography', 'attribute' => 'card_title_size'],
|
||||
'relatedPostCardTitleWeight' => ['group' => 'typography', 'attribute' => 'card_title_weight'],
|
||||
|
||||
// Colors
|
||||
'relatedPostSectionTitleColor' => ['group' => 'colors', 'attribute' => 'section_title_color'],
|
||||
'relatedPostCardBgColor' => ['group' => 'colors', 'attribute' => 'card_bg_color'],
|
||||
'relatedPostCardTitleColor' => ['group' => 'colors', 'attribute' => 'card_title_color'],
|
||||
'relatedPostCardHoverBgColor' => ['group' => 'colors', 'attribute' => 'card_hover_bg_color'],
|
||||
'relatedPostPaginationBgColor' => ['group' => 'colors', 'attribute' => 'pagination_bg_color'],
|
||||
'relatedPostPaginationTextColor' => ['group' => 'colors', 'attribute' => 'pagination_text_color'],
|
||||
'relatedPostPaginationActiveBg' => ['group' => 'colors', 'attribute' => 'pagination_active_bg'],
|
||||
'relatedPostPaginationActiveText' => ['group' => 'colors', 'attribute' => 'pagination_active_text'],
|
||||
|
||||
// Spacing
|
||||
'relatedPostSectionMarginTop' => ['group' => 'spacing', 'attribute' => 'section_margin_top'],
|
||||
'relatedPostSectionMarginBottom' => ['group' => 'spacing', 'attribute' => 'section_margin_bottom'],
|
||||
'relatedPostTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||
'relatedPostGridGap' => ['group' => 'spacing', 'attribute' => 'grid_gap'],
|
||||
'relatedPostCardPadding' => ['group' => 'spacing', 'attribute' => 'card_padding'],
|
||||
'relatedPostPaginationMarginTop' => ['group' => 'spacing', 'attribute' => 'pagination_margin_top'],
|
||||
|
||||
// Visual Effects
|
||||
'relatedPostCardBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'card_border_radius'],
|
||||
'relatedPostCardShadow' => ['group' => 'visual_effects', 'attribute' => 'card_shadow'],
|
||||
'relatedPostCardHoverShadow' => ['group' => 'visual_effects', 'attribute' => 'card_hover_shadow'],
|
||||
'relatedPostCardTransition' => ['group' => 'visual_effects', 'attribute' => 'card_transition'],
|
||||
];
|
||||
}
|
||||
}
|
||||
552
Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php
Normal file
552
Admin/RelatedPost/Infrastructure/Ui/RelatedPostFormBuilder.php
Normal file
@@ -0,0 +1,552 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\RelatedPost\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Related Posts
|
||||
*
|
||||
* @package ROITheme\Admin\RelatedPost\Infrastructure\Ui
|
||||
*/
|
||||
final class RelatedPostFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildLayoutGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-grid-3x3-gap me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Posts Relacionados';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Seccion de posts relacionados con grid de cards';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="related-post">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('relatedPostEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('relatedPostShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('relatedPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'relatedPost');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Section Title
|
||||
$sectionTitle = $this->renderer->getFieldValue($componentId, 'content', 'section_title', 'Descubre Mas Contenido');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostSectionTitle" class="form-label small mb-1 fw-semibold">Titulo de seccion</label>';
|
||||
$html .= ' <input type="text" id="relatedPostSectionTitle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionTitle) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Posts per page
|
||||
$postsPerPage = $this->renderer->getFieldValue($componentId, 'content', 'posts_per_page', '12');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostPerPage" class="form-label small mb-1 fw-semibold">Posts por pagina</label>';
|
||||
$html .= ' <input type="number" id="relatedPostPerPage" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($postsPerPage) . '" min="1" max="50">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Order by
|
||||
$orderby = $this->renderer->getFieldValue($componentId, 'content', 'orderby', 'rand');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostOrderby" class="form-label small mb-1 fw-semibold">Ordenar por</label>';
|
||||
$html .= ' <select id="relatedPostOrderby" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="rand"' . ($orderby === 'rand' ? ' selected' : '') . '>Aleatorio</option>';
|
||||
$html .= ' <option value="date"' . ($orderby === 'date' ? ' selected' : '') . '>Fecha</option>';
|
||||
$html .= ' <option value="title"' . ($orderby === 'title' ? ' selected' : '') . '>Titulo</option>';
|
||||
$html .= ' <option value="comment_count"' . ($orderby === 'comment_count' ? ' selected' : '') . '>Comentarios</option>';
|
||||
$html .= ' <option value="menu_order"' . ($orderby === 'menu_order' ? ' selected' : '') . '>Orden de menu</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Order direction
|
||||
$order = $this->renderer->getFieldValue($componentId, 'content', 'order', 'DESC');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostOrder" class="form-label small mb-1 fw-semibold">Direccion</label>';
|
||||
$html .= ' <select id="relatedPostOrder" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="DESC"' . ($order === 'DESC' ? ' selected' : '') . '>Descendente</option>';
|
||||
$html .= ' <option value="ASC"' . ($order === 'ASC' ? ' selected' : '') . '>Ascendente</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Show pagination
|
||||
$showPagination = $this->renderer->getFieldValue($componentId, 'content', 'show_pagination', true);
|
||||
$html .= $this->buildSwitch('relatedPostShowPagination', 'Mostrar paginacion', 'bi-three-dots', $showPagination);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildLayoutGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-grid me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Disposicion';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Columns desktop
|
||||
$colsDesktop = $this->renderer->getFieldValue($componentId, 'layout', 'columns_desktop', '3');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostColsDesktop" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Columnas escritorio';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="relatedPostColsDesktop" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="2"' . ($colsDesktop === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||
$html .= ' <option value="3"' . ($colsDesktop === '3' ? ' selected' : '') . '>3 columnas</option>';
|
||||
$html .= ' <option value="4"' . ($colsDesktop === '4' ? ' selected' : '') . '>4 columnas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Columns tablet
|
||||
$colsTablet = $this->renderer->getFieldValue($componentId, 'layout', 'columns_tablet', '2');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="relatedPostColsTablet" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-tablet me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Columnas tablet';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="relatedPostColsTablet" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="1"' . ($colsTablet === '1' ? ' selected' : '') . '>1 columna</option>';
|
||||
$html .= ' <option value="2"' . ($colsTablet === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||
$html .= ' <option value="3"' . ($colsTablet === '3' ? ' selected' : '') . '>3 columnas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Columns mobile
|
||||
$colsMobile = $this->renderer->getFieldValue($componentId, 'layout', 'columns_mobile', '1');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="relatedPostColsMobile" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Columnas movil';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="relatedPostColsMobile" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="1"' . ($colsMobile === '1' ? ' selected' : '') . '>1 columna</option>';
|
||||
$html .= ' <option value="2"' . ($colsMobile === '2' ? ' selected' : '') . '>2 columnas</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$sectionTitleSize = $this->renderer->getFieldValue($componentId, 'typography', 'section_title_size', '1.75rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostSectionTitleSize" class="form-label small mb-1 fw-semibold">Tamano titulo seccion</label>';
|
||||
$html .= ' <input type="text" id="relatedPostSectionTitleSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionTitleSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$sectionTitleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'section_title_weight', '500');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostSectionTitleWeight" class="form-label small mb-1 fw-semibold">Peso titulo seccion</label>';
|
||||
$html .= ' <input type="text" id="relatedPostSectionTitleWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionTitleWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$cardTitleSize = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostCardTitleSize" class="form-label small mb-1 fw-semibold">Tamano titulo card</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardTitleSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardTitleSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$cardTitleWeight = $this->renderer->getFieldValue($componentId, 'typography', 'card_title_weight', '500');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostCardTitleWeight" class="form-label small mb-1 fw-semibold">Peso titulo card</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardTitleWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardTitleWeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Seccion
|
||||
$html .= ' <p class="small fw-semibold mb-2">Seccion</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$sectionTitleColor = $this->renderer->getFieldValue($componentId, 'colors', 'section_title_color', '#212529');
|
||||
$html .= $this->buildColorPicker('relatedPostSectionTitleColor', 'Titulo seccion', $sectionTitleColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Cards
|
||||
$html .= ' <p class="small fw-semibold mb-2">Cards</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_bg_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('relatedPostCardBgColor', 'Fondo card', $cardBgColor);
|
||||
|
||||
$cardTitleColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_title_color', '#212529');
|
||||
$html .= $this->buildColorPicker('relatedPostCardTitleColor', 'Titulo card', $cardTitleColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardHoverBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'card_hover_bg_color', '#f8f9fa');
|
||||
$html .= $this->buildColorPicker('relatedPostCardHoverBgColor', 'Fondo hover', $cardHoverBgColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Paginacion
|
||||
$html .= ' <p class="small fw-semibold mb-2">Paginacion</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$paginationBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_bg_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('relatedPostPaginationBgColor', 'Fondo', $paginationBgColor);
|
||||
|
||||
$paginationTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_text_color', '#0d6efd');
|
||||
$html .= $this->buildColorPicker('relatedPostPaginationTextColor', 'Texto', $paginationTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$paginationActiveBg = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_active_bg', '#0d6efd');
|
||||
$html .= $this->buildColorPicker('relatedPostPaginationActiveBg', 'Activo fondo', $paginationActiveBg);
|
||||
|
||||
$paginationActiveText = $this->renderer->getFieldValue($componentId, 'colors', 'pagination_active_text', '#ffffff');
|
||||
$html .= $this->buildColorPicker('relatedPostPaginationActiveText', 'Activo texto', $paginationActiveText);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$sectionMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_top', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostSectionMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
|
||||
$html .= ' <input type="text" id="relatedPostSectionMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionMarginTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$sectionMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'section_margin_bottom', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostSectionMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
|
||||
$html .= ' <input type="text" id="relatedPostSectionMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($sectionMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||
$html .= ' <input type="text" id="relatedPostTitleMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$gridGap = $this->renderer->getFieldValue($componentId, 'spacing', 'grid_gap', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostGridGap" class="form-label small mb-1 fw-semibold">Espacio cards</label>';
|
||||
$html .= ' <input type="text" id="relatedPostGridGap" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($gridGap) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$cardPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'card_padding', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostCardPadding" class="form-label small mb-1 fw-semibold">Padding card</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$paginationMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'pagination_margin_top', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostPaginationMarginTop" class="form-label small mb-1 fw-semibold">Margen paginacion</label>';
|
||||
$html .= ' <input type="text" id="relatedPostPaginationMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($paginationMarginTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$cardBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_border_radius', '0.375rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostCardBorderRadius" class="form-label small mb-1 fw-semibold">Radio borde card</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$cardTransition = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_transition', '0.3s ease');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="relatedPostCardTransition" class="form-label small mb-1 fw-semibold">Transicion</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardTransition" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardTransition) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="mb-3">';
|
||||
$cardShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_shadow', '0 .125rem .25rem rgba(0,0,0,.075)');
|
||||
$html .= ' <label for="relatedPostCardShadow" class="form-label small mb-1 fw-semibold">Sombra card</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="mb-0">';
|
||||
$cardHoverShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'card_hover_shadow', '0 .5rem 1rem rgba(0,0,0,.15)');
|
||||
$html .= ' <label for="relatedPostCardHoverShadow" class="form-label small mb-1 fw-semibold">Sombra hover</label>';
|
||||
$html .= ' <input type="text" id="relatedPostCardHoverShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($cardHoverShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
38
Admin/Shared/Domain/Contracts/FieldMapperInterface.php
Normal file
38
Admin/Shared/Domain/Contracts/FieldMapperInterface.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Contrato para mapeo de campos de formulario a atributos de BD
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Definir el mapeo de field IDs a grupos/atributos
|
||||
* - Cada modulo implementa su propio mapper
|
||||
*
|
||||
* PRINCIPIOS:
|
||||
* - ISP: Interfaz pequena (2 metodos)
|
||||
* - DIP: Capas superiores dependen de esta abstraccion
|
||||
*/
|
||||
interface FieldMapperInterface
|
||||
{
|
||||
/**
|
||||
* Retorna el nombre del componente que mapea
|
||||
*
|
||||
* @return string Nombre en kebab-case (ej: 'cta-box-sidebar')
|
||||
*/
|
||||
public function getComponentName(): string;
|
||||
|
||||
/**
|
||||
* Retorna el mapeo de field IDs a grupo/atributo
|
||||
*
|
||||
* @return array<string, array{group: string, attribute: string}>
|
||||
*
|
||||
* Ejemplo:
|
||||
* [
|
||||
* 'ctaTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||
* 'ctaEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
* ]
|
||||
*/
|
||||
public function getFieldMapping(): array;
|
||||
}
|
||||
158
Admin/Shared/Infrastructure/Api/WordPress/AdminAjaxHandler.php
Normal file
158
Admin/Shared/Infrastructure/Api/WordPress/AdminAjaxHandler.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\Api\WordPress;
|
||||
|
||||
use ROITheme\Shared\Application\UseCases\SaveComponentSettings\SaveComponentSettingsUseCase;
|
||||
use ROITheme\Admin\Shared\Infrastructure\FieldMapping\FieldMapperRegistry;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Services\ExclusionFieldProcessor;
|
||||
|
||||
/**
|
||||
* Handler para peticiones AJAX del panel de administracion
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Manejar HTTP (request/response)
|
||||
* - Delegar mapeo a FieldMapperRegistry
|
||||
* - NO contiene logica de mapeo
|
||||
*
|
||||
* PRINCIPIOS:
|
||||
* - SRP: Solo maneja HTTP
|
||||
* - OCP: Nuevos componentes no requieren modificar esta clase
|
||||
* - DIP: Depende de abstracciones (FieldMapperRegistry)
|
||||
*/
|
||||
final class AdminAjaxHandler
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ?SaveComponentSettingsUseCase $saveComponentSettingsUseCase = null,
|
||||
private readonly ?FieldMapperRegistry $fieldMapperRegistry = null
|
||||
) {}
|
||||
|
||||
public function register(): void
|
||||
{
|
||||
add_action('wp_ajax_roi_save_component_settings', [$this, 'saveComponentSettings']);
|
||||
add_action('wp_ajax_roi_reset_component_defaults', [$this, 'resetComponentDefaults']);
|
||||
}
|
||||
|
||||
public function saveComponentSettings(): void
|
||||
{
|
||||
check_ajax_referer('roi_admin_dashboard', 'nonce');
|
||||
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error(['message' => 'No tienes permisos para realizar esta accion.']);
|
||||
}
|
||||
|
||||
$component = sanitize_text_field($_POST['component'] ?? '');
|
||||
$settings = json_decode(stripslashes($_POST['settings'] ?? '{}'), true);
|
||||
|
||||
if (empty($component) || empty($settings)) {
|
||||
wp_send_json_error(['message' => 'Datos incompletos.']);
|
||||
}
|
||||
|
||||
// Obtener mapper del modulo correspondiente
|
||||
if ($this->fieldMapperRegistry === null || !$this->fieldMapperRegistry->hasMapper($component)) {
|
||||
wp_send_json_error([
|
||||
'message' => "No existe mapper para el componente: {$component}"
|
||||
]);
|
||||
}
|
||||
|
||||
$mapper = $this->fieldMapperRegistry->getMapper($component);
|
||||
$fieldMapping = $mapper->getFieldMapping();
|
||||
|
||||
// Mapear settings usando el mapper del modulo
|
||||
$mappedSettings = $this->mapSettings($settings, $fieldMapping);
|
||||
|
||||
// Guardar usando Use Case
|
||||
if ($this->saveComponentSettingsUseCase !== null) {
|
||||
$updated = $this->saveComponentSettingsUseCase->execute($component, $mappedSettings);
|
||||
wp_send_json_success([
|
||||
'message' => sprintf('Se guardaron %d campos correctamente.', $updated)
|
||||
]);
|
||||
} else {
|
||||
wp_send_json_error(['message' => 'Error: Use Case no disponible.']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapea settings de field IDs a grupos/atributos
|
||||
*
|
||||
* Soporta tipos especiales para campos de exclusion:
|
||||
* - json_array: Convierte "a, b, c" a ["a", "b", "c"]
|
||||
* - json_array_int: Convierte "1, 2, 3" a [1, 2, 3]
|
||||
* - json_array_lines: Convierte lineas a array
|
||||
*/
|
||||
private function mapSettings(array $settings, array $fieldMapping): array
|
||||
{
|
||||
$mappedSettings = [];
|
||||
$fieldProcessor = new ExclusionFieldProcessor();
|
||||
|
||||
foreach ($settings as $fieldId => $value) {
|
||||
if (!isset($fieldMapping[$fieldId])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mapping = $fieldMapping[$fieldId];
|
||||
$groupName = $mapping['group'];
|
||||
$attributeName = $mapping['attribute'];
|
||||
$type = $mapping['type'] ?? null;
|
||||
|
||||
if (!isset($mappedSettings[$groupName])) {
|
||||
$mappedSettings[$groupName] = [];
|
||||
}
|
||||
|
||||
// Procesar valor segun tipo
|
||||
if ($type !== null && is_string($value)) {
|
||||
$value = $fieldProcessor->process($value, $type);
|
||||
}
|
||||
|
||||
$mappedSettings[$groupName][$attributeName] = $value;
|
||||
}
|
||||
|
||||
return $mappedSettings;
|
||||
}
|
||||
|
||||
public function resetComponentDefaults(): void
|
||||
{
|
||||
// Verificar nonce
|
||||
check_ajax_referer('roi_admin_dashboard', 'nonce');
|
||||
|
||||
// Verificar permisos
|
||||
if (!current_user_can('manage_options')) {
|
||||
wp_send_json_error([
|
||||
'message' => 'No tienes permisos para realizar esta accion.'
|
||||
]);
|
||||
}
|
||||
|
||||
// Obtener componente
|
||||
$component = sanitize_text_field($_POST['component'] ?? '');
|
||||
|
||||
if (empty($component)) {
|
||||
wp_send_json_error([
|
||||
'message' => 'Componente no especificado.'
|
||||
]);
|
||||
}
|
||||
|
||||
// Ruta al schema JSON
|
||||
$schemaPath = get_template_directory() . '/Schemas/' . $component . '.json';
|
||||
|
||||
if (!file_exists($schemaPath)) {
|
||||
wp_send_json_error([
|
||||
'message' => 'Schema del componente no encontrado.'
|
||||
]);
|
||||
}
|
||||
|
||||
// Usar repositorio para restaurar valores
|
||||
if ($this->saveComponentSettingsUseCase !== null) {
|
||||
global $wpdb;
|
||||
$repository = new \ROITheme\Shared\Infrastructure\Persistence\WordPress\WordPressComponentSettingsRepository($wpdb);
|
||||
$updated = $repository->resetToDefaults($component, $schemaPath);
|
||||
|
||||
wp_send_json_success([
|
||||
'message' => sprintf('Se restauraron %d campos a sus valores por defecto.', $updated)
|
||||
]);
|
||||
} else {
|
||||
wp_send_json_error([
|
||||
'message' => 'Error: Repositorio no disponible.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Provider para auto-registro de Field Mappers
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Descubrir automaticamente FieldMappers en cada modulo
|
||||
* - Registrarlos en el FieldMapperRegistry
|
||||
*
|
||||
* BENEFICIO:
|
||||
* - Agregar nuevo componente = crear FieldMapper (sin tocar functions.php)
|
||||
* - Eliminar componente = borrar carpeta (limpieza automatica)
|
||||
*/
|
||||
final class FieldMapperProvider
|
||||
{
|
||||
private const MODULES = [
|
||||
'TopNotificationBar',
|
||||
'Navbar',
|
||||
'CtaLetsTalk',
|
||||
'Hero',
|
||||
'FeaturedImage',
|
||||
'TableOfContents',
|
||||
'CtaBoxSidebar',
|
||||
'SocialShare',
|
||||
'CtaPost',
|
||||
'RelatedPost',
|
||||
'ContactForm',
|
||||
'Footer',
|
||||
'ThemeSettings',
|
||||
'AdsensePlacement',
|
||||
'ArchiveHeader',
|
||||
'PostGrid',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly FieldMapperRegistry $registry
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Registra todos los FieldMappers disponibles
|
||||
*/
|
||||
public function registerAll(): void
|
||||
{
|
||||
foreach (self::MODULES as $module) {
|
||||
$this->registerIfExists($module);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra un mapper si existe la clase
|
||||
*/
|
||||
private function registerIfExists(string $module): void
|
||||
{
|
||||
$className = sprintf(
|
||||
'ROITheme\\Admin\\%s\\Infrastructure\\FieldMapping\\%sFieldMapper',
|
||||
$module,
|
||||
$module
|
||||
);
|
||||
|
||||
if (class_exists($className)) {
|
||||
$mapper = new $className();
|
||||
if ($mapper instanceof FieldMapperInterface) {
|
||||
$this->registry->register($mapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Registro central de Field Mappers
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Registrar mappers de cada modulo
|
||||
* - Resolver mapper por nombre de componente
|
||||
*
|
||||
* PRINCIPIOS:
|
||||
* - OCP: Nuevos mappers se registran sin modificar esta clase
|
||||
* - SRP: Solo gestiona el registro, no contiene mapeos
|
||||
*/
|
||||
final class FieldMapperRegistry
|
||||
{
|
||||
/** @var array<string, FieldMapperInterface> */
|
||||
private array $mappers = [];
|
||||
|
||||
/**
|
||||
* Registra un mapper
|
||||
*/
|
||||
public function register(FieldMapperInterface $mapper): void
|
||||
{
|
||||
$this->mappers[$mapper->getComponentName()] = $mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene un mapper por nombre de componente
|
||||
*
|
||||
* @throws \InvalidArgumentException Si no existe mapper para el componente
|
||||
*/
|
||||
public function getMapper(string $componentName): FieldMapperInterface
|
||||
{
|
||||
if (!isset($this->mappers[$componentName])) {
|
||||
throw new \InvalidArgumentException(
|
||||
"No field mapper registered for component: {$componentName}"
|
||||
);
|
||||
}
|
||||
|
||||
return $this->mappers[$componentName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si existe mapper para un componente
|
||||
*/
|
||||
public function hasMapper(string $componentName): bool
|
||||
{
|
||||
return isset($this->mappers[$componentName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene todos los mappers registrados
|
||||
*
|
||||
* @return array<string, FieldMapperInterface>
|
||||
*/
|
||||
public function getAllMappers(): array
|
||||
{
|
||||
return $this->mappers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\Services;
|
||||
|
||||
/**
|
||||
* Servicio para procesar campos de exclusion antes de guardar en BD
|
||||
*
|
||||
* Convierte formatos de UI a JSON para almacenamiento.
|
||||
*
|
||||
* v1.1: Extraido de AdminAjaxHandler (SRP)
|
||||
*
|
||||
* @package ROITheme\Admin\Shared\Infrastructure\Services
|
||||
*/
|
||||
final class ExclusionFieldProcessor
|
||||
{
|
||||
/**
|
||||
* Procesa un valor de campo de exclusion segun su tipo
|
||||
*
|
||||
* @param string $value Valor del campo (desde UI)
|
||||
* @param string $type Tipo de campo: json_array, json_array_int, json_array_lines
|
||||
* @return string JSON string para almacenar en BD
|
||||
*/
|
||||
public function process(string $value, string $type): string
|
||||
{
|
||||
return match ($type) {
|
||||
'json_array' => $this->processJsonArray($value),
|
||||
'json_array_int' => $this->processJsonArrayInt($value),
|
||||
'json_array_lines' => $this->processJsonArrayLines($value),
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* "a, b, c" -> ["a", "b", "c"]
|
||||
*/
|
||||
private function processJsonArray(string $value): string
|
||||
{
|
||||
$items = array_map('trim', explode(',', $value));
|
||||
$items = array_filter($items, fn($item) => $item !== '');
|
||||
return json_encode(array_values($items), JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* "1, 2, 3" -> [1, 2, 3]
|
||||
*/
|
||||
private function processJsonArrayInt(string $value): string
|
||||
{
|
||||
$items = array_map('trim', explode(',', $value));
|
||||
$items = array_filter($items, 'is_numeric');
|
||||
$items = array_map('intval', $items);
|
||||
return json_encode(array_values($items));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lineas separadas -> array
|
||||
*/
|
||||
private function processJsonArrayLines(string $value): string
|
||||
{
|
||||
$items = preg_split('/\r\n|\r|\n/', $value);
|
||||
$items = array_map('trim', $items);
|
||||
$items = array_filter($items, fn($item) => $item !== '');
|
||||
return json_encode(array_values($items), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
}
|
||||
31
Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js
Normal file
31
Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Toggle para mostrar/ocultar reglas de exclusion en FormBuilders
|
||||
*
|
||||
* Escucha cambios en checkboxes con ID que termine en "ExclusionsEnabled"
|
||||
* y muestra/oculta el contenedor de reglas correspondiente.
|
||||
*
|
||||
* @package ROITheme\Admin
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
function initExclusionToggles() {
|
||||
document.querySelectorAll('[id$="ExclusionsEnabled"]').forEach(function(checkbox) {
|
||||
// Handler para cambios
|
||||
checkbox.addEventListener('change', function() {
|
||||
const prefix = this.id.replace('ExclusionsEnabled', '');
|
||||
const rulesContainer = document.getElementById(prefix + 'ExclusionRules');
|
||||
if (rulesContainer) {
|
||||
rulesContainer.style.display = this.checked ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Inicializar cuando DOM este listo
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initExclusionToggles);
|
||||
} else {
|
||||
initExclusionToggles();
|
||||
}
|
||||
})();
|
||||
260
Admin/Shared/Infrastructure/Ui/ExclusionFormPartial.php
Normal file
260
Admin/Shared/Infrastructure/Ui/ExclusionFormPartial.php
Normal file
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
/**
|
||||
* Componente UI parcial reutilizable para reglas de exclusion
|
||||
*
|
||||
* Genera el HTML para la seccion de exclusiones en FormBuilders.
|
||||
* Debe ser incluido despues de la seccion de visibilidad por tipo de pagina.
|
||||
*
|
||||
* Uso en FormBuilder:
|
||||
* ```php
|
||||
* $exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
* $html .= $exclusionPartial->render($componentId, 'prefijo');
|
||||
* ```
|
||||
*
|
||||
* @package ROITheme\Admin\Shared\Infrastructure\Ui
|
||||
*/
|
||||
final class ExclusionFormPartial
|
||||
{
|
||||
private const GROUP_NAME = '_exclusions';
|
||||
|
||||
public function __construct(
|
||||
private readonly AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Renderiza la seccion de exclusiones
|
||||
*
|
||||
* @param string $componentId ID del componente (kebab-case)
|
||||
* @param string $prefix Prefijo para IDs de campos (ej: 'cta' genera 'ctaExclusionsEnabled')
|
||||
* @return string HTML de la seccion
|
||||
*/
|
||||
public function render(string $componentId, string $prefix): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildExclusionHeader();
|
||||
$html .= $this->buildExclusionToggle($componentId, $prefix);
|
||||
$html .= $this->buildExclusionRules($componentId, $prefix);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildExclusionHeader(): string
|
||||
{
|
||||
$html = '<hr class="my-3">';
|
||||
$html .= '<p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-funnel me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Reglas de exclusion avanzadas';
|
||||
$html .= '</p>';
|
||||
$html .= '<p class="small text-muted mb-2">';
|
||||
$html .= ' Excluir este componente de categorias, posts o URLs especificos.';
|
||||
$html .= '</p>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildExclusionToggle(string $componentId, string $prefix): string
|
||||
{
|
||||
$enabled = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclusions_enabled',
|
||||
false
|
||||
);
|
||||
$checked = $this->toBool($enabled);
|
||||
|
||||
$id = $prefix . 'ExclusionsEnabled';
|
||||
|
||||
$html = '<div class="mb-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= ' <i class="bi bi-filter-circle me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Activar reglas de exclusion</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildExclusionRules(string $componentId, string $prefix): string
|
||||
{
|
||||
$enabled = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclusions_enabled',
|
||||
false
|
||||
);
|
||||
$display = $this->toBool($enabled) ? 'block' : 'none';
|
||||
|
||||
$html = sprintf(
|
||||
'<div id="%sExclusionRules" style="display: %s;">',
|
||||
esc_attr($prefix),
|
||||
$display
|
||||
);
|
||||
|
||||
$html .= $this->buildCategoryField($componentId, $prefix);
|
||||
$html .= $this->buildPostIdsField($componentId, $prefix);
|
||||
$html .= $this->buildUrlPatternsField($componentId, $prefix);
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildCategoryField(string $componentId, string $prefix): string
|
||||
{
|
||||
$value = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclude_categories',
|
||||
'[]'
|
||||
);
|
||||
$categories = $this->jsonToCommaList($value);
|
||||
|
||||
$id = $prefix . 'ExcludeCategories';
|
||||
|
||||
$html = '<div class="mb-3">';
|
||||
$html .= sprintf(
|
||||
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= ' <i class="bi bi-folder me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Excluir en categorias';
|
||||
$html .= ' </label>';
|
||||
$html .= sprintf(
|
||||
' <input type="text" id="%s" class="form-control form-control-sm" value="%s" placeholder="noticias, eventos, tutoriales">',
|
||||
esc_attr($id),
|
||||
esc_attr($categories)
|
||||
);
|
||||
$html .= ' <small class="text-muted">Slugs de categorias separados por comas</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPostIdsField(string $componentId, string $prefix): string
|
||||
{
|
||||
$value = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclude_post_ids',
|
||||
'[]'
|
||||
);
|
||||
$postIds = $this->jsonToCommaList($value);
|
||||
|
||||
$id = $prefix . 'ExcludePostIds';
|
||||
|
||||
$html = '<div class="mb-3">';
|
||||
$html .= sprintf(
|
||||
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= ' <i class="bi bi-hash me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Excluir en posts/paginas';
|
||||
$html .= ' </label>';
|
||||
$html .= sprintf(
|
||||
' <input type="text" id="%s" class="form-control form-control-sm" value="%s" placeholder="123, 456, 789">',
|
||||
esc_attr($id),
|
||||
esc_attr($postIds)
|
||||
);
|
||||
$html .= ' <small class="text-muted">IDs de posts o paginas separados por comas</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildUrlPatternsField(string $componentId, string $prefix): string
|
||||
{
|
||||
$value = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclude_url_patterns',
|
||||
'[]'
|
||||
);
|
||||
$patterns = $this->jsonToLineList($value);
|
||||
|
||||
$id = $prefix . 'ExcludeUrlPatterns';
|
||||
|
||||
$html = '<div class="mb-0">';
|
||||
$html .= sprintf(
|
||||
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Excluir por patrones URL';
|
||||
$html .= ' </label>';
|
||||
$html .= sprintf(
|
||||
' <textarea id="%s" class="form-control form-control-sm" rows="3" placeholder="/privado/ /landing-especial/ /^\/categoria\/\d+$/">%s</textarea>',
|
||||
esc_attr($id),
|
||||
esc_textarea($patterns)
|
||||
);
|
||||
$html .= ' <small class="text-muted">Un patron por linea. Soporta texto simple o regex (ej: /^\/blog\/\d+$/)</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte JSON array o array a lista separada por comas
|
||||
*
|
||||
* @param string|array $value Valor desde BD (puede ser JSON string o array ya deserializado)
|
||||
*/
|
||||
private function jsonToCommaList(string|array $value): string
|
||||
{
|
||||
// Si ya es array, usarlo directamente
|
||||
if (is_array($value)) {
|
||||
return empty($value) ? '' : implode(', ', $value);
|
||||
}
|
||||
|
||||
// Si es string, intentar decodificar JSON
|
||||
$decoded = json_decode($value, true);
|
||||
|
||||
if (!is_array($decoded) || empty($decoded)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return implode(', ', $decoded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte JSON array o array a lista separada por lineas
|
||||
*
|
||||
* @param string|array $value Valor desde BD (puede ser JSON string o array ya deserializado)
|
||||
*/
|
||||
private function jsonToLineList(string|array $value): string
|
||||
{
|
||||
// Si ya es array, usarlo directamente
|
||||
if (is_array($value)) {
|
||||
return empty($value) ? '' : implode("\n", $value);
|
||||
}
|
||||
|
||||
// Si es string, intentar decodificar JSON
|
||||
$decoded = json_decode($value, true);
|
||||
|
||||
if (!is_array($decoded) || empty($decoded)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return implode("\n", $decoded);
|
||||
}
|
||||
|
||||
private function toBool(mixed $value): bool
|
||||
{
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\SocialShare\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Social Share
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class SocialShareFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'social-share';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'socialShareEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'socialShareShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'socialShareShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'socialShareVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'socialShareVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'socialShareVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'socialShareVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'socialShareVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'socialShareExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'socialShareExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'socialShareExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'socialShareExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'socialShareShowLabel' => ['group' => 'content', 'attribute' => 'show_label'],
|
||||
'socialShareLabelText' => ['group' => 'content', 'attribute' => 'label_text'],
|
||||
|
||||
// Networks
|
||||
'socialShareFacebook' => ['group' => 'networks', 'attribute' => 'show_facebook'],
|
||||
'socialShareFacebookUrl' => ['group' => 'networks', 'attribute' => 'facebook_url'],
|
||||
'socialShareInstagram' => ['group' => 'networks', 'attribute' => 'show_instagram'],
|
||||
'socialShareInstagramUrl' => ['group' => 'networks', 'attribute' => 'instagram_url'],
|
||||
'socialShareLinkedin' => ['group' => 'networks', 'attribute' => 'show_linkedin'],
|
||||
'socialShareLinkedinUrl' => ['group' => 'networks', 'attribute' => 'linkedin_url'],
|
||||
'socialShareWhatsapp' => ['group' => 'networks', 'attribute' => 'show_whatsapp'],
|
||||
'socialShareWhatsappNumber' => ['group' => 'networks', 'attribute' => 'whatsapp_number'],
|
||||
'socialShareTwitter' => ['group' => 'networks', 'attribute' => 'show_twitter'],
|
||||
'socialShareTwitterUrl' => ['group' => 'networks', 'attribute' => 'twitter_url'],
|
||||
'socialShareEmail' => ['group' => 'networks', 'attribute' => 'show_email'],
|
||||
'socialShareEmailAddress' => ['group' => 'networks', 'attribute' => 'email_address'],
|
||||
|
||||
// Colors
|
||||
'socialShareLabelColor' => ['group' => 'colors', 'attribute' => 'label_color'],
|
||||
'socialShareBorderTopColor' => ['group' => 'colors', 'attribute' => 'border_top_color'],
|
||||
'socialShareButtonBg' => ['group' => 'colors', 'attribute' => 'button_background'],
|
||||
'socialShareFacebookColor' => ['group' => 'colors', 'attribute' => 'facebook_color'],
|
||||
'socialShareInstagramColor' => ['group' => 'colors', 'attribute' => 'instagram_color'],
|
||||
'socialShareLinkedinColor' => ['group' => 'colors', 'attribute' => 'linkedin_color'],
|
||||
'socialShareWhatsappColor' => ['group' => 'colors', 'attribute' => 'whatsapp_color'],
|
||||
'socialShareTwitterColor' => ['group' => 'colors', 'attribute' => 'twitter_color'],
|
||||
'socialShareEmailColor' => ['group' => 'colors', 'attribute' => 'email_color'],
|
||||
|
||||
// Typography
|
||||
'socialShareLabelFontSize' => ['group' => 'typography', 'attribute' => 'label_font_size'],
|
||||
'socialShareIconFontSize' => ['group' => 'typography', 'attribute' => 'icon_font_size'],
|
||||
|
||||
// Spacing
|
||||
'socialShareMarginTop' => ['group' => 'spacing', 'attribute' => 'container_margin_top'],
|
||||
'socialShareMarginBottom' => ['group' => 'spacing', 'attribute' => 'container_margin_bottom'],
|
||||
'socialSharePaddingTop' => ['group' => 'spacing', 'attribute' => 'container_padding_top'],
|
||||
'socialSharePaddingBottom' => ['group' => 'spacing', 'attribute' => 'container_padding_bottom'],
|
||||
'socialShareLabelMarginBottom' => ['group' => 'spacing', 'attribute' => 'label_margin_bottom'],
|
||||
'socialShareButtonsGap' => ['group' => 'spacing', 'attribute' => 'buttons_gap'],
|
||||
'socialShareButtonPadding' => ['group' => 'spacing', 'attribute' => 'button_padding'],
|
||||
|
||||
// Visual Effects
|
||||
'socialShareBorderTopWidth' => ['group' => 'visual_effects', 'attribute' => 'border_top_width'],
|
||||
'socialShareButtonBorderWidth' => ['group' => 'visual_effects', 'attribute' => 'button_border_width'],
|
||||
'socialShareButtonBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'button_border_radius'],
|
||||
'socialShareTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
'socialShareHoverBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'hover_box_shadow'],
|
||||
];
|
||||
}
|
||||
}
|
||||
579
Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php
Normal file
579
Admin/SocialShare/Infrastructure/Ui/SocialShareFormBuilder.php
Normal file
@@ -0,0 +1,579 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\SocialShare\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Social Share
|
||||
*
|
||||
* Responsabilidad:
|
||||
* - Generar HTML del formulario de configuracion
|
||||
* - Usar Design System (Bootstrap 5)
|
||||
* - Cargar valores desde BD via AdminDashboardRenderer
|
||||
*
|
||||
* @package ROITheme\Admin\SocialShare\Infrastructure\Ui
|
||||
*/
|
||||
final class SocialShareFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildNetworksGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-share me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Compartir en Redes';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Botones para compartir contenido en redes sociales';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="social-share">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// is_enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('socialShareEnabled', 'Activar componente', 'bi-power', $enabled);
|
||||
|
||||
// show_on_desktop
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('socialShareShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
// show_on_mobile
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('socialShareShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'socialShare');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// show_label
|
||||
$showLabel = $this->renderer->getFieldValue($componentId, 'content', 'show_label', true);
|
||||
$html .= $this->buildSwitch('socialShareShowLabel', 'Mostrar etiqueta', 'bi-tag', $showLabel);
|
||||
|
||||
// label_text
|
||||
$labelText = $this->renderer->getFieldValue($componentId, 'content', 'label_text', 'Compartir:');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="socialShareLabelText" class="form-label small mb-1 fw-semibold">Texto etiqueta</label>';
|
||||
$html .= ' <input type="text" id="socialShareLabelText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($labelText) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildNetworksGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-globe me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Redes Sociales';
|
||||
$html .= ' </h5>';
|
||||
$html .= ' <p class="small text-muted mb-3">Configura las redes sociales y sus URLs</p>';
|
||||
|
||||
// Facebook
|
||||
$showFacebook = $this->renderer->getFieldValue($componentId, 'networks', 'show_facebook', true);
|
||||
$facebookUrl = $this->renderer->getFieldValue($componentId, 'networks', 'facebook_url', '');
|
||||
$html .= $this->buildNetworkField('socialShareFacebook', 'socialShareFacebookUrl', 'Facebook', 'bi-facebook', $showFacebook, $facebookUrl, 'https://facebook.com/tu-pagina');
|
||||
|
||||
// Instagram
|
||||
$showInstagram = $this->renderer->getFieldValue($componentId, 'networks', 'show_instagram', true);
|
||||
$instagramUrl = $this->renderer->getFieldValue($componentId, 'networks', 'instagram_url', '');
|
||||
$html .= $this->buildNetworkField('socialShareInstagram', 'socialShareInstagramUrl', 'Instagram', 'bi-instagram', $showInstagram, $instagramUrl, 'https://instagram.com/tu-perfil');
|
||||
|
||||
// LinkedIn
|
||||
$showLinkedin = $this->renderer->getFieldValue($componentId, 'networks', 'show_linkedin', true);
|
||||
$linkedinUrl = $this->renderer->getFieldValue($componentId, 'networks', 'linkedin_url', '');
|
||||
$html .= $this->buildNetworkField('socialShareLinkedin', 'socialShareLinkedinUrl', 'LinkedIn', 'bi-linkedin', $showLinkedin, $linkedinUrl, 'https://linkedin.com/in/tu-perfil');
|
||||
|
||||
// WhatsApp
|
||||
$showWhatsapp = $this->renderer->getFieldValue($componentId, 'networks', 'show_whatsapp', true);
|
||||
$whatsappNumber = $this->renderer->getFieldValue($componentId, 'networks', 'whatsapp_number', '');
|
||||
$html .= $this->buildNetworkField('socialShareWhatsapp', 'socialShareWhatsappNumber', 'WhatsApp', 'bi-whatsapp', $showWhatsapp, $whatsappNumber, '521234567890');
|
||||
|
||||
// X (Twitter)
|
||||
$showTwitter = $this->renderer->getFieldValue($componentId, 'networks', 'show_twitter', true);
|
||||
$twitterUrl = $this->renderer->getFieldValue($componentId, 'networks', 'twitter_url', '');
|
||||
$html .= $this->buildNetworkField('socialShareTwitter', 'socialShareTwitterUrl', 'X (Twitter)', 'bi-twitter-x', $showTwitter, $twitterUrl, 'https://x.com/tu-perfil');
|
||||
|
||||
// Email
|
||||
$showEmail = $this->renderer->getFieldValue($componentId, 'networks', 'show_email', true);
|
||||
$emailAddress = $this->renderer->getFieldValue($componentId, 'networks', 'email_address', '');
|
||||
$html .= $this->buildNetworkField('socialShareEmail', 'socialShareEmailAddress', 'Email', 'bi-envelope', $showEmail, $emailAddress, 'contacto@tudominio.com');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildNetworkField(string $switchId, string $urlId, string $label, string $icon, mixed $checked, string $urlValue, string $placeholder): string
|
||||
{
|
||||
// Normalizar valor booleano desde BD
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-3 p-2 rounded" style="background-color: #f8f9fa;">';
|
||||
|
||||
// Switch
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($switchId),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small fw-semibold" for="%s">',
|
||||
esc_attr($switchId)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// URL Input
|
||||
$html .= sprintf(
|
||||
' <input type="text" id="%s" class="form-control form-control-sm mt-2" value="%s" placeholder="%s">',
|
||||
esc_attr($urlId),
|
||||
esc_attr($urlValue),
|
||||
esc_attr($placeholder)
|
||||
);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Colores generales
|
||||
$html .= ' <p class="small fw-semibold mb-2">General</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$labelColor = $this->renderer->getFieldValue($componentId, 'colors', 'label_color', '#6c757d');
|
||||
$html .= $this->buildColorPicker('socialShareLabelColor', 'Etiqueta', $labelColor);
|
||||
|
||||
$borderTopColor = $this->renderer->getFieldValue($componentId, 'colors', 'border_top_color', '#dee2e6');
|
||||
$html .= $this->buildColorPicker('socialShareBorderTopColor', 'Borde superior', $borderTopColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$buttonBackground = $this->renderer->getFieldValue($componentId, 'colors', 'button_background', '#ffffff');
|
||||
$html .= $this->buildColorPicker('socialShareButtonBg', 'Fondo botones', $buttonBackground);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores por red social
|
||||
$html .= ' <p class="small fw-semibold mb-2">Redes Sociales</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$facebookColor = $this->renderer->getFieldValue($componentId, 'colors', 'facebook_color', '#0d6efd');
|
||||
$html .= $this->buildColorPicker('socialShareFacebookColor', 'Facebook', $facebookColor);
|
||||
|
||||
$instagramColor = $this->renderer->getFieldValue($componentId, 'colors', 'instagram_color', '#dc3545');
|
||||
$html .= $this->buildColorPicker('socialShareInstagramColor', 'Instagram', $instagramColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$linkedinColor = $this->renderer->getFieldValue($componentId, 'colors', 'linkedin_color', '#0dcaf0');
|
||||
$html .= $this->buildColorPicker('socialShareLinkedinColor', 'LinkedIn', $linkedinColor);
|
||||
|
||||
$whatsappColor = $this->renderer->getFieldValue($componentId, 'colors', 'whatsapp_color', '#198754');
|
||||
$html .= $this->buildColorPicker('socialShareWhatsappColor', 'WhatsApp', $whatsappColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$twitterColor = $this->renderer->getFieldValue($componentId, 'colors', 'twitter_color', '#212529');
|
||||
$html .= $this->buildColorPicker('socialShareTwitterColor', 'X (Twitter)', $twitterColor);
|
||||
|
||||
$emailColor = $this->renderer->getFieldValue($componentId, 'colors', 'email_color', '#6c757d');
|
||||
$html .= $this->buildColorPicker('socialShareEmailColor', 'Email', $emailColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// label_font_size
|
||||
$labelFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'label_font_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareLabelFontSize" class="form-label small mb-1 fw-semibold">Tamano etiqueta</label>';
|
||||
$html .= ' <input type="text" id="socialShareLabelFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($labelFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// icon_font_size
|
||||
$iconFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'icon_font_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareIconFontSize" class="form-label small mb-1 fw-semibold">Tamano iconos</label>';
|
||||
$html .= ' <input type="text" id="socialShareIconFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($iconFontSize) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// container_margin_top
|
||||
$containerMarginTop = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_top', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareMarginTop" class="form-label small mb-1 fw-semibold">Margen superior</label>';
|
||||
$html .= ' <input type="text" id="socialShareMarginTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerMarginTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// container_margin_bottom
|
||||
$containerMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'container_margin_bottom', '3rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
|
||||
$html .= ' <input type="text" id="socialShareMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// container_padding_top
|
||||
$containerPaddingTop = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding_top', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialSharePaddingTop" class="form-label small mb-1 fw-semibold">Padding superior</label>';
|
||||
$html .= ' <input type="text" id="socialSharePaddingTop" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerPaddingTop) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// container_padding_bottom
|
||||
$containerPaddingBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding_bottom', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialSharePaddingBottom" class="form-label small mb-1 fw-semibold">Padding inferior</label>';
|
||||
$html .= ' <input type="text" id="socialSharePaddingBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerPaddingBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// label_margin_bottom
|
||||
$labelMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'label_margin_bottom', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareLabelMarginBottom" class="form-label small mb-1 fw-semibold">Margen etiqueta</label>';
|
||||
$html .= ' <input type="text" id="socialShareLabelMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($labelMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// buttons_gap
|
||||
$buttonsGap = $this->renderer->getFieldValue($componentId, 'spacing', 'buttons_gap', '0.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareButtonsGap" class="form-label small mb-1 fw-semibold">Espacio botones</label>';
|
||||
$html .= ' <input type="text" id="socialShareButtonsGap" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonsGap) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// button_padding
|
||||
$buttonPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'button_padding', '0.25rem 0.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareButtonPadding" class="form-label small mb-1 fw-semibold">Padding botones</label>';
|
||||
$html .= ' <input type="text" id="socialShareButtonPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// border_top_width
|
||||
$borderTopWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_top_width', '1px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareBorderTopWidth" class="form-label small mb-1 fw-semibold">Grosor borde sup.</label>';
|
||||
$html .= ' <input type="text" id="socialShareBorderTopWidth" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderTopWidth) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// button_border_width
|
||||
$buttonBorderWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_width', '2px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareButtonBorderWidth" class="form-label small mb-1 fw-semibold">Grosor borde btn</label>';
|
||||
$html .= ' <input type="text" id="socialShareButtonBorderWidth" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonBorderWidth) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// button_border_radius
|
||||
$buttonBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'button_border_radius', '0.375rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareButtonBorderRadius" class="form-label small mb-1 fw-semibold">Radio botones</label>';
|
||||
$html .= ' <input type="text" id="socialShareButtonBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($buttonBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// transition_duration
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="socialShareTransitionDuration" class="form-label small mb-1 fw-semibold">Duracion transicion</label>';
|
||||
$html .= ' <input type="text" id="socialShareTransitionDuration" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// hover_box_shadow
|
||||
$hoverBoxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'hover_box_shadow', '0 4px 12px rgba(0, 0, 0, 0.15)');
|
||||
$html .= ' <div class="col-12">';
|
||||
$html .= ' <label for="socialShareHoverBoxShadow" class="form-label small mb-1 fw-semibold">Sombra hover</label>';
|
||||
$html .= ' <input type="text" id="socialShareHoverBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($hoverBoxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
// Normalizar valor booleano desde BD
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\TableOfContents\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Table of Contents
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class TableOfContentsFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'table-of-contents';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'tocEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'tocShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'tocShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'tocVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'tocVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'tocVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'tocVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'tocVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'tocExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'tocExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'tocExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'tocExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'tocTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||
'tocAutoGenerate' => ['group' => 'content', 'attribute' => 'auto_generate'],
|
||||
'tocHeadingLevels' => ['group' => 'content', 'attribute' => 'heading_levels'],
|
||||
'tocSmoothScroll' => ['group' => 'content', 'attribute' => 'smooth_scroll'],
|
||||
|
||||
// Typography
|
||||
'tocTitleFontSize' => ['group' => 'typography', 'attribute' => 'title_font_size'],
|
||||
'tocTitleFontWeight' => ['group' => 'typography', 'attribute' => 'title_font_weight'],
|
||||
'tocLinkFontSize' => ['group' => 'typography', 'attribute' => 'link_font_size'],
|
||||
'tocLinkLineHeight' => ['group' => 'typography', 'attribute' => 'link_line_height'],
|
||||
'tocLevelThreeFontSize' => ['group' => 'typography', 'attribute' => 'level_three_font_size'],
|
||||
'tocLevelFourFontSize' => ['group' => 'typography', 'attribute' => 'level_four_font_size'],
|
||||
|
||||
// Colors
|
||||
'tocBackgroundColor' => ['group' => 'colors', 'attribute' => 'background_color'],
|
||||
'tocBorderColor' => ['group' => 'colors', 'attribute' => 'border_color'],
|
||||
'tocTitleColor' => ['group' => 'colors', 'attribute' => 'title_color'],
|
||||
'tocTitleBorderColor' => ['group' => 'colors', 'attribute' => 'title_border_color'],
|
||||
'tocLinkColor' => ['group' => 'colors', 'attribute' => 'link_color'],
|
||||
'tocLinkHoverColor' => ['group' => 'colors', 'attribute' => 'link_hover_color'],
|
||||
'tocLinkHoverBackground' => ['group' => 'colors', 'attribute' => 'link_hover_background'],
|
||||
'tocActiveBorderColor' => ['group' => 'colors', 'attribute' => 'active_border_color'],
|
||||
'tocActiveBackgroundColor' => ['group' => 'colors', 'attribute' => 'active_background_color'],
|
||||
'tocActiveTextColor' => ['group' => 'colors', 'attribute' => 'active_text_color'],
|
||||
'tocScrollbarTrackColor' => ['group' => 'colors', 'attribute' => 'scrollbar_track_color'],
|
||||
'tocScrollbarThumbColor' => ['group' => 'colors', 'attribute' => 'scrollbar_thumb_color'],
|
||||
|
||||
// Spacing
|
||||
'tocContainerPadding' => ['group' => 'spacing', 'attribute' => 'container_padding'],
|
||||
'tocMarginBottom' => ['group' => 'spacing', 'attribute' => 'margin_bottom'],
|
||||
'tocTitlePaddingBottom' => ['group' => 'spacing', 'attribute' => 'title_padding_bottom'],
|
||||
'tocTitleMarginBottom' => ['group' => 'spacing', 'attribute' => 'title_margin_bottom'],
|
||||
'tocItemMarginBottom' => ['group' => 'spacing', 'attribute' => 'item_margin_bottom'],
|
||||
'tocLinkPadding' => ['group' => 'spacing', 'attribute' => 'link_padding'],
|
||||
'tocLevelThreePaddingLeft' => ['group' => 'spacing', 'attribute' => 'level_three_padding_left'],
|
||||
'tocLevelFourPaddingLeft' => ['group' => 'spacing', 'attribute' => 'level_four_padding_left'],
|
||||
'tocScrollbarWidth' => ['group' => 'spacing', 'attribute' => 'scrollbar_width'],
|
||||
|
||||
// Visual Effects
|
||||
'tocBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'border_radius'],
|
||||
'tocBoxShadow' => ['group' => 'visual_effects', 'attribute' => 'box_shadow'],
|
||||
'tocBorderWidth' => ['group' => 'visual_effects', 'attribute' => 'border_width'],
|
||||
'tocLinkBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'link_border_radius'],
|
||||
'tocActiveBorderLeftWidth' => ['group' => 'visual_effects', 'attribute' => 'active_border_left_width'],
|
||||
'tocTransitionDuration' => ['group' => 'visual_effects', 'attribute' => 'transition_duration'],
|
||||
'tocScrollbarBorderRadius' => ['group' => 'visual_effects', 'attribute' => 'scrollbar_border_radius'],
|
||||
|
||||
// Behavior
|
||||
'tocIsSticky' => ['group' => 'behavior', 'attribute' => 'is_sticky'],
|
||||
'tocScrollOffset' => ['group' => 'behavior', 'attribute' => 'scroll_offset'],
|
||||
'tocMaxHeight' => ['group' => 'behavior', 'attribute' => 'max_height'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,638 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\TableOfContents\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para la Tabla de Contenido
|
||||
*
|
||||
* Responsabilidad:
|
||||
* - Generar HTML del formulario de configuracion
|
||||
* - Usar Design System (Bootstrap 5)
|
||||
* - Cargar valores desde BD via AdminDashboardRenderer
|
||||
*
|
||||
* @package ROITheme\Admin\TableOfContents\Infrastructure\Ui
|
||||
*/
|
||||
final class TableOfContentsFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
|
||||
// Columna izquierda
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= $this->buildBehaviorGroup($componentId);
|
||||
$html .= $this->buildEffectsGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildTypographyGroup($componentId);
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildSpacingGroup($componentId);
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-list-nested me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuracion de Tabla de Contenido';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Navegacion automatica con ScrollSpy';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="table-of-contents">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// is_enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= $this->buildSwitch('tocEnabled', 'Activar tabla de contenido', 'bi-power', $enabled);
|
||||
|
||||
// show_on_desktop
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= $this->buildSwitch('tocShowOnDesktop', 'Mostrar en escritorio', 'bi-display', $showOnDesktop);
|
||||
|
||||
// show_on_mobile
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
||||
$html .= $this->buildSwitch('tocShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'toc');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-card-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// title
|
||||
$title = $this->renderer->getFieldValue($componentId, 'content', 'title', 'Tabla de Contenido');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="tocTitle" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-type me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Titulo';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="tocTitle" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($title) . '" placeholder="Tabla de Contenido">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// auto_generate
|
||||
$autoGenerate = $this->renderer->getFieldValue($componentId, 'content', 'auto_generate', true);
|
||||
$html .= $this->buildSwitch('tocAutoGenerate', 'Generar automaticamente', 'bi-magic', $autoGenerate);
|
||||
$html .= ' <small class="text-muted d-block mb-3">Genera TOC desde los encabezados del contenido</small>';
|
||||
|
||||
// heading_levels
|
||||
$headingLevels = $this->renderer->getFieldValue($componentId, 'content', 'heading_levels', 'h2,h3');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="tocHeadingLevels" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-list-ol me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Niveles de encabezados';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="tocHeadingLevels" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($headingLevels) . '" placeholder="h2,h3">';
|
||||
$html .= ' <small class="text-muted">Separados por coma: h2,h3,h4</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// smooth_scroll
|
||||
$smoothScroll = $this->renderer->getFieldValue($componentId, 'content', 'smooth_scroll', true);
|
||||
$html .= $this->buildSwitch('tocSmoothScroll', 'Scroll suave', 'bi-arrow-down-circle', $smoothScroll);
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildBehaviorGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-gear me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Comportamiento';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// is_sticky
|
||||
$isSticky = $this->renderer->getFieldValue($componentId, 'behavior', 'is_sticky', true);
|
||||
$html .= $this->buildSwitch('tocIsSticky', 'Sticky (fijo al scroll)', 'bi-pin', $isSticky);
|
||||
|
||||
// scroll_offset
|
||||
$scrollOffset = $this->renderer->getFieldValue($componentId, 'behavior', 'scroll_offset', '100');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="tocScrollOffset" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-arrows-vertical me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Offset de scroll (px)';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="tocScrollOffset" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($scrollOffset) . '" placeholder="100">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// max_height
|
||||
$maxHeight = $this->renderer->getFieldValue($componentId, 'behavior', 'max_height', 'calc(100vh - 71px - 10px - 250px - 15px - 15px)');
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <label for="tocMaxHeight" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-arrows-expand me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Altura maxima';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" id="tocMaxHeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($maxHeight) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-fonts me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografia';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// title_font_size
|
||||
$titleFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_size', '1rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocTitleFontSize" class="form-label small mb-1 fw-semibold">Tamano titulo</label>';
|
||||
$html .= ' <input type="text" id="tocTitleFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontSize) . '" placeholder="1rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// title_font_weight
|
||||
$titleFontWeight = $this->renderer->getFieldValue($componentId, 'typography', 'title_font_weight', '600');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocTitleFontWeight" class="form-label small mb-1 fw-semibold">Peso titulo</label>';
|
||||
$html .= ' <input type="text" id="tocTitleFontWeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleFontWeight) . '" placeholder="600">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// link_font_size
|
||||
$linkFontSize = $this->renderer->getFieldValue($componentId, 'typography', 'link_font_size', '0.9rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLinkFontSize" class="form-label small mb-1 fw-semibold">Tamano enlaces</label>';
|
||||
$html .= ' <input type="text" id="tocLinkFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($linkFontSize) . '" placeholder="0.9rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// link_line_height
|
||||
$linkLineHeight = $this->renderer->getFieldValue($componentId, 'typography', 'link_line_height', '1.3');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLinkLineHeight" class="form-label small mb-1 fw-semibold">Altura linea</label>';
|
||||
$html .= ' <input type="text" id="tocLinkLineHeight" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($linkLineHeight) . '" placeholder="1.3">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// level_three_font_size
|
||||
$level3FontSize = $this->renderer->getFieldValue($componentId, 'typography', 'level_three_font_size', '0.85rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLevelThreeFontSize" class="form-label small mb-1 fw-semibold">Tamano H3</label>';
|
||||
$html .= ' <input type="text" id="tocLevelThreeFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($level3FontSize) . '" placeholder="0.85rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// level_four_font_size
|
||||
$level4FontSize = $this->renderer->getFieldValue($componentId, 'typography', 'level_four_font_size', '0.8rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLevelFourFontSize" class="form-label small mb-1 fw-semibold">Tamano H4</label>';
|
||||
$html .= ' <input type="text" id="tocLevelFourFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($level4FontSize) . '" placeholder="0.8rem">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Colores principales
|
||||
$html .= ' <p class="small fw-semibold mb-2">Contenedor</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#ffffff');
|
||||
$html .= $this->buildColorPicker('tocBackgroundColor', 'Fondo', $bgColor);
|
||||
|
||||
$borderColor = $this->renderer->getFieldValue($componentId, 'colors', 'border_color', '#E6E9ED');
|
||||
$html .= $this->buildColorPicker('tocBorderColor', 'Borde', $borderColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores del titulo
|
||||
$html .= ' <p class="small fw-semibold mb-2">Titulo</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$titleColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('tocTitleColor', 'Color', $titleColor);
|
||||
|
||||
$titleBorderColor = $this->renderer->getFieldValue($componentId, 'colors', 'title_border_color', '#E6E9ED');
|
||||
$html .= $this->buildColorPicker('tocTitleBorderColor', 'Borde', $titleBorderColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores de enlaces
|
||||
$html .= ' <p class="small fw-semibold mb-2">Enlaces</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$linkColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_color', '#6B7280');
|
||||
$html .= $this->buildColorPicker('tocLinkColor', 'Normal', $linkColor);
|
||||
|
||||
$linkHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('tocLinkHoverColor', 'Hover', $linkHoverColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$linkHoverBg = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_background', '#F9FAFB');
|
||||
$html .= $this->buildColorPicker('tocLinkHoverBackground', 'Fondo hover', $linkHoverBg);
|
||||
|
||||
$activeBorderColor = $this->renderer->getFieldValue($componentId, 'colors', 'active_border_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('tocActiveBorderColor', 'Borde activo', $activeBorderColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores de activo
|
||||
$html .= ' <p class="small fw-semibold mb-2">Estado Activo</p>';
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
$activeBgColor = $this->renderer->getFieldValue($componentId, 'colors', 'active_background_color', '#F9FAFB');
|
||||
$html .= $this->buildColorPicker('tocActiveBackgroundColor', 'Fondo', $activeBgColor);
|
||||
|
||||
$activeTextColor = $this->renderer->getFieldValue($componentId, 'colors', 'active_text_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('tocActiveTextColor', 'Texto', $activeTextColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Colores de scrollbar
|
||||
$html .= ' <p class="small fw-semibold mb-2">Scrollbar</p>';
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
$scrollbarTrack = $this->renderer->getFieldValue($componentId, 'colors', 'scrollbar_track_color', '#F9FAFB');
|
||||
$html .= $this->buildColorPicker('tocScrollbarTrackColor', 'Pista', $scrollbarTrack);
|
||||
|
||||
$scrollbarThumb = $this->renderer->getFieldValue($componentId, 'colors', 'scrollbar_thumb_color', '#6B7280');
|
||||
$html .= $this->buildColorPicker('tocScrollbarThumbColor', 'Thumb', $scrollbarThumb);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-move me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// container_padding
|
||||
$containerPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'container_padding', '12px 16px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocContainerPadding" class="form-label small mb-1 fw-semibold">Padding contenedor</label>';
|
||||
$html .= ' <input type="text" id="tocContainerPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($containerPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// margin_bottom
|
||||
$marginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'margin_bottom', '13px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocMarginBottom" class="form-label small mb-1 fw-semibold">Margen inferior</label>';
|
||||
$html .= ' <input type="text" id="tocMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($marginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// title_padding_bottom
|
||||
$titlePaddingBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_padding_bottom', '8px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocTitlePaddingBottom" class="form-label small mb-1 fw-semibold">Padding titulo</label>';
|
||||
$html .= ' <input type="text" id="tocTitlePaddingBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titlePaddingBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// title_margin_bottom
|
||||
$titleMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'title_margin_bottom', '0.75rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocTitleMarginBottom" class="form-label small mb-1 fw-semibold">Margen titulo</label>';
|
||||
$html .= ' <input type="text" id="tocTitleMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($titleMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// item_margin_bottom
|
||||
$itemMarginBottom = $this->renderer->getFieldValue($componentId, 'spacing', 'item_margin_bottom', '0.15rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocItemMarginBottom" class="form-label small mb-1 fw-semibold">Margen items</label>';
|
||||
$html .= ' <input type="text" id="tocItemMarginBottom" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($itemMarginBottom) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// link_padding
|
||||
$linkPadding = $this->renderer->getFieldValue($componentId, 'spacing', 'link_padding', '0.3rem 0.85rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLinkPadding" class="form-label small mb-1 fw-semibold">Padding enlaces</label>';
|
||||
$html .= ' <input type="text" id="tocLinkPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($linkPadding) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// level_three_padding_left
|
||||
$level3PaddingLeft = $this->renderer->getFieldValue($componentId, 'spacing', 'level_three_padding_left', '1.5rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLevelThreePaddingLeft" class="form-label small mb-1 fw-semibold">Padding H3</label>';
|
||||
$html .= ' <input type="text" id="tocLevelThreePaddingLeft" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($level3PaddingLeft) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// level_four_padding_left
|
||||
$level4PaddingLeft = $this->renderer->getFieldValue($componentId, 'spacing', 'level_four_padding_left', '2rem');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLevelFourPaddingLeft" class="form-label small mb-1 fw-semibold">Padding H4</label>';
|
||||
$html .= ' <input type="text" id="tocLevelFourPaddingLeft" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($level4PaddingLeft) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildEffectsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-magic me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Efectos Visuales';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// border_radius
|
||||
$borderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_radius', '8px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocBorderRadius" class="form-label small mb-1 fw-semibold">Radio borde</label>';
|
||||
$html .= ' <input type="text" id="tocBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// border_width
|
||||
$borderWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'border_width', '1px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocBorderWidth" class="form-label small mb-1 fw-semibold">Grosor borde</label>';
|
||||
$html .= ' <input type="text" id="tocBorderWidth" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($borderWidth) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// box_shadow
|
||||
$boxShadow = $this->renderer->getFieldValue($componentId, 'visual_effects', 'box_shadow', '0 2px 8px rgba(0, 0, 0, 0.08)');
|
||||
$html .= ' <div class="mb-3">';
|
||||
$html .= ' <label for="tocBoxShadow" class="form-label small mb-1 fw-semibold">Sombra</label>';
|
||||
$html .= ' <input type="text" id="tocBoxShadow" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($boxShadow) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-3">';
|
||||
|
||||
// link_border_radius
|
||||
$linkBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'link_border_radius', '4px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocLinkBorderRadius" class="form-label small mb-1 fw-semibold">Radio enlaces</label>';
|
||||
$html .= ' <input type="text" id="tocLinkBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($linkBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// active_border_left_width
|
||||
$activeBorderLeftWidth = $this->renderer->getFieldValue($componentId, 'visual_effects', 'active_border_left_width', '3px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocActiveBorderLeftWidth" class="form-label small mb-1 fw-semibold">Borde activo</label>';
|
||||
$html .= ' <input type="text" id="tocActiveBorderLeftWidth" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($activeBorderLeftWidth) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// transition_duration
|
||||
$transitionDuration = $this->renderer->getFieldValue($componentId, 'visual_effects', 'transition_duration', '0.3s');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocTransitionDuration" class="form-label small mb-1 fw-semibold">Transicion</label>';
|
||||
$html .= ' <input type="text" id="tocTransitionDuration" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($transitionDuration) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
// scrollbar_border_radius
|
||||
$scrollbarBorderRadius = $this->renderer->getFieldValue($componentId, 'visual_effects', 'scrollbar_border_radius', '3px');
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="tocScrollbarBorderRadius" class="form-label small mb-1 fw-semibold">Radio scrollbar</label>';
|
||||
$html .= ' <input type="text" id="tocScrollbarBorderRadius" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($scrollbarBorderRadius) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSwitch(string $id, string $label, string $icon, bool $checked): string
|
||||
{
|
||||
$html = ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' <strong>%s</strong>', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= sprintf(
|
||||
' <label class="form-label small fw-semibold">%s</label>',
|
||||
esc_html($label)
|
||||
);
|
||||
$html .= ' <div class="input-group input-group-sm">';
|
||||
$html .= sprintf(
|
||||
' <input type="color" class="form-control form-control-color" id="%s" value="%s">',
|
||||
esc_attr($id),
|
||||
esc_attr($value)
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <span class="input-group-text" id="%sValue">%s</span>',
|
||||
esc_attr($id),
|
||||
esc_html(strtoupper($value))
|
||||
);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ThemeSettings\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Theme Settings
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*
|
||||
* NOTA: Logo/branding se gestiona desde el componente navbar
|
||||
*/
|
||||
final class ThemeSettingsFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'theme-settings';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Layout
|
||||
'themeSettingsContainerMaxWidth' => ['group' => 'layout', 'attribute' => 'container_max_width'],
|
||||
'themeSettingsContentColumnWidth' => ['group' => 'layout', 'attribute' => 'content_column_width'],
|
||||
|
||||
// Custom Code
|
||||
'themeSettingsCustomCss' => ['group' => 'custom_code', 'attribute' => 'custom_css'],
|
||||
'themeSettingsCustomJsHeader' => ['group' => 'custom_code', 'attribute' => 'custom_js_header'],
|
||||
'themeSettingsCustomJsFooter' => ['group' => 'custom_code', 'attribute' => 'custom_js_footer'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\ThemeSettings\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
/**
|
||||
* FormBuilder para Theme Settings
|
||||
*
|
||||
* RESPONSABILIDAD: Generar formulario de configuraciones globales del tema
|
||||
* (JavaScript personalizado)
|
||||
*
|
||||
* NOTA: CSS personalizado se gestiona desde CustomCSSManager (TIPO 3)
|
||||
* Analytics y AdSense se gestionan desde el componente adsense-placement
|
||||
*
|
||||
* @package ROITheme\Admin\ThemeSettings\Infrastructure\Ui
|
||||
*/
|
||||
final class ThemeSettingsFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
// Layout Group
|
||||
$html .= $this->buildLayoutGroup($componentId);
|
||||
|
||||
// JavaScript Personalizado (solo 1 card)
|
||||
$html .= $this->buildJsGroup($componentId);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildLayoutGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-4" style="border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-layout-wtf me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Layout y Contenedor';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-3">';
|
||||
|
||||
// Container Max Width
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$containerWidth = $this->renderer->getFieldValue($componentId, 'layout', 'container_max_width', '1320');
|
||||
$containerWidthStr = is_string($containerWidth) ? $containerWidth : '1320';
|
||||
$html .= $this->buildSelect(
|
||||
'themeSettingsContainerMaxWidth',
|
||||
'Ancho maximo del contenedor',
|
||||
$containerWidthStr,
|
||||
[
|
||||
'1140' => '1140px (Bootstrap md)',
|
||||
'1200' => '1200px (Compacto)',
|
||||
'1320' => '1320px (Bootstrap xxl - Default)',
|
||||
'1400' => '1400px (Amplio)',
|
||||
'100%' => '100% (Fluido)'
|
||||
],
|
||||
'Valores menores dejan mas espacio para Rail Ads laterales'
|
||||
);
|
||||
$html .= ' </div>';
|
||||
|
||||
// Content Column Width
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$columnWidth = $this->renderer->getFieldValue($componentId, 'layout', 'content_column_width', 'col-lg-9');
|
||||
$columnWidthStr = is_string($columnWidth) ? $columnWidth : 'col-lg-9';
|
||||
$html .= $this->buildSelect(
|
||||
'themeSettingsContentColumnWidth',
|
||||
'Ancho columna de contenido',
|
||||
$columnWidthStr,
|
||||
[
|
||||
'col-lg-8' => '8 columnas (66.67%)',
|
||||
'col-lg-9' => '9 columnas (75% - Default)',
|
||||
'col-lg-10' => '10 columnas (83.33%)',
|
||||
'col-lg-12' => '12 columnas (100% sin sidebar)'
|
||||
],
|
||||
'Proporcion de la columna principal vs sidebar'
|
||||
);
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="alert alert-info small mb-0 mt-3">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' Reduce el ancho del contenedor para dar mas espacio a los Rail Ads en pantallas grandes.';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildSelect(string $id, string $label, string $value, array $options, string $helpText = ''): string
|
||||
{
|
||||
$html = ' <div class="mb-3">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select class="form-select form-select-sm" id="' . esc_attr($id) . '">';
|
||||
foreach ($options as $optionValue => $optionLabel) {
|
||||
$selected = ($value === (string) $optionValue) ? ' selected' : '';
|
||||
$html .= ' <option value="' . esc_attr($optionValue) . '"' . $selected . '>' . esc_html($optionLabel) . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
if (!empty($helpText)) {
|
||||
$html .= ' <div class="form-text small">' . esc_html($helpText) . '</div>';
|
||||
}
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-gear me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuraciones Globales del Tema';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Layout y JavaScript Personalizado';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="theme-settings">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildJsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-filetype-js me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' JavaScript Personalizado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$customJsHeader = $this->renderer->getFieldValue($componentId, 'custom_code', 'custom_js_header', '');
|
||||
$html .= $this->buildTextareaCode(
|
||||
'themeSettingsCustomJsHeader',
|
||||
'JavaScript en Header',
|
||||
$customJsHeader,
|
||||
'Se inyecta en wp_head. No incluir etiquetas <script>',
|
||||
5
|
||||
);
|
||||
|
||||
$customJsFooter = $this->renderer->getFieldValue($componentId, 'custom_code', 'custom_js_footer', '');
|
||||
$html .= $this->buildTextareaCode(
|
||||
'themeSettingsCustomJsFooter',
|
||||
'JavaScript en Footer',
|
||||
$customJsFooter,
|
||||
'Se inyecta en wp_footer. No incluir etiquetas <script>',
|
||||
5
|
||||
);
|
||||
|
||||
$html .= ' <div class="alert alert-danger small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-exclamation-octagon me-1"></i>';
|
||||
$html .= ' <strong>Advertencia:</strong> El codigo JS puede afectar el rendimiento y seguridad del sitio.';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTextareaCode(string $id, string $label, mixed $value, string $helpText = '', int $rows = 4): string
|
||||
{
|
||||
$value = $this->normalizeStringValue($value);
|
||||
|
||||
$html = ' <div class="mb-3">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea class="form-control form-control-sm font-monospace" id="' . esc_attr($id) . '" rows="' . $rows . '" style="font-size: 0.85em;">' . esc_textarea($value) . '</textarea>';
|
||||
if (!empty($helpText)) {
|
||||
$html .= ' <div class="form-text small">' . $helpText . '</div>';
|
||||
}
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliza un valor a string para inputs de formulario
|
||||
*/
|
||||
private function normalizeStringValue(mixed $value): string
|
||||
{
|
||||
if ($value === false) {
|
||||
return '0';
|
||||
}
|
||||
if ($value === true) {
|
||||
return '1';
|
||||
}
|
||||
return (string) $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\TopNotificationBar\Infrastructure\FieldMapping;
|
||||
|
||||
use ROITheme\Admin\Shared\Domain\Contracts\FieldMapperInterface;
|
||||
|
||||
/**
|
||||
* Field Mapper para Top Notification Bar
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Mapear field IDs del formulario a atributos de BD
|
||||
* - Solo conoce sus propios campos (modularidad)
|
||||
*/
|
||||
final class TopNotificationBarFieldMapper implements FieldMapperInterface
|
||||
{
|
||||
public function getComponentName(): string
|
||||
{
|
||||
return 'top-notification-bar';
|
||||
}
|
||||
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Visibility
|
||||
'topBarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'topBarShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'topBarShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'topBarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
|
||||
'topBarHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'topBarVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'topBarVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'topBarVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'topBarVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'topBarVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'topBarExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'topBarExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'topBarExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'topBarExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'topBarIconClass' => ['group' => 'content', 'attribute' => 'icon_class'],
|
||||
'topBarLabelText' => ['group' => 'content', 'attribute' => 'label_text'],
|
||||
'topBarMessageText' => ['group' => 'content', 'attribute' => 'message_text'],
|
||||
'topBarLinkText' => ['group' => 'content', 'attribute' => 'link_text'],
|
||||
'topBarLinkUrl' => ['group' => 'content', 'attribute' => 'link_url'],
|
||||
|
||||
// Colors
|
||||
'topBarBackgroundColor' => ['group' => 'colors', 'attribute' => 'background_color'],
|
||||
'topBarTextColor' => ['group' => 'colors', 'attribute' => 'text_color'],
|
||||
'topBarLabelColor' => ['group' => 'colors', 'attribute' => 'label_color'],
|
||||
'topBarIconColor' => ['group' => 'colors', 'attribute' => 'icon_color'],
|
||||
'topBarLinkColor' => ['group' => 'colors', 'attribute' => 'link_color'],
|
||||
'topBarLinkHoverColor' => ['group' => 'colors', 'attribute' => 'link_hover_color'],
|
||||
|
||||
// Spacing
|
||||
'topBarFontSize' => ['group' => 'spacing', 'attribute' => 'font_size'],
|
||||
'topBarPadding' => ['group' => 'spacing', 'attribute' => 'padding'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\TopNotificationBar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
final class TopNotificationBarFormBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
public function buildForm(string $componentId): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
// Header
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
// Layout 2 columnas
|
||||
$html .= '<div class="row g-3">';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildVisibilityGroup($componentId);
|
||||
$html .= $this->buildContentGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-lg-6">';
|
||||
$html .= $this->buildColorsGroup($componentId);
|
||||
$html .= $this->buildTypographyAndSpacingGroup($componentId);
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildHeader(string $componentId): string
|
||||
{
|
||||
$html = '<div class="rounded p-4 mb-4 shadow text-white" ';
|
||||
$html .= 'style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="d-flex align-items-center justify-content-between flex-wrap gap-3">';
|
||||
$html .= ' <div>';
|
||||
$html .= ' <h3 class="h4 mb-1 fw-bold">';
|
||||
$html .= ' <i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Configuración de TopBar';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Personaliza la barra de notificación superior del sitio';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="top-notification-bar">';
|
||||
$html .= ' <i class="bi bi-arrow-counterclockwise me-1"></i>';
|
||||
$html .= ' Restaurar valores por defecto';
|
||||
$html .= ' </button>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildVisibilityGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Activación y Visibilidad';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Switch: Enabled
|
||||
$enabled = $this->renderer->getFieldValue($componentId, 'visibility', 'is_enabled', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="topBarEnabled" ';
|
||||
$html .= checked($enabled, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="topBarEnabled" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-power me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Activar TopBar</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Mobile
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="topBarShowOnMobile" ';
|
||||
$html .= checked($showOnMobile, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="topBarShowOnMobile" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-phone me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Mobile</strong> <span class="text-muted">(<768px)</span>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Show on Desktop
|
||||
$showOnDesktop = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_desktop', true);
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="topBarShowOnDesktop" ';
|
||||
$html .= checked($showOnDesktop, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="topBarShowOnDesktop" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-display me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Mostrar en Desktop</strong> <span class="text-muted">(≥768px)</span>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('topBarVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'topBar');
|
||||
|
||||
// Switch: CSS Crítico
|
||||
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
||||
$html .= ' <div class="mb-2 mt-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="topBarIsCritical" ';
|
||||
$html .= checked($isCritical, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="topBarIsCritical" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-lightning-charge me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>CSS Crítico</strong>';
|
||||
$html .= ' <small class="text-muted d-block">Inyectar CSS en <head> para optimizar LCP</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Ocultar para usuarios logueados (Plan 99.16)
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($componentId, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="topBarHideForLoggedIn" ';
|
||||
$html .= checked($hideForLoggedIn, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="topBarHideForLoggedIn" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-person-lock me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Ocultar para usuarios logueados</strong>';
|
||||
$html .= ' <small class="text-muted d-block">No mostrar a usuarios con sesión iniciada</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-chat-text me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Contenido';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// icon_class + label_text (row)
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarIconClass" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-star-fill me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Clase del ícono';
|
||||
$html .= ' </label>';
|
||||
$iconClass = $this->renderer->getFieldValue($componentId, 'content', 'icon_class', 'bi-megaphone-fill');
|
||||
$html .= ' <input type="text" id="topBarIconClass" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($iconClass) . '" placeholder="bi-...">';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarLabelText" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-tag me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Etiqueta';
|
||||
$html .= ' </label>';
|
||||
$labelText = $this->renderer->getFieldValue($componentId, 'content', 'label_text', 'Nuevo:');
|
||||
$html .= ' <input type="text" id="topBarLabelText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($labelText) . '" maxlength="30">';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// message_text (textarea)
|
||||
$messageText = $this->renderer->getFieldValue($componentId, 'content', 'message_text',
|
||||
'Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="topBarMessageText" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-chat-dots me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mensaje';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea id="topBarMessageText" class="form-control form-control-sm" rows="3" maxlength="200">';
|
||||
$html .= esc_textarea($messageText);
|
||||
$html .= ' </textarea>';
|
||||
$html .= ' <small class="text-muted">Máximo 200 caracteres</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// link_text + link_url (row)
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarLinkText" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Texto del enlace';
|
||||
$html .= ' </label>';
|
||||
$linkText = $this->renderer->getFieldValue($componentId, 'content', 'link_text', 'Ver Catálogo');
|
||||
$html .= ' <input type="text" id="topBarLinkText" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($linkText) . '" maxlength="50">';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarLinkUrl" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-box-arrow-up-right me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' URL';
|
||||
$html .= ' </label>';
|
||||
$linkUrl = $this->renderer->getFieldValue($componentId, 'content', 'link_url', '#');
|
||||
$html .= ' <input type="url" id="topBarLinkUrl" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_url($linkUrl) . '" placeholder="https://...">';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-palette me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Colores';
|
||||
$html .= ' </h5>';
|
||||
|
||||
// Grid 2x3 de color pickers
|
||||
$html .= ' <div class="row g-2 mb-2">';
|
||||
|
||||
// Background Color
|
||||
$bgColor = $this->renderer->getFieldValue($componentId, 'colors', 'background_color', '#0E2337');
|
||||
$html .= $this->buildColorPicker('topBarBackgroundColor', 'Color de fondo', 'paint-bucket', $bgColor);
|
||||
|
||||
// Text Color
|
||||
$textColor = $this->renderer->getFieldValue($componentId, 'colors', 'text_color', '#FFFFFF');
|
||||
$html .= $this->buildColorPicker('topBarTextColor', 'Color de texto', 'fonts', $textColor);
|
||||
|
||||
// Label Color
|
||||
$labelColor = $this->renderer->getFieldValue($componentId, 'colors', 'label_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('topBarLabelColor', 'Color etiqueta', 'tag-fill', $labelColor);
|
||||
|
||||
// Icon Color
|
||||
$iconColor = $this->renderer->getFieldValue($componentId, 'colors', 'icon_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('topBarIconColor', 'Color ícono', 'star', $iconColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
// Row 2 de color pickers
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// Link Color
|
||||
$linkColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_color', '#FFFFFF');
|
||||
$html .= $this->buildColorPicker('topBarLinkColor', 'Color enlace', 'link', $linkColor);
|
||||
|
||||
// Link Hover Color
|
||||
$linkHoverColor = $this->renderer->getFieldValue($componentId, 'colors', 'link_hover_color', '#FF8600');
|
||||
$html .= $this->buildColorPicker('topBarLinkHoverColor', 'Color enlace (hover)', 'hand-index', $linkHoverColor);
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTypographyAndSpacingGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-arrows-fullscreen me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tipografía y Espaciado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-2 mb-0">';
|
||||
|
||||
// Font Size
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarFontSize" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-type me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Tamaño de fuente';
|
||||
$html .= ' </label>';
|
||||
$fontSize = $this->renderer->getFieldValue($componentId, 'spacing', 'font_size', '0.9rem');
|
||||
$html .= ' <input type="text" id="topBarFontSize" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($fontSize) . '">';
|
||||
$html .= ' <small class="text-muted">Ej: 0.9rem, 14px</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Padding
|
||||
$html .= ' <div class="col-6">';
|
||||
$html .= ' <label for="topBarPadding" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-bounding-box me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Padding vertical';
|
||||
$html .= ' </label>';
|
||||
$padding = $this->renderer->getFieldValue($componentId, 'spacing', 'padding', '0.5rem 0');
|
||||
$html .= ' <input type="text" id="topBarPadding" class="form-control form-control-sm" ';
|
||||
$html .= ' value="' . esc_attr($padding) . '">';
|
||||
$html .= ' <small class="text-muted">Ej: 0.5rem 0</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildColorPicker(string $id, string $label, string $icon, string $value): string
|
||||
{
|
||||
$html = ' <div class="col-6">';
|
||||
$html .= ' <label for="' . $id . '" class="form-label small mb-1 fw-semibold" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-' . $icon . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . $label;
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="color" id="' . $id . '" class="form-control form-control-color w-100" ';
|
||||
$html .= ' value="' . esc_attr($value) . '" title="' . esc_attr($label) . '">';
|
||||
$html .= ' <small class="text-muted d-block mt-1" id="' . $id . 'Value">' . esc_html(strtoupper($value)) . '</small>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TopBar - Preview de Diseño</title>
|
||||
|
||||
<!-- Bootstrap 5 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
background-color: #f0f0f1;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ============================================================
|
||||
TAB: TOP NOTIFICATION BAR CONFIGURATION
|
||||
============================================================ -->
|
||||
<div class="tab-pane fade show active" id="topBarTab" role="tabpanel">
|
||||
|
||||
<!-- ========================================
|
||||
PATRÓN 1: HEADER CON GRADIENTE
|
||||
======================================== -->
|
||||
<div class="rounded p-4 mb-4 shadow text-white" style="background: linear-gradient(135deg, #0E2337 0%, #1e3a5f 100%); border-left: 4px solid #FF8600;">
|
||||
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h3 class="h4 mb-1 fw-bold">
|
||||
<i class="bi bi-megaphone-fill me-2" style="color: #FF8600;"></i>
|
||||
Configuración de TopBar
|
||||
</h3>
|
||||
<p class="mb-0 small" style="opacity: 0.85;">
|
||||
Personaliza la barra de notificación superior del sitio
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-light" id="resetTopBarDefaults">
|
||||
<i class="bi bi-arrow-counterclockwise me-1"></i>
|
||||
Restaurar valores por defecto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
PATRÓN 2: LAYOUT 2 COLUMNAS
|
||||
======================================== -->
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
<!-- ========================================
|
||||
GRUPO 1: ACTIVACIÓN Y VISIBILIDAD (OBLIGATORIO)
|
||||
PATRÓN 3: CARD CON BORDER-LEFT NAVY
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-toggle-on me-2" style="color: #FF8600;"></i>
|
||||
Activación y Visibilidad
|
||||
</h5>
|
||||
|
||||
<!-- ⚠️ PATRÓN 4: SWITCHES VERTICALES CON ICONOS (3 OBLIGATORIOS) -->
|
||||
|
||||
<!-- Switch 1: Enabled (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="topBarEnabled" checked>
|
||||
<label class="form-check-label small" for="topBarEnabled" style="color: #495057;">
|
||||
<i class="bi bi-power me-1" style="color: #FF8600;"></i>
|
||||
<strong>Activar TopBar</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Switch 2: Show on Mobile (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="topBarShowOnMobile" checked>
|
||||
<label class="form-check-label small" for="topBarShowOnMobile" style="color: #495057;">
|
||||
<i class="bi bi-phone me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar en Mobile</strong> <span class="text-muted">(<768px)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Switch 3: Show on Desktop (OBLIGATORIO) -->
|
||||
<div class="mb-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="topBarShowOnDesktop" checked>
|
||||
<label class="form-check-label small" for="topBarShowOnDesktop" style="color: #495057;">
|
||||
<i class="bi bi-display me-1" style="color: #FF8600;"></i>
|
||||
<strong>Mostrar en Desktop</strong> <span class="text-muted">(≥768px)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campo adicional del schema: show_on_pages (select) -->
|
||||
<div class="mb-0 mt-3">
|
||||
<label for="topBarShowOnPages" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>
|
||||
Mostrar en
|
||||
</label>
|
||||
<select id="topBarShowOnPages" class="form-select form-select-sm">
|
||||
<option value="all" selected>Todas las páginas</option>
|
||||
<option value="home">Solo página de inicio</option>
|
||||
<option value="posts">Solo posts individuales</option>
|
||||
<option value="pages">Solo páginas</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 2: CONTENIDO
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-chat-text me-2" style="color: #FF8600;"></i>
|
||||
Contenido
|
||||
</h5>
|
||||
|
||||
<!-- icon_class + label_text (compactados) -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="topBarIconClass" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-star-fill me-1" style="color: #FF8600;"></i>
|
||||
Clase del ícono
|
||||
</label>
|
||||
<input type="text" id="topBarIconClass" class="form-control form-control-sm" value="bi-megaphone-fill" placeholder="bi-...">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarLabelText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-tag me-1" style="color: #FF8600;"></i>
|
||||
Etiqueta
|
||||
</label>
|
||||
<input type="text" id="topBarLabelText" class="form-control form-control-sm" value="Nuevo:" maxlength="30">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- message_text (textarea full width) -->
|
||||
<div class="mb-2">
|
||||
<label for="topBarMessageText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-chat-dots me-1" style="color: #FF8600;"></i>
|
||||
Mensaje
|
||||
</label>
|
||||
<textarea id="topBarMessageText" class="form-control form-control-sm" rows="3" maxlength="200">Accede a más de 200,000 Análisis de Precios Unitarios actualizados para 2025.</textarea>
|
||||
<small class="text-muted">Máximo 200 caracteres</small>
|
||||
</div>
|
||||
|
||||
<!-- link_text + link_url (compactados) -->
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="topBarLinkText" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>
|
||||
Texto del enlace
|
||||
</label>
|
||||
<input type="text" id="topBarLinkText" class="form-control form-control-sm" value="Ver Catálogo" maxlength="50">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarLinkUrl" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-box-arrow-up-right me-1" style="color: #FF8600;"></i>
|
||||
URL
|
||||
</label>
|
||||
<input type="url" id="topBarLinkUrl" class="form-control form-control-sm" value="#" placeholder="https://...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<!-- ========================================
|
||||
GRUPO 3: ESTILOS - COLORES
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-palette me-2" style="color: #FF8600;"></i>
|
||||
Estilos - Colores
|
||||
</h5>
|
||||
|
||||
<!-- PATRÓN 5: COLOR PICKERS EN GRID 2X2 -->
|
||||
<div class="row g-2 mb-2">
|
||||
<div class="col-6">
|
||||
<label for="topBarBackgroundColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-paint-bucket me-1" style="color: #FF8600;"></i>
|
||||
Color de fondo
|
||||
</label>
|
||||
<input type="color" id="topBarBackgroundColor" class="form-control form-control-color w-100" value="#0E2337" title="Color de fondo">
|
||||
<small class="text-muted d-block mt-1" id="topBarBackgroundColorValue">#0E2337</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarTextColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-fonts me-1" style="color: #FF8600;"></i>
|
||||
Color de texto
|
||||
</label>
|
||||
<input type="color" id="topBarTextColor" class="form-control form-control-color w-100" value="#FFFFFF" title="Color de texto">
|
||||
<small class="text-muted d-block mt-1" id="topBarTextColorValue">#FFFFFF</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarLabelColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-tag-fill me-1" style="color: #FF8600;"></i>
|
||||
Color etiqueta
|
||||
</label>
|
||||
<input type="color" id="topBarLabelColor" class="form-control form-control-color w-100" value="#FF8600" title="Color etiqueta">
|
||||
<small class="text-muted d-block mt-1" id="topBarLabelColorValue">#FF8600</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarIconColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-star me-1" style="color: #FF8600;"></i>
|
||||
Color ícono
|
||||
</label>
|
||||
<input type="color" id="topBarIconColor" class="form-control form-control-color w-100" value="#FF8600" title="Color ícono">
|
||||
<small class="text-muted d-block mt-1" id="topBarIconColorValue">#FF8600</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="topBarLinkColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-link me-1" style="color: #FF8600;"></i>
|
||||
Color enlace
|
||||
</label>
|
||||
<input type="color" id="topBarLinkColor" class="form-control form-control-color w-100" value="#FFFFFF" title="Color enlace">
|
||||
<small class="text-muted d-block mt-1" id="topBarLinkColorValue">#FFFFFF</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarLinkHoverColor" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-hand-index me-1" style="color: #FF8600;"></i>
|
||||
Color enlace (hover)
|
||||
</label>
|
||||
<input type="color" id="topBarLinkHoverColor" class="form-control form-control-color w-100" value="#FF8600" title="Color enlace hover">
|
||||
<small class="text-muted d-block mt-1" id="topBarLinkHoverColorValue">#FF8600</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================================
|
||||
GRUPO 4: ESTILOS - TAMAÑOS
|
||||
======================================== -->
|
||||
<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3" style="color: #1e3a5f;">
|
||||
<i class="bi bi-arrows-fullscreen me-2" style="color: #FF8600;"></i>
|
||||
Estilos - Tamaños
|
||||
</h5>
|
||||
|
||||
<div class="row g-2 mb-0">
|
||||
<div class="col-6">
|
||||
<label for="topBarFontSize" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-type me-1" style="color: #FF8600;"></i>
|
||||
Tamaño de fuente
|
||||
</label>
|
||||
<input type="text" id="topBarFontSize" class="form-control form-control-sm" value="0.9rem">
|
||||
<small class="text-muted">Ej: 0.9rem, 14px</small>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label for="topBarPadding" class="form-label small mb-1 fw-semibold" style="color: #495057;">
|
||||
<i class="bi bi-bounding-box me-1" style="color: #FF8600;"></i>
|
||||
Padding vertical
|
||||
</label>
|
||||
<input type="text" id="topBarPadding" class="form-control form-control-sm" value="0.5rem 0">
|
||||
<small class="text-muted">Ej: 0.5rem 0</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /tab-pane -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1
Assets/CriticalCSS/responsive.critical.css
Normal file
1
Assets/CriticalCSS/responsive.critical.css
Normal file
@@ -0,0 +1 @@
|
||||
@media (max-width:575.98px){:root{--bs-gutter-x:1rem}body{font-size:14px}h1{font-size:24px}h2{font-size:20px}h3{font-size:18px}.container-fluid{padding:0 10px}.navbar{padding:0.5rem 0}.navbar-brand{font-size:18px}main{padding:0.5rem}.sidebar{margin-top:2rem}table{font-size:12px;margin-bottom:1rem;overflow-x:auto}.table-responsive{margin-bottom:1rem}.btn{padding:0.375rem 0.75rem;font-size:14px}.btn-lg{padding:0.5rem 1rem;font-size:16px}.card{margin-bottom:1rem}.form-group{margin-bottom:1rem}.form-control{padding:0.375rem 0.75rem;font-size:16px}.modal-dialog{margin:0.5rem}.modal-content{border-radius:4px}img{max-width:100%;height:auto}ul,ol{padding-left:1.5rem}.mt-1,.my-1{margin-top:0.25rem !important}.mb-1,.my-1{margin-bottom:0.25rem !important}.p-1{padding:0.25rem !important}}@media (min-width:576px){body{font-size:14px}h1{font-size:28px}h2{font-size:22px}h3{font-size:18px}}@media (min-width:768px){body{font-size:15px}h1{font-size:32px}h2{font-size:26px}h3{font-size:20px}.row-md-2{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem}.navbar{padding:1rem 0}.main-content{display:grid;grid-template-columns:1fr 300px;gap:2rem}.main-content.no-sidebar{grid-template-columns:1fr}}@media (min-width:992px){body{font-size:16px}h1{font-size:36px}h2{font-size:28px}h3{font-size:22px}.row-lg-3{display:grid;grid-template-columns:repeat(3,1fr);gap:2rem}.main-content{display:grid;grid-template-columns:1fr 300px;gap:2rem}.main-content.with-left-sidebar{grid-template-columns:250px 1fr 300px}.content-wrapper{max-width:1200px;margin:0 auto}}
|
||||
1
Assets/CriticalCSS/variables.critical.css
Normal file
1
Assets/CriticalCSS/variables.critical.css
Normal file
@@ -0,0 +1 @@
|
||||
:root{--color-navy-dark:#0E2337;--color-navy-primary:#1e3a5f;--color-navy-light:#2c5282;--color-blue-primary:#1e3a5f;--color-blue-secondary:#2c5282;--color-blue-light:#1a73e8;--color-cyan-primary:#61c7cd;--color-cyan-dark:#4db8c4;--color-cyan-darker:#4fb3b9;--color-orange-primary:#FF8600;--color-orange-secondary:#FFB800;--color-orange-light:#FFB800;--color-orange-button:#FF6B35;--color-orange-button-end:#FF8C42;--color-orange-hover:#FF6B35;--color-neutral-50:#f8f9fa;--color-neutral-100:#e9ecef;--color-neutral-600:#495057;--color-neutral-700:#6c757d;--color-slate-gray:#4C5C6B;--color-gray-50:#f8f9fa;--color-gray-100:#f7fafc;--color-gray-200:#e9ecef;--color-gray-300:#dee2e6;--color-gray-400:#cbd5e0;--color-gray-500:#a0aec0;--color-gray-600:#6c757d;--color-gray-700:#495057;--color-gray-800:#333;--color-gray-900:#212529;--color-gray-dark:#1a1a1a;--color-white:#ffffff;--color-black:#000000;--font-family-base:'Poppins',sans-serif;--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--font-size-base:1rem;--font-size-sm:0.875rem;--font-size-lg:1.125rem;--font-size-xl:1.25rem;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--line-height-base:1.5;--line-height-tight:1.25;--line-height-loose:1.8;--spacing-xs:0.25rem;--spacing-sm:0.5rem;--spacing-md:1rem;--spacing-lg:1.5rem;--spacing-xl:2rem;--spacing-2xl:3rem;--spacing-3xl:4rem;--border-width:1px;--border-width-thick:2px;--border-width-thicker:3px;--border-width-lateral:4px;--border-radius-sm:4px;--border-radius-md:8px;--border-radius-lg:12px;--border-radius-xl:16px;--border-color-light:var(--color-gray-200);--border-color-default:var(--color-gray-300);--shadow-xs:0 1px 2px rgba(0,0,0,0.05);--shadow-sm:0 2px 4px rgba(0,0,0,0.1);--shadow-md:0 4px 12px rgba(0,0,0,0.15);--shadow-lg:0 8px 24px rgba(0,0,0,0.2);--shadow-xl:0 12px 32px rgba(0,0,0,0.25);--shadow-2xl:0 20px 60px rgba(0,0,0,0.3);--shadow-navbar:0 2px 4px rgba(0,0,0,0.15);--shadow-navbar-scrolled:0 4px 12px rgba(0,0,0,0.25);--shadow-dropdown:0 8px 24px rgba(0,0,0,0.12);--shadow-cta:0 8px 24px rgba(255,133,0,0.3);--shadow-cta-hover:0 12px 32px rgba(255,133,0,0.4);--shadow-button:0 4px 12px rgba(255,107,53,0.3);--shadow-related-posts:0 12px 32px rgba(26,115,232,0.15);--shadow-pagination:0 4px 12px rgba(26,115,232,0.3);--transition-fast:0.15s ease;--transition-base:0.3s ease;--transition-slow:0.5s ease;--transition-cubic:cubic-bezier(0.4,0,0.2,1);--z-dropdown:1000;--z-sticky:1020;--z-navbar:1030;--z-modal-backdrop:1040;--z-modal:1050;--z-popover:1060;--z-tooltip:1070;--gradient-hero:linear-gradient(135deg,var(--color-blue-primary) 0%,var(--color-blue-secondary) 100%);--gradient-cta:linear-gradient(135deg,var(--color-orange-primary) 0%,var(--color-orange-secondary) 100%);--gradient-button-lets-talk:linear-gradient(135deg,var(--color-orange-button) 0%,var(--color-orange-button-end) 100%);--gradient-pagination:linear-gradient(135deg,var(--color-blue-primary) 0%,var(--color-blue-secondary) 100%);--gradient-underline:linear-gradient(90deg,var(--color-cyan-primary) 0%,var(--color-cyan-dark) 100%);--gradient-border-related:linear-gradient(180deg,var(--color-blue-primary) 0%,var(--color-blue-light) 100%);--opacity-disabled:0.5;--opacity-hover:0.8;--opacity-backdrop:0.5;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--breakpoint-xxl:1400px}
|
||||
821
Assets/Css/critical-bootstrap.css
Normal file
821
Assets/Css/critical-bootstrap.css
Normal file
@@ -0,0 +1,821 @@
|
||||
/**
|
||||
* Critical Bootstrap CSS Subset (TIPO 2)
|
||||
*
|
||||
* Contiene SOLO clases de Bootstrap 5.3.2 usadas en componentes above-the-fold.
|
||||
* NO contiene CSS personalizado (ese va en critical-custom-temp.css - TIPO 3).
|
||||
*
|
||||
* Componentes Bootstrap incluidos:
|
||||
* - Fonts (@font-face Poppins)
|
||||
* - Variables CSS (:root)
|
||||
* - Resets (box-sizing, body)
|
||||
* - Container system
|
||||
* - Grid system (row, col-*)
|
||||
* - Flexbox utilities (d-flex, justify-content-*, align-items-*)
|
||||
* - Spacing utilities (m-*, p-*)
|
||||
* - Text utilities
|
||||
* - Navbar component
|
||||
* - Collapse/Dropdown components
|
||||
* - Button component
|
||||
* - Alert component
|
||||
* - Typography base (h1-h6, p)
|
||||
* - Responsive breakpoints
|
||||
*
|
||||
* Hook: wp_head priority 0
|
||||
* Output: <style id="roi-critical-bootstrap">
|
||||
*
|
||||
* @version 5.3.2-subset
|
||||
* @see Inc/enqueue-scripts.php - Bootstrap diferido
|
||||
* @see Shared/Infrastructure/Services/CriticalBootstrapService.php
|
||||
* @see Assets/Css/critical-custom-temp.css - CSS personalizado (TIPO 3)
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
CRITICAL FONTS (Poppins - LCP optimization)
|
||||
|
||||
font-display: swap + preload = fuente carga rapido y siempre se muestra
|
||||
size-adjust: 100.6% = fallback casi identico a Poppins (minimiza CLS)
|
||||
========================================================================== */
|
||||
@font-face {
|
||||
font-family: 'Poppins Fallback';
|
||||
src: local('Arial'), local('Helvetica Neue'), local('sans-serif');
|
||||
size-adjust: 106%;
|
||||
ascent-override: 105%;
|
||||
descent-override: 35%;
|
||||
line-gap-override: 10%;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('/wp-content/themes/roi-theme/Assets/Fonts/poppins-v24-latin-regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('/wp-content/themes/roi-theme/Assets/Fonts/poppins-v24-latin-600.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('/wp-content/themes/roi-theme/Assets/Fonts/poppins-v24-latin-700.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Fonts */
|
||||
--font-primary: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||
--bs-body-font-family: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||
|
||||
/* Theme Colors (críticos para above-the-fold) */
|
||||
--color-navy-dark: #0E2337;
|
||||
--color-navy-medium: #1e3a5f;
|
||||
--color-orange-primary: #FF8600;
|
||||
--color-orange-hover: #e67a00;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-white: #fff;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
|
||||
/* Spacing */
|
||||
--bs-gutter-x: 1.5rem;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
BOX SIZING & RESETS (Bootstrap Reboot crítico)
|
||||
========================================================================== */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif);
|
||||
font-size: var(--bs-body-font-size, 1rem);
|
||||
font-weight: var(--bs-body-font-weight, 400);
|
||||
line-height: var(--bs-body-line-height, 1.5);
|
||||
color: var(--bs-body-color, #212529);
|
||||
background-color: var(--bs-body-bg, #fff);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--bs-link-color, #0d6efd);
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: var(--bs-link-hover-color, #0a58ca);
|
||||
}
|
||||
|
||||
img, svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
CONTAINER (Layout crítico)
|
||||
========================================================================== */
|
||||
.container,
|
||||
.container-fluid {
|
||||
--bs-gutter-x: 1.5rem;
|
||||
--bs-gutter-y: 0;
|
||||
width: 100%;
|
||||
padding-right: calc(var(--bs-gutter-x) * 0.5);
|
||||
padding-left: calc(var(--bs-gutter-x) * 0.5);
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.container { max-width: 540px; }
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.container { max-width: 720px; }
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.container { max-width: 960px; }
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container { max-width: 1140px; }
|
||||
}
|
||||
@media (min-width: 1400px) {
|
||||
.container { max-width: 1320px; }
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
GRID SYSTEM (Layout crítico - Previene CLS)
|
||||
========================================================================== */
|
||||
.row {
|
||||
--bs-gutter-x: 1.5rem;
|
||||
--bs-gutter-y: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: calc(-1 * var(--bs-gutter-y));
|
||||
margin-right: calc(-0.5 * var(--bs-gutter-x));
|
||||
margin-left: calc(-0.5 * var(--bs-gutter-x));
|
||||
}
|
||||
.row > * {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding-right: calc(var(--bs-gutter-x) * 0.5);
|
||||
padding-left: calc(var(--bs-gutter-x) * 0.5);
|
||||
margin-top: var(--bs-gutter-y);
|
||||
}
|
||||
|
||||
.col { flex: 1 0 0%; }
|
||||
.col-auto { flex: 0 0 auto; width: auto; }
|
||||
.col-1 { flex: 0 0 auto; width: 8.33333333%; }
|
||||
.col-2 { flex: 0 0 auto; width: 16.66666667%; }
|
||||
.col-3 { flex: 0 0 auto; width: 25%; }
|
||||
.col-4 { flex: 0 0 auto; width: 33.33333333%; }
|
||||
.col-5 { flex: 0 0 auto; width: 41.66666667%; }
|
||||
.col-6 { flex: 0 0 auto; width: 50%; }
|
||||
.col-7 { flex: 0 0 auto; width: 58.33333333%; }
|
||||
.col-8 { flex: 0 0 auto; width: 66.66666667%; }
|
||||
.col-9 { flex: 0 0 auto; width: 75%; }
|
||||
.col-10 { flex: 0 0 auto; width: 83.33333333%; }
|
||||
.col-11 { flex: 0 0 auto; width: 91.66666667%; }
|
||||
.col-12 { flex: 0 0 auto; width: 100%; }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.col-md-1 { flex: 0 0 auto; width: 8.33333333%; }
|
||||
.col-md-2 { flex: 0 0 auto; width: 16.66666667%; }
|
||||
.col-md-3 { flex: 0 0 auto; width: 25%; }
|
||||
.col-md-4 { flex: 0 0 auto; width: 33.33333333%; }
|
||||
.col-md-5 { flex: 0 0 auto; width: 41.66666667%; }
|
||||
.col-md-6 { flex: 0 0 auto; width: 50%; }
|
||||
.col-md-7 { flex: 0 0 auto; width: 58.33333333%; }
|
||||
.col-md-8 { flex: 0 0 auto; width: 66.66666667%; }
|
||||
.col-md-9 { flex: 0 0 auto; width: 75%; }
|
||||
.col-md-10 { flex: 0 0 auto; width: 83.33333333%; }
|
||||
.col-md-11 { flex: 0 0 auto; width: 91.66666667%; }
|
||||
.col-md-12 { flex: 0 0 auto; width: 100%; }
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.col-lg-1 { flex: 0 0 auto; width: 8.33333333%; }
|
||||
.col-lg-2 { flex: 0 0 auto; width: 16.66666667%; }
|
||||
.col-lg-3 { flex: 0 0 auto; width: 25%; }
|
||||
.col-lg-4 { flex: 0 0 auto; width: 33.33333333%; }
|
||||
.col-lg-5 { flex: 0 0 auto; width: 41.66666667%; }
|
||||
.col-lg-6 { flex: 0 0 auto; width: 50%; }
|
||||
.col-lg-7 { flex: 0 0 auto; width: 58.33333333%; }
|
||||
.col-lg-8 { flex: 0 0 auto; width: 66.66666667%; }
|
||||
.col-lg-9 { flex: 0 0 auto; width: 75%; }
|
||||
.col-lg-10 { flex: 0 0 auto; width: 83.33333333%; }
|
||||
.col-lg-11 { flex: 0 0 auto; width: 91.66666667%; }
|
||||
.col-lg-12 { flex: 0 0 auto; width: 100%; }
|
||||
}
|
||||
|
||||
/* Gutter utilities */
|
||||
.g-0, .gx-0 { --bs-gutter-x: 0; }
|
||||
.g-0, .gy-0 { --bs-gutter-y: 0; }
|
||||
.g-3, .gx-3 { --bs-gutter-x: 1rem; }
|
||||
.g-3, .gy-3 { --bs-gutter-y: 1rem; }
|
||||
|
||||
/* ==========================================================================
|
||||
FLEXBOX UTILITIES (Layout crítico)
|
||||
========================================================================== */
|
||||
.d-flex {
|
||||
display: flex !important;
|
||||
}
|
||||
.d-none {
|
||||
display: none !important;
|
||||
}
|
||||
.d-block {
|
||||
display: block !important;
|
||||
}
|
||||
.d-inline-block {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
/* Responsive Display Utilities - Previene CLS en TopNotificationBar */
|
||||
@media (min-width: 992px) {
|
||||
.d-lg-none {
|
||||
display: none !important;
|
||||
}
|
||||
.d-lg-block {
|
||||
display: block !important;
|
||||
}
|
||||
.d-lg-flex {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap !important;
|
||||
}
|
||||
.flex-column {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.justify-content-center {
|
||||
justify-content: center !important;
|
||||
}
|
||||
.justify-content-between {
|
||||
justify-content: space-between !important;
|
||||
}
|
||||
.justify-content-start {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
.justify-content-end {
|
||||
justify-content: flex-end !important;
|
||||
}
|
||||
|
||||
.align-items-center {
|
||||
align-items: center !important;
|
||||
}
|
||||
.align-items-start {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
.align-items-end {
|
||||
align-items: flex-end !important;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
.gap-3 {
|
||||
gap: 1rem !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
SPACING UTILITIES (Margin/Padding críticos)
|
||||
========================================================================== */
|
||||
.m-0 { margin: 0 !important; }
|
||||
.m-auto { margin: auto !important; }
|
||||
|
||||
.mb-0 { margin-bottom: 0 !important; }
|
||||
.mb-1 { margin-bottom: 0.25rem !important; }
|
||||
.mb-2 { margin-bottom: 0.5rem !important; }
|
||||
.mb-3 { margin-bottom: 1rem !important; }
|
||||
.mb-4 { margin-bottom: 1.5rem !important; }
|
||||
|
||||
.mt-0 { margin-top: 0 !important; }
|
||||
.mt-2 { margin-top: 0.5rem !important; }
|
||||
.mt-3 { margin-top: 1rem !important; }
|
||||
|
||||
.me-1 { margin-right: 0.25rem !important; }
|
||||
.me-2 { margin-right: 0.5rem !important; }
|
||||
.me-3 { margin-right: 1rem !important; }
|
||||
|
||||
.ms-2 { margin-left: 0.5rem !important; }
|
||||
.ms-3 { margin-left: 1rem !important; }
|
||||
|
||||
.mx-auto { margin-left: auto !important; margin-right: auto !important; }
|
||||
|
||||
.p-0 { padding: 0 !important; }
|
||||
.p-2 { padding: 0.5rem !important; }
|
||||
.p-3 { padding: 1rem !important; }
|
||||
|
||||
.py-2 { padding-top: 0.5rem !important; padding-bottom: 0.5rem !important; }
|
||||
.py-3 { padding-top: 1rem !important; padding-bottom: 1rem !important; }
|
||||
.py-4 { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; }
|
||||
|
||||
.px-2 { padding-left: 0.5rem !important; padding-right: 0.5rem !important; }
|
||||
.px-3 { padding-left: 1rem !important; padding-right: 1rem !important; }
|
||||
.px-4 { padding-left: 1.5rem !important; padding-right: 1.5rem !important; }
|
||||
|
||||
/* ==========================================================================
|
||||
SIZING UTILITIES (Width/Height críticos)
|
||||
========================================================================== */
|
||||
.w-100 { width: 100% !important; }
|
||||
.w-auto { width: auto !important; }
|
||||
.h-100 { height: 100% !important; }
|
||||
.h-auto { height: auto !important; }
|
||||
|
||||
/* ==========================================================================
|
||||
TEXT UTILITIES (Críticos para layout)
|
||||
========================================================================== */
|
||||
.text-center { text-align: center !important; }
|
||||
.text-start { text-align: left !important; }
|
||||
.text-end { text-align: right !important; }
|
||||
.text-white { color: #fff !important; }
|
||||
.text-muted { color: var(--bs-secondary-color, #6c757d) !important; }
|
||||
|
||||
.fw-normal { font-weight: 400 !important; }
|
||||
.fw-medium { font-weight: 500 !important; }
|
||||
.fw-semibold { font-weight: 600 !important; }
|
||||
.fw-bold { font-weight: 700 !important; }
|
||||
|
||||
.fs-5 { font-size: 1.25rem !important; }
|
||||
.fs-6 { font-size: 1rem !important; }
|
||||
|
||||
.small { font-size: 0.875em !important; }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.text-md-start { text-align: left !important; }
|
||||
.text-md-center { text-align: center !important; }
|
||||
.text-md-end { text-align: right !important; }
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
NAVBAR COMPONENT (Crítico - Above the fold)
|
||||
========================================================================== */
|
||||
.navbar {
|
||||
--bs-navbar-padding-x: 0;
|
||||
--bs-navbar-padding-y: 0.5rem;
|
||||
--bs-navbar-color: rgba(255, 255, 255, 0.55);
|
||||
--bs-navbar-hover-color: rgba(255, 255, 255, 0.75);
|
||||
--bs-navbar-disabled-color: rgba(255, 255, 255, 0.25);
|
||||
--bs-navbar-active-color: #fff;
|
||||
--bs-navbar-brand-padding-y: 0.3125rem;
|
||||
--bs-navbar-brand-margin-end: 1rem;
|
||||
--bs-navbar-brand-font-size: 1.25rem;
|
||||
--bs-navbar-brand-color: #fff;
|
||||
--bs-navbar-brand-hover-color: #fff;
|
||||
--bs-navbar-nav-link-padding-x: 0.5rem;
|
||||
--bs-navbar-toggler-padding-y: 0.25rem;
|
||||
--bs-navbar-toggler-padding-x: 0.75rem;
|
||||
--bs-navbar-toggler-font-size: 1.25rem;
|
||||
--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||
--bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1);
|
||||
--bs-navbar-toggler-border-radius: var(--bs-border-radius, 0.375rem);
|
||||
--bs-navbar-toggler-focus-width: 0.25rem;
|
||||
--bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;
|
||||
/* position: controlado por CriticalCSSService según sticky_enabled */
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--bs-navbar-padding-y) var(--bs-navbar-padding-x);
|
||||
}
|
||||
|
||||
.navbar > .container,
|
||||
.navbar > .container-fluid {
|
||||
display: flex;
|
||||
flex-wrap: inherit;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
padding-top: var(--bs-navbar-brand-padding-y);
|
||||
padding-bottom: var(--bs-navbar-brand-padding-y);
|
||||
margin-right: var(--bs-navbar-brand-margin-end);
|
||||
font-size: var(--bs-navbar-brand-font-size);
|
||||
color: var(--bs-navbar-brand-color);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.navbar-brand:hover,
|
||||
.navbar-brand:focus {
|
||||
color: var(--bs-navbar-brand-hover-color);
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
--bs-nav-link-padding-x: 0;
|
||||
--bs-nav-link-padding-y: 0.5rem;
|
||||
--bs-nav-link-color: var(--bs-navbar-color);
|
||||
--bs-nav-link-hover-color: var(--bs-navbar-hover-color);
|
||||
--bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: 0;
|
||||
margin-bottom: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
color: var(--bs-nav-link-color);
|
||||
}
|
||||
.navbar-nav .nav-link:hover,
|
||||
.navbar-nav .nav-link:focus {
|
||||
color: var(--bs-nav-link-hover-color);
|
||||
}
|
||||
.navbar-nav .nav-link.active {
|
||||
color: var(--bs-navbar-active-color);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: block;
|
||||
padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);
|
||||
font-size: var(--bs-nav-link-font-size);
|
||||
font-weight: var(--bs-nav-link-font-weight);
|
||||
color: var(--bs-nav-link-color);
|
||||
text-decoration: none;
|
||||
background: 0 0;
|
||||
border: 0;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.navbar-toggler {
|
||||
padding: var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);
|
||||
font-size: var(--bs-navbar-toggler-font-size);
|
||||
line-height: 1;
|
||||
color: var(--bs-navbar-color);
|
||||
background-color: transparent;
|
||||
border: var(--bs-border-width, 1px) solid var(--bs-navbar-toggler-border-color);
|
||||
border-radius: var(--bs-navbar-toggler-border-radius);
|
||||
transition: var(--bs-navbar-toggler-transition);
|
||||
}
|
||||
.navbar-toggler:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.navbar-toggler:focus {
|
||||
text-decoration: none;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 var(--bs-navbar-toggler-focus-width);
|
||||
}
|
||||
|
||||
.navbar-toggler-icon {
|
||||
display: inline-block;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
vertical-align: middle;
|
||||
background-image: var(--bs-navbar-toggler-icon-bg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
.navbar-dark,
|
||||
.navbar[data-bs-theme="dark"] {
|
||||
--bs-navbar-color: rgba(255, 255, 255, 0.55);
|
||||
--bs-navbar-hover-color: rgba(255, 255, 255, 0.75);
|
||||
--bs-navbar-disabled-color: rgba(255, 255, 255, 0.25);
|
||||
--bs-navbar-active-color: #fff;
|
||||
--bs-navbar-brand-color: #fff;
|
||||
--bs-navbar-brand-hover-color: #fff;
|
||||
--bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1);
|
||||
--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
COLLAPSE COMPONENT (Navbar mobile)
|
||||
========================================================================== */
|
||||
.collapse:not(.show) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
flex-basis: 100%;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
DROPDOWN COMPONENT (Navbar submenus)
|
||||
========================================================================== */
|
||||
.dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dropdown-toggle::after {
|
||||
display: inline-block;
|
||||
margin-left: 0.255em;
|
||||
vertical-align: 0.255em;
|
||||
content: "";
|
||||
border-top: 0.3em solid;
|
||||
border-right: 0.3em solid transparent;
|
||||
border-bottom: 0;
|
||||
border-left: 0.3em solid transparent;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
--bs-dropdown-zindex: 1000;
|
||||
--bs-dropdown-min-width: 10rem;
|
||||
--bs-dropdown-padding-x: 0;
|
||||
--bs-dropdown-padding-y: 0.5rem;
|
||||
--bs-dropdown-spacer: 0.125rem;
|
||||
--bs-dropdown-font-size: 1rem;
|
||||
--bs-dropdown-color: var(--bs-body-color, #212529);
|
||||
--bs-dropdown-bg: var(--bs-body-bg, #fff);
|
||||
--bs-dropdown-border-color: var(--bs-border-color-translucent, rgba(0,0,0,.175));
|
||||
--bs-dropdown-border-radius: var(--bs-border-radius, 0.375rem);
|
||||
--bs-dropdown-border-width: var(--bs-border-width, 1px);
|
||||
--bs-dropdown-inner-border-radius: calc(var(--bs-border-radius, 0.375rem) - var(--bs-border-width, 1px));
|
||||
--bs-dropdown-divider-bg: var(--bs-border-color-translucent, rgba(0,0,0,.175));
|
||||
--bs-dropdown-divider-margin-y: 0.5rem;
|
||||
--bs-dropdown-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-dropdown-link-color: var(--bs-body-color, #212529);
|
||||
--bs-dropdown-link-hover-color: var(--bs-body-color, #212529);
|
||||
--bs-dropdown-link-hover-bg: var(--bs-tertiary-bg, #f8f9fa);
|
||||
--bs-dropdown-link-active-color: #fff;
|
||||
--bs-dropdown-link-active-bg: #0d6efd;
|
||||
--bs-dropdown-link-disabled-color: var(--bs-tertiary-color, #adb5bd);
|
||||
--bs-dropdown-item-padding-x: 1rem;
|
||||
--bs-dropdown-item-padding-y: 0.25rem;
|
||||
--bs-dropdown-header-color: #6c757d;
|
||||
--bs-dropdown-header-padding-x: 1rem;
|
||||
--bs-dropdown-header-padding-y: 0.5rem;
|
||||
position: absolute;
|
||||
z-index: var(--bs-dropdown-zindex);
|
||||
display: none;
|
||||
min-width: var(--bs-dropdown-min-width);
|
||||
padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);
|
||||
margin: 0;
|
||||
font-size: var(--bs-dropdown-font-size);
|
||||
color: var(--bs-dropdown-color);
|
||||
text-align: left;
|
||||
list-style: none;
|
||||
background-color: var(--bs-dropdown-bg);
|
||||
background-clip: padding-box;
|
||||
border: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);
|
||||
border-radius: var(--bs-dropdown-border-radius);
|
||||
}
|
||||
|
||||
.dropdown-menu.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);
|
||||
clear: both;
|
||||
font-weight: 400;
|
||||
color: var(--bs-dropdown-link-color);
|
||||
text-align: inherit;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
.dropdown-item:hover,
|
||||
.dropdown-item:focus {
|
||||
color: var(--bs-dropdown-link-hover-color);
|
||||
background-color: var(--bs-dropdown-link-hover-bg);
|
||||
}
|
||||
.dropdown-item.active,
|
||||
.dropdown-item:active {
|
||||
color: var(--bs-dropdown-link-active-color);
|
||||
text-decoration: none;
|
||||
background-color: var(--bs-dropdown-link-active-bg);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
TEXT UTILITIES
|
||||
========================================================================== */
|
||||
.text-decoration-underline {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
.text-decoration-none {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
IMAGE UTILITIES
|
||||
========================================================================== */
|
||||
.img-fluid {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
ALERT COMPONENT (Above-the-fold notifications)
|
||||
========================================================================== */
|
||||
.alert {
|
||||
--bs-alert-padding-x: 1rem;
|
||||
--bs-alert-padding-y: 1rem;
|
||||
--bs-alert-margin-bottom: 1rem;
|
||||
--bs-alert-border-radius: 0.375rem;
|
||||
position: relative;
|
||||
padding: var(--bs-alert-padding-y) var(--bs-alert-padding-x);
|
||||
margin-bottom: var(--bs-alert-margin-bottom);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--bs-alert-border-radius);
|
||||
}
|
||||
.alert-warning {
|
||||
--bs-alert-color: #664d03;
|
||||
--bs-alert-bg: #fff3cd;
|
||||
--bs-alert-border-color: #ffecb5;
|
||||
color: var(--bs-alert-color);
|
||||
background-color: var(--bs-alert-bg);
|
||||
border-color: var(--bs-alert-border-color);
|
||||
}
|
||||
.alert-info {
|
||||
--bs-alert-color: #055160;
|
||||
--bs-alert-bg: #cff4fc;
|
||||
--bs-alert-border-color: #b6effb;
|
||||
color: var(--bs-alert-color);
|
||||
background-color: var(--bs-alert-bg);
|
||||
border-color: var(--bs-alert-border-color);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
BUTTON COMPONENT (Above-the-fold - Navbar CTA)
|
||||
========================================================================== */
|
||||
.btn {
|
||||
--bs-btn-padding-x: 0.75rem;
|
||||
--bs-btn-padding-y: 0.375rem;
|
||||
--bs-btn-font-size: 1rem;
|
||||
--bs-btn-font-weight: 400;
|
||||
--bs-btn-line-height: 1.5;
|
||||
--bs-btn-color: var(--bs-body-color);
|
||||
--bs-btn-bg: transparent;
|
||||
--bs-btn-border-width: var(--bs-border-width, 1px);
|
||||
--bs-btn-border-color: transparent;
|
||||
--bs-btn-border-radius: var(--bs-border-radius, 0.375rem);
|
||||
--bs-btn-hover-border-color: transparent;
|
||||
--bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
--bs-btn-disabled-opacity: 0.65;
|
||||
--bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), 0.5);
|
||||
display: inline-block;
|
||||
padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x);
|
||||
font-family: var(--bs-btn-font-family);
|
||||
font-size: var(--bs-btn-font-size);
|
||||
font-weight: var(--bs-btn-font-weight);
|
||||
line-height: var(--bs-btn-line-height);
|
||||
color: var(--bs-btn-color);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border: var(--bs-btn-border-width) solid var(--bs-btn-border-color);
|
||||
border-radius: var(--bs-btn-border-radius);
|
||||
background-color: var(--bs-btn-bg);
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
.btn:hover {
|
||||
color: var(--bs-btn-hover-color);
|
||||
background-color: var(--bs-btn-hover-bg);
|
||||
border-color: var(--bs-btn-hover-border-color);
|
||||
}
|
||||
.btn:focus-visible {
|
||||
color: var(--bs-btn-hover-color);
|
||||
background-color: var(--bs-btn-hover-bg);
|
||||
border-color: var(--bs-btn-hover-border-color);
|
||||
outline: 0;
|
||||
box-shadow: var(--bs-btn-focus-box-shadow);
|
||||
}
|
||||
.btn:disabled, .btn.disabled {
|
||||
pointer-events: none;
|
||||
opacity: var(--bs-btn-disabled-opacity);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
BUTTON CLOSE (Dismiss notification)
|
||||
========================================================================== */
|
||||
.btn-close {
|
||||
--bs-btn-close-color: #000;
|
||||
--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");
|
||||
--bs-btn-close-opacity: 0.5;
|
||||
--bs-btn-close-hover-opacity: 0.75;
|
||||
--bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
--bs-btn-close-focus-opacity: 1;
|
||||
--bs-btn-close-disabled-opacity: 0.25;
|
||||
--bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);
|
||||
box-sizing: content-box;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
padding: 0.25em 0.25em;
|
||||
color: var(--bs-btn-close-color);
|
||||
background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat;
|
||||
border: 0;
|
||||
border-radius: 0.375rem;
|
||||
opacity: var(--bs-btn-close-opacity);
|
||||
}
|
||||
.btn-close:hover {
|
||||
color: var(--bs-btn-close-color);
|
||||
text-decoration: none;
|
||||
opacity: var(--bs-btn-close-hover-opacity);
|
||||
}
|
||||
.btn-close:focus {
|
||||
outline: 0;
|
||||
box-shadow: var(--bs-btn-close-focus-shadow);
|
||||
opacity: var(--bs-btn-close-focus-opacity);
|
||||
}
|
||||
|
||||
.btn-close-white {
|
||||
filter: var(--bs-btn-close-white-filter);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
RESPONSIVE BREAKPOINTS (navbar-expand-lg)
|
||||
========================================================================== */
|
||||
@media (min-width: 992px) {
|
||||
.navbar-expand-lg {
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.navbar-expand-lg .navbar-nav {
|
||||
flex-direction: row;
|
||||
}
|
||||
.navbar-expand-lg .navbar-nav .dropdown-menu {
|
||||
position: absolute;
|
||||
}
|
||||
.navbar-expand-lg .navbar-nav .nav-link {
|
||||
padding-right: var(--bs-navbar-nav-link-padding-x);
|
||||
padding-left: var(--bs-navbar-nav-link-padding-x);
|
||||
}
|
||||
.navbar-expand-lg .navbar-collapse {
|
||||
display: flex !important;
|
||||
flex-basis: auto;
|
||||
}
|
||||
.navbar-expand-lg .navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.d-lg-block { display: block !important; }
|
||||
.d-lg-none { display: none !important; }
|
||||
.mb-lg-0 { margin-bottom: 0 !important; }
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.navbar-expand-lg > .container,
|
||||
.navbar-expand-lg > .container-fluid {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
RESPONSIVE DISPLAY UTILITIES (md breakpoint)
|
||||
========================================================================== */
|
||||
@media (min-width: 768px) {
|
||||
.d-md-block { display: block !important; }
|
||||
.d-md-none { display: none !important; }
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
TYPOGRAPHY BASE (Critical)
|
||||
========================================================================== */
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
h1 { font-size: calc(1.375rem + 1.5vw); }
|
||||
h2 { font-size: calc(1.325rem + 0.9vw); }
|
||||
h3 { font-size: calc(1.3rem + 0.6vw); }
|
||||
h4 { font-size: calc(1.275rem + 0.3vw); }
|
||||
h5 { font-size: 1.25rem; }
|
||||
h6 { font-size: 1rem; }
|
||||
@media (min-width: 1200px) {
|
||||
h1 { font-size: 2.5rem; }
|
||||
h2 { font-size: 2rem; }
|
||||
h3 { font-size: 1.75rem; }
|
||||
h4 { font-size: 1.5rem; }
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* screen reader utilities, and minimum touch targets.
|
||||
* Compliant with WCAG 2.1 Level AA standards.
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -662,7 +662,7 @@ select:valid {
|
||||
*/
|
||||
|
||||
/* Links del TOC con focus visible */
|
||||
.apus-toc a:focus,
|
||||
.roi-toc a:focus,
|
||||
.toc-link:focus {
|
||||
outline: 3px solid #0066cc;
|
||||
outline-offset: 2px;
|
||||
@@ -671,7 +671,7 @@ select:valid {
|
||||
}
|
||||
|
||||
/* Item activo del TOC */
|
||||
.apus-toc a.active,
|
||||
.roi-toc a.active,
|
||||
.toc-link.active {
|
||||
font-weight: bold;
|
||||
border-left: 4px solid #0066cc;
|
||||
@@ -679,11 +679,11 @@ select:valid {
|
||||
}
|
||||
|
||||
/* Botón toggle del TOC con ARIA */
|
||||
.apus-toc-toggle[aria-expanded="true"]::before {
|
||||
.roi-toc-toggle[aria-expanded="true"]::before {
|
||||
content: "▼ ";
|
||||
}
|
||||
|
||||
.apus-toc-toggle[aria-expanded="false"]::before {
|
||||
.roi-toc-toggle[aria-expanded="false"]::before {
|
||||
content: "▶ ";
|
||||
}
|
||||
|
||||
1
Assets/Css/css-global-accessibility.min.css
vendored
Normal file
1
Assets/Css/css-global-accessibility.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
||||
* Animation Styles
|
||||
*
|
||||
* CSS animations and keyframes for the theme
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* NOTA: Todos los estilos de badges están en style.css según template original.
|
||||
* Este archivo se mantiene vacío para evitar duplicaciones.
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Sistema de Tipografías - APUS Theme
|
||||
* Sistema de Tipografías - ROI Theme
|
||||
*
|
||||
* RESPONSABILIDAD: SOLO definición de fuentes y variables tipográficas
|
||||
* - Declaraciones @font-face (comentadas - usar Google Fonts)
|
||||
@@ -11,7 +11,7 @@
|
||||
* - Estilos de elementos HTML (van en style.css)
|
||||
* - Variables de colores o espaciados (van en variables.css)
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -24,11 +24,12 @@
|
||||
--font-system: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
|
||||
|
||||
/* Fuente primaria - Poppins según template y documentación */
|
||||
--font-primary: 'Poppins', sans-serif;
|
||||
/* Fuente primaria - Poppins con fallback ajustado (Fase 4.3 PageSpeed)
|
||||
'Poppins Fallback' tiene size-adjust para reducir CLS durante font swap */
|
||||
--font-primary: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||
|
||||
/* Fuente para encabezados - Poppins según template */
|
||||
--font-headings: 'Poppins', sans-serif;
|
||||
/* Fuente para encabezados - Poppins con fallback ajustado */
|
||||
--font-headings: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||
|
||||
/* Fuente para código (monospace) */
|
||||
--font-mono: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono',
|
||||
@@ -45,26 +46,41 @@
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
POPPINS (Opcional - Solo si se activa)
|
||||
POPPINS (Self-hosted)
|
||||
============================================
|
||||
|
||||
Las siguientes declaraciones @font-face solo
|
||||
se cargan cuando el usuario activa "Use Custom Fonts"
|
||||
en Apariencia > Personalizar > Tipografía.
|
||||
Fuentes Poppins alojadas localmente para:
|
||||
- Eliminar dependencia de Google Fonts
|
||||
- Mejorar rendimiento (sin requests externos)
|
||||
- Cumplimiento GDPR (sin tracking de Google)
|
||||
|
||||
Para activar Poppins:
|
||||
1. Descargar archivos WOFF2 de Google Fonts
|
||||
2. Colocar en assets/fonts/poppins/
|
||||
3. Descomentar las declaraciones @font-face
|
||||
4. Activar en Customizer
|
||||
Pesos incluidos: 400, 500, 600, 700
|
||||
Formato: WOFF2 (mejor compresión)
|
||||
|
||||
Fase 4.3 PageSpeed: Fallback con size-adjust para reducir CLS
|
||||
- size-adjust: 100.6% ajustado para coincidir mejor con Poppins
|
||||
- font-display: swap + preload = carga rapida sin salto visual
|
||||
- Preload en CriticalCSSInjector P:-2 acelera descarga de fuentes
|
||||
|
||||
NOTA: El valor 100.6% fue calibrado empiricamente.
|
||||
- 106% causaba un salto visual notable (navbar se "achicaba")
|
||||
- 100.6% minimiza el CLS manteniendo legibilidad del fallback
|
||||
|
||||
============================================ */
|
||||
|
||||
/*
|
||||
/* Fallback font con metricas ajustadas para Poppins */
|
||||
@font-face {
|
||||
font-family: 'Poppins Fallback';
|
||||
src: local('Arial'), local('Helvetica Neue'), local('Helvetica'), local('sans-serif');
|
||||
size-adjust: 106%;
|
||||
ascent-override: 105%;
|
||||
descent-override: 35%;
|
||||
line-gap-override: 10%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../fonts/poppins/Poppins-Regular.woff2') format('woff2'),
|
||||
url('../fonts/poppins/Poppins-Regular.woff') format('woff');
|
||||
src: url('../Fonts/poppins-v24-latin-regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -72,8 +88,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../fonts/poppins/Poppins-Medium.woff2') format('woff2'),
|
||||
url('../fonts/poppins/Poppins-Medium.woff') format('woff');
|
||||
src: url('../Fonts/poppins-v24-latin-500.woff2') format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -81,8 +96,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../fonts/poppins/Poppins-SemiBold.woff2') format('woff2'),
|
||||
url('../fonts/poppins/Poppins-SemiBold.woff') format('woff');
|
||||
src: url('../Fonts/poppins-v24-latin-600.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -90,21 +104,11 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../fonts/poppins/Poppins-Bold.woff2') format('woff2'),
|
||||
url('../fonts/poppins/Poppins-Bold.woff') format('woff');
|
||||
src: url('../Fonts/poppins-v24-latin-700.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
*/
|
||||
|
||||
/* Cuando Poppins esté activo, se aplica con clase .use-custom-fonts */
|
||||
/*
|
||||
.use-custom-fonts {
|
||||
--font-primary: 'Poppins', var(--font-system);
|
||||
--font-headings: 'Poppins', var(--font-system);
|
||||
}
|
||||
*/
|
||||
|
||||
/* ============================================
|
||||
UTILIDADES DE FUENTES
|
||||
@@ -4,7 +4,7 @@
|
||||
* Estilos para tablas genéricas en post-content (NO tablas APU)
|
||||
* Aplica 10 estilos diferentes automáticamente a las primeras 11 tablas
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
BASE STYLES - Todas las tablas genéricas
|
||||
======================================== */
|
||||
|
||||
.post-content table:not(.analisis table) {
|
||||
.post-content table:not(.analisis table):not(.desglose table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 2rem auto;
|
||||
@@ -23,9 +23,9 @@
|
||||
}
|
||||
|
||||
/* Header styles - VERY OBVIOUS */
|
||||
.post-content table:not(.analisis table) thead tr:first-child th,
|
||||
.post-content table:not(.analisis table) tbody tr:first-child td,
|
||||
.post-content table:not(.analisis table) tr:first-child td {
|
||||
.post-content table:not(.analisis table):not(.desglose table) thead tr:first-child th,
|
||||
.post-content table:not(.analisis table):not(.desglose table) tbody tr:first-child td,
|
||||
.post-content table:not(.analisis table):not(.desglose table) tr:first-child td {
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
padding: 1.25rem 1rem;
|
||||
@@ -34,7 +34,7 @@
|
||||
}
|
||||
|
||||
/* Body cells */
|
||||
.post-content table:not(.analisis table) tbody tr:not(:first-child) td {
|
||||
.post-content table:not(.analisis table):not(.desglose table) tbody tr:not(:first-child) td {
|
||||
padding: 0.875rem 1rem;
|
||||
border: 1px solid var(--color-neutral-100);
|
||||
text-align: left;
|
||||
@@ -63,13 +63,14 @@
|
||||
|
||||
/* ========================================
|
||||
STYLE 2: Orange Header with Light Background
|
||||
Fase 4.4 Accesibilidad: Texto oscuro para contraste WCAG AA (4.5:1)
|
||||
======================================== */
|
||||
|
||||
.post-content table:not(.analisis table):nth-of-type(3) thead tr:first-child th,
|
||||
.post-content table:not(.analisis table):nth-of-type(3) tbody tr:first-child td,
|
||||
.post-content table:not(.analisis table):nth-of-type(3) tr:first-child td {
|
||||
background: var(--color-orange-primary);
|
||||
color: #ffffff !important;
|
||||
color: var(--color-navy-dark) !important;
|
||||
border: none !important;
|
||||
box-shadow: 0 2px 8px rgba(255, 133, 0, 0.3);
|
||||
}
|
||||
@@ -126,13 +127,14 @@
|
||||
|
||||
/* ========================================
|
||||
STYLE 5: Orange Gradient Header
|
||||
Fase 4.4 Accesibilidad: Texto oscuro para contraste WCAG AA (4.5:1)
|
||||
======================================== */
|
||||
|
||||
.post-content table:not(.analisis table):nth-of-type(6) thead tr:first-child th,
|
||||
.post-content table:not(.analisis table):nth-of-type(6) tbody tr:first-child td,
|
||||
.post-content table:not(.analisis table):nth-of-type(6) tr:first-child td {
|
||||
background: linear-gradient(135deg, var(--color-orange-primary) 0%, var(--color-orange-light) 100%);
|
||||
color: #ffffff !important;
|
||||
color: var(--color-navy-dark) !important;
|
||||
border: none !important;
|
||||
box-shadow: 0 2px 8px rgba(255, 133, 0, 0.35);
|
||||
}
|
||||
@@ -168,13 +170,14 @@
|
||||
|
||||
/* ========================================
|
||||
STYLE 7: Light Orange Background
|
||||
Fase 4.4 Accesibilidad: Texto oscuro para contraste WCAG AA (4.5:1)
|
||||
======================================== */
|
||||
|
||||
.post-content table:not(.analisis table):nth-of-type(8) thead tr:first-child th,
|
||||
.post-content table:not(.analisis table):nth-of-type(8) tbody tr:first-child td,
|
||||
.post-content table:not(.analisis table):nth-of-type(8) tr:first-child td {
|
||||
background: var(--color-orange-primary);
|
||||
color: #ffffff !important;
|
||||
color: var(--color-navy-dark) !important;
|
||||
border: none !important;
|
||||
border-bottom: 3px solid var(--color-navy-primary) !important;
|
||||
}
|
||||
@@ -235,6 +238,7 @@
|
||||
|
||||
/* ========================================
|
||||
STYLE 10: Bold Orange Border
|
||||
Fase 4.4 Accesibilidad: Texto oscuro para contraste WCAG AA (4.5:1)
|
||||
======================================== */
|
||||
|
||||
.post-content table:not(.analisis table):nth-of-type(11) {
|
||||
@@ -245,7 +249,7 @@
|
||||
.post-content table:not(.analisis table):nth-of-type(11) tbody tr:first-child td,
|
||||
.post-content table:not(.analisis table):nth-of-type(11) tr:first-child td {
|
||||
background: linear-gradient(135deg, var(--color-orange-hover) 0%, var(--color-orange-primary) 100%);
|
||||
color: #ffffff !important;
|
||||
color: var(--color-navy-dark) !important;
|
||||
border: none !important;
|
||||
box-shadow: 0 2px 8px rgba(255, 107, 53, 0.4);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* Estilos personalizados para paginación
|
||||
* Template ref: css/style.css líneas 180-207
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -37,9 +37,7 @@
|
||||
color: var(--color-orange-primary);
|
||||
background-color: rgba(255, 133, 0, 0.1);
|
||||
border-color: var(--color-orange-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(255, 133, 0, 0.15);
|
||||
z-index: 2;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.page-link:focus {
|
||||
@@ -53,17 +51,8 @@
|
||||
/* Active page */
|
||||
.page-item.active .page-link {
|
||||
color: #ffffff;
|
||||
background: var(--color-orange-primary);
|
||||
background-color: var(--color-orange-primary);
|
||||
border-color: var(--color-orange-primary);
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4px 12px rgba(255, 133, 0, 0.3);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.page-item.active .page-link:hover {
|
||||
background: var(--color-orange-light);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(255, 133, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
@@ -2,7 +2,7 @@
|
||||
* Print Styles
|
||||
*
|
||||
* Optimized styling for printing
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Responsive Design Styles
|
||||
*
|
||||
* Media queries and responsive adjustments
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -246,31 +246,12 @@
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1140px;
|
||||
}
|
||||
|
||||
.container-lg {
|
||||
max-width: 1280px;
|
||||
}
|
||||
|
||||
.container-xl {
|
||||
max-width: 1400px;
|
||||
}
|
||||
}
|
||||
|
||||
/* XXL devices (1400px and up) */
|
||||
@media (min-width: 1400px) {
|
||||
.container {
|
||||
max-width: 1320px;
|
||||
}
|
||||
|
||||
.container-xl {
|
||||
max-width: 1500px;
|
||||
}
|
||||
|
||||
/* Container width uses CSS variable from Theme Settings */
|
||||
.container,
|
||||
.container-lg,
|
||||
.container-xl,
|
||||
.container-xxl {
|
||||
max-width: 1700px;
|
||||
max-width: var(--roi-container-width, 1320px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* IMPORTANTE: Bootstrap 5 ya provee la mayoría de utilities (display, flex, spacing, etc.)
|
||||
* Este archivo solo contiene utilities adicionales no incluidas en Bootstrap
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -88,3 +88,43 @@
|
||||
.transition-none {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
COMPONENT VISIBILITY FAILSAFE (Plan 99.15)
|
||||
|
||||
CSS failsafe: Oculta wrappers de componentes
|
||||
cuando body tiene clases roi-hide-*
|
||||
|
||||
Estas clases se agregan via BodyClassHooksRegistrar
|
||||
cuando los componentes están deshabilitados/excluidos.
|
||||
======================================== */
|
||||
|
||||
/* Navbar hidden */
|
||||
body.roi-hide-navbar .navbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Table of Contents hidden */
|
||||
body.roi-hide-toc .roi-toc-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* CTA Sidebar hidden */
|
||||
body.roi-hide-cta-sidebar .roi-cta-box {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Generic sidebar hidden */
|
||||
body.roi-hide-sidebar .sidebar-sticky {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* When ALL sidebar components are hidden, expand main column */
|
||||
body.roi-sidebar-empty .col-lg-9 {
|
||||
flex: 0 0 100% !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
body.roi-sidebar-empty .col-lg-3 {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
* - Clases utilitarias (van en utilities.css o style.css)
|
||||
* - Estilos aplicados (SOLO variables en :root)
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Estilos para videos embebidos (YouTube, Vimeo, etc.) en post-content
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
border-spacing: 0;
|
||||
/* CRITICO: table-layout fixed previene CLS
|
||||
El navegador calcula anchos basado en primera fila,
|
||||
no recalcula cuando carga más contenido */
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
/* Eliminar todos los bordes */
|
||||
@@ -153,12 +157,13 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Columna 3: Unidad - centrada */
|
||||
/* Columna 3: Unidad - centrada
|
||||
Fase 4.4 Accesibilidad: Color #495057 (ratio 7.0:1) en lugar de #6c757d */
|
||||
.analisis table td:nth-child(3),
|
||||
.analisis table td.c3,
|
||||
.desglose table td.c3 {
|
||||
text-align: center !important;
|
||||
color: #6c757d;
|
||||
color: #495057;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
@@ -214,16 +219,17 @@
|
||||
/* ========================================
|
||||
FILAS DE SUBTOTALES
|
||||
(Suma de Material, Suma de Mano de Obra, etc)
|
||||
Fase 4.4 Accesibilidad: Color oscuro para contraste WCAG AA (4.5:1)
|
||||
======================================== */
|
||||
.analisis table tr.subtotal-row,
|
||||
.desglose table tr.subtotal-row {
|
||||
background-color: rgba(255, 133, 0, 0.1) !important;
|
||||
background-color: rgba(255, 133, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.analisis table tr.subtotal-row td,
|
||||
.desglose table tr.subtotal-row td {
|
||||
font-weight: 700;
|
||||
color: var(--color-orange-primary);
|
||||
color: #1e3a5f;
|
||||
padding: 0.875rem 1rem;
|
||||
border: none !important;
|
||||
}
|
||||
@@ -235,7 +241,7 @@
|
||||
.analisis table tr.subtotal-row td.c6,
|
||||
.analisis table tr.subtotal-row td:nth-child(6) {
|
||||
font-size: 1.05rem;
|
||||
color: var(--color-orange-primary);
|
||||
color: #1e3a5f;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* APUS Theme - Main Stylesheet
|
||||
* ROI Theme - Main Stylesheet
|
||||
*
|
||||
* RESPONSABILIDAD: Estilos principales del tema
|
||||
* - Variables CSS específicas del tema (:root en este archivo)
|
||||
@@ -12,7 +12,7 @@
|
||||
* - variables.css: SOLO variables de colores/espaciados/etc
|
||||
* - style.css: Aplica variables a elementos HTML (este archivo)
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
IMPORTANTE: Este archivo style.css es para estilos GLOBALES del tema únicamente.
|
||||
|
||||
El CSS de componentes individuales DEBE ir en archivos separados en:
|
||||
wp-content/themes/apus-theme/assets/css/[nombre-componente].css
|
||||
wp-content/themes/roi-theme/assets/css/[nombre-componente].css
|
||||
|
||||
Ejemplos de componentes con archivos individuales:
|
||||
- CTA Box Sidebar → cta-box-sidebar.css
|
||||
@@ -43,7 +43,7 @@
|
||||
========================================
|
||||
|
||||
El CSS de Share Buttons DEBE estar en:
|
||||
wp-content/themes/apus-theme/assets/css/social-share.css
|
||||
wp-content/themes/roi-theme/assets/css/social-share.css
|
||||
|
||||
Este archivo ya existe y está correctamente enqueued.
|
||||
Ver: inc/enqueue-scripts.php líneas 405-421
|
||||
@@ -55,7 +55,7 @@
|
||||
========================================
|
||||
|
||||
El CSS de CTA A/B Testing DEBE estar en:
|
||||
wp-content/themes/apus-theme/assets/css/cta.css
|
||||
wp-content/themes/roi-theme/assets/css/cta.css
|
||||
|
||||
Este archivo ya existe y está correctamente enqueued.
|
||||
Ver: inc/enqueue-scripts.php líneas 443-477
|
||||
@@ -67,7 +67,7 @@
|
||||
========================================
|
||||
|
||||
El CSS de Related Posts DEBE estar en:
|
||||
wp-content/themes/apus-theme/assets/css/related-posts.css
|
||||
wp-content/themes/roi-theme/assets/css/related-posts.css
|
||||
|
||||
Este archivo ya existe y está correctamente enqueued.
|
||||
Ver: inc/enqueue-scripts.php líneas 148-156
|
||||
@@ -79,7 +79,7 @@
|
||||
========================================
|
||||
|
||||
El CSS de Pagination DEBE estar en:
|
||||
wp-content/themes/apus-theme/assets/css/pagination.css
|
||||
wp-content/themes/roi-theme/assets/css/pagination.css
|
||||
|
||||
Este archivo ya existe y está correctamente enqueued.
|
||||
Ver: inc/enqueue-scripts.php líneas 129-136
|
||||
@@ -91,7 +91,7 @@
|
||||
========================================
|
||||
|
||||
El CSS de Footer Contact Form DEBE estar en:
|
||||
wp-content/themes/apus-theme/assets/css/footer-contact.css
|
||||
wp-content/themes/roi-theme/assets/css/footer-contact.css
|
||||
|
||||
Este archivo ya existe y está correctamente enqueued.
|
||||
Ver: inc/enqueue-scripts.php líneas 506-517
|
||||
@@ -146,7 +146,7 @@
|
||||
--color-text: #212529; /* Contrast ratio 15.52:1 against white */
|
||||
--color-bg: #ffffff;
|
||||
|
||||
/* APU Template Colors (from apus-theme-template/css/style.css) */
|
||||
/* APU Template Colors (from roi-theme-template/css/style.css) */
|
||||
--color-navy-dark: #0E2337;
|
||||
--color-navy-primary: #1e3a5f;
|
||||
--color-navy-light: #2c5282;
|
||||
@@ -341,6 +341,11 @@ img {
|
||||
.content-wrapper {
|
||||
grid-template-columns: 2fr 1fr;
|
||||
}
|
||||
|
||||
/* Full width when no sidebar */
|
||||
.no-sidebar .content-wrapper {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
#primary {
|
||||
1
Assets/Css/style.min.css
vendored
Normal file
1
Assets/Css/style.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
Assets/Fonts/poppins-v24-latin-500.woff2
Normal file
BIN
Assets/Fonts/poppins-v24-latin-500.woff2
Normal file
Binary file not shown.
BIN
Assets/Fonts/poppins-v24-latin-600.woff2
Normal file
BIN
Assets/Fonts/poppins-v24-latin-600.woff2
Normal file
Binary file not shown.
BIN
Assets/Fonts/poppins-v24-latin-700.woff2
Normal file
BIN
Assets/Fonts/poppins-v24-latin-700.woff2
Normal file
Binary file not shown.
BIN
Assets/Fonts/poppins-v24-latin-regular.woff2
Normal file
BIN
Assets/Fonts/poppins-v24-latin-regular.woff2
Normal file
Binary file not shown.
@@ -4,7 +4,7 @@
|
||||
* Mejoras de accesibilidad para navegación por teclado, gestión de focus,
|
||||
* y cumplimiento de WCAG 2.1 Level AA.
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Este script retrasa la carga de Google AdSense hasta que haya interacción
|
||||
* del usuario o se cumpla un timeout, mejorando el rendimiento de carga inicial.
|
||||
*
|
||||
* @package Apus_Theme
|
||||
* @package ROI_Theme
|
||||
* @since 1.0.0
|
||||
*/
|
||||
|
||||
@@ -182,12 +182,74 @@
|
||||
}, CONFIG.timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activa slots de AdSense insertados dinamicamente
|
||||
* Escucha el evento 'roi-adsense-activate' disparado por otros scripts
|
||||
*/
|
||||
function setupDynamicAdsListener() {
|
||||
window.addEventListener('roi-adsense-activate', function() {
|
||||
debugLog('Evento roi-adsense-activate recibido');
|
||||
|
||||
// Si AdSense aun no ha cargado, forzar carga ahora
|
||||
if (!adsenseLoaded) {
|
||||
debugLog('AdSense no cargado, forzando carga...');
|
||||
loadAdSense();
|
||||
return;
|
||||
}
|
||||
|
||||
// AdSense ya cargado - activar nuevos slots
|
||||
debugLog('Activando nuevos slots dinamicos...');
|
||||
activateDynamicSlots();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Activa slots de AdSense que fueron insertados despues de la carga inicial
|
||||
*/
|
||||
function activateDynamicSlots() {
|
||||
// Buscar scripts de push que aun no han sido ejecutados
|
||||
var pendingPushScripts = document.querySelectorAll('script[data-adsense-push][type="text/plain"]');
|
||||
|
||||
if (pendingPushScripts.length === 0) {
|
||||
debugLog('No hay slots pendientes por activar');
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('Activando ' + pendingPushScripts.length + ' slot(s) dinamico(s)');
|
||||
|
||||
// Asegurar que adsbygoogle existe
|
||||
window.adsbygoogle = window.adsbygoogle || [];
|
||||
|
||||
pendingPushScripts.forEach(function(oldScript) {
|
||||
try {
|
||||
// Crear nuevo script ejecutable
|
||||
var newScript = document.createElement('script');
|
||||
newScript.type = 'text/javascript';
|
||||
newScript.innerHTML = oldScript.innerHTML;
|
||||
|
||||
// Reemplazar el placeholder con el script real
|
||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||
} catch (e) {
|
||||
debugLog('Error activando slot: ' + e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa el cargador retrasado de AdSense
|
||||
*/
|
||||
function init() {
|
||||
// =========================================================================
|
||||
// NUEVO: Siempre configurar listener para ads dinamicos
|
||||
// IMPORTANTE: Esto debe ejecutarse ANTES del early return
|
||||
// porque los ads dinamicos pueden necesitar activarse aunque
|
||||
// el delay global este deshabilitado
|
||||
// =========================================================================
|
||||
setupDynamicAdsListener();
|
||||
debugLog('Listener para ads dinamicos configurado');
|
||||
|
||||
// Verificar si el retardo de AdSense está habilitado
|
||||
if (!window.apusAdsenseDelayed) {
|
||||
if (!window.roiAdsenseDelayed) {
|
||||
debugLog('Retardo de AdSense no habilitado');
|
||||
return;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user