mirror of
				https://github.com/imfing/hextra.git
				synced 2025-10-31 03:44:52 -04:00 
			
		
		
		
	Compare commits
	
		
			27 Commits
		
	
	
		
			v0.11.0
			...
			image-zoom
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 26569738f7 | ||
|   | 1c13e24535 | ||
|   | 708358de80 | ||
|   | 8699deb1dd | ||
|   | a03dbf463f | ||
|   | 6fc6391d06 | ||
|   | be49fe6f57 | ||
|   | 9e50415b94 | ||
|   | 6e33f17cba | ||
|   | 1c06ae5580 | ||
|   | ba0934b2e1 | ||
|   | a528d9adc0 | ||
|   | c2c4cafa13 | ||
|   | 09728a4aa9 | ||
|   | ccb63d60f1 | ||
|   | 3bc454bbf6 | ||
|   | 1b536e27a5 | ||
|   | 0e919e77f8 | ||
|   | 83f3b5052e | ||
|   | f8eae96c11 | ||
|   | ec97808b69 | ||
|   | 334158af7a | ||
|   | 184ee25011 | ||
|   | cc5884dd2a | ||
|   | 493cfba523 | ||
|   | 5846274db7 | ||
|   | 4635bdc846 | 
							
								
								
									
										6
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							| @@ -31,7 +31,7 @@ Use [Conventional Commits][conventional commits] message to make it easier to un | ||||
|  | ||||
| Similar to contributing code, you can also contribute to the documentation by submitting a pull request. | ||||
|  | ||||
| The documentation site is located in the [`exampleSite`](../exampleSite/) folder. | ||||
| The documentation site is located in the [`docs`](../docs/) folder. | ||||
| You can make changes to the documentation and create a pull request. A preview of the new documentation will be automatically generated and displayed in the pull request comment via [Netlify][netlify deploy preview]. | ||||
|  | ||||
| ### 💬 GitHub Discussions | ||||
| @@ -71,7 +71,7 @@ npm i | ||||
|  | ||||
| - [`assets`](../assets/): CSS styles and JavaScript files. | ||||
| - [`data`](../data/): The theme data files. Now only contains the `icons.yaml` file. | ||||
| - [`exampleSite`](../exampleSite/): The documentation site for the theme. | ||||
| - [`docs`](../docs/): The documentation site for the theme. | ||||
| - [`i18n`](../i18n/): The theme translation files. | ||||
| - [`layouts`](../layouts/): The theme layouts. | ||||
| - [`static`](../static/): The static files for the theme. For example, the favicon and the site logo. | ||||
| @@ -84,7 +84,7 @@ Please refer to the [Hugo documentation][hugo] for more information. | ||||
| npm run dev:theme | ||||
| ``` | ||||
|  | ||||
| It will start the Hugo server on `http://localhost:1313/` for the `exampleSite` content. | ||||
| It starts the Hugo server on `http://localhost:1313/` for the `docs` content. | ||||
|  | ||||
| ### Compile the styles | ||||
|  | ||||
|   | ||||
							
								
								
									
										10
									
								
								CLAUDE.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								CLAUDE.md
									
									
									
									
									
								
							| @@ -84,12 +84,12 @@ assets/ | ||||
