@striae-org/striae 6.0.1 → 6.1.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 (40) hide show
  1. package/app/components/actions/case-export/core-export.ts +11 -2
  2. package/app/components/actions/case-export/download-handlers.ts +3 -1
  3. package/app/components/canvas/canvas.module.css +1 -1
  4. package/app/components/canvas/canvas.tsx +32 -11
  5. package/app/components/icon/icons.svg +1 -1
  6. package/app/components/icon/manifest.json +1 -1
  7. package/app/components/navbar/navbar.tsx +10 -9
  8. package/app/components/sidebar/cases/case-sidebar.tsx +6 -1
  9. package/app/components/sidebar/files/files-modal.tsx +39 -15
  10. package/app/components/sidebar/notes/addl-notes-modal.tsx +9 -2
  11. package/app/components/sidebar/notes/{class-details/class-details-fields.tsx → item-details/item-details-fields.tsx} +10 -10
  12. package/app/components/sidebar/notes/{class-details/class-details-modal.tsx → item-details/item-details-modal.tsx} +20 -22
  13. package/app/components/sidebar/notes/{class-details/class-details-sections.tsx → item-details/item-details-sections.tsx} +16 -16
  14. package/app/components/sidebar/notes/{class-details/class-details-shared.ts → item-details/item-details-shared.ts} +4 -3
  15. package/app/components/sidebar/notes/{class-details/use-class-details-state.ts → item-details/use-item-details-state.ts} +4 -4
  16. package/app/components/sidebar/notes/notes-editor-form.tsx +333 -124
  17. package/app/components/sidebar/notes/notes-editor-modal.tsx +3 -0
  18. package/app/components/sidebar/notes/notes.module.css +40 -20
  19. package/app/components/sidebar/sidebar-container.tsx +1 -0
  20. package/app/components/sidebar/sidebar.tsx +3 -0
  21. package/app/components/toolbar/toolbar.tsx +5 -5
  22. package/app/hooks/useFileListPreferences.ts +22 -17
  23. package/app/routes/striae/striae.tsx +4 -10
  24. package/app/types/annotations.ts +28 -5
  25. package/app/utils/data/confirmation-summary/summary-core.ts +40 -8
  26. package/app/utils/data/file-filters.ts +39 -17
  27. package/package.json +139 -139
  28. package/workers/audit-worker/package.json +2 -2
  29. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  30. package/workers/data-worker/package.json +2 -2
  31. package/workers/data-worker/wrangler.jsonc.example +1 -1
  32. package/workers/image-worker/package.json +2 -2
  33. package/workers/image-worker/wrangler.jsonc.example +1 -1
  34. package/workers/pdf-worker/package.json +2 -2
  35. package/workers/pdf-worker/src/formats/format-striae.ts +65 -8
  36. package/workers/pdf-worker/src/report-types.ts +13 -1
  37. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  38. package/workers/user-worker/package.json +2 -2
  39. package/workers/user-worker/wrangler.jsonc.example +1 -1
  40. package/wrangler.toml.example +1 -1
