Compare commits

..

10 Commits

Author SHA1 Message Date
Xin
26569738f7 fix(image-zoom): refine gesture handling and zoom limits
- Introduced minimum and maximum scale limits for zoom functionality to prevent excessive scaling.
- Enhanced gesture state management for pinch and drag interactions, improving user experience during zoom.
- Updated event handling to ensure smoother transitions between single and multi-touch interactions, including better tap detection logic.
- Adjusted logic to maintain consistent pan behavior during zoom adjustments, ensuring a more intuitive interaction.
2025-10-05 13:27:31 +01:00
Xin
1c13e24535 Merge branch 'main' into image-zoom 2025-09-15 21:59:39 +01:00
Xin
6fc6391d06 fix(image-zoom): disable dragging during single touch interactions
- Updated logic to prevent dragging when a single touch is detected, improving tap detection accuracy.
- Adjusted event handling to ensure significant movement cancels tap only when necessary, enhancing user experience.
2025-09-11 23:30:38 +01:00
Xin
be49fe6f57 feat(image-zoom): improve zoom interactions and tap detection
- Added `will-change: transform` to CSS for better performance during zoom.
- Enhanced JavaScript to support tap detection for closing the zoom overlay with minimal movement.
- Updated zoom behavior to ensure scaling occurs from the center of the overlay.
- Refined event handling to prevent unintended interactions and improve user experience.
2025-09-11 23:20:06 +01:00
Xin
9e50415b94 feat(image-zoom): enhance zoom functionality with loading states and improved interactions
- Updated CSS to include loading indicators and refined transition effects for zoomed images.
- Enhanced JavaScript to manage image loading states, ensuring a smoother user experience during zoom interactions.
- Improved gesture handling for touch devices, including better management of pinch and drag events.
2025-09-11 23:00:03 +01:00
Xin
6e33f17cba feat(image-zoom): implement multi-touch pinch detection for zoom functionality
- Added support for pinch gestures to enhance the zoom experience on touch devices.
- Implemented event listeners for pointer events to manage pinch start and end.
- Updated closing behavior to account for active pinch gestures, improving user interaction.
2025-09-11 20:52:59 +01:00
Xin
ba0934b2e1 chore(image-zoom): enhance zoom functionality with improved transitions and closing behavior
- Updated CSS for smoother transitions and added closing effects for the zoom overlay.
- Enhanced JavaScript to support dedicated closing transitions and improved event handling for dismissing the overlay.
- Removed unnecessary scroll lock and overflow styles for better user experience.
2025-09-11 09:54:35 +01:00
Xin
a528d9adc0 chore(image-zoom): enhance mobile experience with scroll lock and touch actions 2025-09-11 08:54:52 +01:00
Xin
c2c4cafa13 Merge branch 'main' into image-zoom 2025-09-11 00:00:25 +01:00
Xin
09728a4aa9 feat(image-zoom): add minimal image zoom functionality
- Introduced CSS for image zoom overlay and image styling.
- Implemented JavaScript for handling image zoom interactions, including overlay creation and close functionality.
- Updated configuration to enable image zoom feature in site parameters.
- Added partial for including image zoom assets in the layout.
2025-09-10 23:59:51 +01:00
15 changed files with 660 additions and 112 deletions

View File

