@aiquants/daily-report 0.20.0 → 0.20.1

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 (115) hide show
  1. package/dist/client.d.mts +11 -706
  2. package/dist/client.d.ts +11 -706
  3. package/dist/client.js +0 -1
  4. package/dist/client.mjs +0 -1
  5. package/dist/index.d.mts +5 -197
  6. package/dist/index.d.ts +5 -197
  7. package/dist/index.js +0 -1
  8. package/dist/index.mjs +0 -1
  9. package/dist/logger-D3krZrNK.d.mts +0 -14
  10. package/dist/logger-D3krZrNK.d.ts +0 -14
  11. package/dist/server.d.mts +114 -744
  12. package/dist/server.d.ts +114 -744
  13. package/dist/server.js +0 -1
  14. package/dist/server.mjs +0 -1
  15. package/dist/sse-schema-BdUGThiz.d.ts +1025 -1009
  16. package/dist/sse-schema-ChSFm-4q.d.mts +1025 -1009
  17. package/dist/types-CqgYQgbH.d.mts +0 -34
  18. package/dist/types-CqgYQgbH.d.ts +0 -34
  19. package/package.json +7 -8
  20. package/dist/client.js.map +0 -1
  21. package/dist/client.mjs.map +0 -1
  22. package/dist/index.js.map +0 -1
  23. package/dist/index.mjs.map +0 -1
  24. package/dist/server.js.map +0 -1
  25. package/dist/server.mjs.map +0 -1
  26. package/src/client/components/business-day-thumb-overlay.tsx +0 -19
  27. package/src/client/components/daily-report-attachment-indicator.spec.tsx +0 -51
  28. package/src/client/components/daily-report-attachment-indicator.tsx +0 -45
  29. package/src/client/components/daily-report-attachment-list.spec.tsx +0 -94
  30. package/src/client/components/daily-report-attachment-list.tsx +0 -112
  31. package/src/client/components/daily-report-comment-item.tsx +0 -89
  32. package/src/client/components/daily-report-comment-section.tsx +0 -166
  33. package/src/client/components/daily-report-detail-list.tsx +0 -842
  34. package/src/client/components/daily-report-edit-form.tsx +0 -87
  35. package/src/client/components/daily-report-ids-stream-status.spec.tsx +0 -93
  36. package/src/client/components/daily-report-ids-stream-status.tsx +0 -67
  37. package/src/client/components/daily-report-list.tsx +0 -1232
  38. package/src/client/components/daily-report-page.spec.tsx +0 -119
  39. package/src/client/components/daily-report-page.tsx +0 -140
  40. package/src/client/components/daily-report-resize-panels.spec.tsx +0 -307
  41. package/src/client/components/daily-report-resolved-content.tsx +0 -207
  42. package/src/client/components/daily-report-scroll-restoration.spec.tsx +0 -422
  43. package/src/client/components/report-views.spec.tsx +0 -375
  44. package/src/client/components/unread-indicator.tsx +0 -13
  45. package/src/client/config-context.tsx +0 -184
  46. package/src/client/contexts/daily-report-action-context.spec.tsx +0 -433
  47. package/src/client/contexts/daily-report-action-context.tsx +0 -1147
  48. package/src/client/contexts/daily-report-error-context.spec.tsx +0 -66
  49. package/src/client/contexts/daily-report-error-context.tsx +0 -98
  50. package/src/client/hooks/use-animated-number.spec.ts +0 -103
  51. package/src/client/hooks/use-animated-number.ts +0 -69
  52. package/src/client/hooks/use-daily-report-comments.ts +0 -73
  53. package/src/client/hooks/use-daily-report-sse-connection.ts +0 -86
  54. package/src/client/hooks/use-daily-report.spec.ts +0 -546
  55. package/src/client/hooks/use-daily-report.ts +0 -549
  56. package/src/client/hooks/use-dynamic-viewport-height.ts +0 -127
  57. package/src/client/hooks/use-responsive-layout.spec.ts +0 -74
  58. package/src/client/hooks/use-responsive-layout.ts +0 -63
  59. package/src/client/route-helpers.spec.ts +0 -100
  60. package/src/client/route-helpers.ts +0 -61
  61. package/src/client/streaming/daily-report-ids-stream-client.spec.ts +0 -910
  62. package/src/client/streaming/daily-report-ids-stream-client.ts +0 -730
  63. package/src/client/streaming/daily-report-ids-stream-session.spec.ts +0 -401
  64. package/src/client/streaming/daily-report-ids-stream-session.ts +0 -319
  65. package/src/client/ui/button.tsx +0 -42
  66. package/src/client/ui/cn.ts +0 -14
  67. package/src/client/ui/input.tsx +0 -21
  68. package/src/client/ui/label.tsx +0 -16
  69. package/src/client/ui/switch.tsx +0 -19
  70. package/src/client/ui/tabs.tsx +0 -40
  71. package/src/client/ui/textarea.tsx +0 -19
  72. package/src/client/utils/constants.spec.ts +0 -96
  73. package/src/client/utils/constants.ts +0 -84
  74. package/src/client.ts +0 -28
  75. package/src/index.ts +0 -11
  76. package/src/lucide-icons.d.ts +0 -6
  77. package/src/server/authz.spec.ts +0 -57
  78. package/src/server/authz.ts +0 -99
  79. package/src/server/cache.spec.ts +0 -556
  80. package/src/server/cache.ts +0 -303
  81. package/src/server/etag.ts +0 -18
  82. package/src/server/external-source.ts +0 -60
  83. package/src/server/handlers.action.spec.ts +0 -44
  84. package/src/server/handlers.attachment.spec.ts +0 -470
  85. package/src/server/handlers.ids-stream.spec.ts +0 -618
  86. package/src/server/handlers.ts +0 -1638
  87. package/src/server/handlers.unauthorized-cookie.spec.ts +0 -187
  88. package/src/server/handlers.visibility.spec.ts +0 -774
  89. package/src/server/ports.ts +0 -222
  90. package/src/server/response.ts +0 -37
  91. package/src/server/schema.spec.ts +0 -30
  92. package/src/server/schema.ts +0 -344
  93. package/src/server/service.spec.ts +0 -1130
  94. package/src/server/service.ts +0 -2045
  95. package/src/server/sse-reader.spec.ts +0 -302
  96. package/src/server/sse-reader.ts +0 -267
  97. package/src/server/test-helpers/handlers-config.ts +0 -321
  98. package/src/server/transaction-retry.spec.ts +0 -63
  99. package/src/server/visibility.spec.ts +0 -403
  100. package/src/server/visibility.ts +0 -373
  101. package/src/server.spec.ts +0 -160
  102. package/src/server.ts +0 -131
  103. package/src/shared/business-date.spec.ts +0 -82
  104. package/src/shared/business-date.ts +0 -104
  105. package/src/shared/comment-adapter.spec.ts +0 -38
  106. package/src/shared/comment-adapter.ts +0 -80
  107. package/src/shared/ids-stream.spec.ts +0 -154
  108. package/src/shared/ids-stream.ts +0 -279
  109. package/src/shared/logger.ts +0 -47
  110. package/src/shared/sse-schema.spec.ts +0 -50
  111. package/src/shared/sse-schema.ts +0 -198
  112. package/src/shared/text-utils.spec.ts +0 -42
  113. package/src/shared/text-utils.ts +0 -65
  114. package/src/shared/types.ts +0 -109
  115. package/src/styles/standalone.entry.css +0 -51
