このコミットが含まれているのは:
2026-10-26 12:19:43 +09:00
コミット 3e8f209542
31個のファイルの変更、1676行の追加、394行の削除
+12 -8
ファイルの表示
@@ -8,13 +8,13 @@ import type { ComponentPropsWithoutRef, FC, FocusEvent } from 'react'
const pad = (n: number): string => n.toString ().padStart (2, '0')
const toDateTimeLocalValue = (value: Date) => {
const toDateTimeLocalValue = (value: Date, seconds = false) => {
const y = value.getFullYear ()
const m = pad (value.getMonth () + 1)
const day = pad (value.getDate ())
const h = pad (value.getHours ())
const min = pad (value.getMinutes ())
return `${ y }-${ m }-${ day }T${ h }:${ min }`
return `${ y }-${ m }-${ day }T${ h }:${ min }${seconds ? `:${pad (value.getSeconds ())}` : ''}`
}
@@ -37,15 +37,17 @@ type Props = Omit<ComponentPropsWithoutRef<'input'>, 'onChange'> & {
onChange?: (isoUTC: string | null) => void
className?: string
onBlur?: (ev: FocusEvent<HTMLInputElement>) => void
invalid?: boolean }
invalid?: boolean
precision?: 'minute' | 'second' }
const DateTimeField: FC<Props> = ({ value, onChange, className, onBlur, invalid, ...rest }) => {
const DateTimeField: FC<Props> = props => {
const { value, onChange, className, onBlur, invalid, precision = 'minute', ...rest } = props
const [local, setLocal] = useState ('')
useEffect (() => {
setLocal (value ? toDateTimeLocalValue (new Date (value)) : '')
}, [value])
setLocal (value ? toDateTimeLocalValue (new Date (value), precision === 'second') : '')
}, [value, precision])
return (
<input
@@ -61,13 +63,15 @@ const DateTimeField: FC<Props> = ({ value, onChange, className, onBlur, invalid,
'focus:ring-2 focus:ring-blue-200']),
className)}
type="datetime-local"
step={60}
step={precision === 'second' ? 1 : 60}
value={local}
aria-invalid={invalid}
onChange={ev => {
const v = ev.target.value
setLocal (v)
onChange?.(v ? toMinutePrecisionIsoUtc (v) : null)
const iso = v ? (precision === 'second'
? new Date (v).toISOString () : toMinutePrecisionIsoUtc (v)) : null
onChange?.(iso)
}}
onBlur={onBlur}/>)
}
+35
ファイルの表示
@@ -0,0 +1,35 @@
import { useId, useRef } from 'react'
import { CircleHelp } from 'lucide-react'
import type { ReactNode } from 'react'
const HelpPopover = ({ label, children }: {
label: { text: string; language: string }; children: ReactNode },
) => {
const id = useId ()
const trigger = useRef<HTMLButtonElement> (null)
const panel = useRef<HTMLDivElement> (null)
return <>
<button ref={trigger} type="button" popoverTarget={id} aria-label={label.text}
lang={label.language} className="inline-flex align-middle"
onClick={() => {
const rect = trigger.current?.getBoundingClientRect ()
if (panel.current != null && rect != null)
{
panel.current.style.top = `${rect.bottom + 4}px`
panel.current.style.left = `${Math.min (rect.left, window.innerWidth - 288)}px`
}
}}><CircleHelp size={14}/></button>
<div ref={panel} id={id} popover="auto" tabIndex={-1}
role="dialog" aria-label={label.text} lang={label.language}
onToggle={event => {
if (event.newState === 'open') panel.current?.focus ()
}} className="fixed m-0 w-72 max-w-[90vw] rounded border bg-white p-3
text-sm text-slate-900 shadow-lg dark:border-slate-600 dark:bg-slate-900
dark:text-slate-100">{children}</div>
</>
}
export default HelpPopover
+7 -4
ファイルの表示
@@ -12,6 +12,7 @@ import type { Tag } from '@/types'
type Props = {
includeNico?: boolean
includeUnused?: boolean
describedBy?: string
invalid?: boolean
value: string
@@ -20,6 +21,7 @@ type Props = {
const TagInput: FC<Props> = ({
includeNico = false,
includeUnused = false,
describedBy,
invalid,
value,
@@ -57,11 +59,11 @@ const TagInput: FC<Props> = ({
{
const tags = await apiGet<Tag[]> ('/tags/autocomplete', {
locale, signal: controller.signal,
params: { q: request.query, nico: includeNico ? '1' : '0' } })
params: { q: request.query.replace (/^not:/i, ''), nico: includeNico ? '1' : '0' } })
if (active)
{
setResult ({ locale, includeNico, request,
tags: tags.filter (tag => tag.postCount > 0) })
tags: includeUnused ? tags : tags.filter (tag => tag.postCount > 0) })
}
}
catch
@@ -74,7 +76,7 @@ const TagInput: FC<Props> = ({
active = false
controller.abort ()
}
}, [request, locale, includeNico])
}, [request, locale, includeNico, includeUnused])
useEffect (() => {
setActiveIndex (-1)
@@ -85,7 +87,8 @@ const TagInput: FC<Props> = ({
// TODO: TagSearch からのコピペのため,共通化を考へる.
const handleTagSelect = (tag: Tag) => {
const parts = value?.split (' ')
parts[parts.length - 1] = tag.name
const negative = /^not:/i.test (parts[parts.length - 1])
parts[parts.length - 1] = `${negative ? 'not:' : ''}${tag.name}`
setValue (parts.join (' ') + ' ')
setRequest ({ query: '' })
setActiveIndex (-1)