Compare commits
13 Commits
f4b45b7e17
...
pre-fix-cu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78d2ba57b9 | ||
|
|
1c0750604b | ||
|
|
bf304f08fc | ||
|
|
30b30b065b | ||
|
|
b2d5cdfb57 | ||
|
|
b40e5b671a | ||
|
|
61c67acca5 | ||
|
|
ffe6ea8e65 | ||
|
|
36d5cf56de | ||
|
|
23339e3349 | ||
|
|
caa6413bc6 | ||
|
|
ea695010f3 | ||
|
|
e4c79d3f26 |
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]
|
||||
}
|
||||
}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -73,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 -->
|
||||
@@ -105,6 +105,19 @@ final class AdsensePlacementFieldMapper implements FieldMapperInterface
|
||||
'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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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
|
||||
@@ -96,6 +97,47 @@ final class AdsensePlacementFormBuilder
|
||||
$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>';
|
||||
|
||||
@@ -921,4 +963,23 @@ final class AdsensePlacementFormBuilder
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ 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'],
|
||||
'ctaHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'ctaVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
|
||||
@@ -138,6 +138,20 @@ final class CtaBoxSidebarFormBuilder
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'cta');
|
||||
|
||||
// Switch: Ocultar para usuarios logueados (Plan 99.16)
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($componentId, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaHideForLoggedIn" ';
|
||||
$html .= checked($hideForLoggedIn, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaHideForLoggedIn" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-person-lock me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Ocultar para usuarios logueados</strong>';
|
||||
$html .= ' <small class="text-muted d-block">No mostrar a usuarios con sesión iniciada</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ 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'],
|
||||
'ctaLetsTalkHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'ctaLetsTalkVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
|
||||
@@ -162,6 +162,34 @@ final class CtaLetsTalkFormBuilder
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'letsTalk');
|
||||
|
||||
// Switch: CSS Crítico
|
||||
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
||||
$html .= ' <div class="mb-2 mt-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkIsCritical" ';
|
||||
$html .= checked($isCritical, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkIsCritical" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-lightning-charge me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>CSS Crítico</strong>';
|
||||
$html .= ' <small class="text-muted d-block">Inyectar CSS en <head> para optimizar LCP</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Ocultar para usuarios logueados (Plan 99.16)
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($componentId, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaLetsTalkHideForLoggedIn" ';
|
||||
$html .= checked($hideForLoggedIn, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaLetsTalkHideForLoggedIn" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-person-lock me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Ocultar para usuarios logueados</strong>';
|
||||
$html .= ' <small class="text-muted d-block">No mostrar a usuarios con sesión iniciada</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ 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'],
|
||||
'ctaPostHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'ctaPostVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
|
||||
@@ -127,6 +127,20 @@ final class CtaPostFormBuilder
|
||||
$exclusionPartial = new ExclusionFormPartial($this->renderer);
|
||||
$html .= $exclusionPartial->render($componentId, 'ctaPost');
|
||||
|
||||
// Switch: Ocultar para usuarios logueados (Plan 99.16)
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($componentId, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="ctaPostHideForLoggedIn" ';
|
||||
$html .= checked($hideForLoggedIn, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="ctaPostHideForLoggedIn" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-person-lock me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Ocultar para usuarios logueados</strong>';
|
||||
$html .= ' <small class="text-muted d-block">No mostrar a usuarios con sesion iniciada</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ final class TopNotificationBarFieldMapper implements FieldMapperInterface
|
||||
'topBarShowOnMobile' => ['group' => 'visibility', 'attribute' => 'show_on_mobile'],
|
||||
'topBarShowOnDesktop' => ['group' => 'visibility', 'attribute' => 'show_on_desktop'],
|
||||
'topBarIsCritical' => ['group' => 'visibility', 'attribute' => 'is_critical'],
|
||||
'topBarHideForLoggedIn' => ['group' => 'visibility', 'attribute' => 'hide_for_logged_in'],
|
||||
|
||||
// Page Visibility (grupo especial _page_visibility)
|
||||
'topBarVisibilityHome' => ['group' => '_page_visibility', 'attribute' => 'show_on_home'],
|
||||
|
||||
@@ -149,7 +149,7 @@ final class TopNotificationBarFormBuilder
|
||||
|
||||
// Switch: CSS Crítico
|
||||
$isCritical = $this->renderer->getFieldValue($componentId, 'visibility', 'is_critical', true);
|
||||
$html .= ' <div class="mb-0 mt-3">';
|
||||
$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) . '>';
|
||||
@@ -161,6 +161,20 @@ final class TopNotificationBarFormBuilder
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
// Switch: Ocultar para usuarios logueados (Plan 99.16)
|
||||
$hideForLoggedIn = $this->renderer->getFieldValue($componentId, 'visibility', 'hide_for_logged_in', false);
|
||||
$html .= ' <div class="mb-0">';
|
||||
$html .= ' <div class="form-check form-switch">';
|
||||
$html .= ' <input class="form-check-input" type="checkbox" id="topBarHideForLoggedIn" ';
|
||||
$html .= checked($hideForLoggedIn, true, false) . '>';
|
||||
$html .= ' <label class="form-check-label small" for="topBarHideForLoggedIn" style="color: #495057;">';
|
||||
$html .= ' <i class="bi bi-person-lock me-1" style="color: #FF8600;"></i>';
|
||||
$html .= ' <strong>Ocultar para usuarios logueados</strong>';
|
||||
$html .= ' <small class="text-muted d-block">No mostrar a usuarios con sesión iniciada</small>';
|
||||
$html .= ' </label>';
|
||||
$html .= ' </div>';
|
||||
$html .= ' </div>';
|
||||
|
||||
$html .= ' </div>';
|
||||
$html .= '</div>';
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
use ROITheme\Shared\Infrastructure\Services\UserVisibilityHelper;
|
||||
|
||||
/**
|
||||
* Renderiza un slot de anuncio en una ubicacion
|
||||
*
|
||||
@@ -47,16 +50,21 @@ function roi_render_ad_slot(string $location): string
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar si ocultar para usuarios logueados
|
||||
if (roi_should_hide_for_logged_in($settings)) {
|
||||
// Verificar visibilidad por usuario logueado (Plan 99.16)
|
||||
if (!UserVisibilityHelper::shouldShowForUser($settings['visibility'] ?? [])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar exclusiones
|
||||
// Verificar exclusiones legacy (forms group)
|
||||
if (roi_is_ad_excluded($settings)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
|
||||
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Obtener renderer desde DIContainer (DIP compliant)
|
||||
$renderer = $container->getAdsensePlacementRenderer();
|
||||
|
||||
@@ -72,17 +80,17 @@ function roi_render_ad_slot(string $location): string
|
||||
|
||||
/**
|
||||
* Verifica si se deben ocultar anuncios para usuarios logueados
|
||||
*
|
||||
* @deprecated Plan 99.16: Usar UserVisibilityHelper::shouldShowForUser() en su lugar.
|
||||
* Esta función se mantiene para compatibilidad hacia atrás.
|
||||
*
|
||||
* @param array $settings Configuración del componente
|
||||
* @return bool true si se debe ocultar, false si se debe mostrar
|
||||
*/
|
||||
function roi_should_hide_for_logged_in(array $settings): bool
|
||||
{
|
||||
// Si la opcion esta activada Y el usuario esta logueado, ocultar ads
|
||||
$hideForLoggedIn = $settings['visibility']['hide_for_logged_in'] ?? false;
|
||||
|
||||
if ($hideForLoggedIn && is_user_logged_in()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
// Delegar a UserVisibilityHelper (Plan 99.16)
|
||||
return !UserVisibilityHelper::shouldShowForUser($settings['visibility'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,16 +146,21 @@ function roi_render_rail_ads(): string
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar si ocultar para usuarios logueados
|
||||
if (roi_should_hide_for_logged_in($settings)) {
|
||||
// Verificar visibilidad por usuario logueado (Plan 99.16)
|
||||
if (!UserVisibilityHelper::shouldShowForUser($settings['visibility'] ?? [])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar exclusiones
|
||||
// Verificar exclusiones legacy (forms group)
|
||||
if (roi_is_ad_excluded($settings)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
|
||||
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Obtener renderer desde DIContainer (DIP compliant)
|
||||
$renderer = $container->getAdsensePlacementRenderer();
|
||||
|
||||
@@ -188,8 +201,13 @@ function roi_enqueue_adsense_script(): void
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar si ocultar para usuarios logueados
|
||||
if (roi_should_hide_for_logged_in($settings)) {
|
||||
// Verificar visibilidad por usuario logueado (Plan 99.16)
|
||||
if (!UserVisibilityHelper::shouldShowForUser($settings['visibility'] ?? [])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
|
||||
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -241,16 +259,21 @@ function roi_inject_content_ads(string $content): string
|
||||
return $content;
|
||||
}
|
||||
|
||||
// Verificar si ocultar para usuarios logueados
|
||||
if (roi_should_hide_for_logged_in($settings)) {
|
||||
// Verificar visibilidad por usuario logueado (Plan 99.16)
|
||||
if (!UserVisibilityHelper::shouldShowForUser($settings['visibility'] ?? [])) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
// Verificar exclusiones
|
||||
// Verificar exclusiones legacy (forms group)
|
||||
if (roi_is_ad_excluded($settings)) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
|
||||
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$renderer = $container->getAdsensePlacementRenderer();
|
||||
|
||||
// Inyectar anuncio al inicio (post-top)
|
||||
@@ -441,16 +464,21 @@ function roi_render_anchor_ads(): string
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar si ocultar para usuarios logueados
|
||||
if (roi_should_hide_for_logged_in($settings)) {
|
||||
// Verificar visibilidad por usuario logueado (Plan 99.16)
|
||||
if (!UserVisibilityHelper::shouldShowForUser($settings['visibility'] ?? [])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar exclusiones
|
||||
// Verificar exclusiones legacy (forms group)
|
||||
if (roi_is_ad_excluded($settings)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
|
||||
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Obtener renderer desde DIContainer (DIP compliant)
|
||||
$renderer = $container->getAdsensePlacementRenderer();
|
||||
|
||||
@@ -485,16 +513,21 @@ function roi_render_vignette_ad(): string
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar si ocultar para usuarios logueados
|
||||
if (roi_should_hide_for_logged_in($settings)) {
|
||||
// Verificar visibilidad por usuario logueado (Plan 99.16)
|
||||
if (!UserVisibilityHelper::shouldShowForUser($settings['visibility'] ?? [])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar exclusiones
|
||||
// Verificar exclusiones legacy (forms group)
|
||||
if (roi_is_ad_excluded($settings)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
|
||||
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Obtener renderer desde DIContainer (DIP compliant)
|
||||
$renderer = $container->getAdsensePlacementRenderer();
|
||||
|
||||
@@ -551,8 +584,13 @@ function roi_enqueue_anchor_vignette_scripts(): void
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar si ocultar para usuarios logueados
|
||||
if (roi_should_hide_for_logged_in($settings)) {
|
||||
// Verificar visibilidad por usuario logueado (Plan 99.16)
|
||||
if (!UserVisibilityHelper::shouldShowForUser($settings['visibility'] ?? [])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar exclusiones modernas (Plan 99.11: _exclusions, _page_visibility)
|
||||
if (!PageVisibilityHelper::shouldShow('adsense-placement')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -552,30 +552,22 @@ add_filter( 'wp_lazy_loading_enabled', 'roi_enable_image_dimensions' );
|
||||
/**
|
||||
* Optimizar buffer de salida HTML
|
||||
*
|
||||
* Habilita compresión GZIP si está disponible y no está ya habilitada.
|
||||
* DESACTIVADO: Esta función causa conflicto con W3 Total Cache.
|
||||
* Cuando zlib.output_compression está activo, W3TC no puede cachear
|
||||
* las páginas porque recibe "Response is compressed".
|
||||
*
|
||||
* La compresión GZIP la maneja nginx a nivel de servidor.
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @deprecated 1.0.1 Conflicto con W3TC page cache - Issue #XX
|
||||
*/
|
||||
function roi_enable_gzip_compression() {
|
||||
// Solo en frontend
|
||||
if ( is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verificar si GZIP ya está habilitado
|
||||
if ( ! ini_get( 'zlib.output_compression' ) && 'ob_gzhandler' !== ini_get( 'output_handler' ) ) {
|
||||
// Verificar si la extensión está disponible
|
||||
if ( function_exists( 'gzencode' ) && extension_loaded( 'zlib' ) ) {
|
||||
// Verificar headers
|
||||
if ( ! headers_sent() ) {
|
||||
// Habilitar compresión
|
||||
ini_set( 'zlib.output_compression', 'On' );
|
||||
ini_set( 'zlib.output_compression_level', '6' ); // Balance entre compresión y CPU
|
||||
}
|
||||
}
|
||||
}
|
||||
// DESACTIVADO - No hacer nada
|
||||
// La compresión la maneja nginx, no PHP
|
||||
return;
|
||||
}
|
||||
add_action( 'template_redirect', 'roi_enable_gzip_compression', 0 );
|
||||
// DESACTIVADO - Conflicto con W3 Total Cache page cache
|
||||
// add_action( 'template_redirect', 'roi_enable_gzip_compression', 0 );
|
||||
|
||||
/**
|
||||
* ============================================================================
|
||||
|
||||
@@ -7,6 +7,7 @@ use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
use ROITheme\Shared\Infrastructure\Services\UserVisibilityHelper;
|
||||
|
||||
/**
|
||||
* CtaBoxSidebarRenderer - Renderiza caja CTA en sidebar
|
||||
@@ -51,6 +52,11 @@ final class CtaBoxSidebarRenderer implements RendererInterface
|
||||
return '';
|
||||
}
|
||||
|
||||
// Validar visibilidad por usuario logueado
|
||||
if (!UserVisibilityHelper::shouldShowForUser($data['visibility'] ?? [])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$css = $this->generateCSS($data);
|
||||
$html = $this->buildHTML($data);
|
||||
$script = $this->buildScript();
|
||||
|
||||
@@ -7,6 +7,7 @@ use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
use ROITheme\Shared\Infrastructure\Services\UserVisibilityHelper;
|
||||
|
||||
/**
|
||||
* Class CtaLetsTalkRenderer
|
||||
@@ -61,6 +62,11 @@ final class CtaLetsTalkRenderer implements RendererInterface
|
||||
return '';
|
||||
}
|
||||
|
||||
// Validar visibilidad por usuario logueado
|
||||
if (!UserVisibilityHelper::shouldShowForUser($data['visibility'] ?? [])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Generar CSS usando CSSGeneratorService
|
||||
$css = $this->generateCSS($data);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
use ROITheme\Shared\Infrastructure\Services\UserVisibilityHelper;
|
||||
|
||||
/**
|
||||
* CtaPostRenderer - Renderiza CTA promocional debajo del contenido
|
||||
@@ -41,6 +42,11 @@ final class CtaPostRenderer implements RendererInterface
|
||||
return '';
|
||||
}
|
||||
|
||||
// Validar visibilidad por usuario logueado
|
||||
if (!UserVisibilityHelper::shouldShowForUser($data['visibility'] ?? [])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$css = $this->generateCSS($data);
|
||||
$html = $this->buildHTML($data);
|
||||
|
||||
|
||||
@@ -133,6 +133,9 @@ final class HeroRenderer implements RendererInterface
|
||||
'padding' => "{$paddingVertical} 0",
|
||||
'margin-bottom' => $marginBottom,
|
||||
'min-height' => $minHeight,
|
||||
'display' => 'flex',
|
||||
'align-items' => 'center',
|
||||
'justify-content' => 'center',
|
||||
]);
|
||||
|
||||
$cssRules[] = $this->cssGenerator->generate('.hero-section__title', [
|
||||
|
||||
@@ -1,484 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Public\HeroSection\Infrastructure\Ui;
|
||||
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
|
||||
/**
|
||||
* HeroSectionRenderer - Renderiza la sección hero con badges y título
|
||||
*
|
||||
* RESPONSABILIDAD: Generar HTML de la sección hero
|
||||
*
|
||||
* CARACTERÍSTICAS:
|
||||
* - Badges de categorías con múltiples fuentes de datos
|
||||
* - Título H1 con gradiente opcional
|
||||
* - Múltiples tipos de fondo (color, gradiente, imagen)
|
||||
* - Lógica condicional de visibilidad por tipo de página
|
||||
*
|
||||
* @package ROITheme\Public\HeroSection\Presentation
|
||||
*/
|
||||
final class HeroSectionRenderer implements RendererInterface
|
||||
{
|
||||
public function render(Component $component): string
|
||||
{
|
||||
// Verificar visibilidad por tipo de página y exclusiones (Plan 99.10/99.11)
|
||||
if (!PageVisibilityHelper::shouldShow('hero-section')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$data = $component->getData();
|
||||
|
||||
if (!$this->isEnabled($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$this->shouldShowOnCurrentPage($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$classes = $this->buildSectionClasses($data);
|
||||
$styles = $this->buildInlineStyles($data);
|
||||
|
||||
$html = sprintf(
|
||||
'<div class="%s"%s>',
|
||||
esc_attr($classes),
|
||||
$styles ? ' style="' . esc_attr($styles) . '"' : ''
|
||||
);
|
||||
|
||||
$html .= '<div class="container">';
|
||||
|
||||
// Categories badges
|
||||
if ($this->shouldShowCategories($data)) {
|
||||
$html .= $this->buildCategoriesBadges($data);
|
||||
}
|
||||
|
||||
// Title
|
||||
$html .= $this->buildTitle($data);
|
||||
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Custom styles
|
||||
$html .= $this->buildCustomStyles($data);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function isEnabled(array $data): bool
|
||||
{
|
||||
return isset($data['visibility']['is_enabled']) &&
|
||||
$data['visibility']['is_enabled'] === true;
|
||||
}
|
||||
|
||||
private function shouldShowOnCurrentPage(array $data): bool
|
||||
{
|
||||
$showOn = $data['visibility']['show_on_pages'] ?? 'posts';
|
||||
|
||||
switch ($showOn) {
|
||||
case 'all':
|
||||
return true;
|
||||
|
||||
case 'home':
|
||||
return is_front_page();
|
||||
|
||||
case 'posts':
|
||||
return is_single() && get_post_type() === 'post';
|
||||
|
||||
case 'pages':
|
||||
return is_page();
|
||||
|
||||
case 'custom':
|
||||
$postTypes = $data['visibility']['custom_post_types'] ?? '';
|
||||
$allowedTypes = array_map('trim', explode(',', $postTypes));
|
||||
return in_array(get_post_type(), $allowedTypes, true);
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldShowCategories(array $data): bool
|
||||
{
|
||||
return isset($data['categories']['show_categories']) &&
|
||||
$data['categories']['show_categories'] === true;
|
||||
}
|
||||
|
||||
private function buildSectionClasses(array $data): string
|
||||
{
|
||||
$classes = ['container-fluid', 'hero-title'];
|
||||
|
||||
$paddingClass = $this->getPaddingClass($data['styles']['padding_vertical'] ?? 'normal');
|
||||
$classes[] = $paddingClass;
|
||||
|
||||
$marginClass = $this->getMarginClass($data['styles']['margin_bottom'] ?? 'normal');
|
||||
if ($marginClass) {
|
||||
$classes[] = $marginClass;
|
||||
}
|
||||
|
||||
return implode(' ', $classes);
|
||||
}
|
||||
|
||||
private function getPaddingClass(string $padding): string
|
||||
{
|
||||
$paddings = [
|
||||
'compact' => 'py-3',
|
||||
'normal' => 'py-5',
|
||||
'spacious' => 'py-6',
|
||||
'extra-spacious' => 'py-7'
|
||||
];
|
||||
|
||||
return $paddings[$padding] ?? 'py-5';
|
||||
}
|
||||
|
||||
private function getMarginClass(string $margin): string
|
||||
{
|
||||
$margins = [
|
||||
'none' => '',
|
||||
'small' => 'mb-2',
|
||||
'normal' => 'mb-4',
|
||||
'large' => 'mb-5'
|
||||
];
|
||||
|
||||
return $margins[$margin] ?? 'mb-4';
|
||||
}
|
||||
|
||||
private function buildInlineStyles(array $data): string
|
||||
{
|
||||
$styles = [];
|
||||
$backgroundType = $data['styles']['background_type'] ?? 'gradient';
|
||||
|
||||
switch ($backgroundType) {
|
||||
case 'color':
|
||||
$bgColor = $data['styles']['background_color'] ?? '#1e3a5f';
|
||||
$styles[] = "background-color: {$bgColor}";
|
||||
break;
|
||||
|
||||
case 'gradient':
|
||||
$startColor = $data['styles']['gradient_start_color'] ?? '#1e3a5f';
|
||||
$endColor = $data['styles']['gradient_end_color'] ?? '#2c5282';
|
||||
$angle = $data['styles']['gradient_angle'] ?? 135;
|
||||
$styles[] = "background: linear-gradient({$angle}deg, {$startColor}, {$endColor})";
|
||||
break;
|
||||
|
||||
case 'image':
|
||||
$imageUrl = $data['styles']['background_image_url'] ?? '';
|
||||
if (!empty($imageUrl)) {
|
||||
$styles[] = "background-image: url('" . esc_url($imageUrl) . "')";
|
||||
$styles[] = "background-size: cover";
|
||||
$styles[] = "background-position: center";
|
||||
$styles[] = "background-repeat: no-repeat";
|
||||
|
||||
if (isset($data['styles']['background_overlay']) && $data['styles']['background_overlay']) {
|
||||
$opacity = ($data['styles']['overlay_opacity'] ?? 60) / 100;
|
||||
$styles[] = "position: relative";
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Text color
|
||||
if (!empty($data['styles']['text_color'])) {
|
||||
$styles[] = 'color: ' . $data['styles']['text_color'];
|
||||
}
|
||||
|
||||
return implode('; ', $styles);
|
||||
}
|
||||
|
||||
private function buildCategoriesBadges(array $data): string
|
||||
{
|
||||
$categories = $this->getCategories($data);
|
||||
|
||||
if (empty($categories)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$maxCategories = $data['categories']['max_categories'] ?? 5;
|
||||
$categories = array_slice($categories, 0, $maxCategories);
|
||||
|
||||
$alignment = $data['categories']['categories_alignment'] ?? 'center';
|
||||
$alignmentClasses = [
|
||||
'left' => 'justify-content-start',
|
||||
'center' => 'justify-content-center',
|
||||
'right' => 'justify-content-end'
|
||||
];
|
||||
$alignmentClass = $alignmentClasses[$alignment] ?? 'justify-content-center';
|
||||
|
||||
$icon = $data['categories']['category_icon'] ?? 'bi-folder-fill';
|
||||
if (strpos($icon, 'bi-') !== 0) {
|
||||
$icon = 'bi-' . $icon;
|
||||
}
|
||||
|
||||
$html = sprintf('<div class="mb-3 d-flex %s">', esc_attr($alignmentClass));
|
||||
$html .= '<div class="d-flex gap-2 flex-wrap justify-content-center">';
|
||||
|
||||
foreach ($categories as $category) {
|
||||
$html .= sprintf(
|
||||
'<a href="%s" class="category-badge category-badge-hero"><i class="bi %s me-1"></i>%s</a>',
|
||||
esc_url($category['url']),
|
||||
esc_attr($icon),
|
||||
esc_html($category['name'])
|
||||
);
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private function getCategories(array $data): array
|
||||
{
|
||||
$source = $data['categories']['categories_source'] ?? 'post_categories';
|
||||
|
||||
switch ($source) {
|
||||
case 'post_categories':
|
||||
return $this->getPostCategories();
|
||||
|
||||
case 'post_tags':
|
||||
return $this->getPostTags();
|
||||
|
||||
case 'custom_taxonomy':
|
||||
$taxonomy = $data['categories']['custom_taxonomy_name'] ?? '';
|
||||
return $this->getCustomTaxonomyTerms($taxonomy);
|
||||
|
||||
case 'custom_list':
|
||||
$list = $data['categories']['custom_categories_list'] ?? '';
|
||||
return $this->parseCustomCategoriesList($list);
|
||||
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function getPostCategories(): array
|
||||
{
|
||||
$categories = get_the_category();
|
||||
if (empty($categories)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($categories as $category) {
|
||||
$result[] = [
|
||||
'name' => $category->name,
|
||||
'url' => get_category_link($category->term_id)
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getPostTags(): array
|
||||
{
|
||||
$tags = get_the_tags();
|
||||
if (empty($tags)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($tags as $tag) {
|
||||
$result[] = [
|
||||
'name' => $tag->name,
|
||||
'url' => get_tag_link($tag->term_id)
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getCustomTaxonomyTerms(string $taxonomy): array
|
||||
{
|
||||
if (empty($taxonomy)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$terms = get_the_terms(get_the_ID(), $taxonomy);
|
||||
if (empty($terms) || is_wp_error($terms)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($terms as $term) {
|
||||
$result[] = [
|
||||
'name' => $term->name,
|
||||
'url' => get_term_link($term)
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function parseCustomCategoriesList(string $list): array
|
||||
{
|
||||
if (empty($list)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$lines = explode("\n", $list);
|
||||
$result = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if (empty($line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts = explode('|', $line);
|
||||
if (count($parts) >= 2) {
|
||||
$result[] = [
|
||||
'name' => trim($parts[0]),
|
||||
'url' => trim($parts[1])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function buildTitle(array $data): string
|
||||
{
|
||||
$titleText = $this->getTitleText($data);
|
||||
|
||||
if (empty($titleText)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$titleTag = $data['title']['title_tag'] ?? 'h1';
|
||||
$titleClasses = $data['title']['title_classes'] ?? 'display-5 fw-bold';
|
||||
$alignment = $data['title']['title_alignment'] ?? 'center';
|
||||
|
||||
$alignmentClasses = [
|
||||
'left' => 'text-start',
|
||||
'center' => 'text-center',
|
||||
'right' => 'text-end'
|
||||
];
|
||||
$alignmentClass = $alignmentClasses[$alignment] ?? 'text-center';
|
||||
|
||||
$classes = trim($titleClasses . ' ' . $alignmentClass);
|
||||
|
||||
$titleStyle = '';
|
||||
if (isset($data['title']['enable_gradient']) && $data['title']['enable_gradient']) {
|
||||
$titleStyle = $this->buildGradientStyle($data);
|
||||
$classes .= ' roi-gradient-text';
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<%s class="%s"%s>%s</%s>',
|
||||
esc_attr($titleTag),
|
||||
esc_attr($classes),
|
||||
$titleStyle ? ' style="' . esc_attr($titleStyle) . '"' : '',
|
||||
esc_html($titleText),
|
||||
esc_attr($titleTag)
|
||||
);
|
||||
}
|
||||
|
||||
private function getTitleText(array $data): string
|
||||
{
|
||||
$source = $data['title']['title_source'] ?? 'post_title';
|
||||
|
||||
switch ($source) {
|
||||
case 'post_title':
|
||||
return get_the_title();
|
||||
|
||||
case 'custom_field':
|
||||
$fieldName = $data['title']['custom_field_name'] ?? '';
|
||||
if (!empty($fieldName)) {
|
||||
$value = get_post_meta(get_the_ID(), $fieldName, true);
|
||||
return is_string($value) ? $value : '';
|
||||
}
|
||||
return '';
|
||||
|
||||
case 'custom_text':
|
||||
return $data['title']['custom_text'] ?? '';
|
||||
|
||||
default:
|
||||
return get_the_title();
|
||||
}
|
||||
}
|
||||
|
||||
private function buildGradientStyle(array $data): string
|
||||
{
|
||||
$startColor = $data['title']['gradient_color_start'] ?? '#1e3a5f';
|
||||
$endColor = $data['title']['gradient_color_end'] ?? '#FF8600';
|
||||
$direction = $data['title']['gradient_direction'] ?? 'to-right';
|
||||
|
||||
$directions = [
|
||||
'to-right' => 'to right',
|
||||
'to-left' => 'to left',
|
||||
'to-bottom' => 'to bottom',
|
||||
'to-top' => 'to top',
|
||||
'diagonal' => '135deg'
|
||||
];
|
||||
|
||||
$gradientDirection = $directions[$direction] ?? 'to right';
|
||||
|
||||
return "background: linear-gradient({$gradientDirection}, {$startColor}, {$endColor}); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;";
|
||||
}
|
||||
|
||||
private function buildCustomStyles(array $data): string
|
||||
{
|
||||
$badgeBg = $data['styles']['category_badge_background'] ?? 'rgba(255, 255, 255, 0.2)';
|
||||
$badgeTextColor = $data['styles']['category_badge_text_color'] ?? '#FFFFFF';
|
||||
$badgeBlur = isset($data['styles']['category_badge_blur']) && $data['styles']['category_badge_blur'];
|
||||
|
||||
$blurStyle = $badgeBlur ? 'backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);' : '';
|
||||
|
||||
$overlayStyle = '';
|
||||
if (($data['styles']['background_type'] ?? '') === 'image' &&
|
||||
isset($data['styles']['background_overlay']) &&
|
||||
$data['styles']['background_overlay']) {
|
||||
$opacity = ($data['styles']['overlay_opacity'] ?? 60) / 100;
|
||||
$overlayStyle = <<<CSS
|
||||
.hero-title::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, {$opacity});
|
||||
z-index: 0;
|
||||
}
|
||||
.hero-title > .container {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
CSS;
|
||||
}
|
||||
|
||||
return <<<STYLES
|
||||
<style>
|
||||
.category-badge-hero {
|
||||
background-color: {$badgeBg};
|
||||
color: {$badgeTextColor};
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 2rem;
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s ease;
|
||||
{$blurStyle}
|
||||
}
|
||||
.category-badge-hero:hover {
|
||||
background-color: rgba(255, 134, 0, 0.3);
|
||||
color: {$badgeTextColor};
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.roi-gradient-text {
|
||||
display: inline-block;
|
||||
}
|
||||
{$overlayStyle}
|
||||
</style>
|
||||
STYLES;
|
||||
}
|
||||
|
||||
public function supports(string $componentType): bool
|
||||
{
|
||||
return $componentType === 'hero-section';
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use ROITheme\Shared\Domain\Contracts\RendererInterface;
|
||||
use ROITheme\Shared\Domain\Contracts\CSSGeneratorInterface;
|
||||
use ROITheme\Shared\Domain\Entities\Component;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
use ROITheme\Shared\Infrastructure\Services\UserVisibilityHelper;
|
||||
|
||||
/**
|
||||
* Class TopNotificationBarRenderer
|
||||
@@ -61,6 +62,11 @@ final class TopNotificationBarRenderer implements RendererInterface
|
||||
return '';
|
||||
}
|
||||
|
||||
// Validar visibilidad por usuario logueado
|
||||
if (!UserVisibilityHelper::shouldShowForUser($data['visibility'] ?? [])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Generar HTML
|
||||
$html = $this->buildHTML($data);
|
||||
|
||||
|
||||
@@ -27,6 +27,13 @@
|
||||
"default": false,
|
||||
"editable": true,
|
||||
"description": "Muestra el componente en pantallas < 992px"
|
||||
},
|
||||
"hide_for_logged_in": {
|
||||
"type": "boolean",
|
||||
"label": "Ocultar para usuarios logueados",
|
||||
"default": false,
|
||||
"editable": true,
|
||||
"description": "No mostrar el CTA a usuarios con sesión iniciada en WordPress"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -35,6 +35,13 @@
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"description": "Inyectar CSS inline en <head> para optimizar LCP (componente above-the-fold)"
|
||||
},
|
||||
"hide_for_logged_in": {
|
||||
"type": "boolean",
|
||||
"label": "Ocultar para usuarios logueados",
|
||||
"default": false,
|
||||
"editable": true,
|
||||
"description": "No mostrar el botón a usuarios con sesión iniciada en WordPress"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -27,6 +27,13 @@
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"description": "Muestra el componente en pantallas < 992px"
|
||||
},
|
||||
"hide_for_logged_in": {
|
||||
"type": "boolean",
|
||||
"label": "Ocultar para usuarios logueados",
|
||||
"default": false,
|
||||
"editable": true,
|
||||
"description": "No mostrar el CTA a usuarios con sesión iniciada en WordPress"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -37,6 +37,13 @@
|
||||
"default": true,
|
||||
"editable": true,
|
||||
"description": "Inyectar CSS inline en <head> para optimizar LCP (componente above-the-fold)"
|
||||
},
|
||||
"hide_for_logged_in": {
|
||||
"type": "boolean",
|
||||
"label": "Ocultar para usuarios logueados",
|
||||
"default": false,
|
||||
"editable": true,
|
||||
"description": "No mostrar la barra a usuarios con sesión iniciada en WordPress"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Application\UseCases;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\WrapperVisibilityCheckerInterface;
|
||||
|
||||
/**
|
||||
* UseCase: Verificar si un wrapper de componente debe renderizarse
|
||||
*
|
||||
* Responsabilidad: Orquestar la lógica de verificación de visibilidad
|
||||
* combinando múltiples criterios:
|
||||
* 1. Componente habilitado (is_enabled)
|
||||
* 2. Visible en dispositivo actual (show_on_mobile/desktop)
|
||||
* 3. No excluido por reglas (categoría, post ID, URL, page visibility)
|
||||
*
|
||||
* @package ROITheme\Shared\Application\UseCases
|
||||
* @see Plan 99.15 - Fix Empty Layout Wrappers
|
||||
*/
|
||||
final class CheckWrapperVisibilityUseCase
|
||||
{
|
||||
public function __construct(
|
||||
private readonly WrapperVisibilityCheckerInterface $visibilityChecker
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Ejecuta la verificación de visibilidad del wrapper
|
||||
*
|
||||
* @param string $componentName Nombre del componente (kebab-case)
|
||||
* @param bool $isMobile True si es dispositivo móvil
|
||||
* @return bool True si el wrapper debe renderizarse
|
||||
*/
|
||||
public function execute(string $componentName, bool $isMobile): bool
|
||||
{
|
||||
// Criterio 1: Debe estar habilitado
|
||||
if (!$this->visibilityChecker->isEnabled($componentName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Criterio 2: Debe ser visible en el dispositivo actual
|
||||
if (!$this->visibilityChecker->isVisibleOnDevice($componentName, $isMobile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Criterio 3: No debe estar excluido
|
||||
if (!$this->visibilityChecker->isNotExcluded($componentName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Domain\Contracts;
|
||||
|
||||
/**
|
||||
* Interface para verificar visibilidad de wrappers de componentes
|
||||
*
|
||||
* Responsabilidad: Definir contrato para determinar si un wrapper
|
||||
* de componente debe renderizarse basándose en:
|
||||
* - Estado habilitado/deshabilitado
|
||||
* - Visibilidad por dispositivo
|
||||
* - Reglas de exclusión (categoría, post ID, URL pattern, page visibility)
|
||||
*
|
||||
* @package ROITheme\Shared\Domain\Contracts
|
||||
* @see Plan 99.15 - Fix Empty Layout Wrappers
|
||||
*/
|
||||
interface WrapperVisibilityCheckerInterface
|
||||
{
|
||||
/**
|
||||
* Verifica si el componente está habilitado globalmente
|
||||
*
|
||||
* @param string $componentName Nombre del componente (kebab-case)
|
||||
* @return bool True si is_enabled = true en BD
|
||||
*/
|
||||
public function isEnabled(string $componentName): bool;
|
||||
|
||||
/**
|
||||
* Verifica si el componente es visible en el dispositivo actual
|
||||
*
|
||||
* @param string $componentName Nombre del componente (kebab-case)
|
||||
* @param bool $isMobile True si es dispositivo móvil
|
||||
* @return bool True si show_on_mobile/show_on_desktop según corresponda
|
||||
*/
|
||||
public function isVisibleOnDevice(string $componentName, bool $isMobile): bool;
|
||||
|
||||
/**
|
||||
* Verifica si el componente NO está excluido para la página actual
|
||||
*
|
||||
* Evalúa todas las reglas de exclusión:
|
||||
* - Exclusión por categoría
|
||||
* - Exclusión por post ID
|
||||
* - Exclusión por URL pattern
|
||||
* - Page visibility (home, posts, pages, archives, search)
|
||||
*
|
||||
* @param string $componentName Nombre del componente (kebab-case)
|
||||
* @return bool True si el componente NO está excluido
|
||||
*/
|
||||
public function isNotExcluded(string $componentName): bool;
|
||||
}
|
||||
@@ -37,6 +37,11 @@ use ROITheme\Shared\Infrastructure\Services\WordPressPageContextProvider;
|
||||
use ROITheme\Shared\Infrastructure\Services\WordPressServerRequestProvider;
|
||||
use ROITheme\Shared\Application\UseCases\EvaluateExclusions\EvaluateExclusionsUseCase;
|
||||
use ROITheme\Shared\Application\UseCases\EvaluateComponentVisibility\EvaluateComponentVisibilityUseCase;
|
||||
// Wrapper Visibility System (Plan 99.15)
|
||||
use ROITheme\Shared\Domain\Contracts\WrapperVisibilityCheckerInterface;
|
||||
use ROITheme\Shared\Infrastructure\Persistence\WordPress\WordPressComponentVisibilityRepository;
|
||||
use ROITheme\Shared\Application\UseCases\CheckWrapperVisibilityUseCase;
|
||||
use ROITheme\Shared\Infrastructure\Wordpress\BodyClassHooksRegistrar;
|
||||
|
||||
/**
|
||||
* DIContainer - Contenedor de Inyección de Dependencias
|
||||
@@ -443,4 +448,49 @@ final class DIContainer
|
||||
}
|
||||
return $this->instances['evaluateComponentVisibilityUseCase'];
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// Wrapper Visibility System (Plan 99.15)
|
||||
// ===============================
|
||||
|
||||
/**
|
||||
* Obtiene el repositorio de visibilidad de wrappers
|
||||
*
|
||||
* Implementa WrapperVisibilityCheckerInterface
|
||||
*/
|
||||
public function getWrapperVisibilityChecker(): WrapperVisibilityCheckerInterface
|
||||
{
|
||||
if (!isset($this->instances['wrapperVisibilityChecker'])) {
|
||||
$this->instances['wrapperVisibilityChecker'] = new WordPressComponentVisibilityRepository($this->wpdb);
|
||||
}
|
||||
return $this->instances['wrapperVisibilityChecker'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el caso de uso para verificar visibilidad de wrappers
|
||||
*
|
||||
* Usado por WrapperVisibilityService para templates
|
||||
*/
|
||||
public function getCheckWrapperVisibilityUseCase(): CheckWrapperVisibilityUseCase
|
||||
{
|
||||
if (!isset($this->instances['checkWrapperVisibilityUseCase'])) {
|
||||
$this->instances['checkWrapperVisibilityUseCase'] = new CheckWrapperVisibilityUseCase(
|
||||
$this->getWrapperVisibilityChecker()
|
||||
);
|
||||
}
|
||||
return $this->instances['checkWrapperVisibilityUseCase'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el registrador de hooks para body_class
|
||||
*
|
||||
* CSS failsafe: Agrega clases cuando componentes están ocultos
|
||||
*/
|
||||
public function getBodyClassHooksRegistrar(): BodyClassHooksRegistrar
|
||||
{
|
||||
if (!isset($this->instances['bodyClassHooksRegistrar'])) {
|
||||
$this->instances['bodyClassHooksRegistrar'] = new BodyClassHooksRegistrar();
|
||||
}
|
||||
return $this->instances['bodyClassHooksRegistrar'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Infrastructure\Persistence\WordPress;
|
||||
|
||||
use ROITheme\Shared\Domain\Contracts\WrapperVisibilityCheckerInterface;
|
||||
use ROITheme\Shared\Infrastructure\Services\PageVisibilityHelper;
|
||||
|
||||
/**
|
||||
* Implementación de WrapperVisibilityCheckerInterface para WordPress
|
||||
*
|
||||
* Responsabilidad: Consultar BD y evaluar visibilidad de wrappers de componentes
|
||||
*
|
||||
* - Consulta tabla wp_roi_theme_component_settings para is_enabled, show_on_mobile, show_on_desktop
|
||||
* - Delega evaluación de exclusiones a PageVisibilityHelper (DRY)
|
||||
*
|
||||
* @package ROITheme\Shared\Infrastructure\Persistence\WordPress
|
||||
* @see Plan 99.15 - Fix Empty Layout Wrappers
|
||||
*/
|
||||
final class WordPressComponentVisibilityRepository implements WrapperVisibilityCheckerInterface
|
||||
{
|
||||
private string $tableName;
|
||||
|
||||
public function __construct(
|
||||
private \wpdb $wpdb
|
||||
) {
|
||||
$this->tableName = $this->wpdb->prefix . 'roi_theme_component_settings';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isEnabled(string $componentName): bool
|
||||
{
|
||||
$value = $this->getVisibilityAttribute($componentName, 'is_enabled');
|
||||
|
||||
// Si no existe el registro, asumir habilitado por defecto
|
||||
if ($value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->toBool($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function isVisibleOnDevice(string $componentName, bool $isMobile): bool
|
||||
{
|
||||
$attribute = $isMobile ? 'show_on_mobile' : 'show_on_desktop';
|
||||
$value = $this->getVisibilityAttribute($componentName, $attribute);
|
||||
|
||||
// Si no existe el registro, asumir visible por defecto
|
||||
if ($value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->toBool($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Delega a PageVisibilityHelper que ya implementa:
|
||||
* - Visibilidad por tipo de página (home, posts, pages, archives, search)
|
||||
* - Exclusiones por categoría, post ID, URL pattern
|
||||
*/
|
||||
public function isNotExcluded(string $componentName): bool
|
||||
{
|
||||
return PageVisibilityHelper::shouldShow($componentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene un atributo del grupo visibility desde la BD
|
||||
*
|
||||
* @param string $componentName
|
||||
* @param string $attributeName
|
||||
* @return string|null
|
||||
*/
|
||||
private function getVisibilityAttribute(string $componentName, string $attributeName): ?string
|
||||
{
|
||||
$sql = $this->wpdb->prepare(
|
||||
"SELECT attribute_value
|
||||
FROM {$this->tableName}
|
||||
WHERE component_name = %s
|
||||
AND group_name = %s
|
||||
AND attribute_name = %s
|
||||
LIMIT 1",
|
||||
$componentName,
|
||||
'visibility',
|
||||
$attributeName
|
||||
);
|
||||
|
||||
$result = $this->wpdb->get_var($sql);
|
||||
|
||||
return $result !== null ? (string) $result : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convierte string a boolean
|
||||
*
|
||||
* @param string $value
|
||||
* @return bool
|
||||
*/
|
||||
private function toBool(string $value): bool
|
||||
{
|
||||
return $value === '1' || strtolower($value) === 'true';
|
||||
}
|
||||
}
|
||||
35
Shared/Infrastructure/Services/UserVisibilityHelper.php
Normal file
35
Shared/Infrastructure/Services/UserVisibilityHelper.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Infrastructure\Services;
|
||||
|
||||
/**
|
||||
* Helper para verificar visibilidad basada en estado de autenticación del usuario.
|
||||
*
|
||||
* Similar a PageVisibilityHelper, proporciona método estático para evaluar
|
||||
* si un componente debe ocultarse para usuarios logueados.
|
||||
*
|
||||
* @package ROITheme\Shared\Infrastructure\Services
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class UserVisibilityHelper
|
||||
{
|
||||
/**
|
||||
* Verifica si el componente debe mostrarse según estado de login.
|
||||
*
|
||||
* @param array $visibilityData Datos del grupo 'visibility' del componente
|
||||
* @return bool true si debe mostrarse, false si debe ocultarse
|
||||
*/
|
||||
public static function shouldShowForUser(array $visibilityData): bool
|
||||
{
|
||||
$hideForLoggedIn = $visibilityData['hide_for_logged_in'] ?? false;
|
||||
|
||||
// Si la opción está activada Y el usuario está logueado, ocultar
|
||||
if ($hideForLoggedIn && is_user_logged_in()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
100
Shared/Infrastructure/Services/WrapperVisibilityService.php
Normal file
100
Shared/Infrastructure/Services/WrapperVisibilityService.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Infrastructure\Services;
|
||||
|
||||
use ROITheme\Shared\Application\UseCases\CheckWrapperVisibilityUseCase;
|
||||
use ROITheme\Shared\Infrastructure\Di\DIContainer;
|
||||
|
||||
/**
|
||||
* Servicio facade para verificar visibilidad de wrappers desde templates
|
||||
*
|
||||
* Responsabilidad: Proveer acceso simplificado (singleton/static) al
|
||||
* CheckWrapperVisibilityUseCase para uso en templates PHP.
|
||||
*
|
||||
* USO EN TEMPLATES:
|
||||
* ```php
|
||||
* if (WrapperVisibilityService::shouldRenderWrapper('navbar')) {
|
||||
* // Renderizar wrapper y componente
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @package ROITheme\Shared\Infrastructure\Services
|
||||
* @see Plan 99.15 - Fix Empty Layout Wrappers
|
||||
*/
|
||||
final class WrapperVisibilityService
|
||||
{
|
||||
private static ?CheckWrapperVisibilityUseCase $useCase = null;
|
||||
|
||||
/**
|
||||
* Verifica si el wrapper de un componente debe renderizarse
|
||||
*
|
||||
* @param string $componentName Nombre del componente (kebab-case)
|
||||
* @return bool True si el wrapper debe renderizarse
|
||||
*/
|
||||
public static function shouldRenderWrapper(string $componentName): bool
|
||||
{
|
||||
$useCase = self::getUseCase();
|
||||
$isMobile = self::detectMobile();
|
||||
|
||||
return $useCase->execute($componentName, $isMobile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica visibilidad para múltiples componentes
|
||||
*
|
||||
* Útil para determinar si renderizar un contenedor que agrupa varios componentes
|
||||
*
|
||||
* @param array<string> $componentNames Lista de nombres de componentes
|
||||
* @return bool True si AL MENOS UNO de los componentes debe mostrarse
|
||||
*/
|
||||
public static function shouldRenderAnyWrapper(array $componentNames): bool
|
||||
{
|
||||
foreach ($componentNames as $componentName) {
|
||||
if (self::shouldRenderWrapper($componentName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene o crea el UseCase
|
||||
*
|
||||
* @return CheckWrapperVisibilityUseCase
|
||||
*/
|
||||
private static function getUseCase(): CheckWrapperVisibilityUseCase
|
||||
{
|
||||
if (self::$useCase === null) {
|
||||
$container = DIContainer::getInstance();
|
||||
self::$useCase = $container->getCheckWrapperVisibilityUseCase();
|
||||
}
|
||||
|
||||
return self::$useCase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detecta si el dispositivo actual es móvil
|
||||
*
|
||||
* Usa wp_is_mobile() de WordPress
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function detectMobile(): bool
|
||||
{
|
||||
if (function_exists('wp_is_mobile')) {
|
||||
return wp_is_mobile();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpia la instancia del UseCase (útil para tests)
|
||||
*/
|
||||
public static function reset(): void
|
||||
{
|
||||
self::$useCase = null;
|
||||
}
|
||||
}
|
||||
96
Shared/Infrastructure/Wordpress/BodyClassHooksRegistrar.php
Normal file
96
Shared/Infrastructure/Wordpress/BodyClassHooksRegistrar.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ROITheme\Shared\Infrastructure\Wordpress;
|
||||
|
||||
use ROITheme\Shared\Infrastructure\Services\WrapperVisibilityService;
|
||||
|
||||
/**
|
||||
* Registra hook body_class para agregar clases CSS de componentes ocultos
|
||||
*
|
||||
* RESPONSABILIDAD:
|
||||
* - Registrar hook body_class
|
||||
* - Agregar clases CSS cuando componentes están ocultos
|
||||
*
|
||||
* FLUJO:
|
||||
* 1. body_class filter → addHiddenComponentClasses()
|
||||
* - Verifica visibilidad de componentes clave (navbar, sidebar components)
|
||||
* - Agrega clases: roi-hide-navbar, roi-hide-sidebar, etc.
|
||||
*
|
||||
* PROPÓSITO:
|
||||
* Failsafe CSS: Si los templates no pueden ocultar wrappers completamente,
|
||||
* estas clases permiten ocultarlos via CSS.
|
||||
*
|
||||
* PATRÓN:
|
||||
* - SRP: Solo registra hooks, delega lógica a WrapperVisibilityService
|
||||
*
|
||||
* @package ROITheme\Shared\Infrastructure\Wordpress
|
||||
* @see Plan 99.15 - Fix Empty Layout Wrappers
|
||||
*/
|
||||
final class BodyClassHooksRegistrar
|
||||
{
|
||||
/**
|
||||
* Componentes que afectan el layout principal
|
||||
*/
|
||||
private const LAYOUT_COMPONENTS = [
|
||||
'navbar' => 'roi-hide-navbar',
|
||||
'table-of-contents' => 'roi-hide-toc',
|
||||
'cta-box-sidebar' => 'roi-hide-cta-sidebar',
|
||||
'sidebar' => 'roi-hide-sidebar',
|
||||
];
|
||||
|
||||
/**
|
||||
* Componentes de sidebar que determinan si mostrar columna lateral
|
||||
*/
|
||||
private const SIDEBAR_COMPONENTS = [
|
||||
'table-of-contents',
|
||||
'cta-box-sidebar',
|
||||
];
|
||||
|
||||
/**
|
||||
* Registrar hooks de WordPress
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
add_filter('body_class', [$this, 'addHiddenComponentClasses']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback para body_class - agrega clases para componentes ocultos
|
||||
*
|
||||
* @param array<string> $classes Clases existentes
|
||||
* @return array<string> Clases modificadas
|
||||
*/
|
||||
public function addHiddenComponentClasses(array $classes): array
|
||||
{
|
||||
// Agregar clase por cada componente oculto
|
||||
foreach (self::LAYOUT_COMPONENTS as $componentName => $cssClass) {
|
||||
if (!WrapperVisibilityService::shouldRenderWrapper($componentName)) {
|
||||
$classes[] = $cssClass;
|
||||
}
|
||||
}
|
||||
|
||||
// Verificar si TODOS los componentes de sidebar están ocultos
|
||||
if ($this->allSidebarComponentsHidden()) {
|
||||
$classes[] = 'roi-sidebar-empty';
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si todos los componentes de sidebar están ocultos
|
||||
*
|
||||
* @return bool True si ningún componente de sidebar debe mostrarse
|
||||
*/
|
||||
private function allSidebarComponentsHidden(): bool
|
||||
{
|
||||
foreach (self::SIDEBAR_COMPONENTS as $componentName) {
|
||||
if (WrapperVisibilityService::shouldRenderWrapper($componentName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Hero Section Template
|
||||
*
|
||||
* Hero section con degradado azul para single posts
|
||||
*
|
||||
* @package ROI_Theme
|
||||
*/
|
||||
|
||||
if (!is_single()) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
|
||||
<section class="hero-section">
|
||||
<div class="container-fluid py-5">
|
||||
<div class="hero-content text-center">
|
||||
|
||||
<!-- Category Badges (ARRIBA del H1) -->
|
||||
<?php
|
||||
$categories = get_the_category();
|
||||
if (!empty($categories)) :
|
||||
?>
|
||||
<div class="hero-categories mb-3">
|
||||
<?php foreach ($categories as $category) : ?>
|
||||
<?php if ($category->name !== 'Uncategorized' && $category->name !== 'Sin categoría') : ?>
|
||||
<span class="hero-category-badge"><?php echo esc_html($category->name); ?></span>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- H1 Title -->
|
||||
<h1 class="hero-title"><?php the_title(); ?></h1>
|
||||
|
||||
<!-- Post Meta -->
|
||||
<div class="hero-meta">
|
||||
<span class="hero-meta-item">
|
||||
<i class="bi bi-calendar3 me-1"></i>
|
||||
<?php echo get_the_date(); ?>
|
||||
</span>
|
||||
<span class="hero-meta-separator">|</span>
|
||||
<span class="hero-meta-item">
|
||||
<i class="bi bi-person me-1"></i>
|
||||
<?php the_author(); ?>
|
||||
</span>
|
||||
<?php
|
||||
$reading_time = roi_get_reading_time();
|
||||
if ($reading_time) :
|
||||
?>
|
||||
<span class="hero-meta-separator">|</span>
|
||||
<span class="hero-meta-item">
|
||||
<i class="bi bi-clock me-1"></i>
|
||||
<?php echo esc_html($reading_time); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -297,9 +297,6 @@ function roi_render_component(string $componentName): string {
|
||||
case 'cta-lets-talk':
|
||||
$renderer = new \ROITheme\Public\CtaLetsTalk\Infrastructure\Ui\CtaLetsTalkRenderer($cssGenerator);
|
||||
break;
|
||||
case 'hero-section':
|
||||
$renderer = new \ROITheme\Public\HeroSection\Infrastructure\Ui\HeroSectionRenderer();
|
||||
break;
|
||||
case 'featured-image':
|
||||
$renderer = new \ROITheme\Public\FeaturedImage\Infrastructure\Ui\FeaturedImageRenderer($cssGenerator);
|
||||
break;
|
||||
@@ -373,6 +370,11 @@ add_action('after_setup_theme', function() {
|
||||
$criticalCSSService = roi_get_critical_css_service();
|
||||
$hooksRegistrar = new \ROITheme\Shared\Infrastructure\Wordpress\CriticalCSSHooksRegistrar($criticalCSSService);
|
||||
$hooksRegistrar->register();
|
||||
|
||||
// 3. Body Class Hooks (Plan 99.15) - CSS failsafe para componentes ocultos
|
||||
$container = \ROITheme\Shared\Infrastructure\Di\DIContainer::getInstance();
|
||||
$bodyClassHooksRegistrar = $container->getBodyClassHooksRegistrar();
|
||||
$bodyClassHooksRegistrar->register();
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
@@ -381,6 +383,47 @@ add_action('after_setup_theme', function() {
|
||||
// NO hardcodear CSS aquí - viola la arquitectura Clean Architecture.
|
||||
// =============================================================================
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTION: roi_should_render_wrapper() - Plan 99.15
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Verifica si el wrapper de un componente debe renderizarse
|
||||
*
|
||||
* Evalúa:
|
||||
* - is_enabled
|
||||
* - show_on_mobile / show_on_desktop
|
||||
* - Exclusiones (categoría, post ID, URL pattern, page visibility)
|
||||
*
|
||||
* USO EN TEMPLATES:
|
||||
* ```php
|
||||
* if (roi_should_render_wrapper('navbar')) {
|
||||
* echo '<nav class="navbar">';
|
||||
* echo roi_render_component('navbar');
|
||||
* echo '</nav>';
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param string $componentName Nombre del componente (kebab-case)
|
||||
* @return bool True si el wrapper debe renderizarse
|
||||
* @see Plan 99.15 - Fix Empty Layout Wrappers
|
||||
*/
|
||||
function roi_should_render_wrapper(string $componentName): bool {
|
||||
return \ROITheme\Shared\Infrastructure\Services\WrapperVisibilityService::shouldRenderWrapper($componentName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si AL MENOS UN componente de una lista debe renderizarse
|
||||
*
|
||||
* Útil para determinar si mostrar columna sidebar
|
||||
*
|
||||
* @param array<string> $componentNames Lista de nombres de componentes
|
||||
* @return bool True si al menos uno debe mostrarse
|
||||
*/
|
||||
function roi_should_render_any_wrapper(array $componentNames): bool {
|
||||
return \ROITheme\Shared\Infrastructure\Services\WrapperVisibilityService::shouldRenderAnyWrapper($componentNames);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTION: roi_get_adsense_search_config()
|
||||
// =============================================================================
|
||||
|
||||
@@ -35,6 +35,10 @@ if (function_exists('roi_render_component')) {
|
||||
?>
|
||||
|
||||
<!-- Navbar (Template líneas 264-320) -->
|
||||
<?php
|
||||
// Plan 99.15: Solo renderizar wrapper si navbar debe mostrarse
|
||||
if (function_exists('roi_should_render_wrapper') && roi_should_render_wrapper('navbar')):
|
||||
?>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark py-3" role="navigation" aria-label="<?php esc_attr_e('Primary Navigation', 'roi-theme'); ?>">
|
||||
<div class="container">
|
||||
|
||||
@@ -94,4 +98,5 @@ if (function_exists('roi_render_component')) {
|
||||
</div>
|
||||
|
||||
</div><!-- .container -->
|
||||
</nav><!-- .navbar -->
|
||||
</nav><!-- .navbar -->
|
||||
<?php endif; // roi_should_render_wrapper('navbar') ?>
|
||||
456
openspec/AGENTS.md
Normal file
456
openspec/AGENTS.md
Normal file
@@ -0,0 +1,456 @@
|
||||
# OpenSpec Instructions
|
||||
|
||||
Instructions for AI coding assistants using OpenSpec for spec-driven development.
|
||||
|
||||
## TL;DR Quick Checklist
|
||||
|
||||
- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search)
|
||||
- Decide scope: new capability vs modify existing capability
|
||||
- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`)
|
||||
- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability
|
||||
- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per requirement
|
||||
- Validate: `openspec validate [change-id] --strict` and fix issues
|
||||
- Request approval: Do not start implementation until proposal is approved
|
||||
|
||||
## Three-Stage Workflow
|
||||
|
||||
### Stage 1: Creating Changes
|
||||
Create proposal when you need to:
|
||||
- Add features or functionality
|
||||
- Make breaking changes (API, schema)
|
||||
- Change architecture or patterns
|
||||
- Optimize performance (changes behavior)
|
||||
- Update security patterns
|
||||
|
||||
Triggers (examples):
|
||||
- "Help me create a change proposal"
|
||||
- "Help me plan a change"
|
||||
- "Help me create a proposal"
|
||||
- "I want to create a spec proposal"
|
||||
- "I want to create a spec"
|
||||
|
||||
Loose matching guidance:
|
||||
- Contains one of: `proposal`, `change`, `spec`
|
||||
- With one of: `create`, `plan`, `make`, `start`, `help`
|
||||
|
||||
Skip proposal for:
|
||||
- Bug fixes (restore intended behavior)
|
||||
- Typos, formatting, comments
|
||||
- Dependency updates (non-breaking)
|
||||
- Configuration changes
|
||||
- Tests for existing behavior
|
||||
|
||||
**Workflow**
|
||||
1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context.
|
||||
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes/<id>/`.
|
||||
3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement.
|
||||
4. Run `openspec validate <id> --strict` and resolve any issues before sharing the proposal.
|
||||
|
||||
### Stage 2: Implementing Changes
|
||||
Track these steps as TODOs and complete them one by one.
|
||||
1. **Read proposal.md** - Understand what's being built
|
||||
2. **Read design.md** (if exists) - Review technical decisions
|
||||
3. **Read tasks.md** - Get implementation checklist
|
||||
4. **Implement tasks sequentially** - Complete in order
|
||||
5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses
|
||||
6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality
|
||||
7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
|
||||
|
||||
### Stage 3: Archiving Changes
|
||||
After deployment, create separate PR to:
|
||||
- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/`
|
||||
- Update `specs/` if capabilities changed
|
||||
- Use `openspec archive <change-id> --skip-specs --yes` for tooling-only changes (always pass the change ID explicitly)
|
||||
- Run `openspec validate --strict` to confirm the archived change passes checks
|
||||
|
||||
## Before Any Task
|
||||
|
||||
**Context Checklist:**
|
||||
- [ ] Read relevant specs in `specs/[capability]/spec.md`
|
||||
- [ ] Check pending changes in `changes/` for conflicts
|
||||
- [ ] Read `openspec/project.md` for conventions
|
||||
- [ ] Run `openspec list` to see active changes
|
||||
- [ ] Run `openspec list --specs` to see existing capabilities
|
||||
|
||||
**Before Creating Specs:**
|
||||
- Always check if capability already exists
|
||||
- Prefer modifying existing specs over creating duplicates
|
||||
- Use `openspec show [spec]` to review current state
|
||||
- If request is ambiguous, ask 1–2 clarifying questions before scaffolding
|
||||
|
||||
### Search Guidance
|
||||
- Enumerate specs: `openspec spec list --long` (or `--json` for scripts)
|
||||
- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available)
|
||||
- Show details:
|
||||
- Spec: `openspec show <spec-id> --type spec` (use `--json` for filters)
|
||||
- Change: `openspec show <change-id> --json --deltas-only`
|
||||
- Full-text search (use ripgrep): `rg -n "Requirement:|Scenario:" openspec/specs`
|
||||
|
||||
## Quick Start
|
||||
|
||||
### CLI Commands
|
||||
|
||||
```bash
|
||||
# Essential commands
|
||||
openspec list # List active changes
|
||||
openspec list --specs # List specifications
|
||||
openspec show [item] # Display change or spec
|
||||
openspec validate [item] # Validate changes or specs
|
||||
openspec archive <change-id> [--yes|-y] # Archive after deployment (add --yes for non-interactive runs)
|
||||
|
||||
# Project management
|
||||
openspec init [path] # Initialize OpenSpec
|
||||
openspec update [path] # Update instruction files
|
||||
|
||||
# Interactive mode
|
||||
openspec show # Prompts for selection
|
||||
openspec validate # Bulk validation mode
|
||||
|
||||
# Debugging
|
||||
openspec show [change] --json --deltas-only
|
||||
openspec validate [change] --strict
|
||||
```
|
||||
|
||||
### Command Flags
|
||||
|
||||
- `--json` - Machine-readable output
|
||||
- `--type change|spec` - Disambiguate items
|
||||
- `--strict` - Comprehensive validation
|
||||
- `--no-interactive` - Disable prompts
|
||||
- `--skip-specs` - Archive without spec updates
|
||||
- `--yes`/`-y` - Skip confirmation prompts (non-interactive archive)
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
openspec/
|
||||
├── project.md # Project conventions
|
||||
├── specs/ # Current truth - what IS built
|
||||
│ └── [capability]/ # Single focused capability
|
||||
│ ├── spec.md # Requirements and scenarios
|
||||
│ └── design.md # Technical patterns
|
||||
├── changes/ # Proposals - what SHOULD change
|
||||
│ ├── [change-name]/
|
||||
│ │ ├── proposal.md # Why, what, impact
|
||||
│ │ ├── tasks.md # Implementation checklist
|
||||
│ │ ├── design.md # Technical decisions (optional; see criteria)
|
||||
│ │ └── specs/ # Delta changes
|
||||
│ │ └── [capability]/
|
||||
│ │ └── spec.md # ADDED/MODIFIED/REMOVED
|
||||
│ └── archive/ # Completed changes
|
||||
```
|
||||
|
||||
## Creating Change Proposals
|
||||
|
||||
### Decision Tree
|
||||
|
||||
```
|
||||
New request?
|
||||
├─ Bug fix restoring spec behavior? → Fix directly
|
||||
├─ Typo/format/comment? → Fix directly
|
||||
├─ New feature/capability? → Create proposal
|
||||
├─ Breaking change? → Create proposal
|
||||
├─ Architecture change? → Create proposal
|
||||
└─ Unclear? → Create proposal (safer)
|
||||
```
|
||||
|
||||
### Proposal Structure
|
||||
|
||||
1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique)
|
||||
|
||||
2. **Write proposal.md:**
|
||||
```markdown
|
||||
# Change: [Brief description of change]
|
||||
|
||||
## Why
|
||||
[1-2 sentences on problem/opportunity]
|
||||
|
||||
## What Changes
|
||||
- [Bullet list of changes]
|
||||
- [Mark breaking changes with **BREAKING**]
|
||||
|
||||
## Impact
|
||||
- Affected specs: [list capabilities]
|
||||
- Affected code: [key files/systems]
|
||||
```
|
||||
|
||||
3. **Create spec deltas:** `specs/[capability]/spec.md`
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: New Feature
|
||||
The system SHALL provide...
|
||||
|
||||
#### Scenario: Success case
|
||||
- **WHEN** user performs action
|
||||
- **THEN** expected result
|
||||
|
||||
## MODIFIED Requirements
|
||||
### Requirement: Existing Feature
|
||||
[Complete modified requirement]
|
||||
|
||||
## REMOVED Requirements
|
||||
### Requirement: Old Feature
|
||||
**Reason**: [Why removing]
|
||||
**Migration**: [How to handle]
|
||||
```
|
||||
If multiple capabilities are affected, create multiple delta files under `changes/[change-id]/specs/<capability>/spec.md`—one per capability.
|
||||
|
||||
4. **Create tasks.md:**
|
||||
```markdown
|
||||
## 1. Implementation
|
||||
- [ ] 1.1 Create database schema
|
||||
- [ ] 1.2 Implement API endpoint
|
||||
- [ ] 1.3 Add frontend component
|
||||
- [ ] 1.4 Write tests
|
||||
```
|
||||
|
||||
5. **Create design.md when needed:**
|
||||
Create `design.md` if any of the following apply; otherwise omit it:
|
||||
- Cross-cutting change (multiple services/modules) or a new architectural pattern
|
||||
- New external dependency or significant data model changes
|
||||
- Security, performance, or migration complexity
|
||||
- Ambiguity that benefits from technical decisions before coding
|
||||
|
||||
Minimal `design.md` skeleton:
|
||||
```markdown
|
||||
## Context
|
||||
[Background, constraints, stakeholders]
|
||||
|
||||
## Goals / Non-Goals
|
||||
- Goals: [...]
|
||||
- Non-Goals: [...]
|
||||
|
||||
## Decisions
|
||||
- Decision: [What and why]
|
||||
- Alternatives considered: [Options + rationale]
|
||||
|
||||
## Risks / Trade-offs
|
||||
- [Risk] → Mitigation
|
||||
|
||||
## Migration Plan
|
||||
[Steps, rollback]
|
||||
|
||||
## Open Questions
|
||||
- [...]
|
||||
```
|
||||
|
||||
## Spec File Format
|
||||
|
||||
### Critical: Scenario Formatting
|
||||
|
||||
**CORRECT** (use #### headers):
|
||||
```markdown
|
||||
#### Scenario: User login success
|
||||
- **WHEN** valid credentials provided
|
||||
- **THEN** return JWT token
|
||||
```
|
||||
|
||||
**WRONG** (don't use bullets or bold):
|
||||
```markdown
|
||||
- **Scenario: User login** ❌
|
||||
**Scenario**: User login ❌
|
||||
### Scenario: User login ❌
|
||||
```
|
||||
|
||||
Every requirement MUST have at least one scenario.
|
||||
|
||||
### Requirement Wording
|
||||
- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative)
|
||||
|
||||
### Delta Operations
|
||||
|
||||
- `## ADDED Requirements` - New capabilities
|
||||
- `## MODIFIED Requirements` - Changed behavior
|
||||
- `## REMOVED Requirements` - Deprecated features
|
||||
- `## RENAMED Requirements` - Name changes
|
||||
|
||||
Headers matched with `trim(header)` - whitespace ignored.
|
||||
|
||||
#### When to use ADDED vs MODIFIED
|
||||
- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding "Slash Command Configuration") rather than altering the semantics of an existing requirement.
|
||||
- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details.
|
||||
- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name.
|
||||
|
||||
Common pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead.
|
||||
|
||||
Authoring a MODIFIED requirement correctly:
|
||||
1) Locate the existing requirement in `openspec/specs/<capability>/spec.md`.
|
||||
2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios).
|
||||
3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior.
|
||||
4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`.
|
||||
|
||||
Example for RENAMED:
|
||||
```markdown
|
||||
## RENAMED Requirements
|
||||
- FROM: `### Requirement: Login`
|
||||
- TO: `### Requirement: User Authentication`
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Errors
|
||||
|
||||
**"Change must have at least one delta"**
|
||||
- Check `changes/[name]/specs/` exists with .md files
|
||||
- Verify files have operation prefixes (## ADDED Requirements)
|
||||
|
||||
**"Requirement must have at least one scenario"**
|
||||
- Check scenarios use `#### Scenario:` format (4 hashtags)
|
||||
- Don't use bullet points or bold for scenario headers
|
||||
|
||||
**Silent scenario parsing failures**
|
||||
- Exact format required: `#### Scenario: Name`
|
||||
- Debug with: `openspec show [change] --json --deltas-only`
|
||||
|
||||
### Validation Tips
|
||||
|
||||
```bash
|
||||
# Always use strict mode for comprehensive checks
|
||||
openspec validate [change] --strict
|
||||
|
||||
# Debug delta parsing
|
||||
openspec show [change] --json | jq '.deltas'
|
||||
|
||||
# Check specific requirement
|
||||
openspec show [spec] --json -r 1
|
||||
```
|
||||
|
||||
## Happy Path Script
|
||||
|
||||
```bash
|
||||
# 1) Explore current state
|
||||
openspec spec list --long
|
||||
openspec list
|
||||
# Optional full-text search:
|
||||
# rg -n "Requirement:|Scenario:" openspec/specs
|
||||
# rg -n "^#|Requirement:" openspec/changes
|
||||
|
||||
# 2) Choose change id and scaffold
|
||||
CHANGE=add-two-factor-auth
|
||||
mkdir -p openspec/changes/$CHANGE/{specs/auth}
|
||||
printf "## Why\n...\n\n## What Changes\n- ...\n\n## Impact\n- ...\n" > openspec/changes/$CHANGE/proposal.md
|
||||
printf "## 1. Implementation\n- [ ] 1.1 ...\n" > openspec/changes/$CHANGE/tasks.md
|
||||
|
||||
# 3) Add deltas (example)
|
||||
cat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF'
|
||||
## ADDED Requirements
|
||||
### Requirement: Two-Factor Authentication
|
||||
Users MUST provide a second factor during login.
|
||||
|
||||
#### Scenario: OTP required
|
||||
- **WHEN** valid credentials are provided
|
||||
- **THEN** an OTP challenge is required
|
||||
EOF
|
||||
|
||||
# 4) Validate
|
||||
openspec validate $CHANGE --strict
|
||||
```
|
||||
|
||||
## Multi-Capability Example
|
||||
|
||||
```
|
||||
openspec/changes/add-2fa-notify/
|
||||
├── proposal.md
|
||||
├── tasks.md
|
||||
└── specs/
|
||||
├── auth/
|
||||
│ └── spec.md # ADDED: Two-Factor Authentication
|
||||
└── notifications/
|
||||
└── spec.md # ADDED: OTP email notification
|
||||
```
|
||||
|
||||
auth/spec.md
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: Two-Factor Authentication
|
||||
...
|
||||
```
|
||||
|
||||
notifications/spec.md
|
||||
```markdown
|
||||
## ADDED Requirements
|
||||
### Requirement: OTP Email Notification
|
||||
...
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Simplicity First
|
||||
- Default to <100 lines of new code
|
||||
- Single-file implementations until proven insufficient
|
||||
- Avoid frameworks without clear justification
|
||||
- Choose boring, proven patterns
|
||||
|
||||
### Complexity Triggers
|
||||
Only add complexity with:
|
||||
- Performance data showing current solution too slow
|
||||
- Concrete scale requirements (>1000 users, >100MB data)
|
||||
- Multiple proven use cases requiring abstraction
|
||||
|
||||
### Clear References
|
||||
- Use `file.ts:42` format for code locations
|
||||
- Reference specs as `specs/auth/spec.md`
|
||||
- Link related changes and PRs
|
||||
|
||||
### Capability Naming
|
||||
- Use verb-noun: `user-auth`, `payment-capture`
|
||||
- Single purpose per capability
|
||||
- 10-minute understandability rule
|
||||
- Split if description needs "AND"
|
||||
|
||||
### Change ID Naming
|
||||
- Use kebab-case, short and descriptive: `add-two-factor-auth`
|
||||
- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-`
|
||||
- Ensure uniqueness; if taken, append `-2`, `-3`, etc.
|
||||
|
||||
## Tool Selection Guide
|
||||
|
||||
| Task | Tool | Why |
|
||||
|------|------|-----|
|
||||
| Find files by pattern | Glob | Fast pattern matching |
|
||||
| Search code content | Grep | Optimized regex search |
|
||||
| Read specific files | Read | Direct file access |
|
||||
| Explore unknown scope | Task | Multi-step investigation |
|
||||
|
||||
## Error Recovery
|
||||
|
||||
### Change Conflicts
|
||||
1. Run `openspec list` to see active changes
|
||||
2. Check for overlapping specs
|
||||
3. Coordinate with change owners
|
||||
4. Consider combining proposals
|
||||
|
||||
### Validation Failures
|
||||
1. Run with `--strict` flag
|
||||
2. Check JSON output for details
|
||||
3. Verify spec file format
|
||||
4. Ensure scenarios properly formatted
|
||||
|
||||
### Missing Context
|
||||
1. Read project.md first
|
||||
2. Check related specs
|
||||
3. Review recent archives
|
||||
4. Ask for clarification
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Stage Indicators
|
||||
- `changes/` - Proposed, not yet built
|
||||
- `specs/` - Built and deployed
|
||||
- `archive/` - Completed changes
|
||||
|
||||
### File Purposes
|
||||
- `proposal.md` - Why and what
|
||||
- `tasks.md` - Implementation steps
|
||||
- `design.md` - Technical decisions
|
||||
- `spec.md` - Requirements and behavior
|
||||
|
||||
### CLI Essentials
|
||||
```bash
|
||||
openspec list # What's in progress?
|
||||
openspec show [item] # View details
|
||||
openspec validate --strict # Is it correct?
|
||||
openspec archive <change-id> [--yes|-y] # Mark complete (add --yes for automation)
|
||||
```
|
||||
|
||||
Remember: Specs are truth. Changes are proposals. Keep them in sync.
|
||||
91
openspec/project.md
Normal file
91
openspec/project.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# Project Context
|
||||
|
||||
## Purpose
|
||||
ROI Theme es un tema WordPress profesional siguiendo Clean Architecture para el sitio analisisdepreciosunitarios.com. Proporciona un sistema de componentes configurables con panel de administración y renderizado frontend dinámico.
|
||||
|
||||
## Tech Stack
|
||||
- **CMS**: WordPress 6.x
|
||||
- **Lenguaje**: PHP 8.x (strict types)
|
||||
- **Patrón**: Clean Architecture (Domain, Application, Infrastructure)
|
||||
- **Frontend**: Bootstrap 5, Bootstrap Icons
|
||||
- **Base de Datos**: MySQL (tabla normalizada `wp_roi_theme_component_settings`)
|
||||
- **CLI**: WP-CLI para sincronización de schemas
|
||||
|
||||
## Project Conventions
|
||||
|
||||
### Code Style
|
||||
- `declare(strict_types=1)` en todos los archivos PHP
|
||||
- Namespaces: `ROITheme\[Context]\[Component]\[Layer]`
|
||||
- Clases finales por defecto
|
||||
- Propiedades `private`/`protected`
|
||||
- Escaping WordPress: `esc_html()`, `esc_attr()`, `esc_url()`, `esc_textarea()`
|
||||
|
||||
### Nomenclatura (NO NEGOCIABLE)
|
||||
| Contexto | Formato | Ejemplo |
|
||||
|-----------------------------|--------------|----------------------------------|
|
||||
| component_name (JSON/BD) | kebab-case | "featured-image" |
|
||||
| Nombre archivo schema | kebab-case | featured-image.json |
|
||||
| Carpeta de módulo | PascalCase | FeaturedImage/ |
|
||||
| Namespace PHP | PascalCase | ROITheme\Public\FeaturedImage\...|
|
||||
| Clase Renderer/FormBuilder | PascalCase | FeaturedImageRenderer |
|
||||
|
||||
### Architecture Patterns
|
||||
- **Clean Architecture**: Domain → Application → Infrastructure
|
||||
- Domain NO depende de capas superiores
|
||||
- Domain NO puede tener WordPress, echo/print, HTML
|
||||
- Application NO puede tener WordPress
|
||||
- Infrastructure implementa interfaces de Domain
|
||||
- DI via constructor (interfaces, no clases concretas)
|
||||
|
||||
### Estructura del Tema
|
||||
```
|
||||
roi-theme/
|
||||
├── Schemas/ # JSON schemas (kebab-case)
|
||||
├── Shared/ # Código compartido
|
||||
│ ├── Domain/Contracts/ # Interfaces
|
||||
│ ├── Application/UseCases/ # Casos de uso
|
||||
│ └── Infrastructure/ # Implementaciones
|
||||
├── Public/[PascalCase]/ # Renderers frontend
|
||||
├── Admin/[PascalCase]/ # FormBuilders admin
|
||||
└── functions.php # Bootstrap
|
||||
```
|
||||
|
||||
### Testing Strategy
|
||||
- Validación de arquitectura: `php Shared/Infrastructure/Scripts/validate-architecture.php [nombre]`
|
||||
- Tests unitarios pendientes de implementar
|
||||
|
||||
### Git Workflow
|
||||
- Conventional Commits: `feat:`, `fix:`, `refactor:`, `docs:`
|
||||
- Branch principal: `main`
|
||||
- Tags para releases: `v1.0.0`, `v2.0.0`
|
||||
|
||||
## Domain Context
|
||||
El sistema maneja componentes UI configurables para un sitio de análisis de precios unitarios (construcción). Cada componente tiene:
|
||||
- **Schema JSON**: Define campos configurables
|
||||
- **Renderer**: Genera HTML + CSS dinámico desde BD
|
||||
- **FormBuilder**: Panel admin para configurar valores
|
||||
|
||||
### Flujo de 5 Fases para Componentes
|
||||
1. Schema JSON → `Schemas/[nombre].json`
|
||||
2. Sincronización → `wp roi-theme sync-component [nombre]`
|
||||
3. Renderer → `Public/[Nombre]/Infrastructure/Ui/[Nombre]Renderer.php`
|
||||
4. FormBuilder → `Admin/[Nombre]/Infrastructure/Ui/[Nombre]FormBuilder.php`
|
||||
5. Validación → `validate-architecture.php [nombre]`
|
||||
|
||||
## Important Constraints
|
||||
- CERO CSS hardcodeado en PHP (usar CSSGeneratorService)
|
||||
- NO usar global $wpdb en Domain/Application
|
||||
- NO instanciar servicios directamente (usar DI)
|
||||
- NO modificar campos en BD manualmente
|
||||
- Variables CSS del tema: `--color-navy-dark`, `--color-orange-primary`
|
||||
|
||||
## External Dependencies
|
||||
- WordPress 6.x core
|
||||
- Bootstrap 5 (CSS/JS)
|
||||
- Bootstrap Icons
|
||||
- WP-CLI (`C:\xampp\php_8.0.30_backup\wp-cli.phar`)
|
||||
|
||||
## Referencias Documentación
|
||||
- Arquitectura: `_planeacion/roi-theme/_arquitectura/`
|
||||
- Template HTML: `_planeacion/roi-theme/roi-theme-template/index.html`
|
||||
- Design System: `_planeacion/roi-theme/_arquitectura/01-design-system/`
|
||||
215
openspec/specs/arquitectura-limpia/spec.md
Normal file
215
openspec/specs/arquitectura-limpia/spec.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# Especificacion de Arquitectura Limpia
|
||||
|
||||
## Purpose
|
||||
|
||||
Define la implementacion de Clean Architecture para ROITheme, un tema WordPress que sigue principios de Domain-Driven Design con separacion fisica de contextos delimitados (Admin, Public, Shared).
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Separacion Fisica de Contextos
|
||||
|
||||
The system MUST organize code into three physically delimited contexts: Admin/, Public/, and Shared/.
|
||||
|
||||
#### Scenario: Codigo pertenece al contexto de administracion
|
||||
- **WHEN** el codigo maneja operaciones CRUD, configuracion o funcionalidad del panel admin
|
||||
- **THEN** el codigo DEBE colocarse en el directorio `Admin/`
|
||||
- **AND** el codigo NO DEBE importar del directorio `Public/`
|
||||
|
||||
#### Scenario: Codigo pertenece al contexto publico/frontend
|
||||
- **WHEN** el codigo maneja renderizado, visualizacion o presentacion frontend
|
||||
- **THEN** el codigo DEBE colocarse en el directorio `Public/`
|
||||
- **AND** el codigo NO DEBE importar del directorio `Admin/`
|
||||
|
||||
#### Scenario: Codigo es compartido entre contextos
|
||||
- **WHEN** el codigo es usado por AMBOS contextos Admin/ y Public/
|
||||
- **THEN** el codigo DEBE colocarse en el directorio `Shared/` raiz
|
||||
- **AND** tanto Admin/ como Public/ PUEDEN importar de Shared/
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Organizacion Granular de Codigo Compartido
|
||||
|
||||
The system MUST implement three levels of shared code to avoid mixing context-specific shared code.
|
||||
|
||||
#### Scenario: Codigo compartido solo dentro del contexto Admin
|
||||
- **WHEN** el codigo es reutilizado por multiples modulos Admin pero NO por Public
|
||||
- **THEN** el codigo DEBE colocarse en el directorio `Admin/Shared/`
|
||||
- **AND** los modulos de Public/ NO DEBEN importar de `Admin/Shared/`
|
||||
|
||||
#### Scenario: Codigo compartido solo dentro del contexto Public
|
||||
- **WHEN** el codigo es reutilizado por multiples modulos Public pero NO por Admin
|
||||
- **THEN** el codigo DEBE colocarse en el directorio `Public/Shared/`
|
||||
- **AND** los modulos de Admin/ NO DEBEN importar de `Public/Shared/`
|
||||
|
||||
#### Scenario: Codigo compartido entre ambos contextos
|
||||
- **WHEN** el codigo es reutilizado por AMBOS modulos Admin/ y Public/
|
||||
- **THEN** el codigo DEBE colocarse en el directorio `Shared/` raiz
|
||||
- **AND** esto incluye ValueObjects, Exceptions y Contracts base
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Cada Contexto Sigue las Capas de Clean Architecture
|
||||
|
||||
Each context (Admin/, Public/, Shared/) MUST internally implement the three Clean Architecture layers: Domain, Application, Infrastructure.
|
||||
|
||||
#### Scenario: Estructura de modulo dentro del contexto Admin
|
||||
- **GIVEN** un componente llamado "Navbar" en el contexto Admin
|
||||
- **WHEN** el modulo es creado
|
||||
- **THEN** la estructura DEBE ser: Admin/Navbar/ con subcarpetas Domain/, Application/, Infrastructure/
|
||||
|
||||
#### Scenario: Estructura de modulo dentro del contexto Public
|
||||
- **GIVEN** un componente llamado "Navbar" en el contexto Public
|
||||
- **WHEN** el modulo es creado
|
||||
- **THEN** la estructura DEBE ser: Public/Navbar/ con subcarpetas Domain/, Application/, Infrastructure/
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Cumplimiento de Direccion de Dependencias
|
||||
|
||||
The system MUST enforce that dependencies flow ONLY from outer layers to inner layers.
|
||||
|
||||
#### Scenario: Infrastructure depende de Application y Domain
|
||||
- **WHEN** el codigo esta en la capa Infrastructure
|
||||
- **THEN** PUEDE importar de la capa Application
|
||||
- **AND** PUEDE importar de la capa Domain
|
||||
|
||||
#### Scenario: Application depende solo de Domain
|
||||
- **WHEN** el codigo esta en la capa Application
|
||||
- **THEN** PUEDE importar de la capa Domain
|
||||
- **AND** NO DEBE importar de la capa Infrastructure
|
||||
|
||||
#### Scenario: Domain no tiene dependencias externas
|
||||
- **WHEN** el codigo esta en la capa Domain
|
||||
- **THEN** NO DEBE importar de la capa Application
|
||||
- **AND** NO DEBE importar de la capa Infrastructure
|
||||
- **AND** NO DEBE importar funciones o globales de WordPress
|
||||
|
||||
---
|
||||
|
||||
### Requirement: La Capa Domain Contiene Solo Logica de Negocio Pura
|
||||
|
||||
The Domain layer MUST contain only pure business logic without framework dependencies.
|
||||
|
||||
#### Scenario: Validacion de contenido de capa Domain
|
||||
- **WHEN** el codigo se coloca en la capa Domain
|
||||
- **THEN** PUEDE contener Entities, Value Objects, Domain Services, Interfaces, Exceptions
|
||||
- **AND** NO DEBE contener global $wpdb, $_POST, $_GET, $_SESSION, add_action, add_filter, HTML, CSS, JavaScript
|
||||
|
||||
#### Scenario: Implementacion de entidad Domain
|
||||
- **GIVEN** una entidad Domain como NavbarConfiguration
|
||||
- **WHEN** la entidad es implementada
|
||||
- **THEN** DEBE contener reglas de negocio y validacion
|
||||
- **AND** NO DEBE contener logica de persistencia
|
||||
- **AND** NO DEBE referenciar APIs de WordPress
|
||||
|
||||
---
|
||||
|
||||
### Requirement: La Capa Application Orquesta Domain
|
||||
|
||||
The Application layer MUST orchestrate domain entities without containing business logic.
|
||||
|
||||
#### Scenario: Implementacion de Use Case
|
||||
- **WHEN** un Use Case es implementado
|
||||
- **THEN** DEBE coordinar entidades y servicios de domain
|
||||
- **AND** DEBE depender de interfaces, NO de implementaciones concretas
|
||||
- **AND** NO DEBE contener reglas de validacion de negocio
|
||||
|
||||
#### Scenario: Uso de DTOs para transferencia de datos
|
||||
- **WHEN** los datos cruzan limites entre capas
|
||||
- **THEN** se DEBEN usar DTOs (Data Transfer Objects)
|
||||
- **AND** los DTOs DEBEN ser contenedores de datos simples sin logica de negocio
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Infrastructure Implementa Interfaces
|
||||
|
||||
The Infrastructure layer MUST implement interfaces defined in Domain/Application layers.
|
||||
|
||||
#### Scenario: Implementacion de Repository
|
||||
- **GIVEN** una RepositoryInterface definida en Domain
|
||||
- **WHEN** el repository es implementado
|
||||
- **THEN** DEBE colocarse en Infrastructure/Persistence/
|
||||
- **AND** DEBE implementar la interface de Domain
|
||||
- **AND** PUEDE usar global $wpdb o APIs de WordPress
|
||||
|
||||
#### Scenario: Integracion con WordPress
|
||||
- **WHEN** se necesita codigo especifico de WordPress
|
||||
- **THEN** DEBE colocarse en la capa Infrastructure
|
||||
- **AND** NO DEBE filtrarse a las capas Domain o Application
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Los Modulos Son Autocontenidos e Independientes
|
||||
|
||||
Each module (Navbar, Footer, Toolbar, etc.) MUST be self-contained and independent from other modules.
|
||||
|
||||
#### Scenario: Aislamiento de modulos
|
||||
- **WHEN** un modulo como Admin/Navbar/ es implementado
|
||||
- **THEN** NO DEBE importar de Admin/Footer/
|
||||
- **AND** NO DEBE importar de Admin/Toolbar/
|
||||
- **AND** SOLO PUEDE importar de Shared/
|
||||
|
||||
#### Scenario: Eliminacion de modulos
|
||||
- **WHEN** un modulo necesita ser eliminado
|
||||
- **THEN** borrar la carpeta del modulo NO DEBE romper otros modulos
|
||||
- **AND** no se DEBERIAN requerir cambios de codigo en otros modulos
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Admin y Public Son Bounded Contexts Separados
|
||||
|
||||
Admin/ and Public/ MUST be treated as separate bounded contexts because they have different responsibilities.
|
||||
|
||||
#### Scenario: Responsabilidad del contexto Admin
|
||||
- **WHEN** el codigo maneja administracion de componentes
|
||||
- **THEN** la entidad Domain se enfoca en configuracion, validacion, estados draft/published
|
||||
- **AND** los Use Cases se enfocan en operaciones Save, Update, Delete, Get
|
||||
|
||||
#### Scenario: Responsabilidad del contexto Public
|
||||
- **WHEN** el codigo maneja renderizado de componentes
|
||||
- **THEN** la entidad Domain se enfoca en estado activo, caching, filtrado por permisos
|
||||
- **AND** los Use Cases se enfocan en operaciones GetActive, Render, Cache
|
||||
|
||||
#### Scenario: No hay duplicacion de domain
|
||||
- **WHEN** Admin/Navbar/Domain/ y Public/Navbar/Domain/ ambos existen
|
||||
- **THEN** NO son duplicados sino bounded contexts especializados
|
||||
- **AND** Admin se enfoca en configuracion/gestion
|
||||
- **AND** Public se enfoca en renderizado/visualizacion
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Validacion de Arquitectura Antes de Commit
|
||||
|
||||
The system MUST validate architectural compliance before committing code.
|
||||
|
||||
#### Scenario: Validacion de capa Domain
|
||||
- **WHEN** se valida codigo de la capa Domain
|
||||
- **THEN** grep por global $wpdb DEBE retornar vacio
|
||||
- **AND** grep por add_action DEBE retornar vacio
|
||||
- **AND** grep por $_POST DEBE retornar vacio
|
||||
|
||||
#### Scenario: Validacion de dependencias de modulos
|
||||
- **WHEN** se validan dependencias entre modulos
|
||||
- **THEN** imports de Admin/Navbar/ desde Admin/Footer/ NO DEBEN existir
|
||||
- **AND** imports de Public/Navbar/ desde Public/Footer/ NO DEBEN existir
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Realizacion de Beneficios de la Arquitectura
|
||||
|
||||
The architecture MUST provide measurable benefits.
|
||||
|
||||
#### Scenario: Asignacion granular de trabajo
|
||||
- **WHEN** un desarrollador es asignado a trabajar en Admin/Navbar/
|
||||
- **THEN** puede acceder SOLO a esa carpeta
|
||||
- **AND** no puede ver ni modificar Public/ u otros modulos de Admin/
|
||||
|
||||
#### Scenario: Eliminacion facil de modulos
|
||||
- **WHEN** un componente ya no es necesario
|
||||
- **THEN** eliminarlo requiere solo borrar la carpeta
|
||||
- **AND** no se necesitan otras modificaciones de codigo
|
||||
|
||||
#### Scenario: Codigo compartido consistente
|
||||
- **WHEN** se encuentra un bug en un ValueObject compartido
|
||||
- **THEN** arreglarlo en Shared/Domain/ValueObjects/ lo arregla para TODOS los modulos
|
||||
- **AND** no se necesita actualizar codigo duplicado
|
||||
350
openspec/specs/estandares-codigo/spec.md
Normal file
350
openspec/specs/estandares-codigo/spec.md
Normal file
@@ -0,0 +1,350 @@
|
||||
# Especificacion de Estandares de Codigo
|
||||
|
||||
## Purpose
|
||||
|
||||
Define los principios SOLID, estandares de POO (Programacion Orientada a Objetos) y estandares generales de codigo que DEBEN seguirse en el desarrollo de ROITheme.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Principio de Responsabilidad Unica (SRP)
|
||||
|
||||
Each class MUST have exactly one reason to change and one responsibility.
|
||||
|
||||
#### Scenario: Responsabilidad de clase Use Case
|
||||
- **WHEN** se crea una clase Use Case
|
||||
- **THEN** DEBE manejar exactamente UNA operacion (Save, Get, Delete, etc.)
|
||||
- **AND** NO DEBE combinar multiples operaciones en una clase
|
||||
|
||||
#### Scenario: Validacion de tamano de clase
|
||||
- **WHEN** se crea un archivo de clase
|
||||
- **THEN** DEBERIA tener menos de 300 lineas
|
||||
- **AND** DEBERIA tener maximo 3-5 metodos privados
|
||||
- **AND** el nombre de la clase DEBE describir su unica responsabilidad
|
||||
|
||||
#### Scenario: Violacion de SRP
|
||||
- **WHEN** una clase contiene save(), get(), delete(), validate(), sendEmail()
|
||||
- **THEN** DEBE dividirse en clases Use Case separadas
|
||||
- **AND** cada clase maneja solo una operacion
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Principio Abierto/Cerrado (OCP)
|
||||
|
||||
Classes MUST be open for extension but closed for modification.
|
||||
|
||||
#### Scenario: Agregar nuevo tipo de componente
|
||||
- **WHEN** se necesita un nuevo tipo de componente
|
||||
- **THEN** se DEBE crear una nueva subclase
|
||||
- **AND** la clase BaseComponent existente NO DEBE modificarse
|
||||
- **AND** NO se DEBERIAN agregar cadenas if/elseif para nuevos tipos
|
||||
|
||||
#### Scenario: Extender funcionalidad base
|
||||
- **GIVEN** que existe una clase abstracta BaseComponent
|
||||
- **WHEN** se necesita comportamiento especializado
|
||||
- **THEN** se DEBE usar herencia para extender
|
||||
- **AND** la clase base DEBE permanecer sin cambios
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Principio de Sustitucion de Liskov (LSP)
|
||||
|
||||
Subclasses MUST be substitutable for their base classes without breaking functionality.
|
||||
|
||||
#### Scenario: Uso polimorfico
|
||||
- **GIVEN** una funcion que acepta parametro BaseComponent
|
||||
- **WHEN** cualquier subclase es pasada
|
||||
- **THEN** la funcion DEBE funcionar correctamente
|
||||
- **AND** NO se DEBERIAN lanzar excepciones inesperadas
|
||||
|
||||
#### Scenario: Cumplimiento de contrato
|
||||
- **WHEN** una subclase sobrescribe un metodo padre
|
||||
- **THEN** DEBE respetar el contrato del metodo original
|
||||
- **AND** las precondiciones NO DEBEN ser mas restrictivas
|
||||
- **AND** las postcondiciones NO DEBEN ser mas permisivas
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Principio de Segregacion de Interfaces (ISP)
|
||||
|
||||
Interfaces MUST be small and specific, not large and general.
|
||||
|
||||
#### Scenario: Validacion de tamano de interface
|
||||
- **WHEN** se define una interface
|
||||
- **THEN** DEBE tener maximo 3-5 metodos
|
||||
- **AND** cada metodo DEBE relacionarse con la misma capacidad
|
||||
|
||||
#### Scenario: Evitar interfaces gordas
|
||||
- **WHEN** existen multiples capacidades no relacionadas
|
||||
- **THEN** se DEBEN crear interfaces separadas
|
||||
- **AND** las clases implementan solo las interfaces que usan
|
||||
- **AND** NO se permiten metodos dummy "No implementado"
|
||||
|
||||
#### Scenario: Diseno correcto de interface
|
||||
- **WHEN** se necesita funcionalidad de cache
|
||||
- **THEN** se DEBE usar CacheInterface con get(), set(), delete()
|
||||
- **AND** ValidatorInterface con validate() es separada
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Principio de Inversion de Dependencias (DIP)
|
||||
|
||||
High-level modules MUST depend on abstractions, not concrete implementations.
|
||||
|
||||
#### Scenario: Inyeccion por constructor con interfaces
|
||||
- **WHEN** una clase necesita dependencias
|
||||
- **THEN** el constructor DEBE recibir interfaces, NO clases concretas
|
||||
- **AND** NO debe haber new ClaseConcreta() dentro del cuerpo de la clase
|
||||
|
||||
#### Scenario: Cableado del Contenedor DI
|
||||
- **WHEN** se necesitan implementaciones concretas
|
||||
- **THEN** el DIContainer DEBE manejar el cableado
|
||||
- **AND** las clases permanecen desacopladas de las implementaciones
|
||||
|
||||
#### Scenario: Dependencia incorrecta
|
||||
- **WHEN** el constructor hace this->repo = new WordPressNavbarRepository()
|
||||
- **THEN** esto DEBE refactorizarse para recibir NavbarRepositoryInterface
|
||||
- **AND** el DIContainer proporciona la implementacion concreta
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Encapsulacion de Propiedades
|
||||
|
||||
Class properties MUST be encapsulated with controlled access.
|
||||
|
||||
#### Scenario: Visibilidad de propiedades
|
||||
- **WHEN** se define una propiedad de clase
|
||||
- **THEN** DEBE ser private o protected
|
||||
- **AND** el acceso DEBE ser via metodos getter
|
||||
- **AND** la mutacion DEBE ser via metodos setter o metodos de negocio
|
||||
|
||||
#### Scenario: Encapsulacion de Value Object
|
||||
- **GIVEN** un ValueObject como ComponentName
|
||||
- **WHEN** es construido
|
||||
- **THEN** la validacion DEBE ocurrir en el constructor
|
||||
- **AND** el valor DEBE ser inmutable despues de la construccion
|
||||
- **AND** los detalles internos NO DEBEN exponerse
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Guias de Herencia
|
||||
|
||||
Inheritance MUST be used appropriately with limited depth.
|
||||
|
||||
#### Scenario: Limite de profundidad de herencia
|
||||
- **WHEN** se usa herencia
|
||||
- **THEN** la profundidad maxima DEBE ser 2-3 niveles
|
||||
- **AND** las cadenas de herencia profundas DEBEN evitarse
|
||||
|
||||
#### Scenario: Comportamiento comun en clase base
|
||||
- **WHEN** multiples clases comparten comportamiento comun
|
||||
- **THEN** se DEBERIA crear una clase base abstracta
|
||||
- **AND** las subclases especializan con comportamiento adicional
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Polimorfismo Correcto
|
||||
|
||||
Methods MUST accept base types or interfaces to enable polymorphism.
|
||||
|
||||
#### Scenario: Tipos de parametros de metodo
|
||||
- **WHEN** un metodo acepta parametro de componente
|
||||
- **THEN** el type hint DEBERIA ser BaseComponent o ComponentInterface
|
||||
- **AND** cualquier subclase/implementacion DEBE funcionar correctamente
|
||||
|
||||
#### Scenario: Polimorfismo de repository
|
||||
- **WHEN** un Use Case usa un repository
|
||||
- **THEN** DEBE aceptar RepositoryInterface
|
||||
- **AND** WordPressRepository y MockRepository funcionan transparentemente
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Estandares PHP Estrictos
|
||||
|
||||
All PHP code MUST follow strict type safety and naming conventions.
|
||||
|
||||
#### Scenario: Declaracion de tipos estrictos
|
||||
- **WHEN** se crea un archivo PHP
|
||||
- **THEN** DEBE comenzar con declare(strict_types=1)
|
||||
- **AND** los tipos de retorno DEBEN declararse
|
||||
- **AND** los tipos de parametros DEBEN declararse
|
||||
|
||||
#### Scenario: Convencion de namespace
|
||||
- **WHEN** se crea una clase
|
||||
- **THEN** el namespace DEBE seguir ROITheme\[Contexto]\[Componente]\[Capa]
|
||||
- **AND** DEBE soportar autoloading PSR-4
|
||||
|
||||
#### Scenario: Declaracion de clase
|
||||
- **WHEN** se crea una clase
|
||||
- **THEN** DEBERIA ser final por defecto
|
||||
- **AND** solo hacerla no-final cuando se pretende herencia
|
||||
- **AND** el nombre de clase DEBE ser PascalCase
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Modularidad del Codigo
|
||||
|
||||
Code MUST be organized into independent and cohesive modules.
|
||||
|
||||
#### Scenario: Independencia de modulos
|
||||
- **WHEN** se crea un modulo
|
||||
- **THEN** DEBE ser autocontenido
|
||||
- **AND** NO DEBE depender de otros modulos (solo de Shared/)
|
||||
- **AND** eliminarlo NO DEBE romper otros modulos
|
||||
|
||||
#### Scenario: Alta cohesion
|
||||
- **WHEN** el codigo se coloca en un modulo
|
||||
- **THEN** todo el codigo DEBE relacionarse con el proposito de ese modulo
|
||||
- **AND** el codigo no relacionado DEBE estar en Shared/ u otro modulo
|
||||
|
||||
#### Scenario: Bajo acoplamiento
|
||||
- **WHEN** los modulos interactuan
|
||||
- **THEN** DEBEN comunicarse a traves de interfaces de Shared/
|
||||
- **AND** las dependencias directas entre modulos estan prohibidas
|
||||
|
||||
---
|
||||
|
||||
### Requirement: DRY - No Te Repitas
|
||||
|
||||
Code duplication MUST be eliminated through appropriate abstraction.
|
||||
|
||||
#### Scenario: Ubicacion de codigo compartido
|
||||
- **WHEN** el codigo es usado por multiples modulos
|
||||
- **THEN** DEBE moverse al nivel apropiado de Shared/
|
||||
- **AND** los modulos DEBEN importar de Shared/
|
||||
|
||||
#### Scenario: Deteccion de duplicacion
|
||||
- **WHEN** existe codigo similar en 2+ lugares
|
||||
- **THEN** DEBE refactorizarse a Shared/
|
||||
- **AND** las ubicaciones originales importan de Shared/
|
||||
|
||||
---
|
||||
|
||||
### Requirement: KISS - Mantenlo Simple
|
||||
|
||||
Solutions MUST be simple and avoid over-engineering.
|
||||
|
||||
#### Scenario: Uso de patrones
|
||||
- **WHEN** se considera un patron de diseno
|
||||
- **THEN** DEBE resolver un problema real
|
||||
- **AND** se DEBEN preferir soluciones mas simples
|
||||
- **AND** la abstraccion excesiva DEBE evitarse
|
||||
|
||||
#### Scenario: Claridad del codigo
|
||||
- **WHEN** se escribe codigo
|
||||
- **THEN** DEBERIA ser auto-documentado
|
||||
- **AND** los comentarios DEBERIAN ser innecesarios para entender
|
||||
- **AND** la logica compleja DEBERIA extraerse a metodos bien nombrados
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Separacion de Responsabilidades por Capa
|
||||
|
||||
Each layer MUST have distinct responsibilities.
|
||||
|
||||
#### Scenario: Responsabilidades por capa
|
||||
- **WHEN** se escribe codigo
|
||||
- **THEN** Domain contiene logica de negocio
|
||||
- **AND** Application contiene orquestacion
|
||||
- **AND** Infrastructure contiene implementacion tecnica
|
||||
- **AND** UI contiene solo presentacion
|
||||
|
||||
#### Scenario: Validacion de responsabilidades cruzadas
|
||||
- **WHEN** se valida ubicacion de codigo
|
||||
- **THEN** SQL NO DEBE estar en Domain/Application
|
||||
- **AND** HTML NO DEBE estar en Domain/Application
|
||||
- **AND** logica de negocio NO DEBE estar en Infrastructure
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Limites de Tamano de Archivo
|
||||
|
||||
Files MUST be kept small and focused.
|
||||
|
||||
#### Scenario: Tamano de archivo de clase
|
||||
- **WHEN** se crea un archivo de clase
|
||||
- **THEN** DEBERIA tener menos de 300 lineas
|
||||
- **AND** si es mas grande, DEBERIA dividirse en clases mas pequenas
|
||||
|
||||
#### Scenario: Tamano de metodo
|
||||
- **WHEN** se escribe un metodo
|
||||
- **THEN** DEBERIA tener menos de 30 lineas
|
||||
- **AND** metodos complejos DEBERIAN extraerse a metodos auxiliares
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Convenciones de Nomenclatura
|
||||
|
||||
Names MUST be clear, descriptive, and follow conventions.
|
||||
|
||||
#### Scenario: Nomenclatura de clases
|
||||
- **WHEN** se nombra una clase
|
||||
- **THEN** el nombre DEBE describir su unica responsabilidad
|
||||
- **AND** las clases Use Case DEBEN nombrarse [Accion][Entidad]UseCase
|
||||
- **AND** las clases Repository DEBEN nombrarse [Implementacion][Entidad]Repository
|
||||
|
||||
#### Scenario: Nomenclatura de metodos
|
||||
- **WHEN** se nombra un metodo
|
||||
- **THEN** DEBE describir lo que hace el metodo
|
||||
- **AND** DEBERIA comenzar con un verbo
|
||||
- **AND** metodos booleanos DEBERIAN comenzar con is/has/can
|
||||
|
||||
#### Scenario: Nomenclatura de variables
|
||||
- **WHEN** se nombra una variable
|
||||
- **THEN** DEBE ser descriptiva
|
||||
- **AND** las abreviaturas DEBEN evitarse
|
||||
- **AND** nombres de una letra solo para contadores de bucle
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Validacion Pre-Commit
|
||||
|
||||
Code MUST pass validation before commit.
|
||||
|
||||
#### Scenario: Verificacion de cumplimiento SOLID
|
||||
- **WHEN** el codigo esta listo para commit
|
||||
- **THEN** SRP cada clase tiene una responsabilidad
|
||||
- **AND** OCP nuevas caracteristicas via extension, no modificacion
|
||||
- **AND** LSP las subclases son sustituibles
|
||||
- **AND** ISP las interfaces son pequenas 3-5 metodos
|
||||
- **AND** DIP el constructor recibe interfaces
|
||||
|
||||
#### Scenario: Verificacion de cumplimiento POO
|
||||
- **WHEN** el codigo esta listo para commit
|
||||
- **THEN** las propiedades son private/protected
|
||||
- **AND** la profundidad de herencia es max 2-3 niveles
|
||||
- **AND** el polimorfismo esta implementado correctamente
|
||||
- **AND** la abstraccion oculta complejidad
|
||||
|
||||
#### Scenario: Verificacion de calidad
|
||||
- **WHEN** el codigo esta listo para commit
|
||||
- **THEN** los archivos tienen menos de 300 lineas
|
||||
- **AND** los nombres son claros y descriptivos
|
||||
- **AND** no existe duplicacion de codigo
|
||||
- **AND** no hay sobre-ingenieria presente
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Escaping Obligatorio en Output HTML
|
||||
|
||||
All HTML output MUST use WordPress escaping functions for security.
|
||||
|
||||
#### Scenario: Escaping de textos
|
||||
- **WHEN** se genera output de texto en HTML
|
||||
- **THEN** DEBE usar esc_html() para contenido de texto
|
||||
|
||||
#### Scenario: Escaping de atributos
|
||||
- **WHEN** se genera un atributo HTML
|
||||
- **THEN** DEBE usar esc_attr() para valores de atributos
|
||||
|
||||
#### Scenario: Escaping de URLs
|
||||
- **WHEN** se genera una URL en href o src
|
||||
- **THEN** DEBE usar esc_url() para URLs
|
||||
|
||||
#### Scenario: Escaping de textareas
|
||||
- **WHEN** se genera contenido para textarea
|
||||
- **THEN** DEBE usar esc_textarea() para el valor
|
||||
|
||||
#### Scenario: Prohibicion de output sin escaping
|
||||
- **WHEN** se revisa codigo de Renderer o FormBuilder
|
||||
- **THEN** NO DEBE existir echo o print de variables sin escaping
|
||||
- **AND** NO DEBE existir interpolacion directa de variables en HTML
|
||||
406
openspec/specs/flujo-componentes/spec.md
Normal file
406
openspec/specs/flujo-componentes/spec.md
Normal file
@@ -0,0 +1,406 @@
|
||||
# Especificacion de Flujo de Componentes
|
||||
|
||||
## Purpose
|
||||
|
||||
Define el flujo de trabajo de 5 fases para crear componentes en ROITheme, incluyendo convenciones de nomenclatura, estructura de archivos y validacion.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Nomenclatura NO NEGOCIABLE
|
||||
|
||||
The system MUST follow strict naming conventions that are NON-NEGOTIABLE.
|
||||
|
||||
#### Scenario: Nomenclatura de component_name en JSON y BD
|
||||
- **WHEN** se define el nombre del componente en JSON o base de datos
|
||||
- **THEN** DEBE usar kebab-case
|
||||
- **AND** ejemplo: featured-image, hero-section, top-bar
|
||||
|
||||
#### Scenario: Nomenclatura de archivo schema JSON
|
||||
- **WHEN** se crea un archivo de schema JSON
|
||||
- **THEN** el nombre DEBE ser kebab-case
|
||||
- **AND** ejemplo: featured-image.json, hero-section.json
|
||||
|
||||
#### Scenario: Nomenclatura de carpeta de modulo
|
||||
- **WHEN** se crea la carpeta del modulo
|
||||
- **THEN** DEBE usar PascalCase
|
||||
- **AND** ejemplo: FeaturedImage/, HeroSection/, TopBar/
|
||||
|
||||
#### Scenario: Nomenclatura de namespace PHP
|
||||
- **WHEN** se define el namespace PHP
|
||||
- **THEN** DEBE usar PascalCase
|
||||
- **AND** patron: ROITheme\[Contexto]\[Componente]\[Capa]
|
||||
|
||||
#### Scenario: Nomenclatura de clases Renderer y FormBuilder
|
||||
- **WHEN** se nombran las clases Renderer o FormBuilder
|
||||
- **THEN** DEBEN usar PascalCase
|
||||
- **AND** ejemplo: FeaturedImageRenderer, HeroSectionFormBuilder
|
||||
|
||||
#### Scenario: Conversion kebab-case a PascalCase
|
||||
- **WHEN** se convierte de kebab-case a PascalCase
|
||||
- **THEN** se eliminan los guiones
|
||||
- **AND** se capitaliza cada palabra
|
||||
- **AND** ejemplo: featured-image se convierte en FeaturedImage
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Fase 1 - Creacion de Schema JSON
|
||||
|
||||
The first step MUST be creating the component JSON schema.
|
||||
|
||||
#### Scenario: Ubicacion del schema
|
||||
- **WHEN** se crea un schema JSON
|
||||
- **THEN** DEBE colocarse en Schemas/[nombre-en-kebab-case].json
|
||||
- **AND** ejemplo: Schemas/featured-image.json
|
||||
|
||||
#### Scenario: Campo component_name en schema
|
||||
- **WHEN** se define component_name en el schema
|
||||
- **THEN** DEBE usar kebab-case
|
||||
- **AND** ejemplo: component_name con valor featured-image
|
||||
|
||||
#### Scenario: Fuente del schema
|
||||
- **WHEN** se extrae informacion para el schema
|
||||
- **THEN** DEBE basarse en _planeacion/roi-theme/roi-theme-template/index.html
|
||||
- **AND** DEBEN extraerse TODOS los campos CSS y textos del HTML
|
||||
|
||||
#### Scenario: Campos obligatorios de visibilidad
|
||||
- **WHEN** se crea un schema
|
||||
- **THEN** DEBE incluir is_enabled como boolean
|
||||
- **AND** DEBE incluir show_on_desktop como boolean
|
||||
- **AND** DEBE incluir show_on_mobile como boolean
|
||||
|
||||
#### Scenario: Grupos JSON estandar con priorities
|
||||
- **WHEN** se estructura un schema JSON
|
||||
- **THEN** DEBE organizar campos en los 12 grupos estandar con priorities fijas
|
||||
- **AND** VISIBILITY DEBE tener priority 10
|
||||
- **AND** CONTENT DEBE tener priority 20
|
||||
- **AND** TYPOGRAPHY DEBE tener priority 30
|
||||
- **AND** COLORS DEBE tener priority 40
|
||||
- **AND** SPACING DEBE tener priority 50
|
||||
- **AND** VISUAL_EFFECTS DEBE tener priority 60
|
||||
- **AND** BEHAVIOR DEBE tener priority 70
|
||||
- **AND** LAYOUT DEBE tener priority 80
|
||||
- **AND** LINKS, ICONS, MEDIA, FORMS DEBEN tener priority 90
|
||||
|
||||
#### Scenario: Tipos de campo validos en schema
|
||||
- **WHEN** se define un campo en el schema JSON
|
||||
- **THEN** el type DEBE ser uno de: boolean, text, textarea, url, select, color
|
||||
- **AND** si type es select DEBE incluir array de options
|
||||
- **AND** si type es boolean el default DEBE ser true o false
|
||||
- **AND** si type es color el default DEBE ser formato hexadecimal (#RRGGBB)
|
||||
|
||||
#### Scenario: Campo heading_level para semantica HTML
|
||||
- **WHEN** un componente tiene titulo principal
|
||||
- **THEN** el grupo TYPOGRAPHY DEBE incluir campo heading_level
|
||||
- **AND** heading_level DEBE ser tipo select con options h1, h2, h3, h4, h5, h6
|
||||
- **AND** heading_level es critico para jerarquia semantica y SEO
|
||||
|
||||
#### Scenario: Campos de accesibilidad en MEDIA
|
||||
- **WHEN** un componente tiene imagenes
|
||||
- **THEN** el grupo MEDIA DEBE incluir campo image_alt
|
||||
- **AND** image_alt es obligatorio para accesibilidad (WCAG)
|
||||
- **WHEN** la imagen puede compartirse en redes sociales
|
||||
- **THEN** DEBE incluir campo is_og_image como boolean
|
||||
|
||||
#### Scenario: Campos tipicos del grupo BEHAVIOR
|
||||
- **WHEN** un componente tiene comportamiento interactivo
|
||||
- **THEN** el grupo BEHAVIOR puede incluir is_sticky, sticky_offset, collapse_on_mobile
|
||||
- **AND** para Table of Contents puede incluir generate_jump_links, enable_scrollspy
|
||||
- **AND** para animaciones puede incluir animation_type
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Fase 2 - Sincronizacion JSON a BD
|
||||
|
||||
The second step MUST synchronize the JSON schema with the database.
|
||||
|
||||
#### Scenario: Comando de sincronizacion
|
||||
- **WHEN** se necesita sincronizar un componente
|
||||
- **THEN** ejecutar wp roi-theme sync-component [nombre]
|
||||
- **AND** ejemplo: wp roi-theme sync-component featured-image
|
||||
|
||||
#### Scenario: Tabla destino
|
||||
- **WHEN** se sincroniza
|
||||
- **THEN** los datos van a la tabla wp_roi_theme_component_settings
|
||||
|
||||
#### Scenario: Preservacion de valores
|
||||
- **WHEN** se sincroniza un schema actualizado
|
||||
- **THEN** los valores existentes del usuario DEBEN preservarse
|
||||
- **AND** solo se agregan campos nuevos
|
||||
|
||||
#### Scenario: Conversion de valores para almacenamiento
|
||||
- **WHEN** se sincroniza un campo a BD
|
||||
- **THEN** arrays y objects DEBEN convertirse con json_encode()
|
||||
- **AND** booleans DEBEN convertirse a '1' o '0'
|
||||
- **AND** otros tipos DEBEN convertirse a string con cast
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Fase 3 - Creacion del Renderer
|
||||
|
||||
The third step MUST create the Renderer that converts DB data to HTML + CSS.
|
||||
|
||||
#### Scenario: Ubicacion del Renderer
|
||||
- **WHEN** se crea un Renderer
|
||||
- **THEN** DEBE colocarse en Public/[PascalCase]/Infrastructure/Ui/[PascalCase]Renderer.php
|
||||
- **AND** ejemplo: Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php
|
||||
|
||||
#### Scenario: Inyeccion de CSSGeneratorInterface
|
||||
- **WHEN** se implementa un Renderer
|
||||
- **THEN** DEBE inyectar CSSGeneratorInterface via constructor
|
||||
- **AND** NO DEBE tener CSS hardcodeado
|
||||
|
||||
#### Scenario: Generacion de CSS
|
||||
- **WHEN** se genera CSS en el Renderer
|
||||
- **THEN** DEBE usar $this->cssGenerator->generate()
|
||||
- **AND** CERO CSS hardcodeado en el codigo PHP
|
||||
|
||||
#### Scenario: Validacion de visibilidad
|
||||
- **WHEN** el Renderer procesa un componente
|
||||
- **THEN** DEBE validar is_enabled, show_on_desktop, show_on_mobile
|
||||
- **AND** NO renderizar si no cumple condiciones de visibilidad
|
||||
|
||||
#### Scenario: Clases responsive Bootstrap para visibilidad
|
||||
- **WHEN** el Renderer genera HTML para show_on_desktop y show_on_mobile
|
||||
- **THEN** DEBE usar clases Bootstrap d-none, d-lg-block, d-lg-none
|
||||
- **AND** NO DEBE usar CSS custom para visibilidad responsive
|
||||
- **AND** el breakpoint principal es lg (992px)
|
||||
|
||||
#### Scenario: Tabla de decision para visibilidad responsive
|
||||
- **GIVEN** campos show_on_desktop y show_on_mobile del componente
|
||||
- **WHEN** show_on_desktop es false AND show_on_mobile es true
|
||||
- **THEN** aplicar clase d-lg-none (solo visible en mobile)
|
||||
- **WHEN** show_on_desktop es true AND show_on_mobile es false
|
||||
- **THEN** aplicar clases d-none d-lg-block (solo visible en desktop)
|
||||
- **WHEN** show_on_desktop es false AND show_on_mobile es false
|
||||
- **THEN** NO renderizar componente y retornar string vacio
|
||||
- **WHEN** show_on_desktop es true AND show_on_mobile es true
|
||||
- **THEN** NO aplicar clases de visibilidad (visible en ambos)
|
||||
|
||||
#### Scenario: Metodo supports
|
||||
- **WHEN** se implementa el metodo supports()
|
||||
- **THEN** DEBE retornar el nombre en kebab-case
|
||||
- **AND** ejemplo: return 'featured-image'
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Contrato de CSSGeneratorInterface
|
||||
|
||||
The CSS generation service MUST follow a specific contract defined in Shared.
|
||||
|
||||
#### Scenario: Ubicacion de CSSGeneratorInterface
|
||||
- **WHEN** se necesita la interface de generacion CSS
|
||||
- **THEN** DEBE estar en Shared/Domain/Contracts/CSSGeneratorInterface.php
|
||||
|
||||
#### Scenario: Firma del metodo generate
|
||||
- **WHEN** se implementa CSSGeneratorInterface
|
||||
- **THEN** DEBE tener metodo generate(string $selector, array $styles): string
|
||||
- **AND** $selector es el selector CSS (ej: '.navbar')
|
||||
- **AND** $styles es array asociativo de propiedades CSS
|
||||
- **AND** retorna string CSS formateado
|
||||
|
||||
#### Scenario: Conversion de propiedades CSS
|
||||
- **WHEN** CSSGeneratorService procesa array de estilos
|
||||
- **THEN** DEBE convertir snake_case a kebab-case
|
||||
- **AND** ejemplo: background_color se convierte en background-color
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Fase 4 - Creacion del FormBuilder
|
||||
|
||||
The fourth step MUST create the FormBuilder for the admin panel.
|
||||
|
||||
#### Scenario: Ubicacion del FormBuilder
|
||||
- **WHEN** se crea un FormBuilder
|
||||
- **THEN** DEBE colocarse en Admin/[PascalCase]/Infrastructure/Ui/[PascalCase]FormBuilder.php
|
||||
- **AND** ejemplo: Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php
|
||||
|
||||
#### Scenario: Inyeccion de AdminDashboardRenderer
|
||||
- **WHEN** se implementa un FormBuilder
|
||||
- **THEN** DEBE inyectar AdminDashboardRenderer
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Contrato de AdminDashboardRenderer
|
||||
|
||||
The admin panel rendering service MUST follow a specific contract.
|
||||
|
||||
#### Scenario: Ubicacion de AdminDashboardRenderer
|
||||
- **WHEN** se necesita el renderer del panel admin
|
||||
- **THEN** DEBE estar en Admin/Shared/Infrastructure/Ui/AdminDashboardRenderer.php
|
||||
|
||||
#### Scenario: Responsabilidad de AdminDashboardRenderer
|
||||
- **WHEN** se usa AdminDashboardRenderer
|
||||
- **THEN** DEBE generar el HTML de controles de formulario
|
||||
- **AND** DEBE aplicar el Design System del admin (gradiente, bordes)
|
||||
- **AND** DEBE usar Bootstrap 5 form controls
|
||||
|
||||
#### Scenario: Design System del admin
|
||||
- **WHEN** se implementa la UI del admin
|
||||
- **THEN** DEBE usar gradiente #0E2337 a #1e3a5f
|
||||
- **AND** borde naranja #FF8600
|
||||
- **AND** Bootstrap 5 form controls
|
||||
|
||||
#### Scenario: Registro en getComponents
|
||||
- **WHEN** se registra el FormBuilder
|
||||
- **THEN** DEBE registrarse en getComponents() con ID en kebab-case
|
||||
- **AND** ejemplo: 'featured-image'
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Fase 5 - Validacion de Arquitectura
|
||||
|
||||
The fifth and final step MUST validate the component architecture.
|
||||
|
||||
#### Scenario: Comando de validacion
|
||||
- **WHEN** se necesita validar un componente
|
||||
- **THEN** ejecutar php Shared/Infrastructure/Scripts/validate-architecture.php [nombre]
|
||||
- **AND** ejemplo: php Shared/Infrastructure/Scripts/validate-architecture.php featured-image
|
||||
|
||||
#### Scenario: Elementos validados
|
||||
- **WHEN** se ejecuta la validacion
|
||||
- **THEN** DEBE validar estructura de carpetas
|
||||
- **AND** DEBE validar schema JSON
|
||||
- **AND** DEBE validar datos en BD
|
||||
- **AND** DEBE validar Renderer
|
||||
- **AND** DEBE validar FormBuilder
|
||||
- **AND** DEBE validar cumplimiento SOLID
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Estructura de Archivos por Componente
|
||||
|
||||
A complete component MUST have a specific file structure.
|
||||
|
||||
#### Scenario: Estructura de componente en Public
|
||||
- **GIVEN** un componente FeaturedImage en Public
|
||||
- **WHEN** esta completo
|
||||
- **THEN** DEBE existir Public/FeaturedImage/Infrastructure/Ui/FeaturedImageRenderer.php
|
||||
|
||||
#### Scenario: Estructura de componente en Admin
|
||||
- **GIVEN** un componente FeaturedImage en Admin
|
||||
- **WHEN** esta completo
|
||||
- **THEN** DEBE existir Admin/FeaturedImage/Infrastructure/Ui/FeaturedImageFormBuilder.php
|
||||
|
||||
#### Scenario: Archivo schema
|
||||
- **GIVEN** un componente FeaturedImage
|
||||
- **WHEN** se necesita el schema
|
||||
- **THEN** DEBE existir en Schemas/featured-image.json
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Variables CSS del Tema
|
||||
|
||||
Code MUST use the theme CSS variables.
|
||||
|
||||
#### Scenario: Variables de color disponibles
|
||||
- **WHEN** se necesitan colores
|
||||
- **THEN** DEBEN usarse las variables como --color-navy-dark y --color-orange-primary
|
||||
|
||||
#### Scenario: Prohibicion de CSS hardcodeado
|
||||
- **WHEN** se genera CSS
|
||||
- **THEN** NO DEBE haber colores hardcodeados en PHP
|
||||
- **AND** DEBE usarse CSSGeneratorService
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Reglas NO Negociables del Flujo
|
||||
|
||||
These rules MUST always be followed, without exceptions.
|
||||
|
||||
#### Scenario: Creacion de archivos
|
||||
- **WHEN** se van a crear archivos para un componente
|
||||
- **THEN** DEBE leerse primero el template HTML
|
||||
- **AND** NO crear archivos sin esa referencia
|
||||
|
||||
#### Scenario: Campos de visibilidad
|
||||
- **WHEN** se crea un schema
|
||||
- **THEN** NO DEBE omitirse ninguno de los 3 campos obligatorios de visibilidad
|
||||
|
||||
#### Scenario: CSS en Renderers
|
||||
- **WHEN** se implementa un Renderer
|
||||
- **THEN** NO DEBE haber CSS inline
|
||||
- **AND** todo via CSSGeneratorService
|
||||
|
||||
#### Scenario: Instanciacion de servicios
|
||||
- **WHEN** se necesita un servicio
|
||||
- **THEN** NO instanciar directamente con new Service()
|
||||
- **AND** DEBE usarse Inyeccion de Dependencias
|
||||
|
||||
#### Scenario: Modificacion de campos en BD
|
||||
- **WHEN** se necesita modificar campos
|
||||
- **THEN** NO modificar campos en BD manualmente
|
||||
- **AND** modificar el schema JSON y sincronizar
|
||||
|
||||
#### Scenario: Validacion de arquitectura
|
||||
- **WHEN** se completa un componente
|
||||
- **THEN** NO saltarse la validacion de arquitectura
|
||||
- **AND** ejecutar validate-architecture.php
|
||||
|
||||
#### Scenario: Fases completas
|
||||
- **WHEN** se crea un componente
|
||||
- **THEN** NO crear componentes sin completar las 5 fases
|
||||
- **AND** cada fase es obligatoria
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Comandos WP-CLI
|
||||
|
||||
WP-CLI commands MUST be executed with the correct configuration.
|
||||
|
||||
#### Scenario: Ubicacion de WP-CLI
|
||||
- **WHEN** se necesita ejecutar WP-CLI
|
||||
- **THEN** usar C:\xampp\php_8.0.30_backup\wp-cli.phar
|
||||
|
||||
#### Scenario: Sincronizar un componente
|
||||
- **WHEN** se sincroniza un componente especifico
|
||||
- **THEN** ejecutar powershell -Command "php 'C:\xampp\php_8.0.30_backup\wp-cli.phar' roi-theme sync-component [nombre]"
|
||||
|
||||
#### Scenario: Sincronizar todos los componentes
|
||||
- **WHEN** se sincroniza todo
|
||||
- **THEN** ejecutar powershell -Command "php 'C:\xampp\php_8.0.30_backup\wp-cli.phar' roi-theme sync-all-components"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Flujo de 5 Fases Secuencial
|
||||
|
||||
Component creation MUST follow the exact sequence.
|
||||
|
||||
#### Scenario: Flujo completo de 5 fases
|
||||
- **WHEN** se crea un nuevo componente
|
||||
- **THEN** Fase 1 es crear Schema JSON en Schemas/[nombre].json
|
||||
- **AND** Fase 2 es sincronizar con wp roi-theme sync-component [nombre]
|
||||
- **AND** Fase 3 es crear Renderer en Public/[Nombre]/Infrastructure/Ui/[Nombre]Renderer.php
|
||||
- **AND** Fase 4 es crear FormBuilder en Admin/[Nombre]/Infrastructure/Ui/[Nombre]FormBuilder.php
|
||||
- **AND** Fase 5 es validar con validate-architecture.php [nombre]
|
||||
|
||||
#### Scenario: No saltar fases
|
||||
- **WHEN** se desarrolla un componente
|
||||
- **THEN** NO se DEBE saltar ninguna fase
|
||||
- **AND** cada fase depende de la anterior
|
||||
- **AND** la validacion final es obligatoria
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Checklist de Componente Completo
|
||||
|
||||
A component MUST pass the checklist to be considered complete.
|
||||
|
||||
#### Scenario: Checklist de archivos
|
||||
- **WHEN** se verifica un componente
|
||||
- **THEN** DEBE existir Schemas/[nombre-kebab].json
|
||||
- **AND** DEBE existir Public/[NombrePascal]/Infrastructure/Ui/[NombrePascal]Renderer.php
|
||||
- **AND** DEBE existir Admin/[NombrePascal]/Infrastructure/Ui/[NombrePascal]FormBuilder.php
|
||||
|
||||
#### Scenario: Checklist de codigo
|
||||
- **WHEN** se verifica el codigo
|
||||
- **THEN** schema tiene los 3 campos de visibilidad
|
||||
- **AND** Renderer inyecta CSSGeneratorInterface
|
||||
- **AND** Renderer no tiene CSS hardcodeado
|
||||
- **AND** supports() retorna kebab-case
|
||||
- **AND** FormBuilder registrado con ID kebab-case
|
||||
- **AND** validacion de arquitectura pasa
|
||||
|
||||
#### Scenario: Checklist de BD
|
||||
- **WHEN** se verifica la base de datos
|
||||
- **THEN** datos sincronizados en wp_roi_theme_component_settings
|
||||
- **AND** component_name en kebab-case
|
||||
255
openspec/specs/patrones-wordpress/spec.md
Normal file
255
openspec/specs/patrones-wordpress/spec.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# Especificacion de Patrones WordPress
|
||||
|
||||
## Purpose
|
||||
|
||||
Define como integrar WordPress con Clean Architecture en ROITheme. WordPress tiene caracteristicas propias que requieren patrones especificos para mantener la arquitectura limpia.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Ubicacion de Hooks de WordPress
|
||||
|
||||
WordPress hooks (add_action, add_filter) MUST be located ONLY in Infrastructure.
|
||||
|
||||
#### Scenario: Hooks prohibidos en Domain
|
||||
- **WHEN** el codigo esta en la capa Domain
|
||||
- **THEN** NO DEBE contener add_action
|
||||
- **AND** NO DEBE contener add_filter
|
||||
- **AND** NO DEBE registrar callbacks de WordPress
|
||||
|
||||
#### Scenario: Hooks prohibidos en Application
|
||||
- **WHEN** el codigo esta en la capa Application
|
||||
- **THEN** NO DEBE contener add_action
|
||||
- **AND** NO DEBE contener add_filter
|
||||
|
||||
#### Scenario: Ubicacion correcta de hooks
|
||||
- **WHEN** se necesitan hooks de WordPress
|
||||
- **THEN** DEBEN colocarse en Infrastructure/Wordpress/[Componente]HooksRegistrar.php
|
||||
- **AND** los hooks DEBEN delegar a Use Cases
|
||||
- **AND** los hooks NO DEBEN contener logica de negocio
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Encapsulacion de wpdb
|
||||
|
||||
Access to global $wpdb MUST be encapsulated in Infrastructure repositories.
|
||||
|
||||
#### Scenario: wpdb prohibido en Domain
|
||||
- **WHEN** el codigo esta en la capa Domain
|
||||
- **THEN** NO DEBE contener global $wpdb
|
||||
- **AND** NO DEBE hacer consultas directas a base de datos
|
||||
|
||||
#### Scenario: wpdb prohibido en Application
|
||||
- **WHEN** el codigo esta en la capa Application
|
||||
- **THEN** NO DEBE contener global $wpdb
|
||||
- **AND** DEBE usar interfaces de repository
|
||||
|
||||
#### Scenario: Ubicacion correcta de wpdb
|
||||
- **WHEN** se necesita acceso a base de datos
|
||||
- **THEN** DEBE usarse en Infrastructure/Persistence/WordPress[Componente]Repository.php
|
||||
- **AND** el repository DEBE implementar una interface definida en Domain
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Ubicacion de wp_enqueue_scripts
|
||||
|
||||
Functions wp_enqueue_style and wp_enqueue_script MUST be located ONLY in Infrastructure.
|
||||
|
||||
#### Scenario: Enqueue prohibido en Domain
|
||||
- **WHEN** el codigo esta en la capa Domain
|
||||
- **THEN** NO DEBE contener wp_enqueue_style
|
||||
- **AND** NO DEBE contener wp_enqueue_script
|
||||
|
||||
#### Scenario: Enqueue prohibido en Application
|
||||
- **WHEN** el codigo esta en la capa Application
|
||||
- **THEN** NO DEBE contener funciones de enqueue
|
||||
- **AND** NO DEBE conocer detalles de assets
|
||||
|
||||
#### Scenario: Ubicacion correcta de enqueue
|
||||
- **WHEN** se necesita cargar assets
|
||||
- **THEN** DEBE colocarse en Infrastructure/Services/[Componente]AssetEnqueuer.php
|
||||
- **AND** DEBE registrarse via hooks en Infrastructure
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Ubicacion de register_post_type
|
||||
|
||||
CPT and taxonomy registration MUST be located ONLY in Infrastructure.
|
||||
|
||||
#### Scenario: CPT no es concepto de Domain
|
||||
- **WHEN** se considera donde registrar un Custom Post Type
|
||||
- **THEN** NO DEBE ir en Domain porque no es concepto de negocio
|
||||
- **AND** NO DEBE ir en Application
|
||||
- **AND** ES un detalle de implementacion de WordPress
|
||||
|
||||
#### Scenario: Ubicacion correcta de CPT
|
||||
- **WHEN** se necesita registrar un Custom Post Type
|
||||
- **THEN** DEBE colocarse en Infrastructure/Wordpress/[Componente]CPTRegistrar.php
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Ubicacion de add_shortcode
|
||||
|
||||
Shortcodes MUST be located ONLY in Infrastructure and delegate to Use Cases.
|
||||
|
||||
#### Scenario: Shortcode prohibido en Domain y Application
|
||||
- **WHEN** el codigo esta en Domain o Application
|
||||
- **THEN** NO DEBE contener add_shortcode
|
||||
|
||||
#### Scenario: Ubicacion correcta de shortcode
|
||||
- **WHEN** se necesita un shortcode
|
||||
- **THEN** DEBE colocarse en Infrastructure/Wordpress/[Componente]ShortcodeRegistrar.php
|
||||
- **AND** DEBE delegar la logica a un Use Case
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Ubicacion de Options API
|
||||
|
||||
WordPress Options API MUST be encapsulated in Infrastructure repositories.
|
||||
|
||||
#### Scenario: Options prohibidas en Domain y Application
|
||||
- **WHEN** el codigo esta en Domain o Application
|
||||
- **THEN** NO DEBE contener get_option
|
||||
- **AND** NO DEBE contener update_option
|
||||
- **AND** NO DEBE contener delete_option
|
||||
|
||||
#### Scenario: Ubicacion correcta de Options
|
||||
- **WHEN** se necesita acceso a opciones de WordPress
|
||||
- **THEN** DEBE colocarse en Infrastructure/Persistence/WordPressSettingsRepository.php
|
||||
- **AND** DEBE implementar una interface de Application
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Evitar functions.php Gigante
|
||||
|
||||
The functions.php file MUST contain only bootstrap, not logic.
|
||||
|
||||
#### Scenario: functions.php correcto
|
||||
- **WHEN** se implementa functions.php
|
||||
- **THEN** DEBE contener solo Autoloader, Container DI y Bootstrap
|
||||
- **AND** NO DEBE contener logica de negocio
|
||||
- **AND** NO DEBE contener definiciones de funciones de negocio
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Evitar Logica en Templates
|
||||
|
||||
Templates MUST contain only rendering, not business logic.
|
||||
|
||||
#### Scenario: Template prohibido con logica
|
||||
- **WHEN** un template de WordPress existe
|
||||
- **THEN** NO DEBE contener global $wpdb
|
||||
- **AND** NO DEBE contener validaciones de negocio
|
||||
- **AND** NO DEBE contener consultas a base de datos
|
||||
- **AND** NO DEBE contener cadenas if/elseif complejas
|
||||
|
||||
#### Scenario: Template correcto
|
||||
- **WHEN** se crea un template
|
||||
- **THEN** DEBE recibir un ViewModel preparado
|
||||
- **AND** DEBE contener solo HTML con escaping
|
||||
- **AND** DEBE usar esc_html(), esc_attr(), esc_url()
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Evitar wpdb Disperso
|
||||
|
||||
Usage of wpdb MUST NOT be scattered throughout the code.
|
||||
|
||||
#### Scenario: wpdb centralizado
|
||||
- **WHEN** se necesita acceso a base de datos
|
||||
- **THEN** DEBE usarse SOLO en repositories
|
||||
- **AND** los repositories DEBEN estar en Infrastructure/Persistence/
|
||||
- **AND** multiples archivos NO DEBEN tener global $wpdb
|
||||
|
||||
#### Scenario: Beneficios de centralizacion
|
||||
- **WHEN** wpdb esta centralizado en repositories
|
||||
- **THEN** cambiar la base de datos requiere modificar solo repositories
|
||||
- **AND** el testing es posible via mocks de interface
|
||||
- **AND** la logica de negocio permanece pura
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Evitar Variables Globales Masivas
|
||||
|
||||
Code MUST NOT use massive global variables.
|
||||
|
||||
#### Scenario: Globales prohibidas
|
||||
- **WHEN** se escribe codigo
|
||||
- **THEN** NO DEBE usar global $roi_config
|
||||
- **AND** NO DEBE usar singletons dispersos
|
||||
- **AND** NO DEBE crear estado global
|
||||
|
||||
#### Scenario: Solucion con Dependency Injection
|
||||
- **WHEN** se necesitan dependencias compartidas
|
||||
- **THEN** DEBE usarse un Container de Inyeccion de Dependencias
|
||||
- **AND** las dependencias se inyectan via constructor
|
||||
- **AND** no hay estado global
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Codigo Testeable
|
||||
|
||||
Code MUST be testable without WordPress installed.
|
||||
|
||||
#### Scenario: Domain testeable
|
||||
- **WHEN** se escribe codigo de Domain
|
||||
- **THEN** DEBE ser testeable sin base de datos
|
||||
- **AND** DEBE ser testeable sin WordPress
|
||||
- **AND** DEBE ser testeable sin UI
|
||||
|
||||
#### Scenario: Mocks via interfaces
|
||||
- **WHEN** se escriben tests
|
||||
- **THEN** las dependencias se mockean via interfaces
|
||||
- **AND** los tests unitarios NO requieren setup complejo
|
||||
- **AND** Domain puede probarse de forma aislada
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Arbol de Decisiones WordPress
|
||||
|
||||
WordPress code MUST be located according to code type.
|
||||
|
||||
#### Scenario: Determinar ubicacion de hooks
|
||||
- **WHEN** el codigo es add_action o add_filter
|
||||
- **THEN** va en Infrastructure/Wordpress/[Componente]HooksRegistrar.php
|
||||
|
||||
#### Scenario: Determinar ubicacion de wpdb
|
||||
- **WHEN** el codigo usa global $wpdb
|
||||
- **THEN** va en Infrastructure/Persistence/WordPress[Componente]Repository.php
|
||||
|
||||
#### Scenario: Determinar ubicacion de enqueue
|
||||
- **WHEN** el codigo usa wp_enqueue_style o wp_enqueue_script
|
||||
- **THEN** va en Infrastructure/Services/[Componente]AssetEnqueuer.php
|
||||
|
||||
#### Scenario: Determinar ubicacion de CPT
|
||||
- **WHEN** el codigo usa register_post_type o register_taxonomy
|
||||
- **THEN** va en Infrastructure/Wordpress/[Componente]CPTRegistrar.php
|
||||
|
||||
#### Scenario: Determinar ubicacion de shortcode
|
||||
- **WHEN** el codigo usa add_shortcode
|
||||
- **THEN** va en Infrastructure/Wordpress/[Componente]ShortcodeRegistrar.php
|
||||
|
||||
#### Scenario: Determinar ubicacion de options
|
||||
- **WHEN** el codigo usa get_option o update_option
|
||||
- **THEN** va en Infrastructure/Persistence/WordPressSettingsRepository.php
|
||||
|
||||
#### Scenario: Determinar ubicacion de nonces
|
||||
- **WHEN** el codigo usa wp_nonce_field o check_admin_referer
|
||||
- **THEN** va en Infrastructure/Api/Wordpress/[Componente]Controller.php
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Comandos de Validacion WordPress
|
||||
|
||||
Code MUST be validated with specific commands.
|
||||
|
||||
#### Scenario: Validar Domain sin WordPress
|
||||
- **WHEN** se valida la capa Domain
|
||||
- **THEN** grep por global $wpdb DEBE retornar vacio
|
||||
- **AND** grep por add_action DEBE retornar vacio
|
||||
- **AND** grep por $_POST DEBE retornar vacio
|
||||
|
||||
#### Scenario: Validar Application sin WordPress
|
||||
- **WHEN** se valida la capa Application
|
||||
- **THEN** grep por global $wpdb DEBE retornar vacio
|
||||
- **AND** grep por add_action DEBE retornar vacio
|
||||
- **AND** grep por wp_enqueue DEBE retornar vacio
|
||||
1259
package-lock.json
generated
1259
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "roi-theme",
|
||||
"version": "1.0.0",
|
||||
"description": "WordPress theme con Clean Architecture para analisisdepreciosunitarios.com",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build:bootstrap": "node build-bootstrap-subset.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -18,9 +19,11 @@
|
||||
"url": "https://github.com/prime-leads-app/roi-theme/issues"
|
||||
},
|
||||
"homepage": "https://github.com/prime-leads-app/roi-theme#readme",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^20.1.0",
|
||||
"@commitlint/config-conventional": "^20.0.0",
|
||||
"glob": "^13.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"purgecss": "^7.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
16
page.php
16
page.php
@@ -26,11 +26,19 @@ if (function_exists('roi_render_component')) {
|
||||
?>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<?php
|
||||
// Plan 99.15: Determinar si mostrar sidebar basándose en visibilidad de componentes
|
||||
$sidebar_components = ['table-of-contents', 'cta-box-sidebar'];
|
||||
$show_sidebar = function_exists('roi_should_render_any_wrapper')
|
||||
? roi_should_render_any_wrapper($sidebar_components)
|
||||
: true;
|
||||
$main_col_class = $show_sidebar ? 'col-lg-9' : 'col-lg-12';
|
||||
?>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
||||
<!-- Main Content Column (col-lg-9) -->
|
||||
<div class="col-lg-9">
|
||||
<!-- Main Content Column -->
|
||||
<div class="<?php echo esc_attr($main_col_class); ?>">
|
||||
|
||||
<!-- Featured Image - Componente dinámico -->
|
||||
<?php
|
||||
@@ -79,8 +87,9 @@ if (function_exists('roi_render_component')) {
|
||||
}
|
||||
?>
|
||||
|
||||
</div><!-- .col-lg-9 -->
|
||||
</div><!-- .<?php echo esc_attr($main_col_class); ?> -->
|
||||
|
||||
<?php if ($show_sidebar): ?>
|
||||
<!-- Sidebar Column (col-lg-3) -->
|
||||
<div class="col-lg-3">
|
||||
<div class="sidebar-sticky">
|
||||
@@ -99,6 +108,7 @@ if (function_exists('roi_render_component')) {
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div><!-- .row -->
|
||||
</div><!-- .container -->
|
||||
|
||||
16
single.php
16
single.php
@@ -24,11 +24,19 @@ if (function_exists('roi_render_component')) {
|
||||
?>
|
||||
|
||||
<!-- Main Content Grid (Template líneas 169-1020) -->
|
||||
<?php
|
||||
// Plan 99.15: Determinar si mostrar sidebar basándose en visibilidad de componentes
|
||||
$sidebar_components = ['table-of-contents', 'cta-box-sidebar'];
|
||||
$show_sidebar = function_exists('roi_should_render_any_wrapper')
|
||||
? roi_should_render_any_wrapper($sidebar_components)
|
||||
: true;
|
||||
$main_col_class = $show_sidebar ? 'col-lg-9' : 'col-lg-12';
|
||||
?>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
||||
<!-- Main Content Column (col-lg-9) -->
|
||||
<div class="col-lg-9">
|
||||
<!-- Main Content Column -->
|
||||
<div class="<?php echo esc_attr($main_col_class); ?>">
|
||||
|
||||
<!-- Featured Image - Componente dinámico -->
|
||||
<?php
|
||||
@@ -77,8 +85,9 @@ if (function_exists('roi_render_component')) {
|
||||
}
|
||||
?>
|
||||
|
||||
</div><!-- .col-lg-9 -->
|
||||
</div><!-- .<?php echo esc_attr($main_col_class); ?> -->
|
||||
|
||||
<?php if ($show_sidebar): ?>
|
||||
<!-- Sidebar Column (col-lg-3) -->
|
||||
<div class="col-lg-3">
|
||||
<div class="sidebar-sticky">
|
||||
@@ -97,6 +106,7 @@ if (function_exists('roi_render_component')) {
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div><!-- .row -->
|
||||
</div><!-- .container -->
|
||||
|
||||
Reference in New Issue
Block a user