diff --git a/src/TwigHooks/assets/admin/entrypoint.js b/src/TwigHooks/assets/admin/entrypoint.js
new file mode 100644
index 00000000..5de4a6d1
--- /dev/null
+++ b/src/TwigHooks/assets/admin/entrypoint.js
@@ -0,0 +1,222 @@
+import '../../../../assets/admin/entrypoint';
+import { createPopper } from '@popperjs/core';
+
+// Globalny przełącznik mechanizmu tooltipów
+let tooltipEnabled = false;
+
+// Nasłuchujemy skrótu klawiszowego (Ctrl/Cmd+Shift+K) do włączania/wyłączania tooltipów
+document.addEventListener('keydown', function(e) {
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'k') {
+ tooltipEnabled = !tooltipEnabled;
+ console.log(`Tooltips are now ${tooltipEnabled ? 'enabled' : 'disabled'}`);
+ // Jeśli wyłączamy tooltipy, usuwamy wszystkie widoczne tooltipy...
+ if (!tooltipEnabled) {
+ document.querySelectorAll('.custom-tooltip').forEach(t => t.remove());
+ // ...oraz usuwamy obramowania ze wszystkich elementów [data-hook]
+ document.querySelectorAll('[data-hook]').forEach(el => {
+ el.style.boxShadow = '';
+ });
+ } else {
+ // Gdy tooltipy zostały włączone, sprawdzamy elementy, które są już hoverowane
+ document.querySelectorAll('[data-hook]').forEach(el => {
+ if (el.matches(':hover')) {
+ // Wywołujemy syntetyczne zdarzenie, aby natychmiast pokazać tooltip
+ el.dispatchEvent(new Event('mouseenter'));
+ }
+ });
+ }
+ }
+});
+
+// Numer do przydzielenia unikalnego ID tooltipowi
+let nextTooltipId = 0;
+// Globalna tablica przechowująca pozycje już wyrenderowanych tooltipów
+const renderedTooltips = [];
+
+/**
+ * Generuje losowy kolor RGB w przedziale 50-205 dla każdego kanału.
+ */
+function generateRGBColor() {
+ const r = Math.floor(Math.random() * 156 + 50);
+ const g = Math.floor(Math.random() * 156 + 50);
+ const b = Math.floor(Math.random() * 156 + 50);
+ return `rgb(${r}, ${g}, ${b})`;
+}
+
+/**
+ * Pobiera kolor tooltipa z atrybutu lub generuje go automatycznie,
+ * zapisując wynik w atrybucie, by przy kolejnych wywołaniach użyć tego samego.
+ */
+function getTooltipColor(el) {
+ let color = el.getAttribute('data-tooltip-color');
+ if (!color) {
+ color = generateRGBColor();
+ el.setAttribute('data-tooltip-color', color);
+ }
+ return color;
+}
+
+/**
+ * Oblicza kontrastujący kolor tekstu (biały lub czarny) na podstawie podanego koloru tła.
+ */
+function getContrastingTextColor(rgbStr) {
+ const matches = rgbStr.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
+ if (!matches) return '#fff';
+ const r = parseInt(matches[1], 10);
+ const g = parseInt(matches[2], 10);
+ const b = parseInt(matches[3], 10);
+ // Prosty wzór na jasność – im jaśniejszy, tym lepszy kontrast osiągniemy czarnym tekstem
+ const brightness = (r * 299 + g * 587 + b * 114) / 1000;
+ return brightness > 128 ? '#000' : '#fff';
+}
+
+/**
+ * Sprawdza, czy tooltip, którego pozycję pobieramy (rect), koliduje
+ * z którymś z już wyrenderowanych tooltipów. Jeśli tak – dodaje dodatkowy offset.
+ */
+function adjustTooltipPosition(tooltip) {
+ const rect = tooltip.getBoundingClientRect();
+ let additionalOffset = { x: 0, y: 0 };
+
+ renderedTooltips.forEach(existing => {
+ if (Math.abs(existing.top - rect.top) < 20 && Math.abs(existing.left - rect.left) < 20) {
+ additionalOffset.x += 20;
+ additionalOffset.y += 20;
+ }
+ });
+
+ if (additionalOffset.x || additionalOffset.y) {
+ tooltip.style.transform = `translate(${additionalOffset.x}px, ${additionalOffset.y}px)`;
+ }
+
+ const newRect = tooltip.getBoundingClientRect();
+ renderedTooltips.push({ id: tooltip.dataset.tooltipId, top: newRect.top, left: newRect.left });
+}
+
+/**
+ * Usuwa pozycję tooltipa z globalnej tablicy po jego usunięciu.
+ */
+function removeTooltipPosition(tooltipId) {
+ const index = renderedTooltips.findIndex(entry => entry.id === tooltipId);
+ if (index !== -1) {
+ renderedTooltips.splice(index, 1);
+ }
+}
+
+document.querySelectorAll('[data-hook]').forEach(el => {
+ let tooltip;
+ let hideTimeout;
+
+ function showTooltip() {
+ // Jeśli mechanizm tooltipów jest wyłączony, nie robimy nic
+ if (!tooltipEnabled) return;
+
+ // Anulujemy ewentualne ukrywanie
+ clearTimeout(hideTimeout);
+
+ // Jeżeli tooltip jeszcze nie istnieje, go tworzymy
+ if (!tooltip) {
+ tooltip = document.createElement('div');
+ tooltip.className = 'custom-tooltip';
+ tooltip.dataset.tooltipId = nextTooltipId++;
+
+ // Pobieramy dane z atrybutów
+ const hook = el.getAttribute('data-hook') || 'N/A';
+ const hookable = el.getAttribute('data-hookable') || 'N/A';
+
+ // Ładne formatowanie zawartości tooltipa
+ tooltip.innerHTML = `
+
+ Skopiowano!
+ `;
+
+ // Ustawiamy kolor tooltipa i obramowania
+ const color = getTooltipColor(el);
+ tooltip.style.backgroundColor = color;
+ tooltip.style.border = `2px solid ${color}`;
+ tooltip.style.boxShadow = `0 0 5px ${color}`;
+ // Ustawiamy kontrastujący kolor tekstu
+ tooltip.style.color = getContrastingTextColor(color);
+
+ document.body.appendChild(tooltip);
+
+ // Inicjalizacja Popper.js – dynamiczne pozycjonowanie
+ createPopper(el, tooltip, {
+ placement: 'bottom',
+ modifiers: [
+ {
+ name: 'offset',
+ options: { offset: [0, 8] },
+ },
+ {
+ name: 'preventOverflow',
+ options: { boundary: 'viewport' },
+ },
+ {
+ name: 'flip',
+ options: { fallbackPlacements: ['top', 'right', 'left'] },
+ },
+ ],
+ });
+
+ // Po krótkim czasie korygujemy pozycję tooltipa
+ setTimeout(() => {
+ adjustTooltipPosition(tooltip);
+ }, 50);
+
+ // Tooltip nie znika natychmiast – gdy kursor wejdzie na tooltip, przerywamy ukrywanie
+ tooltip.addEventListener('mouseenter', () => {
+ clearTimeout(hideTimeout);
+ tooltip.style.opacity = '1';
+ });
+ tooltip.addEventListener('mouseleave', hideTooltip);
+
+ // Obsługa kopiowania – kliknięcie na element z klasą "copy"
+ tooltip.querySelectorAll('.copy').forEach(span => {
+ span.style.cursor = 'pointer';
+ span.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const textToCopy = span.textContent;
+ navigator.clipboard.writeText(textToCopy).then(() => {
+ const copiedDiv = tooltip.querySelector('.tooltip-copied');
+ if (copiedDiv) {
+ copiedDiv.textContent = `Skopiowano ${span.getAttribute('data-copy').toUpperCase()}`;
+ copiedDiv.style.display = 'block';
+ setTimeout(() => {
+ copiedDiv.style.display = 'none';
+ }, 1500);
+ }
+ }).catch(err => {
+ console.error('Error copying text: ', err);
+ });
+ });
+ });
+ }
+ tooltip.style.opacity = '1';
+ const color = getTooltipColor(el);
+ el.style.boxShadow = `0 0 0 3px ${color}, 0 0 10px 3px ${color}`;
+ }
+
+ function hideTooltip() {
+ hideTimeout = setTimeout(() => {
+ if (tooltip) {
+ removeTooltipPosition(tooltip.dataset.tooltipId);
+ tooltip.remove();
+ tooltip = null;
+ }
+ el.style.boxShadow = '';
+ }, 500);
+ }
+
+ el.addEventListener('mouseenter', showTooltip);
+ el.addEventListener('mouseleave', hideTooltip);
+});
diff --git a/src/TwigHooks/assets/admin/styles/app.scss b/src/TwigHooks/assets/admin/styles/app.scss
new file mode 100644
index 00000000..516950a8
--- /dev/null
+++ b/src/TwigHooks/assets/admin/styles/app.scss
@@ -0,0 +1,49 @@
+[data-hook] {
+ position: relative;
+ transition: box-shadow 0.3s ease-in-out;
+}
+
+/* Styl tooltipa generowanego przez Popper.js */
+.custom-tooltip {
+ position: absolute;
+ width: fit-content;
+ height: fit-content;
+ z-index: 9999;
+ padding: 2px 2px;
+ border-radius: 6px;
+ white-space: pre-wrap;
+ font-size: 1em;
+ transition: opacity 0.3s ease-in-out;
+ opacity: 0;
+ user-select: text;
+
+ .tooltip-section {
+ display: flex;
+ flex-direction: column;
+ gap: 1px;
+ }
+
+ .tooltip-line {
+ line-height: 1;
+ }
+
+ .tooltip-line strong {
+ margin-right: 3px;
+ }
+
+ .tooltip-config {
+ margin: 0;
+ padding-left: 2px;
+ list-style-type: disc;
+ }
+
+ .tooltip-copied {
+ margin-top: 6px;
+ font-size: 0.85em;
+ color: #fff;
+ background: rgba(0, 0, 0, 0.6);
+ padding: 3px 6px;
+ border-radius: 4px;
+ text-align: center;
+ }
+}
diff --git a/src/TwigHooks/assets/shop/entrypoint.js b/src/TwigHooks/assets/shop/entrypoint.js
new file mode 100644
index 00000000..e69de29b
diff --git a/src/TwigHooks/assets/shop/styles/app.scss b/src/TwigHooks/assets/shop/styles/app.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/TwigHooks/src/Hook/Renderer/Debug/HookDebugCommentRenderer.php b/src/TwigHooks/src/Hook/Renderer/Debug/HookDebugCommentRenderer.php
index 8c2733b6..b8e3ecf6 100644
--- a/src/TwigHooks/src/Hook/Renderer/Debug/HookDebugCommentRenderer.php
+++ b/src/TwigHooks/src/Hook/Renderer/Debug/HookDebugCommentRenderer.php
@@ -24,9 +24,7 @@ public function __construct(private readonly HookRendererInterface $innerRendere
public function render(array $hookNames, array $hookContext = []): string
{
$renderedParts = [];
- $renderedParts[] = $this->getOpeningDebugComment($hookNames);
$renderedParts[] = trim($this->innerRenderer->render($hookNames, $hookContext));
- $renderedParts[] = $this->getClosingDebugComment($hookNames);
return implode(\PHP_EOL, $renderedParts);
}
diff --git a/src/TwigHooks/src/Hookable/Renderer/Debug/HookableDebugCommentRenderer.php b/src/TwigHooks/src/Hookable/Renderer/Debug/HookableDebugCommentRenderer.php
index 240a63cb..ec281762 100644
--- a/src/TwigHooks/src/Hookable/Renderer/Debug/HookableDebugCommentRenderer.php
+++ b/src/TwigHooks/src/Hookable/Renderer/Debug/HookableDebugCommentRenderer.php
@@ -27,10 +27,7 @@ public function __construct(private readonly HookableRendererInterface $innerRen
public function render(AbstractHookable $hookable, HookableMetadata $metadata): string
{
- $renderedParts = [];
- $renderedParts[] = $this->getOpeningDebugComment($hookable);
- $renderedParts[] = trim($this->innerRenderer->render($hookable, $metadata));
- $renderedParts[] = $this->getClosingDebugComment($hookable);
+ $renderedParts[] = $this->wrapWithDebugAttributes(trim($this->innerRenderer->render($hookable, $metadata)), $hookable);
return implode(\PHP_EOL, $renderedParts);
}
@@ -70,4 +67,15 @@ private function getClosingDebugComment(AbstractHookable $hookable): string
$hookable->priority(),
);
}
+
+ private function wrapWithDebugAttributes(string $content, AbstractHookable $hookable): string
+ {
+ $debugAttributes = sprintf(' data-hook="%s" data-hookable="%s" data-hookable-config="%s"',
+ htmlspecialchars($hookable->hookName, ENT_QUOTES),
+ htmlspecialchars($hookable->name, ENT_QUOTES),
+ $hookable->priority()
+ );
+
+ return preg_replace('/<(?!\/)(\w+)([^>]*)>/', '<$1$2' . $debugAttributes . '>', $content, 1);
+ }
}
diff --git a/src/TwigHooks/tests/Application/templates/admin/javascripts.html.twig b/src/TwigHooks/tests/Application/templates/admin/javascripts.html.twig
new file mode 100644
index 00000000..f6071973
--- /dev/null
+++ b/src/TwigHooks/tests/Application/templates/admin/javascripts.html.twig
@@ -0,0 +1 @@
+{{ encore_entry_script_tags('app-admin-entry', null, 'app.admin') }}
diff --git a/src/TwigHooks/tests/Application/templates/admin/stylesheets.html.twig b/src/TwigHooks/tests/Application/templates/admin/stylesheets.html.twig
new file mode 100644
index 00000000..d1a2b97c
--- /dev/null
+++ b/src/TwigHooks/tests/Application/templates/admin/stylesheets.html.twig
@@ -0,0 +1 @@
+{{ encore_entry_link_tags('app-admin-styles', null, 'app.admin') }}
diff --git a/src/TwigHooks/tests/Application/templates/shop/scripts.html.twig b/src/TwigHooks/tests/Application/templates/shop/scripts.html.twig
new file mode 100644
index 00000000..a659c2c1
--- /dev/null
+++ b/src/TwigHooks/tests/Application/templates/shop/scripts.html.twig
@@ -0,0 +1 @@
+{{ encore_entry_script_tags('app-shop-entry', null, 'app.shop') }}
diff --git a/src/TwigHooks/tests/Application/templates/shop/stylesheets.html.twig b/src/TwigHooks/tests/Application/templates/shop/stylesheets.html.twig
new file mode 100644
index 00000000..e67708b4
--- /dev/null
+++ b/src/TwigHooks/tests/Application/templates/shop/stylesheets.html.twig
@@ -0,0 +1 @@
+{{ encore_entry_link_tags('app-shop-styles', null, 'app.shop') }}