このコミットが含まれているのは:
2026-07-05 18:21:39 +09:00
コミット 1b6cac43c2
14個のファイルの変更1540行の追加397行の削除
+7
ファイルの表示
@@ -5,6 +5,7 @@ import { createPath, useNavigate } from 'react-router-dom'
import { useOverlayStore } from '@/components/RouteBlockerOverlay'
import { prefetchForURL } from '@/lib/prefetchers'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { cn } from '@/lib/utils'
import type { AnchorHTMLAttributes, MouseEvent, TouchEvent } from 'react'
@@ -33,6 +34,8 @@ export default forwardRef<HTMLAnchorElement, Props> (({
const navigate = useNavigate ()
const qc = useQueryClient ()
const behaviourSettings = useClientBehaviourSettings ()
const linkPreloadMode = behaviourSettings.linkPreload ?? 'intent'
const url = useMemo (() => {
const path = (typeof to === 'string') ? to : createPath (to)
return (new URL (path, location.origin)).toString ()
@@ -54,11 +57,15 @@ export default forwardRef<HTMLAnchorElement, Props> (({
const handleMouseEnter = async (ev: MouseEvent<HTMLAnchorElement>) => {
onMouseEnter?.(ev)
if (ev.defaultPrevented || linkPreloadMode !== 'intent')
return
await doPrefetch ()
}
const handleTouchStart = async (ev: TouchEvent<HTMLAnchorElement>) => {
onTouchStart?.(ev)
if (ev.defaultPrevented || linkPreloadMode !== 'intent')
return
await doPrefetch ()
}
+54 -2
ファイルの表示
@@ -21,6 +21,7 @@ import { toast } from '@/components/ui/use-toast'
import { CATEGORIES, CATEGORY_NAMES } from '@/consts'
import { apiDelete, apiGet, apiPatch, apiPost } from '@/lib/api'
import { postsKeys, tagsKeys } from '@/lib/queryKeys'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { dateString, originalCreatedAtString } from '@/lib/utils'
import type { DragEndEvent } from '@dnd-kit/core'
@@ -29,6 +30,9 @@ import type { FC, MutableRefObject, ReactNode } from 'react'
import type { Category, Post, TagWithSections } from '@/types'
type TagByCategory = { [key in Category]: TagWithSections[] }
type FlatTagRow = {
tag: TagWithSections
parentTagId?: number }
const renderTagTree = (
@@ -61,6 +65,33 @@ const renderTagTree = (
}
const flattenTagTree = (
tags: TagWithSections[],
): FlatTagRow[] => {
const seen = new Set<number> ()
const rows: FlatTagRow[] = []
const visit = (
tag: TagWithSections,
parentTagId?: number,
) => {
if (seen.has (tag.id))
return
seen.add (tag.id)
rows.push ({ tag, parentTagId })
for (const child of tag.children ?? [])
visit (child, tag.id)
}
for (const tag of tags)
visit (tag)
return rows.sort ((rowA, rowB) => rowA.tag.name < rowB.tag.name ? -1 : 1)
}
const isDescendant = (
root: TagWithSections,
targetId: number,
@@ -156,6 +187,8 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
sp = Boolean (sp)
const qc = useQueryClient ()
const behaviourSettings = useClientBehaviourSettings ()
const tagRelationDisplay = behaviourSettings.tagRelationDisplay ?? 'grouped'
const baseTags = useMemo<TagByCategory> (() => {
const tagsTmp = { } as TagByCategory
@@ -321,8 +354,27 @@ const TagDetailSidebar: FC<Props> = ({ className, post, sp }) => {
</SubsectionTitle>
<ul>
{(tags[cat] ?? []).flatMap (tag => (
renderTagTree (tag, 0, `cat-${ cat }`, suppressClickRef, undefined, sp)))}
{(tagRelationDisplay === 'grouped'
? (tags[cat] ?? []).flatMap (tag =>
renderTagTree (
tag,
0,
`cat-${ cat }`,
suppressClickRef,
undefined,
sp,
),
)
: flattenTagTree (tags[cat] ?? []).map (row => (
<li key={`flat-${ cat }-${ row.tag.id }`} className="mb-1">
<DraggableDroppableTagRow
tag={row.tag}
nestLevel={0}
pathKey={`flat-${ cat }-${ row.tag.id }`}
parentTagId={row.parentTagId}
suppressClickRef={suppressClickRef}
sp={sp}/>
</li>)))}
<DropSlot cat={cat}/>
</ul>
</div>))}
+17 -20
ファイルの表示
@@ -209,12 +209,11 @@ const TopNav: FC<Props> = ({ user }) => {
return (
<>
<nav className="px-3 flex justify-between items-center w-full
bg-yellow-200 dark:bg-red-975 md:bg-yellow-50">
border-b top-nav-themed">
<div className="flex items-center gap-2 h-12">
<PrefetchLink
to="/posts"
className="mx-4 text-xl font-bold text-pink-600 hover:text-pink-400
dark:text-pink-300 dark:hover:text-pink-100"
className="mx-4 text-xl font-bold"
onClick={() => {
scroll (0, 0)
}}>
@@ -224,7 +223,7 @@ const TopNav: FC<Props> = ({ user }) => {
<div ref={navRef} className="relative hidden md:flex h-12 items-center">
<div aria-hidden
className={cn ('absolute inset-y-0 h-12',
'bg-yellow-200 dark:bg-red-950',
'top-nav-themed-active',
highlightTransitionClass)}
style={{ width: hl.width,
transform: `translateX(${ hl.left }px)`,
@@ -247,7 +246,7 @@ const TopNav: FC<Props> = ({ user }) => {
itemsRef.current[i] = el
}}
className={cn ('relative z-10 flex h-full items-center px-5',
(i === openItemIdx) && 'font-bold')}>
(i === openItemIdx) && 'top-nav-themed-active font-bold')}>
{item.name}
</PrefetchLink>
</motion.div>))}
@@ -262,7 +261,7 @@ const TopNav: FC<Props> = ({ user }) => {
measure (-1)
}}
className={cn ('relative z-10 flex h-full items-center px-5',
(openItemIdx < 0 || moreVsbl) && 'font-bold')}>
(openItemIdx < 0 || moreVsbl) && 'top-nav-themed-active font-bold')}>
&raquo;
</PrefetchLink>
</div>
@@ -272,9 +271,7 @@ const TopNav: FC<Props> = ({ user }) => {
<button
type="button"
className="md:hidden ml-auto border-0 bg-transparent pr-4
text-pink-600 hover:text-pink-400
dark:text-pink-300 dark:hover:text-pink-100"
className="md:hidden ml-auto border-0 bg-transparent pr-4"
onClick={() => {
setMenuOpen (!(menuOpen))
}}>
@@ -288,7 +285,7 @@ const TopNav: FC<Props> = ({ user }) => {
{...(animationsOff ? { }
: { layout: true })}
className="relative z-20 hidden md:block overflow-hidden
bg-yellow-200 dark:bg-red-950"
top-nav-themed-submenu"
animate={{ height: submenuHeight }}
onMouseLeave={() => {
if (moreVsbl)
@@ -429,8 +426,8 @@ const TopNav: FC<Props> = ({ user }) => {
? (
menuOpen && (
<div
className={cn ('flex flex-col md:hidden',
'bg-yellow-200 dark:bg-red-975 items-start')}>
className={cn ('top-nav-themed top-nav-themed-submenu flex flex-col md:hidden',
'items-start')}>
<Separator/>
{visibleMenu.map ((item, i) => (
<Fragment key={i}>
@@ -438,7 +435,7 @@ const TopNav: FC<Props> = ({ user }) => {
to={i === openItemIdx ? item.to : '#'}
className={cn ('w-full min-h-[40px] flex items-center pl-8',
((i === openItemIdx)
&& 'font-bold bg-yellow-50 dark:bg-red-950'))}
&& 'top-nav-themed-active font-bold'))}
onClick={(ev: MouseEvent<HTMLAnchorElement>) => {
if (i !== openItemIdx)
{
@@ -450,7 +447,7 @@ const TopNav: FC<Props> = ({ user }) => {
</PrefetchLink>
{i === openItemIdx && (
<div className="w-full bg-yellow-50 dark:bg-red-950">
<div className="top-nav-themed-submenu w-full">
{item.subMenu
.filter (subItem => subItem.visible ?? true)
.map ((subItem, j) => (
@@ -478,7 +475,7 @@ const TopNav: FC<Props> = ({ user }) => {
}}
className={cn ('w-full min-h-[40px] flex items-center pl-8',
((openItemIdx < 0)
&& 'font-bold bg-yellow-50 dark:bg-red-950'))}>
&& 'top-nav-themed-active font-bold'))}>
&raquo;
</PrefetchLink>
<TopNavUser user={user} sp/>
@@ -489,8 +486,8 @@ const TopNav: FC<Props> = ({ user }) => {
{menuOpen && (
<motion.div
key="spmenu"
className={cn ('flex flex-col md:hidden',
'bg-yellow-200 dark:bg-red-975 items-start')}
className={cn ('top-nav-themed top-nav-themed-submenu flex flex-col md:hidden',
'items-start')}
variants={{ closed: { clipPath: 'inset(0 0 100% 0)',
height: 0 },
open: { clipPath: 'inset(0 0 0% 0)',
@@ -506,7 +503,7 @@ const TopNav: FC<Props> = ({ user }) => {
to={i === openItemIdx ? item.to : '#'}
className={cn ('w-full min-h-[40px] flex items-center pl-8',
((i === openItemIdx)
&& 'font-bold bg-yellow-50 dark:bg-red-950'))}
&& 'top-nav-themed-active font-bold'))}
onClick={(ev: MouseEvent<HTMLAnchorElement>) => {
if (i !== openItemIdx)
{
@@ -521,7 +518,7 @@ const TopNav: FC<Props> = ({ user }) => {
{i === openItemIdx && (
<motion.div
key={`sp-sub-${ i }`}
className="w-full bg-yellow-50 dark:bg-red-950"
className="top-nav-themed-submenu w-full"
variants={{ closed: { clipPath: 'inset(0 0 100% 0)',
height: 0,
opacity: 0 },
@@ -560,7 +557,7 @@ const TopNav: FC<Props> = ({ user }) => {
}}
className={cn ('w-full min-h-[40px] flex items-center pl-8',
((openItemIdx < 0)
&& 'font-bold bg-yellow-50 dark:bg-red-950'))}>
&& 'top-nav-themed-active font-bold'))}>
&raquo;
</PrefetchLink>
<TopNavUser user={user} sp/>
+30 -2
ファイルの表示
@@ -10,6 +10,8 @@ import type { FC, ReactNode } from 'react'
import type { ClientAnimationMode,
ClientBehaviorSettings,
ClientEmbedAutoLoadMode,
ClientLinkPreloadMode,
ClientTagRelationDisplayMode,
ClientThumbnailMode } from '@/lib/settings'
type Props = {
@@ -39,6 +41,14 @@ const embedAutoLoadOptions: SegmentedOption<ClientEmbedAutoLoadMode>[] = [
{ value: 'manual', label: 'クリック時' },
{ value: 'auto', label: '自動' }]
const linkPreloadOptions: SegmentedOption<ClientLinkPreloadMode>[] = [
{ value: 'off', label: 'しない' },
{ value: 'intent', label: 'マウスを置いたら' }]
const tagRelationDisplayOptions: SegmentedOption<ClientTagRelationDisplayMode>[] = [
{ value: 'flat', label: 'まとめない' },
{ value: 'grouped', label: 'まとめる' }]
const thumbnailModeOptions: SegmentedOption<ClientThumbnailMode>[] = [
{ value: 'off', label: '非表示' },
{ value: 'light', label: '軽量' },
@@ -63,8 +73,8 @@ const SegmentedControl = <T extends string,> (
'transition-colors focus-visible:outline-none focus-visible:ring-2',
'focus-visible:ring-ring focus-visible:ring-offset-2',
value === option.value
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:bg-background/70 hover:text-foreground',
? 'bg-primary text-primary-foreground shadow-sm ring-1 ring-primary/40'
: 'bg-transparent text-muted-foreground hover:bg-background/70 hover:text-foreground',
)}
onClick={() => onChange (option.value)}>
{option.label}
@@ -157,6 +167,24 @@ const BehaviourSettingsSection: FC<Props> = ({ sectionClassName }) => {
onChange={value => updateDraft ('animation', value)}/>
</SettingBlock>
<SettingBlock
title="リンク先の先読み"
description="リンクにマウスを置いた時などに、移動先の情報を先に読みます。通信量や意図しない読込みが気になる場合は「しない」にできます。">
<SegmentedControl
value={draftSettings.linkPreload ?? 'intent'}
options={linkPreloadOptions}
onChange={value => updateDraft ('linkPreload', value)}/>
</SettingBlock>
<SettingBlock
title="タグの親子関係表示"
description="親子関係があるタグを、まとまりとして表示します。見た目を単純にしたい場合は「まとめない」にできます。">
<SegmentedControl
value={draftSettings.tagRelationDisplay ?? 'grouped'}
options={tagRelationDisplayOptions}
onChange={value => updateDraft ('tagRelationDisplay', value)}/>
</SettingBlock>
<SettingBlock
title="埋め込み自動読込"
description="外部埋め込みを自動で読み込むかを切り替えます。">