Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e123a30b7 | ||
|
|
e409eddf5c | ||
|
|
03bf8b0c3f | ||
|
|
e294e4ed70 | ||
|
|
d3eacc9daf | ||
|
|
c0f57bab7a | ||
|
|
752fd5805f | ||
|
|
0fc8264927 | ||
|
|
55340540b5 | ||
|
|
df9253a8e9 |
@@ -0,0 +1,80 @@
|
||||
@import "../../breakpoints";
|
||||
@import "../../mixins";
|
||||
|
||||
.artalk-container {
|
||||
@include card-style;
|
||||
padding: 5px;
|
||||
|
||||
@include respond(md) {
|
||||
padding: var(--card-padding);
|
||||
}
|
||||
}
|
||||
|
||||
.artalk {
|
||||
--at-color-bg: var(--card-background);
|
||||
--at-color-font: var(--card-text-color-main);
|
||||
--at-color-sub: var(--card-text-color-secondary);
|
||||
--at-color-main: var(--accent-color);
|
||||
--at-color-border: var(--card-separator-color);
|
||||
--at-border-radius: var(--card-border-radius);
|
||||
--at-color-link: var(--accent-color);
|
||||
}
|
||||
|
||||
.artalk a {
|
||||
color: var(--at-color-link);
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent-color-darker);
|
||||
}
|
||||
}
|
||||
|
||||
.artalk .atk-content a,
|
||||
.artalk .atk-editor-plug-preview a {
|
||||
color: var(--accent-color);
|
||||
box-shadow: 0px -2px 0px rgba(var(--link-background-color), var(--link-background-opacity)) inset;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent-color-darker);
|
||||
box-shadow: 0px calc(-1rem * var(--article-line-height)) 0px rgba(var(--link-background-color), var(--link-background-opacity-hover)) inset;
|
||||
}
|
||||
}
|
||||
|
||||
[data-scheme="dark"] .artalk {
|
||||
--at-color-main: var(--accent-color);
|
||||
--at-color-link: var(--accent-color);
|
||||
--at-color-bg: var(--card-background);
|
||||
--at-color-bg-transl: rgba(66, 66, 66, 0.95);
|
||||
--at-color-bg-grey: rgba(255, 255, 255, 0.08);
|
||||
--at-color-bg-grey-transl: rgba(255, 255, 255, 0.06);
|
||||
--at-color-bg-light: rgba(255, 255, 255, 0.08);
|
||||
--at-color-border: var(--card-separator-color);
|
||||
--at-color-font: var(--card-text-color-main);
|
||||
--at-color-deep: var(--card-text-color-main);
|
||||
--at-color-sub: var(--card-text-color-secondary);
|
||||
--at-color-meta: var(--card-text-color-secondary);
|
||||
--at-color-grey: var(--card-text-color-secondary);
|
||||
--at-color-gradient: linear-gradient(
|
||||
180deg,
|
||||
transparent,
|
||||
var(--card-background)
|
||||
);
|
||||
}
|
||||
|
||||
.artalk code {
|
||||
font-family: var(--code-font-family);
|
||||
}
|
||||
|
||||
.artalk .atk-avatar img {
|
||||
border-radius: var(--card-border-radius);
|
||||
}
|
||||
|
||||
[data-scheme="dark"] .artalk .atk-avatar img {
|
||||
filter: brightness(0.75);
|
||||
}
|
||||
|
||||
[data-scheme="dark"] .artalk .atk-send-btn {
|
||||
color: var(--accent-color-text);
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
padding: var(--container-padding);
|
||||
pointer-events: none;
|
||||
|
||||
&[aria-hidden="true"] {
|
||||
display: none;
|
||||
@@ -21,6 +22,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
pointer-events: auto;
|
||||
|
||||
@include respond(md) {
|
||||
flex-direction: row;
|
||||
|
||||
@@ -95,13 +95,6 @@
|
||||
-webkit-mask-image: var(--subsection-mask-image);
|
||||
mask-image: var(--subsection-mask-image);
|
||||
|
||||
// CSS-only: left fade ramps in quickly once horizontal scrolling starts,
|
||||
// and right fade turns off near the end of horizontal scroll.
|
||||
animation: subsection-left-fade linear both, subsection-right-fade linear both;
|
||||
animation-timeline: scroll(self inline);
|
||||
animation-range: 1px 24px, 0% 100%;
|
||||
animation-timing-function: ease-out, linear;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin-left: calc(var(--overlap-x) * -1);
|
||||
margin-right: calc(var(--overlap-x) * -1);
|
||||
@@ -132,6 +125,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Only apply scroll-driven edge fades in browsers that support scroll timelines.
|
||||
// Browsers without support (e.g. Firefox) fall back to the static right-edge fade
|
||||
// defined by the explicit CSS variable declarations above.
|
||||
@supports (animation-timeline: scroll(self inline)) {
|
||||
.subsection-list {
|
||||
animation: subsection-left-fade linear both, subsection-right-fade linear both;
|
||||
animation-timeline: scroll(self inline);
|
||||
animation-range: 1px 24px, 0% 100%;
|
||||
animation-timing-function: ease-out, linear;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes subsection-left-fade {
|
||||
from {
|
||||
--left-edge-fade-size: 0px;
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Consent-gated comments bootstrap.
|
||||
*
|
||||
* This module controls whether third-party comments are rendered based on
|
||||
* functional cookie consent, and injects template scripts in sequence to
|
||||
* avoid eager or duplicate script execution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Consent event payload used by the comments gate.
|
||||
* It mirrors cookie consent state shape but keeps optional fields for safety.
|
||||
*/
|
||||
interface CommentsConsentState {
|
||||
necessary?: boolean;
|
||||
analytics?: boolean;
|
||||
functional?: boolean;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve functional consent with layered fallback:
|
||||
* 1) explicit event detail, 2) CookieConsent public API, 3) dataset mirror.
|
||||
*/
|
||||
const hasFunctionalConsent = (consentDetail?: CommentsConsentState | null): boolean => {
|
||||
const cookieConsent = (window as Window & {
|
||||
cookieConsent?: {
|
||||
hasConsent?: (category: 'necessary' | 'analytics' | 'functional') => boolean;
|
||||
};
|
||||
}).cookieConsent;
|
||||
|
||||
return (
|
||||
consentDetail?.functional ??
|
||||
cookieConsent?.hasConsent?.('functional') ??
|
||||
document.documentElement.dataset.consentFunctional === 'true'
|
||||
);
|
||||
};
|
||||
|
||||
interface DeferredScript {
|
||||
placeholder: Comment;
|
||||
script: HTMLScriptElement;
|
||||
}
|
||||
|
||||
const prepareDeferredScripts = (fragment: DocumentFragment): DeferredScript[] => {
|
||||
return Array.from(fragment.querySelectorAll('script')).map(script => {
|
||||
const placeholder = document.createComment('comments-script-placeholder');
|
||||
script.replaceWith(placeholder);
|
||||
return { placeholder, script };
|
||||
});
|
||||
};
|
||||
|
||||
const activateDeferredScripts = async (scripts: DeferredScript[]): Promise<void> => {
|
||||
// Inject scripts sequentially so third-party embeds can rely on execution order.
|
||||
for (const { placeholder, script } of scripts) {
|
||||
if (!placeholder.parentNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newScript = document.createElement('script');
|
||||
Array.from(script.attributes).forEach(attr => {
|
||||
newScript.setAttribute(attr.name, attr.value);
|
||||
});
|
||||
|
||||
if (script.textContent) {
|
||||
newScript.text = script.textContent;
|
||||
}
|
||||
|
||||
let done: Promise<void> = Promise.resolve();
|
||||
if (newScript.src) {
|
||||
done = new Promise<void>(resolve => {
|
||||
newScript.onload = newScript.onerror = () => resolve();
|
||||
});
|
||||
}
|
||||
|
||||
placeholder.replaceWith(newScript);
|
||||
await done;
|
||||
}
|
||||
};
|
||||
|
||||
const initCommentsConsent = (): void => {
|
||||
// Main entry: gate comments rendering by functional cookie consent.
|
||||
const placeholder = document.getElementById('comments-consent-placeholder') as HTMLElement | null;
|
||||
const container = document.getElementById('comments-container') as HTMLElement | null;
|
||||
const template = document.getElementById('comments-template') as HTMLTemplateElement | null;
|
||||
|
||||
if (!placeholder || !container || !template) {
|
||||
return;
|
||||
}
|
||||
|
||||
let commentsLoaded = false;
|
||||
let commentsLoading = false;
|
||||
|
||||
const showComments = async (): Promise<void> => {
|
||||
placeholder.style.display = 'none';
|
||||
container.style.display = 'block';
|
||||
|
||||
if (commentsLoaded || commentsLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent duplicate template hydration while scripts are still loading.
|
||||
commentsLoading = true;
|
||||
|
||||
try {
|
||||
const clone = template.content.cloneNode(true) as DocumentFragment;
|
||||
const scripts = prepareDeferredScripts(clone);
|
||||
|
||||
container.appendChild(clone);
|
||||
await activateDeferredScripts(scripts);
|
||||
commentsLoaded = true;
|
||||
} finally {
|
||||
commentsLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
const hideComments = (): void => {
|
||||
placeholder.style.display = 'block';
|
||||
container.style.display = 'none';
|
||||
};
|
||||
|
||||
window.addEventListener('onCookieConsentChange', (event: Event) => {
|
||||
// Cross-module contract: this event is dispatched by cookies.ts.
|
||||
const customEvent = event as CustomEvent<CommentsConsentState | null>;
|
||||
if (hasFunctionalConsent(customEvent.detail)) {
|
||||
showComments();
|
||||
} else {
|
||||
hideComments();
|
||||
}
|
||||
});
|
||||
|
||||
if (hasFunctionalConsent()) {
|
||||
showComments();
|
||||
} else {
|
||||
hideComments();
|
||||
}
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initCommentsConsent, { once: true });
|
||||
} else {
|
||||
initCommentsConsent();
|
||||
}
|
||||
@@ -175,6 +175,7 @@ class CookieConsent {
|
||||
}
|
||||
|
||||
private dispatchConsentEvent(): void {
|
||||
// Cross-module event consumed by consent-gated features (for example commentsConsent.ts).
|
||||
const event = new CustomEvent('onCookieConsentChange', {
|
||||
detail: this.state
|
||||
});
|
||||
|
||||
@@ -101,6 +101,10 @@ SortBy = "default"
|
||||
enabled = true
|
||||
provider = "disqus"
|
||||
|
||||
[comments.artalk]
|
||||
serverUrl = ""
|
||||
site = ""
|
||||
|
||||
[comments.disqusjs]
|
||||
shortname = ""
|
||||
apiUrl = ""
|
||||
|
||||
@@ -13,3 +13,7 @@ Cactus = [
|
||||
Style = "https://cdn.jsdelivr.net/npm/photoswipe@5.4.4/dist/photoswipe.css"
|
||||
Core = "https://cdn.jsdelivr.net/npm/photoswipe@5.4.4/dist/photoswipe.esm.min.js"
|
||||
Lightbox = "https://cdn.jsdelivr.net/npm/photoswipe@5.4.4/dist/photoswipe-lightbox.esm.min.js"
|
||||
|
||||
[Artalk]
|
||||
Style = "https://cdn.jsdelivr.net/npm/artalk@2.9.1/dist/Artalk.css"
|
||||
Script = "https://cdn.jsdelivr.net/npm/artalk@2.9.1/+esm"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
baseURL = "https://demo.stack.cai.im/"
|
||||
languageCode = "en-us"
|
||||
locale = "en-us"
|
||||
title = "Hugo Theme Stack"
|
||||
# Theme i18n support
|
||||
# See i18n folder for available languages
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[en]
|
||||
languageName = "English"
|
||||
label = "English"
|
||||
title = "Hugo Theme Stack"
|
||||
weight = 1
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
subtitle = "Card-style Hugo theme designed for bloggers"
|
||||
|
||||
[zh]
|
||||
languageName = "简体中文"
|
||||
label = "简体中文"
|
||||
title = "Hugo 主题 Stack"
|
||||
weight = 2
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
subtitle = "为博客设计的卡片式 Hugo 主题"
|
||||
|
||||
[zh-hant-tw]
|
||||
languageName = "正體中文"
|
||||
label = "正體中文"
|
||||
title = "Hugo 主題 Stack"
|
||||
weight = 3
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
subtitle = "為博客設計的卡片式 Hugo 主題"
|
||||
|
||||
[ja]
|
||||
languageName = "日本語"
|
||||
label = "日本語"
|
||||
title = "Hugo Theme Stack"
|
||||
weight = 4
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<div class="inline-meta">
|
||||
{{ partial "helper/icon" "language" }}
|
||||
{{ range $Page.Translations }}
|
||||
<a href="{{ .RelPermalink }}" class="link">{{ .Language.LanguageName }}</a>
|
||||
<a href="{{ .RelPermalink }}" class="link">{{ .Language.Label }}</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
{{/*
|
||||
Comments include entry.
|
||||
- If functional cookie consent is required, render a placeholder + template and load commentsConsent.ts.
|
||||
- Otherwise, render provider partial directly.
|
||||
*/}}
|
||||
{{ if .Site.Params.comments.enabled }}
|
||||
{{- $needsConsent := and .Site.Params.cookies.enabled .Site.Params.cookies.categories.functional -}}
|
||||
{{- if $needsConsent -}}
|
||||
@@ -8,38 +13,14 @@
|
||||
{{ T "cookies.managePreferences" }}
|
||||
</button>
|
||||
</div>
|
||||
<div id="comments-container" style="display: none;">
|
||||
<div id="comments-container" style="display: none;"></div>
|
||||
<template id="comments-template">
|
||||
{{ partial (printf "comments/provider/%s" .Site.Params.comments.provider) . }}
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
var placeholder = document.getElementById('comments-consent-placeholder');
|
||||
var container = document.getElementById('comments-container');
|
||||
|
||||
function showComments() {
|
||||
if (placeholder) placeholder.style.display = 'none';
|
||||
if (container) container.style.display = 'block';
|
||||
}
|
||||
|
||||
function hideComments() {
|
||||
if (placeholder) placeholder.style.display = 'block';
|
||||
if (container) container.style.display = 'none';
|
||||
}
|
||||
|
||||
window.addEventListener('onCookieConsentChange', function(e) {
|
||||
if (e.detail && e.detail.functional) {
|
||||
showComments();
|
||||
} else {
|
||||
hideComments();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if already consented
|
||||
if (window.cookieConsent && window.cookieConsent.hasConsent('functional')) {
|
||||
showComments();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</template>
|
||||
{{- $opts := dict "minify" hugo.IsProduction -}}
|
||||
{{/* commentsConsent.ts is intentionally built and loaded here as an independent entry */}}
|
||||
{{- $commentsScript := resources.Get "ts/commentsConsent.ts" | js.Build $opts | fingerprint -}}
|
||||
<script type="text/javascript" src="{{ $commentsScript.RelPermalink }}" integrity="{{ $commentsScript.Data.Integrity }}" defer></script>
|
||||
{{- else -}}
|
||||
{{/* No consent required - load comments normally */}}
|
||||
{{ partial (printf "comments/provider/%s" .Site.Params.comments.provider) . }}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{{- $style := resources.Get "scss/partials/comments/artalk.scss" | toCSS | minify -}}
|
||||
{{- with .Site.Params.comments.artalk -}}
|
||||
<link rel="stylesheet" href="{{ hugo.Data.external.Artalk.Style }}">
|
||||
<link rel="stylesheet" href="{{ $style.RelPermalink }}">
|
||||
|
||||
<div id="Comments" class="artalk-container"></div>
|
||||
|
||||
<script type="module">
|
||||
import Artalk from '{{ hugo.Data.external.Artalk.Script }}';
|
||||
|
||||
function isDarkMode() {
|
||||
return document.documentElement.dataset.scheme === 'dark';
|
||||
}
|
||||
|
||||
const artalkInstance = Artalk.init({
|
||||
el: '#Comments',
|
||||
pageKey: '{{ $.RelPermalink }}',
|
||||
pageTitle: '{{ $.Title }}',
|
||||
server: '{{ .serverUrl }}',
|
||||
site: '{{ .site }}',
|
||||
darkMode: isDarkMode(),
|
||||
locale: 'auto',
|
||||
});
|
||||
|
||||
window.addEventListener('onColorSchemeChange', (e) => {
|
||||
if (!artalkInstance || typeof artalkInstance.setDarkMode !== 'function') return;
|
||||
artalkInstance.setDarkMode(e.detail === 'dark');
|
||||
});
|
||||
</script>
|
||||
{{- end -}}
|
||||
@@ -1,4 +1,4 @@
|
||||
{{- $ThemeVersion := "4.0.0" -}}
|
||||
{{- $ThemeVersion := "4.0.3" -}}
|
||||
<footer class="site-footer">
|
||||
<section class="copyright">
|
||||
©
|
||||
|
||||
@@ -14,12 +14,14 @@
|
||||
{{ if .Image }}
|
||||
{{ $url := urls.Parse .Image }}
|
||||
{{ $permalink := .Image }}
|
||||
{{ $resource := dict
|
||||
|
||||
{{ $result = dict
|
||||
"Resource" nil
|
||||
"Permalink" $permalink
|
||||
"Height" nil
|
||||
"Width" nil
|
||||
}}
|
||||
{{ $resource := dict }}
|
||||
{{ $local := true }}
|
||||
|
||||
{{ if not (in (slice "https" "http") $url.Scheme) }}
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
{{ end }}
|
||||
<li class="menu-bottom-section">
|
||||
<ol class="menu">
|
||||
{{- $currentLanguageCode := .Language.Lang -}}
|
||||
{{- $currentLanguageCode := .Language.Name -}}
|
||||
{{ if hugo.IsMultilingual }}
|
||||
<li id="i18n-switch">
|
||||
{{ partial "helper/icon" "language" }}
|
||||
@@ -72,11 +72,11 @@
|
||||
{{ range .Site.Home.AllTranslations }}
|
||||
{{ $target := . }}
|
||||
{{ range $.AllTranslations }}
|
||||
{{ if eq .Language.Lang $target.Language.Lang }}
|
||||
{{ if eq .Language.Name $target.Language.Name }}
|
||||
{{ $target = . }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<option value="{{ $target.RelPermalink }}" {{ if eq $target.Language.Lang $currentLanguageCode }}selected{{ end }}>{{ .Language.LanguageName }}</option>
|
||||
<option value="{{ $target.RelPermalink }}" {{ if eq $target.Language.Name $currentLanguageCode }}selected{{ end }}>{{ .Language.Label }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</li>
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ .Site.LanguageCode }}" dir="{{ default `ltr` .Language.LanguageDirection }}">
|
||||
<html lang="{{ .Site.Language.Locale }}" dir="{{ default `ltr` .Language.Direction }}">
|
||||
<head>
|
||||
{{- partial "head/head.html" . -}}
|
||||
{{- block "head" . -}}{{ end }}
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@
|
||||
<link>{{ .Permalink }}</link>
|
||||
<description>Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{ . }} {{ end }}{{ end }}on {{ .Site.Title }}</description>
|
||||
<generator>Hugo -- gohugo.io</generator>
|
||||
<language>{{ site.Language.LanguageCode }}</language>{{ with $authorEmail }}
|
||||
<language>{{ site.Language.Locale }}</language>{{ with $authorEmail }}
|
||||
<managingEditor>{{.}}{{ with $authorName }} ({{ . }}){{ end }}</managingEditor>{{ end }}{{ with $authorEmail }}
|
||||
<webMaster>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}</webMaster>{{ end }}{{ with .Site.Copyright }}
|
||||
<copyright>{{ . }}</copyright>{{ end }}{{ if not .Date.IsZero }}
|
||||
|
||||
+1
-2
@@ -15,8 +15,7 @@
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- $TOCManuallyDisabled := eq .Params.toc false -}}
|
||||
{{- $TOCEnabled := and (not $TOCManuallyDisabled) $TOCWidgetEnabled -}}
|
||||
{{- $TOCEnabled := and (default .Site.Params.article.toc .Params.toc) $TOCWidgetEnabled -}}
|
||||
{{- $hasTOC := ge (len .TableOfContents) 100 -}}
|
||||
{{- .Scratch.Set "TOCEnabled" (and $TOCEnabled $hasTOC) -}}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user