Compare commits
199 Commits
migration/
...
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 |
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]
|
||||
}
|
||||
}
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -40,9 +40,6 @@ Desktop.ini
|
||||
node_modules/
|
||||
npm-debug.log
|
||||
|
||||
# Composer (si hay dependencias PHP)
|
||||
vendor/
|
||||
composer.lock
|
||||
|
||||
# PHPUnit
|
||||
.phpunit.result.cache
|
||||
@@ -76,5 +73,4 @@ _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
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,19 @@ final class ContactFormFieldMapper implements FieldMapperInterface
|
||||
'contactFormEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'contactFormShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'contactFormShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'contactFormShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'contactFormVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'contactFormVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'contactFormVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'contactFormVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'contactFormVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'contactFormExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'contactFormExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'contactFormExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'contactFormExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'contactFormSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\ContactForm\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Contact Form
|
||||
@@ -93,19 +94,47 @@ final class ContactFormFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('contactFormShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="contactFormShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="contactFormShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('contactFormVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'contactForm');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
@@ -598,4 +627,26 @@ final class ContactFormFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,20 @@ final class CtaBoxSidebarFieldMapper implements FieldMapperInterface
|
||||
'ctaEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
'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'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\CtaBoxSidebar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para el CTA Box Sidebar
|
||||
@@ -94,18 +95,61 @@ final class CtaBoxSidebarFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
||||
$html .= $this->buildSwitch('ctaShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// show_on_pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
// =============================================
|
||||
// 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 .= ' <label for="ctaShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="ctaShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
$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>';
|
||||
@@ -515,4 +559,29 @@ final class CtaBoxSidebarFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genera un checkbox de visibilidad por tipo de pagina
|
||||
*
|
||||
* Sigue Design System: form-check-checkbox es obligatorio
|
||||
*/
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, bool $checked): string
|
||||
{
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,20 @@ final class CtaLetsTalkFieldMapper implements FieldMapperInterface
|
||||
'ctaLetsTalkEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaLetsTalkShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaLetsTalkShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaLetsTalkShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
'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'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\CtaLetsTalk\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* Class CtaLetsTalkFormBuilder
|
||||
@@ -120,16 +121,73 @@ final class CtaLetsTalkFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Show on Pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('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 .= ' <label for="ctaLetsTalkShowOnPages" class="form-label small mb-1 fw-semibold">Mostrar en</label>';
|
||||
$html .= ' <select id="ctaLetsTalkShowOnPages" name="visibility[show_on_pages]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
||||
$html .= ' </select>';
|
||||
$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>';
|
||||
@@ -447,4 +505,26 @@ final class CtaLetsTalkFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,20 @@ final class CtaPostFieldMapper implements FieldMapperInterface
|
||||
'ctaPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'ctaPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'ctaPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'ctaPostShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
'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'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\CtaPost\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para CTA Post
|
||||
@@ -85,17 +86,59 @@ final class CtaPostFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('ctaPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
// =============================================
|
||||
// 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 .= ' <label for="ctaPostShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="ctaPostShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
$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>';
|
||||
@@ -437,4 +480,26 @@ final class CtaPostFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Application\DTOs;
|
||||
|
||||
/**
|
||||
* DTO para solicitud de guardado de snippet
|
||||
*
|
||||
* Inmutable - una vez creado no puede modificarse.
|
||||
* Transporta datos desde Infrastructure (form) hacia Application (use case).
|
||||
*/
|
||||
final class SaveSnippetRequest
|
||||
{
|
||||
/**
|
||||
* @param string $id ID único del snippet (nuevo o existente)
|
||||
* @param string $name Nombre descriptivo
|
||||
* @param string $description Descripción opcional
|
||||
* @param string $css Código CSS
|
||||
* @param string $type Tipo de carga: 'critical' | 'deferred'
|
||||
* @param array<string> $pages Páginas donde aplicar: ['all'], ['home', 'posts'], etc.
|
||||
* @param bool $enabled Si el snippet está activo
|
||||
* @param int $order Orden de carga (menor = primero)
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $id,
|
||||
public readonly string $name,
|
||||
public readonly string $description,
|
||||
public readonly string $css,
|
||||
public readonly string $type,
|
||||
public readonly array $pages,
|
||||
public readonly bool $enabled,
|
||||
public readonly int $order
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Factory desde array (formulario o API)
|
||||
*
|
||||
* @param array $data Datos del formulario
|
||||
* @return self
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
id: $data['id'] ?? '',
|
||||
name: $data['name'] ?? '',
|
||||
description: $data['description'] ?? '',
|
||||
css: $data['css'] ?? '',
|
||||
type: $data['type'] ?? 'deferred',
|
||||
pages: $data['pages'] ?? ['all'],
|
||||
enabled: (bool)($data['enabled'] ?? true),
|
||||
order: (int)($data['order'] ?? 100)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte a array para persistencia
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'css' => $this->css,
|
||||
'type' => $this->type,
|
||||
'pages' => $this->pages,
|
||||
'enabled' => $this->enabled,
|
||||
'order' => $this->order,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Application\UseCases;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\CSSSnippetRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Caso de uso: Eliminar snippet CSS
|
||||
*
|
||||
* SRP: Solo responsable de orquestar la eliminación
|
||||
*/
|
||||
final class DeleteSnippetUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CSSSnippetRepositoryInterface $repository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Ejecuta la eliminación del snippet
|
||||
*
|
||||
* @param string $snippetId ID del snippet a eliminar
|
||||
* @return void
|
||||
*/
|
||||
public function execute(string $snippetId): void
|
||||
{
|
||||
$this->repository->delete($snippetId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Application\UseCases;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\CSSSnippetRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Caso de uso: Obtener todos los snippets (para Admin UI)
|
||||
*
|
||||
* SRP: Solo responsable de obtener lista completa
|
||||
*/
|
||||
final class GetAllSnippetsUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CSSSnippetRepositoryInterface $repository
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Ejecuta la obtención de todos los snippets
|
||||
*
|
||||
* @return array<array> Lista de snippets ordenados por 'order'
|
||||
*/
|
||||
public function execute(): array
|
||||
{
|
||||
return $this->repository->getAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Application\UseCases;
|
||||
|
||||
use ROITheme\Admin\CustomCSSManager\Application\DTOs\SaveSnippetRequest;
|
||||
use ROITheme\Admin\CustomCSSManager\Domain\Entities\CSSSnippet;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSSnippetRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Caso de uso: Guardar snippet CSS
|
||||
*
|
||||
* SRP: Solo responsable de orquestar el guardado
|
||||
*/
|
||||
final class SaveSnippetUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CSSSnippetRepositoryInterface $repository
|
||||
) {}
|
||||
|
||||
public function execute(SaveSnippetRequest $request): void
|
||||
{
|
||||
// 1. Crear entidad desde DTO
|
||||
$snippet = CSSSnippet::fromArray($request->toArray());
|
||||
|
||||
// 2. Validar en dominio
|
||||
$snippet->validate();
|
||||
|
||||
// 3. Validar tamaño según tipo
|
||||
$snippet->css()->validateForLoadType($snippet->loadType());
|
||||
|
||||
// 4. Persistir
|
||||
$this->repository->save($snippet->toArray());
|
||||
}
|
||||
}
|
||||
115
Admin/CustomCSSManager/Domain/Entities/CSSSnippet.php
Normal file
115
Admin/CustomCSSManager/Domain/Entities/CSSSnippet.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Domain\Entities;
|
||||
|
||||
use ROITheme\Admin\CustomCSSManager\Domain\ValueObjects\SnippetId;
|
||||
use ROITheme\Admin\CustomCSSManager\Domain\ValueObjects\CSSCode;
|
||||
use ROITheme\Admin\CustomCSSManager\Domain\ValueObjects\LoadType;
|
||||
use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* Entidad de dominio para snippet CSS (contexto Admin)
|
||||
*
|
||||
* Responsabilidad: Reglas de negocio para ADMINISTRAR snippets
|
||||
*/
|
||||
final class CSSSnippet
|
||||
{
|
||||
private function __construct(
|
||||
private readonly SnippetId $id,
|
||||
private readonly string $name,
|
||||
private readonly string $description,
|
||||
private readonly CSSCode $css,
|
||||
private readonly LoadType $loadType,
|
||||
private readonly array $pages,
|
||||
private readonly bool $enabled,
|
||||
private readonly int $order
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Factory method desde array (BD)
|
||||
*/
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
SnippetId::fromString($data['id']),
|
||||
$data['name'],
|
||||
$data['description'] ?? '',
|
||||
CSSCode::fromString($data['css']),
|
||||
LoadType::fromString($data['type']),
|
||||
$data['pages'] ?? ['all'],
|
||||
$data['enabled'] ?? true,
|
||||
$data['order'] ?? 100
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida que el snippet pueda ser guardado
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function validate(): void
|
||||
{
|
||||
if (empty($this->name)) {
|
||||
throw new ValidationException('El nombre del snippet es requerido');
|
||||
}
|
||||
|
||||
if (strlen($this->name) > 100) {
|
||||
throw new ValidationException('El nombre no puede exceder 100 caracteres');
|
||||
}
|
||||
|
||||
// CSS ya validado en Value Object CSSCode
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte a array para persistencia
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id->value(),
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'css' => $this->css->value(),
|
||||
'type' => $this->loadType->value(),
|
||||
'pages' => $this->pages,
|
||||
'enabled' => $this->enabled,
|
||||
'order' => $this->order,
|
||||
];
|
||||
}
|
||||
|
||||
// Getters
|
||||
public function id(): SnippetId
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function css(): CSSCode
|
||||
{
|
||||
return $this->css;
|
||||
}
|
||||
|
||||
public function loadType(): LoadType
|
||||
{
|
||||
return $this->loadType;
|
||||
}
|
||||
|
||||
public function pages(): array
|
||||
{
|
||||
return $this->pages;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
public function order(): int
|
||||
{
|
||||
return $this->order;
|
||||
}
|
||||
}
|
||||
74
Admin/CustomCSSManager/Domain/ValueObjects/CSSCode.php
Normal file
74
Admin/CustomCSSManager/Domain/ValueObjects/CSSCode.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Domain\ValueObjects;
|
||||
|
||||
use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* Value Object para código CSS validado
|
||||
*/
|
||||
final class CSSCode
|
||||
{
|
||||
private const MAX_SIZE_CRITICAL = 14336; // 14KB para CSS crítico
|
||||
private const MAX_SIZE_DEFERRED = 102400; // 100KB para CSS diferido
|
||||
|
||||
private function __construct(
|
||||
private readonly string $value
|
||||
) {}
|
||||
|
||||
public static function fromString(string $css): self
|
||||
{
|
||||
$sanitized = self::sanitize($css);
|
||||
self::validate($sanitized);
|
||||
return new self($sanitized);
|
||||
}
|
||||
|
||||
private static function sanitize(string $css): string
|
||||
{
|
||||
// Eliminar etiquetas <style>
|
||||
$css = preg_replace('/<\/?style[^>]*>/i', '', $css);
|
||||
|
||||
// Eliminar comentarios HTML
|
||||
$css = preg_replace('/<!--.*?-->/s', '', $css);
|
||||
|
||||
return trim($css);
|
||||
}
|
||||
|
||||
private static function validate(string $css): void
|
||||
{
|
||||
// Detectar código potencialmente peligroso
|
||||
$dangerous = ['javascript:', 'expression(', '@import', 'behavior:'];
|
||||
foreach ($dangerous as $pattern) {
|
||||
if (stripos($css, $pattern) !== false) {
|
||||
throw new ValidationException("CSS contiene patrón no permitido: {$pattern}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function validateForLoadType(LoadType $loadType): void
|
||||
{
|
||||
$maxSize = $loadType->isCritical()
|
||||
? self::MAX_SIZE_CRITICAL
|
||||
: self::MAX_SIZE_DEFERRED;
|
||||
|
||||
if (strlen($this->value) > $maxSize) {
|
||||
throw new ValidationException(
|
||||
sprintf('CSS excede el tamaño máximo de %d bytes para tipo %s',
|
||||
$maxSize,
|
||||
$loadType->value()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function value(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->value);
|
||||
}
|
||||
}
|
||||
56
Admin/CustomCSSManager/Domain/ValueObjects/LoadType.php
Normal file
56
Admin/CustomCSSManager/Domain/ValueObjects/LoadType.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\CustomCSSManager\Domain\ValueObjects;
|
||||
|
||||
use ROITheme\Shared\Domain\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* Value Object para tipo de carga CSS
|
||||
*/
|
||||
final class LoadType
|
||||
{
|
||||
private const VALID_TYPES = ['critical', 'deferred'];
|
||||
|
||||
private function __construct(
|
||||
private readonly string $value
|
||||
) {}
|
||||
|
||||
public static function fromString(string $value): self
|
||||
{
|
||||
if (!in_array($value, self::VALID_TYPES, true)) {
|
||||
throw new ValidationException(
|
||||
sprintf('LoadType inválido: %s. Valores válidos: %s',
|
||||
$value,
|
||||
implode(', ', self::VALID_TYPES)
|
||||
)
|
||||
);
|
||||
}
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
public static function critical(): self
|
||||
{
|
||||
return new self('critical');
|
||||
}
|
||||
|
||||
public static function deferred(): self
|
||||
{
|
||||
return new self('deferred');
|
||||
}
|
||||
|
||||
public function isCritical(): bool
|
||||
{
|
||||
return $this->value === 'critical';
|
||||
}
|
||||
|
||||
public function isDeferred(): bool
|
||||
{
|
||||
return $this->value === 'deferred';
|
||||
}
|
||||
|
||||
public function value(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,19 @@ final class FeaturedImageFieldMapper implements FieldMapperInterface
|
||||
'featuredImageEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'featuredImageShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'featuredImageShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'featuredImageShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'featuredImageVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'featuredImageVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'featuredImageVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'featuredImageVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'featuredImageVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'featuredImageExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'featuredImageExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'featuredImageExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'featuredImageExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'featuredImageSize' => ['group' => 'content', 'attribute' => 'image_size'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\FeaturedImage\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
final class FeaturedImageFormBuilder
|
||||
{
|
||||
@@ -100,25 +101,75 @@ final class FeaturedImageFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="featuredImageShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="featuredImageShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las paginas</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('featuredImageVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'featuredImage');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildContentGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
|
||||
@@ -27,6 +27,19 @@ final class FooterFieldMapper implements FieldMapperInterface
|
||||
'footerShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'footerShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'footerVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'footerVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'footerVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'footerVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'footerVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'footerExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'footerExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'footerExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'footerExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Widget 1
|
||||
'footerWidget1Visible' => ['group' => 'widget_1', 'attribute' => 'widget_1_visible'],
|
||||
'footerWidget1Title' => ['group' => 'widget_1', 'attribute' => 'widget_1_title'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\Footer\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Footer
|
||||
@@ -90,6 +91,47 @@ final class FooterFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('footerShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', true);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', true);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('footerVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'footer');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
@@ -410,4 +452,19 @@ final class FooterFormBuilder
|
||||
}
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, $value): string
|
||||
{
|
||||
$checked = $value === true || $value === '1' || $value === 1 ? 'checked' : '';
|
||||
|
||||
$html = '<div class="form-check">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="' . esc_attr($id) . '" ' . $checked . '>';
|
||||
$html .= ' <label class="form-check-label small" for="' . esc_attr($id) . '">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,20 @@ final class HeroFieldMapper implements FieldMapperInterface
|
||||
'heroEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'heroShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'heroShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'heroShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
'heroIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'heroVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'heroVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'heroVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'heroVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'heroVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'heroExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'heroExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'heroExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'heroExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'heroShowCategories' => ['group' => 'content', 'attribute' => 'show_categories'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\Hero\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
final class HeroFormBuilder
|
||||
{
|
||||
@@ -102,18 +103,59 @@ final class HeroFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('heroVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'hero');
|
||||
|
||||
// Switch: CSS Crítico
|
||||
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="heroShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="heroShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
||||
$html .= ' </select>';
|
||||
$html .= ' <div 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>';
|
||||
@@ -413,4 +455,26 @@ final class HeroFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Infrastructure\Api\Wordpress;
|
||||
namespace ROITheme\Admin\Infrastructure\Api\WordPress;
|
||||
|
||||
use ROITheme\Admin\Domain\Contracts\MenuRegistrarInterface;
|
||||
use ROITheme\Admin\Domain\ValueObjects\MenuItem;
|
||||
@@ -107,6 +107,15 @@ final class AdminAssetEnqueuer
|
||||
true
|
||||
);
|
||||
|
||||
// Script de toggle para exclusiones (Plan 99.11)
|
||||
wp_enqueue_script(
|
||||
'roi-exclusion-toggle',
|
||||
$this->themeUri . '/Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js',
|
||||
['roi-admin-dashboard'],
|
||||
filemtime(get_template_directory() . '/Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js'),
|
||||
true
|
||||
);
|
||||
|
||||
// Pasar variables al JavaScript
|
||||
wp_localize_script(
|
||||
'roi-admin-dashboard',
|
||||
|
||||
@@ -18,10 +18,12 @@ final class AdminDashboardRenderer implements DashboardRendererInterface
|
||||
|
||||
/**
|
||||
* @param GetComponentSettingsUseCase|null $getComponentSettingsUseCase
|
||||
* @param ComponentGroupRegistry|null $groupRegistry Registro de grupos de componentes
|
||||
* @param array<string, mixed> $components Componentes disponibles
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ?GetComponentSettingsUseCase $getComponentSettingsUseCase = null,
|
||||
private readonly ?ComponentGroupRegistry $groupRegistry = null,
|
||||
private readonly array $components = []
|
||||
) {
|
||||
}
|
||||
@@ -96,6 +98,16 @@ final class AdminDashboardRenderer implements DashboardRendererInterface
|
||||
'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',
|
||||
@@ -111,6 +123,16 @@ final class AdminDashboardRenderer implements DashboardRendererInterface
|
||||
'label' => 'Theme Settings',
|
||||
'icon' => 'bi-gear',
|
||||
],
|
||||
'adsense-placement' => [
|
||||
'id' => 'adsense-placement',
|
||||
'label' => 'AdSense',
|
||||
'icon' => 'bi-megaphone',
|
||||
],
|
||||
'custom-css-manager' => [
|
||||
'id' => 'custom-css-manager',
|
||||
'label' => 'CSS Personalizado',
|
||||
'icon' => 'bi-file-earmark-code',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -153,13 +175,51 @@ final class AdminDashboardRenderer implements DashboardRendererInterface
|
||||
*/
|
||||
public function getFormBuilderClass(string $componentId): string
|
||||
{
|
||||
// Convertir kebab-case a PascalCase
|
||||
// 'top-notification-bar' → 'TopNotificationBar'
|
||||
$className = str_replace('-', '', ucwords($componentId, '-'));
|
||||
// Mapeo especial para componentes con acrónimos (CSS, API, etc.)
|
||||
$specialMappings = [
|
||||
'custom-css-manager' => 'CustomCSSManager',
|
||||
];
|
||||
|
||||
// Usar mapeo especial si existe, sino convertir kebab-case a PascalCase
|
||||
if (isset($specialMappings[$componentId])) {
|
||||
$className = $specialMappings[$componentId];
|
||||
} else {
|
||||
// 'top-notification-bar' → 'TopNotificationBar'
|
||||
$className = str_replace('-', '', ucwords($componentId, '-'));
|
||||
}
|
||||
|
||||
// Construir namespace completo
|
||||
// ROITheme\Admin\TopNotificationBar\Infrastructure\Ui\TopNotificationBarFormBuilder
|
||||
return "ROITheme\\Admin\\{$className}\\Infrastructure\\Ui\\{$className}FormBuilder";
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene los grupos de componentes
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public function getComponentGroups(): array
|
||||
{
|
||||
if ($this->groupRegistry === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->groupRegistry->getGroups();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el grupo al que pertenece un componente
|
||||
*
|
||||
* @param string $componentId ID del componente
|
||||
* @return string|null ID del grupo o null
|
||||
*/
|
||||
public function getGroupForComponent(string $componentId): ?string
|
||||
{
|
||||
if ($this->groupRegistry === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->groupRegistry->getGroupForComponent($componentId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +1,53 @@
|
||||
/**
|
||||
* Estilos para el Dashboard del Panel de Administración ROI Theme
|
||||
* Siguiendo especificaciones del Design System
|
||||
* ROI Theme Admin Dashboard - Improved Version
|
||||
* Basado en improved-panel.css del Design System
|
||||
*/
|
||||
|
||||
/* ================================================
|
||||
CSS VARIABLES - DESIGN SYSTEM
|
||||
================================================ */
|
||||
|
||||
:root {
|
||||
/* Navy Colors */
|
||||
--roi-navy-dark: #0E2337;
|
||||
--roi-navy-primary: #1e3a5f;
|
||||
--roi-navy-light: #2c5282;
|
||||
|
||||
/* Orange Colors */
|
||||
--roi-orange-primary: #FF8600;
|
||||
--roi-orange-hover: #FF6B35;
|
||||
--roi-orange-light: #FFB800;
|
||||
|
||||
/* Neutral Colors */
|
||||
--roi-neutral-50: #f8f9fa;
|
||||
--roi-neutral-100: #e9ecef;
|
||||
--roi-neutral-600: #495057;
|
||||
--roi-neutral-700: #6c757d;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
--shadow-xl: 0 12px 32px rgba(0, 0, 0, 0.15);
|
||||
--shadow-orange: 0 6px 20px rgba(255, 134, 0, 0.15);
|
||||
|
||||
/* Transitions */
|
||||
--transition-base: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-fast: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
WORDPRESS ADMIN OVERRIDES
|
||||
================================================ */
|
||||
|
||||
/* Sobrescribir max-width de .card de WordPress */
|
||||
.wrap.roi-admin-panel .card {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
/* Fix para switches de Bootstrap - resetear completamente estilos de WordPress */
|
||||
/* Fix para switches de Bootstrap */
|
||||
.wrap.roi-admin-panel .form-switch .form-check-input {
|
||||
all: unset !important;
|
||||
/* Restaurar estilos necesarios de Bootstrap */
|
||||
width: 2em !important;
|
||||
height: 1em !important;
|
||||
margin-left: -2.5em !important;
|
||||
@@ -49,7 +85,6 @@
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25) !important;
|
||||
}
|
||||
|
||||
/* Alinear verticalmente los labels con los switches */
|
||||
.wrap.roi-admin-panel .form-check {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
@@ -62,7 +97,10 @@
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
|
||||
/* Tabs Navigation */
|
||||
/* ================================================
|
||||
TABS NAVIGATION (Legacy)
|
||||
================================================ */
|
||||
|
||||
.nav-tabs-admin {
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
}
|
||||
@@ -77,7 +115,7 @@
|
||||
border-bottom: 3px solid transparent;
|
||||
padding: 0.3rem 0.3rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.83rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
@@ -113,8 +151,394 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
/* ================================================
|
||||
HEADER MEJORADO
|
||||
================================================ */
|
||||
|
||||
.roi-home-header {
|
||||
position: relative;
|
||||
padding: 2.5rem 2rem;
|
||||
background: var(--roi-navy-dark);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-lg);
|
||||
border: none;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
/* Patrón de fondo sutil */
|
||||
.roi-home-header__pattern {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 50%, rgba(255, 134, 0, 0.05) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 50%, rgba(44, 82, 130, 0.08) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.roi-home-header__content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.roi-home-header__icon-wrapper {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--roi-orange-primary), var(--roi-orange-light));
|
||||
border-radius: 16px;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4px 16px rgba(255, 134, 0, 0.3);
|
||||
}
|
||||
|
||||
.roi-home-header__icon {
|
||||
font-size: 2rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.roi-home-header__text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.roi-home-header__title {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
margin: 0 0 0.5rem 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.roi-home-header__subtitle {
|
||||
font-size: 0.95rem;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
GRID DE GRUPOS
|
||||
================================================ */
|
||||
|
||||
.roi-groups-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.roi-groups-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
GROUP CARDS MEJORADOS
|
||||
================================================ */
|
||||
|
||||
.roi-group-card {
|
||||
position: relative;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--roi-neutral-100);
|
||||
transition: var(--transition-base);
|
||||
animation: fadeInUp 0.4s cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||
}
|
||||
|
||||
/* Efecto glow en hover */
|
||||
.roi-group-card__glow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 12px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 0 0 1px var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
.roi-group-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
border-color: var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
.roi-group-card:hover .roi-group-card__glow {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Header del grupo */
|
||||
.roi-group-card__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1.25rem;
|
||||
border-bottom: 2px solid var(--roi-neutral-50);
|
||||
}
|
||||
|
||||
.roi-group-card__icon-wrapper {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 134, 0, 0.1);
|
||||
border-radius: 12px;
|
||||
flex-shrink: 0;
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
|
||||
.roi-group-card:hover .roi-group-card__icon-wrapper {
|
||||
background: rgba(255, 134, 0, 0.15);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.roi-group-card__icon {
|
||||
font-size: 1.75rem;
|
||||
color: var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
.roi-group-card__header-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.roi-group-card__title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.35rem 0;
|
||||
color: var(--roi-navy-primary);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.roi-group-card__description {
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
color: var(--roi-neutral-700);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
COMPONENTS GRID
|
||||
================================================ */
|
||||
|
||||
.roi-components-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
MINI CARDS MEJORADOS
|
||||
================================================ */
|
||||
|
||||
.roi-component-minicard {
|
||||
position: relative;
|
||||
background: white;
|
||||
border: 1px solid var(--roi-neutral-100);
|
||||
border-radius: 10px;
|
||||
padding: 1.25rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-base);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.roi-component-minicard::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--roi-orange-primary), var(--roi-orange-light));
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.roi-component-minicard:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.roi-component-minicard:hover {
|
||||
transform: translateY(-3px) scale(1.02);
|
||||
box-shadow: var(--shadow-orange);
|
||||
border-color: var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
.roi-component-minicard:active {
|
||||
transform: translateY(-2px) scale(0.98);
|
||||
}
|
||||
|
||||
.roi-component-minicard:focus {
|
||||
outline: 2px solid var(--roi-orange-primary);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
/* Icono del mini card */
|
||||
.roi-component-minicard__icon-bg {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 134, 0, 0.08);
|
||||
border-radius: 10px;
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
|
||||
.roi-component-minicard:hover .roi-component-minicard__icon-bg {
|
||||
background: rgba(255, 134, 0, 0.15);
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
.roi-component-minicard__icon {
|
||||
font-size: 1.5rem;
|
||||
color: var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
.roi-component-minicard__label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--roi-navy-dark);
|
||||
line-height: 1.3;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.roi-component-minicard:hover .roi-component-minicard__label {
|
||||
color: var(--roi-orange-primary);
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
BREADCRUMB
|
||||
================================================ */
|
||||
|
||||
.roi-breadcrumb {
|
||||
padding: 1rem 1.5rem;
|
||||
background: var(--roi-neutral-50);
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid var(--roi-orange-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.roi-breadcrumb__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.roi-breadcrumb__separator {
|
||||
color: var(--roi-neutral-700);
|
||||
}
|
||||
|
||||
.roi-breadcrumb__group {
|
||||
color: var(--roi-neutral-700);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.roi-breadcrumb__current {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--roi-navy-primary);
|
||||
}
|
||||
|
||||
/* Botón Volver */
|
||||
.roi-back-to-home {
|
||||
border-color: var(--roi-navy-primary);
|
||||
color: var(--roi-navy-primary);
|
||||
}
|
||||
|
||||
.roi-back-to-home:hover {
|
||||
background-color: var(--roi-navy-primary);
|
||||
border-color: var(--roi-navy-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
COMPONENT FORM CONTAINER
|
||||
================================================ */
|
||||
|
||||
.roi-component-form-container {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
ANIMATIONS
|
||||
================================================ */
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
#roi-home-view,
|
||||
#roi-component-view {
|
||||
animation: fadeIn 0.4s ease-in;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
UTILITY CLASSES
|
||||
================================================ */
|
||||
|
||||
.roi-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ================================================
|
||||
RESPONSIVE
|
||||
================================================ */
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.roi-group-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.roi-home-header {
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.roi-home-header__icon-wrapper {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.roi-home-header__icon {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.roi-home-header__title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-tabs-admin {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -125,7 +549,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
@media (max-width: 768px) {
|
||||
.roi-home-header__content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.roi-components-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.roi-component-minicard {
|
||||
padding: 1rem 0.75rem;
|
||||
}
|
||||
|
||||
.roi-component-minicard__icon-bg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.roi-component-minicard__icon {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.roi-group-card__icon-wrapper {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.roi-group-card__icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-tabs-admin {
|
||||
overflow-x: auto;
|
||||
flex-wrap: nowrap;
|
||||
@@ -135,3 +592,39 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.roi-groups-grid {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.roi-home-header {
|
||||
padding: 1.5rem 1rem;
|
||||
}
|
||||
|
||||
.roi-home-header__title {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.roi-home-header__subtitle {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.roi-group-card {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.roi-group-card__title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.roi-components-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.roi-component-minicard {
|
||||
flex-direction: row;
|
||||
text-align: left;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,73 @@
|
||||
* Inicializa el dashboard cuando el DOM está listo
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeTabs();
|
||||
// Nueva navegación por Cards/Grupos
|
||||
initializeCardNavigation();
|
||||
|
||||
// Funcionalidad existente (solo si hay tabs visibles)
|
||||
if (document.querySelector('.nav-tabs-admin')) {
|
||||
initializeTabs();
|
||||
}
|
||||
|
||||
initializeFormValidation();
|
||||
initializeButtons();
|
||||
initializeColorPickers();
|
||||
});
|
||||
|
||||
/**
|
||||
* Inicializa la navegación por Cards/Grupos (App-Style)
|
||||
*/
|
||||
function initializeCardNavigation() {
|
||||
// Verificar que estamos en el panel correcto
|
||||
const adminPanel = document.querySelector('.roi-admin-panel');
|
||||
if (!adminPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delegación de eventos para mini-cards
|
||||
document.addEventListener('click', function(e) {
|
||||
const minicard = e.target.closest('.roi-component-minicard');
|
||||
if (minicard) {
|
||||
e.preventDefault();
|
||||
const componentId = minicard.getAttribute('data-component-id');
|
||||
if (componentId) {
|
||||
navigateToComponent(componentId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Botón volver al home
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.closest('.roi-back-to-home')) {
|
||||
e.preventDefault();
|
||||
navigateToHome();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navega a un componente específico
|
||||
*
|
||||
* @param {string} componentId ID del componente en kebab-case
|
||||
*/
|
||||
function navigateToComponent(componentId) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('component', componentId);
|
||||
// Eliminar el parámetro admin-tab si existe (legacy)
|
||||
url.searchParams.delete('admin-tab');
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navega de vuelta al home (vista de grupos)
|
||||
*/
|
||||
function navigateToHome() {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('component');
|
||||
url.searchParams.delete('admin-tab');
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa el sistema de tabs con persistencia en URL
|
||||
*/
|
||||
|
||||
104
Admin/Infrastructure/Ui/ComponentGroupRegistry.php
Normal file
104
Admin/Infrastructure/Ui/ComponentGroupRegistry.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Infrastructure\Ui;
|
||||
|
||||
/**
|
||||
* Registro de grupos de componentes para el admin panel
|
||||
*
|
||||
* Responsabilidad única: Gestionar la configuración de grupos
|
||||
* y la asignación de componentes a grupos.
|
||||
*
|
||||
* @package ROITheme\Admin\Infrastructure\Ui
|
||||
*/
|
||||
final class ComponentGroupRegistry
|
||||
{
|
||||
/**
|
||||
* Obtiene los grupos de componentes con sus configuraciones
|
||||
*
|
||||
* Los grupos son extensibles via filtro WordPress para permitir
|
||||
* que plugins agreguen componentes a grupos existentes o creen nuevos.
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public function getGroups(): array
|
||||
{
|
||||
// Design System: Todos los grupos usan el mismo gradiente Navy (#0E2337 → #1e3a5f)
|
||||
// No se requiere propiedad 'color' ya que está definido en CSS
|
||||
$defaultGroups = [
|
||||
'header-navigation' => [
|
||||
'label' => __('Header & Navegación', 'roi-theme'),
|
||||
'icon' => 'bi-layout-text-window',
|
||||
'description' => __('Barras superiores, menú y pie de página', 'roi-theme'),
|
||||
'components' => ['top-notification-bar', 'navbar', 'footer']
|
||||
],
|
||||
'main-content' => [
|
||||
'label' => __('Contenido Principal', 'roi-theme'),
|
||||
'icon' => 'bi-file-richtext',
|
||||
'description' => __('Secciones principales de páginas y posts', 'roi-theme'),
|
||||
'components' => ['hero', 'featured-image', 'table-of-contents', 'related-post', '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;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
/**
|
||||
* ROI Theme - Panel de Administración Principal
|
||||
*
|
||||
* Nueva UI con sistema de Cards/Grupos (App-Style Navigation)
|
||||
*
|
||||
* @var AdminDashboardRenderer $this
|
||||
*/
|
||||
|
||||
@@ -13,76 +15,34 @@ if (!defined('ABSPATH')) {
|
||||
}
|
||||
|
||||
$components = $this->getComponents();
|
||||
$groups = $this->getComponentGroups();
|
||||
|
||||
// Determinar tab activo: desde URL o primer componente
|
||||
$activeComponentId = array_key_first($components);
|
||||
|
||||
// Leer parametro admin-tab de la URL con sanitizacion
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Solo lectura de parametro para UI
|
||||
if (isset($_GET['admin-tab'])) {
|
||||
$requestedTab = sanitize_text_field(wp_unslash($_GET['admin-tab']));
|
||||
// =====================================================
|
||||
// SANITIZACIÓN OBLIGATORIA según estándares WordPress
|
||||
// =====================================================
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Solo lectura de parámetro para UI
|
||||
$activeComponent = null;
|
||||
if (isset($_GET['component'])) {
|
||||
$requestedComponent = sanitize_text_field(wp_unslash($_GET['component']));
|
||||
// Validar que el componente exista
|
||||
if (array_key_exists($requestedTab, $components)) {
|
||||
$activeComponentId = $requestedTab;
|
||||
if (array_key_exists($requestedComponent, $components)) {
|
||||
$activeComponent = $requestedComponent;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="wrap roi-admin-panel">
|
||||
<!-- Navigation Tabs -->
|
||||
<ul class="nav nav-tabs nav-tabs-admin mb-0" role="tablist">
|
||||
<?php foreach ($components as $componentId => $component): ?>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link <?php echo $componentId === $activeComponentId ? 'active' : ''; ?>"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#<?php echo esc_attr($componentId); ?>Tab"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="<?php echo esc_attr($componentId); ?>Tab"
|
||||
aria-selected="<?php echo $componentId === $activeComponentId ? 'true' : 'false'; ?>">
|
||||
<i class="bi <?php echo esc_attr($component['icon']); ?> me-1"></i>
|
||||
<?php echo esc_html($component['label']); ?>
|
||||
</button>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content mt-3">
|
||||
<?php foreach ($components as $componentId => $component):
|
||||
$isActive = ($componentId === $activeComponentId);
|
||||
$componentSettings = $this->getComponentSettings($componentId);
|
||||
?>
|
||||
<!-- Tab: <?php echo esc_html($component['label']); ?> -->
|
||||
<div class="tab-pane fade <?php echo $isActive ? 'show active' : ''; ?>"
|
||||
id="<?php echo esc_attr($componentId); ?>Tab"
|
||||
role="tabpanel">
|
||||
|
||||
<?php
|
||||
// Renderizar FormBuilder del componente
|
||||
$formBuilderClass = $this->getFormBuilderClass($componentId);
|
||||
if (class_exists($formBuilderClass)) {
|
||||
$formBuilder = new $formBuilderClass($this);
|
||||
echo $formBuilder->buildForm($componentId);
|
||||
} else {
|
||||
echo '<p class="text-danger">FormBuilder no encontrado: ' . esc_html($formBuilderClass) . '</p>';
|
||||
}
|
||||
?>
|
||||
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Botones Globales Save/Cancel -->
|
||||
<div class="d-flex justify-content-end gap-2 p-3 rounded border mt-4" style="background-color: #f8f9fa; border-color: #e9ecef !important;">
|
||||
<button type="button" class="btn btn-outline-secondary" id="cancelChanges">
|
||||
<i class="bi bi-x-circle me-1"></i>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="button" id="saveSettings" class="btn fw-semibold text-white" style="background-color: #FF8600; border-color: #FF8600;">
|
||||
<i class="bi bi-check-circle me-1"></i>
|
||||
Guardar Cambios
|
||||
</button>
|
||||
</div>
|
||||
<?php if ($activeComponent !== null): ?>
|
||||
<!-- =====================================================
|
||||
Vista de Componente Individual
|
||||
===================================================== -->
|
||||
<?php include __DIR__ . '/partials/component-view.php'; ?>
|
||||
<?php else: ?>
|
||||
<!-- =====================================================
|
||||
Vista Home: Grupos y Cards
|
||||
===================================================== -->
|
||||
<?php include __DIR__ . '/partials/groups-home.php'; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
</div><!-- /wrap -->
|
||||
|
||||
48
Admin/Infrastructure/Ui/Views/partials/breadcrumb.php
Normal file
48
Admin/Infrastructure/Ui/Views/partials/breadcrumb.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
/**
|
||||
* Breadcrumb de navegación
|
||||
*
|
||||
* @var AdminDashboardRenderer $this
|
||||
* @var string $activeComponent ID del componente activo
|
||||
* @var array<string, array<string, mixed>> $groups Grupos de componentes
|
||||
* @var array<string, array<string, string>> $components Componentes disponibles
|
||||
* @var array<string, mixed>|null $group Grupo del componente activo
|
||||
* @var array<string, string>|null $component Datos del componente activo
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<nav class="roi-breadcrumb mb-4" aria-label="<?php echo esc_attr__('Navegación', 'roi-theme'); ?>">
|
||||
<div class="d-flex align-items-center flex-wrap gap-2">
|
||||
<!-- Botón Volver -->
|
||||
<button type="button" class="roi-back-to-home btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i>
|
||||
<?php echo esc_html__('Volver', 'roi-theme'); ?>
|
||||
</button>
|
||||
|
||||
<!-- Separador -->
|
||||
<span class="roi-breadcrumb__separator text-muted">/</span>
|
||||
|
||||
<!-- Grupo -->
|
||||
<?php if ($group): ?>
|
||||
<span class="roi-breadcrumb__group">
|
||||
<i class="bi <?php echo esc_attr($group['icon']); ?> me-1" style="color: <?php echo esc_attr($group['color']); ?>;"></i>
|
||||
<?php echo esc_html($group['label']); ?>
|
||||
</span>
|
||||
<span class="roi-breadcrumb__separator text-muted">/</span>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Componente actual -->
|
||||
<?php if ($component): ?>
|
||||
<span class="roi-breadcrumb__current fw-semibold" aria-current="page" style="color: #FF8600;">
|
||||
<i class="bi <?php echo esc_attr($component['icon']); ?> me-1"></i>
|
||||
<?php echo esc_html($component['label']); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</nav>
|
||||
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>
|
||||
@@ -26,8 +26,21 @@ final class NavbarFieldMapper implements FieldMapperInterface
|
||||
'navbarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'navbarShowMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'navbarShowDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'navbarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
'navbarSticky' => ['group' => 'visibility', 'attribute' => 'sticky_enabled'],
|
||||
'navbarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'navbarVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'navbarVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'navbarVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'navbarVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'navbarVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'navbarExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'navbarExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'navbarExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'navbarExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Layout
|
||||
'navbarContainerType' => ['group' => 'layout', 'attribute' => 'container_type'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\Navbar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
final class NavbarFormBuilder
|
||||
{
|
||||
@@ -105,21 +106,50 @@ final class NavbarFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Show on Pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <label for="navbarShowOnPages" class="form-label small mb-1 fw-semibold">Mostrar en</label>';
|
||||
$html .= ' <select id="navbarShowOnPages" name="visibility[show_on_pages]" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', true);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', true);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('navbarVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'navbar');
|
||||
|
||||
// Switch: Sticky
|
||||
$sticky = $this->renderer->getFieldValue($componentId, 'visibility', 'sticky_enabled', true);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="mb-2">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarSticky" name="visibility[sticky_enabled]" ';
|
||||
$html .= checked($sticky, true, false) . '>';
|
||||
@@ -129,6 +159,19 @@ final class NavbarFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: CSS Crítico
|
||||
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="navbarIsCritical" name="visibility[is_critical]" ';
|
||||
$html .= checked($isCritical, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="navbarIsCritical">';
|
||||
$html .= ' <strong>CSS Crítico</strong>';
|
||||
$html .= ' <small class="text-muted d-block">Inyectar CSS en <head> para optimizar LCP</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
@@ -514,4 +557,26 @@ final class NavbarFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,19 @@ final class RelatedPostFieldMapper implements FieldMapperInterface
|
||||
'relatedPostEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'relatedPostShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'relatedPostShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'relatedPostShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'relatedPostVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'relatedPostVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'relatedPostVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'relatedPostVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'relatedPostVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'relatedPostExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'relatedPostExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'relatedPostExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'relatedPostExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'relatedPostSectionTitle' => ['group' => 'content', 'attribute' => 'section_title'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\RelatedPost\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Related Posts
|
||||
@@ -86,19 +87,47 @@ final class RelatedPostFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('relatedPostShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="relatedPostShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="relatedPostShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('relatedPostVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'relatedPost');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
@@ -498,4 +527,26 @@ final class RelatedPostFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\Api\Wordpress;
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\Api\WordPress;
|
||||
|
||||
use ROITheme\Shared\Application\UseCases\SaveComponentSettings\SaveComponentSettingsUseCase;
|
||||
use ROITheme\Admin\Shared\Infrastructure\FieldMapping\FieldMapperRegistry;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Services\ExclusionFieldProcessor;
|
||||
|
||||
/**
|
||||
* Handler para peticiones AJAX del panel de administracion
|
||||
@@ -73,10 +74,16 @@ final class AdminAjaxHandler
|
||||
|
||||
/**
|
||||
* Mapea settings de field IDs a grupos/atributos
|
||||
*
|
||||
* Soporta tipos especiales para campos de exclusion:
|
||||
* - json_array: Convierte "a, b, c" a ["a", "b", "c"]
|
||||
* - json_array_int: Convierte "1, 2, 3" a [1, 2, 3]
|
||||
* - json_array_lines: Convierte lineas a array
|
||||
*/
|
||||
private function mapSettings(array $settings, array $fieldMapping): array
|
||||
{
|
||||
$mappedSettings = [];
|
||||
$fieldProcessor = new ExclusionFieldProcessor();
|
||||
|
||||
foreach ($settings as $fieldId => $value) {
|
||||
if (!isset($fieldMapping[$fieldId])) {
|
||||
@@ -86,11 +93,17 @@ final class AdminAjaxHandler
|
||||
$mapping = $fieldMapping[$fieldId];
|
||||
$groupName = $mapping['group'];
|
||||
$attributeName = $mapping['attribute'];
|
||||
$type = $mapping['type'] ?? null;
|
||||
|
||||
if (!isset($mappedSettings[$groupName])) {
|
||||
$mappedSettings[$groupName] = [];
|
||||
}
|
||||
|
||||
// Procesar valor segun tipo
|
||||
if ($type !== null && is_string($value)) {
|
||||
$value = $fieldProcessor->process($value, $type);
|
||||
}
|
||||
|
||||
$mappedSettings[$groupName][$attributeName] = $value;
|
||||
}
|
||||
|
||||
@@ -130,7 +143,7 @@ final class AdminAjaxHandler
|
||||
// Usar repositorio para restaurar valores
|
||||
if ($this->saveComponentSettingsUseCase !== null) {
|
||||
global $wpdb;
|
||||
$repository = new \ROITheme\Shared\Infrastructure\Persistence\Wordpress\WordPressComponentSettingsRepository($wpdb);
|
||||
$repository = new \ROITheme\Shared\Infrastructure\Persistence\WordPress\WordPressComponentSettingsRepository($wpdb);
|
||||
$updated = $repository->resetToDefaults($component, $schemaPath);
|
||||
|
||||
wp_send_json_success([
|
||||
@@ -32,6 +32,9 @@ final class FieldMapperProvider
|
||||
'ContactForm',
|
||||
'Footer',
|
||||
'ThemeSettings',
|
||||
'AdsensePlacement',
|
||||
'ArchiveHeader',
|
||||
'PostGrid',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\Services;
|
||||
|
||||
/**
|
||||
* Servicio para procesar campos de exclusion antes de guardar en BD
|
||||
*
|
||||
* Convierte formatos de UI a JSON para almacenamiento.
|
||||
*
|
||||
* v1.1: Extraido de AdminAjaxHandler (SRP)
|
||||
*
|
||||
* @package ROITheme\Admin\Shared\Infrastructure\Services
|
||||
*/
|
||||
final class ExclusionFieldProcessor
|
||||
{
|
||||
/**
|
||||
* Procesa un valor de campo de exclusion segun su tipo
|
||||
*
|
||||
* @param string $value Valor del campo (desde UI)
|
||||
* @param string $type Tipo de campo: json_array, json_array_int, json_array_lines
|
||||
* @return string JSON string para almacenar en BD
|
||||
*/
|
||||
public function process(string $value, string $type): string
|
||||
{
|
||||
return match ($type) {
|
||||
'json_array' => $this->processJsonArray($value),
|
||||
'json_array_int' => $this->processJsonArrayInt($value),
|
||||
'json_array_lines' => $this->processJsonArrayLines($value),
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* "a, b, c" -> ["a", "b", "c"]
|
||||
*/
|
||||
private function processJsonArray(string $value): string
|
||||
{
|
||||
$items = array_map('trim', explode(',', $value));
|
||||
$items = array_filter($items, fn($item) => $item !== '');
|
||||
return json_encode(array_values($items), JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* "1, 2, 3" -> [1, 2, 3]
|
||||
*/
|
||||
private function processJsonArrayInt(string $value): string
|
||||
{
|
||||
$items = array_map('trim', explode(',', $value));
|
||||
$items = array_filter($items, 'is_numeric');
|
||||
$items = array_map('intval', $items);
|
||||
return json_encode(array_values($items));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lineas separadas -> array
|
||||
*/
|
||||
private function processJsonArrayLines(string $value): string
|
||||
{
|
||||
$items = preg_split('/\r\n|\r|\n/', $value);
|
||||
$items = array_map('trim', $items);
|
||||
$items = array_filter($items, fn($item) => $item !== '');
|
||||
return json_encode(array_values($items), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
}
|
||||
31
Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js
Normal file
31
Admin/Shared/Infrastructure/Ui/Assets/Js/exclusion-toggle.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Toggle para mostrar/ocultar reglas de exclusion en FormBuilders
|
||||
*
|
||||
* Escucha cambios en checkboxes con ID que termine en "ExclusionsEnabled"
|
||||
* y muestra/oculta el contenedor de reglas correspondiente.
|
||||
*
|
||||
* @package ROITheme\Admin
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
function initExclusionToggles() {
|
||||
document.querySelectorAll('[id$="ExclusionsEnabled"]').forEach(function(checkbox) {
|
||||
// Handler para cambios
|
||||
checkbox.addEventListener('change', function() {
|
||||
const prefix = this.id.replace('ExclusionsEnabled', '');
|
||||
const rulesContainer = document.getElementById(prefix + 'ExclusionRules');
|
||||
if (rulesContainer) {
|
||||
rulesContainer.style.display = this.checked ? 'block' : 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Inicializar cuando DOM este listo
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initExclusionToggles);
|
||||
} else {
|
||||
initExclusionToggles();
|
||||
}
|
||||
})();
|
||||
260
Admin/Shared/Infrastructure/Ui/ExclusionFormPartial.php
Normal file
260
Admin/Shared/Infrastructure/Ui/ExclusionFormPartial.php
Normal file
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Admin\Shared\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
|
||||
/**
|
||||
* Componente UI parcial reutilizable para reglas de exclusion
|
||||
*
|
||||
* Genera el HTML para la seccion de exclusiones en FormBuilders.
|
||||
* Debe ser incluido despues de la seccion de visibilidad por tipo de pagina.
|
||||
*
|
||||
* Uso en FormBuilder:
|
||||
* ```php
|
||||
* $exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
* $html .= $exclusionPartial->render($componentId, 'prefijo');
|
||||
* ```
|
||||
*
|
||||
* @package ROITheme\Admin\Shared\Infrastructure\Ui
|
||||
*/
|
||||
final class ExclusionFormPartial
|
||||
{
|
||||
private const GROUP_NAME = '_exclusions';
|
||||
|
||||
public function __construct(
|
||||
private readonly AdminDashboardRenderer $renderer
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Renderiza la seccion de exclusiones
|
||||
*
|
||||
* @param string $componentId ID del componente (kebab-case)
|
||||
* @param string $prefix Prefijo para IDs de campos (ej: 'cta' genera 'ctaExclusionsEnabled')
|
||||
* @return string HTML de la seccion
|
||||
*/
|
||||
public function render(string $componentId, string $prefix): string
|
||||
{
|
||||
$html = '';
|
||||
|
||||
$html .= $this->buildExclusionHeader();
|
||||
$html .= $this->buildExclusionToggle($componentId, $prefix);
|
||||
$html .= $this->buildExclusionRules($componentId, $prefix);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildExclusionHeader(): string
|
||||
{
|
||||
$html = '<hr class="my-3">';
|
||||
$html .= '<p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-funnel me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Reglas de exclusion avanzadas';
|
||||
$html .= '</p>';
|
||||
$html .= '<p class="small text-muted mb-2">';
|
||||
$html .= ' Excluir este componente de categorias, posts o URLs especificos.';
|
||||
$html .= '</p>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildExclusionToggle(string $componentId, string $prefix): string
|
||||
{
|
||||
$enabled = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclusions_enabled',
|
||||
false
|
||||
);
|
||||
$checked = $this->toBool($enabled);
|
||||
|
||||
$id = $prefix . 'ExclusionsEnabled';
|
||||
|
||||
$html = '<div class="mb-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= ' <i class="bi bi-filter-circle me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Activar reglas de exclusion</strong>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildExclusionRules(string $componentId, string $prefix): string
|
||||
{
|
||||
$enabled = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclusions_enabled',
|
||||
false
|
||||
);
|
||||
$display = $this->toBool($enabled) ? 'block' : 'none';
|
||||
|
||||
$html = sprintf(
|
||||
'<div id="%sExclusionRules" style="display: %s;">',
|
||||
esc_attr($prefix),
|
||||
$display
|
||||
);
|
||||
|
||||
$html .= $this->buildCategoryField($componentId, $prefix);
|
||||
$html .= $this->buildPostIdsField($componentId, $prefix);
|
||||
$html .= $this->buildUrlPatternsField($componentId, $prefix);
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildCategoryField(string $componentId, string $prefix): string
|
||||
{
|
||||
$value = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclude_categories',
|
||||
'[]'
|
||||
);
|
||||
$categories = $this->jsonToCommaList($value);
|
||||
|
||||
$id = $prefix . 'ExcludeCategories';
|
||||
|
||||
$html = '<div class="mb-3">';
|
||||
$html .= sprintf(
|
||||
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= ' <i class="bi bi-folder me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Excluir en categorias';
|
||||
$html .= ' </label>';
|
||||
$html .= sprintf(
|
||||
' <input type="text" id="%s" class="form-control form-control-sm" value="%s" placeholder="noticias, eventos, tutoriales">',
|
||||
esc_attr($id),
|
||||
esc_attr($categories)
|
||||
);
|
||||
$html .= ' <small class="text-muted">Slugs de categorias separados por comas</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPostIdsField(string $componentId, string $prefix): string
|
||||
{
|
||||
$value = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclude_post_ids',
|
||||
'[]'
|
||||
);
|
||||
$postIds = $this->jsonToCommaList($value);
|
||||
|
||||
$id = $prefix . 'ExcludePostIds';
|
||||
|
||||
$html = '<div class="mb-3">';
|
||||
$html .= sprintf(
|
||||
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= ' <i class="bi bi-hash me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Excluir en posts/paginas';
|
||||
$html .= ' </label>';
|
||||
$html .= sprintf(
|
||||
' <input type="text" id="%s" class="form-control form-control-sm" value="%s" placeholder="123, 456, 789">',
|
||||
esc_attr($id),
|
||||
esc_attr($postIds)
|
||||
);
|
||||
$html .= ' <small class="text-muted">IDs de posts o paginas separados por comas</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildUrlPatternsField(string $componentId, string $prefix): string
|
||||
{
|
||||
$value = $this->renderer->getFieldValue(
|
||||
$componentId,
|
||||
self::GROUP_NAME,
|
||||
'exclude_url_patterns',
|
||||
'[]'
|
||||
);
|
||||
$patterns = $this->jsonToLineList($value);
|
||||
|
||||
$id = $prefix . 'ExcludeUrlPatterns';
|
||||
|
||||
$html = '<div class="mb-0">';
|
||||
$html .= sprintf(
|
||||
' <label for="%s" class="form-label small mb-1 fw-semibold">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= ' <i class="bi bi-link-45deg me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Excluir por patrones URL';
|
||||
$html .= ' </label>';
|
||||
$html .= sprintf(
|
||||
' <textarea id="%s" class="form-control form-control-sm" rows="3" placeholder="/privado/ /landing-especial/ /^\/categoria\/\d+$/">%s</textarea>',
|
||||
esc_attr($id),
|
||||
esc_textarea($patterns)
|
||||
);
|
||||
$html .= ' <small class="text-muted">Un patron por linea. Soporta texto simple o regex (ej: /^\/blog\/\d+$/)</small>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte JSON array o array a lista separada por comas
|
||||
*
|
||||
* @param string|array $value Valor desde BD (puede ser JSON string o array ya deserializado)
|
||||
*/
|
||||
private function jsonToCommaList(string|array $value): string
|
||||
{
|
||||
// Si ya es array, usarlo directamente
|
||||
if (is_array($value)) {
|
||||
return empty($value) ? '' : implode(', ', $value);
|
||||
}
|
||||
|
||||
// Si es string, intentar decodificar JSON
|
||||
$decoded = json_decode($value, true);
|
||||
|
||||
if (!is_array($decoded) || empty($decoded)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return implode(', ', $decoded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte JSON array o array a lista separada por lineas
|
||||
*
|
||||
* @param string|array $value Valor desde BD (puede ser JSON string o array ya deserializado)
|
||||
*/
|
||||
private function jsonToLineList(string|array $value): string
|
||||
{
|
||||
// Si ya es array, usarlo directamente
|
||||
if (is_array($value)) {
|
||||
return empty($value) ? '' : implode("\n", $value);
|
||||
}
|
||||
|
||||
// Si es string, intentar decodificar JSON
|
||||
$decoded = json_decode($value, true);
|
||||
|
||||
if (!is_array($decoded) || empty($decoded)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return implode("\n", $decoded);
|
||||
}
|
||||
|
||||
private function toBool(mixed $value): bool
|
||||
{
|
||||
return $value === true || $value === '1' || $value === 1;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,19 @@ final class SocialShareFieldMapper implements FieldMapperInterface
|
||||
'socialShareEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'socialShareShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'socialShareShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'socialShareShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'socialShareVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'socialShareVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'socialShareVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'socialShareVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'socialShareVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'socialShareExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'socialShareExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'socialShareExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'socialShareExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'socialShareShowLabel' => ['group' => 'content', 'attribute' => 'show_label'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\SocialShare\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para Social Share
|
||||
@@ -94,20 +95,47 @@ final class SocialShareFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', true);
|
||||
$html .= $this->buildSwitch('socialShareShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// show_on_pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="socialShareShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="socialShareShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all"' . ($showOnPages === 'all' ? ' selected' : '') . '>Todos</option>';
|
||||
$html .= ' <option value="posts"' . ($showOnPages === 'posts' ? ' selected' : '') . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages"' . ($showOnPages === 'pages' ? ' selected' : '') . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', true);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('socialShareVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'socialShare');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
@@ -526,4 +554,26 @@ final class SocialShareFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,19 @@ final class TableOfContentsFieldMapper implements FieldMapperInterface
|
||||
'tocEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'tocShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'tocShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'tocShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'tocVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
'tocVisibilityPosts' => ['group' => '_page_visibility', 'attribute' => 'show_on_posts'],
|
||||
'tocVisibilityPages' => ['group' => '_page_visibility', 'attribute' => 'show_on_pages'],
|
||||
'tocVisibilityArchives' => ['group' => '_page_visibility', 'attribute' => 'show_on_archives'],
|
||||
'tocVisibilitySearch' => ['group' => '_page_visibility', 'attribute' => 'show_on_search'],
|
||||
|
||||
// Exclusions (grupo especial _exclusions - Plan 99.11)
|
||||
'tocExclusionsEnabled' => ['group' => '_exclusions', 'attribute' => 'exclusions_enabled'],
|
||||
'tocExcludeCategories' => ['group' => '_exclusions', 'attribute' => 'exclude_categories', 'type' => 'json_array'],
|
||||
'tocExcludePostIds' => ['group' => '_exclusions', 'attribute' => 'exclude_post_ids', 'type' => 'json_array_int'],
|
||||
'tocExcludeUrlPatterns' => ['group' => '_exclusions', 'attribute' => 'exclude_url_patterns', 'type' => 'json_array_lines'],
|
||||
|
||||
// Content
|
||||
'tocTitle' => ['group' => 'content', 'attribute' => 'title'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\TableOfContents\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
/**
|
||||
* FormBuilder para la Tabla de Contenido
|
||||
@@ -94,20 +95,47 @@ final class TableOfContentsFormBuilder
|
||||
$showOnMobile = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_mobile', false);
|
||||
$html .= $this->buildSwitch('tocShowOnMobile', 'Mostrar en movil', 'bi-phone', $showOnMobile);
|
||||
|
||||
// show_on_pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'posts');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="tocShowOnPages" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="tocShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las paginas</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo paginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// Checkboxes de visibilidad por tipo de página
|
||||
// Grupo especial: _page_visibility
|
||||
// =============================================
|
||||
$html .= ' <hr class="my-3">';
|
||||
$html .= ' <p class="small fw-semibold mb-2">';
|
||||
$html .= ' <i class="bi bi-eye me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en tipos de pagina';
|
||||
$html .= ' </p>';
|
||||
|
||||
$showOnHome = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_home', false);
|
||||
$showOnPosts = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_posts', true);
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_pages', true);
|
||||
$showOnArchives = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_archives', false);
|
||||
$showOnSearch = $this->renderer->getFieldValue($componentId, '_page_visibility', 'show_on_search', false);
|
||||
|
||||
$html .= ' <div class="row g-2">';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityHome', 'Home', 'bi-house', $showOnHome);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityPosts', 'Posts', 'bi-file-earmark-text', $showOnPosts);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityPages', 'Paginas', 'bi-file-earmark', $showOnPages);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilityArchives', 'Archivos', 'bi-archive', $showOnArchives);
|
||||
$html .= ' </div>';
|
||||
$html .= ' <div class="col-md-4">';
|
||||
$html .= $this->buildPageVisibilityCheckbox('tocVisibilitySearch', 'Busqueda', 'bi-search', $showOnSearch);
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// =============================================
|
||||
// Reglas de exclusion avanzadas
|
||||
// Grupo especial: _exclusions (Plan 99.11)
|
||||
// =============================================
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'toc');
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
@@ -585,4 +613,26 @@ final class TableOfContentsFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,9 @@ final class ThemeSettingsFieldMapper implements FieldMapperInterface
|
||||
public function getFieldMapping(): array
|
||||
{
|
||||
return [
|
||||
// Analytics
|
||||
'themeSettingsGaTrackingId' => ['group' => 'analytics', 'attribute' => 'ga_tracking_id'],
|
||||
'themeSettingsGaAnonymizeIp' => ['group' => 'analytics', 'attribute' => 'ga_anonymize_ip'],
|
||||
|
||||
// AdSense
|
||||
'themeSettingsAdsensePublisherId' => ['group' => 'adsense', 'attribute' => 'adsense_publisher_id'],
|
||||
'themeSettingsAdsenseAutoAds' => ['group' => 'adsense', 'attribute' => 'adsense_auto_ads'],
|
||||
// Layout
|
||||
'themeSettingsContainerMaxWidth' => ['group' => 'layout', 'attribute' => 'container_max_width'],
|
||||
'themeSettingsContentColumnWidth' => ['group' => 'layout', 'attribute' => 'content_column_width'],
|
||||
|
||||
// Custom Code
|
||||
'themeSettingsCustomCss' => ['group' => 'custom_code', 'attribute' => 'custom_css'],
|
||||
|
||||
@@ -9,9 +9,10 @@ use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
* FormBuilder para Theme Settings
|
||||
*
|
||||
* RESPONSABILIDAD: Generar formulario de configuraciones globales del tema
|
||||
* (analytics, adsense, codigo personalizado)
|
||||
* (JavaScript personalizado)
|
||||
*
|
||||
* NOTA: Logo/branding se gestiona desde el componente navbar
|
||||
* NOTA: CSS personalizado se gestiona desde CustomCSSManager (TIPO 3)
|
||||
* Analytics y AdSense se gestionan desde el componente adsense-placement
|
||||
*
|
||||
* @package ROITheme\Admin\ThemeSettings\Infrastructure\Ui
|
||||
*/
|
||||
@@ -27,20 +28,92 @@ final class ThemeSettingsFormBuilder
|
||||
|
||||
$html .= $this->buildHeader($componentId);
|
||||
|
||||
$html .= '<div class="row g-3">';
|
||||
// Layout Group
|
||||
$html .= $this->buildLayoutGroup($componentId);
|
||||
|
||||
// Columna izquierda - Analytics + AdSense
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildAnalyticsGroup($componentId);
|
||||
$html .= $this->buildAdSenseGroup($componentId);
|
||||
// JavaScript Personalizado (solo 1 card)
|
||||
$html .= $this->buildJsGroup($componentId);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildLayoutGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-4" style="border-left: 4px solid #FF8600;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-layout-wtf me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Layout y Contenedor';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$html .= ' <div class="row g-3">';
|
||||
|
||||
// Container Max Width
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$containerWidth = $this->renderer->getFieldValue($componentId, 'layout', 'container_max_width', '1320');
|
||||
$containerWidthStr = is_string($containerWidth) ? $containerWidth : '1320';
|
||||
$html .= $this->buildSelect(
|
||||
'themeSettingsContainerMaxWidth',
|
||||
'Ancho maximo del contenedor',
|
||||
$containerWidthStr,
|
||||
[
|
||||
'1140' => '1140px (Bootstrap md)',
|
||||
'1200' => '1200px (Compacto)',
|
||||
'1320' => '1320px (Bootstrap xxl - Default)',
|
||||
'1400' => '1400px (Amplio)',
|
||||
'100%' => '100% (Fluido)'
|
||||
],
|
||||
'Valores menores dejan mas espacio para Rail Ads laterales'
|
||||
);
|
||||
$html .= ' </div>';
|
||||
|
||||
// Content Column Width
|
||||
$html .= ' <div class="col-md-6">';
|
||||
$columnWidth = $this->renderer->getFieldValue($componentId, 'layout', 'content_column_width', 'col-lg-9');
|
||||
$columnWidthStr = is_string($columnWidth) ? $columnWidth : 'col-lg-9';
|
||||
$html .= $this->buildSelect(
|
||||
'themeSettingsContentColumnWidth',
|
||||
'Ancho columna de contenido',
|
||||
$columnWidthStr,
|
||||
[
|
||||
'col-lg-8' => '8 columnas (66.67%)',
|
||||
'col-lg-9' => '9 columnas (75% - Default)',
|
||||
'col-lg-10' => '10 columnas (83.33%)',
|
||||
'col-lg-12' => '12 columnas (100% sin sidebar)'
|
||||
],
|
||||
'Proporcion de la columna principal vs sidebar'
|
||||
);
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' <div class="alert alert-info small mb-0 mt-3">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' Reduce el ancho del contenedor para dar mas espacio a los Rail Ads en pantallas grandes.';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Columna derecha - Custom Code
|
||||
$html .= '<div class="col-lg-6">';
|
||||
$html .= $this->buildCustomCodeGroup($componentId);
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
private function buildSelect(string $id, string $label, string $value, array $options, string $helpText = ''): string
|
||||
{
|
||||
$html = ' <div class="mb-3">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select class="form-select form-select-sm" id="' . esc_attr($id) . '">';
|
||||
foreach ($options as $optionValue => $optionLabel) {
|
||||
$selected = ($value === (string) $optionValue) ? ' selected' : '';
|
||||
$html .= ' <option value="' . esc_attr($optionValue) . '"' . $selected . '>' . esc_html($optionLabel) . '</option>';
|
||||
}
|
||||
$html .= ' </select>';
|
||||
if (!empty($helpText)) {
|
||||
$html .= ' <div class="form-text small">' . esc_html($helpText) . '</div>';
|
||||
}
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
@@ -56,7 +129,7 @@ final class ThemeSettingsFormBuilder
|
||||
$html .= ' Configuraciones Globales del Tema';
|
||||
$html .= ' </h3>';
|
||||
$html .= ' <p class="mb-0 small" style="opacity: 0.85;">';
|
||||
$html .= ' Analytics, AdSense y Codigo Personalizado';
|
||||
$html .= ' Layout y JavaScript Personalizado';
|
||||
$html .= ' </p>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' <button type="button" class="btn btn-sm btn-outline-light btn-reset-defaults" data-component="theme-settings">';
|
||||
@@ -69,83 +142,36 @@ final class ThemeSettingsFormBuilder
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildAnalyticsGroup(string $componentId): string
|
||||
private function buildJsGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-graph-up me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Analytics';
|
||||
$html .= ' <i class="bi bi-filetype-js me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' JavaScript Personalizado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$gaTrackingId = $this->renderer->getFieldValue($componentId, 'analytics', 'ga_tracking_id', '');
|
||||
$html .= $this->buildTextInput('themeSettingsGaTrackingId', 'Google Analytics ID', 'bi-bar-chart', $gaTrackingId);
|
||||
|
||||
$html .= ' <div class="form-text small mb-2">Formato: G-XXXXXXXXXX o UA-XXXXXXXX-X</div>';
|
||||
|
||||
$gaAnonymizeIp = $this->renderer->getFieldValue($componentId, 'analytics', 'ga_anonymize_ip', true);
|
||||
$html .= $this->buildSwitch('themeSettingsGaAnonymizeIp', 'Anonimizar IP (GDPR)', 'bi-shield-check', $gaAnonymizeIp);
|
||||
|
||||
$html .= ' <div class="alert alert-warning small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-exclamation-triangle me-1"></i>';
|
||||
$html .= ' Recomendado activar para cumplir con GDPR/RGPD';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildAdSenseGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-badge-ad me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Google AdSense';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$publisherId = $this->renderer->getFieldValue($componentId, 'adsense', 'adsense_publisher_id', '');
|
||||
$html .= $this->buildTextInput('themeSettingsAdsensePublisherId', 'Publisher ID', 'bi-key', $publisherId);
|
||||
|
||||
$html .= ' <div class="form-text small mb-2">Formato: ca-pub-1234567890123456</div>';
|
||||
|
||||
$autoAds = $this->renderer->getFieldValue($componentId, 'adsense', 'adsense_auto_ads', false);
|
||||
$html .= $this->buildSwitch('themeSettingsAdsenseAutoAds', 'Activar Auto Ads', 'bi-magic', $autoAds);
|
||||
|
||||
$html .= ' <div class="alert alert-info small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-info-circle me-1"></i>';
|
||||
$html .= ' Auto Ads permite que Google coloque anuncios automaticamente en las mejores ubicaciones.';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildCustomCodeGroup(string $componentId): string
|
||||
{
|
||||
$html = '<div class="card shadow-sm mb-3" style="border-left: 4px solid #1e3a5f;">';
|
||||
$html .= ' <div class="card-body">';
|
||||
$html .= ' <h5 class="fw-bold mb-3" style="color: #1e3a5f;">';
|
||||
$html .= ' <i class="bi bi-code-slash me-2" style="color: #FF8600;"></i>';
|
||||
$html .= ' Codigo Personalizado';
|
||||
$html .= ' </h5>';
|
||||
|
||||
$customCss = $this->renderer->getFieldValue($componentId, 'custom_code', 'custom_css', '');
|
||||
$html .= $this->buildTextareaCode('themeSettingsCustomCss', 'CSS Personalizado', 'bi-filetype-css', $customCss, 'Se inyecta en wp_head. No incluir etiquetas <style>');
|
||||
|
||||
$customJsHeader = $this->renderer->getFieldValue($componentId, 'custom_code', 'custom_js_header', '');
|
||||
$html .= $this->buildTextareaCode('themeSettingsCustomJsHeader', 'JavaScript en Header', 'bi-filetype-js', $customJsHeader, 'Se inyecta en wp_head. No incluir etiquetas <script>');
|
||||
$html .= $this->buildTextareaCode(
|
||||
'themeSettingsCustomJsHeader',
|
||||
'JavaScript en Header',
|
||||
$customJsHeader,
|
||||
'Se inyecta en wp_head. No incluir etiquetas <script>',
|
||||
5
|
||||
);
|
||||
|
||||
$customJsFooter = $this->renderer->getFieldValue($componentId, 'custom_code', 'custom_js_footer', '');
|
||||
$html .= $this->buildTextareaCode('themeSettingsCustomJsFooter', 'JavaScript en Footer', 'bi-filetype-js', $customJsFooter, 'Se inyecta en wp_footer. No incluir etiquetas <script>');
|
||||
$html .= $this->buildTextareaCode(
|
||||
'themeSettingsCustomJsFooter',
|
||||
'JavaScript en Footer',
|
||||
$customJsFooter,
|
||||
'Se inyecta en wp_footer. No incluir etiquetas <script>',
|
||||
5
|
||||
);
|
||||
|
||||
$html .= ' <div class="alert alert-danger small mb-0 mt-2">';
|
||||
$html .= ' <i class="bi bi-exclamation-octagon me-1"></i>';
|
||||
$html .= ' <strong>Advertencia:</strong> El codigo personalizado puede afectar el rendimiento y seguridad del sitio.';
|
||||
$html .= ' <strong>Advertencia:</strong> El codigo JS puede afectar el rendimiento y seguridad del sitio.';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
@@ -154,47 +180,15 @@ final class ThemeSettingsFormBuilder
|
||||
return $html;
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private function buildSwitch(string $id, string $label, string $icon, $value): string
|
||||
{
|
||||
$checked = $value === true || $value === '1' || $value === 1 ? 'checked' : '';
|
||||
|
||||
$html = ' <div class="form-check form-switch mb-2">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="' . esc_attr($id) . '" ' . $checked . '>';
|
||||
$html .= ' <label class="form-check-label small" for="' . esc_attr($id) . '">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTextInput(string $id, string $label, string $icon, mixed $value): string
|
||||
private function buildTextareaCode(string $id, string $label, mixed $value, string $helpText = '', int $rows = 4): string
|
||||
{
|
||||
$value = $this->normalizeStringValue($value);
|
||||
|
||||
$html = ' <div class="mb-3">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' <input type="text" class="form-control form-control-sm" id="' . esc_attr($id) . '" value="' . esc_attr($value) . '">';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildTextareaCode(string $id, string $label, string $icon, mixed $value, string $helpText = ''): string
|
||||
{
|
||||
$value = $this->normalizeStringValue($value);
|
||||
|
||||
$html = ' <div class="mb-3">';
|
||||
$html .= ' <label for="' . esc_attr($id) . '" class="form-label small mb-1 fw-semibold">';
|
||||
$html .= ' <i class="bi ' . esc_attr($icon) . ' me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' ' . esc_html($label);
|
||||
$html .= ' </label>';
|
||||
$html .= ' <textarea class="form-control form-control-sm font-monospace" id="' . esc_attr($id) . '" rows="4" style="font-size: 0.85em;">' . esc_textarea($value) . '</textarea>';
|
||||
$html .= ' <textarea class="form-control form-control-sm font-monospace" id="' . esc_attr($id) . '" rows="' . $rows . '" style="font-size: 0.85em;">' . esc_textarea($value) . '</textarea>';
|
||||
if (!empty($helpText)) {
|
||||
$html .= ' <div class="form-text small">' . $helpText . '</div>';
|
||||
}
|
||||
|
||||
@@ -26,7 +26,21 @@ final class TopNotificationBarFieldMapper implements FieldMapperInterface
|
||||
'topBarEnabled' => ['group' => 'visibility', 'attribute' => 'is_enabled'],
|
||||
'topBarShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'topBarShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'topBarShowOnPages' => ['group' => 'visibility', 'attribute' => 'show_on_pages'],
|
||||
'topBarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
|
||||
'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'],
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace ROITheme\Admin\TopNotificationBar\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Admin\Infrastructure\Ui\AdminDashboardRenderer;
|
||||
use ROITheme\Admin\Shared\Infrastructure\Ui\ExclusionFormPartial;
|
||||
|
||||
final class TopNotificationBarFormBuilder
|
||||
{
|
||||
@@ -105,19 +106,73 @@ final class TopNotificationBarFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Select: Show on Pages
|
||||
$showOnPages = $this->renderer->getFieldValue($componentId, 'visibility', 'show_on_pages', 'all');
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <label for="topBarShowOnPages" class="form-label small mb-1 fw-semibold" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-file-earmark-text me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' Mostrar en';
|
||||
$html .= ' </label>';
|
||||
$html .= ' <select id="topBarShowOnPages" class="form-select form-select-sm">';
|
||||
$html .= ' <option value="all" ' . selected($showOnPages, 'all', false) . '>Todas las páginas</option>';
|
||||
$html .= ' <option value="home" ' . selected($showOnPages, 'home', false) . '>Solo página de inicio</option>';
|
||||
$html .= ' <option value="posts" ' . selected($showOnPages, 'posts', false) . '>Solo posts individuales</option>';
|
||||
$html .= ' <option value="pages" ' . selected($showOnPages, 'pages', false) . '>Solo páginas</option>';
|
||||
$html .= ' </select>';
|
||||
// =============================================
|
||||
// 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>';
|
||||
@@ -305,4 +360,26 @@ final class TopNotificationBarFormBuilder
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function buildPageVisibilityCheckbox(string $id, string $label, string $icon, mixed $checked): string
|
||||
{
|
||||
$checked = $checked === true || $checked === '1' || $checked === 1;
|
||||
|
||||
$html = ' <div class="form-check form-check-checkbox mb-2">';
|
||||
$html .= sprintf(
|
||||
' <input class="form-check-input" type="checkbox" id="%s" %s>',
|
||||
esc_attr($id),
|
||||
$checked ? 'checked' : ''
|
||||
);
|
||||
$html .= sprintf(
|
||||
' <label class="form-check-label small" for="%s">',
|
||||
esc_attr($id)
|
||||
);
|
||||
$html .= sprintf(' <i class="bi %s me-1" style="color: #FF8600;"></i>', esc_attr($icon));
|
||||
$html .= sprintf(' %s', esc_html($label));
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
|
||||
1
Assets/CriticalCSS/responsive.critical.css
Normal file
1
Assets/CriticalCSS/responsive.critical.css
Normal file
@@ -0,0 +1 @@
|
||||
@media (max-width:575.98px){:root{--bs-gutter-x:1rem}body{font-size:14px}h1{font-size:24px}h2{font-size:20px}h3{font-size:18px}.container-fluid{padding:0 10px}.navbar{padding:0.5rem 0}.navbar-brand{font-size:18px}main{padding:0.5rem}.sidebar{margin-top:2rem}table{font-size:12px;margin-bottom:1rem;overflow-x:auto}.table-responsive{margin-bottom:1rem}.btn{padding:0.375rem 0.75rem;font-size:14px}.btn-lg{padding:0.5rem 1rem;font-size:16px}.card{margin-bottom:1rem}.form-group{margin-bottom:1rem}.form-control{padding:0.375rem 0.75rem;font-size:16px}.modal-dialog{margin:0.5rem}.modal-content{border-radius:4px}img{max-width:100%;height:auto}ul,ol{padding-left:1.5rem}.mt-1,.my-1{margin-top:0.25rem !important}.mb-1,.my-1{margin-bottom:0.25rem !important}.p-1{padding:0.25rem !important}}@media (min-width:576px){body{font-size:14px}h1{font-size:28px}h2{font-size:22px}h3{font-size:18px}}@media (min-width:768px){body{font-size:15px}h1{font-size:32px}h2{font-size:26px}h3{font-size:20px}.row-md-2{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem}.navbar{padding:1rem 0}.main-content{display:grid;grid-template-columns:1fr 300px;gap:2rem}.main-content.no-sidebar{grid-template-columns:1fr}}@media (min-width:992px){body{font-size:16px}h1{font-size:36px}h2{font-size:28px}h3{font-size:22px}.row-lg-3{display:grid;grid-template-columns:repeat(3,1fr);gap:2rem}.main-content{display:grid;grid-template-columns:1fr 300px;gap:2rem}.main-content.with-left-sidebar{grid-template-columns:250px 1fr 300px}.content-wrapper{max-width:1200px;margin:0 auto}}
|
||||
1
Assets/CriticalCSS/variables.critical.css
Normal file
1
Assets/CriticalCSS/variables.critical.css
Normal file
@@ -0,0 +1 @@
|
||||
:root{--color-navy-dark:#0E2337;--color-navy-primary:#1e3a5f;--color-navy-light:#2c5282;--color-blue-primary:#1e3a5f;--color-blue-secondary:#2c5282;--color-blue-light:#1a73e8;--color-cyan-primary:#61c7cd;--color-cyan-dark:#4db8c4;--color-cyan-darker:#4fb3b9;--color-orange-primary:#FF8600;--color-orange-secondary:#FFB800;--color-orange-light:#FFB800;--color-orange-button:#FF6B35;--color-orange-button-end:#FF8C42;--color-orange-hover:#FF6B35;--color-neutral-50:#f8f9fa;--color-neutral-100:#e9ecef;--color-neutral-600:#495057;--color-neutral-700:#6c757d;--color-slate-gray:#4C5C6B;--color-gray-50:#f8f9fa;--color-gray-100:#f7fafc;--color-gray-200:#e9ecef;--color-gray-300:#dee2e6;--color-gray-400:#cbd5e0;--color-gray-500:#a0aec0;--color-gray-600:#6c757d;--color-gray-700:#495057;--color-gray-800:#333;--color-gray-900:#212529;--color-gray-dark:#1a1a1a;--color-white:#ffffff;--color-black:#000000;--font-family-base:'Poppins',sans-serif;--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--font-size-base:1rem;--font-size-sm:0.875rem;--font-size-lg:1.125rem;--font-size-xl:1.25rem;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--line-height-base:1.5;--line-height-tight:1.25;--line-height-loose:1.8;--spacing-xs:0.25rem;--spacing-sm:0.5rem;--spacing-md:1rem;--spacing-lg:1.5rem;--spacing-xl:2rem;--spacing-2xl:3rem;--spacing-3xl:4rem;--border-width:1px;--border-width-thick:2px;--border-width-thicker:3px;--border-width-lateral:4px;--border-radius-sm:4px;--border-radius-md:8px;--border-radius-lg:12px;--border-radius-xl:16px;--border-color-light:var(--color-gray-200);--border-color-default:var(--color-gray-300);--shadow-xs:0 1px 2px rgba(0,0,0,0.05);--shadow-sm:0 2px 4px rgba(0,0,0,0.1);--shadow-md:0 4px 12px rgba(0,0,0,0.15);--shadow-lg:0 8px 24px rgba(0,0,0,0.2);--shadow-xl:0 12px 32px rgba(0,0,0,0.25);--shadow-2xl:0 20px 60px rgba(0,0,0,0.3);--shadow-navbar:0 2px 4px rgba(0,0,0,0.15);--shadow-navbar-scrolled:0 4px 12px rgba(0,0,0,0.25);--shadow-dropdown:0 8px 24px rgba(0,0,0,0.12);--shadow-cta:0 8px 24px rgba(255,133,0,0.3);--shadow-cta-hover:0 12px 32px rgba(255,133,0,0.4);--shadow-button:0 4px 12px rgba(255,107,53,0.3);--shadow-related-posts:0 12px 32px rgba(26,115,232,0.15);--shadow-pagination:0 4px 12px rgba(26,115,232,0.3);--transition-fast:0.15s ease;--transition-base:0.3s ease;--transition-slow:0.5s ease;--transition-cubic:cubic-bezier(0.4,0,0.2,1);--z-dropdown:1000;--z-sticky:1020;--z-navbar:1030;--z-modal-backdrop:1040;--z-modal:1050;--z-popover:1060;--z-tooltip:1070;--gradient-hero:linear-gradient(135deg,var(--color-blue-primary) 0%,var(--color-blue-secondary) 100%);--gradient-cta:linear-gradient(135deg,var(--color-orange-primary) 0%,var(--color-orange-secondary) 100%);--gradient-button-lets-talk:linear-gradient(135deg,var(--color-orange-button) 0%,var(--color-orange-button-end) 100%);--gradient-pagination:linear-gradient(135deg,var(--color-blue-primary) 0%,var(--color-blue-secondary) 100%);--gradient-underline:linear-gradient(90deg,var(--color-cyan-primary) 0%,var(--color-cyan-dark) 100%);--gradient-border-related:linear-gradient(180deg,var(--color-blue-primary) 0%,var(--color-blue-light) 100%);--opacity-disabled:0.5;--opacity-hover:0.8;--opacity-backdrop:0.5;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--breakpoint-xxl:1400px}
|
||||
821
Assets/Css/critical-bootstrap.css
Normal file
821
Assets/Css/critical-bootstrap.css
Normal file
@@ -0,0 +1,821 @@
|
||||
/**
|
||||
* Critical Bootstrap CSS Subset (TIPO 2)
|
||||
*
|
||||
* Contiene SOLO clases de Bootstrap 5.3.2 usadas en componentes above-the-fold.
|
||||
* NO contiene CSS personalizado (ese va en critical-custom-temp.css - TIPO 3).
|
||||
*
|
||||
* Componentes Bootstrap incluidos:
|
||||
* - Fonts (@font-face Poppins)
|
||||
* - Variables CSS (:root)
|
||||
* - Resets (box-sizing, body)
|
||||
* - Container system
|
||||
* - Grid system (row, col-*)
|
||||
* - Flexbox utilities (d-flex, justify-content-*, align-items-*)
|
||||
* - Spacing utilities (m-*, p-*)
|
||||
* - Text utilities
|
||||
* - Navbar component
|
||||
* - Collapse/Dropdown components
|
||||
* - Button component
|
||||
* - Alert component
|
||||
* - Typography base (h1-h6, p)
|
||||
* - Responsive breakpoints
|
||||
*
|
||||
* Hook: wp_head priority 0
|
||||
* Output: <style id="roi-critical-bootstrap">
|
||||
*
|
||||
* @version 5.3.2-subset
|
||||
* @see Inc/enqueue-scripts.php - Bootstrap diferido
|
||||
* @see Shared/Infrastructure/Services/CriticalBootstrapService.php
|
||||
* @see Assets/Css/critical-custom-temp.css - CSS personalizado (TIPO 3)
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
CRITICAL FONTS (Poppins - LCP optimization)
|
||||
|
||||
font-display: swap + preload = fuente carga rapido y siempre se muestra
|
||||
size-adjust: 100.6% = fallback casi identico a Poppins (minimiza CLS)
|
||||
========================================================================== */
|
||||
@font-face {
|
||||
font-family: 'Poppins Fallback';
|
||||
src: local('Arial'), local('Helvetica Neue'), local('sans-serif');
|
||||
size-adjust: 106%;
|
||||
ascent-override: 105%;
|
||||
descent-override: 35%;
|
||||
line-gap-override: 10%;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('/wp-content/themes/roi-theme/Assets/Fonts/poppins-v24-latin-regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('/wp-content/themes/roi-theme/Assets/Fonts/poppins-v24-latin-600.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('/wp-content/themes/roi-theme/Assets/Fonts/poppins-v24-latin-700.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Fonts */
|
||||
--font-primary: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||
--bs-body-font-family: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||
|
||||
/* Theme Colors (críticos para above-the-fold) */
|
||||
--color-navy-dark: #0E2337;
|
||||
--color-navy-medium: #1e3a5f;
|
||||
--color-orange-primary: #FF8600;
|
||||
--color-orange-hover: #e67a00;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-white: #fff;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
|
||||
/* Spacing */
|
||||
--bs-gutter-x: 1.5rem;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
BOX SIZING & RESETS (Bootstrap Reboot crítico)
|
||||
========================================================================== */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif);
|
||||
font-size: var(--bs-body-font-size, 1rem);
|
||||
font-weight: var(--bs-body-font-weight, 400);
|
||||
line-height: var(--bs-body-line-height, 1.5);
|
||||
color: var(--bs-body-color, #212529);
|
||||
background-color: var(--bs-body-bg, #fff);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--bs-link-color, #0d6efd);
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: var(--bs-link-hover-color, #0a58ca);
|
||||
}
|
||||
|
||||
img, svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
CONTAINER (Layout crítico)
|
||||
========================================================================== */
|
||||
.container,
|
||||
.container-fluid {
|
||||
--bs-gutter-x: 1.5rem;
|
||||
--bs-gutter-y: 0;
|
||||
width: 100%;
|
||||
padding-right: calc(var(--bs-gutter-x) * 0.5);
|
||||
padding-left: calc(var(--bs-gutter-x) * 0.5);
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 576px) {
|
||||
.container { max-width: 540px; }
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.container { max-width: 720px; }
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.container { max-width: 960px; }
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container { max-width: 1140px; }
|
||||
}
|
||||
@media (min-width: 1400px) {
|
||||
.container { max-width: 1320px; }
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
GRID SYSTEM (Layout crítico - Previene CLS)
|
||||
========================================================================== */
|
||||
.row {
|
||||
--bs-gutter-x: 1.5rem;
|
||||
--bs-gutter-y: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: calc(-1 * var(--bs-gutter-y));
|
||||
margin-right: calc(-0.5 * var(--bs-gutter-x));
|
||||
margin-left: calc(-0.5 * var(--bs-gutter-x));
|
||||
}
|
||||
.row > * {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding-right: calc(var(--bs-gutter-x) * 0.5);
|
||||
padding-left: calc(var(--bs-gutter-x) * 0.5);
|
||||
margin-top: var(--bs-gutter-y);
|
||||
}
|
||||
|
||||
.col { flex: 1 0 0%; }
|
||||
.col-auto { flex: 0 0 auto; width: auto; }
|
||||
.col-1 { flex: 0 0 auto; width: 8.33333333%; }
|
||||
.col-2 { flex: 0 0 auto; width: 16.66666667%; }
|
||||
.col-3 { flex: 0 0 auto; width: 25%; }
|
||||
.col-4 { flex: 0 0 auto; width: 33.33333333%; }
|
||||
.col-5 { flex: 0 0 auto; width: 41.66666667%; }
|
||||
.col-6 { flex: 0 0 auto; width: 50%; }
|
||||
.col-7 { flex: 0 0 auto; width: 58.33333333%; }
|
||||
.col-8 { flex: 0 0 auto; width: 66.66666667%; }
|
||||
.col-9 { flex: 0 0 auto; width: 75%; }
|
||||
.col-10 { flex: 0 0 auto; width: 83.33333333%; }
|
||||
.col-11 { flex: 0 0 auto; width: 91.66666667%; }
|
||||
.col-12 { flex: 0 0 auto; width: 100%; }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.col-md-1 { flex: 0 0 auto; width: 8.33333333%; }
|
||||
.col-md-2 { flex: 0 0 auto; width: 16.66666667%; }
|
||||
.col-md-3 { flex: 0 0 auto; width: 25%; }
|
||||
.col-md-4 { flex: 0 0 auto; width: 33.33333333%; }
|
||||
.col-md-5 { flex: 0 0 auto; width: 41.66666667%; }
|
||||
.col-md-6 { flex: 0 0 auto; width: 50%; }
|
||||
.col-md-7 { flex: 0 0 auto; width: 58.33333333%; }
|
||||
.col-md-8 { flex: 0 0 auto; width: 66.66666667%; }
|
||||
.col-md-9 { flex: 0 0 auto; width: 75%; }
|
||||
.col-md-10 { flex: 0 0 auto; width: 83.33333333%; }
|
||||
.col-md-11 { flex: 0 0 auto; width: 91.66666667%; }
|
||||
.col-md-12 { flex: 0 0 auto; width: 100%; }
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.col-lg-1 { flex: 0 0 auto; width: 8.33333333%; }
|
||||
.col-lg-2 { flex: 0 0 auto; width: 16.66666667%; }
|
||||
.col-lg-3 { flex: 0 0 auto; width: 25%; }
|
||||
.col-lg-4 { flex: 0 0 auto; width: 33.33333333%; }
|
||||
.col-lg-5 { flex: 0 0 auto; width: 41.66666667%; }
|
||||
.col-lg-6 { flex: 0 0 auto; width: 50%; }
|
||||
.col-lg-7 { flex: 0 0 auto; width: 58.33333333%; }
|
||||
.col-lg-8 { flex: 0 0 auto; width: 66.66666667%; }
|
||||
.col-lg-9 { flex: 0 0 auto; width: 75%; }
|
||||
.col-lg-10 { flex: 0 0 auto; width: 83.33333333%; }
|
||||
.col-lg-11 { flex: 0 0 auto; width: 91.66666667%; }
|
||||
.col-lg-12 { flex: 0 0 auto; width: 100%; }
|
||||
}
|
||||
|
||||
/* Gutter utilities */
|
||||
.g-0, .gx-0 { --bs-gutter-x: 0; }
|
||||
.g-0, .gy-0 { --bs-gutter-y: 0; }
|
||||
.g-3, .gx-3 { --bs-gutter-x: 1rem; }
|
||||
.g-3, .gy-3 { --bs-gutter-y: 1rem; }
|
||||
|
||||
/* ==========================================================================
|
||||
FLEXBOX UTILITIES (Layout crítico)
|
||||
========================================================================== */
|
||||
.d-flex {
|
||||
display: flex !important;
|
||||
}
|
||||
.d-none {
|
||||
display: none !important;
|
||||
}
|
||||
.d-block {
|
||||
display: block !important;
|
||||
}
|
||||
.d-inline-block {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
/* Responsive Display Utilities - Previene CLS en TopNotificationBar */
|
||||
@media (min-width: 992px) {
|
||||
.d-lg-none {
|
||||
display: none !important;
|
||||
}
|
||||
.d-lg-block {
|
||||
display: block !important;
|
||||
}
|
||||
.d-lg-flex {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap !important;
|
||||
}
|
||||
.flex-column {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.justify-content-center {
|
||||
justify-content: center !important;
|
||||
}
|
||||
.justify-content-between {
|
||||
justify-content: space-between !important;
|
||||
}
|
||||
.justify-content-start {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
.justify-content-end {
|
||||
justify-content: flex-end !important;
|
||||
}
|
||||
|
||||
.align-items-center {
|
||||
align-items: center !important;
|
||||
}
|
||||
.align-items-start {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
.align-items-end {
|
||||
align-items: flex-end !important;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
.gap-3 {
|
||||
gap: 1rem !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
SPACING UTILITIES (Margin/Padding críticos)
|
||||
========================================================================== */
|
||||
.m-0 { margin: 0 !important; }
|
||||
.m-auto { margin: auto !important; }
|
||||
|
||||
.mb-0 { margin-bottom: 0 !important; }
|
||||
.mb-1 { margin-bottom: 0.25rem !important; }
|
||||
.mb-2 { margin-bottom: 0.5rem !important; }
|
||||
.mb-3 { margin-bottom: 1rem !important; }
|
||||
.mb-4 { margin-bottom: 1.5rem !important; }
|
||||
|
||||
.mt-0 { margin-top: 0 !important; }
|
||||
.mt-2 { margin-top: 0.5rem !important; }
|
||||
.mt-3 { margin-top: 1rem !important; }
|
||||
|
||||
.me-1 { margin-right: 0.25rem !important; }
|
||||
.me-2 { margin-right: 0.5rem !important; }
|
||||
.me-3 { margin-right: 1rem !important; }
|
||||
|
||||
.ms-2 { margin-left: 0.5rem !important; }
|
||||
.ms-3 { margin-left: 1rem !important; }
|
||||
|
||||
.mx-auto { margin-left: auto !important; margin-right: auto !important; }
|
||||
|
||||
.p-0 { padding: 0 !important; }
|
||||
.p-2 { padding: 0.5rem !important; }
|
||||
.p-3 { padding: 1rem !important; }
|
||||
|
||||
.py-2 { padding-top: 0.5rem !important; padding-bottom: 0.5rem !important; }
|
||||
.py-3 { padding-top: 1rem !important; padding-bottom: 1rem !important; }
|
||||
.py-4 { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; }
|
||||
|
||||
.px-2 { padding-left: 0.5rem !important; padding-right: 0.5rem !important; }
|
||||
.px-3 { padding-left: 1rem !important; padding-right: 1rem !important; }
|
||||
.px-4 { padding-left: 1.5rem !important; padding-right: 1.5rem !important; }
|
||||
|
||||
/* ==========================================================================
|
||||
SIZING UTILITIES (Width/Height críticos)
|
||||
========================================================================== */
|
||||
.w-100 { width: 100% !important; }
|
||||
.w-auto { width: auto !important; }
|
||||
.h-100 { height: 100% !important; }
|
||||
.h-auto { height: auto !important; }
|
||||
|
||||
/* ==========================================================================
|
||||
TEXT UTILITIES (Críticos para layout)
|
||||
========================================================================== */
|
||||
.text-center { text-align: center !important; }
|
||||
.text-start { text-align: left !important; }
|
||||
.text-end { text-align: right !important; }
|
||||
.text-white { color: #fff !important; }
|
||||
.text-muted { color: var(--bs-secondary-color, #6c757d) !important; }
|
||||
|
||||
.fw-normal { font-weight: 400 !important; }
|
||||
.fw-medium { font-weight: 500 !important; }
|
||||
.fw-semibold { font-weight: 600 !important; }
|
||||
.fw-bold { font-weight: 700 !important; }
|
||||
|
||||
.fs-5 { font-size: 1.25rem !important; }
|
||||
.fs-6 { font-size: 1rem !important; }
|
||||
|
||||
.small { font-size: 0.875em !important; }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.text-md-start { text-align: left !important; }
|
||||
.text-md-center { text-align: center !important; }
|
||||
.text-md-end { text-align: right !important; }
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
NAVBAR COMPONENT (Crítico - Above the fold)
|
||||
========================================================================== */
|
||||
.navbar {
|
||||
--bs-navbar-padding-x: 0;
|
||||
--bs-navbar-padding-y: 0.5rem;
|
||||
--bs-navbar-color: rgba(255, 255, 255, 0.55);
|
||||
--bs-navbar-hover-color: rgba(255, 255, 255, 0.75);
|
||||
--bs-navbar-disabled-color: rgba(255, 255, 255, 0.25);
|
||||
--bs-navbar-active-color: #fff;
|
||||
--bs-navbar-brand-padding-y: 0.3125rem;
|
||||
--bs-navbar-brand-margin-end: 1rem;
|
||||
--bs-navbar-brand-font-size: 1.25rem;
|
||||
--bs-navbar-brand-color: #fff;
|
||||
--bs-navbar-brand-hover-color: #fff;
|
||||
--bs-navbar-nav-link-padding-x: 0.5rem;
|
||||
--bs-navbar-toggler-padding-y: 0.25rem;
|
||||
--bs-navbar-toggler-padding-x: 0.75rem;
|
||||
--bs-navbar-toggler-font-size: 1.25rem;
|
||||
--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||
--bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1);
|
||||
--bs-navbar-toggler-border-radius: var(--bs-border-radius, 0.375rem);
|
||||
--bs-navbar-toggler-focus-width: 0.25rem;
|
||||
--bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out;
|
||||
/* position: controlado por CriticalCSSService según sticky_enabled */
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--bs-navbar-padding-y) var(--bs-navbar-padding-x);
|
||||
}
|
||||
|
||||
.navbar > .container,
|
||||
.navbar > .container-fluid {
|
||||
display: flex;
|
||||
flex-wrap: inherit;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
padding-top: var(--bs-navbar-brand-padding-y);
|
||||
padding-bottom: var(--bs-navbar-brand-padding-y);
|
||||
margin-right: var(--bs-navbar-brand-margin-end);
|
||||
font-size: var(--bs-navbar-brand-font-size);
|
||||
color: var(--bs-navbar-brand-color);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.navbar-brand:hover,
|
||||
.navbar-brand:focus {
|
||||
color: var(--bs-navbar-brand-hover-color);
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
--bs-nav-link-padding-x: 0;
|
||||
--bs-nav-link-padding-y: 0.5rem;
|
||||
--bs-nav-link-color: var(--bs-navbar-color);
|
||||
--bs-nav-link-hover-color: var(--bs-navbar-hover-color);
|
||||
--bs-nav-link-disabled-color: var(--bs-navbar-disabled-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: 0;
|
||||
margin-bottom: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.navbar-nav .nav-link {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
color: var(--bs-nav-link-color);
|
||||
}
|
||||
.navbar-nav .nav-link:hover,
|
||||
.navbar-nav .nav-link:focus {
|
||||
color: var(--bs-nav-link-hover-color);
|
||||
}
|
||||
.navbar-nav .nav-link.active {
|
||||
color: var(--bs-navbar-active-color);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: block;
|
||||
padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);
|
||||
font-size: var(--bs-nav-link-font-size);
|
||||
font-weight: var(--bs-nav-link-font-weight);
|
||||
color: var(--bs-nav-link-color);
|
||||
text-decoration: none;
|
||||
background: 0 0;
|
||||
border: 0;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.navbar-toggler {
|
||||
padding: var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);
|
||||
font-size: var(--bs-navbar-toggler-font-size);
|
||||
line-height: 1;
|
||||
color: var(--bs-navbar-color);
|
||||
background-color: transparent;
|
||||
border: var(--bs-border-width, 1px) solid var(--bs-navbar-toggler-border-color);
|
||||
border-radius: var(--bs-navbar-toggler-border-radius);
|
||||
transition: var(--bs-navbar-toggler-transition);
|
||||
}
|
||||
.navbar-toggler:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
.navbar-toggler:focus {
|
||||
text-decoration: none;
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 var(--bs-navbar-toggler-focus-width);
|
||||
}
|
||||
|
||||
.navbar-toggler-icon {
|
||||
display: inline-block;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
vertical-align: middle;
|
||||
background-image: var(--bs-navbar-toggler-icon-bg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
.navbar-dark,
|
||||
.navbar[data-bs-theme="dark"] {
|
||||
--bs-navbar-color: rgba(255, 255, 255, 0.55);
|
||||
--bs-navbar-hover-color: rgba(255, 255, 255, 0.75);
|
||||
--bs-navbar-disabled-color: rgba(255, 255, 255, 0.25);
|
||||
--bs-navbar-active-color: #fff;
|
||||
--bs-navbar-brand-color: #fff;
|
||||
--bs-navbar-brand-hover-color: #fff;
|
||||
--bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1);
|
||||
--bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
COLLAPSE COMPONENT (Navbar mobile)
|
||||
========================================================================== */
|
||||
.collapse:not(.show) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-collapse {
|
||||
flex-basis: 100%;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
DROPDOWN COMPONENT (Navbar submenus)
|
||||
========================================================================== */
|
||||
.dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dropdown-toggle::after {
|
||||
display: inline-block;
|
||||
margin-left: 0.255em;
|
||||
vertical-align: 0.255em;
|
||||
content: "";
|
||||
border-top: 0.3em solid;
|
||||
border-right: 0.3em solid transparent;
|
||||
border-bottom: 0;
|
||||
border-left: 0.3em solid transparent;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
--bs-dropdown-zindex: 1000;
|
||||
--bs-dropdown-min-width: 10rem;
|
||||
--bs-dropdown-padding-x: 0;
|
||||
--bs-dropdown-padding-y: 0.5rem;
|
||||
--bs-dropdown-spacer: 0.125rem;
|
||||
--bs-dropdown-font-size: 1rem;
|
||||
--bs-dropdown-color: var(--bs-body-color, #212529);
|
||||
--bs-dropdown-bg: var(--bs-body-bg, #fff);
|
||||
--bs-dropdown-border-color: var(--bs-border-color-translucent, rgba(0,0,0,.175));
|
||||
--bs-dropdown-border-radius: var(--bs-border-radius, 0.375rem);
|
||||
--bs-dropdown-border-width: var(--bs-border-width, 1px);
|
||||
--bs-dropdown-inner-border-radius: calc(var(--bs-border-radius, 0.375rem) - var(--bs-border-width, 1px));
|
||||
--bs-dropdown-divider-bg: var(--bs-border-color-translucent, rgba(0,0,0,.175));
|
||||
--bs-dropdown-divider-margin-y: 0.5rem;
|
||||
--bs-dropdown-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-dropdown-link-color: var(--bs-body-color, #212529);
|
||||
--bs-dropdown-link-hover-color: var(--bs-body-color, #212529);
|
||||
--bs-dropdown-link-hover-bg: var(--bs-tertiary-bg, #f8f9fa);
|
||||
--bs-dropdown-link-active-color: #fff;
|
||||
--bs-dropdown-link-active-bg: #0d6efd;
|
||||
--bs-dropdown-link-disabled-color: var(--bs-tertiary-color, #adb5bd);
|
||||
--bs-dropdown-item-padding-x: 1rem;
|
||||
--bs-dropdown-item-padding-y: 0.25rem;
|
||||
--bs-dropdown-header-color: #6c757d;
|
||||
--bs-dropdown-header-padding-x: 1rem;
|
||||
--bs-dropdown-header-padding-y: 0.5rem;
|
||||
position: absolute;
|
||||
z-index: var(--bs-dropdown-zindex);
|
||||
display: none;
|
||||
min-width: var(--bs-dropdown-min-width);
|
||||
padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);
|
||||
margin: 0;
|
||||
font-size: var(--bs-dropdown-font-size);
|
||||
color: var(--bs-dropdown-color);
|
||||
text-align: left;
|
||||
list-style: none;
|
||||
background-color: var(--bs-dropdown-bg);
|
||||
background-clip: padding-box;
|
||||
border: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);
|
||||
border-radius: var(--bs-dropdown-border-radius);
|
||||
}
|
||||
|
||||
.dropdown-menu.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);
|
||||
clear: both;
|
||||
font-weight: 400;
|
||||
color: var(--bs-dropdown-link-color);
|
||||
text-align: inherit;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
.dropdown-item:hover,
|
||||
.dropdown-item:focus {
|
||||
color: var(--bs-dropdown-link-hover-color);
|
||||
background-color: var(--bs-dropdown-link-hover-bg);
|
||||
}
|
||||
.dropdown-item.active,
|
||||
.dropdown-item:active {
|
||||
color: var(--bs-dropdown-link-active-color);
|
||||
text-decoration: none;
|
||||
background-color: var(--bs-dropdown-link-active-bg);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
TEXT UTILITIES
|
||||
========================================================================== */
|
||||
.text-decoration-underline {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
.text-decoration-none {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
IMAGE UTILITIES
|
||||
========================================================================== */
|
||||
.img-fluid {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
ALERT COMPONENT (Above-the-fold notifications)
|
||||
========================================================================== */
|
||||
.alert {
|
||||
--bs-alert-padding-x: 1rem;
|
||||
--bs-alert-padding-y: 1rem;
|
||||
--bs-alert-margin-bottom: 1rem;
|
||||
--bs-alert-border-radius: 0.375rem;
|
||||
position: relative;
|
||||
padding: var(--bs-alert-padding-y) var(--bs-alert-padding-x);
|
||||
margin-bottom: var(--bs-alert-margin-bottom);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--bs-alert-border-radius);
|
||||
}
|
||||
.alert-warning {
|
||||
--bs-alert-color: #664d03;
|
||||
--bs-alert-bg: #fff3cd;
|
||||
--bs-alert-border-color: #ffecb5;
|
||||
color: var(--bs-alert-color);
|
||||
background-color: var(--bs-alert-bg);
|
||||
border-color: var(--bs-alert-border-color);
|
||||
}
|
||||
.alert-info {
|
||||
--bs-alert-color: #055160;
|
||||
--bs-alert-bg: #cff4fc;
|
||||
--bs-alert-border-color: #b6effb;
|
||||
color: var(--bs-alert-color);
|
||||
background-color: var(--bs-alert-bg);
|
||||
border-color: var(--bs-alert-border-color);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
BUTTON COMPONENT (Above-the-fold - Navbar CTA)
|
||||
========================================================================== */
|
||||
.btn {
|
||||
--bs-btn-padding-x: 0.75rem;
|
||||
--bs-btn-padding-y: 0.375rem;
|
||||
--bs-btn-font-size: 1rem;
|
||||
--bs-btn-font-weight: 400;
|
||||
--bs-btn-line-height: 1.5;
|
||||
--bs-btn-color: var(--bs-body-color);
|
||||
--bs-btn-bg: transparent;
|
||||
--bs-btn-border-width: var(--bs-border-width, 1px);
|
||||
--bs-btn-border-color: transparent;
|
||||
--bs-btn-border-radius: var(--bs-border-radius, 0.375rem);
|
||||
--bs-btn-hover-border-color: transparent;
|
||||
--bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
--bs-btn-disabled-opacity: 0.65;
|
||||
--bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), 0.5);
|
||||
display: inline-block;
|
||||
padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x);
|
||||
font-family: var(--bs-btn-font-family);
|
||||
font-size: var(--bs-btn-font-size);
|
||||
font-weight: var(--bs-btn-font-weight);
|
||||
line-height: var(--bs-btn-line-height);
|
||||
color: var(--bs-btn-color);
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border: var(--bs-btn-border-width) solid var(--bs-btn-border-color);
|
||||
border-radius: var(--bs-btn-border-radius);
|
||||
background-color: var(--bs-btn-bg);
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
.btn:hover {
|
||||
color: var(--bs-btn-hover-color);
|
||||
background-color: var(--bs-btn-hover-bg);
|
||||
border-color: var(--bs-btn-hover-border-color);
|
||||
}
|
||||
.btn:focus-visible {
|
||||
color: var(--bs-btn-hover-color);
|
||||
background-color: var(--bs-btn-hover-bg);
|
||||
border-color: var(--bs-btn-hover-border-color);
|
||||
outline: 0;
|
||||
box-shadow: var(--bs-btn-focus-box-shadow);
|
||||
}
|
||||
.btn:disabled, .btn.disabled {
|
||||
pointer-events: none;
|
||||
opacity: var(--bs-btn-disabled-opacity);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
BUTTON CLOSE (Dismiss notification)
|
||||
========================================================================== */
|
||||
.btn-close {
|
||||
--bs-btn-close-color: #000;
|
||||
--bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");
|
||||
--bs-btn-close-opacity: 0.5;
|
||||
--bs-btn-close-hover-opacity: 0.75;
|
||||
--bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
--bs-btn-close-focus-opacity: 1;
|
||||
--bs-btn-close-disabled-opacity: 0.25;
|
||||
--bs-btn-close-white-filter: invert(1) grayscale(100%) brightness(200%);
|
||||
box-sizing: content-box;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
padding: 0.25em 0.25em;
|
||||
color: var(--bs-btn-close-color);
|
||||
background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat;
|
||||
border: 0;
|
||||
border-radius: 0.375rem;
|
||||
opacity: var(--bs-btn-close-opacity);
|
||||
}
|
||||
.btn-close:hover {
|
||||
color: var(--bs-btn-close-color);
|
||||
text-decoration: none;
|
||||
opacity: var(--bs-btn-close-hover-opacity);
|
||||
}
|
||||
.btn-close:focus {
|
||||
outline: 0;
|
||||
box-shadow: var(--bs-btn-close-focus-shadow);
|
||||
opacity: var(--bs-btn-close-focus-opacity);
|
||||
}
|
||||
|
||||
.btn-close-white {
|
||||
filter: var(--bs-btn-close-white-filter);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
RESPONSIVE BREAKPOINTS (navbar-expand-lg)
|
||||
========================================================================== */
|
||||
@media (min-width: 992px) {
|
||||
.navbar-expand-lg {
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.navbar-expand-lg .navbar-nav {
|
||||
flex-direction: row;
|
||||
}
|
||||
.navbar-expand-lg .navbar-nav .dropdown-menu {
|
||||
position: absolute;
|
||||
}
|
||||
.navbar-expand-lg .navbar-nav .nav-link {
|
||||
padding-right: var(--bs-navbar-nav-link-padding-x);
|
||||
padding-left: var(--bs-navbar-nav-link-padding-x);
|
||||
}
|
||||
.navbar-expand-lg .navbar-collapse {
|
||||
display: flex !important;
|
||||
flex-basis: auto;
|
||||
}
|
||||
.navbar-expand-lg .navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.d-lg-block { display: block !important; }
|
||||
.d-lg-none { display: none !important; }
|
||||
.mb-lg-0 { margin-bottom: 0 !important; }
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.navbar-expand-lg > .container,
|
||||
.navbar-expand-lg > .container-fluid {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
RESPONSIVE DISPLAY UTILITIES (md breakpoint)
|
||||
========================================================================== */
|
||||
@media (min-width: 768px) {
|
||||
.d-md-block { display: block !important; }
|
||||
.d-md-none { display: none !important; }
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
TYPOGRAPHY BASE (Critical)
|
||||
========================================================================== */
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
h1 { font-size: calc(1.375rem + 1.5vw); }
|
||||
h2 { font-size: calc(1.325rem + 0.9vw); }
|
||||
h3 { font-size: calc(1.3rem + 0.6vw); }
|
||||
h4 { font-size: calc(1.275rem + 0.3vw); }
|
||||
h5 { font-size: 1.25rem; }
|
||||
h6 { font-size: 1rem; }
|
||||
@media (min-width: 1200px) {
|
||||
h1 { font-size: 2.5rem; }
|
||||
h2 { font-size: 2rem; }
|
||||
h3 { font-size: 1.75rem; }
|
||||
h4 { font-size: 1.5rem; }
|
||||
}
|
||||
|
||||
1
Assets/Css/css-global-accessibility.min.css
vendored
Normal file
1
Assets/Css/css-global-accessibility.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -24,11 +24,12 @@
|
||||
--font-system: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
|
||||
|
||||
/* Fuente primaria - Poppins según template y documentación */
|
||||
--font-primary: 'Poppins', sans-serif;
|
||||
/* Fuente primaria - Poppins con fallback ajustado (Fase 4.3 PageSpeed)
|
||||
'Poppins Fallback' tiene size-adjust para reducir CLS durante font swap */
|
||||
--font-primary: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||
|
||||
/* Fuente para encabezados - Poppins según template */
|
||||
--font-headings: 'Poppins', sans-serif;
|
||||
/* Fuente para encabezados - Poppins con fallback ajustado */
|
||||
--font-headings: 'Poppins', 'Poppins Fallback', sans-serif;
|
||||
|
||||
/* Fuente para código (monospace) */
|
||||
--font-mono: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono',
|
||||
@@ -56,11 +57,30 @@
|
||||
Pesos incluidos: 400, 500, 600, 700
|
||||
Formato: WOFF2 (mejor compresión)
|
||||
|
||||
Fase 4.3 PageSpeed: Fallback con size-adjust para reducir CLS
|
||||
- size-adjust: 100.6% ajustado para coincidir mejor con Poppins
|
||||
- font-display: swap + preload = carga rapida sin salto visual
|
||||
- Preload en CriticalCSSInjector P:-2 acelera descarga de fuentes
|
||||
|
||||
NOTA: El valor 100.6% fue calibrado empiricamente.
|
||||
- 106% causaba un salto visual notable (navbar se "achicaba")
|
||||
- 100.6% minimiza el CLS manteniendo legibilidad del fallback
|
||||
|
||||
============================================ */
|
||||
|
||||
/* Fallback font con metricas ajustadas para Poppins */
|
||||
@font-face {
|
||||
font-family: 'Poppins Fallback';
|
||||
src: local('Arial'), local('Helvetica Neue'), local('Helvetica'), local('sans-serif');
|
||||
size-adjust: 106%;
|
||||
ascent-override: 105%;
|
||||
descent-override: 35%;
|
||||
line-gap-override: 10%;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../fonts/poppins-v24-latin-regular.woff2') format('woff2');
|
||||
src: url('../Fonts/poppins-v24-latin-regular.woff2') format('woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -68,7 +88,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../fonts/poppins-v24-latin-500.woff2') format('woff2');
|
||||
src: url('../Fonts/poppins-v24-latin-500.woff2') format('woff2');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -76,7 +96,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../fonts/poppins-v24-latin-600.woff2') format('woff2');
|
||||
src: url('../Fonts/poppins-v24-latin-600.woff2') format('woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -84,7 +104,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('../fonts/poppins-v24-latin-700.woff2') format('woff2');
|
||||
src: url('../Fonts/poppins-v24-latin-700.woff2') format('woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -28,6 +28,10 @@
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
border-spacing: 0;
|
||||
/* CRITICO: table-layout fixed previene CLS
|
||||
El navegador calcula anchos basado en primera fila,
|
||||
no recalcula cuando carga más contenido */
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
/* Eliminar todos los bordes */
|
||||
@@ -153,12 +157,13 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Columna 3: Unidad - centrada */
|
||||
/* Columna 3: Unidad - centrada
|
||||
Fase 4.4 Accesibilidad: Color #495057 (ratio 7.0:1) en lugar de #6c757d */
|
||||
.analisis table td:nth-child(3),
|
||||
.analisis table td.c3,
|
||||
.desglose table td.c3 {
|
||||
text-align: center !important;
|
||||
color: #6c757d;
|
||||
color: #495057;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
@@ -214,16 +219,17 @@
|
||||
/* ========================================
|
||||
FILAS DE SUBTOTALES
|
||||
(Suma de Material, Suma de Mano de Obra, etc)
|
||||
Fase 4.4 Accesibilidad: Color oscuro para contraste WCAG AA (4.5:1)
|
||||
======================================== */
|
||||
.analisis table tr.subtotal-row,
|
||||
.desglose table tr.subtotal-row {
|
||||
background-color: rgba(255, 133, 0, 0.1) !important;
|
||||
background-color: rgba(255, 133, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.analisis table tr.subtotal-row td,
|
||||
.desglose table tr.subtotal-row td {
|
||||
font-weight: 700;
|
||||
color: var(--color-orange-primary);
|
||||
color: #1e3a5f;
|
||||
padding: 0.875rem 1rem;
|
||||
border: none !important;
|
||||
}
|
||||
@@ -235,7 +241,7 @@
|
||||
.analisis table tr.subtotal-row td.c6,
|
||||
.analisis table tr.subtotal-row td:nth-child(6) {
|
||||
font-size: 1.05rem;
|
||||
color: var(--color-orange-primary);
|
||||
color: #1e3a5f;
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
@@ -341,6 +341,11 @@ img {
|
||||
.content-wrapper {
|
||||
grid-template-columns: 2fr 1fr;
|
||||
}
|
||||
|
||||
/* Full width when no sidebar */
|
||||
.no-sidebar .content-wrapper {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
#primary {
|
||||
1
Assets/Css/style.min.css
vendored
Normal file
1
Assets/Css/style.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -182,12 +182,74 @@
|
||||
}, CONFIG.timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activa slots de AdSense insertados dinamicamente
|
||||
* Escucha el evento 'roi-adsense-activate' disparado por otros scripts
|
||||
*/
|
||||
function setupDynamicAdsListener() {
|
||||
window.addEventListener('roi-adsense-activate', function() {
|
||||
debugLog('Evento roi-adsense-activate recibido');
|
||||
|
||||
// Si AdSense aun no ha cargado, forzar carga ahora
|
||||
if (!adsenseLoaded) {
|
||||
debugLog('AdSense no cargado, forzando carga...');
|
||||
loadAdSense();
|
||||
return;
|
||||
}
|
||||
|
||||
// AdSense ya cargado - activar nuevos slots
|
||||
debugLog('Activando nuevos slots dinamicos...');
|
||||
activateDynamicSlots();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Activa slots de AdSense que fueron insertados despues de la carga inicial
|
||||
*/
|
||||
function activateDynamicSlots() {
|
||||
// Buscar scripts de push que aun no han sido ejecutados
|
||||
var pendingPushScripts = document.querySelectorAll('script[data-adsense-push][type="text/plain"]');
|
||||
|
||||
if (pendingPushScripts.length === 0) {
|
||||
debugLog('No hay slots pendientes por activar');
|
||||
return;
|
||||
}
|
||||
|
||||
debugLog('Activando ' + pendingPushScripts.length + ' slot(s) dinamico(s)');
|
||||
|
||||
// Asegurar que adsbygoogle existe
|
||||
window.adsbygoogle = window.adsbygoogle || [];
|
||||
|
||||
pendingPushScripts.forEach(function(oldScript) {
|
||||
try {
|
||||
// Crear nuevo script ejecutable
|
||||
var newScript = document.createElement('script');
|
||||
newScript.type = 'text/javascript';
|
||||
newScript.innerHTML = oldScript.innerHTML;
|
||||
|
||||
// Reemplazar el placeholder con el script real
|
||||
oldScript.parentNode.replaceChild(newScript, oldScript);
|
||||
} catch (e) {
|
||||
debugLog('Error activando slot: ' + e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializa el cargador retrasado de AdSense
|
||||
*/
|
||||
function init() {
|
||||
// =========================================================================
|
||||
// NUEVO: Siempre configurar listener para ads dinamicos
|
||||
// IMPORTANTE: Esto debe ejecutarse ANTES del early return
|
||||
// porque los ads dinamicos pueden necesitar activarse aunque
|
||||
// el delay global este deshabilitado
|
||||
// =========================================================================
|
||||
setupDynamicAdsListener();
|
||||
debugLog('Listener para ads dinamicos configurado');
|
||||
|
||||
// Verificar si el retardo de AdSense está habilitado
|
||||
if (!window.roidsenseDelayed) {
|
||||
if (!window.roiAdsenseDelayed) {
|
||||
debugLog('Retardo de AdSense no habilitado');
|
||||
return;
|
||||
}
|
||||
135
Assets/Js/lazy-css-loader.js
Normal file
135
Assets/Js/lazy-css-loader.js
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* TIPO 5: Lazy CSS Loader
|
||||
*
|
||||
* Carga CSS no critico despues del evento load usando:
|
||||
* - requestIdleCallback para CSS de baja prioridad
|
||||
* - Event listeners para CSS condicional
|
||||
*
|
||||
* @package ROITheme
|
||||
* @since 1.0.20
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Configuracion de CSS lazy (inyectada desde PHP)
|
||||
var config = window.roiLazyCSSConfig || {
|
||||
baseUrl: '',
|
||||
version: '1.0.0',
|
||||
idleTimeout: 2000,
|
||||
cssFiles: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Carga un archivo CSS de forma asincrona
|
||||
*
|
||||
* @param {string} href URL del archivo CSS
|
||||
* @param {string} id ID del elemento link
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function loadCSS(href, id) {
|
||||
// Evitar duplicados
|
||||
if (document.getElementById(id)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
var link = document.createElement('link');
|
||||
link.id = id;
|
||||
link.rel = 'stylesheet';
|
||||
link.href = href;
|
||||
link.onload = resolve;
|
||||
link.onerror = reject;
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Carga CSS cuando el navegador esta idle
|
||||
*
|
||||
* @param {Array} files Lista de archivos a cargar
|
||||
*/
|
||||
function loadOnIdle(files) {
|
||||
var load = function() {
|
||||
files.forEach(function(file) {
|
||||
loadCSS(
|
||||
config.baseUrl + file.path + '?ver=' + config.version,
|
||||
'roi-lazy-' + file.id
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(load, { timeout: config.idleTimeout });
|
||||
} else {
|
||||
// Fallback para Safari
|
||||
setTimeout(load, config.idleTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Carga CSS de print solo cuando se va a imprimir
|
||||
*
|
||||
* @param {Object} file Archivo de print CSS
|
||||
*/
|
||||
function setupPrintCSS(file) {
|
||||
var loaded = false;
|
||||
|
||||
var load = function() {
|
||||
if (loaded) return;
|
||||
loaded = true;
|
||||
loadCSS(
|
||||
config.baseUrl + file.path + '?ver=' + config.version,
|
||||
'roi-lazy-print'
|
||||
);
|
||||
};
|
||||
|
||||
// Evento antes de imprimir
|
||||
window.addEventListener('beforeprint', load);
|
||||
|
||||
// Fallback: detectar Ctrl+P / Cmd+P
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'p') {
|
||||
load();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inicializacion
|
||||
*/
|
||||
function init() {
|
||||
var idleFiles = [];
|
||||
var printFile = null;
|
||||
|
||||
config.cssFiles.forEach(function(file) {
|
||||
switch (file.trigger) {
|
||||
case 'idle':
|
||||
idleFiles.push(file);
|
||||
break;
|
||||
case 'print':
|
||||
printFile = file;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Cargar CSS idle despues de que la pagina este lista
|
||||
if (idleFiles.length > 0) {
|
||||
if (document.readyState === 'complete') {
|
||||
loadOnIdle(idleFiles);
|
||||
} else {
|
||||
window.addEventListener('load', function() {
|
||||
loadOnIdle(idleFiles);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Configurar CSS de print
|
||||
if (printFile) {
|
||||
setupPrintCSS(printFile);
|
||||
}
|
||||
}
|
||||
|
||||
// Iniciar
|
||||
init();
|
||||
|
||||
})();
|
||||
27
Assets/Js/main.js
Normal file
27
Assets/Js/main.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* ROI THEME - MAIN JAVASCRIPT
|
||||
*
|
||||
* OPTIMIZACIÓN TBT Fase 2.3 (2025-11-27):
|
||||
* - Eliminado ~300 líneas de código muerto
|
||||
* - Removido: loadContactModal (modalContainer no existe)
|
||||
* - Removido: initContactForm (contactForm no existe)
|
||||
* - Removido: footerContactForm handler (ID incorrecto)
|
||||
* - Removido: TOC ScrollSpy duplicado (.toc-container no existe)
|
||||
* - Removido: smooth scroll duplicado (Bootstrap lo maneja)
|
||||
* - Removido: console.log de debug
|
||||
*
|
||||
* Código activo: Solo efecto scroll del navbar
|
||||
* Reducción: ~315 líneas → ~25 líneas
|
||||
*/
|
||||
|
||||
// Navbar scroll effect - adds 'scrolled' class when user scrolls
|
||||
window.addEventListener('scroll', function() {
|
||||
const navbar = document.querySelector('.navbar');
|
||||
if (navbar) {
|
||||
if (window.scrollY > 50) {
|
||||
navbar.classList.add('scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('scrolled');
|
||||
}
|
||||
}
|
||||
});
|
||||
4085
Assets/Vendor/Bootstrap/Css/bootstrap-grid.css
vendored
Normal file
4085
Assets/Vendor/Bootstrap/Css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
Assets/Vendor/Bootstrap/Css/bootstrap-grid.css.map
vendored
Normal file
1
Assets/Vendor/Bootstrap/Css/bootstrap-grid.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
Assets/Vendor/Bootstrap/Css/bootstrap-grid.min.css
vendored
Normal file
6
Assets/Vendor/Bootstrap/Css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
Assets/Vendor/Bootstrap/Css/bootstrap-grid.min.css.map
vendored
Normal file
1
Assets/Vendor/Bootstrap/Css/bootstrap-grid.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user