@@ -63,17 +63,26 @@ export async function exportCaseData(
63
63
  try {
64
64
  annotations = await getNotes(user, caseNumber, file.id) || undefined;
65
65
 
66
- // Check if file has any annotation data beyond just defaults
66
+ // Check if file has any annotation data beyond just defaults.
67
+ // Includes left/right split fields and legacy single-value fields as fallbacks.
67
68
  hasAnnotations = !!(annotations && (
68
69
  annotations.additionalNotes ||
70
+ annotations.leftAdditionalNotes ||
71
+ annotations.rightAdditionalNotes ||
69
72
  annotations.classNote ||
73
+ annotations.leftClassNote ||
74
+ annotations.rightClassNote ||
70
75
  annotations.customClass ||
76
+ annotations.leftCustomClass ||
77
+ annotations.rightCustomClass ||
71
78
  annotations.leftCase ||
72
79
  annotations.rightCase ||
73
80
  annotations.leftItem ||
74
81
  annotations.rightItem ||
82
+ annotations.leftItemType ||
83
+ annotations.rightItemType ||
75
84
  annotations.supportLevel ||
76
- annotations.classType ||
85
+ annotations.itemType ||
77
86
  (annotations.boxAnnotations && annotations.boxAnnotations.length > 0)
78
87
  ));
79
88
 
@@ -95,10 +95,12 @@ export async function downloadCaseAsZip(
95
95
  exportData.metadata.exportedByName ||
96
96
  exportData.metadata.exportedBy ||
97
97
  'Unknown';
98
+ // Don't add forensic warning comment to encrypted content; it will break JSON parsing on decryption.
99
+ // The archive package already includes forensic metadata in README.txt and FORENSIC_MANIFEST.json.
98
100
  const caseJsonContent = await generateJSONContent(
99
101
  exportData,
100
102
  options.includeUserInfo,
101
- protectForensicData
103
+ false
102
104
  );
103
105
 
104
106
  const archivePackage = await buildArchivePackage({
@@ -187,7 +187,7 @@
187
187
  letter-spacing: 0.5px;
188
188
  }
189
189
 
190
- /* Class Characteristics Display */
190
+ /* Item Type Display */
191
191
  .classCharacteristics {
192
192
  position: absolute;
193
193
  bottom: calc(100% + 0.5rem);
@@ -167,7 +167,8 @@ export const Canvas = ({
167
167
  }, [imageUrl, resetImageLoadState]);
168
168
 
169
169
  useEffect(() => {
170
- if (!activeAnnotations?.has('class') || !annotationData?.hasSubclass) {
170
+ const hasAnySubclass = annotationData?.leftHasSubclass || annotationData?.rightHasSubclass || annotationData?.hasSubclass;
171
+ if (!activeAnnotations?.has('item') || !hasAnySubclass) {
171
172
  const flashResetTimer = window.setTimeout(() => {
172
173
  clearFlashingState();
173
174
  }, 0);
@@ -187,7 +188,7 @@ export const Canvas = ({
187
188
  }, 60000);
188
189
 
189
190
  return () => clearInterval(flashInterval);
190
- }, [activeAnnotations, annotationData?.hasSubclass, clearFlashingState]);
191
+ }, [activeAnnotations, annotationData?.leftHasSubclass, annotationData?.rightHasSubclass, annotationData?.hasSubclass, clearFlashingState]);
191
192
 
192
193
  const getErrorMessage = () => {
193
194
  if (error) return error;
@@ -302,15 +303,35 @@ export const Canvas = ({
302
303
  <div className={styles.imageAndNotesContainer}>
303
304
  <div className={styles.imageContainer}>
304
305
  <div className={styles.imageWrapper}>
305
- {/* Class Characteristics - Above Image */}
306
- {activeAnnotations?.has('class') && annotationData && (annotationData.customClass || annotationData.classType) && (
307
- <div className={styles.classCharacteristics}>
308
- <div className={styles.classText}>
309
- {annotationData.customClass || annotationData.classType}
310
- {annotationData.classNote && ` (${annotationData.classNote})`}
306
+ {/* Item Type - Above Image */}
307
+ {activeAnnotations?.has('item') && annotationData && (() => {
308
+ // Resolve display values from left/right fields, falling back to legacy single-set fields.
309
+ // When both sides are populated and differ, combine them as "Left / Right".
310
+ // classType is a legacy field kept for backward compat with older annotations (also handled in PDF generation).
311
+ const leftValue = annotationData.leftCustomClass || annotationData.leftItemType;
312
+ const rightValue = annotationData.rightCustomClass || annotationData.rightItemType;
313
+ const legacyValue = annotationData.customClass || annotationData.itemType || annotationData.classType;
314
+ const displayValue =
315
+ leftValue && rightValue && leftValue !== rightValue
316
+ ? `${leftValue} / ${rightValue}`
317
+ : leftValue || rightValue || legacyValue;
318
+ const leftClassNote = annotationData.leftClassNote?.trim();
319
+ const rightClassNote = annotationData.rightClassNote?.trim();
320
+ const legacyClassNote = annotationData.classNote?.trim();
321
+ const displayClassNote =
322
+ leftClassNote && rightClassNote && leftClassNote !== rightClassNote
323
+ ? `${leftClassNote} / ${rightClassNote}`
324
+ : leftClassNote || rightClassNote || legacyClassNote;
325
+ if (!displayValue) return null;
326
+ return (
327
+ <div className={styles.classCharacteristics}>
328
+ <div className={styles.classText}>
329
+ {displayValue}
330
+ {displayClassNote && ` (${displayClassNote})`}
331
+ </div>
311
332
  </div>
312
- </div>
313
- )}
333
+ );
334
+ })()}
314
335
 
315
336
  <img
316
337
  ref={imageRef}
@@ -440,7 +461,7 @@ export const Canvas = ({
440
461
  )}
441
462
 
442
463
  {/* Subclass Warning - Bottom Right of Canvas */}
443
- {activeAnnotations?.has('class') && annotationData?.hasSubclass && (
464
+ {activeAnnotations?.has('item') && annotationData && (annotationData.leftHasSubclass || annotationData.rightHasSubclass || annotationData.hasSubclass) && (
444
465
  <div className={`${styles.subclassWarning} ${isFlashing ? styles.flashing : ''}`}>
445
466
  <div className={styles.subclassText}>
446
467
  POTENTIAL SUBCLASS
@@ -3,7 +3,7 @@
3
3
  <symbol id="number" viewBox="0 0 24 24">
4
4
  <path d="M1,9h24M1,17h24M10,1l-3,24M19,1l-3,24" style="stroke:#1e1e1e; stroke-linecap:round; stroke-linejoin:round; stroke-width:2px;" />
5
5
  </symbol>
6
- <symbol id="class" viewBox="0 0 24 24">
6
+ <symbol id="item" viewBox="0 0 24 24">
7
7
  <path d="M12,24c-1.66,0-3.22-.31-4.68-.95-1.46-.63-2.73-1.48-3.81-2.56s-1.94-2.35-2.57-3.81c-.63-1.46-.94-3.02-.94-4.68s.31-3.22.94-4.68,1.49-2.73,2.57-3.81,2.35-1.94,3.81-2.57,3.02-.94,4.68-.94,3.22.31,4.68.94c1.46.63,2.73,1.49,3.81,2.57s1.94,2.35,2.56,3.81c.63,1.46.95,3.02.95,4.68s-.31,3.22-.95,4.68c-.63,1.46-1.48,2.73-2.56,3.81s-2.35,1.94-3.81,2.56c-1.46.63-3.02.95-4.68.95ZM12,21.6c2.68,0,4.95-.93,6.81-2.79s2.79-4.13,2.79-6.81-.93-4.95-2.79-6.81-4.13-2.79-6.81-2.79-4.95.93-6.81,2.79-2.79,4.13-2.79,6.81.93,4.95,2.79,6.81,4.13,2.79,6.81,2.79Z" />
8
8
  <path d="M12.29,15.89c-.5,0-.97-.09-1.4-.28-.44-.19-.82-.45-1.14-.77-.32-.32-.58-.71-.77-1.14-.19-.44-.28-.91-.28-1.4s.09-.97.28-1.4c.19-.44.45-.82.77-1.14.32-.32.7-.58,1.14-.77.44-.19.91-.28,1.4-.28s.97.09,1.4.28c.44.19.82.45,1.14.77.32.32.58.7.77,1.14.19.44.28.91.28,1.4s-.09.97-.28,1.4c-.19.44-.45.82-.77,1.14-.32.32-.71.58-1.14.77-.44.19-.91.28-1.4.28ZM12.29,15.17c.8,0,1.49-.28,2.04-.84s.84-1.24.84-2.04-.28-1.48-.84-2.04-1.24-.84-2.04-.84-1.48.28-2.04.84-.84,1.24-.84,2.04.28,1.49.84,2.04,1.24.84,2.04.84Z" />
9
9
  </symbol>
@@ -3,7 +3,7 @@
3
3
  "width": 24,
4
4
  "height": 24
5
5
  },
6
- "class": {
6
+ "item": {
7
7
  "width": 24,
8
8
  "height": 24
9
9
  },
@@ -117,11 +117,12 @@ export const Navbar = ({
117
117
  const disableLongRunningCaseActions = isUploading;
118
118
  const isCaseManagementActive = true;
119
119
  const isFileManagementActive = isFileMenuOpen || hasLoadedImage;
120
- const canOpenImageNotes = hasLoadedImage && !isCurrentImageConfirmed && !isReadOnly;
120
+ const canOpenImageNotes = hasLoadedImage;
121
+ const isImageNotesReadOnly = isReadOnly || isCurrentImageConfirmed || isUploading;
121
122
  const isImageNotesActive = canOpenImageNotes;
122
123
  const canDeleteCurrentFile = hasLoadedImage && !isReadOnly;
123
- const isArchivedRegularReadOnly = Boolean(isReadOnly && archiveDetails?.archived && !isReviewOnlyCase);
124
- const caseExportLabel = isArchivedRegularReadOnly
124
+ const isArchivedCase = Boolean(isReadOnly && archiveDetails?.archived);
125
+ const caseExportLabel = isArchivedCase
125
126
  ? 'Export Archive'
126
127
  : isReadOnly
127
128
  ? 'Export Confirmations'
@@ -184,13 +185,15 @@ export const Navbar = ({
184
185
  type="button"
185
186
  role="menuitem"
186
187
  className={`${styles.caseMenuItem} ${styles.caseMenuItemExport}`}
187
- disabled={!hasLoadedCase || disableLongRunningCaseActions}
188
+ disabled={!hasLoadedCase || disableLongRunningCaseActions || (isArchivedCase && isReviewOnlyCase)}
188
189
  title={
189
190
  !hasLoadedCase
190
191
  ? 'Load a case to export case data'
191
192
  : disableLongRunningCaseActions
192
193
  ? 'Export is unavailable while files are uploading'
193
- : undefined
194
+ : isArchivedCase && isReviewOnlyCase
195
+ ? 'Cannot export imported archive packages'
196
+ : undefined
194
197
  }
195
198
  onClick={() => {
196
199
  onOpenCaseExport?.();
@@ -365,10 +368,8 @@ export const Navbar = ({
365
368
  title={
366
369
  !hasLoadedImage
367
370
  ? 'Load an image to enable image notes'
368
- : isCurrentImageConfirmed
369
- ? 'Confirmed images are read-only and viewable via toolbar only'
370
- : isReadOnly
371
- ? 'Image notes are disabled for read-only cases'
371
+ : isImageNotesReadOnly
372
+ ? 'Image notes are view-only in this state'
372
373
  : undefined
373
374
  }
374
375
  onClick={() => {
@@ -26,6 +26,7 @@ interface CaseSidebarProps {
26
26
  setFiles: React.Dispatch<React.SetStateAction<FileData[]>>;
27
27
  currentCase: string | null;
28
28
  isReadOnly?: boolean;
29
+ isReviewOnlyCase?: boolean;
29
30
  isArchivedCase?: boolean;
30
31
  isConfirmed?: boolean;
31
32
  confirmationSaveVersion?: number;
@@ -47,6 +48,7 @@ export const CaseSidebar = ({
47
48
  setFiles,
48
49
  currentCase,
49
50
  isReadOnly = false,
51
+ isReviewOnlyCase = false,
50
52
  isArchivedCase = false,
51
53
  isConfirmed = false,
52
54
  confirmationSaveVersion = 0,
@@ -260,11 +262,14 @@ const handleImageSelect = (file: FileData) => {
260
262
 
261
263
  const showCaseExportButton = Boolean(currentCase && isReadOnly);
262
264
  const caseExportButtonLabel = isArchivedCase ? 'Export Archive' : 'Export Confirmations';
265
+ const isImportedArchive = isArchivedCase && isReviewOnlyCase;
263
266
 
264
267
  const exportCaseTitle = isUploading
265
268
  ? 'Cannot export while uploading'
266
269
  : !currentCase
267
270
  ? 'Load a case first'
271
+ : isImportedArchive
272
+ ? 'Cannot export imported archive packages'
268
273
  : undefined;
269
274
 
270
275
  return (
@@ -389,7 +394,7 @@ return (
389
394
  <button
390
395
  className={styles.confirmationExportButton}
391
396
  onClick={onOpenCaseExport}
392
- disabled={isUploading || !currentCase}
397
+ disabled={isUploading || !currentCase || isImportedArchive}
393
398
  title={exportCaseTitle}
394
399
  >
395
400
  {caseExportButtonLabel}
@@ -9,7 +9,7 @@ import {
9
9
  import {
10
10
  type FilesModalSortBy,
11
11
  type FilesModalConfirmationFilter,
12
- type FilesModalClassTypeFilter,
12
+ type FilesModalItemTypeFilter,
13
13
  getFilesForModal,
14
14
  } from '~/utils/data/file-filters';
15
15
  import { deleteFile } from '~/components/actions/image-manage';
@@ -61,12 +61,24 @@ function formatDate(dateString: string): string {
61
61
  return new Date(parsed).toLocaleDateString();
62
62
  }
63
63
 
64
- function getClassTypeLabel(classType?: FileConfirmationSummary['classType']): string {
65
- if (!classType) {
64
+ function getItemTypeLabel(summary: FileConfirmationSummary): string {
65
+ const itemTypes = [
66
+ summary.leftItemType,
67
+ summary.rightItemType,
68
+ summary.itemType,
69
+ ].filter((value): value is NonNullable<FileConfirmationSummary['itemType']> => Boolean(value));
70
+
71
+ const uniqueItemTypes = Array.from(new Set(itemTypes));
72
+
73
+ if (uniqueItemTypes.length === 0) {
66
74
  return 'Unset';
67
75
  }
68
76
 
69
- return classType;
77
+ if (uniqueItemTypes.length === 1) {
78
+ return uniqueItemTypes[0];
79
+ }
80
+
81
+ return `${uniqueItemTypes[0]} / ${uniqueItemTypes[1]}`;
70
82
  }
71
83
 
72
84
  function getConfirmationLabel(summary: FileConfirmationSummary): string {
@@ -110,7 +122,7 @@ export const FilesModal = ({
110
122
  preferences,
111
123
  setSortBy,
112
124
  setConfirmationFilter,
113
- setClassTypeFilter,
125
+ setItemTypeFilter,
114
126
  resetPreferences,
115
127
  } = useFileListPreferences();
116
128
  const {
@@ -125,7 +137,7 @@ export const FilesModal = ({
125
137
  const hasCustomPreferences =
126
138
  preferences.sortBy !== DEFAULT_FILES_MODAL_PREFERENCES.sortBy ||
127
139
  preferences.confirmationFilter !== DEFAULT_FILES_MODAL_PREFERENCES.confirmationFilter ||
128
- preferences.classTypeFilter !== DEFAULT_FILES_MODAL_PREFERENCES.classTypeFilter;
140
+ preferences.itemTypeFilter !== DEFAULT_FILES_MODAL_PREFERENCES.itemTypeFilter;
129
141
 
130
142
  const existingFileIdSet = useMemo(
131
143
  () => new Set(files.map((file) => file.id)),
@@ -162,6 +174,9 @@ export const FilesModal = ({
162
174
  (effectiveCurrentPage + 1) * FILES_PER_PAGE
163
175
  );
164
176
 
177
+ const shouldForceItemTypeSummaryRefresh =
178
+ preferences.sortBy === 'itemType' || preferences.itemTypeFilter !== 'all';
179
+
165
180
  useEffect(() => {
166
181
  let isCancelled = false;
167
182
 
@@ -173,7 +188,9 @@ export const FilesModal = ({
173
188
  return;
174
189
  }
175
190
 
176
- const caseSummary = await ensureCaseConfirmationSummary(user, currentCase, files).catch((err) => {
191
+ const caseSummary = await ensureCaseConfirmationSummary(user, currentCase, files, {
192
+ forceRefresh: shouldForceItemTypeSummaryRefresh,
193
+ }).catch((err) => {
177
194
  console.error(`Error fetching confirmation summary for case ${currentCase}:`, err);
178
195
  return null;
179
196
  });
@@ -190,7 +207,14 @@ export const FilesModal = ({
190
207
  return () => {
191
208
  isCancelled = true;
192
209
  };
193
- }, [isOpen, currentCase, files, user, confirmationSaveVersion]);
210
+ }, [
211
+ isOpen,
212
+ currentCase,
213
+ files,
214
+ user,
215
+ confirmationSaveVersion,
216
+ shouldForceItemTypeSummaryRefresh,
217
+ ]);
194
218
 
195
219
  const toggleDeleteSelection = (fileId: string) => {
196
220
  setDeleteSelectedFileIds((previous) => {
@@ -339,7 +363,7 @@ export const FilesModal = ({
339
363
  <option value="recent">Date Uploaded</option>
340
364
  <option value="filename">File Name</option>
341
365
  <option value="confirmation">Confirmation Status</option>
342
- <option value="classType">Class Type</option>
366
+ <option value="itemType">Item Type</option>
343
367
  </select>
344
368
  </div>
345
369
 
@@ -361,12 +385,12 @@ export const FilesModal = ({
361
385
  </div>
362
386
 
363
387
  <div className={styles.controlGroup}>
364
- <label htmlFor="files-class-filter">Class Type</label>
388
+ <label htmlFor="files-item-filter">Item Type</label>
365
389
  <select
366
- id="files-class-filter"
367
- value={preferences.classTypeFilter}
390
+ id="files-item-filter"
391
+ value={preferences.itemTypeFilter}
368
392
  onChange={(event) => {
369
- setClassTypeFilter(event.target.value as FilesModalClassTypeFilter);
393
+ setItemTypeFilter(event.target.value as FilesModalItemTypeFilter);
370
394
  setCurrentPage(0);
371
395
  }}
372
396
  >
@@ -434,7 +458,7 @@ export const FilesModal = ({
434
458
  const isOpenSelected = effectiveOpenSelectedFileId === file.id;
435
459
  const isDeleteSelected = effectiveDeleteSelectedFileIds.has(file.id);
436
460
  const confirmationLabel = getConfirmationLabel(summary);
437
- const classTypeLabel = getClassTypeLabel(summary.classType);
461
+ const itemTypeLabel = getItemTypeLabel(summary);
438
462
 
439
463
  let confirmationClass = '';
440
464
  if (summary.includeConfirmation) {
@@ -472,7 +496,7 @@ export const FilesModal = ({
472
496
  </div>
473
497
  <div className={styles.fileMetaRow}>
474
498
  <span className={styles.fileDate}>Uploaded: {formatDate(file.uploadedAt)}</span>
475
- <span className={styles.classTypeBadge}>Class: {classTypeLabel}</span>
499
+ <span className={styles.classTypeBadge}>Item: {itemTypeLabel}</span>
476
500
  </div>
477
501
  </div>
478
502
 
@@ -7,10 +7,11 @@ interface AddlNotesModalProps {
7
7
  onClose: () => void;
8
8
  notes: string;
9
9
  onSave: (notes: string) => void;
10
+ isReadOnly?: boolean;
10
11
  showNotification?: (message: string, type: 'success' | 'error' | 'warning') => void;
11
12
  }
12
13
 
13
- export const AddlNotesModal = ({ isOpen, onClose, notes, onSave, showNotification }: AddlNotesModalProps) => {
14
+ export const AddlNotesModal = ({ isOpen, onClose, notes, onSave, isReadOnly = false, showNotification }: AddlNotesModalProps) => {
14
15
  const [tempNotes, setTempNotes] = useState(notes);
15
16
  const [isSaving, setIsSaving] = useState(false);
16
17
 
@@ -31,6 +32,11 @@ export const AddlNotesModal = ({ isOpen, onClose, notes, onSave, showNotificatio
31
32
  if (!isOpen) return null;
32
33
 
33
34
  const handleSave = async () => {
35
+ if (isReadOnly) {
36
+ showNotification?.('This case is read-only. Notes cannot be modified.', 'error');
37
+ return;
38
+ }
39
+
34
40
  setIsSaving(true);
35
41
  try {
36
42
  await Promise.resolve(onSave(tempNotes));
@@ -58,12 +64,13 @@ export const AddlNotesModal = ({ isOpen, onClose, notes, onSave, showNotificatio
58
64
  onChange={(e) => setTempNotes(e.target.value)}
59
65
  className={styles.modalTextarea}
60
66
  placeholder="Enter additional notes..."
67
+ disabled={isReadOnly}
61
68
  />
62
69
  <div className={styles.modalButtons}>
63
70
  <button
64
71
  onClick={handleSave}
65
72
  className={styles.saveButton}
66
- disabled={isSaving}
73
+ disabled={isSaving || isReadOnly}
67
74
  aria-busy={isSaving}
68
75
  >
69
76
  {isSaving ? 'Saving...' : 'Save'}
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import styles from '../notes.module.css';
3
- import { CUSTOM, handleSelectWithCustom } from './class-details-shared';
3
+ import { CUSTOM, handleSelectWithCustom } from './item-details-shared';
4
4
 
5
5
  interface BaseFieldProps {
6
6
  label: string;
@@ -37,7 +37,7 @@ interface CheckboxFieldProps {
37
37
  }
38
38
 
39
39
  const fieldClassName = (fullWidth = false): string =>
40
- fullWidth ? `${styles.classDetailsField} ${styles.classDetailsFieldFull}` : styles.classDetailsField;
40
+ fullWidth ? `${styles.itemDetailsField} ${styles.itemDetailsFieldFull}` : styles.itemDetailsField;
41
41
 
42
42
  export const TextField = ({
43
43
  label,
@@ -50,14 +50,14 @@ export const TextField = ({
50
50
  min,
51
51
  }: TextFieldProps) => (
52
52
  <div className={fieldClassName(fullWidth)}>
53
- <span className={styles.classDetailsLabel}>{label}</span>
53
+ <span className={styles.itemDetailsLabel}>{label}</span>
54
54
  <input
55
55
  type={type}
56
56
  min={min}
57
57
  aria-label={label}
58
58
  value={value}
59
59
  onChange={(event) => onChange(event.target.value)}
60
- className={styles.classDetailsInput}
60
+ className={styles.itemDetailsInput}
61
61
  disabled={disabled}
62
62
  placeholder={placeholder}
63
63
  />
@@ -74,12 +74,12 @@ export const SelectField = ({
74
74
  fullWidth = false,
75
75
  }: SelectFieldProps) => (
76
76
  <div className={fieldClassName(fullWidth)}>
77
- <span className={styles.classDetailsLabel}>{label}</span>
77
+ <span className={styles.itemDetailsLabel}>{label}</span>
78
78
  <select
79
79
  aria-label={label}
80
80
  value={value}
81
81
  onChange={(event) => onChange(event.target.value)}
82
- className={styles.classDetailsInput}
82
+ className={styles.itemDetailsInput}
83
83
  disabled={disabled}
84
84
  >
85
85
  <option value="">{placeholder}</option>
@@ -101,12 +101,12 @@ export const SelectWithCustomField = ({
101
101
  fullWidth = false,
102
102
  }: SelectWithCustomFieldProps) => (
103
103
  <div className={fieldClassName(fullWidth)}>
104
- <span className={styles.classDetailsLabel}>{label}</span>
104
+ <span className={styles.itemDetailsLabel}>{label}</span>
105
105
  <select
106
106
  aria-label={label}
107
107
  value={isCustom ? CUSTOM : value}
108
108
  onChange={(event) => handleSelectWithCustom(event.target.value, onChange, onCustomChange)}
109
- className={styles.classDetailsInput}
109
+ className={styles.itemDetailsInput}
110
110
  disabled={disabled}
111
111
  >
112
112
  <option value="">{placeholder}</option>
@@ -119,7 +119,7 @@ export const SelectWithCustomField = ({
119
119
  aria-label={label}
120
120
  value={value}
121
121
  onChange={(event) => onChange(event.target.value)}
122
- className={styles.classDetailsInput}
122
+ className={styles.itemDetailsInput}
123
123
  disabled={disabled}
124
124
  placeholder={customPlaceholder}
125
125
  />
@@ -133,7 +133,7 @@ export const CheckboxField = ({
133
133
  onChange,
134
134
  disabled,
135
135
  }: CheckboxFieldProps) => (
136
- <label className={styles.classDetailsCheckboxLabel}>
136
+ <label className={styles.itemDetailsCheckboxLabel}>
137
137
  <input
138
138
  type="checkbox"
139
139
  aria-label={label}
@@ -3,18 +3,16 @@ import type {
3
3
  BulletAnnotationData,
4
4
  CartridgeCaseAnnotationData,
5
5
  ShotshellAnnotationData,
6
+ ItemType,
6
7
  } from '~/types/annotations';
7
- import {
8
- type ClassType,
9
- } from './class-details-shared';
10
- import { BulletSection, CartridgeCaseSection, ShotshellSection } from './class-details-sections';
11
- import { useClassDetailsState } from './use-class-details-state';
8
+ import { BulletSection, CartridgeCaseSection, ShotshellSection } from './item-details-sections';
9
+ import { useItemDetailsState } from './use-item-details-state';
12
10
  import styles from '../notes.module.css';
13
11
 
14
- interface ClassDetailsModalProps {
12
+ interface ItemDetailsModalProps {
15
13
  isOpen: boolean;
16
14
  onClose: () => void;
17
- classType: ClassType | '';
15
+ itemType: ItemType | '';
18
16
  bulletData?: BulletAnnotationData;
19
17
  cartridgeCaseData?: CartridgeCaseAnnotationData;
20
18
  shotshellData?: ShotshellAnnotationData;
@@ -27,17 +25,17 @@ interface ClassDetailsModalProps {
27
25
  isReadOnly?: boolean;
28
26
  }
29
27
 
30
- const ClassDetailsModalContent = ({
28
+ const ItemDetailsModalContent = ({
31
29
  isOpen,
32
30
  onClose,
33
- classType,
31
+ itemType,
34
32
  bulletData,
35
33
  cartridgeCaseData,
36
34
  shotshellData,
37
35
  onSave,
38
36
  showNotification,
39
37
  isReadOnly = false,
40
- }: ClassDetailsModalProps) => {
38
+ }: ItemDetailsModalProps) => {
41
39
  const {
42
40
  bullet,
43
41
  cartridgeCase,
@@ -45,7 +43,7 @@ const ClassDetailsModalContent = ({
45
43
  isSaving,
46
44
  setIsSaving,
47
45
  buildSaveData,
48
- } = useClassDetailsState({
46
+ } = useItemDetailsState({
49
47
  bulletData,
50
48
  cartridgeCaseData,
51
49
  shotshellData,
@@ -55,10 +53,10 @@ const ClassDetailsModalContent = ({
55
53
 
56
54
  if (!isOpen) return null;
57
55
 
58
- const showBullet = classType === 'Bullet' || classType === 'Other' || classType === '';
59
- const showCartridge = classType === 'Cartridge Case' || classType === 'Other' || classType === '';
60
- const showShotshell = classType === 'Shotshell' || classType === 'Other' || classType === '';
61
- const showHeaders = classType === 'Other' || classType === '';
56
+ const showBullet = itemType === 'Bullet' || itemType === 'Other' || itemType === '';
57
+ const showCartridge = itemType === 'Cartridge Case' || itemType === 'Other' || itemType === '';
58
+ const showShotshell = itemType === 'Shotshell' || itemType === 'Other' || itemType === '';
59
+ const showHeaders = itemType === 'Other' || itemType === '';
62
60
 
63
61
  const handleSave = async () => {
64
62
  setIsSaving(true);
@@ -90,10 +88,10 @@ const ClassDetailsModalContent = ({
90
88
  aria-label="Close class details dialog"
91
89
  {...overlayProps}
92
90
  >
93
- <div className={`${styles.modal} ${styles.classDetailsModal}`}>
91
+ <div className={`${styles.modal} ${styles.itemDetailsModal}`}>
94
92
  <button {...getCloseButtonProps({ ariaLabel: 'Close class details dialog' })}>×</button>
95
93
  <h5 className={styles.modalTitle}>Class Characteristic Details</h5>
96
- <div className={styles.classDetailsContent}>
94
+ <div className={styles.itemDetailsContent}>
97
95
  {showBullet && (
98
96
  <BulletSection
99
97
  showHeader={showHeaders}
@@ -118,10 +116,10 @@ const ClassDetailsModalContent = ({
118
116
  />
119
117
  )}
120
118
  </div>
121
- <div className={`${styles.modalButtons} ${styles.classDetailsModalButtons}`}>
119
+ <div className={`${styles.modalButtons} ${styles.itemDetailsModalButtons}`}>
122
120
  <button
123
121
  onClick={handleSave}
124
- className={`${styles.saveButton} ${styles.classDetailsModalAction}`}
122
+ className={`${styles.saveButton} ${styles.itemDetailsModalAction}`}
125
123
  disabled={isSaving || isReadOnly}
126
124
  aria-busy={isSaving}
127
125
  >
@@ -129,7 +127,7 @@ const ClassDetailsModalContent = ({
129
127
  </button>
130
128
  <button
131
129
  onClick={requestClose}
132
- className={`${styles.cancelButton} ${styles.classDetailsModalAction}`}
130
+ className={`${styles.cancelButton} ${styles.itemDetailsModalAction}`}
133
131
  disabled={isSaving}
134
132
  >
135
133
  Cancel
@@ -140,8 +138,8 @@ const ClassDetailsModalContent = ({
140
138
  );
141
139
  };
142
140
 
143
- export const ClassDetailsModal = (props: ClassDetailsModalProps) => {
141
+ export const ItemDetailsModal = (props: ItemDetailsModalProps) => {
144
142
  if (!props.isOpen) return null;
145
143
 
146
- return <ClassDetailsModalContent {...props} />;
144
+ return <ItemDetailsModalContent {...props} />;
147
145
  };