コミットを比較

..
1 コミット
作成者 SHA1 メッセージ 日付
みてるぞ 5840987162 #445 2026-11-09 00:02:32 +09:00
20個のファイルの変更、224行の追加、917行の削除
+5
ファイルの表示
@@ -0,0 +1,5 @@
class AddLinkedTagIdsToNicoTagVersions < ActiveRecord::Migration[8.0]
def change
add_column :nico_tag_versions, :linked_tag_ids, :text
end
end
+42
ファイルの表示
@@ -0,0 +1,42 @@
class MigrateLinkedTagsInNicoTagVersions < ActiveRecord::Migration[8.0]
class MigrationTagVersion < ActiveRecord::Base
self.table_name = 'tag_versions'
end
class MigrationNicoTagVersion < ActiveRecord::Base
self.table_name = 'nico_tag_versions'
end
def up
# 除票外部タグの恢復はないので除票履歴があれば全消しでよいはず.
execute <<~SQL
DELETE
t1
FROM
nico_tag_versions t1
INNER JOIN
nico_tag_versions t2
ON
t1.tag_id = t2.tag_id
AND t2.event_type = 'discard'
SQL
MigrationNicoTagVersion.find_each do |ntv|
linked_tag_ids = ntv.linked_tags.split.map { |name|
# 厳密には一意ぢゃないけどめんだぅだからこれでいいや.
MigrationTagVersion
.order(created_at: :desc, id: :desc)
.find_by("names ->> '$.ja.name' = ?", name)
&.tag_id
}.compact.uniq.sort.join(' ')
ntv.update_columns(linked_tag_ids:)
end
change_column_null :nico_tag_versions, :linked_tag_ids, false
end
def down
raise ActiveRecord::IrreversibleMigration, '戻せません.'
end
end
+6
ファイルの表示
@@ -0,0 +1,6 @@
class RemoveLinkedTagsFromNicoTagVersions < ActiveRecord::Migration[8.0]
def change
remove_column :nico_tag_versions, :linked_tags
remove_column :nico_tag_versions, :name
end
end
生成ファイル
+2 -3
ファイルの表示
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2026_10_15_010000) do
ActiveRecord::Schema[8.0].define(version: 2026_11_08_020000) do
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
@@ -301,10 +301,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_15_010000) do
t.bigint "tag_id", null: false
t.integer "version_no", null: false
t.string "event_type", null: false
t.string "name", null: false
t.text "linked_tags", null: false
t.datetime "created_at", null: false
t.bigint "created_by_user_id"
t.text "linked_tag_ids", null: false
t.index ["created_at"], name: "index_nico_tag_versions_on_created_at"
t.index ["created_by_user_id", "created_at"], name: "index_nico_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc }
t.index ["tag_id", "created_at"], name: "index_nico_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc }
-94
ファイルの表示
@@ -1,94 +0,0 @@
Copyright 2018 The Noto Project Authors (github.com/googlei18n/noto-fonts)
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
バイナリファイルは表示されません。
-11
ファイルの表示
@@ -1,11 +0,0 @@
Noto Sans Mongolian Regular, unmodified TrueType font.
Font copyright: Copyright 2018 Google Inc. All Rights Reserved.
- Source: https://github.com/notofonts/noto-fonts/tree/main/hinted/ttf/NotoSansMongolian
- Font: https://raw.githubusercontent.com/notofonts/noto-fonts/main/hinted/ttf/NotoSansMongolian/NotoSansMongolian-Regular.ttf
- License: SIL Open Font License 1.1; see the accompanying LICENSE.
- Retrieved: 2026-11-08.
Used only by the Unicode Mongolian sample in the development layout route.
The font and license are served together from this directory.
+11 -16
ファイルの表示
@@ -20,13 +20,13 @@ import { Toaster } from '@/components/ui/toaster'
import { isApiError } from '@/lib/api'
import { initialiseUserSession } from '@/lib/users'
import { prepareHelpPage } from '@/lib/help'
import { fetchLocales, isLocaleCode, isVerticalPreviewPath, legacyLocalePath,
localiseTo, switchLocalePath, unlocalisedPath } from '@/lib/locales'
import { fetchLocales, isLocaleCode, legacyLocalePath,
switchLocalePath, unlocalisedPath } from '@/lib/locales'
import { isAuthorityDependentQuery, localesKeys, sessionKeys } from '@/lib/queryKeys'
import { isCountsPath } from '@/lib/routeLocales'
import { switchPostFilterLocale } from '@/lib/posts'
import { switchWikiLocale } from '@/lib/wiki'
import { useLocale, useVerticalPreview } from '@/lib/useLocale'
import { useLocale } from '@/lib/useLocale'
import { applyClientAnimationMode,
applyClientAppearance,
fetchUserThemeSlots,
@@ -267,7 +267,6 @@ const RoutedApp = ({ locales }: { locales: AvailableLocale[] }) => {
const client = useQueryClient ()
const location = useLocation ()
const locale = useLocale ()
const verticalPreview = useVerticalPreview ()
const route = resolvePageRoute (location.pathname)
const robots = isCountsPath (location.pathname) ? 'noindex' : route?.route.handle?.robots
const pagePath = unlocalisedPath (location.pathname)
@@ -394,8 +393,7 @@ const RoutedApp = ({ locales }: { locales: AvailableLocale[] }) => {
<motion.div
layout={(animationMode === 'off' || countsPage) ? false : 'position'}
transition={{ layout: appLayoutTransition }}
data-vertical-preview={verticalPreview || undefined}
className="app-layout relative flex h-dvh w-full flex-col overflow-y-hidden">
className="relative flex h-dvh w-full flex-col overflow-y-hidden">
<TopNav user={user} locales={locales} resolveLocaleSwitch={switchLocale}/>
<RouteTransitionWrapper
user={user}
@@ -414,7 +412,6 @@ const RoutedApp = ({ locales }: { locales: AvailableLocale[] }) => {
const LocaleRoute: FC = () => {
const { locale } = useParams ()
const location = useLocation ()
const verticalPreview = isVerticalPreviewPath (location.pathname)
const { i18n } = useTranslation ()
const { data, isError } = useQuery ({
queryKey: localesKeys.index,
@@ -424,11 +421,10 @@ const LocaleRoute: FC = () => {
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchOnReconnect: true })
const contentLocale = verticalPreview ? 'ja' : locale
const activeLocale =
(isLocaleCode (contentLocale)
&& data?.locales.some (entry => entry.code === contentLocale))
? contentLocale
(isLocaleCode (locale)
&& data?.locales.some (entry => entry.code === locale))
? locale
: null
useLayoutEffect (() => {
@@ -439,15 +435,15 @@ const LocaleRoute: FC = () => {
}, [activeLocale, i18n])
const redirect = legacyLocalePath (location, pathname => resolvePageRoute (pathname) != null)
if (!(isLocaleCode (locale)) && !verticalPreview && redirect == null)
if (!(isLocaleCode (locale)) && redirect == null)
return <NotFound/>
if (isError && data == null)
return <ServiceUnavailable/>
if (data == null)
return null
if (isLocaleCode (locale) || verticalPreview)
if (isLocaleCode (locale))
{
return data.locales.some (entry => entry.code === contentLocale)
return data.locales.some (entry => entry.code === locale)
? <RoutedApp locales={data.locales}/>
: <NotFound/>
}
@@ -459,8 +455,7 @@ const LocaleRoute: FC = () => {
const LocaleRedirect = ({ to }: { to: string }) => {
const locale = useLocale ()
const location = useLocation ()
return <Navigate to={localiseTo (
{ ...location, pathname: to }, locale, location.pathname)} replace/>
return <Navigate to={switchLocalePath ({ ...location, pathname: to }, locale)} replace/>
}
+1 -1
ファイルの表示
@@ -34,7 +34,7 @@ const PostList: FC<Props> = ({ posts, onClick }) => {
const cardRef = useRef<HTMLDivElement> (null)
return (
<div className="post-list flex flex-wrap gap-6 p-4">
<div className="flex flex-wrap gap-6 p-4">
{posts.map ((post, i) => {
const sharedId = `page-${ post.id }`
const layoutId = sharedId
+5 -9
ファイルの表示
@@ -1,10 +1,9 @@
import { useQueryClient } from '@tanstack/react-query'
import { forwardRef, useMemo } from 'react'
import { flushSync } from 'react-dom'
import { createPath, useLocation } from 'react-router-dom'
import { createPath } from 'react-router-dom'
import { isVerticalPreviewPath, localeFromPath,
localiseTo, unlocalisedPath } from '@/lib/locales'
import { localiseTo } from '@/lib/locales'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import { useOverlayStore } from '@/components/RouteBlockerOverlay'
import { prefetchForURL } from '@/lib/prefetchers'
@@ -39,18 +38,15 @@ export default forwardRef<HTMLAnchorElement, Props> (({
const navigate = useLocaleNavigate ()
const locale = useLocale ()
const { pathname } = useLocation ()
const qc = useQueryClient ()
const behaviourSettings = useClientBehaviourSettings ()
const linkPreloadMode = behaviourSettings.linkPreload ?? 'intent'
const path = useMemo (
() => {
const localised = localiseTo (to, locale, pathname)
const destination = typeof localised === 'string' ? localised : createPath (localised)
return (isVerticalPreviewPath (pathname) && localeFromPath (destination) === locale)
? `/mn-Mong${ unlocalisedPath (destination) }` : destination
const localised = localiseTo (to, locale)
return typeof localised === 'string' ? localised : createPath (localised)
},
[to, locale, pathname],
[to, locale],
)
const url = useMemo (() => {
return (new URL (path, currentRenderContext ()?.origin ?? 'http://localhost')).toString ()
+27 -65
ファイルの表示
@@ -4,7 +4,7 @@ import { Globe } from 'lucide-react'
import { Fragment, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { useLocale, useLocaleNavigate, useVerticalPreview } from '@/lib/useLocale'
import { useLocale, useLocaleNavigate } from '@/lib/useLocale'
import LocalisedText, { formatUIString } from '@/components/LocalisedText'
import Separator from '@/components/MenuSeparator'
import PrefetchLink from '@/components/PrefetchLink'
@@ -137,7 +137,6 @@ export const menuOutline = (
const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
const { message, label } = useUITranslation ('shared')
const locale = useLocale ()
const verticalPreview = useVerticalPreview ()
const localeName = locales.find (entry => entry.code === locale)?.name
const navigate = useLocaleNavigate ()
const location = useLocation ()
@@ -181,12 +180,11 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
let node: HTMLElement | null = el
while (node != null && node !== nav)
{
left += verticalPreview ? node.offsetTop : node.offsetLeft
left += node.offsetLeft
const parent: Element | null = node.offsetParent
node = parent instanceof HTMLElement ? parent : null
}
const width = node === nav
? (verticalPreview ? el.offsetHeight : el.offsetWidth) : 0
const width = node === nav ? el.offsetWidth : 0
const visible = width > 0
setHL (current =>
(current.left === left && current.width === width && current.visible === visible)
@@ -204,7 +202,6 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
const [moreVsbl, setMoreVsbl] = useState (false)
const [openItemIdx, setOpenItemIdx] = useState (-1)
const [submenuHeight, setSubmenuHeight] = useState (0)
const [submenuWidth, setSubmenuWidth] = useState (0)
const [wikiId, setWikiId] = useState<number | null> (WikiIdBus.get ())
const wikiIdStr = String (wikiId ?? '')
@@ -258,22 +255,8 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
animationsOff
? undefined
: (reducedAnimations
? 'transition-[transform,width,height] duration-75 ease-linear'
: 'transition-[transform,width,height] duration-200 ease-out')
const submenuItemMotion = animationsOff ? { } : {
initial: { x: verticalPreview ? -40 : 0,
y: verticalPreview ? 0 : -40, opacity: 0 },
animate: { x: 0, y: 0, opacity: 1 },
exit: { x: verticalPreview ? -40 : 0,
y: verticalPreview ? 0 : -40, opacity: 0 } }
const submenuVariants = {
enter: (direction: -1 | 1) => ({
x: verticalPreview ? direction * 24 : 0,
y: verticalPreview ? 0 : direction * 24, opacity: 0 }),
centre: { x: 0, y: 0, opacity: 1 },
exit: (direction: -1 | 1) => ({
x: verticalPreview ? (-direction) * 24 : 0,
y: verticalPreview ? 0 : (-direction) * 24, opacity: 0 }) }
? 'transition-[transform,width] duration-75 ease-linear'
: 'transition-[transform,width] duration-200 ease-out')
const prevActiveIdxRef = useRef<number> (activeIdx)
@@ -304,14 +287,6 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
return
}
if (verticalPreview)
{
setDesktop (true)
setDensity ('auto')
measure (moreVsbl ? -1 : activeIdx)
return
}
// Measure the same desktop elements while the mobile layout is active.
layout.setAttribute ('data-measuring', '')
const previousDensity = layout.getAttribute ('data-density') ?? 'auto'
@@ -376,8 +351,7 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
removeEventListener ('resize', schedule)
document.fonts?.removeEventListener ('loadingdone', schedule)
}
}, [activeIdx, moreVsbl, desktop, density, user?.name, locale, locales,
menuLabels, verticalPreview])
}, [activeIdx, moreVsbl, desktop, density, user?.name, locale, locales, menuLabels])
useEffect (() => {
if (desktop == null || layoutReady)
@@ -404,8 +378,7 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
measure (moreVsbl ? -1 : activeIdx)
if (!(desktop))
setMoreVsbl (false)
}, [activeIdx, moreVsbl, desktop, density, locale, animationMode,
menuLabels, verticalPreview])
}, [activeIdx, moreVsbl, desktop, density, locale, animationMode, menuLabels])
useEffect (() => {
const unsubscribe = WikiIdBus.subscribe (setWikiId)
@@ -419,7 +392,6 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
const update = () => {
setSubmenuHeight (Math.ceil (content.getBoundingClientRect ().height))
setSubmenuWidth (Math.ceil (content.getBoundingClientRect ().width))
}
const observer =
typeof ResizeObserver === 'undefined' ? null : new ResizeObserver (update)
@@ -431,7 +403,7 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
observer?.disconnect ()
removeEventListener ('resize', update)
}
}, [activeIdx, moreVsbl, desktop, density, locale, verticalPreview])
}, [activeIdx, moreVsbl, desktop, density, locale])
useEffect (() => {
setMenuOpen (false)
@@ -507,11 +479,8 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
className={cn ('absolute inset-y-0 h-12',
'top-nav-highlight',
highlightTransitionClass)}
style={{ width: verticalPreview ? '100%' : hl.width,
height: verticalPreview ? hl.width : undefined,
transform: verticalPreview
? `translateY(${ hl.left }px)`
: `translateX(${ hl.left }px)`,
style={{ width: hl.width,
transform: `translateX(${ hl.left }px)`,
opacity: hl.visible ? 1 : 0 }}/>
{visibleMenu.map ((item, i) => (
@@ -560,18 +529,10 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
<div className="top-nav-account ml-auto flex min-w-0 items-center gap-2">
<TopNavUser user={user}/>
{localeSelector}
{verticalPreview && (
<span className="vertical-preview-sample" lang="mn-Mong">
ᠮᠣᠩᠭᠣᠯ
</span>)}
</div>
<div className="top-nav-mobile-controls ml-auto flex shrink-0 items-center gap-2">
{menuOpen && localeSelector}
{verticalPreview && (
<span className="vertical-preview-sample" lang="mn-Mong">
ᠮᠣᠩᠭᠣᠯ
</span>)}
<button
type="button"
className="top-nav-mobile-toggle shrink-0 whitespace-nowrap
@@ -593,9 +554,7 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
layout
className="top-nav-desktop-submenu relative z-20 shrink-0
overflow-hidden top-nav-submenu"
animate={verticalPreview && desktop
? { width: submenuWidth, height: '100%' }
: { width: 'auto', height: desktop == null ? 'auto' : submenuHeight }}
animate={{ height: desktop == null ? 'auto' : submenuHeight }}
onMouseLeave={() => {
if (moreVsbl)
setMoreVsbl (false)
@@ -607,8 +566,7 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
onLayoutAnimationComplete={() => {
measure (highlightTargetRef.current)
}}>
<div ref={submenuContentRef}
className={cn ('top-nav-submenu-content', moreVsbl && 'top-nav-more-menu')}>
<div ref={submenuContentRef} className={cn (moreVsbl && 'top-nav-more-menu')}>
{moreVsbl
? (
moreMenu.map ((item, i) => (
@@ -618,11 +576,9 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
{...((item.visible ?? true)
? { layoutId: `menu-${ menu.indexOf (item) }` }
: (animationsOff ? { }
: { initial: {
x: 40, y: verticalPreview ? 0 : -40, opacity: 0 },
: { initial: { x: 40, y: -40, opacity: 0 },
animate: { x: 0, y: 0, opacity: 1 },
exit: {
x: 40, y: verticalPreview ? 0 : -40, opacity: 0 } }))}
exit: { x: 40, y: -40, opacity: 0 } }))}
onLayoutAnimationComplete={() => measure (highlightTargetRef.current)}
className="z-10 min-w-0 px-3 py-2 font-bold">
<h2>
@@ -643,7 +599,10 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
transition={topNavTransition}
{...(visibleMenu[activeIdx] === item
? { layoutId: `submenu-${ menu.indexOf (item) }-${ j }` }
: submenuItemMotion)}>
: (animationsOff ? { }
: { initial: { y: -40, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: -40, opacity: 0 } }))}>
{subItem.component}
</motion.div>)
: (
@@ -653,7 +612,10 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
transition={topNavTransition}
{...(visibleMenu[activeIdx] === item
? { layoutId: `submenu-${ menu.indexOf (item) }-${ j }` }
: submenuItemMotion)}>
: (animationsOff ? { }
: { initial: { y: -40, opacity: 0 },
animate: { y: 0, opacity: 1 },
exit: { y: -40, opacity: 0 } }))}>
<PrefetchLink
to={subItem.to}
target={subItem.to.slice (0, 2) === '//' ? '_blank' : undefined}
@@ -671,8 +633,7 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
<div className="relative min-h-[40px]">
{animationsOff
? (
<div className="top-nav-submenu-items flex min-h-[40px]
flex-wrap items-center px-3">
<div className="flex min-h-[40px] flex-wrap items-center px-3">
{(visibleMenu[activeIdx]?.subMenu ?? [])
.filter (item => item.visible ?? true)
.map ((item, i) => (
@@ -701,9 +662,10 @@ const TopNav: FC<Props> = ({ user, locales, resolveLocaleSwitch }) => {
<motion.div
key={activeIdx}
custom={dir}
variants={submenuVariants}
className="top-nav-submenu-items relative flex min-h-[40px]
flex-wrap items-center px-3"
variants={{ enter: (d: -1 | 1) => ({ y: d * 24, opacity: 0 }),
centre: { y: 0, opacity: 1 },
exit: (d: -1 | 1) => ({ y: (-d) * 24, opacity: 0 }) }}
className="relative flex min-h-[40px] flex-wrap items-center px-3"
initial="enter"
animate="centre"
exit="exit"
+5 -58
ファイルの表示
@@ -1,10 +1,7 @@
import { motion } from 'framer-motion'
import { useEffect, useRef } from 'react'
import { useLocation } from 'react-router-dom'
import { clientAnimationTransition } from '@/lib/clientAnimation'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { useVerticalPreview } from '@/lib/useLocale'
import { cn } from '@/lib/utils'
import type { FC, ReactNode } from 'react'
@@ -12,71 +9,21 @@ import type { FC, ReactNode } from 'react'
type Props = {
children: ReactNode
className?: string
animateLayout?: boolean
verticalContent?: boolean }
animateLayout?: boolean }
const MainArea: FC<Props> = (
{ children, className, animateLayout = true, verticalContent = false },
) => {
const MainArea: FC<Props> = ({ children, className, animateLayout = true }) => {
const behaviourSettings = useClientBehaviourSettings ()
const verticalPreview = useVerticalPreview ()
const horizontal = verticalPreview && verticalContent
const { pathname, search } = useLocation ()
const rootRef = useRef<HTMLElement | null> (null)
const animationMode = behaviourSettings.animation ?? 'normal'
const layoutTransition = clientAnimationTransition (
animationMode,
{ normal: { duration: .2, ease: 'easeOut' as const } })
useEffect (() => {
if (horizontal && rootRef.current != null)
rootRef.current.scrollLeft = 0
}, [horizontal, pathname, search])
useEffect (() => {
const root = rootRef.current
if (!horizontal || root == null)
return
const handleWheel = (event: WheelEvent) => {
if (event.ctrlKey || event.metaKey || event.shiftKey
|| event.deltaX !== 0 || event.deltaY === 0 || event.defaultPrevented)
return
// Nested tables, forms and code blocks retain their own scrolling.
let node = event.target instanceof HTMLElement ? event.target : null
while (node != null && node !== root)
{
if (node.matches ('input, select, textarea, [contenteditable="true"]'))
return
const style = getComputedStyle (node)
if ((/auto|scroll/.test (style.overflowY) && node.scrollHeight > node.clientHeight)
|| (/auto|scroll/.test (style.overflowX) && node.scrollWidth > node.clientWidth))
return
node = node.parentElement
}
const unit = event.deltaMode === 1 ? 16 : (event.deltaMode === 2 ? root.clientWidth : 1)
const delta = event.deltaY * unit
if ((delta < 0 && root.scrollLeft <= 0)
|| (delta > 0 && root.scrollLeft + root.clientWidth >= root.scrollWidth))
return
event.preventDefault ()
root.scrollLeft += delta
}
root.addEventListener ('wheel', handleWheel, { passive: false })
return () => root.removeEventListener ('wheel', handleWheel)
}, [horizontal])
{ normal: { duration: .2, ease: 'easeOut' as const } },
)
return (
<motion.main
ref={rootRef}
data-vertical-content={horizontal || undefined}
tabIndex={horizontal ? 0 : undefined}
transition={{ layout: layoutTransition }}
className={cn ('main-area flex-1 overflow-y-auto p-4', className)}
className={cn ('flex-1 overflow-y-auto p-4', className)}
layout={(animationMode === 'off' || !animateLayout) ? false : 'position'}>
{children}
</motion.main>)
+103 -126
ファイルの表示
@@ -1,14 +1,13 @@
import { motion } from 'framer-motion'
import { useCallback, useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { clientAnimationTransition } from '@/lib/clientAnimation'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { cn } from '@/lib/utils'
import { useUITranslation } from '@/lib/useUITranslation'
import { useVerticalPreview } from '@/lib/useLocale'
import type { CSSProperties, FC, MouseEvent, PointerEvent, ReactNode } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
type SidebarSide = 'left' | 'right'
@@ -24,12 +23,12 @@ type Props = {
type ResizeState = {
pointerId: number
startSize: number
startWidth: number
startX: number
startY: number
verticalPreview: boolean
side: SidebarSide }
type PendingPressState = ResizeState & { startY: number }
const DEFAULT_SIDEBAR_WIDTH = 256
const MIN_SIDEBAR_WIDTH = 192
const MIN_MAIN_WIDTH = 360
@@ -38,97 +37,84 @@ const LONG_PRESS_DELAY_MS = 350
const TOUCH_MOVE_THRESHOLD_PX = 8
const storageKeyForSidebar = (
sidebarKey: string,
side: SidebarSide,
verticalPreview = false,
): string =>
`sidebar:${ sidebarKey }:${ side }:${ verticalPreview ? 'height' : 'width' }`
const storageKeyForSidebar = (sidebarKey: string, side: SidebarSide): string =>
`sidebar:${ sidebarKey }:${ side }:width`
const getSidebarMaxWidth = (containerSize: number): number => {
const getSidebarMaxWidth = (containerWidth: number): number => {
const viewportWidth =
typeof window === 'undefined' ? containerSize : window.innerWidth
typeof window === 'undefined' ? containerWidth : window.innerWidth
const viewportMaxWidth = Math.floor (viewportWidth * MAX_VIEWPORT_WIDTH_RATIO)
const containerMaxWidth = Math.floor (containerSize - MIN_MAIN_WIDTH)
const containerMaxWidth = Math.floor (containerWidth - MIN_MAIN_WIDTH)
const dynamicMaxWidth = Math.min (viewportMaxWidth, containerMaxWidth)
return Math.max (MIN_SIDEBAR_WIDTH, dynamicMaxWidth)
}
const readStoredSidebarSize = (
const readStoredSidebarWidth = (
sidebarKey: string,
side: SidebarSide,
verticalPreview: boolean,
): number => {
side: SidebarSide): number => {
if (typeof window === 'undefined')
return DEFAULT_SIDEBAR_WIDTH
try
{
const value = window.localStorage.getItem (
storageKeyForSidebar (sidebarKey, side, verticalPreview))
const size = value == null ? Number.NaN : Number (value)
return Number.isFinite (size) ? size : DEFAULT_SIDEBAR_WIDTH
}
catch
{
return DEFAULT_SIDEBAR_WIDTH
}
const value = window.localStorage.getItem (storageKeyForSidebar (sidebarKey, side))
const width = value == null ? Number.NaN : Number (value)
return Number.isFinite (width) ? width : DEFAULT_SIDEBAR_WIDTH
}
const SidebarComponent: FC<Props> = (
{ children, className, maxWidth, maxWidthRatio, animateLayout = true,
onWidthChange, sidebarKey, side = 'left' },
) => {
const SidebarComponent: FC<Props> = ({
children,
className,
maxWidth,
maxWidthRatio,
animateLayout = true,
onWidthChange,
sidebarKey,
side = 'left',
}) => {
const { message } = useUITranslation ('shared')
const behaviourSettings = useClientBehaviourSettings ()
const verticalPreview = useVerticalPreview ()
const animationMode = behaviourSettings.animation ?? 'normal'
const layoutTransition = clientAnimationTransition (
animationMode,
{ normal: { duration: .2, ease: 'easeOut' as const } })
{ normal: { duration: .2, ease: 'easeOut' as const } },
)
const rootRef = useRef<HTMLDivElement | null> (null)
const maxWidthRef = useRef (maxWidth)
const maxWidthRatioRef = useRef (maxWidthRatio)
const getContainerSize = useCallback (() => {
const container = rootRef.current?.closest ('[data-sidebar-container]')
return verticalPreview
? container?.clientHeight ?? window.innerHeight
: container?.clientWidth ?? window.innerWidth
}, [verticalPreview])
const getClampedSize = useCallback ((
nextSize: number,
containerSize: number,
): number => {
if (verticalPreview)
{
const maximum = Math.max (64, Math.min (
window.innerHeight * .6, containerSize - 160))
return Math.min (maximum, Math.max (64, nextSize))
}
const getContainerWidth = useCallback (() => {
const containerWidth = rootRef.current
?.closest ('[data-sidebar-container]')
?.clientWidth
?? window.innerWidth
return containerWidth
}, [])
const getClampedWidth = useCallback ((
nextWidth: number,
containerWidth: number): number => {
const viewportWidth =
typeof window === 'undefined' ? containerSize : window.innerWidth
typeof window === 'undefined' ? containerWidth : window.innerWidth
const viewportMaxWidth = Math.floor (viewportWidth * MAX_VIEWPORT_WIDTH_RATIO)
const ratioMaxWidth = maxWidthRatioRef.current == null
? Infinity : Math.floor (containerSize * maxWidthRatioRef.current)
? Infinity : Math.floor (containerWidth * maxWidthRatioRef.current)
const configuredMaxWidth = maxWidthRef.current == null
? getSidebarMaxWidth (containerSize)
? getSidebarMaxWidth (containerWidth)
: Math.max (MIN_SIDEBAR_WIDTH,
Math.min (viewportMaxWidth, maxWidthRef.current))
const maxAllowedWidth = Math.max (MIN_SIDEBAR_WIDTH,
Math.min (ratioMaxWidth, configuredMaxWidth))
return Math.min (maxAllowedWidth, Math.max (MIN_SIDEBAR_WIDTH, nextSize))
}, [verticalPreview])
return Math.min (maxAllowedWidth, Math.max (MIN_SIDEBAR_WIDTH, nextWidth))
}, [])
const [size, setSize] = useState (DEFAULT_SIDEBAR_WIDTH)
const [width, setWidth] = useState (DEFAULT_SIDEBAR_WIDTH)
const [resizing, setResizing] = useState (false)
const sizeRef = useRef (size)
const widthRef = useRef (width)
const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null> (null)
const pendingPressRef = useRef<ResizeState | null> (null)
const pendingPressRef = useRef<PendingPressState | null> (null)
const resizeRef = useRef<ResizeState | null> (null)
const bodyStyleRef = useRef<{ cursor: string, userSelect: string } | null> (null)
@@ -162,7 +148,7 @@ const SidebarComponent: FC<Props> = (
userSelect: document.body.style.userSelect }
}
document.body.style.cursor = state.verticalPreview ? 'row-resize' : 'col-resize'
document.body.style.cursor = 'col-resize'
document.body.style.userSelect = 'none'
}, [clearLongPressTimer])
@@ -173,50 +159,43 @@ const SidebarComponent: FC<Props> = (
const state = resizeRef.current
resizeRef.current = null
try
{
if (state != null)
window.localStorage.setItem (
storageKeyForSidebar (sidebarKey, state.side, state.verticalPreview),
String (sizeRef.current))
}
catch
{
// Resizing remains usable when browser storage is unavailable.
}
if (state != null)
window.localStorage.setItem (
storageKeyForSidebar (sidebarKey, state.side),
String (widthRef.current))
setResizing (false)
restoreBodyStyle ()
}, [clearLongPressTimer, restoreBodyStyle, sidebarKey])
const applySize = useCallback ((nextSize: number) => {
if (nextSize === sizeRef.current)
const applyWidth = useCallback ((nextWidth: number) => {
if (nextWidth === widthRef.current)
return
sizeRef.current = nextSize
setSize (nextSize)
widthRef.current = nextWidth
setWidth (nextWidth)
}, [])
useEffect (() => {
maxWidthRef.current = maxWidth
maxWidthRatioRef.current = maxWidthRatio
const containerSize = getContainerSize ()
const nextSize = getClampedSize (sizeRef.current, containerSize)
applySize (nextSize)
}, [applySize, getClampedSize, getContainerSize, maxWidth, maxWidthRatio])
const containerWidth = getContainerWidth ()
const nextWidth = getClampedWidth (widthRef.current, containerWidth)
applyWidth (nextWidth)
}, [applyWidth, getClampedWidth, getContainerWidth, maxWidth, maxWidthRatio])
const updateSize = useCallback ((clientX: number, clientY: number) => {
const updateWidth = useCallback ((clientX: number) => {
const state = resizeRef.current
if (state == null)
return
const containerSize = getContainerSize ()
const delta = state.verticalPreview
? clientY - state.startY
: (state.side === 'left' ? clientX - state.startX : state.startX - clientX)
const nextSize = getClampedSize (state.startSize + delta, containerSize)
applySize (nextSize)
}, [applySize, getClampedSize, getContainerSize])
const containerWidth = getContainerWidth ()
const delta = state.side === 'left'
? clientX - state.startX
: state.startX - clientX
const nextWidth = getClampedWidth (state.startWidth + delta, containerWidth)
applyWidth (nextWidth)
}, [applyWidth, getClampedWidth, getContainerWidth])
const handlePointerDown = (ev: PointerEvent<HTMLDivElement>) => {
if (ev.pointerType !== 'mouse' && ev.pointerType !== 'touch')
@@ -232,10 +211,8 @@ const SidebarComponent: FC<Props> = (
const state = {
pointerId: ev.pointerId,
side,
startSize: sizeRef.current,
startX: ev.clientX,
startY: ev.clientY,
verticalPreview }
startWidth: widthRef.current,
startX: ev.clientX }
if (ev.pointerType === 'mouse')
{
@@ -243,7 +220,7 @@ const SidebarComponent: FC<Props> = (
return
}
pendingPressRef.current = state
pendingPressRef.current = { ...state, startY: ev.clientY }
clearLongPressTimer ()
longPressTimerRef.current = setTimeout (() => {
if (pendingPressRef.current != null)
@@ -257,7 +234,7 @@ const SidebarComponent: FC<Props> = (
if (resizeRef.current != null)
{
ev.preventDefault ()
updateSize (ev.clientX, ev.clientY)
updateWidth (ev.clientX)
return
}
@@ -285,58 +262,58 @@ const SidebarComponent: FC<Props> = (
finishResize ()
}
const resetSize = useCallback (() => {
const containerSize = getContainerSize ()
const nextSize = getClampedSize (DEFAULT_SIDEBAR_WIDTH, containerSize)
applySize (nextSize)
window.localStorage.removeItem (
storageKeyForSidebar (sidebarKey, side, verticalPreview))
}, [applySize, getClampedSize, getContainerSize, sidebarKey, side, verticalPreview])
const resetWidth = useCallback (() => {
const containerWidth = getContainerWidth ()
const nextWidth = getClampedWidth (DEFAULT_SIDEBAR_WIDTH, containerWidth)
applyWidth (nextWidth)
window.localStorage.removeItem (storageKeyForSidebar (sidebarKey, side))
}, [applyWidth, getClampedWidth, getContainerWidth, sidebarKey, side])
const handleDoubleClick = (ev: MouseEvent<HTMLDivElement>) => {
ev.preventDefault ()
ev.stopPropagation ()
resetSize ()
resetWidth ()
}
useEffect (() => {
const containerSize = getContainerSize ()
const nextSize = getClampedSize (
readStoredSidebarSize (sidebarKey, side, verticalPreview),
containerSize)
applySize (nextSize)
}, [applySize, getClampedSize, getContainerSize, sidebarKey, side, verticalPreview])
const containerWidth = getContainerWidth ()
const nextWidth = getClampedWidth (
readStoredSidebarWidth (sidebarKey, side),
containerWidth)
applyWidth (nextWidth)
}, [applyWidth, getClampedWidth, getContainerWidth, sidebarKey, side])
useEffect (() => {
const containerSize = getContainerSize ()
const nextSize = getClampedSize (sizeRef.current, containerSize)
applySize (nextSize)
}, [applySize, getClampedSize, getContainerSize])
const containerWidth = getContainerWidth ()
const nextWidth = getClampedWidth (widthRef.current, containerWidth)
applyWidth (nextWidth)
}, [applyWidth, getClampedWidth, getContainerWidth])
useEffect (() => {
if (typeof window === 'undefined')
return
const handleResize = () => {
const containerSize = getContainerSize ()
const nextSize = getClampedSize (sizeRef.current, containerSize)
applySize (nextSize)
const containerWidth = getContainerWidth ()
const nextWidth = getClampedWidth (widthRef.current, containerWidth)
applyWidth (nextWidth)
}
window.addEventListener ('resize', handleResize)
return () => window.removeEventListener ('resize', handleResize)
}, [applySize, getClampedSize, getContainerSize])
}, [applyWidth, getClampedWidth, getContainerWidth])
useEffect (() => {
if (!verticalPreview)
onWidthChange?.(size)
}, [onWidthChange, size, verticalPreview])
onWidthChange?.(width)
}, [onWidthChange, width])
useEffect (() => () => finishResize (), [finishResize, verticalPreview])
useEffect (() => () => {
clearLongPressTimer ()
restoreBodyStyle ()
}, [clearLongPressTimer, restoreBodyStyle])
const style = {
'--sidebar-width': `${ size }px`,
'--sidebar-height': `${ size }px` } as CSSProperties
'--sidebar-width': `${ width }px` } as CSSProperties
return (
<motion.div
@@ -345,7 +322,7 @@ const SidebarComponent: FC<Props> = (
transition={{ layout: layoutTransition }}
style={style}
className={cn (
'sidebar-panel relative w-full md:w-[var(--sidebar-width)] md:shrink-0 md:h-full',
'relative w-full md:w-[var(--sidebar-width)] md:shrink-0 md:h-full',
className)}>
<Helmet>
<style>
@@ -368,16 +345,16 @@ const SidebarComponent: FC<Props> = (
<div
role="separator"
aria-orientation={verticalPreview ? 'horizontal' : 'vertical'}
aria-label={message (verticalPreview ? 'labels.menu' : 'sidebarWidth').text}
lang={message (verticalPreview ? 'labels.menu' : 'sidebarWidth').language}
aria-orientation="vertical"
aria-label={message ('sidebarWidth').text}
lang={message ('sidebarWidth').language}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerEnd}
onPointerCancel={handlePointerEnd}
onDoubleClick={handleDoubleClick}
className={cn (
'sidebar-resize hidden md:block absolute top-0 bottom-0 z-20 w-4 cursor-col-resize',
'hidden md:block absolute top-0 bottom-0 z-20 w-4 cursor-col-resize',
'touch-none select-none',
side === 'left' ? '-right-2' : '-left-2',
'before:absolute before:top-0 before:bottom-0 before:w-px',
+3 -7
ファイルの表示
@@ -9,8 +9,7 @@ import { createStaticHandler, createStaticRouter,
import { AppFrame, appRoutes, resolvePageRoute } from '@/App'
import { apiGet, isApiError } from '@/lib/api'
import { createUII18n } from '@/lib/i18n'
import { fetchLocales, isLocaleCode, isVerticalPreviewPath,
legacyLocalePath } from '@/lib/locales'
import { fetchLocales, isLocaleCode, legacyLocalePath } from '@/lib/locales'
import { localesKeys, sessionKeys } from '@/lib/queryKeys'
import { withRenderContext } from '@/lib/renderContext.server'
@@ -73,15 +72,13 @@ export const render = async (
pathname => resolvePageRoute (pathname) != null)
const pathname = legacy == null ? url.pathname : new URL (legacy, origin).pathname
const segment = pathname.split ('/')[1]
const verticalPreview = isVerticalPreviewPath (pathname)
const known = resolvePageRoute (pathname)
if (legacy != null && known != null)
return { status: 308, location: legacy }
const locale = isLocaleCode (segment) ? segment : 'ja'
const routePrefix = verticalPreview ? 'mn-Mong' : locale
if (known?.route.handle?.redirectTo != null)
return { status: 308,
location: `/${routePrefix}${known.route.handle.redirectTo}${url.search}` }
location: `/${locale}${known.route.handle.redirectTo}${url.search}` }
const requestContext: RenderContext & { status: number } = {
pathname: url.pathname, origin, apiBaseURL,
...authentication, status: 200 }
@@ -109,8 +106,7 @@ export const render = async (
}
}
const prepare = known?.route.handle?.prepare
const routeLocaleValid = isLocaleCode (segment) || verticalPreview
let status = known == null || !active || !routeLocaleValid ? 404 : 200
let status = known == null || !active || !isLocaleCode (segment) ? 404 : 200
if (status === 200 && known?.route.handle?.authentication != null
&& client.getQueryData (sessionKeys.current) == null)
status = 401
-500
ファイルの表示
@@ -625,503 +625,3 @@ body
white-space: nowrap;
will-change: transform;
}
/* Loaded only by the development-only sample, with the OFL alongside the font. */
@font-face
{
font-family: 'BTRC Mongolian Preview';
src: url('/fonts/noto-sans-mongolian/NotoSansMongolian-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}
/* Development-only /mn-Mong layout. Unannotated pages retain horizontal text. */
[data-vertical-preview]
{
min-width: 0;
min-height: 0;
overflow: hidden;
}
[data-vertical-preview] > :not(.top-nav-layout)
{
flex: 1;
min-width: 0;
min-height: 0;
overflow: auto;
}
[data-vertical-preview] [data-sidebar-container]
{
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
overflow: hidden;
}
[data-vertical-preview] .main-area
{
min-width: 0;
min-height: 0;
overscroll-behavior: contain;
}
[data-vertical-preview] .main-area[data-vertical-content]
{
overflow-x: auto;
overflow-y: hidden;
overscroll-behavior-x: contain;
}
[data-vertical-preview] .sidebar-panel
{
width: 100%;
height: var(--sidebar-height);
min-height: 0;
flex: none;
}
[data-vertical-preview] .sidebar
{
display: flex;
align-items: flex-start;
gap: 1rem;
direction: ltr;
overflow-x: auto;
overflow-y: hidden;
overscroll-behavior: contain;
}
[data-vertical-preview] .sidebar > *
{
flex-shrink: 0;
}
[data-vertical-preview] .sidebar > form
{
width: 16rem;
height: 100%;
overflow-y: auto;
}
[data-vertical-preview] .sidebar > form > ul
{
position: relative;
}
[data-vertical-preview] .sidebar > div
{
writing-mode: vertical-lr;
text-orientation: mixed;
height: 100%;
margin: 0;
}
[data-vertical-preview] .sidebar :is(h2, h3, ul)
{
margin: 0;
margin-block-end: 1rem;
}
[data-vertical-preview] .sidebar-resize
{
display: block;
inset: auto 0 -.5rem;
width: 100%;
height: 1rem;
cursor: row-resize;
}
[data-vertical-preview] .sidebar-resize::before
{
inset: 50% 0 auto;
width: 100%;
height: 1px;
}
[data-vertical-preview] .vertical-preview-sample
{
writing-mode: vertical-lr;
text-orientation: mixed;
font-family: 'BTRC Mongolian Preview', 'Mongolian Baiti', sans-serif;
font-size: 1.25rem;
line-height: 1.5;
flex-shrink: 0;
}
/* The post grid fills each column downwards before continuing to the right. */
[data-vertical-preview] .posts-main-area > div
{
display: flex;
height: 100%;
width: max-content;
margin: 0;
gap: 1rem;
}
[data-vertical-preview] .posts-main-area > div > div:first-child
{
flex-direction: column;
justify-content: flex-start;
writing-mode: vertical-lr;
text-orientation: mixed;
}
[data-vertical-preview] .posts-main-area > div > div:last-child
{
display: flex;
width: max-content;
height: 100%;
margin: 0;
gap: 1rem;
}
[data-vertical-preview] .main-area[data-vertical-content] .post-list
{
display: grid;
grid-auto-flow: column;
grid-auto-columns: 10rem;
grid-template-rows: repeat(auto-fill, min(10rem, 100%));
flex: none;
width: max-content;
height: 100%;
writing-mode: horizontal-tb;
}
[data-vertical-preview] .main-area[data-vertical-content] .post-list > a
{
height: 100%;
}
[data-vertical-preview] .main-area[data-vertical-content] nav
{
flex: none;
align-self: flex-start;
margin: 0;
writing-mode: horizontal-tb;
}
[data-vertical-preview] .tags-main-area
{
display: flex;
align-items: stretch;
gap: 1.5rem;
}
[data-vertical-preview] .tag-list-filter
{
flex: none;
width: min(24rem, 75vw);
max-width: none;
overflow-y: auto;
}
[data-vertical-preview] .tag-list-filter h1
{
writing-mode: vertical-lr;
text-orientation: mixed;
max-height: 8rem;
margin-block: 0;
}
[data-vertical-preview] .tag-list-filter input[type='number']
{
max-width: 7rem;
}
[data-vertical-preview] .tag-list-results
{
display: flex;
flex: none;
gap: 1rem;
max-width: none;
margin: 0;
}
[data-vertical-preview] .tag-list-table
{
max-width: none;
overflow-x: visible;
overflow-y: auto;
}
[data-vertical-preview] .tag-list-table table
{
writing-mode: vertical-lr;
text-orientation: mixed;
inline-size: max-content;
block-size: max-content;
min-width: 0;
}
[data-vertical-preview] .tag-list-table col
{
width: auto;
}
[data-vertical-preview] .tag-list-table :is(th, td)
{
min-inline-size: 2rem;
max-inline-size: 16rem;
text-align: start;
}
[data-vertical-preview] .tag-list-table thead
{
border-bottom: 0;
border-block-end: 2px solid currentColor;
}
[data-vertical-preview] .tag-list-table td:nth-child(2),
[data-vertical-preview] .tag-list-table time
{
writing-mode: horizontal-tb;
white-space: nowrap;
}
[data-vertical-preview] :is(.sidebar, .tag-list-table, .wiki-main-area) .tag-marquee,
[data-vertical-preview] :is(.sidebar, .tag-list-table, .wiki-main-area) .tag-marquee__static
{
white-space: normal;
overflow: visible;
text-overflow: clip;
max-inline-size: 16rem;
opacity: 1;
}
[data-vertical-preview] :is(.sidebar, .tag-list-table, .wiki-main-area) .tag-marquee__animated
{
display: none;
}
/* Prose flows through the viewport height and grows into columns on the right. */
[data-vertical-preview] .wiki-main-area
{
display: flex;
gap: 1rem;
}
[data-vertical-preview] .wiki-main-area > div
{
flex: none;
writing-mode: vertical-lr;
text-orientation: mixed;
margin: 0;
}
[data-vertical-preview] .wiki-main-area > div > span
{
writing-mode: horizontal-tb;
}
[data-vertical-preview] .wiki-main-area article,
[data-vertical-preview] .posts-main-area .localised-document
{
writing-mode: vertical-lr;
text-orientation: mixed;
inline-size: 100%;
block-size: max-content;
max-width: none;
margin: 0;
flex: none;
}
[data-vertical-preview] .main-area[data-vertical-content] .localised-document
{
max-width: none;
overflow: visible;
}
[data-vertical-preview] .wiki-main-area article :is(h1, h2, h3, h4, p, ul, ol, blockquote)
{
margin: 0;
margin-block: 1em;
}
[data-vertical-preview] .wiki-main-area article :is(ul, ol)
{
padding: 0;
padding-inline-start: 1.5em;
}
[data-vertical-preview] .wiki-main-area article :is(img, video, iframe, svg)
{
writing-mode: horizontal-tb;
max-height: 100%;
max-width: none;
object-fit: contain;
}
[data-vertical-preview] .wiki-main-area article :is(pre, table, .not-prose),
[data-vertical-preview] .main-area[data-vertical-content] :is(code, time, input, select, textarea)
{
writing-mode: horizontal-tb;
text-orientation: mixed;
}
[data-vertical-preview] .wiki-main-area article :is(pre, table, .not-prose)
{
display: inline-block;
max-height: 100%;
max-width: min(48rem, 80vw);
overflow: auto;
vertical-align: top;
}
[data-vertical-preview] .wiki-main-area article .not-prose
{
width: max-content;
height: 100%;
}
[data-vertical-preview] .wiki-main-area .not-prose > div
{
display: flex;
flex-direction: column;
height: 100%;
margin: 0;
}
[data-vertical-preview] .wiki-main-area .not-prose > div > div:last-child
{
flex: 1;
min-height: 0;
}
@media (min-width: 768px)
{
[data-vertical-preview].app-layout
{
flex-direction: row;
}
[data-vertical-preview] .top-nav-layout
{
display: flex;
width: max-content;
max-width: 50vw;
height: 100%;
}
[data-vertical-preview] .top-nav-bar
{
flex-direction: column;
flex: none;
width: 7rem;
height: 100%;
padding: .5rem;
gap: .5rem;
}
[data-vertical-preview] .top-nav-primary
{
flex-direction: column;
flex: 1;
min-height: 0;
width: 100%;
height: auto;
overflow-y: auto;
}
[data-vertical-preview] .top-nav-brand-link
{
writing-mode: vertical-lr;
text-orientation: mixed;
max-height: 9rem;
white-space: normal;
margin-block: .5rem;
}
[data-vertical-preview] .top-nav-desktop-menu
{
flex-direction: column;
width: 100%;
height: auto;
}
[data-vertical-preview] .top-nav-desktop-menu > div:not(.top-nav-highlight)
{
width: 100%;
}
[data-vertical-preview] .top-nav-desktop-menu .top-nav-menu-link
{
justify-content: center;
width: 100%;
min-height: 3rem;
height: auto;
padding: .5rem;
}
[data-vertical-preview] .top-nav-menu-link > :not(svg, select),
[data-vertical-preview] .top-nav-more-menu h2
{
writing-mode: vertical-lr;
text-orientation: mixed;
max-height: 9rem;
white-space: normal;
}
[data-vertical-preview] .top-nav-highlight
{
inset-inline-start: 0;
}
[data-vertical-preview] .top-nav-account
{
flex-direction: column;
width: 100%;
margin: 0;
}
[data-vertical-preview] .top-nav-account .top-nav-user-desktop
{
max-width: 100%;
margin: 0;
padding: 0;
}
[data-vertical-preview] .top-nav-locale-name
{
display: inline;
writing-mode: horizontal-tb;
}
[data-vertical-preview] .top-nav-desktop-submenu
{
min-width: 0;
height: 100%;
}
[data-vertical-preview] .top-nav-submenu-content
{
width: max-content;
max-width: 35vw;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
}
[data-vertical-preview] :is(.top-nav-submenu-items, .top-nav-more-links)
{
flex-direction: column;
flex-wrap: nowrap;
align-items: stretch;
}
[data-vertical-preview] .top-nav-submenu-items .top-nav-menu-link
{
justify-content: center;
}
}
@media (max-width: 767px)
{
[data-vertical-preview] .top-nav-layout
{
max-height: 50dvh;
overflow-y: auto;
}
}
+5 -13
ファイルの表示
@@ -28,11 +28,6 @@ export const localeFromPath = (pathname: string): LocaleCode | null => {
}
// A development-only layout route; API and dictionary locales stay unchanged.
export const isVerticalPreviewPath = (pathname: string): boolean =>
import.meta.env.DEV && /^\/mn-Mong(?:[/?#]|$)/.test (pathname)
export const resolveViewingLocale = (
pathname: string,
routeLocale?: unknown,
@@ -45,22 +40,20 @@ export const currentViewingLocale = (): LocaleCode =>
export const unlocalisedPath = (pathname: string): string => {
const locale = isVerticalPreviewPath (pathname) ? 'mn-Mong' : localeFromPath (pathname)
const locale = localeFromPath (pathname)
return locale == null ? pathname : pathname.slice (locale.length + 1) || '/'
}
export const localiseTo = (to: To, locale: LocaleCode, viewingPath = ''): To => {
export const localiseTo = (to: To, locale: LocaleCode): To => {
const pathname = typeof to === 'string' ? to : to.pathname
if (pathname == null
|| !(pathname.startsWith ('/'))
|| pathname.startsWith ('//')
|| localeFromPath (pathname) != null
|| isVerticalPreviewPath (pathname))
|| localeFromPath (pathname) != null)
return to
const prefix = isVerticalPreviewPath (viewingPath) ? 'mn-Mong' : locale
const localised = `/${ prefix }${ pathname }`
const localised = `/${ locale }${ pathname }`
return typeof to === 'string' ? localised : { ...to, pathname: localised }
}
@@ -80,8 +73,7 @@ export const legacyLocalePath = (
location: { pathname: string; search: string; hash: string },
isPage: (pathname: string) => boolean = () => false,
): string | null => {
if (localeFromPath (location.pathname) != null
|| isVerticalPreviewPath (location.pathname) || !isPage (location.pathname))
if (localeFromPath (location.pathname) != null || !isPage (location.pathname))
return null
return switchLocalePath (location, automaticallySelectedLocale ())
}
+3 -8
ファイルの表示
@@ -1,7 +1,7 @@
import { useCallback } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { isVerticalPreviewPath, localiseTo, resolveViewingLocale } from '@/lib/locales'
import { localiseTo, resolveViewingLocale } from '@/lib/locales'
import type { NavigateFunction, NavigateOptions, To } from 'react-router-dom'
@@ -18,15 +18,10 @@ export const useLocale = (): LocaleCode => {
export const useLocaleNavigate = (): NavigateFunction => {
const navigate = useNavigate ()
const locale = useLocale ()
const { pathname } = useLocation ()
return useCallback ((to: To | number, options?: NavigateOptions) => {
if (typeof to === 'number')
navigate (to)
else
navigate (localiseTo (to, locale, pathname), options)
}, [locale, navigate, pathname])
navigate (localiseTo (to, locale), options)
}, [locale, navigate])
}
export const useVerticalPreview = (): boolean =>
isVerticalPreviewPath (useLocation ().pathname)
+1 -1
ファイルの表示
@@ -146,7 +146,7 @@ const PostListPage: FC = () => {
JSON.stringify (statesToSave))
}}/>
<MainArea className="posts-main-area" verticalContent>
<MainArea>
<TabGroup
rightAddon={
<label className="flex items-center gap-2 text-xs text-muted-foreground">
+4 -4
ファイルの表示
@@ -220,10 +220,10 @@ const TagListPage: FC = () => {
}), [page, totalPages, location.search]))
return (
<MainArea className="tags-main-area min-w-0" verticalContent>
<MainArea className="min-w-0">
<PageHead title={message ('title')}/>
<div className="tag-list-filter max-w-xl">
<div className="max-w-xl">
<div className="flex flex-wrap items-center justify-between gap-3">
<PageTitle>{label ('title')}</PageTitle>
<label className="flex items-center gap-2 text-xs text-muted-foreground">
@@ -358,8 +358,8 @@ const TagListPage: FC = () => {
{isError && <p role="alert">{label ('common:fetchError')}</p>}
{loading ? label ('common:loading') : (results.length > 0 ? (
<div className="tag-list-results mt-4 min-w-0 max-w-full">
<div className="tag-list-table min-w-0 max-w-full overflow-x-auto">
<div className="mt-4 min-w-0 max-w-full">
<div className="min-w-0 max-w-full overflow-x-auto">
<table className="w-full min-w-[2000px] table-auto border-collapse">
<colgroup>
<col className="w-72"/>
+1 -1
ファイルの表示
@@ -114,7 +114,7 @@ const WikiDetailPage: FC = () => {
return <NotFound/>
return (
<MainArea className="wiki-main-area" verticalContent>
<MainArea>
<PageHead title={{
text: `${displayTitle}${deprecated
? message ('labels.deprecatedSuffix').text : ''} Wiki`,