|  | ||||
| ### Example Site Development | ||||
|  | ||||
| The `exampleSite/` directory serves as both documentation and testing ground: | ||||
| The `docs/` directory serves as both documentation and testing ground: | ||||
|  | ||||
| - Test new features here before releasing | ||||
| - Configuration examples in `exampleSite/hugo.yaml` showing multi-language setup | ||||
| - Configuration examples in `docs/hugo.yaml` showing multi-language setup | ||||
| - Content examples demonstrate all theme capabilities | ||||
| - Run from exampleSite with: `hugo server --themesDir=../..` | ||||
| - Run from docs with: `hugo server --themesDir=../..` | ||||
|  | ||||
| ### CSS Development Workflow | ||||
|  | ||||
| @@ -115,7 +115,7 @@ The `exampleSite/` directory serves as both documentation and testing ground: | ||||
|  | ||||
| ### Key Configuration Files | ||||
|  | ||||
| - `exampleSite/hugo.yaml` - Example Hugo configuration with multi-language setup | ||||
| - `docs/hugo.yaml` - Example Hugo configuration with multi-language setup | ||||
| - `postcss.config.mjs` - PostCSS configuration for CSS processing | ||||
| - `package.json` - Node.js dependencies and build scripts | ||||
| - `taskfile.yaml` - Task runner configuration | ||||
| @@ -155,7 +155,7 @@ The `exampleSite/` directory serves as both documentation and testing ground: | ||||
|  | ||||
| ### Testing & Quality Assurance | ||||
|  | ||||
| - Test all changes in `exampleSite/` before releasing | ||||
| - Test all changes in `docs/` before releasing | ||||
| - Use `npm run dev:theme` for theme development with hot reloading | ||||
| - Format code with `npx prettier --write .` before committing | ||||
| - Verify multi-language functionality across supported languages | ||||
|   | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										84
									
								
								assets/css/components/image-zoom.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								assets/css/components/image-zoom.css
									
									
									
									
									
										Normal 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; | ||||
| } | ||||
| @@ -33,19 +33,19 @@ | ||||
|     @apply hx:border-black/4 hx:bg-black/3 hx:break-words hx:rounded-md hx:border hx:py-0.5 hx:px-[.25em] hx:text-[.9em] hx:dark:border-white/10 hx:dark:bg-white/10; | ||||
|   } | ||||
|   :where(table):not(:where(.hextra-code-block table, [class~=not-prose],[class~=not-prose] *)) { | ||||
|     @apply hx:block hx:overflow-x-auto hx:my-6 hx:p-0 hx:first:mt-0 hx:w-full hx:text-sm hx:leading-5; | ||||
|     @apply hx:block hx:overflow-x-auto hx:my-6 hx:p-0 hx:first:mt-0 hx:w-full hx:text-sm hx:leading-5 hx:border-collapse; | ||||
|  | ||||
|     thead { | ||||
|       @apply hx:border-b hx:border-gray-200 hx:dark:border-neutral-800; | ||||
|       @apply hx:bg-gray-50 hx:dark:bg-gray-600/20; | ||||
|     } | ||||
|     tbody tr { | ||||
|       @apply hx:m-0 hx:border-b hx:border-gray-100 hx:dark:border-neutral-800/50; | ||||
|     tr { | ||||
|       @apply hx:m-0 hx:border-t hx:border-gray-300 hx:p-0 hx:dark:border-gray-600; | ||||
|     } | ||||
|     th { | ||||
|       @apply hx:m-0 hx:p-2 hx:font-semibold hx:first:pl-0 hx:last:pr-0; | ||||
|       @apply hx:m-0 hx:border hx:border-gray-300 hx:p-2 hx:font-semibold hx:dark:border-gray-600; | ||||
|     } | ||||
|     td { | ||||
|       @apply hx:m-0 hx:p-2 hx:first:pl-0 hx:last:pr-0; | ||||
|       @apply hx:m-0 hx:border hx:border-gray-300 hx:p-2 hx:dark:border-gray-600; | ||||
|     } | ||||
|   } | ||||
|   :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)) { | ||||
|   | ||||
							
								
								
									
										6
									
								
								assets/js/head/banner.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								assets/js/head/banner.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| // The section must not be in the banner.js (body) file because it can create a quick flash. | ||||
|  | ||||
| if (localStorage.getItem('{{ site.Params.banner.key | default `banner-closed` }}')) { | ||||
|   document.documentElement.style.setProperty("--hextra-banner-height", "0px"); | ||||
|   document.documentElement.classList.add("hextra-banner-hidden"); | ||||
| } | ||||
							
								
								
									
										14
									
								
								assets/js/head/theme.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								assets/js/head/theme.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| // The section must not be in the theme.js (body) file because it can create a quick flash (switch between light and dark). | ||||
|  | ||||
| function setTheme(theme) { | ||||
|   document.documentElement.classList.remove("light", "dark"); | ||||
|  | ||||
|   if (theme !== "light" && theme !== "dark") { | ||||
|     theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; | ||||
|   } | ||||
|  | ||||
|   document.documentElement.classList.add(theme); | ||||
|   document.documentElement.style.colorScheme = theme; | ||||
| } | ||||
|  | ||||
| setTheme("color-theme" in localStorage ? localStorage.getItem("color-theme") : '{{ site.Params.theme.default | default `system`}}') | ||||
							
								
								
									
										508
									
								
								assets/js/image-zoom.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										508
									
								
								assets/js/image-zoom.js
									
									
									
									
									
										Normal 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(); | ||||
|   } | ||||
| })(); | ||||
							
								
								
									
										19
									
								
								build.sh
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								build.sh
									
									
									
									
									
								
							| @@ -7,16 +7,17 @@ BASE_URL=${1:-"http://localhost:1313"} | ||||
