@aiquants/daily-report 0.18.0 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/client.d.mts +6 -3
- package/dist/client.d.ts +6 -3
- package/dist/client.js +5 -5
- package/dist/client.js.map +1 -1
- package/dist/client.mjs +5 -5
- package/dist/client.mjs.map +1 -1
- package/dist/server.d.mts +17 -7
- package/dist/server.d.ts +17 -7
- package/dist/server.js +10 -10
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +10 -10
- package/dist/server.mjs.map +1 -1
- package/dist/styles/daily-report.standalone.css +1 -1
- package/package.json +6 -4
- package/src/client/components/daily-report-list.tsx +199 -80
- package/src/client/components/daily-report-resize-panels.spec.tsx +305 -0
- package/src/client/hooks/use-responsive-layout.ts +34 -27
- package/src/server/handlers.ts +218 -176
- package/src/server/handlers.unauthorized-cookie.spec.ts +187 -0
- package/src/server/ports.ts +6 -1
- package/src/server/service.spec.ts +1 -1
- package/src/server/test-helpers/handlers-config.ts +16 -3
- package/src/server.spec.ts +7 -7
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Virtual-scrolling summary list for daily reports driven by fetcher-based detail hooks.
|
|
3
3
|
* useFetcher ベースの詳細取得フックで日報サマリーを仮想スクロール表示。
|
|
4
4
|
*/
|
|
5
|
+
import { Panel, PanelGroup, PanelResizeHandle } from "@aiquants/resize-panels"
|
|
5
6
|
import { SwipeCloseGuardContext, useSwipeGuardState, useSwipeToDismissOverlay, useTapClick, useTransitionDisplay } from "@aiquants/swipe-overlay"
|
|
6
7
|
import { type ScrollBarThumbOverlayRenderProps, VirtualScroll, type VirtualScrollHandle } from "@aiquants/virtualscroll"
|
|
7
8
|
import Check from "lucide-react/dist/esm/icons/check.js"
|
|
@@ -21,7 +22,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"
|
|
|
21
22
|
import {
|
|
22
23
|
CONTENT_DRAG_POINTER_INPUTS,
|
|
23
24
|
DEFAULT_ITEM_HEIGHT,
|
|
24
|
-
EMPTY_DETAIL,
|
|
25
25
|
EMPTY_ITEM,
|
|
26
26
|
FAST_SCROLL_INERTIA_OPTIONS,
|
|
27
27
|
FAST_SCROLL_WHEEL_MULTIPLIER,
|
|
@@ -40,11 +40,39 @@ import { DailyReportCommentForm, DailyReportCommentList } from "./daily-report-c
|
|
|
40
40
|
import { DailyReportEditForm } from "./daily-report-edit-form"
|
|
41
41
|
import { UnreadIndicator } from "./unread-indicator"
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Static behavior options for the virtual scroll component to ensure stable references across renders.
|
|
45
|
+
* レンダリングをまたいで安定した参照を保証するための仮想スクロール静的動作オプション。
|
|
46
|
+
*/
|
|
47
|
+
const VIRTUAL_SCROLL_BEHAVIOR_OPTIONS = {
|
|
48
|
+
pointerDragInputs: CONTENT_DRAG_POINTER_INPUTS,
|
|
49
|
+
inertiaOptions: FAST_SCROLL_INERTIA_OPTIONS,
|
|
50
|
+
wheelSpeedMultiplier: FAST_SCROLL_WHEEL_MULTIPLIER,
|
|
51
|
+
enableKeyboardNavigation: false,
|
|
52
|
+
} as const
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Static content insets for the virtual scroll component.
|
|
56
|
+
* 仮想スクロールコンポーネント用の静的インセット定義。
|
|
57
|
+
*/
|
|
58
|
+
const VIRTUAL_SCROLL_CONTENT_INSETS = {
|
|
59
|
+
top: ITEM_VERTICAL_PADDING,
|
|
60
|
+
bottom: ITEM_VERTICAL_PADDING,
|
|
61
|
+
} as const
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Testable virtual scroll handle extended with search and ID inspection methods.
|
|
65
|
+
* 検索および ID 参照メソッドを拡張したテスト用仮想スクロールハンドル型。
|
|
66
|
+
*/
|
|
43
67
|
type TestableVirtualScrollHandle = VirtualScrollHandle & {
|
|
44
68
|
findReportIndex: (id: number) => number
|
|
45
69
|
getReportIds: (limit?: number) => number[]
|
|
46
70
|
}
|
|
47
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Props for the DailyReportList component.
|
|
74
|
+
* DailyReportList コンポーネントのプロパティ型定義。
|
|
75
|
+
*/
|
|
48
76
|
type DailyReportListProps = {
|
|
49
77
|
dailyReportItems: DailyReportItem[]
|
|
50
78
|
autoMarkRead?: boolean
|
|
@@ -52,11 +80,10 @@ type DailyReportListProps = {
|
|
|
52
80
|
onSelectItem: (reportHubId: number | null) => void
|
|
53
81
|
userId?: string | null
|
|
54
82
|
/**
|
|
83
|
+
* Shared slot to preserve scroll position across unmounted tab switches.
|
|
55
84
|
* タブ切替 (アンマウント) をまたいでスクロール位置を持ち回る共有スロット。
|
|
56
|
-
* 値は VirtualScroll
|
|
85
|
+
* 値は VirtualScroll の論理座標 (inset 非依存。onScroll が配信する座標系)。
|
|
57
86
|
* マウント時に一度だけ読み、スクロールのたびに書き戻す。
|
|
58
|
-
* ❗ 値をレンダー時に読んで props へ焼き込む方式は禁止: 非制御タブでは親が
|
|
59
|
-
* 再レンダーされず、復帰マウントに古い値が渡る (実測で再現した実害バグ)
|
|
60
87
|
*/
|
|
61
88
|
scrollOffsetRef?: RefObject<number>
|
|
62
89
|
}
|
|
@@ -64,6 +91,12 @@ type DailyReportListProps = {
|
|
|
64
91
|
/**
|
|
65
92
|
* Manages mobile overlay state, history navigation, and scroll locking.
|
|
66
93
|
* モバイルオーバーレイの状態、履歴ナビゲーション、スクロールロックを管理するフック。
|
|
94
|
+
*
|
|
95
|
+
* @param isMobileLayout - Whether the current viewport matches mobile layout. モバイルレイアウトかどうか。
|
|
96
|
+
* @param selectedReportHubId - Currently selected report identifier. 現在選択中の日報 ID。
|
|
97
|
+
* @param dailyReportItems - Full list of daily report items. 日報アイテム一覧。
|
|
98
|
+
* @param onSelectItem - Callback to update selected report identifier. 選択日報 ID 更新コールバック。
|
|
99
|
+
* @returns Mobile overlay state and control handlers. モバイルオーバーレイ状態と操作ハンドラ。
|
|
67
100
|
*/
|
|
68
101
|
const useMobileOverlayManager = (isMobileLayout: boolean, selectedReportHubId: number | null, dailyReportItems: DailyReportItem[], onSelectItem: (reportHubId: number | null) => void) => {
|
|
69
102
|
const [isMobileDetailOpen, setIsMobileDetailOpen] = useState(false)
|
|
@@ -71,8 +104,36 @@ const useMobileOverlayManager = (isMobileLayout: boolean, selectedReportHubId: n
|
|
|
71
104
|
const overlayHistoryKeyRef = useRef<string | null>(null)
|
|
72
105
|
const lastClosedItemIdRef = useRef<number | null>(null)
|
|
73
106
|
const bodyOverflowRef = useRef<string | null>(null)
|
|
107
|
+
const prevIsMobileRef = useRef(isMobileLayout)
|
|
108
|
+
const isInitialMountRef = useRef(true)
|
|
74
109
|
|
|
75
110
|
const selectedItem = useMemo(() => (selectedReportHubId === null ? null : (dailyReportItems.find((e) => e.reportHubId === selectedReportHubId) ?? null)), [dailyReportItems, selectedReportHubId])
|
|
111
|
+
|
|
112
|
+
// デスクトップで日報を選択中に画面幅がモバイル幅へ縮小・回転した際、詳細の閲覧文脈を失わないようオーバーレイへ引き継ぐ
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
// 初回マウント時は SSR とのハイドレーション整合 (useIsMobileLayout の false -> true 遷移) による
|
|
115
|
+
// 誤展開を防止するため、リサイズ遷移処理をスキップする
|
|
116
|
+
if (isInitialMountRef.current) {
|
|
117
|
+
isInitialMountRef.current = false
|
|
118
|
+
prevIsMobileRef.current = isMobileLayout
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const wasDesktop = !prevIsMobileRef.current
|
|
123
|
+
prevIsMobileRef.current = isMobileLayout
|
|
124
|
+
|
|
125
|
+
if (wasDesktop && isMobileLayout && selectedReportHubId !== null) {
|
|
126
|
+
setIsMobileDetailOpen(true)
|
|
127
|
+
} else if (!isMobileLayout && isMobileDetailOpen) {
|
|
128
|
+
// デスクトップ復帰時はオーバーレイを閉じ、ロックされていたスクロールを即時解放する
|
|
129
|
+
setIsMobileDetailOpen(false)
|
|
130
|
+
if (bodyOverflowRef.current !== null) {
|
|
131
|
+
document.body.style.overflow = bodyOverflowRef.current
|
|
132
|
+
bodyOverflowRef.current = null
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}, [isMobileLayout, selectedReportHubId, isMobileDetailOpen])
|
|
136
|
+
|
|
76
137
|
const shouldShowOverlay = isMobileLayout && isMobileDetailOpen && selectedItem !== null
|
|
77
138
|
|
|
78
139
|
/**
|
|
@@ -103,7 +164,7 @@ const useMobileOverlayManager = (isMobileLayout: boolean, selectedReportHubId: n
|
|
|
103
164
|
useEffect(() => {
|
|
104
165
|
if (typeof window === "undefined") return
|
|
105
166
|
|
|
106
|
-
//
|
|
167
|
+
// モバイルオーバーレイ展開時のみブラウザの戻る操作でオーバーレイを閉じられるよう履歴を積む
|
|
107
168
|
if (isMobileLayout && isMobileDetailOpen && selectedReportHubId !== null && !overlayHistoryKeyRef.current) {
|
|
108
169
|
const key = `overlay-${Date.now()}`
|
|
109
170
|
window.history.pushState({ ...window.history.state, dailyReportOverlay: key }, "", window.location.href)
|
|
@@ -112,8 +173,8 @@ const useMobileOverlayManager = (isMobileLayout: boolean, selectedReportHubId: n
|
|
|
112
173
|
overlayHistoryKeyRef.current = null
|
|
113
174
|
}
|
|
114
175
|
|
|
115
|
-
//
|
|
116
|
-
if (isMobileDetailOpen) {
|
|
176
|
+
// オーバーレイ背面のスクロールを抑止して誤スクロールを防止
|
|
177
|
+
if (isMobileLayout && isMobileDetailOpen) {
|
|
117
178
|
bodyOverflowRef.current = document.body.style.overflow
|
|
118
179
|
document.body.style.overflow = "hidden"
|
|
119
180
|
} else if (bodyOverflowRef.current !== null) {
|
|
@@ -251,11 +312,37 @@ export const DailyReportList = ({ dailyReportItems, autoMarkRead = false, select
|
|
|
251
312
|
const [overlayLabel, setOverlayLabel] = useState<string>("")
|
|
252
313
|
const [isWheelScrollActive, setIsWheelScrollActive] = useState(false)
|
|
253
314
|
const isMobileLayout = useIsMobileLayout()
|
|
315
|
+
const rootContainerRef = useRef<HTMLDivElement>(null)
|
|
316
|
+
const [isContainerNarrow, setIsContainerNarrow] = useState(false)
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Observes root container width to fallback to single-column layout when available space is constrained.
|
|
320
|
+
* 利用可能なコンテナ幅が狭小化した際に単一カラム表示へフォールバックするためのコンテナ監視。
|
|
321
|
+
*/
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
if (typeof window === "undefined" || typeof ResizeObserver === "undefined") return
|
|
324
|
+
const el = rootContainerRef.current
|
|
325
|
+
if (!el) return
|
|
326
|
+
|
|
327
|
+
const observer = new ResizeObserver((entries) => {
|
|
328
|
+
for (const entry of entries) {
|
|
329
|
+
if (entry.target === el) {
|
|
330
|
+
const width = entry.contentRect.width
|
|
331
|
+
// 2 カラム表示に必要な安全幅 (460px) を下回った場合は 1 カラムへフォールバック
|
|
332
|
+
setIsContainerNarrow(width > 0 && width < 460)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
observer.observe(el)
|
|
337
|
+
return () => observer.disconnect()
|
|
338
|
+
}, [])
|
|
339
|
+
|
|
340
|
+
const effectiveIsMobile = isMobileLayout || isContainerNarrow
|
|
254
341
|
const wheelResetTimerRef = useRef<number | null>(null)
|
|
255
342
|
const itemCount = dailyReportItems.length
|
|
256
343
|
const scrollEndTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
257
344
|
|
|
258
|
-
const { isMobileDetailOpen, setIsMobileDetailOpen, isOverlayMounted, shouldShowOverlay, handleOverlayClose, lastClosedItemIdRef, selectedItem } = useMobileOverlayManager(
|
|
345
|
+
const { isMobileDetailOpen, setIsMobileDetailOpen, isOverlayMounted, shouldShowOverlay, handleOverlayClose, lastClosedItemIdRef, selectedItem } = useMobileOverlayManager(effectiveIsMobile, selectedReportHubId, dailyReportItems, onSelectItem)
|
|
259
346
|
|
|
260
347
|
const columnHeightStyle = useMemo<CSSProperties | undefined>(() => (Number.isFinite(viewportHeight) && viewportHeight > 0 ? { height: viewportHeight } : undefined), [viewportHeight])
|
|
261
348
|
|
|
@@ -314,7 +401,7 @@ export const DailyReportList = ({ dailyReportItems, autoMarkRead = false, select
|
|
|
314
401
|
* Cleanup: None.
|
|
315
402
|
*/
|
|
316
403
|
useEffect(() => {
|
|
317
|
-
if (
|
|
404
|
+
if (effectiveIsMobile || isMobileDetailOpen || lastClosedItemIdRef.current === null) return
|
|
318
405
|
const targetId = lastClosedItemIdRef.current
|
|
319
406
|
lastClosedItemIdRef.current = null
|
|
320
407
|
|
|
@@ -330,7 +417,7 @@ export const DailyReportList = ({ dailyReportItems, autoMarkRead = false, select
|
|
|
330
417
|
card?.focus()
|
|
331
418
|
})
|
|
332
419
|
}
|
|
333
|
-
}, [isMobileDetailOpen,
|
|
420
|
+
}, [isMobileDetailOpen, effectiveIsMobile, dailyReportItems, lastClosedItemIdRef])
|
|
334
421
|
|
|
335
422
|
/**
|
|
336
423
|
* Syncs overlay label with the first item's date.
|
|
@@ -368,9 +455,9 @@ export const DailyReportList = ({ dailyReportItems, autoMarkRead = false, select
|
|
|
368
455
|
(id: number) => {
|
|
369
456
|
if (id <= 0) return
|
|
370
457
|
onSelectItem(id)
|
|
371
|
-
if (
|
|
458
|
+
if (effectiveIsMobile) setIsMobileDetailOpen(true)
|
|
372
459
|
},
|
|
373
|
-
[
|
|
460
|
+
[effectiveIsMobile, onSelectItem, setIsMobileDetailOpen],
|
|
374
461
|
)
|
|
375
462
|
|
|
376
463
|
const getItem = useCallback((index: number) => (index < itemCount ? dailyReportItems[index] : EMPTY_ITEM), [dailyReportItems, itemCount])
|
|
@@ -397,11 +484,7 @@ export const DailyReportList = ({ dailyReportItems, autoMarkRead = false, select
|
|
|
397
484
|
|
|
398
485
|
const handleScroll = useCallback(
|
|
399
486
|
(position: number) => {
|
|
400
|
-
// onScroll
|
|
401
|
-
// 補足 (virtualscroll 2.0.0): ハンドル getScrollPosition も論理座標に統一されたため
|
|
402
|
-
// 読み戻しても座標は一致するが、配信引数が正 (追加のハンドル呼び出しは冗長)。
|
|
403
|
-
// 歴史的経緯: 2.0.0 以前はペイン座標を返す非対称があり、読み戻しが inset 分ずれる
|
|
404
|
-
// 実害バグの原因だった
|
|
487
|
+
// onScroll が配信する論理座標をそのまま共有スロットへ記録し、タブ復帰時の復元座標を一致させる
|
|
405
488
|
if (scrollOffsetRef) {
|
|
406
489
|
scrollOffsetRef.current = position
|
|
407
490
|
}
|
|
@@ -429,59 +512,70 @@ export const DailyReportList = ({ dailyReportItems, autoMarkRead = false, select
|
|
|
429
512
|
|
|
430
513
|
const renderThumbOverlay = useCallback((props: ScrollBarThumbOverlayRenderProps) => <BusinessDayThumbOverlay {...props} label={overlayLabel} isWheelScrollActive={isWheelScrollActive} />, [overlayLabel, isWheelScrollActive])
|
|
431
514
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
<div className="flex h-full min-h-0
|
|
435
|
-
<
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
contentInsets={{ top: ITEM_VERTICAL_PADDING, bottom: ITEM_VERTICAL_PADDING }}
|
|
464
|
-
testId="daily-report-list"
|
|
465
|
-
className="relative w-full"
|
|
466
|
-
contentProps={{
|
|
467
|
-
// 行 (role="option" ではなく role="listitem") だけを所有する list を、
|
|
468
|
-
// スクロールバーと兄弟のコンテンツ要素に置く (virtualscroll 1.23.0 の口)。
|
|
469
|
-
// 各行が aria-posinset/aria-setsize を持つので SR は「全 N 件中 M 番目」を読める
|
|
470
|
-
role: "list",
|
|
471
|
-
"aria-label": title,
|
|
472
|
-
}}>
|
|
473
|
-
{renderListRow}
|
|
474
|
-
</VirtualScroll>
|
|
475
|
-
</div>
|
|
476
|
-
<DailyReportSidePane dailyReportItems={dailyReportItems} selectedItem={selectedItem} heightStyle={columnHeightStyle} autoMarkRead={!isMobileLayout && autoMarkRead} isActive={selectedReportHubId !== null} userId={userId} />
|
|
515
|
+
const virtualScrollNode = useMemo(
|
|
516
|
+
() => (
|
|
517
|
+
<div ref={scrollContainerRef} className="isolate flex h-full min-h-0 w-full overflow-hidden" style={columnHeightStyle}>
|
|
518
|
+
<VirtualScroll
|
|
519
|
+
ref={virtualScrollRef}
|
|
520
|
+
itemCount={itemCount}
|
|
521
|
+
getItem={getItem}
|
|
522
|
+
getItemHeight={getItemHeight}
|
|
523
|
+
viewportSize={viewportHeight}
|
|
524
|
+
initialScrollOffset={initialScrollOffset}
|
|
525
|
+
overscanCount={VIRTUAL_SCROLL_OVERSCAN_COUNT}
|
|
526
|
+
scrollBarOptions={{
|
|
527
|
+
width: SCROLL_BAR_WIDTH,
|
|
528
|
+
tapScrollCircleOptions: TAP_SCROLL_CIRCLE_OPTIONS,
|
|
529
|
+
renderThumbOverlay: renderThumbOverlay,
|
|
530
|
+
}}
|
|
531
|
+
behaviorOptions={VIRTUAL_SCROLL_BEHAVIOR_OPTIONS}
|
|
532
|
+
onRangeChange={handleRangeChange}
|
|
533
|
+
onScroll={handleScroll}
|
|
534
|
+
contentInsets={VIRTUAL_SCROLL_CONTENT_INSETS}
|
|
535
|
+
testId="daily-report-list"
|
|
536
|
+
className="relative w-full"
|
|
537
|
+
contentProps={{
|
|
538
|
+
// 行 (role="option" ではなく role="listitem") だけを所有する list を、
|
|
539
|
+
// スクロールバーと兄弟のコンテンツ要素に置く (virtualscroll 1.23.0 の口)。
|
|
540
|
+
// 各行が aria-posinset/aria-setsize を持つので SR は「全 N 件中 M 番目」を読める
|
|
541
|
+
role: "list",
|
|
542
|
+
"aria-label": title,
|
|
543
|
+
}}>
|
|
544
|
+
{renderListRow}
|
|
545
|
+
</VirtualScroll>
|
|
477
546
|
</div>
|
|
547
|
+
),
|
|
548
|
+
[columnHeightStyle, itemCount, getItem, getItemHeight, viewportHeight, initialScrollOffset, renderThumbOverlay, handleRangeChange, handleScroll, title, renderListRow],
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
return (
|
|
552
|
+
<div ref={rootContainerRef} className="h-full min-h-0 w-full" data-testid="daily-report-root">
|
|
553
|
+
{effectiveIsMobile ? (
|
|
554
|
+
<div className="flex h-full min-h-0">{virtualScrollNode}</div>
|
|
555
|
+
) : (
|
|
556
|
+
<PanelGroup direction="horizontal" className="flex h-full min-h-0" data-testid="daily-report-panel-group">
|
|
557
|
+
<Panel id="daily-report-list-panel" defaultSize={{ value: 360, unit: "pixels" }} minSize={{ value: 160, unit: "pixels" }} maxSize={{ value: 520, unit: "pixels" }} contentOverflow="hidden" className="flex min-h-0">
|
|
558
|
+
{virtualScrollNode}
|
|
559
|
+
</Panel>
|
|
560
|
+
<PanelResizeHandle
|
|
561
|
+
id="daily-report-list-handle"
|
|
562
|
+
thickness={10}
|
|
563
|
+
indicator={{ visible: true, thickness: 6, length: 32, barThickness: 2, barLength: 16 }}
|
|
564
|
+
className="cursor-col-resize rounded transition-colors hover:bg-slate-200/60 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1 dark:hover:bg-slate-700/60"
|
|
565
|
+
aria-label="日報一覧と詳細ペインの境界リサイズ"
|
|
566
|
+
/>
|
|
567
|
+
<Panel id="daily-report-detail-panel" minSize={{ value: 160, unit: "pixels" }} contentOverflow="hidden" className="flex min-h-0 flex-1 pl-1.5">
|
|
568
|
+
<DailyReportSidePane dailyReportItems={dailyReportItems} selectedItem={selectedItem} autoMarkRead={autoMarkRead} isActive={selectedReportHubId !== null} userId={userId} />
|
|
569
|
+
</Panel>
|
|
570
|
+
</PanelGroup>
|
|
571
|
+
)}
|
|
478
572
|
{itemCount === 0 && (
|
|
479
573
|
<div className="py-8 text-center">
|
|
480
574
|
<p className="text-slate-500">データがありません</p>
|
|
481
575
|
</div>
|
|
482
576
|
)}
|
|
483
|
-
{isOverlayMounted && <DailyReportMobileOverlay isOpen={shouldShowOverlay} selectedItem={selectedItem} onClose={handleOverlayClose} autoMarkRead={
|
|
484
|
-
|
|
577
|
+
{isOverlayMounted && <DailyReportMobileOverlay isOpen={shouldShowOverlay} selectedItem={selectedItem} onClose={handleOverlayClose} autoMarkRead={effectiveIsMobile && autoMarkRead} userId={userId} />}
|
|
578
|
+
</div>
|
|
485
579
|
)
|
|
486
580
|
}
|
|
487
581
|
|
|
@@ -509,6 +603,10 @@ const ListSkeletonItem = (wrapperProps: React.HTMLAttributes<HTMLDivElement> & {
|
|
|
509
603
|
</div>
|
|
510
604
|
)
|
|
511
605
|
|
|
606
|
+
/**
|
|
607
|
+
* Tab types available in the daily report side pane.
|
|
608
|
+
* 日報サイドペインで利用可能なタブ種別。
|
|
609
|
+
*/
|
|
512
610
|
type DailyReportSidePaneTab = "article" | "relations"
|
|
513
611
|
|
|
514
612
|
/**
|
|
@@ -523,7 +621,8 @@ const DailyReportSidePane = ({
|
|
|
523
621
|
isActive = true,
|
|
524
622
|
userId,
|
|
525
623
|
}: {
|
|
526
|
-
|
|
624
|
+
/** Full list of daily report items; omitted when rendered inside mobile overlay. 日報アイテム一覧。モバイルオーバーレイ時は未指定。 */
|
|
625
|
+
dailyReportItems?: DailyReportItem[]
|
|
527
626
|
selectedItem: DailyReportItem | null
|
|
528
627
|
heightStyle?: CSSProperties
|
|
529
628
|
autoMarkRead?: boolean
|
|
@@ -578,8 +677,8 @@ const DailyReportSidePane = ({
|
|
|
578
677
|
|
|
579
678
|
if (dailyReportItems && (dailyReportItems.length === 0 || !selectedItem)) {
|
|
580
679
|
return (
|
|
581
|
-
<aside className="hidden min-h-0 min-w-0 flex-1 gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm sm:
|
|
582
|
-
<div className="flex flex-1 items-center justify-center rounded-xl border border-slate-300 border-dashed bg-slate-50 p-4 text-center text-slate-500 text-sm dark:border-slate-600 dark:bg-slate-900/40 dark:text-slate-300">
|
|
680
|
+
<aside className="hidden h-full min-h-0 min-w-0 flex-1 gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm sm:flex dark:border-slate-700 dark:bg-slate-900/70" style={heightStyle}>
|
|
681
|
+
<div className="flex h-full flex-1 items-center justify-center rounded-xl border border-slate-300 border-dashed bg-slate-50 p-4 text-center text-slate-500 text-sm dark:border-slate-600 dark:bg-slate-900/40 dark:text-slate-300">
|
|
583
682
|
<p>左側の一覧から日報を選択してください。</p>
|
|
584
683
|
</div>
|
|
585
684
|
</aside>
|
|
@@ -646,19 +745,18 @@ const DailyReportSidePane = ({
|
|
|
646
745
|
</>
|
|
647
746
|
)
|
|
648
747
|
|
|
649
|
-
// If rendered as a standalone pane (desktop)
|
|
650
748
|
if (dailyReportItems) {
|
|
651
749
|
return (
|
|
652
750
|
<aside
|
|
653
751
|
data-testid="daily-report-side-pane"
|
|
654
752
|
data-report-id={selectedItem?.reportHubId}
|
|
655
|
-
className="hidden min-h-0 min-w-0 flex-1 flex-col gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm sm:flex dark:border-slate-700 dark:bg-slate-900/70"
|
|
753
|
+
className="hidden h-full min-h-0 w-full min-w-0 flex-1 flex-col gap-3 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm sm:flex dark:border-slate-700 dark:bg-slate-900/70"
|
|
656
754
|
style={heightStyle}>
|
|
657
755
|
{content}
|
|
658
756
|
</aside>
|
|
659
757
|
)
|
|
660
758
|
}
|
|
661
|
-
|
|
759
|
+
|
|
662
760
|
return (
|
|
663
761
|
<div data-testid="daily-report-side-pane" data-report-id={selectedItem?.reportHubId} className="flex min-h-0 flex-1 flex-col gap-3 overflow-hidden">
|
|
664
762
|
{content}
|
|
@@ -779,14 +877,29 @@ const DailyReportDisplayPane = ({
|
|
|
779
877
|
<div className="relative space-y-4 text-slate-700 dark:text-slate-200" onDoubleClick={onDoubleClick}>
|
|
780
878
|
{!showSkeleton && report && (
|
|
781
879
|
<div className="absolute top-0 right-2 z-10 flex items-center justify-end gap-3">
|
|
782
|
-
<button
|
|
880
|
+
<button
|
|
881
|
+
type="button"
|
|
882
|
+
onClick={() => handleToggleRead(false)}
|
|
883
|
+
className="rounded text-lg leading-none transition-transform focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 active:scale-95"
|
|
884
|
+
title={isRead ? "未読にする" : "既読にする"}
|
|
885
|
+
data-detail-read-button={report.reportHubId}>
|
|
783
886
|
{isRead ? <span className="text-slate-300 hover:text-blue-500">✓</span> : <UnreadIndicator className="hover:text-blue-600" />}
|
|
784
887
|
</button>
|
|
785
|
-
<button
|
|
888
|
+
<button
|
|
889
|
+
type="button"
|
|
890
|
+
onClick={handleToggleStar}
|
|
891
|
+
className="rounded text-lg leading-none transition-transform focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 active:scale-95"
|
|
892
|
+
title={isStarred ? "スターを外す" : "スターを付ける"}
|
|
893
|
+
data-detail-star-button={report.reportHubId}>
|
|
786
894
|
{isStarred ? <span className="text-yellow-400">★</span> : <span className="text-slate-300 hover:text-yellow-400">☆</span>}
|
|
787
895
|
</button>
|
|
788
896
|
{isMine && (
|
|
789
|
-
<button
|
|
897
|
+
<button
|
|
898
|
+
type="button"
|
|
899
|
+
onClick={handleDelete}
|
|
900
|
+
data-detail-delete-button={report.reportHubId}
|
|
901
|
+
className="rounded text-lg leading-none transition-transform focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 active:scale-95"
|
|
902
|
+
title="削除する">
|
|
790
903
|
<Trash2 className="h-4 w-4 text-slate-300 hover:text-red-500" />
|
|
791
904
|
</button>
|
|
792
905
|
)}
|
|
@@ -866,8 +979,6 @@ const DailyReportMobileOverlay = ({ isOpen, selectedItem, onClose, autoMarkRead
|
|
|
866
979
|
const { report, error, isLoading } = useDailyReportDetail(selectedItem?.reportHubId ?? -1, selectedItem?.businessDate ?? null)
|
|
867
980
|
const displayData = useTransitionDisplay(isOpen, selectedItem ? { item: selectedItem, detail: { report, error, isLoading } } : null)
|
|
868
981
|
const displayItem = displayData?.item ?? null
|
|
869
|
-
const _displayDetail = displayData?.detail ?? EMPTY_DETAIL
|
|
870
|
-
_displayDetail
|
|
871
982
|
const { overlayRef, overlayStyle, attemptClose, isSwiping } = useSwipeToDismissOverlay({
|
|
872
983
|
isOpen,
|
|
873
984
|
onClose,
|
|
@@ -899,13 +1010,21 @@ const DailyReportMobileOverlay = ({ isOpen, selectedItem, onClose, autoMarkRead
|
|
|
899
1010
|
if (!displayItem) return null
|
|
900
1011
|
|
|
901
1012
|
return (
|
|
902
|
-
<div
|
|
1013
|
+
<div
|
|
1014
|
+
ref={containerRef}
|
|
1015
|
+
data-daily-report-mobile-overlay="true"
|
|
1016
|
+
className={`fixed top-0 bottom-0 left-0 z-[100] flex touch-manipulation transition-[visibility] duration-200 ${isOpen ? "visible" : "invisible"} ${isOpen && !isSwiping ? "pointer-events-auto" : "pointer-events-none"}`}
|
|
1017
|
+
style={{
|
|
1018
|
+
right: "var(--aqdr-overlay-right, 0px)",
|
|
1019
|
+
transition: "right 0.5s, visibility 0.2s",
|
|
1020
|
+
}}
|
|
1021
|
+
aria-hidden={!isOpen}>
|
|
903
1022
|
<button type="button" aria-label="背景をタップして閉じる" className={`absolute inset-0 touch-manipulation bg-slate-900/50 transition-opacity duration-200 focus:outline-none ${isOpen ? "opacity-100" : "opacity-0"}`} {...tapHandlers}></button>
|
|
904
1023
|
<aside
|
|
905
1024
|
role="dialog"
|
|
906
1025
|
aria-modal="true"
|
|
907
1026
|
aria-label="日報詳細"
|
|
908
|
-
className={`relative ml-auto flex h-full w-full max-w-full transform touch-none
|
|
1027
|
+
className={`relative ml-auto flex h-full w-full max-w-full transform touch-none bg-white shadow-xl transition-transform duration-200 ease-out dark:bg-slate-900/95 ${isOpen ? "pointer-events-auto" : "pointer-events-none"}`}
|
|
909
1028
|
ref={overlayRef}
|
|
910
1029
|
onKeyDown={(e) => {
|
|
911
1030
|
if (e.key === "Escape") {
|
|
@@ -1011,7 +1130,7 @@ const ListReportItem = memo(({ item, index, setSize, isActive, onSelect }: { ite
|
|
|
1011
1130
|
try {
|
|
1012
1131
|
await toggleStar(report.reportHubId, nextValue, report.date ?? "")
|
|
1013
1132
|
} catch (_e) {
|
|
1014
|
-
//
|
|
1133
|
+
// コンテキスト層のアクションハンドラでトースト通知およびロールバックが処理されるため握り潰す
|
|
1015
1134
|
}
|
|
1016
1135
|
}
|
|
1017
1136
|
|
|
@@ -1021,7 +1140,7 @@ const ListReportItem = memo(({ item, index, setSize, isActive, onSelect }: { ite
|
|
|
1021
1140
|
try {
|
|
1022
1141
|
await toggleRead(report.reportHubId, nextValue, report.date ?? "")
|
|
1023
1142
|
} catch (_e) {
|
|
1024
|
-
//
|
|
1143
|
+
// コンテキスト層のアクションハンドラでトースト通知およびロールバックが処理されるため握り潰す
|
|
1025
1144
|
}
|
|
1026
1145
|
}
|
|
1027
1146
|
|
|
@@ -1041,7 +1160,7 @@ const ListReportItem = memo(({ item, index, setSize, isActive, onSelect }: { ite
|
|
|
1041
1160
|
{/* biome-ignore lint/a11y/useSemanticElements: Card contains nested interactive elements (Star/Read buttons) */}
|
|
1042
1161
|
<div
|
|
1043
1162
|
data-daily-report-button={item.reportHubId}
|
|
1044
|
-
className={`flex h-full w-full in-[[data-scrolling]]:transform-none cursor-pointer select-none flex-col rounded-2xl border in-[[data-scrolling]]:border-slate-200 bg-white p-4 text-left in-[[data-scrolling]]:shadow-sm shadow-sm in-[[data-scrolling]]:transition-none transition-
|
|
1163
|
+
className={`flex h-full w-full in-[[data-scrolling]]:transform-none cursor-pointer select-none flex-col rounded-2xl border in-[[data-scrolling]]:border-slate-200 bg-white p-4 text-left in-[[data-scrolling]]:shadow-sm shadow-sm in-[[data-scrolling]]:transition-none transition-[transform,border-color,box-shadow] duration-200 hover:-translate-y-0.5 hover:border-blue-300 hover:shadow-lg dark:bg-slate-900/70 ${
|
|
1045
1164
|
isActive ? "border-blue-300 shadow-md ring-2 ring-blue-200 dark:border-blue-500/70 dark:ring-blue-500/40" : "border-slate-200 dark:border-slate-700"
|
|
1046
1165
|
}`}
|
|
1047
1166
|
style={{ touchAction: "manipulation" }}
|