@aiquants/daily-report 0.3.1 → 0.4.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 (36) hide show
  1. package/README.md +24 -22
  2. package/dist/client.d.mts +3 -10
  3. package/dist/client.d.ts +3 -10
  4. package/dist/client.js +4 -4
  5. package/dist/client.js.map +1 -1
  6. package/dist/client.mjs +4 -4
  7. package/dist/client.mjs.map +1 -1
  8. package/dist/index.d.mts +2 -2
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +1 -1
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/server.d.mts +1 -6
  15. package/dist/server.d.ts +1 -6
  16. package/dist/server.js +4 -4
  17. package/dist/server.js.map +1 -1
  18. package/dist/server.mjs +4 -4
  19. package/dist/server.mjs.map +1 -1
  20. package/dist/{sse-schema-DO9Qe8XF.d.ts → sse-schema-CK7cUnEo.d.ts} +1 -34
  21. package/dist/{sse-schema-DKUjXMW3.d.mts → sse-schema-yl5AaSsj.d.mts} +1 -34
  22. package/dist/styles/daily-report.standalone.css +3 -0
  23. package/dist/{types-DrjeK3_G.d.mts → types-CVhwLhSN.d.mts} +0 -1
  24. package/dist/{types-DrjeK3_G.d.ts → types-CVhwLhSN.d.ts} +0 -1
  25. package/package.json +21 -13
  26. package/src/client/components/daily-report-comment-item.tsx +4 -1
  27. package/src/client/components/daily-report-detail-list.tsx +16 -25
  28. package/src/client/components/daily-report-list.tsx +17 -24
  29. package/src/client/config-context.tsx +0 -9
  30. package/src/client/contexts/daily-report-action-context.tsx +0 -1
  31. package/src/server/cache.ts +0 -18
  32. package/src/server/service.ts +0 -2
  33. package/src/shared/sse-schema.ts +0 -1
  34. package/src/shared/types.ts +0 -1
  35. package/src/styles/standalone.entry.css +51 -0
  36. package/src/server/cache.spec.ts +0 -47
package/package.json CHANGED
@@ -1,8 +1,10 @@
1
1
  {
2
2
  "name": "@aiquants/daily-report",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Reusable daily-report feature package: shared types/schemas, React (Router v7) UI with virtual scrolling + optimistic updates + SSE sync, and a drizzle (mssql) server layer with DI ports (auth, user resolution, redis, external sources).",
5
- "sideEffects": false,
5
+ "sideEffects": [
6
+ "**/*.css"
7
+ ],
6
8
  "main": "dist/index.js",
7
9
  "module": "dist/index.mjs",
8
10
  "types": "dist/index.d.ts",
@@ -21,7 +23,8 @@
21
23
  "types": "./dist/server.d.ts",
22
24
  "import": "./dist/server.mjs",
23
25
  "require": "./dist/server.js"
24
- }
26
+ },
27
+ "./styles/daily-report.standalone.css": "./dist/styles/daily-report.standalone.css"
25
28
  },
26
29
  "files": [
27
30
  "dist",
@@ -52,6 +55,7 @@
52
55
  "tailwind-merge": "^3.5.0"
53
56
  },
54
57
  "devDependencies": {
58
+ "@tailwindcss/cli": "^4.1.18",
55
59
  "@testing-library/jest-dom": "^6.9.1",
56
60
  "@testing-library/react": "^16.3.2",
57
61
  "@types/node": "^25.9.1",
@@ -62,12 +66,14 @@
62
66
  "react-dom": "^19.2.7",
63
67
  "react-router": "^7.13.0",
64
68
  "rimraf": "^6.1.2",
69
+ "tailwindcss": "^4.1.18",
70
+ "tailwindcss-animate": "^1.0.7",
65
71
  "tsup": "^8.5.1",
66
72
  "typescript": "^5.9.3",
67
73
  "vitest": "^4.1.8",
68
74
  "zod": "^3.25.76",
69
- "@aiquants/swipe-overlay": "1.2.3",
70
- "@aiquants/virtualscroll": "1.18.3"
75
+ "@aiquants/swipe-overlay": "1.2.4",
76
+ "@aiquants/virtualscroll": "1.19.0"
71
77
  },
72
78
  "peerDependencies": {
73
79
  "drizzle-orm": ">=1.0.0-beta.4 <2.0.0",
@@ -75,8 +81,8 @@
75
81
  "react-dom": ">=18",
76
82
  "react-router": "^7.0.0",
77
83
  "zod": ">=3.25.0 <5.0.0",
78
- "@aiquants/swipe-overlay": "^1.2.3",
79
- "@aiquants/virtualscroll": "^1.18.3"
84
+ "@aiquants/virtualscroll": "^1.19.0",
85
+ "@aiquants/swipe-overlay": "^1.2.4"
80
86
  },