@@ -0,0 +1,84 @@
.hextra-zoom-image-overlay {
position: fixed;
inset: 0;
background: var(--hextra-image-zoom-backdrop, rgba(0, 0, 0, 0.9));
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
opacity: 0;
transition: opacity 260ms cubic-bezier(0.2, 0, 0, 1);
cursor: zoom-out;
overscroll-behavior: contain;
touch-action: none;
backdrop-filter: blur(var(--hextra-image-zoom-blur, 4px));
-webkit-backdrop-filter: blur(var(--hextra-image-zoom-blur, 4px));
/* Prevent iOS bounce */
position: fixed;
overflow: hidden;
will-change: transform;
}
.hextra-zoom-image-overlay.show {
opacity: 1;
}
.hextra-zoom-image-overlay.closing {
opacity: 0;
transition: opacity 360ms cubic-bezier(0.2, 0, 0, 1);
}
.hextra-zoom-image {
border-radius: 8px;
box-shadow: 0 18px 80px rgba(0, 0, 0, 0.5);
image-rendering: -webkit-optimize-contrast;
image-rendering: high-quality;
will-change: transform;
/* Prevent image selection on mobile */
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
/* Hardware acceleration */
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
.hextra-zoom-image-overlay.show .hextra-zoom-image {
transition:
transform 320ms cubic-bezier(0.2, 0.8, 0.2, 1),
left 320ms cubic-bezier(0.2, 0.8, 0.2, 1),
top 320ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.hextra-zoom-image-overlay.closing .hextra-zoom-image {
transition:
transform 340ms cubic-bezier(0.3, 0, 0.2, 1),
left 340ms cubic-bezier(0.3, 0, 0.2, 1),
top 340ms cubic-bezier(0.3, 0, 0.2, 1);
}
/* Disable transitions during interaction */
.hextra-zoom-image-overlay.interacting .hextra-zoom-image {
transition: none !important;
}
@media (prefers-reduced-motion: reduce) {
.hextra-zoom-image-overlay,
.hextra-zoom-image {
transition: none !important;
}
}
.content img:not([data-no-zoom]):not(.not-prose img) {
cursor: zoom-in;
}
/* Loading indicator */
.hextra-zoom-image.loading {
opacity: 0;
}
.hextra-zoom-image.loaded {
opacity: 1;
transition: opacity 200ms ease-in-out;
}

508
assets/js/image-zoom.js Normal file
View File

@@ -0,0 +1,508 @@
(function () {
'use strict';
function createOverlayFromTarget(targetImg) {
const src = targetImg.currentSrc || targetImg.src;
const alt = targetImg.alt || "";
if (!src) return;
const rect = targetImg.getBoundingClientRect();
const overlay = document.createElement("div");
overlay.className = "hextra-zoom-image-overlay";
overlay.setAttribute("role", "dialog");
overlay.setAttribute("aria-modal", "true");
overlay.setAttribute("aria-label", alt || "Zoomed image");
const img = document.createElement("img");
img.className = "hextra-zoom-image loading";
img.src = src;
if (alt) img.alt = alt;
// Center-origin positioning for cleaner transforms
const startCX = rect.left + rect.width / 2;
const startCY = rect.top + rect.height / 2;
img.style.position = "fixed";
img.style.left = startCX + "px";
img.style.top = startCY + "px";
img.style.width = rect.width + "px";
img.style.height = rect.height + "px";
img.style.transformOrigin = "center center";
img.style.transform = "translate3d(-50%, -50%, 0) scale(1)";
// Ensure overlay scales from center when zooming the whole overlay
overlay.style.transformOrigin = "center center";
overlay.appendChild(img);
// Image loaded handler
img.addEventListener('load', function () {
img.classList.remove('loading');
img.classList.add('loaded');
});
// Gesture state management
let isPinching = false;
let isDragging = false;
let isInteracting = false;
let pinchEndTimer = null;
const pointers = new Map(); // pointerId -> {x, y, startX, startY}
const SCALE_MIN = 1;
const SCALE_MAX = 5;
let gestureState = {
scale: 1,
panX: 0,
panY: 0,
startScale: 1,
startPanX: 0,
startPanY: 0,
initialDistance: 0,
initialMidpointX: 0,
initialMidpointY: 0,
dragStartX: 0,
dragStartY: 0,
dragPanX: 0,
dragPanY: 0
};
// Utility functions
function getDistance(p1, p2) {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
return Math.sqrt(dx * dx + dy * dy);
}
function getMidpoint(p1, p2) {
return {
x: (p1.x + p2.x) / 2,
y: (p1.y + p2.y) / 2
};
}
function setInteracting(value) {
isInteracting = value;
if (value) {
overlay.classList.add('interacting');
} else {
overlay.classList.remove('interacting');
}
}
// Pointer event handlers
let tapCandidate = false;
let tapStartX = 0;
let tapStartY = 0;
let tapStartTime = 0;
function onPointerDown(e) {
e.preventDefault();
if (typeof overlay.setPointerCapture === 'function') {
try {
overlay.setPointerCapture(e.pointerId);
} catch (err) {
// ignore pointer capture failures (e.g. Safari)
}
}
const pointerData = {
x: e.clientX,
y: e.clientY,
startX: e.clientX,
startY: e.clientY
};
pointers.set(e.pointerId, pointerData);
if (pointers.size === 1) {
isDragging = false;
if (gestureState.scale > SCALE_MIN) {
setInteracting(true);
}
// Set drag baseline so a single finger can pan when zoomed
gestureState.dragStartX = e.clientX;
gestureState.dragStartY = e.clientY;
gestureState.dragPanX = gestureState.panX;
gestureState.dragPanY = gestureState.panY;
tapCandidate = true;
tapStartX = e.clientX;
tapStartY = e.clientY;
tapStartTime = (typeof performance !== 'undefined' ? performance.now() : Date.now());
} else if (pointers.size === 2) {
isPinching = true;
isDragging = false;
setInteracting(true);
tapCandidate = false;
const pts = Array.from(pointers.values());
gestureState.initialDistance = getDistance(pts[0], pts[1]) || 1;
gestureState.startScale = gestureState.scale;
gestureState.startPanX = gestureState.panX;
gestureState.startPanY = gestureState.panY;
const midpoint = getMidpoint(pts[0], pts[1]);
gestureState.initialMidpointX = midpoint.x;
gestureState.initialMidpointY = midpoint.y;
if (pinchEndTimer) {
clearTimeout(pinchEndTimer);
pinchEndTimer = null;
}
}
}
function onPointerMove(e) {
const pointer = pointers.get(e.pointerId);
if (!pointer) return;
e.preventDefault();
pointer.x = e.clientX;
pointer.y = e.clientY;
if (pointers.size === 2) {
const pts = Array.from(pointers.values());
const currentDistance = getDistance(pts[0], pts[1]);
if (!currentDistance) return;
const currentMidpoint = getMidpoint(pts[0], pts[1]);
const distanceRatio = currentDistance / (gestureState.initialDistance || currentDistance);
let nextScale = gestureState.startScale * distanceRatio;
nextScale = Math.max(SCALE_MIN, Math.min(SCALE_MAX, nextScale));
const totalStart = baseScale * gestureState.startScale;
const totalNext = baseScale * nextScale;
let nextPanX = gestureState.panX;
let nextPanY = gestureState.panY;
if (totalStart > 0) {
const startOffsetX = gestureState.initialMidpointX - final.cx - gestureState.startPanX;
const startOffsetY = gestureState.initialMidpointY - final.cy - gestureState.startPanY;
const currentOffsetX = currentMidpoint.x - final.cx;
const currentOffsetY = currentMidpoint.y - final.cy;
const ratio = totalNext / totalStart;
nextPanX = currentOffsetX - ratio * startOffsetX;
nextPanY = currentOffsetY - ratio * startOffsetY;
}
gestureState.scale = nextScale;
gestureState.panX = nextPanX;
gestureState.panY = nextPanY;
tapCandidate = false;
applyTransform();
} else if (pointers.size === 1) {
const moveX = pointer.x - gestureState.dragStartX;
const moveY = pointer.y - gestureState.dragStartY;
const dragThreshold = 6;
if (!isDragging) {
const distanceSq = moveX * moveX + moveY * moveY;
if (gestureState.scale > SCALE_MIN && distanceSq > dragThreshold * dragThreshold) {
isDragging = true;
tapCandidate = false;
setInteracting(true);
gestureState.dragPanX = gestureState.panX;
gestureState.dragPanY = gestureState.panY;
}
}
if (isDragging) {
gestureState.panX = gestureState.dragPanX + moveX;
gestureState.panY = gestureState.dragPanY + moveY;
applyTransform();
} else {
const cancelTapThreshold = 10;
if (
Math.abs(pointer.x - tapStartX) > cancelTapThreshold ||
Math.abs(pointer.y - tapStartY) > cancelTapThreshold
) {
tapCandidate = false;
}
}
}
}
function onPointerUp(e) {
if (typeof overlay.releasePointerCapture === 'function') {
try {
overlay.releasePointerCapture(e.pointerId);
} catch (err) {
// ignore release failures
}
}
pointers.delete(e.pointerId);
if (pointers.size === 0) {
const now = (typeof performance !== 'undefined' ? performance.now() : Date.now());
const duration = now - tapStartTime;
const shouldClose = tapCandidate && !isPinching && !isDragging && duration < 300;
if (shouldClose) {
close();
}
tapCandidate = false;
isDragging = false;
if (isPinching) {
pinchEndTimer = setTimeout(() => {
isPinching = false;
}, 180);
} else {
isPinching = false;
}
gestureState.startScale = gestureState.scale;
gestureState.startPanX = gestureState.panX;
gestureState.startPanY = gestureState.panY;
setTimeout(() => setInteracting(false), 120);
} else if (pointers.size === 1) {
isPinching = false;
isDragging = false;
const remaining = Array.from(pointers.values())[0];
remaining.startX = remaining.x;
remaining.startY = remaining.y;
gestureState.dragStartX = remaining.x;
gestureState.dragStartY = remaining.y;
gestureState.dragPanX = gestureState.panX;
gestureState.dragPanY = gestureState.panY;
if (gestureState.scale <= SCALE_MIN) {
setTimeout(() => setInteracting(false), 120);
}
}
}
function onPointerCancel(e) {
onPointerUp(e);
}
// Mouse wheel zoom and scroll handling
function onWheel(e) {
e.preventDefault();
// If it's a regular scroll (not pinch), dismiss the overlay
if (!e.ctrlKey && !e.metaKey) {
// Regular scroll - dismiss overlay gracefully
close();
return;
}
// Handle trackpad pinch (ctrl+wheel or cmd+wheel on Mac)
const delta = e.deltaY;
const scaleFactor = 0.01;
const zoomSpeed = Math.exp(-delta * scaleFactor);
const prevScale = gestureState.scale;
const unclamped = prevScale * zoomSpeed;
const nextScale = Math.max(SCALE_MIN, Math.min(SCALE_MAX, unclamped));
if (Math.abs(nextScale - prevScale) > 0.0001) {
const totalPrev = baseScale * prevScale;
const totalNext = baseScale * nextScale;
if (totalPrev > 0) {
const anchorOffsetX = e.clientX - final.cx;
const anchorOffsetY = e.clientY - final.cy;
const ratio = totalNext / totalPrev;
gestureState.panX = anchorOffsetX + (gestureState.panX - anchorOffsetX) * ratio;
gestureState.panY = anchorOffsetY + (gestureState.panY - anchorOffsetY) * ratio;
}
}
gestureState.scale = nextScale;
gestureState.startScale = nextScale;
gestureState.startPanX = gestureState.panX;
gestureState.startPanY = gestureState.panY;
setInteracting(true);
applyTransform();
setTimeout(() => setInteracting(false), 150);
}
// Keyboard navigation
function onKeyDown(e) {
if (e.key === "Escape") {
close();
} else if (e.key === "+" || e.key === "=") {
gestureState.scale = Math.min(SCALE_MAX, gestureState.scale * 1.2);
gestureState.startScale = gestureState.scale;
gestureState.startPanX = gestureState.panX;
gestureState.startPanY = gestureState.panY;
applyTransform();
} else if (e.key === "-") {
gestureState.scale = Math.max(SCALE_MIN, gestureState.scale / 1.2);
gestureState.startScale = gestureState.scale;
gestureState.startPanX = gestureState.panX;
gestureState.startPanY = gestureState.panY;
applyTransform();
} else if (e.key === "0") {
gestureState.scale = 1;
gestureState.panX = 0;
gestureState.panY = 0;
gestureState.startScale = gestureState.scale;
gestureState.startPanX = 0;
gestureState.startPanY = 0;
applyTransform();
}
}
// Apply transforms
let final = computeFinal();
let baseScale = final.scale;
function computeFinal() {
const vv = window.visualViewport;
const vw = vv ? vv.width : window.innerWidth;
const vh = vv ? vv.height : window.innerHeight;
const vx = vv ? vv.offsetLeft : 0;
const vy = vv ? vv.offsetTop : 0;
const margin = 20;
const maxW = Math.min(vw - margin * 2, 1200);
const maxH = vh - margin * 2;
const scale = Math.min(maxW / rect.width, maxH / rect.height, 2);
const centerX = vx + vw / 2;
const centerY = vy + vh / 2;
return { cx: centerX, cy: centerY, scale };
}
function applyTransform() {
img.style.left = final.cx + "px";
img.style.top = final.cy + "px";
overlay.style.transform = "none";
const totalScale = baseScale * gestureState.scale;
const transform = `translate3d(-50%, -50%, 0) translate3d(${gestureState.panX}px, ${gestureState.panY}px, 0) scale(${totalScale})`;
img.style.transform = transform;
}
// Close function
function close(immediate = false) {
overlay.classList.add("closing");
// Animate back to original position
img.style.left = startCX + "px";
img.style.top = startCY + "px";
img.style.transform = `translate3d(-50%, -50%, 0) scale(1)`;
// Cleanup event listeners
cleanup();
if (immediate) {
overlay.remove();
return;
}
// Remove after animation
const done = () => overlay.remove();
overlay.addEventListener("transitionend", done, { once: true });
img.addEventListener("transitionend", done, { once: true });
// Fallback removal
setTimeout(() => {
if (overlay.parentNode) {
overlay.remove();
}
}, 400);
}
function cleanup() {
window.removeEventListener("keydown", onKeyDown, true);
overlay.removeEventListener("wheel", onWheel);
overlay.removeEventListener("pointermove", onPointerMove);
overlay.removeEventListener("pointerdown", onPointerDown);
overlay.removeEventListener("pointerup", onPointerUp);
overlay.removeEventListener("pointercancel", onPointerCancel);
window.removeEventListener("resize", onResize);
if (window.visualViewport) {
window.visualViewport.removeEventListener("resize", onResize);
window.visualViewport.removeEventListener("scroll", onResize);
}
if (pinchEndTimer) {
clearTimeout(pinchEndTimer);
}
}
// Handle viewport changes
function onResize() {
final = computeFinal();
baseScale = final.scale;
gestureState.startScale = gestureState.scale;
gestureState.startPanX = gestureState.panX;
gestureState.startPanY = gestureState.panY;
applyTransform();
}
// Setup event listeners
overlay.addEventListener("wheel", onWheel, { passive: false });
overlay.addEventListener("pointermove", onPointerMove);
overlay.addEventListener("pointerdown", onPointerDown);
overlay.addEventListener("pointerup", onPointerUp);
overlay.addEventListener("pointercancel", onPointerCancel);
window.addEventListener("keydown", onKeyDown, true);
window.addEventListener("resize", onResize, { passive: true });
if (window.visualViewport) {
window.visualViewport.addEventListener("resize", onResize, { passive: true });
window.visualViewport.addEventListener("scroll", onResize, { passive: true });
}
// Add to DOM
document.body.appendChild(overlay);
// Trigger opening animation
requestAnimationFrame(() => {
overlay.classList.add("show");
applyTransform();
});
}
// Initialize after DOM ready
function init() {
const container = document.querySelector(".content");
if (!container) return;
container.addEventListener("click", function (e) {
const target = e.target;
if (!(target instanceof HTMLImageElement)) return;
// Only allow images inside `.content` that are NOT within a `.not-prose` block
if (target.closest('.not-prose')) return;
if (target.hasAttribute('data-no-zoom')) return;
if (e.defaultPrevented) return;
if (typeof e.button === 'number' && e.button !== 0) return;
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
const interactiveParent = target.closest('a[href], button, [role="button"], summary, label');
if (interactiveParent && interactiveParent !== target) {
return;
}
e.preventDefault();
createOverlayFromTarget(target);
}, true);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();

View File

@@ -383,6 +383,16 @@ excludeSearch: true
---
```
### Google Analytics
To enable [Google Analytics](https://marketingplatform.google.com/about/analytics/), set `services.googleAnalytics.ID` flag in `hugo.yaml`:
```yaml {filename="hugo.yaml"}
services:
googleAnalytics:
ID: G-MEASUREMENT_ID
```
### Google Search Index
To [block Google Search](https://developers.google.com/search/docs/crawling-indexing/block-indexing) from indexing a page, set `noindex` to true in your page frontmatter:
@@ -399,25 +409,7 @@ To exclude an entire directory, use the [`cascade`](https://gohugo.io/configurat
> To block search crawlers, you can make a [`robots.txt` template](https://gohugo.io/templates/robots/).
> However, `robots.txt` instructions do not necessarily keep a page out of Google search results.
### Analytics
Hextra has support for several different analytics solutions. Hextra only supports analytics in production environments. This is to ensure that you do not accidentally send analytic events when working locally. If, however, you do want to test analytics locally, you can run a production server using:
```
hugo server --environment production
```
#### Google Analytics
To enable [Google Analytics](https://marketingplatform.google.com/about/analytics/), set `services.googleAnalytics.ID` flag in `hugo.yaml`:
```yaml {filename="hugo.yaml"}
services:
googleAnalytics:
ID: G-MEASUREMENT_ID
```
#### Umami Analytics
### Umami Analytics
To enable [Umami](https://umami.is/docs/), set `params.analytics.umami.serverURL` and `params.analytics.umami.websiteID` flag in `hugo.yaml`:
@@ -444,7 +436,7 @@ params:
# doNotTrack: "true" # optional
```
#### Matomo Analytics
### Matomo Analytics
To enable [Matomo](https://matomo.org/), set `params.analytics.matomo.URL` and `params.analytics.matomo.ID` flag in `hugo.yaml`:
@@ -456,32 +448,6 @@ params:
websiteID: "94db1cb1-74f4-4a40-ad6c-962362670409"
```
#### GoatCounter Analytics
To enable [GoatCounter](https://www.goatcounter.com/), set `params.analytics.goatCounter.code` in `hugo.yaml`
All settings available here are mirrors of the settings described in GoatCounter [settings](https://www.goatcounter.com/help/js#settings-44186)
```yaml {filename="hugo.yaml"}
params:
analytics:
goatCounter:
code: "ABCDE"
# Optional Settings
#------------------
# disables automatic collection of data
# noOnload: true
# disables event binding. See more here https://www.goatcounter.com/help/events
# noEvents: true
# allows data collection from local addresses. Use this with a production environment to test locally
# allowLocal: true
# Allow data collection when a page is loaded in a frame or iframe
# allowFrame: true
```
### LLMS.txt Support
To enable [llms.txt](https://llmstxt.org/) output format for your site, which provides a structured text outline for [large language models](https://en.wikipedia.org/wiki/Large_language_model) and AI agents, add the `llms` output format to your site's `hugo.yaml`:

View File

@@ -8,36 +8,36 @@ title: جزئیات
## مثال
{{< details title="جزئیات" >}}
{{% details title="جزئیات" %}}
این محتوای جزئیات است.
مارک‌داون **پشتیبانی می‌شود**.
{{< /details >}}
{{% /details %}}
{{< details title="برای نمایش کلیک کنید" closed="true" >}}
{{% details title="برای نمایش کلیک کنید" closed="true" %}}
این به‌صورت پیش‌فرض مخفی خواهد بود.
{{< /details >}}
{{% /details %}}
## نحوه استفاده
````markdown
{{</* details title="جزئیات" */>}}
{{%/* details title="جزئیات" */%}}
این محتوای جزئیات است.
مارک‌داون **پشتیبانی می‌شود**.
{{</* /details */>}}
{{%/* /details */%}}
````
````markdown
{{</* details title="برای نمایش کلیک کنید" closed="true" */>}}
{{%/* details title="برای نمایش کلیک کنید" closed="true" */%}}
این به‌صورت پیش‌فرض مخفی خواهد بود.
{{</* /details */>}}
{{%/* /details */%}}
````

View File

@@ -8,36 +8,36 @@ title: 詳細
## 例
{{< details title="詳細" >}}
{{% details title="詳細" %}}
これは詳細のコンテンツです。
Markdown は **サポートされています**
{{< /details >}}
{{% /details %}}
{{< details title="クリックして表示" closed="true" >}}
{{% details title="クリックして表示" closed="true" %}}
これはデフォルトで非表示になります。
{{< /details >}}
{{% /details %}}
## 使用方法
````markdown
{{</* details title="詳細" */>}}
{{%/* details title="詳細" */%}}
これは詳細のコンテンツです。
Markdown は **サポートされています**。
{{</* /details */>}}
{{%/* /details */%}}
````
````markdown
{{</* details title="クリックして表示" closed="true" */>}}
{{%/* details title="クリックして表示" closed="true" */%}}
これはデフォルトで非表示になります。
{{</* /details */>}}
{{%/* /details */%}}
````

View File

@@ -8,36 +8,36 @@ A built-in component to display a collapsible content.
## Example
{{< details title="Details" >}}
{{% details title="Details" %}}
This is the content of the details.
Markdown is **supported**.
{{< /details >}}
{{% /details %}}
{{< details title="Click me to reveal" closed="true" >}}
{{% details title="Click me to reveal" closed="true" %}}
This will be hidden by default.
{{< /details >}}
{{% /details %}}
## Usage
````markdown
{{</* details title="Details" */>}}
{{%/* details title="Details" */%}}
This is the content of the details.
Markdown is **supported**.
{{</* /details */>}}
{{%/* /details */%}}
````
````markdown
{{</* details title="Click me to reveal" closed="true" */>}}
{{%/* details title="Click me to reveal" closed="true" */%}}
This will be hidden by default.
{{</* /details */>}}
{{%/* /details */%}}
````

View File

@@ -8,36 +8,36 @@ title: 详情
## 示例
{{< details title="详情" >}}
{{% details title="详情" %}}
这是详情的内容。
支持 **Markdown** 格式。
{{< /details >}}
{{% /details %}}
{{< details title="点击我展开" closed="true" >}}
{{% details title="点击我展开" closed="true" %}}
默认情况下,这部分内容会被隐藏。
{{< /details >}}
{{% /details %}}
## 使用方法
````markdown
{{</* details title="详情" */>}}
{{%/* details title="详情" */%}}
这是详情的内容。
支持 **Markdown** 格式。
{{</* /details */>}}
{{%/* /details */%}}
````
````markdown
{{</* details title="点击我展开" closed="true" */>}}
{{%/* details title="点击我展开" closed="true" */%}}
默认情况下,这部分内容会被隐藏。
{{</* /details */>}}
{{%/* /details */%}}
````

View File

@@ -4,8 +4,6 @@ title: Steps
A built-in component to display a series of steps.
You can use the Markdown attribute `{class="no-step-marker"}` to prevent a heading from being counted as a step.
## Example
{{% steps %}}
@@ -18,10 +16,6 @@ This is the first step.
This is the second step.
#### Step subheading {class="no-step-marker"}
This will not be counted as a step.
### Step 3
This is the third step.
@@ -49,9 +43,5 @@ This is the first step.
This is the second step.
#### Step subheading {class="no-step-marker"}
This will not be counted as a step.
{{%/* /steps */%}}
```

View File

@@ -237,3 +237,6 @@ params:
# inputPosition: top
# lang: en
# theme: noborder_dark
imageZoom:
enable: true

View File

@@ -715,7 +715,6 @@
"msupsub",
"mtable",
"mtight",
"no-step-marker",
"not-prose",
"nulldelimiter",
"op-symbol",

View File

@@ -1,4 +1,4 @@
<h{{ .Level }} {{- with .Attributes.class }} class="{{ . }}" {{- end }}>
<h{{ .Level }}>
{{- .Text | safeHTML -}}
{{- if gt .Level 1 -}}
<span class="hx:absolute hx:-mt-20" id="{{ .Anchor | safeURL }}"></span>

View File

@@ -16,9 +16,4 @@
{{ partial "components/analytics/matomo.html" . }}
{{- end }}
<!-- GoatCounter -->
{{- if .Site.Params.analytics.goatCounter -}}
{{ partial "components/analytics/goat-counter.html" . }}
{{- end -}}
{{- end }}

View File

@@ -1,17 +0,0 @@
{{- with .Site.Params.analytics.goatCounter -}}
{{- if not .code -}}
{{- errorf "Missing GoatCounter 'code' configuration. See https://imfing.github.io/hextra/versions/latest/docs/guide/configuration/#goatcounter-analytics" -}}
{{- end -}}
<script
data-goatcounter="https://{{ .code }}.goatcounter.com/count"
data-goatcounter-settings='
{
"no_onload":{{ .noOnload | default false }},
"no_events":{{ .noEvents | default false }},
"allow_local":{{ .allowLocal | default false }},
"allow_frame":{{ .allowFrame | default false }}
}
'
async src="//gc.zgo.at/count.js"></script>
{{- end -}}

View File

@@ -13,3 +13,10 @@
{{- if (.Store.Get "hasAsciinema") -}}
{{- partial "scripts/asciinema.html" . -}}
{{- end -}}
{{/* Image zoom */}}
{{- with site.Params.imageZoom }}
{{- if .enable }}
{{- partial "scripts/image-zoom.html" $ -}}
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,13 @@
{{/* Optional minimal image zoom assets */}}
{{- $js := resources.Get "js/image-zoom.js" -}}
{{- $css := resources.Get "css/components/image-zoom.css" -}}
{{- if hugo.IsProduction -}}
{{- $js = $js | minify | fingerprint -}}
{{- $css = $css | minify | fingerprint -}}
{{- end -}}
<link rel="preload" href="{{ $css.RelPermalink }}" as="style" integrity="{{ $css.Data.Integrity }}" />
<link href="{{ $css.RelPermalink }}" rel="stylesheet" integrity="{{ $css.Data.Integrity }}" />
<script defer src="{{ $js.RelPermalink }}" integrity="{{ $js.Data.Integrity }}"></script>