| echo "Using base URL: $BASE_URL" | ||||
|  | ||||
| # Version configuration - modify these arrays to specify versions to build | ||||
| # Format: "ref:display_name" (ref can be tag, branch, or commit hash, display name is what will appear in URL) | ||||
| MAIN_VERSION="v0.10.1:latest" | ||||
| # MAIN_VERSION format: "ref:display_name:source_dir" | ||||
| # VERSIONS format: "ref:display_name:source_dir" where source_dir is either "docs" or "exampleSite" | ||||
| MAIN_VERSION="v0.11.1:latest:docs" | ||||
| VERSIONS=( | ||||
|   "main:latest" # latest version always builds from main | ||||
|   "v0.9.6:v0.9" | ||||
|   "v0.8.6:v0.8" | ||||
|   "main:latest:docs" # latest version always builds from main | ||||
|   "v0.10.2:v0.10:exampleSite" | ||||
|   "v0.9.6:v0.9:exampleSite" | ||||
| ) | ||||
|  | ||||
| # Parse main version | ||||
| IFS=':' read -r MAIN_REF MAIN_NAME <<< "$MAIN_VERSION" | ||||
| IFS=':' read -r MAIN_REF MAIN_NAME MAIN_DIR <<< "$MAIN_VERSION" | ||||
|  | ||||
| # Ensure clean public directory | ||||
| rm -rf public | ||||
| @@ -29,13 +30,13 @@ GIT_HASH=$(git rev-parse --short HEAD) | ||||
| echo "Building main site from $MAIN_REF (commit: $GIT_HASH)" | ||||
| hugo \ | ||||
|   --minify \ | ||||
|   --themesDir=../.. --source=exampleSite \ | ||||
|   --themesDir=../.. --source=$MAIN_DIR \ | ||||
|   --baseURL "$BASE_URL/" \ | ||||
|   --destination=../public | ||||
|  | ||||
| # Build all versions | ||||
| for VERSION in "${VERSIONS[@]}"; do | ||||
|   IFS=':' read -r REF NAME <<< "$VERSION" | ||||
|   IFS=':' read -r REF NAME DIR <<< "$VERSION" | ||||
|  | ||||
|   git checkout $REF | ||||
|   GIT_HASH=$(git rev-parse --short HEAD) | ||||
| @@ -44,7 +45,7 @@ for VERSION in "${VERSIONS[@]}"; do | ||||
|   mkdir -p "public/versions/$NAME" | ||||
|   hugo \ | ||||
|     --minify \ | ||||
|     --themesDir=../.. --source=exampleSite \ | ||||
|     --themesDir=../.. --source=$DIR \ | ||||
|     --baseURL "$BASE_URL/versions/$NAME/" \ | ||||
|     --destination="../public/versions/$NAME" | ||||
| done | ||||
|   | ||||
							
								
								
									
										2
									
								
								dev.toml
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								dev.toml
									
									
									
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| # Theme development config for exampleSite | ||||
| # Theme development config for documentation site | ||||
| # https://gohugo.io/configuration/build/#cache-busters | ||||
| [build] | ||||
|   [build.buildStats] | ||||
|   | ||||
| Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 168 KiB | 
| @@ -221,14 +221,14 @@ hugo gen chromastyles --style=github | ||||
| میتوانید اسکریپتهای سفارشی را به انتهای head برای هر صفحه با افزودن فایل زیر اضافه کنید: | ||||
| 
 | ||||