@@ -1,319 +0,0 @@
1
- /**
2
- * Module-resident session and React hook for the daily-report ids stream.
3
- * 日報 ID ストリームのモジュール常駐セッションと React フック。
4
- *
5
- * セッション (クライアント実体 + 完走時刻) をモジュールスコープに置くことで、
6
- * ページのアンマウント/再マウントをまたいで受信済みデータが生き残り、
7
- * 再訪時はローディング画面なしで即座に一覧を復元できる。鮮度は
8
- * 「完走から一定時間経過後の再訪でバックグラウンド再検証」(SWR) で担保する。
9
- * マウント中のリアルタイム性は SSE が担うため、これで欠落窓は実質「離脱中」のみになる。
10
- */
11
- import { useSyncExternalStore } from "react"
12
- import type { DailyReportItem } from "../../shared/types"
13
- import { defaultDailyReportClientConfig } from "../config-context"
14
- import { clearDailyReportCache } from "../hooks/use-daily-report"
15
- import { DailyReportIdsStreamClient, type DailyReportIdsStreamState } from "./daily-report-ids-stream-client"
16
-
17
- /** 完走からこの時間を過ぎた再訪で、バックグラウンド再検証を走らせる。 */
18
- const REVALIDATE_AFTER_MS = 60_000
19
-
20
- /** セッション 1 世代分の記録。 */
21
- type SessionRecord = {
22
- client: DailyReportIdsStreamClient
23
- /** セッションの持ち主 (難読化ユーザー ID。未登録ユーザーは null)。 */
24
- userKey: string | null
25
- /** 正規化済み API ベースパス (既定値解決済み)。 */
26
- apiBasePath: string
27
- unsubscribe: () => void
28
- }
29
-
30
- let session: SessionRecord | null = null
31
-
32
- /** 可視性/フォーカスのリスナーを登録済みか (モジュール寿命で 1 組だけ持つ)。 */
33
- let freshnessListenersBound = false
34
-
35
- /** セッションの生成/破棄とクライアント publish の両方を伝えるリスナー群。 */
36
- const sessionListeners = new Set<() => void>()
37
-
38
- /** セッション未生成時に返す不変スナップショット (SSR の getServerSnapshot も兼ねる)。 */
39
- const IDLE_STATE: DailyReportIdsStreamState = {
40
- items: [],
41
- loadedCount: 0,
42
- totalCount: null,
43
- phase: "idle",
44
- isRevalidating: false,
45
- hasCompletedOnce: false,
46
- completedAt: null,
47
- error: null,
48
- }
49
-
50
- /**
51
- * Full-document reload indirection (mockable seam for auth-expiry tests).
52
- * ドキュメント全体を再読込する間接層 (認証切れテストで差し替え可能にする継ぎ目)。
53
- */
54
- export const reloadDocument = (): void => {
55
- if (typeof window !== "undefined") {
56
- window.location.reload()
57
- }
58
- }
59
-
60
- /** 全セッションリスナーへ変更を通知する。 */
61
- const notifySessionListeners = (): void => {
62
- for (const listener of sessionListeners) {
63
- listener()
64
- }
65
- }
66
-
67
- /** 現行セッションを破棄する (ユーザー切替・HMR・テストリセット用)。 */
68
- const destroySession = (): void => {
69
- if (!session) return
70
- session.unsubscribe()
71
- session.client.dispose()
72
- session = null
73
- notifySessionListeners()
74
- }
75
-
76
- /**
77
- * Ensures a stream session for the given user, creating/refreshing as needed.
78
- * 指定ユーザーのストリームセッションを確立する処理 (必要に応じ生成/再検証)。
79
- *
80
- * - ユーザーまたは API パスが変わっていれば作り直す (別ユーザーのデータを見せない)。
81
- * - failed なら自動 retry (再訪 = ユーザーの再試行意思とみなす)。
82
- * - 完走から {@link REVALIDATE_AFTER_MS} 経過していればバックグラウンド再検証。
83
- *
84
- * SSR では何もしない。clientLoader とページマウントの両方から呼ばれ、冪等。
85
- *
86
- * @param options.apiBasePath API ベースパス (config の apiBasePath)。
87
- * @param options.userKey 難読化ユーザー ID (未登録ユーザーは null)。
88
- */
89
- export const ensureDailyReportIdsStreamSession = (options: { apiBasePath?: string; userKey: string | null }): void => {
90
- if (typeof window === "undefined") return
91
-
92
- // 呼び出し元によって undefined / 既定文字列が混在するため、比較前に必ず正規化する。
93
- // 正規化を欠くと clientLoader (undefined) とページ effect (既定文字列) の往復で
94
- // セッションが毎回破棄され、モジュール常駐による即時復元が丸ごと無効化される
95
- const apiBasePath = options.apiBasePath ?? defaultDailyReportClientConfig.apiBasePath
96
-
97
- if (session && (session.userKey !== options.userKey || session.apiBasePath !== apiBasePath)) {
98
- // ユーザーが変わる場合はモジュール常駐の詳細キャッシュも破棄する
99
- // (旧ユーザーが閲覧した下書き詳細等が TTL 内の新ユーザーへ供給されるのを防ぐ)
100
- if (session.userKey !== options.userKey) {
101
- clearDailyReportCache()
102
- }
103
- destroySession()
104
- }
105
-
106
- if (!session) {
107
- const client = new DailyReportIdsStreamClient({ apiBasePath })
108
- session = {
109
- client,
110
- userKey: options.userKey,
111
- apiBasePath,
112
- // 通知はそのままセッションリスナーへ中継する (完走時刻はクライアントが state に刻む)
113
- unsubscribe: client.subscribe(notifySessionListeners),
114
- }
115
- // 再訪以外にも新規走査の起動経路を確保する (可視化復帰 / フォーカス取得)
116
- ensureFreshnessListeners()
117
- client.start()
118
- notifySessionListeners()
119
- return
120
- }
121
-
122
- runFreshnessCheck(session)
123
- }
124
-
125
- /**
126
- * Retries a failed session or starts a background revalidation when stale.
127
- * failed セッションの retry、または鮮度切れ時のバックグラウンド再検証を起動する処理。
128
- *
129
- * 再検証が既に走行中 (`isRevalidating`) の間は何もしない。ガードを欠くと、1 回の
130
- * 再訪で走る複数の確立呼び出し (clientLoader の prime / ensure / ページ effect) が
131
- * それぞれ走行中の再検証を中断して先頭からやり直し、NDJSON を多重ダウンロードする。
132
- */
133
- const runFreshnessCheck = (record: SessionRecord): void => {
134
- const state = record.client.getState()
135
- if (state.phase === "failed") {
136
- record.client.retry()
137
- return
138
- }
139
- if (state.phase === "complete" && !state.isRevalidating && state.completedAt !== null && Date.now() - state.completedAt > REVALIDATE_AFTER_MS) {
140
- record.client.refresh()
141
- }
142
- }
143
-
144
- /**
145
- * Registers the visibility/focus listeners that drive freshness checks (idempotent).
146
- * 鮮度チェックを駆動する可視性/フォーカスリスナーを登録する処理 (冪等)。
147
- *
148
- * ❗ これが無いと**新規走査を起動する経路が「再訪 (ナビゲーション)」しか無くなる**。
149
- * `REVALIDATE_AFTER_MS` はタイマーではなく再訪時の閾値なので、タブを開いたまま放置する
150
- * という本ページの主要な使われ方では新しい走査が二度と走らず、走査時点の不整合
151
- * (削除済み行の残留・作成された行の欠落) がセッション中ずっと残り続ける。
152
- *
153
- * 定期タイマーは置かない。開いているタブの数だけ全量走査が周期的に走り、サーバー負荷が
154
- * 利用者の意図と無関係に増える。可視化復帰とフォーカス取得は「利用者が今この画面を見た」
155
- * という意図そのものであり、頻度も自然に抑えられる (`runFreshnessCheck` 側で完走からの
156
- * 経過時間と再検証中フラグにより二重に間引かれる)。
157
- */
158
- const ensureFreshnessListeners = (): void => {
159
- if (freshnessListenersBound || typeof document === "undefined" || typeof window === "undefined") {
160
- return
161
- }
162
- freshnessListenersBound = true
163
- // セッション破棄後に発火してもよいよう、ハンドラ側で存在チェックする
164
- // (リスナーはモジュール寿命で 1 組だけ持ち、付け外しをしない)
165
- const check = (): void => {
166
- if (session) {
167
- runFreshnessCheck(session)
168
- }
169
- }
170
- document.addEventListener("visibilitychange", () => {
171
- if (document.visibilityState === "visible") {
172
- check()
173
- }
174
- })
175
- window.addEventListener("focus", check)
176
- }
177
-
178
- /**
179
- * Bootstraps the session from the clientLoader: create when absent, else reconcile.
180
- * clientLoader からのセッション起動処理。不在なら生成、既存なら照合と鮮度確認。
181
- *
182
- * コールドロードの最重要ゲート対策: 生成をページマウントの effect まで遅らせると、
183
- * ストリームの最初のバイト要求が「serverLoader 完了 → ハイドレーション → effect flush」
184
- * の後ろに直列化される。ローダー時点で生成すればハイドレーションと並行してフェッチが走る。
185
- *
186
- * パス契約: 生成時のパスは factory オプション (未指定は既定値へ正規化)。
187
- * `config.apiBasePath` だけを上書きした利用者では初回のみ誤パスで生成されるが、
188
- * ページマウント側の ensure がパス不一致を検知して作り直す (自己修復・初回 1 回きり)。
189
- * **既存セッションのパスはここでは照合しない** — 照合すると、factory 未指定 ×
190
- * config 上書きの組み合わせでナビゲーションごとにセッションが破棄されるスラッシングになる。
191
- *
192
- * @param options.apiBasePath 生成時に使う API ベースパス (未指定は既定値)。
193
- * @param options.userKey 難読化ユーザー ID (未登録ユーザーは null)。
194
- */
195
- export const bootstrapDailyReportIdsStreamSession = (options: { apiBasePath?: string; userKey: string | null }): void => {
196
- if (typeof window === "undefined") return
197
- if (!session) {
198
- ensureDailyReportIdsStreamSession(options)
199
- return
200
- }
201
- if (session.userKey !== options.userKey) {
202
- // 別ユーザーのデータは描画前に破棄し (詳細キャッシュ含む)、新ユーザーのストリームを即時開始する
203
- clearDailyReportCache()
204
- destroySession()
205
- ensureDailyReportIdsStreamSession(options)
206
- return
207
- }
208
- runFreshnessCheck(session)
209
- }
210
-
211
- /**
212
- * Kicks a freshness check for an existing session before the user is known.
213
- * ユーザー確定前に既存セッションの鮮度確認だけを先行させる処理。
214
- *
215
- * clientLoader が serverLoader を await する前に呼び、再訪 (支配的経路) の
216
- * 再検証開始を数十〜数百 ms 前倒しする。セッションが無ければ何もしない
217
- * (誰のデータを読むべきか分からないため)。直後の reconcile が userKey を照合し、
218
- * ユーザーが変わっていればセッションごと破棄されるので先行は安全。
219
- * 走行中の再検証は中断しない (runFreshnessCheck の isRevalidating ガード)。
220
- */
221
- export const primeDailyReportIdsStreamSession = (): void => {
222
- if (typeof window === "undefined" || !session) return
223
- runFreshnessCheck(session)
224
- }
225
-
226
- /**
227
- * Relays an externally-known item upsert into the resident stream data.
228
- * 外部由来のアイテム upsert を常駐ストリームデータへ中継する処理。
229
- *
230
- * プロバイダーの作成/公開系ミューテーション (ローカル操作と SSE) から呼ばれる。
231
- * これを欠くと、鮮度窓内の再訪で「作成したばかりの日報が消える」。
232
- */
233
- export const upsertDailyReportIdsStreamItem = (item: DailyReportItem): void => {
234
- session?.client.applyExternalUpsert(item)
235
- }
236
-
237
- /**
238
- * Relays an externally-known item removal into the resident stream data.
239
- * 外部由来のアイテム削除を常駐ストリームデータへ中継する処理。
240
- *
241
- * report-delete SSE・ローカル削除・stale 安全弁から呼ばれる。
242
- * これを欠くと、鮮度窓内の再訪で「削除したはずの日報が蘇る」。
243
- */
244
- export const removeDailyReportIdsStreamItem = (reportHubId: number): void => {
245
- session?.client.applyExternalRemoval(reportHubId)
246
- }
247
-
248
- /**
249
- * Restarts a failed stream (UI retry button entry point).
250
- * failed ストリームを再開する処理 (UI の再試行ボタン入口)。
251
- */
252
- export const retryDailyReportIdsStream = (): void => {
253
- session?.client.retry()
254
- }
255
-
256
- /**
257
- * Re-validates the stream, optionally bypassing the server-side cache.
258
- * ストリームを再検証する処理 (サーバーキャッシュのバイパス指定可)。
259
- *
260
- * 開発向けリロード/キャッシュクリアボタンから呼ばれる。
261
- *
262
- * @param options.forceRefresh サーバー側 SqlResultCache をバイパスするか。
263
- */
264
- export const refreshDailyReportIdsStream = (options: { forceRefresh?: boolean } = {}): void => {
265
- session?.client.refresh(options)
266
- }
267
-
268
- /**
269
- * Returns the current session's owner key (undefined when no session exists).
270
- * 現行セッションの持ち主 (userKey) を返す処理 (セッション不在時は undefined)。
271
- *
272
- * 遅延解決するミューテーション継続の門番に使う。POST /action の待機中にリロード無しの
273
- * ユーザー切替が起きると、切替時の三重遮断 (セッション破棄 + 詳細キャッシュ破棄 +
274
- * プロバイダーリマウント) の**後**に旧ユーザーの継続が解決し、モジュール常駐状態
275
- * (新ユーザーのセッション・詳細キャッシュ) へ旧ユーザーのデータを注入してしまう。
276
- * 継続側は開始時点の userKey とこの値を比較し、不一致 (undefined 含む) なら
277
- * モジュール常駐状態への書き込みを放棄する (fail-closed)。
278
- */
279
- export const getDailyReportIdsStreamSessionUserKey = (): string | null | undefined => session?.userKey
280
-
281
- /** セッション購読 (useSyncExternalStore 用)。 */
282
- const subscribeSession = (listener: () => void): (() => void) => {
283
- sessionListeners.add(listener)
284
- return () => sessionListeners.delete(listener)
285
- }
286
-
287
- /** 現在のスナップショットを返す (セッション未生成時は不変の IDLE)。 */
288
- const getSessionSnapshot = (): DailyReportIdsStreamState => session?.client.getState() ?? IDLE_STATE
289
-
290
- /** SSR スナップショット (常に IDLE)。 */
291
- const getServerSnapshot = (): DailyReportIdsStreamState => IDLE_STATE
292
-
293
- /**
294
- * React binding for the module-resident ids stream session.
295
- * モジュール常駐 ids ストリームセッションの React バインディング。
296
- *
297
- * セッションが後から生成されても再購読なしで追従する (リスナーはセッションではなく
298
- * モジュールに属する)。スナップショット参照は publish 単位で安定している。
299
- *
300
- * @returns Current stream state. 現在のストリーム状態。
301
- */
302
- export const useDailyReportIdsStream = (): DailyReportIdsStreamState => {
303
- return useSyncExternalStore(subscribeSession, getSessionSnapshot, getServerSnapshot)
304
- }
305
-
306
- /**
307
- * Destroys the session (test isolation helper).
308
- * セッションを破棄するテスト用ヘルパー。
309
- */
310
- export const resetDailyReportIdsStreamSessionForTesting = (): void => {
311
- destroySession()
312
- }
313
-
314
- // HMR: モジュール差し替え時に旧セッションの接続とリスナーを確実に始末する
315
- if (typeof import.meta !== "undefined" && (import.meta as { hot?: { dispose: (cb: () => void) => void } }).hot) {
316
- ;(import.meta as unknown as { hot: { dispose: (cb: () => void) => void } }).hot.dispose(() => {
317
- destroySession()
318
- })
319
- }
@@ -1,42 +0,0 @@
1
- import { Slot } from "@radix-ui/react-slot"
2
- import { cva, type VariantProps } from "class-variance-authority"
3
- import * as React from "react"
4
- import { cn } from "./cn"
5
-
6
- const buttonVariants = cva(
7
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
8
- {
9
- variants: {
10
- variant: {
11
- default: "bg-primary text-primary-foreground hover:bg-primary/90",
12
- destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
13
- outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
14
- secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
15
- ghost: "hover:bg-accent hover:text-accent-foreground",
16
- link: "text-primary underline-offset-4 hover:underline",
17
- },
18
- size: {
19
- default: "h-10 px-4 py-2",
20
- sm: "h-9 rounded-md px-3",
21
- lg: "h-11 rounded-md px-8",
22
- icon: "h-10 w-10",
23
- },
24
- },
25
- defaultVariants: {
26
- variant: "default",
27
- size: "default",
28
- },
29
- },
30
- )
31
-
32
- export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
33
- asChild?: boolean
34
- }
35
-
36
- const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, ...props }, ref) => {
37
- const Comp = asChild ? Slot : "button"
38
- return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
39
- })
40
- Button.displayName = "Button"
41
-
42
- export { Button, buttonVariants }
@@ -1,14 +0,0 @@
1
- /**
2
- * Tailwind class combiner (clsx + tailwind-merge).
3
- * Tailwind クラスを結合・重複解決するユーティリティ。
4
- */
5
- import { type ClassValue, clsx } from "clsx"
6
- import { twMerge } from "tailwind-merge"
7
-
8
- /**
9
- * Combines class values and resolves Tailwind conflicts.
10
- * クラス値を結合し Tailwind の衝突を解決する処理。
11
- */
12
- export function cn(...inputs: ClassValue[]) {
13
- return twMerge(clsx(inputs))
14
- }
@@ -1,21 +0,0 @@
1
- import type * as React from "react"
2
-
3
- import { cn } from "./cn"
4
-
5
- function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
- return (
7
- <input
8
- type={type}
9
- data-slot="input"
10
- className={cn(
11
- "flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs outline-none transition-[color,box-shadow] selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:font-medium file:text-foreground file:text-sm placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
12
- "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
13
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
14
- className,
15
- )}
16
- {...props}
17
- />
18
- )
19
- }
20
-
21
- export { Input }
@@ -1,16 +0,0 @@
1
- import * as LabelPrimitive from "@radix-ui/react-label"
2
- import type * as React from "react"
3
-
4
- import { cn } from "./cn"
5
-
6
- function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
7
- return (
8
- <LabelPrimitive.Root
9
- data-slot="label"
10
- className={cn("flex select-none items-center gap-2 font-medium text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50 group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50", className)}
11
- {...props}
12
- />
13
- )
14
- }
15
-
16
- export { Label }
@@ -1,19 +0,0 @@
1
- import * as SwitchPrimitives from "@radix-ui/react-switch"
2
- import * as React from "react"
3
-
4
- import { cn } from "./cn"
5
-
6
- const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.Root>, React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>>(({ className, ...props }, ref) => (
7
- <SwitchPrimitives.Root
8
- className={cn(
9
- "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
10
- className,
11
- )}
12
- {...props}
13
- ref={ref}>
14
- <SwitchPrimitives.Thumb className={cn("pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0")} />
15
- </SwitchPrimitives.Root>
16
- ))
17
- Switch.displayName = SwitchPrimitives.Root.displayName
18
-
19
- export { Switch }
@@ -1,40 +0,0 @@
1
- import * as TabsPrimitive from "@radix-ui/react-tabs"
2
- import * as React from "react"
3
-
4
- import { cn } from "./cn"
5
-
6
- const Tabs = TabsPrimitive.Root
7
-
8
- const TabsList = React.forwardRef<React.ElementRef<typeof TabsPrimitive.List>, React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>>(({ className, ...props }, ref) => (
9
- <TabsPrimitive.List ref={ref} className={cn("inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground", className)} {...props} />
10
- ))
11
- TabsList.displayName = TabsPrimitive.List.displayName
12
-
13
- const TabsTrigger = React.forwardRef<React.ElementRef<typeof TabsPrimitive.Trigger>, React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>>(({ className, ...props }, ref) => (
14
- <TabsPrimitive.Trigger
15
- ref={ref}
16
- className={cn(
17
- "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 font-medium text-sm ring-offset-background transition-all focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-2xs",
18
- className,
19
- )}
20
- {...props}
21
- />
22
- ))
23
- TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
24
-
25
- // https://github.com/radix-ui/primitives/issues/2359
26
- const TabsContent = React.forwardRef<React.ElementRef<typeof TabsPrimitive.Content>, React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>>(({ className, ...props }, ref) => (
27
- <TabsPrimitive.Content
28
- ref={ref}
29
- // forceMount={true} //<=======Add this line
30
- className={cn(
31
- "data-[state=inactive]:hidden", //<=========Add this line
32
- "mt-2 ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
33
- className,
34
- )}
35
- {...props}
36
- />
37
- ))
38
- TabsContent.displayName = TabsPrimitive.Content.displayName
39
-
40
- export { Tabs, TabsContent, TabsList, TabsTrigger }
@@ -1,19 +0,0 @@
1
- import type * as React from "react"
2
-
3
- import { cn } from "./cn"
4
-
5
- function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6
- return (
7
- <textarea
8
- data-slot="textarea"
9
- className={cn(
10
- "flex min-h-15 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
11
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
12
- className,
13
- )}
14
- {...props}
15
- />
16
- )
17
- }
18
-
19
- export { Textarea }
@@ -1,96 +0,0 @@
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
- })