81
87
  "peerDependenciesMeta": {
82
88
  "drizzle-orm": {
@@ -91,14 +97,16 @@
91
97
  "access": "public"
92
98
  },
93
99
  "scripts": {
94
- "build": "tsup",
95
- "build:watch": "tsup --watch",
96
- "dev": "tsup --watch",
100
+ "build": "tsup && pnpm build:css",
101
+ "build:css": "tailwindcss -i src/styles/standalone.entry.css -o dist/styles/daily-report.standalone.css --minify",
102
+ "watch:css": "tailwindcss -i src/styles/standalone.entry.css -o dist/styles/daily-report.standalone.css --minify --watch",
103
+ "build:watch": "tsup --watch --onSuccess \"pnpm build:css\"",
104
+ "dev": "tsup --watch --onSuccess \"pnpm build:css\"",
97
105
  "typecheck": "tsc --noEmit",
98
106
  "clean": "rimraf dist",
99
- "publish:patch": "pnpm version patch --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
100
- "publish:minor": "pnpm version minor --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
101
- "publish:major": "pnpm version major --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
107
+ "publish:patch": "pnpm run typecheck && pnpm run --if-present test && pnpm version patch --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
108
+ "publish:minor": "pnpm run typecheck && pnpm run --if-present test && pnpm version minor --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
109
+ "publish:major": "pnpm run typecheck && pnpm run --if-present test && pnpm version major --no-git-tag-version --no-git-checks && pnpm publish --no-git-checks",
102
110
  "lint": "biome lint src/",
103
111
  "check": "biome check src/",
104
112
  "check:fix": "biome check --write src/",
@@ -51,7 +51,10 @@ export const DailyReportCommentItem = ({ comment, reportHubId: _reportHubId, bus
51
51
  }
52
52
 
53
53
  return (
54
- <li data-comment-id={comment.id} className={cn("group relative rounded-xl border border-slate-200 bg-white p-3", comment.isMine && "border-blue-200 bg-blue-50", isDeleted && "hidden", isPendingDelete ? "z-50" : "z-0 hover:z-10")}>
54
+ <li
55
+ data-comment-id={comment.id}
56
+ data-deleted={isDeleted ? "true" : undefined}
57
+ className={cn("group relative rounded-xl border border-slate-200 bg-white p-3", comment.isMine && "border-blue-200 bg-blue-50", isDeleted && "hidden", isPendingDelete ? "z-50" : "z-0 hover:z-10")}>
55
58
  <div className="mb-1 flex items-center justify-between">
56
59
  <div className={cn("font-semibold text-xs", resolveCommentColorClass(comment.color))}>{fallbackText(comment.authorName)}</div>
57
60
  <div className="flex items-center gap-2">
@@ -19,12 +19,11 @@ import { Check, Star, Trash2 } from "lucide-react"
19
19
  import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react"
20
20
  import { fallbackText, formatToIsoDate } from "../../shared/text-utils"
21
21
  import type { DailyReportDetail, DailyReportItem } from "../../shared/types"
22
- import { type DailyReportFieldLabels, type SourceTypeConfig, useDailyReportConfig } from "../config-context"
22
+ import { type DailyReportFieldLabels, useDailyReportConfig } from "../config-context"
23
23
  import { useDailyReportActionContext } from "../contexts/daily-report-action-context"
24
24
  import { useDailyReportDetail, useDailyReportPrefetch } from "../hooks/use-daily-report"
25
25
  import { useDailyReportComments } from "../hooks/use-daily-report-comments"
26
26
  import { useDynamicViewportHeight } from "../hooks/use-dynamic-viewport-height"
27
- import { cn } from "../ui/cn"
28
27
  import { EMPTY_ITEM, FAST_SCROLL_INERTIA_OPTIONS, FAST_SCROLL_WHEEL_MULTIPLIER, PREFETCH_LOOKAHEAD, PREFETCH_MAX_DATES, VIRTUAL_SCROLL_OVERSCAN_COUNT } from "../utils/constants"
29
28
  import { BusinessDayThumbOverlay } from "./business-day-thumb-overlay"
30
29
  import { DailyReportCommentForm, DailyReportCommentList } from "./daily-report-comment-section"
@@ -120,7 +119,7 @@ export const DailyReportDetailList = ({ dailyReportItems, userId, selectedItemId
120
119
 
121
120
  useEffect(() => {
122
121
  if (virtualScrollRef.current) {
123
- const el = document.querySelector(".test-daily-report-detail-list")
122
+ const el = document.querySelector('[data-testid="daily-report-detail-list"]')
124
123
  if (el) {
125
124
  const handle = virtualScrollRef.current
126
125
  const extendedHandle: DailyReportVirtualScrollHandle = Object.assign({}, handle, {
@@ -185,8 +184,8 @@ export const DailyReportDetailList = ({ dailyReportItems, userId, selectedItemId
185
184
  const container = scrollContainerRef.current
186
185
  if (!container) return
187
186
 
188
- if (!container.classList.contains("is-scrolling")) {
189
- container.classList.add("is-scrolling")
187
+ if (container.dataset.scrolling !== "true") {
188
+ container.dataset.scrolling = "true"
190
189
  }
191
190
 
192
191
  if (scrollEndTimerRef.current) {
@@ -195,7 +194,7 @@ export const DailyReportDetailList = ({ dailyReportItems, userId, selectedItemId
195
194
 
196
195
  scrollEndTimerRef.current = setTimeout(() => {
197
196
  if (container) {
198
- container.classList.remove("is-scrolling")
197
+ delete container.dataset.scrolling
199
198
  }
200
199
  scrollEndTimerRef.current = null
201
200
  }, 150)
@@ -306,7 +305,8 @@ export const DailyReportDetailList = ({ dailyReportItems, userId, selectedItemId
306
305
  }}
307
306
  onRangeChange={handleRangeChange}
308
307
  onScroll={handleScroll}
309
- className="t-detail-list test-daily-report-detail-list relative w-full">
308
+ testId="daily-report-detail-list"
309
+ className="relative w-full">
310
310
  {renderItem}
311
311
  </VirtualScroll>
312
312
  </div>
@@ -439,7 +439,6 @@ const DailyReportHeader = ({
439
439
  visitTimeDisplay,
440
440
  categoryLabel,
441
441
  fieldLabels,
442
- sourceTypeConfigs,
443
442
  }: {
444
443
  report: DailyReportDetail
445
444
  isRead: boolean
@@ -451,22 +450,8 @@ const DailyReportHeader = ({
451
450
  visitTimeDisplay: string
452
451
  categoryLabel: string
453
452
  fieldLabels: DailyReportFieldLabels
454
- sourceTypeConfigs?: Record<string, SourceTypeConfig>
455
453
  }) => (
456
454
  <div className="flex flex-wrap items-center gap-2 font-medium text-slate-600 text-xs">
457
- {(() => {
458
- const sourceType = report.sourceType
459
- const sourceConfig = sourceType ? sourceTypeConfigs?.[sourceType] : undefined
460
- return sourceConfig ? <span className={cn("inline-flex items-center rounded px-2 py-0.5 font-semibold text-[10px] tracking-wide", sourceConfig.badgeClassName)}>{sourceConfig.label}</span> : null
461
- })()}
462
- <button
463
- type="button"
464
- onClick={onToggleStar}
465
- className="flex h-6 w-6 items-center justify-center rounded-full transition-colors hover:bg-slate-200 focus:outline-none dark:hover:bg-slate-700"
466
- title={isStarred ? "スターを外す" : "スターを付ける"}
467
- data-detail-star-button={report.reportHubId}>
468
- {isStarred ? <Star className="h-3.5 w-3.5 fill-yellow-400 text-yellow-400" /> : <Star className="h-3.5 w-3.5 text-slate-300 dark:text-slate-500" />}
469
- </button>
470
455
  <button
471
456
  type="button"
472
457
  onClick={onToggleRead}
@@ -475,6 +460,14 @@ const DailyReportHeader = ({
475
460
  data-detail-read-button={report.reportHubId}>
476
461
  {isRead ? <Check className="h-3.5 w-3.5 text-blue-500" /> : <UnreadIndicator className="text-xs" />}
477
462
  </button>
463
+ <button
464
+ type="button"
465
+ onClick={onToggleStar}
466
+ className="flex h-6 w-6 items-center justify-center rounded-full transition-colors hover:bg-slate-200 focus:outline-none dark:hover:bg-slate-700"
467
+ title={isStarred ? "スターを外す" : "スターを付ける"}
468
+ data-detail-star-button={report.reportHubId}>
469
+ {isStarred ? <Star className="h-3.5 w-3.5 fill-yellow-400 text-yellow-400" /> : <Star className="h-3.5 w-3.5 text-slate-300 dark:text-slate-500" />}
470
+ </button>
478
471
  {isMine && (
479
472
  <button type="button" onClick={onDelete} data-detail-delete-button={report.reportHubId} className="text-lg leading-none transition-transform focus:outline-none active:scale-125" title="削除する">
480
473
  <Trash2 className="h-4 w-4 text-slate-300 hover:text-red-500" />
@@ -484,7 +477,6 @@ const DailyReportHeader = ({
484
477
  <span className="text-[10px] text-blue-500 uppercase tracking-wide">{fieldLabels.id}</span>
485
478
  {report.reportHubId}
486
479
  </span>
487
-
488
480
  <span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-slate-100 px-3 py-1">
489
481
  <span className="text-[10px] text-slate-400 uppercase tracking-wide">{fieldLabels.businessDate}</span>
490
482
  {formatToIsoDate(report.date)}
@@ -560,7 +552,7 @@ const DailyReportResolvedItem = ({ item, report, onCommentAdded, userId, onDoubl
560
552
  // 表示中ユーザーはアクションコンテキストから取得する (root loader 非依存)
561
553
  const { toggleStar, toggleRead, pendingStarUpdates, pendingReadUpdates, deleteReport, user } = useDailyReportActionContext()
562
554
  const { uiComments, handleAddComment, handleDeleteComment, pendingDeleteId, setPendingDeleteId, resolvedIdMap } = useDailyReportComments(report, user, userId)
563
- const { fieldLabels, sourceTypeConfigs } = useDailyReportConfig()
555
+ const { fieldLabels } = useDailyReportConfig()
564
556
 
565
557
  const handleAddCommentWrapper = async (content: string) => {
566
558
  await handleAddComment(content)
@@ -618,7 +610,6 @@ const DailyReportResolvedItem = ({ item, report, onCommentAdded, userId, onDoubl
618
610
  visitTimeDisplay={visitTimeDisplay}
619
611
  categoryLabel={categoryLabel}
620
612
  fieldLabels={fieldLabels}
621
- sourceTypeConfigs={sourceTypeConfigs}
622
613
  />
623
614
  <div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_240px]">
624
615
  <div className="flex flex-col gap-4">
@@ -13,7 +13,6 @@ import { useDailyReportActionContext } from "../contexts/daily-report-action-con
13
13
  import { useDailyReportDetail } from "../hooks/use-daily-report"
14
14
  import { useDailyReportComments } from "../hooks/use-daily-report-comments"
15
15
  import { useDynamicViewportHeight } from "../hooks/use-dynamic-viewport-height"
16
- import { cn } from "../ui/cn"
17
16
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"
18
17
  import {
19
18
  DEFAULT_ITEM_HEIGHT,
@@ -353,7 +352,7 @@ export const DailyReportList = ({ dailyReportItems, initialSelectedBusinessDate
353
352
 
354
353
  useEffect(() => {
355
354
  if (virtualScrollRef.current) {
356
- const el = document.querySelector(".test-daily-report-list") as (Element & { __virtualScroll?: TestableVirtualScrollHandle }) | null
355
+ const el = document.querySelector('[data-testid="daily-report-list"]') as (Element & { __virtualScroll?: TestableVirtualScrollHandle }) | null
357
356
  if (el) {
358
357
  el.__virtualScroll = {
359
358
  ...virtualScrollRef.current,
@@ -403,8 +402,8 @@ export const DailyReportList = ({ dailyReportItems, initialSelectedBusinessDate
403
402
  const container = scrollContainerRef.current
404
403
  if (!container) return
405
404
 
406
- if (!container.classList.contains("is-scrolling")) {
407
- container.classList.add("is-scrolling")
405
+ if (container.dataset.scrolling !== "true") {
406
+ container.dataset.scrolling = "true"
408
407
  }
409
408
 
410
409
  if (scrollEndTimerRef.current) {
@@ -413,7 +412,7 @@ export const DailyReportList = ({ dailyReportItems, initialSelectedBusinessDate
413
412
 
414
413
  scrollEndTimerRef.current = setTimeout(() => {
415
414
  if (container) {
416
- container.classList.remove("is-scrolling")
415
+ delete container.dataset.scrolling
417
416
  }
418
417
  scrollEndTimerRef.current = null
419
418
  }, 150)
@@ -445,7 +444,8 @@ export const DailyReportList = ({ dailyReportItems, initialSelectedBusinessDate
445
444
  onScroll={handleScroll}
446
445
  onItemFocus={handleItemFocus}
447
446
  contentInsets={{ top: ITEM_VERTICAL_PADDING, bottom: ITEM_VERTICAL_PADDING }}
448
- className="test-daily-report-list relative w-full">
447
+ testId="daily-report-list"
448
+ className="relative w-full">
449
449
  {(item) => <ListReportItem key={item.reportHubId} item={item} isActive={item.reportHubId === selectedReportHubId} onSelect={handleItemSelect} />}
450
450
  </VirtualScroll>
451
451
  </div>
@@ -467,7 +467,7 @@ export const DailyReportList = ({ dailyReportItems, initialSelectedBusinessDate
467
467
  */
468
468
  const ListSkeletonItem = () => (
469
469
  <div className="px-2 pb-3" style={{ height: ITEM_CONTAINER_HEIGHT }}>
470
- <div className="flex h-full animate-pulse flex-col justify-between rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
470
+ <div data-skeleton className="flex h-full animate-pulse flex-col justify-between rounded-2xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-700 dark:bg-slate-900/70">
471
471
  <div className="mb-3 flex flex-wrap items-center gap-3">
472
472
  <div className="h-4 w-24 rounded bg-slate-200"></div>
473
473
  <div className="h-4 w-24 rounded bg-slate-200"></div>
@@ -505,7 +505,7 @@ const DailyReportSidePane = ({
505
505
  }) => {
506
506
  const [activeTab, setActiveTab] = useState<DailyReportSidePaneTab>("article")
507
507
  const [isEditing, setIsEditing] = useState(false)
508
- const { draftLabelName, fieldLabels, sourceTypeConfigs } = useDailyReportConfig()
508
+ const { draftLabelName, fieldLabels } = useDailyReportConfig()
509
509
  const { report, error, isLoading } = useDailyReportDetail(selectedItem?.reportHubId ?? -1, selectedItem?.businessDate ?? null)
510
510
 
511
511
  const scrollContainerRef = useRef<HTMLDivElement>(null)
@@ -570,12 +570,7 @@ const DailyReportSidePane = ({
570
570
  <div className="text-slate-900 text-sm dark:text-slate-100">{headerDate}</div>
571
571
  <div className="flex min-h-4 items-center gap-1 text-slate-500 text-xs dark:text-slate-300">
572
572
  <span>{fieldLabels.author}:</span>
573
- {showHeaderSkeleton ? <div className="h-3 w-24 animate-pulse rounded bg-slate-200 dark:bg-slate-700" /> : <span>{headerCreator}</span>}
574
- {(() => {
575
- const sourceType = report?.sourceType
576
- const sourceConfig = sourceType ? sourceTypeConfigs?.[sourceType] : undefined
577
- return sourceConfig ? <span className={cn("ml-2 inline-flex shrink-0 items-center rounded px-2 py-0.5 font-semibold text-[10px] tracking-wide", sourceConfig.badgeClassName)}>{sourceConfig.label}</span> : null
578
- })()}
573
+ {showHeaderSkeleton ? <div data-skeleton className="h-3 w-24 animate-pulse rounded bg-slate-200 dark:bg-slate-700" /> : <span>{headerCreator}</span>}
579
574
  </div>
580
575
  </div>
581
576
  <Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as DailyReportSidePaneTab)} className="flex min-h-0 flex-1 flex-col">
@@ -622,14 +617,18 @@ const DailyReportSidePane = ({
622
617
  // If rendered as a standalone pane (desktop)
623
618
  if (dailyReportItems) {
624
619
  return (
625
- <aside data-report-id={selectedItem?.reportHubId} className="test-daily-report-side-pane 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" style={heightStyle}>
620
+ <aside
621
+ data-testid="daily-report-side-pane"
622
+ data-report-id={selectedItem?.reportHubId}
623
+ 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"
624
+ style={heightStyle}>
626
625
  {content}
627
626
  </aside>
628
627
  )
629
628
  }
630
629
  // If rendered inside mobile overlay
631
630
  return (
632
- <div data-report-id={selectedItem?.reportHubId} className="test-daily-report-side-pane flex min-h-0 flex-1 flex-col gap-3 overflow-hidden">
631
+ <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">
633
632
  {content}
634
633
  </div>
635
634
  )
@@ -728,7 +727,7 @@ const DailyReportDisplayPane = ({
728
727
  const isMine = !!(userId && report?.userId === userId)
729
728
  const hasComments = uiComments.length > 0
730
729
 
731
- const SkeletonText = ({ className = "w-32" }: { className?: string }) => <div className={`h-4 animate-pulse rounded bg-slate-200 dark:bg-slate-700 ${className}`} />
730
+ const SkeletonText = ({ className = "w-32" }: { className?: string }) => <div data-skeleton className={`h-4 animate-pulse rounded bg-slate-200 dark:bg-slate-700 ${className}`} />
732
731
 
733
732
  return (
734
733
  // biome-ignore lint/a11y/noStaticElementInteractions: Double click to edit feature
@@ -910,7 +909,6 @@ const extractPreviewLines = (content: string | null): string[] => {
910
909
  const ListReportItem = ({ item, isActive, onSelect }: { item: DailyReportItem; isActive: boolean; onSelect: (id: number) => void }) => {
911
910
  const { report, error, isLoading, isRefetching } = useDailyReportDetail(item.reportHubId, item.businessDate)
912
911
  const { toggleStar, toggleRead, removeStaleItem } = useDailyReportActionContext()
913
- const { sourceTypeConfigs } = useDailyReportConfig()
914
912
 
915
913
  // フェッチが一度でも試行されたかを追跡する ref
916
914
  const hasEverFetched = useRef(false)
@@ -980,7 +978,7 @@ const ListReportItem = ({ item, isActive, onSelect }: { item: DailyReportItem; i
980
978
  {/* biome-ignore lint/a11y/useSemanticElements: Card contains nested interactive elements (Star/Read buttons) */}
981
979
  <div
982
980
  data-daily-report-button={item.reportHubId}
983
- className={`flex h-full w-full in-[.is-scrolling]:transform-none cursor-pointer select-none flex-col rounded-2xl border in-[.is-scrolling]:border-slate-200 bg-white p-4 text-left in-[.is-scrolling]:shadow-sm shadow-sm in-[.is-scrolling]:transition-none transition-all duration-200 hover:-translate-y-0.5 hover:border-blue-300 hover:shadow-lg dark:bg-slate-900/70 ${
981
+ 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-all duration-200 hover:-translate-y-0.5 hover:border-blue-300 hover:shadow-lg dark:bg-slate-900/70 ${
984
982
  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"
985
983
  }`}
986
984
  style={{ touchAction: "manipulation" }}
@@ -996,11 +994,6 @@ const ListReportItem = ({ item, isActive, onSelect }: { item: DailyReportItem; i
996
994
  <span className="truncate font-medium text-slate-900 dark:text-slate-100">{creator}</span>
997
995
  </span>
998
996
  <div className="ml-auto flex items-center gap-1.5">
999
- {(() => {
1000
- const sourceType = report?.sourceType
1001
- const sourceConfig = sourceType ? sourceTypeConfigs?.[sourceType] : undefined
1002
- return sourceConfig ? <span className={cn("inline-flex shrink-0 items-center rounded px-2 py-0.5 font-semibold text-[10px] tracking-wide", sourceConfig.badgeClassName)}>{sourceConfig.label}</span> : null
1003
- })()}
1004
997
  <button
1005
998
  type="button"
1006
999
  title={report.isStarred ? "スターを外す" : "スターを付ける"}
@@ -38,11 +38,6 @@ export type DailyReportFieldLabels = {
38
38
  relationsEmpty: string
39
39
  }
40
40
 
41
- export type SourceTypeConfig = {
42
- label: string
43
- badgeClassName?: string
44
- }
45
-
46
41
  /** クライアント層の設定一式。 */
47
42
  export type DailyReportClientConfig = {
48
43
  /** 画面タイトル (ヘッダーへ渡す)。 */
@@ -55,8 +50,6 @@ export type DailyReportClientConfig = {
55
50
  draftLabelName: string
56
51
  /** 詳細/一覧カードのフィールド見出しラベル。既定は中立英語。アプリが自ドメインの表記を注入する。 */
57
52
  fieldLabels: DailyReportFieldLabels
58
- /** 外部ソース区分ごとのバッジ表示設定。 */
59
- sourceTypeConfigs?: Record<string, SourceTypeConfig>
60
53
  /** ヘッダー描画スロット。アプリのヘッダーコンポーネントを差し込む。 */
61
54
  renderHeader: (props: DailyReportHeaderProps) => ReactNode
62
55
  /** 開発向けコントロール (SSE 購読トグル等) を表示するか。 */
@@ -69,7 +62,6 @@ export type DailyReportClientConfig = {
69
62
  */
70
63
  export type DailyReportClientConfigInput = Partial<Omit<DailyReportClientConfig, "fieldLabels">> & {
71
64
  fieldLabels?: Partial<DailyReportFieldLabels>
72
- sourceTypeConfigs?: Record<string, SourceTypeConfig>
73
65
  }
74
66
 
75
67
  /** 中立英語のフィールドラベル既定 (ドメイン非依存)。 */
@@ -102,7 +94,6 @@ export const defaultDailyReportClientConfig: DailyReportClientConfig = {
102
94
  // 中立プレースホルダー。実運用のラベル名 (アプリ固有) は Provider / config prop で上書きする。
103
95
  draftLabelName: "draft",
104
96
  fieldLabels: defaultDailyReportFieldLabels,
105
- sourceTypeConfigs: {},
106
97
  renderHeader: ({ title, annotation }) => (
107
98
  <header className="flex flex-wrap items-baseline justify-between gap-2 px-2 py-1">
108
99
  <span className="text-nowrap text-slate-700 text-sm dark:text-slate-200">{title}</span>
@@ -703,7 +703,6 @@ export const DailyReportActionProvider = ({ children, user, initialItems = [], u
703
703
  createdAt: now,
704
704
  author: getUserDisplayName(user),
705
705
  userId: user?.id ?? "unknown",
706
- sourceType: "Internal",
707
706
  employeeName: getUserDisplayName(user),
708
707
  updatedBy: getUserDisplayName(user),
709
708
  updatedAt: now,
@@ -123,10 +123,6 @@ export class SqlResultCache {
123
123
  // キャッシュ保存時に現在の epoch を記録
124
124
  const epoch = opts.epochKey ? await this.epochStore.getEpoch(opts.epochKey) : 0
125
125
  this.buckets.set(cacheKey, { records, expireAt, epoch })
126
- // 蓄積防止のため、サイズが一定値を超えたら期限切れキャッシュを一括クリーンアップ
127
- if (this.buckets.size > 500) {
128
- this.cleanExpired()
129
- }
130
126
  }
131
127
  return records
132
128
  })()
@@ -166,18 +162,4 @@ export class SqlResultCache {
166
162
  }
167
163
  }
168
164
  }
169
-
170
- /**
171
- * Cleans up all expired cache buckets to prevent memory accumulation.
172
- * メモリー蓄積を防ぐため、期限切れのキャッシュバケットをすべてクリーンアップする処理。
173
- */
174
- private cleanExpired(): void {
175
- const now = Date.now()
176
- // 期限切れのキーを削除
177
- for (const [key, bucket] of this.buckets.entries()) {
178
- if (bucket.expireAt <= now) {
179
- this.buckets.delete(key)
180
- }
181
- }
182
- }
183
165
  }
@@ -326,7 +326,6 @@ export function createDailyReportService(config: DailyReportServiceConfig) {
326
326
  date: formatDateValue(hub.businessDate, "YYYY-MM-DD"),
327
327
  author: row.creatorName ?? maskAuditActor(hub.createdBy) ?? "",
328
328
  userId: hub.userId ? encodeUserId(hub.userId) : "",
329
- sourceType: hub.sourceType ?? "Internal",
330
329
  createdAt: formatDateValue(hub.createdAt, "YYYY-MM-DD HH:mm:ss"),
331
330
  updatedAt: formatDateValue(hub.updatedAt, "YYYY-MM-DD HH:mm:ss"),
332
331
  updatedBy: maskAuditActor(hub.updatedBy),
@@ -1035,7 +1034,6 @@ export function createDailyReportService(config: DailyReportServiceConfig) {
1035
1034
  createdAt: formatDateValue(hub.createdAt, "YYYY-MM-DD HH:mm:ss"),
1036
1035
  author: userName,
1037
1036
  userId: encodeUserId(userId),
1038
- sourceType: "Internal",
1039
1037
  employeeName: userName,
1040
1038
  // 監査列と同様、クライアント公開時は生内部 ID を難読化する (mapHubRecord と整合)
1041
1039
  updatedBy: encodeUserId(userId),
@@ -39,7 +39,6 @@ export const dailyReportDetailSchema = z.object({
39
39
  createdAt: z.string().nullable(),
40
40
  author: z.string(),
41
41
  userId: z.string(),
42
- sourceType: z.string().nullable(),
43
42
  employeeName: z.string().nullable(),
44
43
  updatedBy: z.string().nullable(),
45
44
  updatedAt: z.string().nullable(),
@@ -46,7 +46,6 @@ export type DailyReportDetail = {
46
46
  createdAt: string | null
47
47
  author: string
48
48
  userId: string
49
- sourceType: string | null
50
49
  employeeName: string | null
51
50
  updatedBy: string | null
52
51
  updatedAt: string | null
@@ -0,0 +1,51 @@
1
+ /*! @aiquants/daily-report standalone CSS — 非 Tailwind ホスト専用。ホストの Tailwind ビルドと混在させないこと */
2
+ @layer theme, base, components, utilities;
3
+ @import "tailwindcss/theme.css" layer(theme);
4
+ /* source(none): 自動ソース検出を無効化し、src 外のクラス混入を防ぐ */
5
+ @import "tailwindcss/utilities.css" layer(utilities) source(none);
6
+ /*
7
+ * virtualscroll の CSS は同梱しない: ホスト側で import 済みの新しいコピーを、
8
+ * ここに焼き込んだ古いコピーが後勝ちで上書きする事故 (stale-copy-wins) を防ぐため。
9
+ * ホストアプリは @aiquants/virtualscroll/styles/virtualscroll.css を自前で import すること。
10
+ */
11
+ @source "../**/*.{ts,tsx}";
12
+
13
+ /* JSX は .dark クラス切替の dark: バリアントを使うため、ホストアプリと同じ定義が必要 */
14
+ @plugin "tailwindcss-animate";
15
+ @custom-variant dark (&:where(.dark, .dark *));
16
+
17
+ /*
18
+ * ホストアプリ (quants-react-router) の @theme と同じトークン定義を複製する。
19
+ * bg-background / text-muted-foreground などのセマンティックトークンは
20
+ * Tailwind 既定テーマに存在しないため、定義しないとユーティリティ自体が生成されない。
21
+ * 値はホストの CSS 変数 (--background 等) を参照し、実行時にホスト側で解決される。
22
+ */
23
+ @theme {
24
+ --color-border: hsl(var(--border));
25
+ --color-input: hsl(var(--input));
26
+ --color-ring: hsl(var(--ring));
27
+ --color-background: hsl(var(--background));
28
+ --color-foreground: hsl(var(--foreground));
29
+ --color-primary: hsl(var(--primary));
30
+ --color-primary-foreground: hsl(var(--primary-foreground));
31
+ --color-secondary: hsl(var(--secondary));
32
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
33
+ --color-destructive: hsl(var(--destructive));
34
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
35
+ --color-muted: hsl(var(--muted));
36
+ --color-muted-foreground: hsl(var(--muted-foreground));
37
+ --color-accent: hsl(var(--accent));
38
+ --color-accent-foreground: hsl(var(--accent-foreground));
39
+ --color-popover: hsl(var(--popover));
40
+ --color-popover-foreground: hsl(var(--popover-foreground));
41
+ --color-card: hsl(var(--card));
42
+ --color-card-foreground: hsl(var(--card-foreground));
43
+ --breakpoint-sm: 640px;
44
+ --breakpoint-md: 768px;
45
+ --breakpoint-lg: 1024px;
46
+ --breakpoint-xl: 1280px;
47
+ --breakpoint-2xl: 1536px;
48
+ --radius-lg: var(--radius, 0.5rem);
49
+ --radius-md: calc(var(--radius, 0.5rem) - 2px);
50
+ --radius-sm: calc(var(--radius, 0.5rem) - 4px);
51
+ }
@@ -1,47 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest"
2
- import { createEpochStore, SqlResultCache } from "./cache"
3
-
4
- describe("SqlResultCache Memory Cleanup", () => {
5
- it("should clean up expired cache entries when map size exceeds 500", async () => {
6
- // Mock epoch store
7
- const epochStore = createEpochStore(undefined)
8
- vi.spyOn(epochStore, "getEpoch").mockResolvedValue(0)
9
-
10
- const cache = new SqlResultCache({ defaultTtlMs: 1000 }, epochStore)
11
-
12
- // 1. 期限切れのキャッシュを 500 件作成 (expireAt を過去に設定)
13
- // 直接プライベートフィールド buckets に流し込むことでモック状態を作る
14
- const buckets = (cache as any).buckets
15
- for (let i = 0; i < 505; i++) {
16
- buckets.set(`expired-key-${i}`, {
17
- records: [i],
18
- expireAt: Date.now() - 1000, // 過去時間
19
- epoch: 0,
20
- })
21
- }
22
-
23
- // 2. 有効なキャッシュを 10 件作成 (expireAt を未来に設定)
24
- for (let i = 0; i < 10; i++) {
25
- buckets.set(`valid-key-${i}`, {
26
- records: [i],
27
- expireAt: Date.now() + 100000, // 未来時間
28
- epoch: 0,
29
- })
30
- }
31
-
32
- expect(buckets.size).toBe(515)
33
-
34
- // 3. getOrFetch を実行して 500 件超えの閾値トリガーを引く
35
- const fetcher = vi.fn().mockResolvedValue(["new-value"])
36
- await cache.getOrFetch({
37
- cacheKey: "trigger-key",
38
- fetcher,
39
- })
40
-
41
- // 4. 検証: 期限切れの 505 件のデータが cleanExpired で削除され、有効な 10 件 + 新規の 1 件のみ残っていること
42
- expect(buckets.size).toBe(11) // 10 (valid) + 1 (trigger-key)
43
- expect(buckets.has("trigger-key")).toBe(true)
44
- expect(buckets.has("valid-key-0")).toBe(true)
45
- expect(buckets.has("expired-key-0")).toBe(false)
46
- })
47
- })