| ``` | ||||
| layouts/partials/custom/head-end.html | ||||
| layouts/_partials/custom/head-end.html | ||||
| ``` | ||||
| 
 | ||||
| ## بخش اضافی سفارشی در پاورقی | ||||
| 
 | ||||
| میتوانید بخش اضافی در پاورقی با ایجاد یک فایل `layouts/partials/custom/footer.html` در سایت خود اضافه کنید. | ||||
| میتوانید بخش اضافی در پاورقی با ایجاد یک فایل `layouts/_partials/custom/footer.html` در سایت خود اضافه کنید. | ||||
| 
 | ||||
| ```html {filename="layouts/partials/custom/footer.html"} | ||||
| ```html {filename="layouts/_partials/custom/footer.html"} | ||||
| <!-- عنصر پاورقی شما اینجا --> | ||||
| ``` | ||||
| 
 | ||||
| @@ -221,14 +221,14 @@ hugo gen chromastyles --style=github | ||||
| すべてのページのheadの終わりにカスタムスクリプトを追加するには、以下のファイルを作成します: | ||||
| 
 | ||||
| ``` | ||||
| layouts/partials/custom/head-end.html | ||||
| layouts/_partials/custom/head-end.html | ||||
| ``` | ||||
| 
 | ||||
| ## フッターへのカスタムセクション追加 | ||||
| 
 | ||||
| フッターに追加セクションを追加するには、サイト内に`layouts/partials/custom/footer.html`ファイルを作成します。 | ||||
| フッターに追加セクションを追加するには、サイト内に`layouts/_partials/custom/footer.html`ファイルを作成します。 | ||||
| 
 | ||||
| ```html {filename="layouts/partials/custom/footer.html"} | ||||
| ```html {filename="layouts/_partials/custom/footer.html"} | ||||
| <!-- ここにフッター要素を追加 --> | ||||
| ``` | ||||
| 
 | ||||
| @@ -221,14 +221,14 @@ To override the default syntax highlighting theme, we can add the generated styl | ||||
| You may add custom scripts to the end of the head for every page by adding the following file: | ||||
| 
 | ||||
| ``` | ||||
| layouts/partials/custom/head-end.html | ||||
| layouts/_partials/custom/head-end.html | ||||
| ``` | ||||
| 
 | ||||
| ## Custom Extra Section in Footer | ||||
| 
 | ||||
| You can add extra section in the footer by creating a file `layouts/partials/custom/footer.html` in your site. | ||||
| You can add extra section in the footer by creating a file `layouts/_partials/custom/footer.html` in your site. | ||||
| 
 | ||||
| ```html {filename="layouts/partials/custom/footer.html"} | ||||
| ```html {filename="layouts/_partials/custom/footer.html"} | ||||
| <!-- Your footer element here --> | ||||
| ``` | ||||
| 
 | ||||
| @@ -221,14 +221,14 @@ hugo gen chromastyles --style=github | ||||
| 您可以通过添加以下文件在每个页面的 head 末尾添加自定义脚本: | ||||
| 
 | ||||
| ``` | ||||
| layouts/partials/custom/head-end.html | ||||
| layouts/_partials/custom/head-end.html | ||||
| ``` | ||||
| 
 | ||||
| ## 自定义页脚额外部分 | ||||
| 
 | ||||
| 您可以通过在站点中创建 `layouts/partials/custom/footer.html` 文件来添加页脚的额外部分。 | ||||
| 您可以通过在站点中创建 `layouts/_partials/custom/footer.html` 文件来添加页脚的额外部分。 | ||||
| 
 | ||||
| ```html {filename="layouts/partials/custom/footer.html"} | ||||
| ```html {filename="layouts/_partials/custom/footer.html"} | ||||
| <!-- 您的页脚元素放在这里 --> | ||||
| ``` | ||||
| 
 | ||||
