コミットを比較

..

1 コミット

作成者 SHA1 メッセージ 日付
みてるぞ d6670bb65e #383 2026-06-29 01:56:36 +09:00
14個のファイルの変更29行の追加483行の削除
+3 -4
ファイルの表示
@@ -140,11 +140,10 @@ class PostsController < ApplicationController
original_created_from = params[:original_created_from] original_created_from = params[:original_created_from]
original_created_before = params[:original_created_before] original_created_before = params[:original_created_before]
parent_post_ids = parse_parent_post_ids parent_post_ids = parse_parent_post_ids
resized_thumbnail = thumbnail.present? ? Post.resized_thumbnail_attachment(thumbnail) : nil
post = Post.new(title:, url:, thumbnail_base: nil, uploaded_user: current_user, post = Post.new(title:, url:, thumbnail_base: nil, uploaded_user: current_user,
original_created_from:, original_created_before:) original_created_from:, original_created_before:)
post.thumbnail.attach(resized_thumbnail) if resized_thumbnail post.thumbnail.attach(thumbnail) if thumbnail.present?
ApplicationRecord.transaction do ApplicationRecord.transaction do
post.save! post.save!
@@ -157,6 +156,8 @@ class PostsController < ApplicationController
sync_parent_posts!(post, parent_post_ids) sync_parent_posts!(post, parent_post_ids)
post.resized_thumbnail!
PostVersionRecorder.record!(post:, event_type: :create, created_by_user: current_user) PostVersionRecorder.record!(post:, event_type: :create, created_by_user: current_user)
end end
@@ -166,8 +167,6 @@ class PostsController < ApplicationController
render_validation_error fields: { tags: 'ニコニコ・タグは直接指定できません.' } render_validation_error fields: { tags: 'ニコニコ・タグは直接指定できません.' }
rescue Tag::DeprecatedTagNormalisationError rescue Tag::DeprecatedTagNormalisationError
render_unprocessable_entity '廃止済みタグは付与できません.', field: :tags render_unprocessable_entity '廃止済みタグは付与できません.', field: :tags
rescue MiniMagick::Error
render_validation_error fields: { thumbnail: ['サムネイル画像の変換に失敗しました.'] }
rescue ArgumentError => e rescue ArgumentError => e
render_validation_error fields: { parent_post_ids: [e.message] } render_validation_error fields: { parent_post_ids: [e.message] }
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
+6 -15
ファイルの表示
@@ -1,19 +1,5 @@
class Post < ApplicationRecord class Post < ApplicationRecord
require 'mini_magick' require 'mini_magick'
require 'stringio'
def self.resized_thumbnail_attachment(upload)
upload.rewind
image = MiniMagick::Image.read(upload.read)
image.resize '180x180'
image.format 'jpg'
{ io: StringIO.new(image.to_blob),
filename: 'resized_thumbnail.jpg',
content_type: 'image/jpeg' }
ensure
upload.rewind
end
belongs_to :uploaded_user, class_name: 'User', optional: true belongs_to :uploaded_user, class_name: 'User', optional: true
@@ -101,7 +87,12 @@ class Post < ApplicationRecord
def resized_thumbnail! def resized_thumbnail!
return unless thumbnail.attached? return unless thumbnail.attached?
thumbnail.attach(self.class.resized_thumbnail_attachment(StringIO.new(thumbnail.download))) image = MiniMagick::Image.read(thumbnail.download)
image.resize '180x180'
thumbnail.purge
thumbnail.attach(io: File.open(image.path),
filename: 'resized_thumbnail.jpg',
content_type: 'image/jpeg')
end end
private private
-24
ファイルの表示
@@ -6,7 +6,6 @@ class TagImplication < ApplicationRecord
validates :parent_tag_id, presence: true validates :parent_tag_id, presence: true
validate :parent_tag_mustnt_be_itself validate :parent_tag_mustnt_be_itself
validate :parent_tag_mustnt_create_cycle
private private
@@ -15,27 +14,4 @@ class TagImplication < ApplicationRecord
errors.add :parent_tag_id, '親タグは子タグと同一であってはなりません.' errors.add :parent_tag_id, '親タグは子タグと同一であってはなりません.'
end end
end end
def parent_tag_mustnt_create_cycle
return if tag_id.blank? || parent_tag_id.blank?
return if errors[:parent_tag_id].present?
seen = { }
stack = [parent_tag_id]
until stack.empty?
current_id = stack.pop
next if seen[current_id]
seen[current_id] = true
if current_id == tag_id
errors.add :parent_tag_id, '親タグに子孫タグを指定すると循環します.'
errors.add :base, 'タグの親子関係が循環します.'
return
end
stack.concat(TagImplication.where(tag_id: current_id).pluck(:parent_tag_id))
end
end
end end
-36
ファイルの表示
@@ -1,36 +0,0 @@
require 'rails_helper'
RSpec.describe TagImplication, type: :model do
it 'rejects a parent tag that would create a cycle' do
child = create(:tag, name: 'tag_implication_cycle_child')
parent = create(:tag, name: 'tag_implication_cycle_parent')
described_class.create!(tag: child, parent_tag: parent)
implication = described_class.new(tag: parent, parent_tag: child)
expect(implication).not_to be_valid
expect(implication.errors[:parent_tag_id]).to include(
'親タグに子孫タグを指定すると循環します.'
)
expect(implication.errors[:base]).to be_present
end
it 'terminates even when existing data already contains a cycle' do
child = create(:tag, name: 'tag_implication_existing_cycle_child')
parent = create(:tag, name: 'tag_implication_existing_cycle_parent')
ancestor = create(:tag, name: 'tag_implication_existing_cycle_ancestor')
described_class.create!(tag: parent, parent_tag: ancestor)
described_class.insert_all!(
[
{ tag_id: ancestor.id, parent_tag_id: parent.id,
created_at: Time.current, updated_at: Time.current }
]
)
implication = described_class.new(tag: child, parent_tag: parent)
expect(implication).to be_valid
end
end
+1 -11
ファイルの表示
@@ -54,17 +54,7 @@ RSpec.describe Tag, type: :model do
first = create(:tag, name: 'expand_cycle_first') first = create(:tag, name: 'expand_cycle_first')
second = create(:tag, name: 'expand_cycle_second') second = create(:tag, name: 'expand_cycle_second')
TagImplication.create!(tag: first, parent_tag: second) TagImplication.create!(tag: first, parent_tag: second)
now = Time.current TagImplication.create!(tag: second, parent_tag: first)
TagImplication.insert_all!(
[
{
tag_id: second.id,
parent_tag_id: first.id,
created_at: now,
updated_at: now
}
]
)
expect(described_class.expand_parent_tags([first])).to contain_exactly(first, second) expect(described_class.expand_parent_tags([first])).to contain_exactly(first, second)
end end
-78
ファイルの表示
@@ -1,5 +1,4 @@
require 'rails_helper' require 'rails_helper'
require 'base64'
require 'set' require 'set'
include ActiveSupport::Testing::TimeHelpers include ActiveSupport::Testing::TimeHelpers
@@ -9,11 +8,6 @@ RSpec.describe 'Posts API', type: :request do
# resized_thumbnail! が MiniMagick 依存でコケやすいので request spec ではスタブしとくのが無難。 # resized_thumbnail! が MiniMagick 依存でコケやすいので request spec ではスタブしとくのが無難。
before do before do
allow_any_instance_of(Post).to receive(:resized_thumbnail!).and_return(true) allow_any_instance_of(Post).to receive(:resized_thumbnail!).and_return(true)
allow(Post).to receive(:resized_thumbnail_attachment).and_return(
io: StringIO.new('dummy'),
filename: 'resized_thumbnail.jpg',
content_type: 'image/jpeg'
)
end end
def create_nico_tag!(name) def create_nico_tag!(name)
@@ -25,18 +19,6 @@ RSpec.describe 'Posts API', type: :request do
Rack::Test::UploadedFile.new(StringIO.new('dummy'), 'image/jpeg', original_filename: 'dummy.jpg') Rack::Test::UploadedFile.new(StringIO.new('dummy'), 'image/jpeg', original_filename: 'dummy.jpg')
end end
def real_thumbnail_upload
gif =
Base64.decode64(
'R0lGODdhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs=')
Rack::Test::UploadedFile.new(
StringIO.new(gif),
'image/gif',
original_filename: 'thumbnail.gif'
)
end
def post_write_params params = { } def post_write_params params = { }
{ parent_post_ids: '' }.merge(params) { parent_post_ids: '' }.merge(params)
end end
@@ -714,66 +696,6 @@ RSpec.describe 'Posts API', type: :request do
expect(json['tags'][0]).to have_key('name') expect(json['tags'][0]).to have_key('name')
end end
it '201 when posting manually with a thumbnail' do
sign_in_as(member)
allow(Post).to receive(:resized_thumbnail_attachment).and_call_original
post '/posts', params: post_write_params(
title: 'thumbnail post',
url: 'https://example.com/thumbnail-post',
tags: 'spec_tag',
thumbnail: real_thumbnail_upload
)
expect(response).to have_http_status(:created)
post_record = Post.find(json.fetch('id'))
expect(post_record.thumbnail).to be_attached
expect(post_record.thumbnail.blob.content_type).to eq('image/jpeg')
expect { post_record.thumbnail.download }.not_to raise_error
end
it 'resizes the thumbnail before the create transaction begins' do
sign_in_as(member)
open_transactions = []
baseline_open_transactions = Post.connection.open_transactions
allow(Post).to receive(:resized_thumbnail_attachment) do |_upload|
open_transactions << Post.connection.open_transactions
{ io: StringIO.new('dummy'),
filename: 'resized_thumbnail.jpg',
content_type: 'image/jpeg' }
end
post '/posts', params: post_write_params(
title: 'transaction post',
url: 'https://example.com/transaction-post',
tags: 'spec_tag',
thumbnail: dummy_upload
)
expect(response).to have_http_status(:created)
expect(open_transactions).to eq([baseline_open_transactions])
end
it 'returns 422 and does not create a post when thumbnail resize fails' do
sign_in_as(member)
allow(Post).to receive(:resized_thumbnail_attachment).and_raise(MiniMagick::Error)
expect {
post '/posts', params: post_write_params(
title: 'broken thumbnail post',
url: 'https://example.com/broken-thumbnail-post',
tags: 'spec_tag',
thumbnail: dummy_upload
)
}.not_to change(Post, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(json.fetch('errors')).to include(
'thumbnail' => ['サムネイル画像の変換に失敗しました.']
)
end
it '201 and creates post + tags when member and tags have aliases' do it '201 and creates post + tags when member and tags have aliases' do
sign_in_as(member) sign_in_as(member)
-11
ファイルの表示
@@ -56,17 +56,6 @@ RSpec.describe "TagChildren", type: :request do
expect(response).to have_http_status(:no_content) expect(response).to have_http_status(:no_content)
end end
it 'returns 422 and does not create relation when the new link makes a cycle' do
TagImplication.create!(tag: parent, parent_tag: child)
expect {
do_request
}.not_to change(TagImplication, :count)
expect(response).to have_http_status(:unprocessable_entity)
expect(TagImplication.where(tag: child, parent_tag: parent)).not_to exist
end
end end
context "when Tag.find raises (invalid ids)" do context "when Tag.find raises (invalid ids)" do
+3 -11
ファイルの表示
@@ -797,17 +797,7 @@ RSpec.describe 'Tags API', type: :request do
) )
TagImplication.create!(tag: first, parent_tag: root_material) TagImplication.create!(tag: first, parent_tag: root_material)
TagImplication.create!(tag: second, parent_tag: first) TagImplication.create!(tag: second, parent_tag: first)
now = Time.current TagImplication.create!(tag: first, parent_tag: second)
TagImplication.insert_all!(
[
{
tag_id: first.id,
parent_tag_id: second.id,
created_at: now,
updated_at: now
}
]
)
get '/tags/with-depth', params: { parent: root_material.id } get '/tags/with-depth', params: { parent: root_material.id }
@@ -1374,6 +1364,8 @@ RSpec.describe 'Tags API', type: :request do
end end
it 'parent_tags に指定すると循環する tag は 422 にする' do it 'parent_tags に指定すると循環する tag は 422 にする' do
pending '#332 で対応予定'
child = Tag.create!( child = Tag.create!(
tag_name: TagName.create!(name: 'put_cycle_child'), tag_name: TagName.create!(name: 'put_cycle_child'),
category: :general category: :general
+2 -5
ファイルの表示
@@ -87,13 +87,10 @@ const DraggableDroppableTagRow: FC<Props> = ({ tag, nestLevel, pathKey, parentTa
setDropRef (node) setDropRef (node)
}} }}
style={style} style={style}
className={cn ( className={cn ('rounded select-none', over && 'ring-2 ring-offset-2')}
'min-w-0 max-w-full overflow-hidden rounded select-none',
over && 'ring-2 ring-offset-2')}
{...attributes} {...attributes}
{...listeners}> {...listeners}>
<motion.div <motion.div
className="flex min-w-0 max-w-full items-baseline overflow-hidden"
transition={{ layout: { duration: .2, ease: 'easeOut' } }} transition={{ layout: { duration: .2, ease: 'easeOut' } }}
layoutId={`tag-${ sp ? 'sp-' : '' }${ tag.id }`}> layoutId={`tag-${ sp ? 'sp-' : '' }${ tag.id }`}>
<TagLink tag={tag} nestLevel={nestLevel}/> <TagLink tag={tag} nestLevel={nestLevel}/>
@@ -101,4 +98,4 @@ const DraggableDroppableTagRow: FC<Props> = ({ tag, nestLevel, pathKey, parentTa
</div>) </div>)
} }
export default DraggableDroppableTagRow export default DraggableDroppableTagRow
-243
ファイルの表示
@@ -1,243 +0,0 @@
import { cn } from '@/lib/utils'
import { useEffect, useRef, useState } from 'react'
import type { FC } from 'react'
type Props = {
text: string
className?: string
title?: string }
const DESKTOP_MARQUEE_MEDIA =
'(min-width: 768px) and (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)'
const MARQUEE_START_DELAY_MS = 1600
const MARQUEE_END_HOLD_MS = 2400
const MARQUEE_SCROLL_PX_PER_SECOND = 43
const MIN_MARQUEE_OVERFLOW_PX = 1
const ResponsiveMarqueeText: FC<Props> = ({ text, className, title }) => {
const outerRef = useRef<HTMLSpanElement | null> (null)
const staticRef = useRef<HTMLSpanElement | null> (null)
const animatedRef = useRef<HTMLSpanElement | null> (null)
const animationRef = useRef<Animation | null> (null)
const timeoutRef = useRef<number | null> (null)
const rafRef = useRef<number | null> (null)
const [overflowPx, setOverflowPx] = useState (0)
const [desktopMarqueeEnabled, setDesktopMarqueeEnabled] = useState (false)
const [active, setActive] = useState (false)
const [marqueeVisible, setMarqueeVisible] = useState (false)
useEffect (() => {
const outer = outerRef.current
const inner = staticRef.current
if (!(outer) || !(inner))
return
const measure = () => {
const nextOverflow = Math.max (0, Math.ceil (inner.scrollWidth - outer.clientWidth))
setOverflowPx (prev => Math.abs (prev - nextOverflow) <= 1 ? prev : nextOverflow)
}
measure ()
const resizeObserver =
typeof ResizeObserver === 'undefined'
? null
: new ResizeObserver (() => {
measure ()
})
resizeObserver?.observe (outer)
resizeObserver?.observe (inner)
addEventListener ('resize', measure)
return () => {
resizeObserver?.disconnect ()
removeEventListener ('resize', measure)
}
}, [text])
useEffect (() => {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function')
return
const media = window.matchMedia (DESKTOP_MARQUEE_MEDIA)
const update = () => {
setDesktopMarqueeEnabled (media.matches)
}
update ()
media.addEventListener ('change', update)
return () => {
media.removeEventListener ('change', update)
}
}, [])
useEffect (() => {
const clearScheduled = () => {
if (timeoutRef.current != null)
{
clearTimeout (timeoutRef.current)
timeoutRef.current = null
}
if (rafRef.current != null)
{
cancelAnimationFrame (rafRef.current)
rafRef.current = null
}
animationRef.current?.cancel ()
animationRef.current = null
}
const animated = animatedRef.current
const canMarquee = (
active
&& desktopMarqueeEnabled
&& overflowPx >= MIN_MARQUEE_OVERFLOW_PX
&& animated != null)
const resetAnimated = () => {
const node = animatedRef.current
animationRef.current?.cancel ()
animationRef.current = null
node?.getAnimations?.().forEach (animation => {
animation.cancel ()
})
if (!(node))
return
node.style.transition = 'none'
node.style.transform = 'translateX(0)'
}
if (!canMarquee)
{
clearScheduled ()
resetAnimated ()
setMarqueeVisible (false)
return
}
let cancelled = false
const sleep = (ms: number) =>
new Promise<void> (resolve => {
timeoutRef.current = window.setTimeout (() => {
timeoutRef.current = null
resolve ()
}, ms)
})
const runLoop = async () => {
while (!cancelled)
{
resetAnimated ()
setMarqueeVisible (true)
await sleep (MARQUEE_START_DELAY_MS)
if (cancelled || !(animatedRef.current))
break
const moveDurationMs =
overflowPx / MARQUEE_SCROLL_PX_PER_SECOND * 1000
const node = animatedRef.current
if (typeof node.animate === 'function')
{
const animation = node.animate (
[
{ transform: 'translateX(0)' },
{ transform: `translateX(-${ overflowPx }px)` },
],
{ duration: moveDurationMs, easing: 'linear', fill: 'forwards' })
animationRef.current = animation
try
{
await animation.finished
}
catch
{
break
}
node.style.transform = `translateX(-${ overflowPx }px)`
animation.cancel ()
if (animationRef.current === animation)
animationRef.current = null
}
else
{
node.style.transition = `transform ${ moveDurationMs }ms linear`
await new Promise<void> (resolve => {
rafRef.current = requestAnimationFrame (() => {
rafRef.current = null
node.style.transform = `translateX(-${ overflowPx }px)`
resolve ()
})
})
await sleep (moveDurationMs)
node.style.transition = 'none'
node.style.transform = `translateX(-${ overflowPx }px)`
}
if (cancelled)
break
await sleep (MARQUEE_END_HOLD_MS)
if (cancelled)
break
resetAnimated ()
}
}
void runLoop ()
return () => {
cancelled = true
clearScheduled ()
resetAnimated ()
setMarqueeVisible (false)
}
}, [active, desktopMarqueeEnabled, overflowPx, text])
return (
<span
ref={outerRef}
title={title ?? text}
onMouseEnter={() => setActive (true)}
onMouseLeave={() => setActive (false)}
onFocus={() => setActive (true)}
onBlur={() => setActive (false)}
className={cn (
'tag-marquee inline-block max-w-full min-w-0 align-bottom',
'whitespace-normal [overflow-wrap:anywhere]',
'md:overflow-hidden md:whitespace-nowrap',
className)}>
<span
ref={staticRef}
className={cn (
'tag-marquee__static block max-w-full',
'whitespace-normal [overflow-wrap:anywhere]',
'md:overflow-hidden md:text-ellipsis md:whitespace-nowrap',
marqueeVisible && 'md:opacity-0')}>
{text}
</span>
{overflowPx >= MIN_MARQUEE_OVERFLOW_PX && (
<span
ref={animatedRef}
aria-hidden="true"
className={cn (
'tag-marquee__animated hidden md:block',
marqueeVisible ? 'md:opacity-100' : 'md:opacity-0')}>
{text}
</span>)}
</span>)
}
export default ResponsiveMarqueeText
+9 -20
ファイルの表示
@@ -1,5 +1,4 @@
import PrefetchLink from '@/components/PrefetchLink' import PrefetchLink from '@/components/PrefetchLink'
import ResponsiveMarqueeText from '@/components/ResponsiveMarqueeText'
import { LIGHT_COLOUR_SHADE, DARK_COLOUR_SHADE, TAG_COLOUR } from '@/consts' import { LIGHT_COLOUR_SHADE, DARK_COLOUR_SHADE, TAG_COLOUR } from '@/consts'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
@@ -34,7 +33,6 @@ const TagLink: FC<Props> = ({ tag,
withWiki = true, withWiki = true,
withCount = true, withCount = true,
className, className,
title,
...props }) => { ...props }) => {
const spanClass = cn ( const spanClass = cn (
`text-${ TAG_COLOUR[tag.category] }-${ LIGHT_COLOUR_SHADE }`, `text-${ TAG_COLOUR[tag.category] }-${ LIGHT_COLOUR_SHADE }`,
@@ -43,14 +41,11 @@ const TagLink: FC<Props> = ({ tag,
spanClass, spanClass,
`hover:text-${ TAG_COLOUR[tag.category] }-${ LIGHT_COLOUR_SHADE - 200 }`, `hover:text-${ TAG_COLOUR[tag.category] }-${ LIGHT_COLOUR_SHADE - 200 }`,
`dark:hover:text-${ TAG_COLOUR[tag.category] }-${ DARK_COLOUR_SHADE - 200 }`) `dark:hover:text-${ TAG_COLOUR[tag.category] }-${ DARK_COLOUR_SHADE - 200 }`)
const textClass = 'group inline-block min-w-0 max-w-full align-bottom'
const textTitle = title
?? (tag.matchedAlias == null ? tag.name : `${ tag.matchedAlias }${ tag.name }`)
return ( return (
<> <>
{(linkFlg && withWiki) && ( {(linkFlg && withWiki) && (
<span className="mr-1 shrink-0"> <span className="mr-1">
{(tag.materialId != null || tag.hasWiki || tag.hasDeerjikists) {(tag.materialId != null || tag.hasWiki || tag.hasDeerjikists)
? ( ? (
tag.materialId == null && !(tag.hasDeerjikists) tag.materialId == null && !(tag.hasDeerjikists)
@@ -105,17 +100,14 @@ const TagLink: FC<Props> = ({ tag,
</span>)} </span>)}
{nestLevel > 0 && ( {nestLevel > 0 && (
<span <span
className="ml-1 mr-1 shrink-0" className="ml-1 mr-1"
style={{ paddingLeft: `${ (nestLevel - 1) }rem` }}> style={{ paddingLeft: `${ (nestLevel - 1) }rem` }}>
</span>)} </span>)}
{tag.matchedAlias != null && ( {tag.matchedAlias != null && (
<> <>
<span <span className={cn (spanClass, className)} {...props}>
title={textTitle} {tag.matchedAlias}
className={cn (spanClass, textClass, className)}
{...props}>
<ResponsiveMarqueeText text={tag.matchedAlias} title={textTitle}/>
</span> </span>
<> </> <> </>
</>)} </>)}
@@ -123,20 +115,17 @@ const TagLink: FC<Props> = ({ tag,
? ( ? (
<PrefetchLink <PrefetchLink
to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`} to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
title={textTitle} className={cn (linkClass, className)}
className={cn (linkClass, textClass, className)}
{...props}> {...props}>
<ResponsiveMarqueeText text={tag.name} title={textTitle}/> {tag.name}
</PrefetchLink>) </PrefetchLink>)
: ( : (
<span <span className={cn (spanClass, className)}
title={textTitle}
className={cn (spanClass, textClass, className)}
{...props}> {...props}>
<ResponsiveMarqueeText text={tag.name} title={textTitle}/> {tag.name}
</span>)} </span>)}
{withCount && ( {withCount && (
<span className="ml-1 shrink-0">{tag.postCount}</span>)} <span className="ml-1">{tag.postCount}</span>)}
</>) </>)
} }
+3 -5
ファイルの表示
@@ -20,14 +20,12 @@ const TagSearchBox: FC<Props> = ({ suggestions, activeIndex, onSelect }) => {
rounded shadow"> rounded shadow">
{suggestions.map ((tag, i) => ( {suggestions.map ((tag, i) => (
<li key={tag.id} <li key={tag.id}
className={cn ('min-w-0 overflow-hidden px-3 py-2 cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-700', className={cn ('px-3 py-2 cursor-pointer hover:bg-gray-300 dark:hover:bg-gray-700',
i === activeIndex && 'bg-gray-300 dark:bg-gray-700')} i === activeIndex && 'bg-gray-300 dark:bg-gray-700')}
onMouseDown={() => onSelect (tag)}> onMouseDown={() => onSelect (tag)}>
<div className="flex min-w-0 max-w-full items-baseline overflow-hidden"> <TagLink tag={tag} linkFlg={false} withWiki={false}/>
<TagLink tag={tag} linkFlg={false} withWiki={false}/>
</div>
</li>))} </li>))}
</ul>) </ul>)
} }
export default TagSearchBox export default TagSearchBox
+2 -3
ファイルの表示
@@ -64,9 +64,8 @@ const TagSidebar: FC<Props> = ({ posts, onClick }) => {
<ul> <ul>
{CATEGORIES.flatMap (cat => cat in tags ? ( {CATEGORIES.flatMap (cat => cat in tags ? (
tags[cat].map (tag => ( tags[cat].map (tag => (
<li key={tag.id} className="mb-1 min-w-0 max-w-full overflow-hidden"> <li key={tag.id} className="mb-1">
<motion.div <motion.div
className="flex min-w-0 max-w-full items-baseline overflow-hidden"
transition={{ layout: { duration: .2, ease: 'easeOut' } }} transition={{ layout: { duration: .2, ease: 'easeOut' } }}
layoutId={`tag-${ tag.id }`}> layoutId={`tag-${ tag.id }`}>
<TagLink tag={tag} onClick={onClick}/> <TagLink tag={tag} onClick={onClick}/>
@@ -129,4 +128,4 @@ const TagSidebar: FC<Props> = ({ posts, onClick }) => {
</SidebarComponent>) </SidebarComponent>)
} }
export default TagSidebar export default TagSidebar
-17
ファイルの表示
@@ -132,20 +132,3 @@ body
0%, 100% { color: #f87171; } 0%, 100% { color: #f87171; }
50% { color: #60a5fa; } 50% { color: #60a5fa; }
} }
.tag-marquee
{
position: relative;
}
.tag-marquee__animated
{
position: absolute;
inset: 0 auto 0 0;
min-width: 100%;
width: max-content;
opacity: 0;
pointer-events: none;
white-space: nowrap;
will-change: transform;
}