@aiquants/daily-report 0.6.2 → 0.7.0

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.
Files changed (49) hide show
  1. package/dist/client.d.mts +81 -3
  2. package/dist/client.d.ts +81 -3
  3. package/dist/client.js +4 -4
  4. package/dist/client.js.map +1 -1
  5. package/dist/client.mjs +4 -4
  6. package/dist/client.mjs.map +1 -1
  7. package/dist/index.d.mts +2 -2
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +1 -1
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/server.d.mts +434 -6
  14. package/dist/server.d.ts +434 -6
  15. package/dist/server.js +6 -6
  16. package/dist/server.js.map +1 -1
  17. package/dist/server.mjs +6 -6
  18. package/dist/server.mjs.map +1 -1
  19. package/dist/{sse-schema-rbG114od.d.mts → sse-schema-Df49KA7B.d.mts} +643 -249
  20. package/dist/{sse-schema-eXcMG-Ej.d.ts → sse-schema-RHckD7TS.d.ts} +643 -249
  21. package/dist/styles/daily-report.standalone.css +1 -1
  22. package/dist/{types-D1PKubyo.d.mts → types-Ct1ggzy-.d.mts} +30 -1
  23. package/dist/{types-D1PKubyo.d.ts → types-Ct1ggzy-.d.ts} +30 -1
  24. package/package.json +3 -3
  25. package/src/client/components/daily-report-attachment-indicator.spec.tsx +51 -0
  26. package/src/client/components/daily-report-attachment-indicator.tsx +45 -0
  27. package/src/client/components/daily-report-attachment-list.spec.tsx +94 -0
  28. package/src/client/components/daily-report-attachment-list.tsx +112 -0
  29. package/src/client/components/daily-report-detail-list.tsx +14 -7
  30. package/src/client/components/daily-report-list.tsx +19 -11
  31. package/src/client/components/report-views.spec.tsx +340 -0
  32. package/src/client/config-context.tsx +8 -0
  33. package/src/client/contexts/daily-report-action-context.tsx +2 -0
  34. package/src/client/hooks/use-responsive-layout.spec.ts +74 -0
  35. package/src/client/hooks/use-responsive-layout.ts +56 -0
  36. package/src/client/utils/constants.spec.ts +96 -0
  37. package/src/client/utils/constants.ts +47 -2
  38. package/src/client.ts +2 -0
  39. package/src/server/handlers.attachment.spec.ts +470 -0
  40. package/src/server/handlers.ts +426 -1
  41. package/src/server/ports.ts +74 -0
  42. package/src/server/schema.ts +80 -5
  43. package/src/server/service.spec.ts +280 -14
  44. package/src/server/service.ts +308 -25
  45. package/src/server/test-helpers/handlers-config.ts +142 -0
  46. package/src/server.ts +16 -1
  47. package/src/shared/sse-schema.spec.ts +50 -0
  48. package/src/shared/sse-schema.ts +27 -0
  49. package/src/shared/types.ts +31 -0
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Responsive layout detection shared by the daily-report views.
3
+ * 日報ビューが共有するレスポンシブ判定。
4
+ *
5
+ * 閾値を 1 箇所に集約する。各ビューが独自に `matchMedia` を持つと、片方だけ変更されて
6
+ * レイアウト分岐が食い違う。
7
+ *
8
+ * ❗ **スクロール操作の可否をここで決めてはならない。** 画面幅で判定すると
9
+ * 「主入力がマウスのタッチ対応ノート」のような端末を取りこぼす。ドラッグスクロールは
10
+ * `ScrollPane` の `pointerDragInputs` で**イベントのポインタ種別ごとに**制御する
11
+ * (`CONTENT_DRAG_POINTER_INPUTS` 参照)。
12
+ */
13
+ import { useEffect, useState } from "react"
14
+
15
+ /**
16
+ * Viewport width at which the daily-report views switch to the mobile layout.
17
+ * 日報ビューがモバイルレイアウトへ切り替わるビューポート幅。
18
+ *
19
+ * Tailwind の `sm` (640px) の直下に合わせてある。リスト + サイドペインの 2 カラムを
20
+ * 畳んでオーバーレイ表示にする閾値であり、**この値は 1 箇所でだけ定義する**。
21
+ */
22
+ export const MOBILE_LAYOUT_MEDIA_QUERY = "(max-width: 639px)"
23
+
24
+ /**
25
+ * Reports whether the viewport is narrow enough for the mobile layout.
26
+ * ビューポートがモバイルレイアウトの幅かどうかを返す処理。
27
+ *
28
+ * SSR と初回描画では `false` を返し、マウント後に実環境で更新する
29
+ * (サーバーとクライアントで初期値を揃えてハイドレーション不一致を避ける)。
30
+ *
31
+ * @returns True on the mobile layout. モバイルレイアウトなら true。
32
+ */
33
+ export const useIsMobileLayout = (): boolean => {
34
+ const [isMobileLayout, setIsMobileLayout] = useState(false)
35
+
36
+ /**
37
+ * Tracks the viewport width.
38
+ * ビューポート幅を追跡する副作用。
39
+ *
40
+ * 目的: 画面回転・ウィンドウリサイズに追随する。
41
+ * 依存関係: [] - 監視対象は固定の定数なので購読は 1 度だけ張る。
42
+ * クリーンアップ: リスナーを解除する。
43
+ */
44
+ useEffect(() => {
45
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return
46
+
47
+ const query = window.matchMedia(MOBILE_LAYOUT_MEDIA_QUERY)
48
+ const sync = () => setIsMobileLayout(query.matches)
49
+
50
+ sync()
51
+ query.addEventListener("change", sync)
52
+ return () => query.removeEventListener("change", sync)
53
+ }, [])
54
+
55
+ return isMobileLayout
56
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Value-pinning tests for the shared scroll tuning constants.
3
+ * 共有スクロール調整値の値を固定するテスト。
4
+ *
5
+ * ❗ **「共有定数を参照していること」だけを見るテストでは守れない。** 参照はそのままに
6
+ * 値だけを是正前へ戻す変更は、ビューを描画するテストでは検出できない
7
+ * (どちらも同じオブジェクト参照になるため)。ここで値そのものを固定する。
8
+ *
9
+ * `TAP_SCROLL_CIRCLE_OPTIONS` の docstring は「ビューポート 800px での実測表」を
10
+ * 根拠として掲げている。その表をパッケージ本体の `computeTapScrollSpeed` で
11
+ * 実際に再現し、**文書と実装が食い違ったらテストが落ちる**ようにする。
12
+ */
13
+ import { computeTapScrollSpeed, TAP_SCROLL_SPEED_DEFAULTS } from "@aiquants/virtualscroll"
14
+ import { describe, expect, it } from "vitest"
15
+ import { CONTENT_DRAG_POINTER_INPUTS, TAP_SCROLL_CIRCLE_OPTIONS } from "./constants"
16
+
17
+ /** docstring の実測表が前提とするビューポート。 */
18
+ const VIEWPORT_SIZE = 800
19
+
20
+ /** 引き幅の基準 (パッケージの既定 `maxVisualDistance`)。 */
21
+ const MAX_DISTANCE = TAP_SCROLL_SPEED_DEFAULTS.maxDistance
22
+
23
+ /**
24
+ * `ScrollBar` の `minSpeedMultiplier` 既定値。
25
+ *
26
+ * ❗ `TAP_SCROLL_CIRCLE_OPTIONS` は `minSpeedMultiplier` を指定せず既定に委ねている。
27
+ * **値をここへ写さず、パッケージが公開している定数を読む。** 写すと、既定が変わったときに
28
+ * この表が「間違った値のまま緑で通り続ける」状態になる。
29
+ */
30
+ const DEFAULT_MIN_SPEED_MULTIPLIER = TAP_SCROLL_SPEED_DEFAULTS.minSpeedMultiplier
31
+
32
+ /** タップサークルを `ratio` (0〜1) だけ引いたときの速度 (px/s)。 */
33
+ const speedAt = (ratio: number) =>
34
+ computeTapScrollSpeed({
35
+ distance: ratio * MAX_DISTANCE,
36
+ maxDistance: MAX_DISTANCE,
37
+ viewportSize: VIEWPORT_SIZE,
38
+ minSpeedMultiplier: DEFAULT_MIN_SPEED_MULTIPLIER,
39
+ maxSpeedMultiplier: TAP_SCROLL_CIRCLE_OPTIONS.maxSpeedMultiplier ?? 0,
40
+ hasCustomMaxSpeedMultiplier: TAP_SCROLL_CIRCLE_OPTIONS.maxSpeedMultiplier !== undefined,
41
+ curve: TAP_SCROLL_CIRCLE_OPTIONS.maxSpeedCurve,
42
+ })
43
+
44
+ describe("TAP_SCROLL_CIRCLE_OPTIONS", () => {
45
+ it("チューニング値が固定されている", () => {
46
+ expect(TAP_SCROLL_CIRCLE_OPTIONS.maxSpeedMultiplier).toBe(8)
47
+ expect(TAP_SCROLL_CIRCLE_OPTIONS.maxSpeedCurve).toEqual({ exponentialSteepness: 3, exponentialScale: 8 })
48
+ })
49
+
50
+ it("easedOffset を使わない (低速域の分解能を下駄で潰さない)", () => {
51
+ // `easedOffset` は速度レンジ全体に対する比率として引き距離 0 へ適用される。
52
+ // 0.18 を入れると引き距離 0 で 1,283 px/s (160px 行で毎秒 8 行) から始まってしまう
53
+ expect(TAP_SCROLL_CIRCLE_OPTIONS.maxSpeedCurve?.easedOffset).toBeUndefined()
54
+ })
55
+
56
+ it("minSpeedMultiplier を指定せずパッケージ既定に委ねている", () => {
57
+ expect(TAP_SCROLL_CIRCLE_OPTIONS.minSpeedMultiplier).toBeUndefined()
58
+ })
59
+
60
+ it.each([
61
+ [0, 160],
62
+ [0.25, 525],
63
+ [0.5, 1298],
64
+ [1, 6400],
65
+ ])("docstring の実測表を再現する (引き %s → 約 %s px/s)", (ratio, expected) => {
66
+ expect(speedAt(ratio)).toBeCloseTo(expected, 0)
67
+ })
68
+
69
+ it("引き距離に対して狭義単調で、死角 (速度が変わらない区間) が無い", () => {
70
+ // 旧値 (steepness 7.5 / scale 14) では引き幅の 43.85% が死角だった
71
+ const samples = Array.from({ length: 101 }, (_, index) => speedAt(index / 100))
72
+ for (let index = 1; index < samples.length; index += 1) {
73
+ expect(samples[index], `ratio=${index / 100}`).toBeGreaterThan(samples[index - 1])
74
+ }
75
+ })
76
+
77
+ it("最高速が 1 フレームでオーバースキャン分を飛び越さない", () => {
78
+ // 60fps で 1 フレーム 106.7px = 160px 行の 0.67 行。オーバースキャン 15 行に対して十分小さい
79
+ expect(speedAt(1) / 60).toBeLessThan(160)
80
+ })
81
+ })
82
+
83
+ describe("CONTENT_DRAG_POINTER_INPUTS", () => {
84
+ it("直接操作 (指・ペン) を必ず含む", () => {
85
+ // ❗ ここが最重要。`ScrollPane` はネイティブのスクロール領域を持たないため、
86
+ // "touch" を外すとタッチ端末から一覧をスクロールする手段が完全に消える
87
+ expect(CONTENT_DRAG_POINTER_INPUTS).toContain("touch")
88
+ expect(CONTENT_DRAG_POINTER_INPUTS).toContain("pen")
89
+ })
90
+
91
+ it("マウスだけを除外する", () => {
92
+ // マウスのドラッグは行のタップ判定・テキスト選択と競合するため外す
93
+ expect(CONTENT_DRAG_POINTER_INPUTS).not.toContain("mouse")
94
+ expect([...CONTENT_DRAG_POINTER_INPUTS]).toEqual(["pen", "touch"])
95
+ })
96
+ })
@@ -21,9 +21,54 @@ export const WHEEL_RESET_TIMEOUT_MS = 240
21
21
  export const VIRTUAL_SCROLL_OVERSCAN_COUNT = 15
22
22
  export const SCROLL_BAR_WIDTH = 8
23
23
  export const MAX_PREVIEW_LINES = 3
24
+
25
+ /**
26
+ * Tap-scroll circle tuning shared by every daily-report list.
27
+ * 日報のすべての一覧が共有するタップスクロールサークルの調整値。
28
+ *
29
+ * ❗ **List タブと DetailList タブで同じ値を使うこと。** 片方だけ独自定義にすると、
30
+ * 同じ指の位置でタブごとに速度が何十倍も違う (実測で最大 77 倍) という事故になる。
31
+ *
32
+ * 値の根拠 (ビューポート 800px で実測):
33
+ *
34
+ * | 引き距離 | 速度 | List (行 160px) |
35
+ * | --- | --- | --- |
36
+ * | 0 | 160 px/s | 1 行/秒 |
37
+ * | 1/4 | 525 px/s | 3.3 行/秒 |
38
+ * | 1/2 | 1,298 px/s | 8 行/秒 |
39
+ * | 最大 | 6,400 px/s | 40 行/秒 (0.67 行/フレーム) |
40
+ *
41
+ * - `exponentialSteepness: 3` — 引き幅の前半をゆっくりにして微速の分解能を確保する。
42
+ * 大きくしすぎると低速域が最小速度へ張り付いて**引いても動かない死角**になる
43
+ * (旧値 7.5 では引き幅の 43.85% が死角だった)。
44
+ * - `exponentialScale: 8` — 最高速の上限 (ビューポート比)。`maxSpeedMultiplier` と揃えて
45
+ * 上限がどちらで決まるかを一意にする。1 フレームあたりの移動をオーバースキャン
46
+ * (`VIRTUAL_SCROLL_OVERSCAN_COUNT`) の行数より十分小さく保つ値にすること。
47
+ * - `easedOffset` は使わない。最小速度 160 px/s で十分に遅く、下駄を履かせると
48
+ * 低速域の分解能がその分だけ失われる。
49
+ *
50
+ * 長距離の移動はタップスクロールではなくスクロールバーのつまみ (と
51
+ * `BusinessDayThumbOverlay` の日付表示) が担う。ここを速くして解こうとしない。
52
+ */
24
53
  export const TAP_SCROLL_CIRCLE_OPTIONS: ScrollBarTapCircleOptions = {
25
54
  enabled: true,
26
55
  renderVisual: tapScrollCircleSampleVisual,
27
- maxSpeedCurve: { exponentialSteepness: 7.5, exponentialScale: 14, easedOffset: 0.18 },
28
- maxSpeedMultiplier: 18,
56
+ maxSpeedCurve: { exponentialSteepness: 3, exponentialScale: 8 },
57
+ maxSpeedMultiplier: 8,
29
58
  }
59
+
60
+ /**
61
+ * Pointer types allowed to drag-scroll the list content.
62
+ * 一覧のコンテンツ領域をドラッグしてスクロールできるポインタ種別。
63
+ *
64
+ * ❗ **`enablePointerDrag` を丸ごと切ってはならない。** `ScrollPane` はコンテンツを
65
+ * transform で動かしておりネイティブのスクロール領域を持たない
66
+ * (`.aqvs-scroll-pane-content` は `overflow: hidden`) ため、切るとタッチ端末から
67
+ * スクロール手段が消える (一度そうしてしまい、モバイルで一切スクロールできなくなった)。
68
+ *
69
+ * マウスのドラッグだけを外す。行のタップ判定 (`useTapClick`) ・テキスト選択と競合するため。
70
+ * 画面幅のメディアクエリで切り替える案は採らなかった。画面幅は入力手段を表さないので
71
+ * 「主入力がマウスのタッチ対応ノート」のような端末を取りこぼす。判定は**イベントの
72
+ * ポインタ種別**で行うため、同一端末でマウスと指を使い分けても常に正しく振る舞う。
73
+ */
74
+ export const CONTENT_DRAG_POINTER_INPUTS = ["pen", "touch"] as const
package/src/client.ts CHANGED
@@ -3,6 +3,8 @@
3
3
  * @aiquants/daily-report の client エントリ。React コンポーネント・フック・ルートヘルパーを公開。
4
4
  */
5
5
  export * from "./client/components/business-day-thumb-overlay"
6
+ export * from "./client/components/daily-report-attachment-indicator"
7
+ export * from "./client/components/daily-report-attachment-list"
6
8
  export * from "./client/components/daily-report-comment-item"
7
9
  export * from "./client/components/daily-report-comment-section"
8
10
  export * from "./client/components/daily-report-detail-list"