mirror of
https://github.com/Ad-closeNN/blog-fuwari.git
synced 2026-05-31 02:20:05 -04:00
feat(theme): 主题相关合并一起、查看源代码按钮
This commit is contained in:
@@ -1,79 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { DARK_MODE, LIGHT_MODE } from "@constants/constants.ts";
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import Icon from "@iconify/svelte";
|
||||
import {
|
||||
applyThemeToDocument,
|
||||
getStoredTheme,
|
||||
setTheme,
|
||||
} from "@utils/setting-utils.ts";
|
||||
import { onMount } from "svelte";
|
||||
import type { LIGHT_DARK_MODE } from "@/types/config.ts";
|
||||
|
||||
const seq: LIGHT_DARK_MODE[] = [LIGHT_MODE, DARK_MODE];
|
||||
let mode: LIGHT_DARK_MODE = LIGHT_MODE;
|
||||
|
||||
onMount(() => {
|
||||
mode = getStoredTheme();
|
||||
// 移除了系统主题监听功能
|
||||
// 只在初始化时应用存储的主题
|
||||
applyThemeToDocument(mode);
|
||||
});
|
||||
|
||||
function switchScheme(newMode: LIGHT_DARK_MODE) {
|
||||
mode = newMode;
|
||||
setTheme(newMode);
|
||||
}
|
||||
|
||||
function toggleScheme() {
|
||||
let i = 0;
|
||||
for (; i < seq.length; i++) {
|
||||
if (seq[i] === mode) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
switchScheme(seq[(i + 1) % seq.length]);
|
||||
}
|
||||
|
||||
function showPanel() {
|
||||
const panel = document.querySelector("#light-dark-panel");
|
||||
panel.classList.remove("float-panel-closed");
|
||||
}
|
||||
|
||||
function hidePanel() {
|
||||
const panel = document.querySelector("#light-dark-panel");
|
||||
panel.classList.add("float-panel-closed");
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- z-50 make the panel higher than other float panels -->
|
||||
<div class="relative z-50" role="menu" tabindex="-1" onmouseleave={hidePanel}>
|
||||
<button aria-label="Light/Dark Mode" role="menuitem" class="relative btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="scheme-switch" onclick={toggleScheme} onmouseenter={showPanel}>
|
||||
<div class="absolute" class:opacity-0={mode !== LIGHT_MODE}>
|
||||
<Icon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem]"></Icon>
|
||||
</div>
|
||||
<div class="absolute" class:opacity-0={mode !== DARK_MODE}>
|
||||
<Icon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem]"></Icon>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div id="light-dark-panel" class="hidden lg:block absolute transition float-panel-closed top-11 -right-2 pt-5">
|
||||
<div class="card-base float-panel p-2">
|
||||
<button class="flex transition whitespace-nowrap items-center !justify-start w-full btn-plain scale-animation rounded-lg h-9 px-3 font-medium active:scale-95 mb-0.5"
|
||||
class:current-theme-btn={mode === LIGHT_MODE}
|
||||
onclick={() => switchScheme(LIGHT_MODE)}
|
||||
>
|
||||
<Icon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem] mr-3"></Icon>
|
||||
{i18n(I18nKey.lightMode)}
|
||||
</button>
|
||||
<button class="flex transition whitespace-nowrap items-center !justify-start w-full btn-plain scale-animation rounded-lg h-9 px-3 font-medium active:scale-95 mb-0.5"
|
||||
class:current-theme-btn={mode === DARK_MODE}
|
||||
onclick={() => switchScheme(DARK_MODE)}
|
||||
>
|
||||
<Icon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem] mr-3"></Icon>
|
||||
{i18n(I18nKey.darkMode)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4,10 +4,9 @@ import { navBarConfig, siteConfig } from "../config";
|
||||
import { LinkPresets } from "../constants/link-presets";
|
||||
import { LinkPreset, type NavBarLink } from "../types/config";
|
||||
import { url } from "../utils/url-utils";
|
||||
import LightDarkSwitch from "./LightDarkSwitch.svelte";
|
||||
import Search from "./Search.svelte";
|
||||
import DisplaySettings from "./widget/DisplaySettings.svelte";
|
||||
import NavMenuPanel from "./widget/NavMenuPanel.astro";
|
||||
import ThemeSettingsBlock from "./widget/ThemeSettingsBlock.svelte";
|
||||
|
||||
const className = Astro.props.class;
|
||||
|
||||
@@ -28,7 +27,7 @@ let links: NavBarLink[] = navBarConfig.links.map(
|
||||
<div aria-hidden="true" class="pointer-events-none absolute inset-x-0 top-0 h-px bg-white/55 dark:bg-white/12"></div>
|
||||
<a href={url('/')} class="btn-plain scale-animation rounded-lg h-[3.25rem] px-5 font-bold active:scale-95">
|
||||
<div class="flex flex-row text-[var(--primary)] items-center text-md">
|
||||
<Icon name="material-symbols:home-outline-rounded" class="text-[1.75rem] mb-1 mr-2" />
|
||||
<Icon is:inline name="material-symbols:home-outline-rounded" class="text-[1.75rem] mb-1 mr-2" />
|
||||
{siteConfig.title}
|
||||
</div>
|
||||
</a>
|
||||
@@ -39,7 +38,7 @@ let links: NavBarLink[] = navBarConfig.links.map(
|
||||
>
|
||||
<div class="flex items-center">
|
||||
{l.name}
|
||||
{l.external && <Icon name="fa6-solid:arrow-up-right-from-square" class="text-[0.875rem] transition -translate-y-[1px] ml-1 text-black/[0.2] dark:text-white/[0.2]"></Icon>}
|
||||
{l.external && <Icon is:inline name="fa6-solid:arrow-up-right-from-square" class="text-[0.875rem] transition -translate-y-[1px] ml-1 text-black/[0.2] dark:text-white/[0.2]"></Icon>}
|
||||
</div>
|
||||
</a>;
|
||||
})}
|
||||
@@ -47,50 +46,17 @@ let links: NavBarLink[] = navBarConfig.links.map(
|
||||
<div class="flex">
|
||||
<!--<SearchPanel client:load>-->
|
||||
<Search client:only="svelte"></Search>
|
||||
{!siteConfig.themeColor.fixed && (
|
||||
<button aria-label="Display Settings" class="btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="display-settings-switch">
|
||||
<Icon name="material-symbols:palette-outline" class="text-[1.25rem]"></Icon>
|
||||
</button>
|
||||
)}
|
||||
<LightDarkSwitch client:only="svelte"></LightDarkSwitch>
|
||||
<ThemeSettingsBlock showThemeColor={!siteConfig.themeColor.fixed} client:only="svelte"></ThemeSettingsBlock>
|
||||
<button aria-label="Menu" name="Nav Menu" class="btn-plain scale-animation rounded-lg w-11 h-11 active:scale-90 md:!hidden" id="nav-menu-switch">
|
||||
<Icon name="material-symbols:menu-rounded" class="text-[1.25rem]"></Icon>
|
||||
<Icon is:inline name="material-symbols:menu-rounded" class="text-[1.25rem]"></Icon>
|
||||
</button>
|
||||
</div>
|
||||
<NavMenuPanel links={links}></NavMenuPanel>
|
||||
<DisplaySettings client:only="svelte"></DisplaySettings>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function switchTheme() {
|
||||
if (localStorage.theme === 'dark') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.theme = 'light';
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.theme = 'dark';
|
||||
}
|
||||
}
|
||||
|
||||
function loadButtonScript() {
|
||||
let switchBtn = document.getElementById("scheme-switch");
|
||||
if (switchBtn) {
|
||||
switchBtn.onclick = function () {
|
||||
switchTheme()
|
||||
};
|
||||
}
|
||||
|
||||
let settingBtn = document.getElementById("display-settings-switch");
|
||||
if (settingBtn) {
|
||||
settingBtn.onclick = function () {
|
||||
let settingPanel = document.getElementById("display-setting");
|
||||
if (settingPanel) {
|
||||
settingPanel.classList.toggle("float-panel-closed");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let menuBtn = document.getElementById("nav-menu-switch");
|
||||
if (menuBtn) {
|
||||
menuBtn.onclick = function () {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import { render } from "astro:content";
|
||||
import path from "node:path";
|
||||
import { Icon } from "astro-icon/components";
|
||||
import I18nKey from "../i18n/i18nKey";
|
||||
@@ -7,7 +8,6 @@ import { i18n } from "../i18n/translation";
|
||||
import { getDir } from "../utils/url-utils";
|
||||
import ImageWrapper from "./misc/ImageWrapper.astro";
|
||||
import PostMetadata from "./PostMeta.astro";
|
||||
import { render } from 'astro:content';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
@@ -57,15 +57,15 @@ const { remarkPluginFrontmatter } = await render(entry);
|
||||
<span class="flex flex-wrap items-center gap-x-3 gap-y-2">
|
||||
{isPinned && (
|
||||
<span class="inline-flex items-center gap-1 rounded-full border border-[var(--primary)]/20 bg-[var(--primary)]/10 px-2.5 py-1 text-xs font-semibold text-[var(--primary)] leading-none">
|
||||
<Icon name="material-symbols:keep-rounded" class="text-lg"></Icon>
|
||||
<Icon is:inline name="material-symbols:keep-rounded" class="text-lg"></Icon>
|
||||
置顶
|
||||
</span>
|
||||
)}
|
||||
<span class="min-w-0">
|
||||
{title}
|
||||
<span class="inline-flex items-center align-middle whitespace-nowrap">
|
||||
<Icon class="inline text-[2rem] text-[var(--primary)] translate-y-0.5 md:hidden" name="material-symbols:chevron-right-rounded" ></Icon>
|
||||
<Icon class="text-[var(--primary)] text-[2rem] transition hidden md:inline translate-y-0.5 opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0" name="material-symbols:chevron-right-rounded"></Icon>
|
||||
<Icon is:inline class="inline text-[2rem] text-[var(--primary)] translate-y-0.5 md:hidden" name="material-symbols:chevron-right-rounded" ></Icon>
|
||||
<Icon is:inline class="text-[var(--primary)] text-[2rem] transition hidden md:inline translate-y-0.5 opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0" name="material-symbols:chevron-right-rounded"></Icon>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
@@ -105,7 +105,7 @@ const { remarkPluginFrontmatter } = await render(entry);
|
||||
]} >
|
||||
<div class="absolute pointer-events-none z-10 w-full h-full group-hover:bg-black/30 group-active:bg-black/50 transition"></div>
|
||||
<div class="absolute pointer-events-none z-20 w-full h-full flex items-center justify-center ">
|
||||
<Icon name="material-symbols:chevron-right-rounded"
|
||||
<Icon is:inline name="material-symbols:chevron-right-rounded"
|
||||
class="transition opacity-0 group-hover:opacity-100 scale-50 group-hover:scale-100 text-white text-5xl">
|
||||
</Icon>
|
||||
</div>
|
||||
@@ -119,7 +119,7 @@ const { remarkPluginFrontmatter } = await render(entry);
|
||||
absolute right-3 top-3 bottom-3 rounded-xl bg-[var(--enter-btn-bg)]
|
||||
hover:bg-[var(--enter-btn-bg-hover)] active:bg-[var(--enter-btn-bg-active)] active:scale-95
|
||||
">
|
||||
<Icon name="material-symbols:chevron-right-rounded"
|
||||
<Icon is:inline name="material-symbols:chevron-right-rounded"
|
||||
class="transition text-[var(--primary)] text-4xl mx-auto">
|
||||
</Icon>
|
||||
</a>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Icon } from "astro-icon/components";
|
||||
import I18nKey from "../i18n/i18nKey";
|
||||
import { i18n } from "../i18n/translation";
|
||||
import { formatDateToYYYYMMDD } from "../utils/date-utils";
|
||||
import { getCategoryUrl, getTagUrl, getDir, url } from "../utils/url-utils";
|
||||
import { getCategoryUrl, getDir, getTagUrl, url } from "../utils/url-utils";
|
||||
|
||||
interface Props {
|
||||
class: string;
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
category: string | null;
|
||||
hideTagsForMobile?: boolean;
|
||||
hideUpdateDate?: boolean;
|
||||
slug?: string;
|
||||
slug?: string;
|
||||
}
|
||||
const {
|
||||
published,
|
||||
@@ -23,7 +23,7 @@ const {
|
||||
category,
|
||||
hideTagsForMobile = false,
|
||||
hideUpdateDate = false,
|
||||
slug,
|
||||
slug,
|
||||
} = Astro.props;
|
||||
const className = Astro.props.class;
|
||||
---
|
||||
@@ -33,7 +33,7 @@ const className = Astro.props.class;
|
||||
<div class="flex items-center">
|
||||
<div class="meta-icon"
|
||||
>
|
||||
<Icon name="material-symbols:calendar-today-outline-rounded" class="text-xl"></Icon>
|
||||
<Icon is:inline name="material-symbols:calendar-today-outline-rounded" class="text-xl"></Icon>
|
||||
</div>
|
||||
<span class="text-50 text-sm font-medium">{formatDateToYYYYMMDD(published)}</span>
|
||||
</div>
|
||||
@@ -43,7 +43,7 @@ const className = Astro.props.class;
|
||||
<div class="flex items-center">
|
||||
<div class="meta-icon"
|
||||
>
|
||||
<Icon name="material-symbols:edit-calendar-outline-rounded" class="text-xl"></Icon>
|
||||
<Icon is:inline name="material-symbols:edit-calendar-outline-rounded" class="text-xl"></Icon>
|
||||
</div>
|
||||
<span class="text-50 text-sm font-medium">{formatDateToYYYYMMDD(updated)}</span>
|
||||
</div>
|
||||
@@ -54,7 +54,7 @@ const className = Astro.props.class;
|
||||
<div class="flex items-center">
|
||||
<div class="meta-icon"
|
||||
>
|
||||
<Icon name="material-symbols:book-2-outline-rounded" class="text-xl"></Icon>
|
||||
<Icon is:inline name="material-symbols:book-2-outline-rounded" class="text-xl"></Icon>
|
||||
</div>
|
||||
<div class="flex flex-row flex-nowrap items-center">
|
||||
<a href={getCategoryUrl(category)} aria-label={`View all posts in the ${category} category`}
|
||||
@@ -69,7 +69,7 @@ const className = Astro.props.class;
|
||||
<div class:list={["items-center", {"flex": !hideTagsForMobile, "hidden md:flex": hideTagsForMobile}]}>
|
||||
<div class="meta-icon"
|
||||
>
|
||||
<Icon name="material-symbols:tag-rounded" class="text-xl"></Icon>
|
||||
<Icon is:inline name="material-symbols:tag-rounded" class="text-xl"></Icon>
|
||||
</div>
|
||||
<div class="flex flex-row flex-nowrap items-center">
|
||||
{(tags && tags.length > 0) && tags.map((tag, i) => (
|
||||
@@ -87,7 +87,7 @@ const className = Astro.props.class;
|
||||
{slug && (
|
||||
<div class="flex items-center">
|
||||
<div class="meta-icon">
|
||||
<Icon name="material-symbols:visibility-outline-rounded" class="text-xl"></Icon>
|
||||
<Icon is:inline name="material-symbols:visibility-outline-rounded" class="text-xl"></Icon>
|
||||
</div>
|
||||
<span class="text-50 text-sm font-medium" id="page-views-display">加载中...</span>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Icon } from "astro-icon/components";
|
||||
<div class="back-to-top-wrapper hidden lg:block">
|
||||
<div id="back-to-top-btn" class="back-to-top-btn hide flex items-center rounded-2xl overflow-hidden transition" onclick="backToTop()">
|
||||
<button aria-label="Back to Top" class="btn-card h-[3.75rem] w-[3.75rem]">
|
||||
<Icon name="material-symbols:keyboard-arrow-up-rounded" class="mx-auto"></Icon>
|
||||
<Icon is:inline name="material-symbols:keyboard-arrow-up-rounded" class="mx-auto"></Icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,12 +57,12 @@ const getPageUrl = (p: number) => {
|
||||
{"disabled": page.url.prev == undefined}
|
||||
]}
|
||||
>
|
||||
<Icon name="material-symbols:chevron-left-rounded" class="text-[1.75rem]"></Icon>
|
||||
<Icon is:inline name="material-symbols:chevron-left-rounded" class="text-[1.75rem]"></Icon>
|
||||
</a>
|
||||
<div class="bg-[var(--card-bg)] flex flex-row rounded-lg items-center text-neutral-700 dark:text-neutral-300 font-bold">
|
||||
{pages.map((p) => {
|
||||
if (p == HIDDEN)
|
||||
return <Icon name="material-symbols:more-horiz" class="mx-1"/>;
|
||||
return <Icon is:inline name="material-symbols:more-horiz" class="mx-1"/>;
|
||||
if (p == page.currentPage)
|
||||
return <div class="h-11 w-11 rounded-lg bg-[var(--primary)] flex items-center justify-center
|
||||
font-bold text-white dark:text-black/70"
|
||||
@@ -79,6 +79,6 @@ const getPageUrl = (p: number) => {
|
||||
{"disabled": page.url.next == undefined}
|
||||
]}
|
||||
>
|
||||
<Icon name="material-symbols:chevron-right-rounded" class="text-[1.75rem]"></Icon>
|
||||
<Icon is:inline name="material-symbols:chevron-right-rounded" class="text-[1.75rem]"></Icon>
|
||||
</a>
|
||||
</div>
|
||||
@@ -39,5 +39,5 @@ const postUrl = decodeURIComponent(Astro.url.toString());
|
||||
<a href={licenseConf.url} target="_blank" class="link text-[var(--primary)] line-clamp-2">{licenseConf.name}</a>
|
||||
</div>
|
||||
</div>
|
||||
<Icon name="fa6-brands:creative-commons" class="transition text-[15rem] absolute pointer-events-none right-6 top-1/2 -translate-y-1/2 text-black/5 dark:text-white/5"></Icon>
|
||||
<Icon is:inline name="fa6-brands:creative-commons" class="transition text-[15rem] absolute pointer-events-none right-6 top-1/2 -translate-y-1/2 text-black/5 dark:text-white/5"></Icon>
|
||||
</div>
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
<script lang="ts">
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { getDefaultHue, getHue, setHue } from "@utils/setting-utils";
|
||||
|
||||
let hue = getHue();
|
||||
const defaultHue = getDefaultHue();
|
||||
|
||||
function resetHue() {
|
||||
hue = getDefaultHue();
|
||||
}
|
||||
|
||||
$: if (hue || hue === 0) {
|
||||
setHue(hue);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="display-setting" class="float-panel float-panel-closed absolute transition-all w-80 right-4 px-4 py-4">
|
||||
<div class="flex flex-row gap-2 mb-3 items-center justify-between">
|
||||
<div class="flex gap-2 font-bold text-lg text-neutral-900 dark:text-neutral-100 transition relative ml-3
|
||||
before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)]
|
||||
before:absolute before:-left-3 before:top-[0.33rem]"
|
||||
>
|
||||
{i18n(I18nKey.themeColor)}
|
||||
<button aria-label="Reset to Default" class="btn-regular w-7 h-7 rounded-md active:scale-90"
|
||||
class:opacity-0={hue === defaultHue} class:pointer-events-none={hue === defaultHue} on:click={resetHue}>
|
||||
<div class="text-[var(--btn-content)]">
|
||||
<Icon icon="fa6-solid:arrow-rotate-left" class="text-[0.875rem]"></Icon>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<div id="hueValue" class="transition bg-[var(--btn-regular-bg)] w-10 h-7 rounded-md flex justify-center
|
||||
font-bold text-sm items-center text-[var(--btn-content)]">
|
||||
{hue}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-6 px-1 bg-[oklch(0.80_0.10_0)] dark:bg-[oklch(0.70_0.10_0)] rounded select-none">
|
||||
<input aria-label={i18n(I18nKey.themeColor)} type="range" min="0" max="360" bind:value={hue}
|
||||
class="slider" id="colorSlider" step="5" style="width: 100%">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<style lang="stylus">
|
||||
#display-setting
|
||||
input[type="range"]
|
||||
-webkit-appearance none
|
||||
height 1.5rem
|
||||
background-image var(--color-selection-bar)
|
||||
transition background-image 0.15s ease-in-out
|
||||
|
||||
/* Input Thumb */
|
||||
&::-webkit-slider-thumb
|
||||
-webkit-appearance none
|
||||
height 1rem
|
||||
width 0.5rem
|
||||
border-radius 0.125rem
|
||||
background rgba(255, 255, 255, 0.7)
|
||||
box-shadow none
|
||||
&:hover
|
||||
background rgba(255, 255, 255, 0.8)
|
||||
&:active
|
||||
background rgba(255, 255, 255, 0.6)
|
||||
|
||||
&::-moz-range-thumb
|
||||
-webkit-appearance none
|
||||
height 1rem
|
||||
width 0.5rem
|
||||
border-radius 0.125rem
|
||||
border-width 0
|
||||
background rgba(255, 255, 255, 0.7)
|
||||
box-shadow none
|
||||
&:hover
|
||||
background rgba(255, 255, 255, 0.8)
|
||||
&:active
|
||||
background rgba(255, 255, 255, 0.6)
|
||||
|
||||
&::-ms-thumb
|
||||
-webkit-appearance none
|
||||
height 1rem
|
||||
width 0.5rem
|
||||
border-radius 0.125rem
|
||||
background rgba(255, 255, 255, 0.7)
|
||||
box-shadow none
|
||||
&:hover
|
||||
background rgba(255, 255, 255, 0.8)
|
||||
&:active
|
||||
background rgba(255, 255, 255, 0.6)
|
||||
|
||||
</style>
|
||||
@@ -19,11 +19,11 @@ const links = Astro.props.links;
|
||||
<div class="transition text-black/75 dark:text-white/75 font-bold group-hover:text-[var(--primary)] group-active:text-[var(--primary)]">
|
||||
{link.name}
|
||||
</div>
|
||||
{!link.external && <Icon name="material-symbols:chevron-right-rounded"
|
||||
{!link.external && <Icon is:inline name="material-symbols:chevron-right-rounded"
|
||||
class="transition text-[1.25rem] text-[var(--primary)]"
|
||||
>
|
||||
</Icon>}
|
||||
{link.external && <Icon name="fa6-solid:arrow-up-right-from-square"
|
||||
{link.external && <Icon is:inline name="fa6-solid:arrow-up-right-from-square"
|
||||
class="transition text-[0.75rem] text-black/25 dark:text-white/25 -translate-x-1"
|
||||
>
|
||||
</Icon>}
|
||||
|
||||
@@ -19,7 +19,7 @@ const config = profileConfig;
|
||||
max-w-[12rem] lg:max-w-none overflow-hidden rounded-xl active:scale-95">
|
||||
<div class="absolute transition pointer-events-none group-hover:bg-black/30 group-active:bg-black/50
|
||||
w-full h-full z-50 flex items-center justify-center">
|
||||
<Icon name="fa6-regular:address-card"
|
||||
<Icon is:inline name="fa6-regular:address-card"
|
||||
class="transition opacity-0 scale-90 group-hover:scale-100 group-hover:opacity-100 text-white text-5xl">
|
||||
</Icon>
|
||||
</div>
|
||||
@@ -36,12 +36,12 @@ const config = profileConfig;
|
||||
<div class="flex gap-2 justify-center mb-1">
|
||||
{config.links.length > 1 && config.links.map(item =>
|
||||
<a rel="me" aria-label={item.name} href={item.url} target="_blank" class="btn-regular rounded-lg h-10 w-10 active:scale-90">
|
||||
<Icon name={item.icon} class="text-[1.5rem]"></Icon>
|
||||
<Icon is:inline name={item.icon} class="text-[1.5rem]"></Icon>
|
||||
</a>
|
||||
)}
|
||||
{config.links.length == 1 && <a rel="me" aria-label={config.links[0].name} href={config.links[0].url} target="_blank"
|
||||
class="btn-regular rounded-lg h-10 gap-2 px-3 font-bold active:scale-95">
|
||||
<Icon name={config.links[0].icon} class="text-[1.5rem]"></Icon>
|
||||
<Icon is:inline name={config.links[0].icon} class="text-[1.5rem]"></Icon>
|
||||
{config.links[0].name}
|
||||
</a>}
|
||||
</div>
|
||||
@@ -51,7 +51,7 @@ const config = profileConfig;
|
||||
<!-- 全站访问量统计 https://github.com/afoim/fuwari/blob/81f22decb17ff7ee1dd480c10773f7ba8f4df296/src/components/widget/Profile.astro -->
|
||||
<div class="text-center text-sm text-neutral-500 dark:text-neutral-400 mt-3 pt-3 border-t border-neutral-200 dark:border-neutral-700">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<Icon name="material-symbols:visibility-outline" class="text-base"></Icon>
|
||||
<Icon is:inline name="material-symbols:visibility-outline" class="text-base"></Icon>
|
||||
<span id="site-stats">加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,7 +59,7 @@ const config = profileConfig;
|
||||
<!-- 最新 Commit 提交信息 -->
|
||||
<div class="text-center text-sm text-neutral-500 dark:text-neutral-400 mt-3 pt-3 border-t border-neutral-200 dark:border-neutral-700">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<Icon name="ic:baseline-commit" class="text-base"></Icon>
|
||||
<Icon is:inline name="ic:baseline-commit" class="text-base"></Icon>
|
||||
<span><a id="github-commit-link" href="#">
|
||||
<span id="github-commit">加载中...</span></a></span>
|
||||
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
<script lang="ts">
|
||||
import { DARK_MODE, LIGHT_MODE } from "@constants/constants.ts";
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import Icon from "@iconify/svelte";
|
||||
import {
|
||||
applyCustomThemeStyles,
|
||||
applyThemeToDocument,
|
||||
getCodeFontFamily,
|
||||
getCustomCss,
|
||||
getDefaultHue,
|
||||
getDisplayFontFamily,
|
||||
getHue,
|
||||
getStoredTheme,
|
||||
setCodeFontFamily,
|
||||
setCustomCss,
|
||||
setDisplayFontFamily,
|
||||
setHue,
|
||||
setTheme,
|
||||
} from "@utils/setting-utils.ts";
|
||||
import { onMount } from "svelte";
|
||||
import type { LIGHT_DARK_MODE } from "@/types/config.ts";
|
||||
|
||||
export let showThemeColor = true;
|
||||
|
||||
let hue = getHue();
|
||||
const defaultHue = getDefaultHue();
|
||||
let mode: LIGHT_DARK_MODE = LIGHT_MODE;
|
||||
let customCss = "";
|
||||
let displayFontFamily = "";
|
||||
let codeFontFamily = "";
|
||||
let customThemeReady = false;
|
||||
|
||||
onMount(() => {
|
||||
mode = getStoredTheme();
|
||||
customCss = getCustomCss();
|
||||
displayFontFamily = getDisplayFontFamily();
|
||||
codeFontFamily = getCodeFontFamily();
|
||||
applyThemeToDocument(mode);
|
||||
applyCustomThemeStyles();
|
||||
customThemeReady = true;
|
||||
});
|
||||
|
||||
function resetHue() {
|
||||
hue = getDefaultHue();
|
||||
}
|
||||
|
||||
function switchScheme(newMode: LIGHT_DARK_MODE) {
|
||||
mode = newMode;
|
||||
setTheme(newMode);
|
||||
}
|
||||
|
||||
function togglePanel() {
|
||||
document
|
||||
.getElementById("display-setting")
|
||||
?.classList.toggle("float-panel-closed");
|
||||
}
|
||||
|
||||
function resetDisplayFontFamily() {
|
||||
displayFontFamily = "";
|
||||
}
|
||||
|
||||
function resetCodeFontFamily() {
|
||||
codeFontFamily = "";
|
||||
}
|
||||
|
||||
function resetCustomCss() {
|
||||
customCss = "";
|
||||
}
|
||||
|
||||
$: if (hue || hue === 0) {
|
||||
setHue(hue);
|
||||
}
|
||||
|
||||
$: if (customThemeReady) {
|
||||
setCustomCss(customCss);
|
||||
}
|
||||
|
||||
$: if (customThemeReady) {
|
||||
setDisplayFontFamily(displayFontFamily);
|
||||
}
|
||||
|
||||
$: if (customThemeReady) {
|
||||
setCodeFontFamily(codeFontFamily);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<button
|
||||
aria-label="主题设置"
|
||||
class="relative btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90"
|
||||
id="display-settings-switch"
|
||||
onclick={togglePanel}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="h-[1.35rem] w-[1.35rem]"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M4 7h5" class="stroke-current" stroke-width="1.8" stroke-linecap="round" />
|
||||
<path d="M15 7h5" class="stroke-current" stroke-width="1.8" stroke-linecap="round" />
|
||||
<circle cx="12" cy="7" r="2.4" class="stroke-current" stroke-width="1.8" />
|
||||
<path d="M4 17h5" class="stroke-current" stroke-width="1.8" stroke-linecap="round" />
|
||||
<path d="M15 17h5" class="stroke-current" stroke-width="1.8" stroke-linecap="round" />
|
||||
<circle cx="12" cy="17" r="2.4" class="stroke-current" stroke-width="1.8" />
|
||||
<path d="M4 12h9" class="stroke-current opacity-70" stroke-width="1.8" stroke-linecap="round" />
|
||||
<path d="M17 12h3" class="stroke-current opacity-70" stroke-width="1.8" stroke-linecap="round" />
|
||||
<circle cx="15" cy="12" r="1.8" class="fill-current" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div id="display-setting" class="float-panel float-panel-closed absolute transition-all z-50 w-[24rem] max-h-[calc(100vh_-_6rem)] overflow-y-auto right-4 px-4 py-4">
|
||||
<div class="grid grid-cols-2 gap-2 mb-4">
|
||||
<button
|
||||
class="flex transition whitespace-nowrap items-center !justify-start w-full btn-plain scale-animation rounded-lg h-9 px-3 font-medium active:scale-95"
|
||||
class:current-theme-btn={mode === LIGHT_MODE}
|
||||
onclick={() => switchScheme(LIGHT_MODE)}
|
||||
>
|
||||
<Icon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem] mr-2"></Icon>
|
||||
{i18n(I18nKey.lightMode)}
|
||||
</button>
|
||||
<button
|
||||
class="flex transition whitespace-nowrap items-center !justify-start w-full btn-plain scale-animation rounded-lg h-9 px-3 font-medium active:scale-95"
|
||||
class:current-theme-btn={mode === DARK_MODE}
|
||||
onclick={() => switchScheme(DARK_MODE)}
|
||||
>
|
||||
<Icon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem] mr-2"></Icon>
|
||||
{i18n(I18nKey.darkMode)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if showThemeColor}
|
||||
<div class="flex flex-row gap-2 mb-3 items-center justify-between">
|
||||
<div
|
||||
class="select-none flex gap-2 font-bold text-lg text-neutral-900 dark:text-neutral-100 transition relative ml-3 before:w-1 before:h-4 before:rounded-md before:bg-[var(--primary)] before:absolute before:-left-3 before:top-[0.33rem]"
|
||||
>
|
||||
{i18n(I18nKey.themeColor)}
|
||||
<button
|
||||
aria-label="Reset to Default"
|
||||
class="btn-regular w-7 h-7 rounded-md active:scale-90"
|
||||
class:opacity-0={hue === defaultHue}
|
||||
class:pointer-events-none={hue === defaultHue}
|
||||
onclick={resetHue}
|
||||
>
|
||||
<div class="text-[var(--btn-content)]">
|
||||
<Icon icon="fa6-solid:arrow-rotate-left" class="text-[0.875rem]"></Icon>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<div
|
||||
id="hueValue"
|
||||
class="transition bg-[var(--btn-regular-bg)] w-10 h-7 rounded-md flex justify-center font-bold text-sm items-center text-[var(--btn-content)]"
|
||||
>
|
||||
{hue}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full h-6 px-1 bg-[oklch(0.80_0.10_0)] dark:bg-[oklch(0.70_0.10_0)] rounded select-none">
|
||||
<input
|
||||
aria-label={i18n(I18nKey.themeColor)}
|
||||
type="range"
|
||||
min="0"
|
||||
max="360"
|
||||
bind:value={hue}
|
||||
class="slider"
|
||||
id="colorSlider"
|
||||
step="5"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-5 border-t border-black/10 pt-4 dark:border-white/10">
|
||||
<div
|
||||
class="pointer-events-none relative mb-3 ml-3 select-none text-lg font-bold text-neutral-900 transition before:absolute before:-left-3 before:top-[0.33rem] before:h-4 before:w-1 before:rounded-md before:bg-[var(--primary)] dark:text-neutral-100"
|
||||
>
|
||||
字体系列
|
||||
</div>
|
||||
<label class="mb-3 block">
|
||||
<span class="mb-1 block text-sm font-medium text-black/60 transition dark:text-white/60">显示字体</span>
|
||||
<div class="flex w-full gap-2">
|
||||
<input
|
||||
aria-label="显示字体"
|
||||
class="theme-setting-input min-w-0 flex-1"
|
||||
bind:value={displayFontFamily}
|
||||
placeholder='"MiSans VF", Inter, sans-serif'
|
||||
/>
|
||||
<button
|
||||
aria-label="重置显示字体"
|
||||
class="btn-regular h-9 w-9 shrink-0 rounded-md active:scale-90"
|
||||
class:opacity-0={!displayFontFamily}
|
||||
class:pointer-events-none={!displayFontFamily}
|
||||
onclick={resetDisplayFontFamily}
|
||||
>
|
||||
<Icon icon="fa6-solid:arrow-rotate-left" class="text-[0.875rem]"></Icon>
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="mb-1 block text-sm font-medium text-black/60 transition dark:text-white/60">代码字体</span>
|
||||
<div class="flex w-full gap-2">
|
||||
<input
|
||||
aria-label="代码字体"
|
||||
class="theme-setting-input min-w-0 flex-1"
|
||||
bind:value={codeFontFamily}
|
||||
placeholder='"Cascadia Mono", "JetBrains Mono", monospace'
|
||||
/>
|
||||
<button
|
||||
aria-label="重置代码字体"
|
||||
class="btn-regular h-9 w-9 shrink-0 rounded-md active:scale-90"
|
||||
class:opacity-0={!codeFontFamily}
|
||||
class:pointer-events-none={!codeFontFamily}
|
||||
onclick={resetCodeFontFamily}
|
||||
>
|
||||
<Icon icon="fa6-solid:arrow-rotate-left" class="text-[0.875rem]"></Icon>
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 border-t border-black/10 pt-4 dark:border-white/10">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<div
|
||||
class="pointer-events-none relative ml-3 select-none text-lg font-bold text-neutral-900 transition before:absolute before:-left-3 before:top-[0.33rem] before:h-4 before:w-1 before:rounded-md before:bg-[var(--primary)] dark:text-neutral-100"
|
||||
>
|
||||
自定义 CSS
|
||||
</div>
|
||||
<button
|
||||
aria-label="重置自定义 CSS"
|
||||
class="btn-regular h-7 w-7 rounded-md active:scale-90"
|
||||
class:opacity-0={!customCss}
|
||||
class:pointer-events-none={!customCss}
|
||||
onclick={resetCustomCss}
|
||||
>
|
||||
<Icon icon="fa6-solid:arrow-rotate-left" class="text-[0.875rem]"></Icon>
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
aria-label="自定义 CSS"
|
||||
class="theme-setting-textarea"
|
||||
bind:value={customCss}
|
||||
spellcheck="false"
|
||||
placeholder={":root { --radius-large: 1.5rem; }"}
|
||||
></textarea>
|
||||
<div class="mt-2 text-xs text-black/40 transition dark:text-white/40">
|
||||
字体系列 会在 自定义 CSS 之后注入,所以字体设置优先级更高。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.theme-setting-input,
|
||||
.theme-setting-textarea {
|
||||
width: 100%;
|
||||
border-radius: 0.5rem;
|
||||
background: var(--btn-regular-bg);
|
||||
color: var(--btn-content);
|
||||
outline: none;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.theme-setting-input {
|
||||
height: 2.25rem;
|
||||
padding: 0 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.theme-setting-textarea {
|
||||
min-height: 8rem;
|
||||
resize: vertical;
|
||||
padding: 0.75rem;
|
||||
font-family: "Cascadia Mono", "JetBrainsMono-VF", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.theme-setting-input:hover,
|
||||
.theme-setting-textarea:hover {
|
||||
background: var(--btn-regular-bg-hover);
|
||||
}
|
||||
|
||||
.theme-setting-input:focus,
|
||||
.theme-setting-textarea:focus {
|
||||
background: var(--btn-regular-bg-active);
|
||||
}
|
||||
|
||||
#display-setting input[type="range"] {
|
||||
-webkit-appearance: none;
|
||||
height: 1.5rem;
|
||||
background-image: var(--color-selection-bar);
|
||||
transition: background-image 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
#display-setting input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
height: 1rem;
|
||||
width: 0.5rem;
|
||||
border-radius: 0.125rem;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#display-setting input[type="range"]::-webkit-slider-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
#display-setting input[type="range"]::-webkit-slider-thumb:active {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
#display-setting input[type="range"]::-moz-range-thumb {
|
||||
-webkit-appearance: none;
|
||||
height: 1rem;
|
||||
width: 0.5rem;
|
||||
border-radius: 0.125rem;
|
||||
border-width: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#display-setting input[type="range"]::-moz-range-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
#display-setting input[type="range"]::-moz-range-thumb:active {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
#display-setting input[type="range"]::-ms-thumb {
|
||||
-webkit-appearance: none;
|
||||
height: 1rem;
|
||||
width: 0.5rem;
|
||||
border-radius: 0.125rem;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#display-setting input[type="range"]::-ms-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
#display-setting input[type="range"]::-ms-thumb:active {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +1,15 @@
|
||||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import { getTotalWords } from "@utils/content-utils";
|
||||
import { Icon } from "astro-icon/components";
|
||||
|
||||
const totalWords = await getTotalWords();
|
||||
const formattedTotalWords = totalWords.toLocaleString("zh-CN");
|
||||
---
|
||||
<div class="text-center text-sm text-neutral-500 dark:text-neutral-400 mt-3 pt-3 border-t border-neutral-200 dark:border-neutral-700">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<Icon name="material-symbols:article-outline" class="text-base"></Icon>
|
||||
<Icon is:inline name="material-symbols:article-outline" class="text-base"></Icon>
|
||||
<span>总计 {formattedTotalWords} {i18n(I18nKey.wordsCount)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ const className = Astro.props.class;
|
||||
{isCollapsed && <div class="expand-btn px-4 -mb-2">
|
||||
<button class="btn-plain rounded-lg w-full h-9">
|
||||
<div class="text-[var(--primary)] flex items-center justify-center gap-2 -translate-x-2">
|
||||
<Icon name="material-symbols:more-horiz" class="text-[1.75rem]"></Icon> {i18n(I18nKey.more)}
|
||||
<Icon is:inline name="material-symbols:more-horiz" class="text-[1.75rem]"></Icon> {i18n(I18nKey.more)}
|
||||
</div>
|
||||
</button>
|
||||
</div>}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { LinkPreset } from "./types/config";
|
||||
export const siteConfig: SiteConfig = {
|
||||
title: "Ad_closeNN 的小站",
|
||||
subtitle: "Ad_closeNN の 小站,时不时会刷新一些野生东西 | ✨ 欢迎友链 ✨",
|
||||
githubRepo: "https://github.com/Ad-closeNN/blog-fuwari",
|
||||
lang: "zh_CN", // Language code, e.g. 'en', 'zh-CN', 'ja', etc.
|
||||
themeColor: {
|
||||
hue: 160, // Default hue for the theme color, from 0 to 360. e.g. red: 0, teal: 200, cyan: 250, pink: 345
|
||||
|
||||
@@ -276,7 +276,7 @@ const bannerOffset =
|
||||
<ConfigCarrier></ConfigCarrier>
|
||||
<div id="dns-warning-banner" class="hidden sticky top-0 z-[100] w-full bg-amber-50/90 dark:bg-amber-950/80 backdrop-blur-md border-b border-amber-200/60 dark:border-amber-700/40 text-amber-900 dark:text-amber-100 px-4 py-3 text-sm text-center">
|
||||
<div class="select-none flex items-center justify-center gap-2 max-w-[var(--page-width)] mx-auto">
|
||||
<Icon name="material-symbols:info-outline-rounded" class="pointer-events-none shrink-0 text-lg" aria-hidden="true"></Icon>
|
||||
<Icon is:inline name="material-symbols:info-outline-rounded" class="pointer-events-none shrink-0 text-lg" aria-hidden="true"></Icon>
|
||||
<span>本站近期遭到反诈部门 DNS 污染,可能无法正常访问。如有需要可开启代理或使用国外 DNS 服务器。</span>
|
||||
<button id="dns-warning-close" class="select-auto shrink-0 btn-plain scale-animation rounded-lg w-7 h-7 flex items-center justify-center text-amber-600 dark:text-amber-400 hover:text-amber-800 dark:hover:text-amber-200 active:scale-90" aria-label="Close notice">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
|
||||
@@ -347,7 +347,7 @@ import {
|
||||
// SizeObserverPlugin,
|
||||
// ClickScrollPlugin
|
||||
} from 'overlayscrollbars';
|
||||
import {getHue, getStoredTheme, setHue, setTheme} from "../utils/setting-utils";
|
||||
import {applyCustomThemeStyles, getHue, getStoredTheme, setHue, setTheme} from "../utils/setting-utils";
|
||||
import {pathsEqual, url} from "../utils/url-utils";
|
||||
import {
|
||||
BANNER_HEIGHT,
|
||||
@@ -519,6 +519,7 @@ function init() {
|
||||
// disableAnimation()() // TODO
|
||||
loadTheme();
|
||||
loadHue();
|
||||
applyCustomThemeStyles();
|
||||
initCustomScrollbar();
|
||||
showBanner();
|
||||
}
|
||||
|
||||
@@ -71,9 +71,9 @@ const mainPanelTop = siteConfig.banner.enable
|
||||
class:list={["group onload-animation transition-all absolute flex justify-center items-center rounded-full " +
|
||||
"px-3 right-4 -top-[3.25rem] bg-black/60 hover:bg-black/70 h-9", {"hover:pr-9 active:bg-black/80": hasBannerLink}]}
|
||||
>
|
||||
<Icon class="text-white/75 text-[1.25rem] mr-1" name="material-symbols:copyright-outline-rounded" ></Icon>
|
||||
<Icon is:inline class="text-white/75 text-[1.25rem] mr-1" name="material-symbols:copyright-outline-rounded" ></Icon>
|
||||
<div class="text-white/75 text-xs">{siteConfig.banner.credit.text}</div>
|
||||
<Icon class:list={["transition absolute text-[oklch(0.75_0.14_var(--hue))] right-4 text-[0.75rem] opacity-0",
|
||||
<Icon is:inline class:list={["transition absolute text-[oklch(0.75_0.14_var(--hue))] right-4 text-[0.75rem] opacity-0",
|
||||
{"group-hover:opacity-100": hasBannerLink}]}
|
||||
name="fa6-solid:arrow-up-right-from-square">
|
||||
</Icon>
|
||||
|
||||
+12
-8
@@ -5,16 +5,20 @@ import MarkdownIt from "markdown-it";
|
||||
|
||||
const markdown = new MarkdownIt();
|
||||
const siteNameHtml = markdown.renderInline("`Ad_closeNN 的小站`");
|
||||
const siteDescriptionHtml = markdown.renderInline("`永远相信美好的事情即将发生`");
|
||||
const siteDescriptionHtml = markdown.renderInline(
|
||||
"`永远相信美好的事情即将发生`",
|
||||
);
|
||||
const siteUrlHtml = markdown.renderInline("`https://blog.adclosenn.top`");
|
||||
const siteAvatarHtml = markdown.renderInline("`https://static.adclosenn.top/icon/avatar.jpg`");
|
||||
const siteAvatarHtml = markdown.renderInline(
|
||||
"`https://static.adclosenn.top/icon/avatar.jpg`",
|
||||
);
|
||||
---
|
||||
|
||||
<MainGridLayout title="友情链接">
|
||||
<div class="card-base p-6 md:p-8">
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
<div class="h-8 w-8 rounded-lg bg-[var(--primary)] flex items-center justify-center text-white dark:text-black/70">
|
||||
<Icon name="material-symbols:diversity-3" class="text-[1.5rem]">
|
||||
<Icon is:inline name="material-symbols:diversity-3" class="text-[1.5rem]" />
|
||||
</div>
|
||||
<h1 class="text-2xl font-bold text-black dark:text-white">友情链接</h1>
|
||||
</div>
|
||||
@@ -245,11 +249,11 @@ const siteAvatarHtml = markdown.renderInline("`https://static.adclosenn.top/icon
|
||||
|
||||
<!-- 申请友链 -->
|
||||
<div class="sponsors-section mt-8">
|
||||
<h2 class="text-xl font-bold text-black dark:text-white mb-4 flex items-center gap-2">
|
||||
<Icon name="material-symbols:group" class="text-[1.5rem] text-[var(--primary)]"/>
|
||||
将您的网站加入本站友链板块
|
||||
</h2>
|
||||
<p class="text-sm text-black/60 dark:text-white/60">
|
||||
<h2 class="text-xl font-bold text-black dark:text-white mb-4 flex items-center gap-2">
|
||||
<Icon is:inline name="material-symbols:group" class="text-[1.5rem] text-[var(--primary)]" />
|
||||
将您的网站加入本站友链板块
|
||||
</h2>
|
||||
<p class="text-sm text-black/60 dark:text-white/60">
|
||||
请自行提交 GitHub Issue : <a target="_blank" href="https://github.com/Ad-closeNN/form/issues/new?template=friends-link.yml" class="transition link text-[var(--primary)] font-medium">点击这里提交</a>
|
||||
</p>
|
||||
<div class="mt-4 h-px bg-black/10 dark:bg-white/10"></div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
import { type CollectionEntry, render } from "astro:content";
|
||||
import path from "node:path";
|
||||
import { render, type CollectionEntry } from "astro:content";
|
||||
import License from "@components/misc/License.astro";
|
||||
import Markdown from "@components/misc/Markdown.astro";
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
@@ -40,9 +40,35 @@ const postSources = import.meta.glob("../../content/posts/**/*.{md,mdx}", {
|
||||
eager: true,
|
||||
}) as Record<string, string>;
|
||||
const normalizedPostSources = Object.fromEntries(
|
||||
Object.entries(postSources).map(([key, source]) => [key.toLowerCase(), source]),
|
||||
Object.entries(postSources).map(([key, source]) => [
|
||||
key.toLowerCase(),
|
||||
source,
|
||||
]),
|
||||
);
|
||||
|
||||
function getPostSourcePath(entryId: string) {
|
||||
const normalizedEntryPath = `../../content/posts/${entryId}`.toLowerCase();
|
||||
const sourceKey = Object.keys(postSources).find((key) => {
|
||||
const normalizedKey = key.toLowerCase();
|
||||
return (
|
||||
normalizedKey === normalizedEntryPath ||
|
||||
normalizedKey === `${normalizedEntryPath}.md` ||
|
||||
normalizedKey === `${normalizedEntryPath}.mdx`
|
||||
);
|
||||
});
|
||||
|
||||
return sourceKey?.replace("../../content/posts/", "src/content/posts/");
|
||||
}
|
||||
|
||||
function getGitHubPostSourceUrl(repo: string | undefined, sourcePath: string | undefined) {
|
||||
if (!repo || !sourcePath) return "";
|
||||
|
||||
const repoUrl = repo.includes("://") ? repo : `https://github.com/${repo}`;
|
||||
const normalizedRepo = repoUrl.replace(/\.git$/, "").replace(/\/+$/, "");
|
||||
const encodedSourcePath = sourcePath.split("/").map(encodeURIComponent).join("/");
|
||||
return `${normalizedRepo}/blob/main/${encodedSourcePath}?plain=1`;
|
||||
}
|
||||
|
||||
function parseAiSummaryValue(value: string) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return "";
|
||||
@@ -65,7 +91,9 @@ function extractFrontmatterValue(frontmatter: string, key: string) {
|
||||
return parseAiSummaryValue(inlineValue);
|
||||
}
|
||||
|
||||
const afterValue = frontmatter.slice(valueMatch.index! + valueMatch[0].length);
|
||||
const afterValue = frontmatter.slice(
|
||||
valueMatch.index! + valueMatch[0].length,
|
||||
);
|
||||
const lines = afterValue.split(/\r?\n/);
|
||||
const blockLines = [];
|
||||
for (const line of lines) {
|
||||
@@ -82,12 +110,20 @@ function getAiSummaryMeta(entryId: string) {
|
||||
normalizedPostSources[`../../content/posts/${entryId}.mdx`.toLowerCase()];
|
||||
const frontmatter = source?.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1];
|
||||
return {
|
||||
summary: frontmatter ? extractFrontmatterValue(frontmatter, "aiSummary") : "",
|
||||
model: frontmatter ? extractFrontmatterValue(frontmatter, "aiSummaryModel") : "",
|
||||
summary: frontmatter
|
||||
? extractFrontmatterValue(frontmatter, "aiSummary")
|
||||
: "",
|
||||
model: frontmatter
|
||||
? extractFrontmatterValue(frontmatter, "aiSummaryModel")
|
||||
: "",
|
||||
};
|
||||
}
|
||||
|
||||
const aiSummaryMeta = getAiSummaryMeta(entry.id);
|
||||
const postSourceUrl = getGitHubPostSourceUrl(
|
||||
siteConfig.githubRepo,
|
||||
getPostSourcePath(entry.id),
|
||||
);
|
||||
|
||||
const jsonLd = {
|
||||
"@context": "https://schema.org",
|
||||
@@ -113,7 +149,6 @@ const showcover = entry.data.showcover;
|
||||
// 获取自定义的头图(Markdown 内部)
|
||||
const customcover = entry.data.customcover;
|
||||
const isOutdated = entry.data.outdated;
|
||||
|
||||
---
|
||||
<MainGridLayout banner={entry.data.image} title={entry.data.title} description={entry.data.description} lang={entry.data.lang} setOGTypeArticle={true} headings={headings}>
|
||||
<script is:inline slot="head" type="application/ld+json" set:html={JSON.stringify(jsonLd)}></script>
|
||||
@@ -126,13 +161,13 @@ const isOutdated = entry.data.outdated;
|
||||
<div class="flex flex-row flex-wrap text-black/30 dark:text-white/30 gap-5 transition">
|
||||
<div class="flex flex-row items-center pointer-events-none select-none cursor-default">
|
||||
<div class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2">
|
||||
<Icon name="material-symbols:notes-rounded"></Icon>
|
||||
<Icon is:inline name="material-symbols:notes-rounded"></Icon>
|
||||
</div>
|
||||
<div class="text-sm">{remarkPluginFrontmatter.words} {" " + i18n(I18nKey.wordsCount)}</div>
|
||||
</div>
|
||||
<div class="flex flex-row items-center pointer-events-none select-none cursor-default">
|
||||
<div class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2">
|
||||
<Icon name="material-symbols:schedule-outline-rounded"></Icon>
|
||||
<Icon is:inline name="material-symbols:schedule-outline-rounded"></Icon>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
大约 {remarkPluginFrontmatter.minutes} {" " + i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : I18nKey.minutesCount)}
|
||||
@@ -140,12 +175,25 @@ const isOutdated = entry.data.outdated;
|
||||
</div>
|
||||
<div class="flex flex-row items-center pointer-events-none select-none cursor-default">
|
||||
<div class="transition h-6 w-6 rounded-md bg-black/5 dark:bg-white/10 text-black/50 dark:text-white/50 flex items-center justify-center mr-2">
|
||||
<Icon name="material-symbols:visibility-outline-rounded"></Icon>
|
||||
<Icon is:inline name="material-symbols:visibility-outline-rounded"></Icon>
|
||||
</div>
|
||||
<div class="text-sm" id="post-top-page-views">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-2 text-black/40 dark:text-white/40 md:shrink-0">
|
||||
{postSourceUrl && (
|
||||
<a
|
||||
href={postSourceUrl}
|
||||
aria-label="查看文章源代码"
|
||||
title="查看文章源代码"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn-card flex h-9 items-center gap-2 rounded-xl px-3 font-medium active:scale-95"
|
||||
>
|
||||
<Icon is:inline name="fa6-brands:github" class="text-[1rem] text-[var(--primary)]"></Icon>
|
||||
<span class="hidden text-sm text-black/75 dark:text-white/75 sm:inline">查看源代码</span>
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
href={entry.data.nextSlug ? getPostUrlBySlug(entry.data.nextSlug) : "#"}
|
||||
aria-label={entry.data.nextTitle ? `上一篇:${entry.data.nextTitle}` : "没有上一篇"}
|
||||
@@ -155,7 +203,7 @@ const isOutdated = entry.data.outdated;
|
||||
{"pointer-events-none opacity-40": !entry.data.nextSlug},
|
||||
]}
|
||||
>
|
||||
<Icon name="material-symbols:chevron-left-rounded" class="text-[1.5rem] text-[var(--primary)]"></Icon>
|
||||
<Icon is:inline name="material-symbols:chevron-left-rounded" class="text-[1.5rem] text-[var(--primary)]"></Icon>
|
||||
</a>
|
||||
<a
|
||||
href={entry.data.prevSlug ? getPostUrlBySlug(entry.data.prevSlug) : "#"}
|
||||
@@ -166,7 +214,7 @@ const isOutdated = entry.data.outdated;
|
||||
{"pointer-events-none opacity-40": !entry.data.prevSlug},
|
||||
]}
|
||||
>
|
||||
<Icon name="material-symbols:chevron-right-rounded" class="text-[1.5rem] text-[var(--primary)]"></Icon>
|
||||
<Icon is:inline name="material-symbols:chevron-right-rounded" class="text-[1.5rem] text-[var(--primary)]"></Icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,6 +252,7 @@ const isOutdated = entry.data.outdated;
|
||||
]}>
|
||||
<div class="flex items-start gap-2">
|
||||
<Icon
|
||||
is:inline
|
||||
name={isOutdated ? "material-symbols:warning-outline-rounded" : "material-symbols:info-outline-rounded"}
|
||||
class="mt-0.5 shrink-0 text-lg"
|
||||
></Icon>
|
||||
@@ -236,7 +285,7 @@ const isOutdated = entry.data.outdated;
|
||||
<div class="mb-4 rounded-xl p-4 bg-gradient-to-r from-violet-500/10 to-indigo-500/10 dark:from-violet-500/15 dark:to-indigo-500/15 border border-violet-500/20 onload-animation">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="shrink-0 w-8 h-8 rounded-lg bg-violet-500/20 dark:bg-violet-500/30 flex items-center justify-center">
|
||||
<Icon name="material-symbols:info-outline-rounded" class="text-violet-600 dark:text-violet-400 text-lg"></Icon>
|
||||
<Icon is:inline name="material-symbols:info-outline-rounded" class="text-violet-600 dark:text-violet-400 text-lg"></Icon>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-violet-700 dark:text-violet-300 mb-1 select-none pointer-events-none">
|
||||
@@ -279,7 +328,7 @@ const isOutdated = entry.data.outdated;
|
||||
<a href={entry.data.nextSlug ? getPostUrlBySlug(entry.data.nextSlug) : "#"}
|
||||
class:list={["w-full font-bold overflow-hidden active:scale-95", {"pointer-events-none": !entry.data.nextSlug}]}>
|
||||
{entry.data.nextSlug && <div class="btn-card rounded-2xl w-full h-[3.75rem] max-w-full px-4 flex items-center !justify-start gap-4" >
|
||||
<Icon name="material-symbols:chevron-left-rounded" class="text-[2rem] text-[var(--primary)]" />
|
||||
<Icon is:inline name="material-symbols:chevron-left-rounded" class="text-[2rem] text-[var(--primary)]" />
|
||||
<div class="overflow-hidden transition overflow-ellipsis whitespace-nowrap max-w-[calc(100%_-_3rem)] text-black/75 dark:text-white/75">
|
||||
{entry.data.nextTitle}
|
||||
</div>
|
||||
@@ -292,7 +341,7 @@ const isOutdated = entry.data.outdated;
|
||||
<div class="overflow-hidden transition overflow-ellipsis whitespace-nowrap max-w-[calc(100%_-_3rem)] text-black/75 dark:text-white/75">
|
||||
{entry.data.prevTitle}
|
||||
</div>
|
||||
<Icon name="material-symbols:chevron-right-rounded" class="text-[2rem] text-[var(--primary)]" />
|
||||
<Icon is:inline name="material-symbols:chevron-right-rounded" class="text-[2rem] text-[var(--primary)]" />
|
||||
</div>}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { AUTO_MODE, DARK_MODE, LIGHT_MODE } from "@constants/constants";
|
||||
export type SiteConfig = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
githubRepo?: string;
|
||||
|
||||
lang:
|
||||
| "en"
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
AUTO_MODE,
|
||||
DARK_MODE,
|
||||
DEFAULT_THEME,
|
||||
LIGHT_MODE,
|
||||
} from "@constants/constants.ts";
|
||||
import { DARK_MODE, DEFAULT_THEME, LIGHT_MODE } from "@constants/constants.ts";
|
||||
import { expressiveCodeConfig } from "@/config";
|
||||
import type { LIGHT_DARK_MODE } from "@/types/config";
|
||||
|
||||
@@ -52,3 +47,76 @@ export function setTheme(theme: LIGHT_DARK_MODE): void {
|
||||
export function getStoredTheme(): LIGHT_DARK_MODE {
|
||||
return (localStorage.getItem("theme") as LIGHT_DARK_MODE) || DEFAULT_THEME;
|
||||
}
|
||||
|
||||
const CUSTOM_CSS_STORAGE_KEY = "custom-css";
|
||||
const DISPLAY_FONT_STORAGE_KEY = "display-font-family";
|
||||
const CODE_FONT_STORAGE_KEY = "code-font-family";
|
||||
const CUSTOM_CSS_STYLE_ID = "custom-theme-css";
|
||||
const CUSTOM_FONT_STYLE_ID = "custom-theme-font";
|
||||
|
||||
function getOrCreateStyleElement(id: string): HTMLStyleElement {
|
||||
let style = document.getElementById(id) as HTMLStyleElement | null;
|
||||
if (!style) {
|
||||
style = document.createElement("style");
|
||||
style.id = id;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
function normalizeFontFamily(fontFamily: string): string {
|
||||
return fontFamily.replace(/[{};]/g, "").trim();
|
||||
}
|
||||
|
||||
export function getCustomCss(): string {
|
||||
return localStorage.getItem(CUSTOM_CSS_STORAGE_KEY) || "";
|
||||
}
|
||||
|
||||
export function setCustomCss(css: string): void {
|
||||
localStorage.setItem(CUSTOM_CSS_STORAGE_KEY, css);
|
||||
applyCustomThemeStyles();
|
||||
}
|
||||
|
||||
export function getDisplayFontFamily(): string {
|
||||
return localStorage.getItem(DISPLAY_FONT_STORAGE_KEY) || "";
|
||||
}
|
||||
|
||||
export function setDisplayFontFamily(fontFamily: string): void {
|
||||
localStorage.setItem(
|
||||
DISPLAY_FONT_STORAGE_KEY,
|
||||
normalizeFontFamily(fontFamily),
|
||||
);
|
||||
applyCustomThemeStyles();
|
||||
}
|
||||
|
||||
export function getCodeFontFamily(): string {
|
||||
return localStorage.getItem(CODE_FONT_STORAGE_KEY) || "";
|
||||
}
|
||||
|
||||
export function setCodeFontFamily(fontFamily: string): void {
|
||||
localStorage.setItem(CODE_FONT_STORAGE_KEY, normalizeFontFamily(fontFamily));
|
||||
applyCustomThemeStyles();
|
||||
}
|
||||
|
||||
export function applyCustomThemeStyles(): void {
|
||||
getOrCreateStyleElement(CUSTOM_CSS_STYLE_ID).textContent = getCustomCss();
|
||||
|
||||
const displayFont = normalizeFontFamily(getDisplayFontFamily());
|
||||
const codeFont = normalizeFontFamily(getCodeFontFamily());
|
||||
const fontRules: string[] = [];
|
||||
|
||||
if (displayFont) {
|
||||
fontRules.push(
|
||||
`html, body, button, input, textarea, select { font-family: ${displayFont} !important; }`,
|
||||
);
|
||||
}
|
||||
|
||||
if (codeFont) {
|
||||
fontRules.push(
|
||||
`code, pre, kbd, samp, .expressive-code, .expressive-code * { font-family: ${codeFont} !important; }`,
|
||||
);
|
||||
}
|
||||
|
||||
getOrCreateStyleElement(CUSTOM_FONT_STYLE_ID).textContent =
|
||||
fontRules.join("\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user