| @@ -7,7 +7,7 @@ tags: | ||||
| 
 | ||||
| Hugo تنظیمات خود را از فایل `hugo.yaml` در ریشه سایت شما میخواند. | ||||
| فایل پیکربندی جایی است که میتوانید تمام جنبههای سایت خود را تنظیم کنید. | ||||
| برای آشنایی جامع با تنظیمات موجود و بهترین روشها، فایل پیکربندی این سایت [`exampleSite/hugo.yaml`](https://github.com/imfing/hextra/blob/main/exampleSite/hugo.yaml) را در GitHub بررسی کنید. | ||||
| برای آشنایی جامع با تنظیمات موجود و بهترین روشها، فایل پیکربندی این سایت [`docs/hugo.yaml`](https://github.com/imfing/hextra/blob/main/docs/hugo.yaml) را در GitHub بررسی کنید. | ||||
| 
 | ||||
| <!--more--> | ||||
| 
 | ||||
| @@ -416,4 +416,4 @@ outputs: | ||||
| سایر ویژگیهای Open Graph میتوانند فقط یک مقدار داشته باشند. | ||||
| به عنوان مثال، این صفحه یک تگ `og:image` (که تصویری برای پیشنمایش در اشتراکگذاریهای اجتماعی پیکربندی میکند) و یک تگ `og:audio` دارد. | ||||
| 
 | ||||
| ```yaml {filename | ||||
| ```yaml {filename | ||||
| @@ -7,7 +7,7 @@ tags: | ||||
| 
 | ||||
| Hugo はサイトのルートにある `hugo.yaml` から設定を読み込みます。 | ||||
| この設定ファイルであなたのサイトのあらゆる側面を設定できます。 | ||||
| 利用可能な設定項目とベストプラクティスを網羅的に理解するには、GitHub 上のこのサイトの設定ファイル [`exampleSite/hugo.yaml`](https://github.com/imfing/hextra/blob/main/exampleSite/hugo.yaml) を参照してください。 | ||||
| 利用可能な設定項目とベストプラクティスを網羅的に理解するには、GitHub 上のこのサイトの設定ファイル [`docs/hugo.yaml`](https://github.com/imfing/hextra/blob/main/docs/hugo.yaml) を参照してください。 | ||||
| 
 | ||||
| <!--more--> | ||||
| 
 | ||||
| @@ -422,4 +422,4 @@ params: | ||||
|   images: | ||||
|     - "/img/config-image.jpg" | ||||
|   audio: "config-talk.mp3" | ||||
| ``` | ||||
| ``` | ||||
| @@ -7,7 +7,7 @@ tags: | ||||
| 
 | ||||
| Hugo reads its configuration from `hugo.yaml` in the root of your Hugo site. | ||||
| The config file is where you can configure all aspects of your site. | ||||
| Check out the config file for this site [`exampleSite/hugo.yaml`](https://github.com/imfing/hextra/blob/main/exampleSite/hugo.yaml) on GitHub to get a comprehensive idea of available settings and best practices. | ||||
| Check out the config file for this site [`docs/hugo.yaml`](https://github.com/imfing/hextra/blob/main/docs/hugo.yaml) on GitHub to get a comprehensive idea of available settings and best practices. | ||||
| 
 | ||||
| <!--more--> | ||||
| 
 | ||||
| @@ -164,6 +164,21 @@ menu: | ||||
|       weight: 3 | ||||
| ``` | ||||
| 
 | ||||
| ### Hiding | ||||
| 
 | ||||
| Hiding the sidebar can be done using front matter: | ||||
| 
 | ||||
| ```yaml {filename="content/docs/guide/configuration.md"} | ||||
| --- | ||||
| title: Configuration | ||||
| sidebar: | ||||
|   hide: true | ||||
| --- | ||||
| ``` | ||||
| 
 | ||||
| This will hide the main sidebar from the page, freeing up space for the main content of the page. | ||||
| 
 | ||||
| 
 | ||||
| ## Right Sidebar | ||||
| 
 | ||||
| ### Table of Contents | ||||
| @@ -404,7 +419,7 @@ params: | ||||
|     umami: | ||||
|       serverURL: "https://example.com" | ||||
|       websiteID: "94db1cb1-74f4-4a40-ad6c-962362670409" | ||||
|       # scriptName: "umami.js" # optional (default: umami.js) | ||||
|       # scriptName: "script.js" # optional (default: script.js) | ||||
|       # https://umami.is/docs/tracker-configuration#data-host-url | ||||
|       # hostURL: "http://stats.example.org" # optional | ||||
|       # https://umami.is/docs/tracker-configuration#data-auto-track | ||||
| @@ -456,19 +471,41 @@ The llms.txt file is automatically generated from your content structure and mak | ||||
| 
 | ||||
| ### Open Graph | ||||
| 
 | ||||
| To add [Open Graph](https://ogp.me/) metadata to a page, add values in the frontmatter params. | ||||
| To add [Open Graph](https://ogp.me/) metadata, you can: | ||||
| - add values in the front-matter params of a page | ||||
| - or add values in the Hugo configuration file | ||||
| 
 | ||||
| As a page can have multiple `image` and `video` tags, place their values in an array. | ||||
| Other Open Graph properties can have only one value. | ||||
| For example, this page has an `og:image` tag (which configures an image to preview on social shares) and an `og:audio` tag. | ||||
| 
 | ||||
| ```yaml {filename="content/docs/guide/configuration.md"} | ||||
| title: "Configuration" | ||||
| {{< tabs items="Page Level, Global Level" >}} | ||||
| {{< tab >}} | ||||
| 
 | ||||
| ```md {filename="mypage.md"} | ||||
| --- | ||||
| title: "My Page" | ||||
| params: | ||||
|   images: | ||||
|     - "/img/config-image.jpg" | ||||
|   audio: "config-talk.mp3" | ||||
|     - "/images/image01.jpg" | ||||
|   audio: "podcast02.mp3" | ||||
|   videos: | ||||
|     - "video01.mp4" | ||||
| --- | ||||
| 
 | ||||
| Page content. | ||||
| ``` | ||||
| {{< /tab >}} | ||||
| {{< tab >}} | ||||
| ```yaml {filename="hugo.yaml"} | ||||
| params: | ||||
|   images: | ||||
|     - "/images/image01.jpg" | ||||
|   audio: "podcast02.mp3" | ||||
|   videos: | ||||
|     - "video01.mp4" | ||||
| ``` | ||||
| {{< /tab >}} | ||||
| {{< /tabs >}} | ||||
| 
 | ||||
| ### Banner | ||||
| 
 | ||||
| @@ -7,7 +7,7 @@ tags: | ||||
| 
 | ||||
| Hugo 从站点根目录的 `hugo.yaml` 读取配置。 | ||||
| 配置文件可用来调整站点的所有方面。 | ||||
| 查看本网站的示例配置文件 [`exampleSite/hugo.yaml`](https://github.com/imfing/hextra/blob/main/exampleSite/hugo.yaml) 以全面了解可用设置和最佳实践。 | ||||
| 查看本网站的示例配置文件 [`docs/hugo.yaml`](https://github.com/imfing/hextra/blob/main/docs/hugo.yaml) 以全面了解可用设置和最佳实践。 | ||||
| 
 | ||||
| <!--more--> | ||||
| 
 | ||||
| @@ -422,4 +422,4 @@ params: | ||||
|   images: | ||||
|     - "/img/config-image.jpg" | ||||
|   audio: "config-talk.mp3" | ||||
| ``` | ||||
| ``` | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user