Merge branch 'main' into tailwind-v4-poc-1

This commit is contained in:
L4Ph committed 2025-05-30 22:30:07 +09:00
commit d7714c1bd6
29 files changed
+1250 -975

No files matched your search

-118
View File
@@ -1,118 +0,0 @@
---
import { UNCATEGORIZED } from "@constants/constants";
import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation";
import { getSortedPosts } from "../utils/content-utils";
import { getPostUrlBySlug } from "../utils/url-utils";
interface Props {
keyword?: string;
tags?: string[];
categories?: string[];
}
const { tags, categories } = Astro.props;
let posts = await getSortedPosts();
if (Array.isArray(tags) && tags.length > 0) {
posts = posts.filter(
(post) =>
Array.isArray(post.data.tags) &&
post.data.tags.some((tag) => tags.includes(tag)),
);
}
if (Array.isArray(categories) && categories.length > 0) {
posts = posts.filter(
(post) =>
(post.data.category && categories.includes(post.data.category)) ||
(!post.data.category && categories.includes(UNCATEGORIZED)),
);
}
const groups: { year: number; posts: typeof posts }[] = (() => {
const groupedPosts = posts.reduce(
(grouped: { [year: number]: typeof posts }, post) => {
const year = post.data.published.getFullYear();
if (!grouped[year]) {
grouped[year] = [];
}
grouped[year].push(post);
return grouped;
},
{},
);
// convert the object to an array
const groupedPostsArray = Object.keys(groupedPosts).map((key) => ({
year: Number.parseInt(key),
posts: groupedPosts[Number.parseInt(key)],
}));
// sort years by latest first
groupedPostsArray.sort((a, b) => b.year - a.year);
return groupedPostsArray;
})();
function formatDate(date: Date) {
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
return `${month}-${day}`;
}
function formatTag(tag: string[]) {
return tag.map((t) => `#${t}`).join(" ");
}
---
<div class="card-base px-8 py-6">
{
groups.map(group => (
<div>
<div class="flex flex-row w-full items-center h-[3.75rem]">
<div class="w-[15%] md:w-[10%] transition text-2xl font-bold text-right text-75">{group.year}</div>
<div class="w-[15%] md:w-[10%]">
<div class="h-3 w-3 bg-none rounded-full outline outline-[var(--primary)] mx-auto -outline-offset-[2px] z-50 outline-3"></div>
</div>
<div class="w-[70%] md:w-[80%] transition text-left text-50">{group.posts.length} {i18n(I18nKey.postsCount)}</div>
</div>
{group.posts.map(post => (
<a href={getPostUrlBySlug(post.slug)}
aria-label={post.data.title}
class="group btn-plain block! h-10 w-full rounded-lg hover:text-[initial]"
>
<div class="flex flex-row justify-start items-center h-full">
<!-- date -->
<div class="w-[15%] md:w-[10%] transition text-sm text-right text-50">
{formatDate(post.data.published)}
</div>
<!-- dot and line -->
<div class="w-[15%] md:w-[10%] relative dash-line h-full flex items-center">
<div class="transition-all mx-auto w-1 h-1 rounded group-hover:h-5
bg-[oklch(0.5_0.05_var(--hue))] group-hover:bg-[var(--primary)]
outline outline-4 z-50
outline-[var(--card-bg)]
group-hover:outline-[var(--btn-plain-bg-hover)]
group-active:outline-[var(--btn-plain-bg-active)]
"
></div>
</div>
<!-- post title -->
<div class="w-[70%] md:max-w-[65%] md:w-[65%] text-left font-bold
group-hover:translate-x-1 transition-all group-hover:text-[var(--primary)]
text-75 pr-8 whitespace-nowrap text-ellipsis overflow-hidden"
>
{post.data.title}
</div>
<!-- tag list -->
<div class="hidden md:block md:w-[15%] text-left text-sm transition
whitespace-nowrap text-ellipsis overflow-hidden
text-30"
>{formatTag(post.data.tags)}</div>
</div>
</a>
))}
</div>
))
}
</div>
+151
View File
@@ -0,0 +1,151 @@
<script lang="ts">
import { onMount } from "svelte";
import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation";
import { getPostUrlBySlug } from "../utils/url-utils";
export let tags: string[];
export let categories: string[];
export let sortedPosts: Post[] = [];
const params = new URLSearchParams(window.location.search);
tags = params.has("tag") ? params.getAll("tag") : [];
categories = params.has("category") ? params.getAll("category") : [];
const uncategorized = params.get("uncategorized");
interface Post {
slug: string;
data: {
title: string;
tags: string[];
category?: string;
published: Date;
};
}
interface Group {
year: number;
posts: Post[];
}
let groups: Group[] = [];
function formatDate(date: Date) {
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
return `${month}-${day}`;
}
function formatTag(tagList: string[]) {
return tagList.map((t) => `#${t}`).join(" ");
}
onMount(async () => {
let filteredPosts: Post[] = sortedPosts;
if (tags.length > 0) {
filteredPosts = filteredPosts.filter(
(post) =>
Array.isArray(post.data.tags) &&
post.data.tags.some((tag) => tags.includes(tag)),
);
}
if (categories.length > 0) {
filteredPosts = filteredPosts.filter(
(post) => post.data.category && categories.includes(post.data.category),
);
}
if (uncategorized) {
filteredPosts = filteredPosts.filter((post) => !post.data.category);
}
const grouped = filteredPosts.reduce(
(acc, post) => {
const year = post.data.published.getFullYear();
if (!acc[year]) {
acc[year] = [];
}
acc[year].push(post);
return acc;
},
{} as Record<number, Post[]>,
);
const groupedPostsArray = Object.keys(grouped).map((yearStr) => ({
year: Number.parseInt(yearStr),
posts: grouped[Number.parseInt(yearStr)],
}));
groupedPostsArray.sort((a, b) => b.year - a.year);
groups = groupedPostsArray;
});
</script>
<div class="card-base px-8 py-6">
{#each groups as group}
<div>
<div class="flex flex-row w-full items-center h-[3.75rem]">
<div class="w-[15%] md:w-[10%] transition text-2xl font-bold text-right text-75">
{group.year}
</div>
<div class="w-[15%] md:w-[10%]">
<div
class="h-3 w-3 bg-none rounded-full outline outline-[var(--primary)] mx-auto
-outline-offset-[2px] z-50 outline-3"
></div>
</div>
<div class="w-[70%] md:w-[80%] transition text-left text-50">
{group.posts.length} {i18n(I18nKey.postsCount)}
</div>
</div>
{#each group.posts as post}
<a
href={getPostUrlBySlug(post.slug)}
aria-label={post.data.title}
class="group btn-plain !block h-10 w-full rounded-lg hover:text-[initial]"
>
<div class="flex flex-row justify-start items-center h-full">
<!-- date -->
<div class="w-[15%] md:w-[10%] transition text-sm text-right text-50">
{formatDate(post.data.published)}
</div>
<!-- dot and line -->
<div class="w-[15%] md:w-[10%] relative dash-line h-full flex items-center">
<div
class="transition-all mx-auto w-1 h-1 rounded group-hover:h-5
bg-[oklch(0.5_0.05_var(--hue))] group-hover:bg-[var(--primary)]
outline outline-4 z-50
outline-[var(--card-bg)]
group-hover:outline-[var(--btn-plain-bg-hover)]
group-active:outline-[var(--btn-plain-bg-active)]"
></div>
</div>
<!-- post title -->
<div
class="w-[70%] md:max-w-[65%] md:w-[65%] text-left font-bold
group-hover:translate-x-1 transition-all group-hover:text-[var(--primary)]
text-75 pr-8 whitespace-nowrap overflow-ellipsis overflow-hidden"
>
{post.data.title}
</div>
<!-- tag list -->
<div
class="hidden md:block md:w-[15%] text-left text-sm transition
whitespace-nowrap overflow-ellipsis overflow-hidden text-30"
>
{formatTag(post.data.tags)}
</div>
</div>
</a>
{/each}
</div>
{/each}
</div>
+1 -1
View File
@@ -16,7 +16,7 @@ interface Props {
published: Date;
updated?: Date;
tags: string[];
category: string;
category: string | null;
image: string;
description: string;
draft: boolean;
+5 -5
View File
@@ -3,14 +3,14 @@ import { Icon } from "astro-icon/components";
import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation";
import { formatDateToYYYYMMDD } from "../utils/date-utils";
import { url } from "../utils/url-utils";
import { getCategoryUrl, getTagUrl } from "../utils/url-utils";
interface Props {
class: string;
published: Date;
updated?: Date;
tags: string[];
category: string;
category: string | null;
hideTagsForMobile?: boolean;
hideUpdateDate?: boolean;
}
@@ -53,7 +53,7 @@ const className = Astro.props.class;
<Icon name="material-symbols:book-2-outline-rounded" class="text-xl"></Icon>
</div>
<div class="flex flex-row flex-nowrap items-center">
<a href={url(`/archive/category/${category || 'uncategorized'}/`)} aria-label=`View all posts in the ${category} category`
<a href={getCategoryUrl(category)} aria-label={`View all posts in the ${category} category`}
class="link-lg transition text-50 text-sm font-medium
hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap">
{category || i18n(I18nKey.uncategorized)}
@@ -70,10 +70,10 @@ const className = Astro.props.class;
<div class="flex flex-row flex-nowrap items-center">
{(tags && tags.length > 0) && tags.map((tag, i) => (
<div class:list={[{"hidden": i == 0}, "mx-1.5 text-[var(--meta-divider)] text-sm"]}>/</div>
<a href={url(`/archive/tag/${tag}/`)} aria-label=`View all posts with the ${tag} tag`
<a href={getTagUrl(tag)} aria-label={`View all posts with the ${tag.trim()} tag`}
class="link-lg transition text-50 text-sm font-medium
hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap">
{tag}
{tag.trim()}
</a>
))}
{!(tags && tags.length > 0) && <div class="transition text-50 text-sm font-medium">{i18n(I18nKey.noTags)}</div>}
+1 -1
View File
@@ -68,7 +68,7 @@ const getPageUrl = (p: number) => {
>
{p}
</div>
return <a href={url(getPageUrl(p))} aria-label=`Page ${p}`
return <a href={url(getPageUrl(p))} aria-label={`Page ${p}`}
class="btn-card w-11 h-11 rounded-lg overflow-hidden active:scale-[0.85]"
>{p}</a>
})}
+1 -1
View File
@@ -18,7 +18,7 @@ const profileConf = profileConfig;
const licenseConf = licenseConfig;
const postUrl = decodeURIComponent(Astro.url.toString());
---
<div class=`relative transition overflow-hidden bg-[var(--license-block-bg)] py-5 px-6 ${className}`>
<div class={`relative transition overflow-hidden bg-[var(--license-block-bg)] py-5 px-6 ${className}`}>
<div class="transition font-bold text-black/75 dark:text-white/75">
{title}
</div>
+3 -4
View File
@@ -4,7 +4,6 @@ import WidgetLayout from "./WidgetLayout.astro";
import I18nKey from "../../i18n/i18nKey";
import { i18n } from "../../i18n/translation";
import { getCategoryList } from "../../utils/content-utils";
import { getCategoryUrl } from "../../utils/url-utils";
import ButtonLink from "../control/ButtonLink.astro";
const categories = await getCategoryList();
@@ -27,11 +26,11 @@ const style = Astro.props.style;
>
{categories.map((c) =>
<ButtonLink
url={getCategoryUrl(c.name)}
url={c.url}
badge={String(c.count)}
label=`View all posts in the ${c.name} category`
label={`View all posts in the ${c.name.trim()} category`}
>
{c.name}
{c.name.trim()}
</ButtonLink>
)}
</WidgetLayout>
+3 -3
View File
@@ -3,7 +3,7 @@
import I18nKey from "../../i18n/i18nKey";
import { i18n } from "../../i18n/translation";
import { getTagList } from "../../utils/content-utils";
import { url } from "../../utils/url-utils";
import { getTagUrl } from "../../utils/url-utils";
import ButtonTag from "../control/ButtonTag.astro";
import WidgetLayout from "./WidgetLayout.astro";
@@ -23,8 +23,8 @@ const style = Astro.props.style;
<WidgetLayout name={i18n(I18nKey.tags)} id="tags" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT} class={className} style={style}>
<div class="flex gap-2 flex-wrap">
{tags.map(t => (
<ButtonTag href={url(`/archive/tag/${t.name}/`)} label={`View all posts with the ${t.name} tag`}>
{t.name}
<ButtonTag href={getTagUrl(t.name)} label={`View all posts with the ${t.name.trim()} tag`}>
{t.name.trim()}
</ButtonTag>
))}
</div>
-2
View File
@@ -1,5 +1,3 @@
export const UNCATEGORIZED = "__uncategorized__";
export const PAGE_SIZE = 8;
export const LIGHT_MODE = "light",
+1 -1
View File
@@ -9,7 +9,7 @@ const postsCollection = defineCollection({
description: z.string().optional().default(""),
image: z.string().optional().default(""),
tags: z.array(z.string()).optional().default([]),
category: z.string().optional().default(""),
category: z.string().optional().nullable().default(""),
lang: z.string().optional().default(""),
/* For internal use */
+39 -3
View File
@@ -245,13 +245,49 @@ function initCustomScrollbar() {
}
});
});
const katexElements = document.querySelectorAll('.katex-display') as NodeListOf<HTMLElement>;
katexElements.forEach((ele) => {
OverlayScrollbars(ele, {
const katexObserverOptions = {
root: null,
rootMargin: '100px',
threshold: 0.1
};
const processKatexElement = (element: HTMLElement) => {
if (!element.parentNode) return;
if (element.hasAttribute('data-scrollbar-initialized')) return;
const container = document.createElement('div');
container.className = 'katex-display-container';
container.setAttribute('aria-label', 'scrollable container for formulas');
element.parentNode.insertBefore(container, element);
container.appendChild(element);
OverlayScrollbars(container, {
scrollbars: {
theme: 'scrollbar-base scrollbar-auto py-1',
theme: 'scrollbar-base scrollbar-auto',
autoHide: 'leave',
autoHideDelay: 500,
autoHideSuspend: false
}
});
element.setAttribute('data-scrollbar-initialized', 'true');
};
const katexObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
processKatexElement(entry.target as HTMLElement);
observer.unobserve(entry.target);
}
});
}, katexObserverOptions);
katexElements.forEach(element => {
katexObserver.observe(element);
});
}
+3 -3
View File
@@ -52,7 +52,7 @@ const mainPanelTop = siteConfig.banner.enable
</div>
<!-- Banner -->
{siteConfig.banner.enable && <div id="banner-wrapper" class=`absolute z-10 w-full transition duration-700 overflow-hidden` style=`top: -${BANNER_HEIGHT_EXTEND}vh`>
{siteConfig.banner.enable && <div id="banner-wrapper" class={`absolute z-10 w-full transition duration-700 overflow-hidden`} style={`top: -${BANNER_HEIGHT_EXTEND}vh`}>
<ImageWrapper id="banner" alt="Banner image of the blog" class:list={["object-cover h-full transition duration-700 opacity-0 scale-105"]}
src={siteConfig.banner.src} position={siteConfig.banner.position}
>
@@ -60,7 +60,7 @@ const mainPanelTop = siteConfig.banner.enable
</div>}
<!-- Main content -->
<div class="absolute w-full z-30 pointer-events-none" style=`top: ${mainPanelTop}`>
<div class="absolute w-full z-30 pointer-events-none" style={`top: ${mainPanelTop}`}>
<!-- The pointer-events-none here prevent blocking the click event of the TOC -->
<div class="relative max-w-[var(--page-width)] mx-auto pointer-events-auto">
<div id="main-grid" class="transition duration-700 w-full left-0 right-0 grid grid-cols-[17.5rem_auto] grid-rows-[auto_1fr_auto] lg:grid-rows-[auto]
@@ -106,7 +106,7 @@ const mainPanelTop = siteConfig.banner.enable
<div class="absolute w-full z-0 hidden 2xl:block">
<div class="relative max-w-[var(--page-width)] mx-auto">
<!-- TOC component -->
{siteConfig.toc.enable && <div id="toc-wrapper" class:list={["hidden lg:block transition absolute top-0 -right-[var(--toc-width)] w-[var(--toc-width)] flex items-center",
{siteConfig.toc.enable && <div id="toc-wrapper" class:list={["hidden lg:block transition absolute top-0 -right-[var(--toc-width)] w-[var(--toc-width)] items-center",
{"toc-hide": siteConfig.banner.enable}]}
>
<div id="toc-inner-wrapper" class="fixed top-14 w-[var(--toc-width)] h-[calc(100vh_-_20rem)] overflow-y-scroll overflow-x-hidden hide-scrollbar">
+1 -1
View File
@@ -19,5 +19,5 @@ const len = page.data.length;
<MainGridLayout>
<PostPage page={page}></PostPage>
<Pagination class="mx-auto onload-animation" page={page} style=`animation-delay: calc(var(--content-delay) + ${(len)*50}ms)`></Pagination>
<Pagination class="mx-auto onload-animation" page={page} style={`animation-delay: calc(var(--content-delay) + ${(len)*50}ms)`}></Pagination>
</MainGridLayout>
+14
View File
@@ -0,0 +1,14 @@
---
import ArchivePanel from "@components/ArchivePanel.svelte";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import MainGridLayout from "@layouts/MainGridLayout.astro";
import { getSortedPosts } from "../utils/content-utils";
const sortedPosts = await getSortedPosts();
---
<MainGridLayout title={i18n(I18nKey.archive)}>
<ArchivePanel sortedPosts={sortedPosts} client:only="svelte"></ArchivePanel>
</MainGridLayout>
@@ -1,24 +0,0 @@
---
import ArchivePanel from "@components/ArchivePanel.astro";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import MainGridLayout from "@layouts/MainGridLayout.astro";
import { getCategoryList } from "@utils/content-utils";
export async function getStaticPaths() {
const categories = await getCategoryList();
return categories.map((category) => {
return {
params: {
category: category.name,
},
};
});
}
const category = Astro.params.category as string;
---
<MainGridLayout title={i18n(I18nKey.archive)} description={i18n(I18nKey.archive)}>
<ArchivePanel categories={[category]}></ArchivePanel>
</MainGridLayout>
@@ -1,11 +0,0 @@
---
import ArchivePanel from "@components/ArchivePanel.astro";
import { UNCATEGORIZED } from "@constants/constants";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import MainGridLayout from "@layouts/MainGridLayout.astro";
---
<MainGridLayout title={i18n(I18nKey.archive)}>
<ArchivePanel categories={[UNCATEGORIZED]}></ArchivePanel>
</MainGridLayout>
-11
View File
@@ -1,11 +0,0 @@
---
import ArchivePanel from "@components/ArchivePanel.astro";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import MainGridLayout from "@layouts/MainGridLayout.astro";
---
<MainGridLayout title={i18n(I18nKey.archive)}>
<ArchivePanel></ArchivePanel>
</MainGridLayout>
-32
View File
@@ -1,32 +0,0 @@
---
import ArchivePanel from "@components/ArchivePanel.astro";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import MainGridLayout from "@layouts/MainGridLayout.astro";
import { getSortedPosts } from "@utils/content-utils";
export async function getStaticPaths() {
const posts = await getSortedPosts();
// タグを集めるための Set の型を指定
const allTags = posts.reduce<Set<string>>((acc, post) => {
// biome-ignore lint/complexity/noForEach: <explanation>
post.data.tags.forEach((tag) => acc.add(tag));
return acc;
}, new Set());
const allTagsArray = Array.from(allTags);
return allTagsArray.map((tag) => ({
params: {
tag: tag,
},
}));
}
const tag = Astro.params.tag as string;
---
<MainGridLayout title={i18n(I18nKey.archive)} description={i18n(I18nKey.archive)}>
<ArchivePanel tags={[tag]}></ArchivePanel>
</MainGridLayout>
+7
View File
@@ -91,4 +91,11 @@
}
}
.katex-display-container {
max-width: 100%;
overflow-x: auto;
margin: 1em 0;
}
}
+15 -5
View File
@@ -1,6 +1,7 @@
import { getCollection } from "astro:content";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import { getCategoryUrl } from "@utils/url-utils.ts";
export async function getSortedPosts() {
const allBlogPosts = await getCollection("posts", ({ data }) => {
@@ -54,6 +55,7 @@ export async function getTagList(): Promise<Tag[]> {
export type Category = {
name: string;
count: number;
url: string;
};
export async function getCategoryList(): Promise<Category[]> {
@@ -61,15 +63,19 @@ export async function getCategoryList(): Promise<Category[]> {
return import.meta.env.PROD ? data.draft !== true : true;
});
const count: { [key: string]: number } = {};
allBlogPosts.map((post: { data: { category: string | number } }) => {
allBlogPosts.map((post: { data: { category: string | null } }) => {
if (!post.data.category) {
const ucKey = i18n(I18nKey.uncategorized);
count[ucKey] = count[ucKey] ? count[ucKey] + 1 : 1;
return;
}
count[post.data.category] = count[post.data.category]
? count[post.data.category] + 1
: 1;
const categoryName =
typeof post.data.category === "string"
? post.data.category.trim()
: String(post.data.category).trim();
count[categoryName] = count[categoryName] ? count[categoryName] + 1 : 1;
});
const lst = Object.keys(count).sort((a, b) => {
@@ -78,7 +84,11 @@ export async function getCategoryList(): Promise<Category[]> {
const ret: Category[] = [];
for (const c of lst) {
ret.push({ name: c, count: count[c] });
ret.push({
name: c,
count: count[c],
url: getCategoryUrl(c),
});
}
return ret;
}
+14 -5
View File
@@ -1,4 +1,4 @@
import i18nKey from "@i18n/i18nKey";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
export function pathsEqual(path1: string, path2: string) {
@@ -16,10 +16,19 @@ export function getPostUrlBySlug(slug: string): string {
return url(`/posts/${slug}/`);
}
export function getCategoryUrl(category: string): string {
if (category === i18n(i18nKey.uncategorized))
return url("/archive/category/uncategorized/");
return url(`/archive/category/${category}/`);
export function getTagUrl(tag: string): string {
if (!tag) return url("/archive/");
return url(`/archive/?tag=${encodeURIComponent(tag.trim())}`);
}
export function getCategoryUrl(category: string | null): string {
if (
!category ||
category.trim() === "" ||
category.trim().toLowerCase() === i18n(I18nKey.uncategorized).toLowerCase()
)
return url("/archive/?uncategorized=true");
return url(`/archive/?category=${encodeURIComponent(category.trim())}`);
}
export function getDir(path: string): string {