@striae-org/striae 4.2.1 → 4.3.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.
- package/app/components/actions/case-import/confirmation-import.ts +20 -1
- package/app/components/actions/case-import/orchestrator.ts +3 -0
- package/app/components/actions/case-manage.ts +5 -1
- package/app/components/actions/confirm-export.ts +12 -3
- package/app/components/audit/viewer/audit-entries-list.tsx +20 -2
- package/app/components/audit/viewer/use-audit-viewer-export.ts +2 -2
- package/app/components/audit/viewer/use-audit-viewer-filters.ts +11 -1
- package/app/components/canvas/canvas.tsx +2 -1
- package/app/components/navbar/case-modals/archive-case-modal.module.css +0 -76
- package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -8
- package/app/components/navbar/case-modals/case-modal-shared.module.css +94 -0
- package/app/components/navbar/case-modals/delete-case-modal.module.css +9 -0
- package/app/components/navbar/case-modals/delete-case-modal.tsx +79 -0
- package/app/components/navbar/case-modals/open-case-modal.module.css +2 -1
- package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -72
- package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -8
- package/app/components/navbar/navbar.module.css +11 -0
- package/app/components/navbar/navbar.tsx +38 -19
- package/app/components/sidebar/case-import/hooks/useImportExecution.ts +2 -0
- package/app/components/sidebar/cases/case-sidebar.tsx +27 -3
- package/app/components/sidebar/cases/cases-modal.module.css +312 -10
- package/app/components/sidebar/cases/cases-modal.tsx +690 -110
- package/app/components/sidebar/cases/cases.module.css +23 -0
- package/app/components/sidebar/files/delete-files-modal.module.css +26 -0
- package/app/components/sidebar/files/delete-files-modal.tsx +94 -0
- package/app/components/sidebar/files/files-modal.module.css +285 -44
- package/app/components/sidebar/files/files-modal.tsx +452 -145
- package/app/components/sidebar/notes/class-details-fields.tsx +146 -0
- package/app/components/sidebar/notes/class-details-modal.tsx +147 -0
- package/app/components/sidebar/notes/class-details-sections.tsx +561 -0
- package/app/components/sidebar/notes/class-details-shared.ts +239 -0
- package/app/components/sidebar/notes/notes-editor-form.tsx +43 -5
- package/app/components/sidebar/notes/notes.module.css +236 -4
- package/app/components/sidebar/notes/use-class-details-state.ts +371 -0
- package/app/components/sidebar/sidebar-container.tsx +2 -0
- package/app/components/sidebar/sidebar.tsx +8 -1
- package/app/hooks/useCaseListPreferences.ts +99 -0
- package/app/hooks/useFileListPreferences.ts +106 -0
- package/app/routes/striae/striae.tsx +45 -1
- package/app/services/audit/audit-export-csv.ts +4 -2
- package/app/services/audit/audit-export-report.ts +36 -4
- package/app/services/audit/audit.service.ts +2 -0
- package/app/services/audit/builders/audit-entry-builder.ts +1 -0
- package/app/services/audit/builders/audit-event-builders-workflow.ts +8 -2
- package/app/types/annotations.ts +48 -1
- package/app/types/audit.ts +1 -0
- package/app/utils/data/case-filters.ts +127 -0
- package/app/utils/data/confirmation-summary/summary-core.ts +18 -2
- package/app/utils/data/file-filters.ts +201 -0
- package/app/utils/forensics/confirmation-signature.ts +20 -5
- package/functions/api/image/[[path]].ts +4 -0
- package/package.json +3 -4
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/src/signing-payload-utils.ts +5 -0
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/keys-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/src/formats/format-striae.ts +84 -118
- package/workers/pdf-worker/src/pdf-worker.example.ts +28 -10
- package/workers/pdf-worker/src/report-layout.ts +227 -0
- package/workers/pdf-worker/src/report-types.ts +20 -0
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/workers/pdf-worker/src/assets/icon-256.png +0 -0
- /package/workers/pdf-worker/src/assets/{generated-assets.ts → generated-assets.example.ts} +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { BulletAnnotationData, CartridgeCaseAnnotationData, ShotshellAnnotationData } from '~/types/annotations';
|
|
2
|
+
|
|
3
|
+
export type ClassType = 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other';
|
|
4
|
+
|
|
5
|
+
export const CUSTOM = '__custom__';
|
|
6
|
+
|
|
7
|
+
export const PISTOL_CALIBERS: string[] = [
|
|
8
|
+
'.22 LR',
|
|
9
|
+
'.25 ACP',
|
|
10
|
+
'.32 ACP',
|
|
11
|
+
'.380 ACP (9 mm Kurz, 9×17)',
|
|
12
|
+
'9 mm Luger / 9×19 (9 mm Parabellum, 9 mm NATO)',
|
|
13
|
+
'.38 Special',
|
|
14
|
+
'.357 Magnum',
|
|
15
|
+
'.40 S&W',
|
|
16
|
+
'10 mm Auto',
|
|
17
|
+
'.44 Special',
|
|
18
|
+
'.44 Magnum',
|
|
19
|
+
'.45 ACP',
|
|
20
|
+
'.45 Colt (.45 Long Colt)',
|
|
21
|
+
'.454 Casull',
|
|
22
|
+
'.50 AE',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export const RIFLE_CALIBERS: string[] = [
|
|
26
|
+
'.22 LR',
|
|
27
|
+
'.17 HMR',
|
|
28
|
+
'.22 WMR (.22 Magnum)',
|
|
29
|
+
'.223 Remington / 5.56×45 NATO',
|
|
30
|
+
'.243 Winchester',
|
|
31
|
+
'6 mm Creedmoor / .243 class',
|
|
32
|
+
'6.5×55 Swedish',
|
|
33
|
+
'6.5 Creedmoor',
|
|
34
|
+
'.270 Winchester',
|
|
35
|
+
'7 mm-08 Remington / 7 mm Remington Magnum',
|
|
36
|
+
'.30-30 Winchester',
|
|
37
|
+
'.308 Winchester / 7.62×51 NATO',
|
|
38
|
+
'.30-06 Springfield',
|
|
39
|
+
'7.62×39 (AK family)',
|
|
40
|
+
'7.62×54R',
|
|
41
|
+
'.300 Winchester Magnum',
|
|
42
|
+
'.300 AAC Blackout',
|
|
43
|
+
'.338 Winchester Magnum',
|
|
44
|
+
'.45-70 Government',
|
|
45
|
+
'.50 BMG (12.7×99)',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
export const SHOTSHELL_GAUGES: string[] = [
|
|
49
|
+
'10 gauge',
|
|
50
|
+
'12 gauge',
|
|
51
|
+
'16 gauge',
|
|
52
|
+
'20 gauge',
|
|
53
|
+
'28 gauge',
|
|
54
|
+
'.410 bore',
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
export const SHOTSHELL_BIRDSHOT_OPTIONS = [
|
|
58
|
+
'#9',
|
|
59
|
+
'#8 1/2',
|
|
60
|
+
'#8',
|
|
61
|
+
'#7 1/2',
|
|
62
|
+
'#7',
|
|
63
|
+
'#6',
|
|
64
|
+
'#5',
|
|
65
|
+
'#4',
|
|
66
|
+
'#3',
|
|
67
|
+
'#2',
|
|
68
|
+
'#1',
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
export const SHOTSHELL_STEEL_WATERFOWL_OPTIONS = [
|
|
72
|
+
'#4',
|
|
73
|
+
'#3',
|
|
74
|
+
'#2',
|
|
75
|
+
'#1',
|
|
76
|
+
'B',
|
|
77
|
+
'BB',
|
|
78
|
+
'BBB',
|
|
79
|
+
'T',
|
|
80
|
+
] as const;
|
|
81
|
+
|
|
82
|
+
export const SHOTSHELL_BUCKSHOT_OPTIONS = [
|
|
83
|
+
'#4 buck',
|
|
84
|
+
'#1 buck',
|
|
85
|
+
'0 buck',
|
|
86
|
+
'00 buck',
|
|
87
|
+
'000 buck',
|
|
88
|
+
] as const;
|
|
89
|
+
|
|
90
|
+
export const ALL_CALIBERS: string[] = [...PISTOL_CALIBERS, ...RIFLE_CALIBERS];
|
|
91
|
+
export const BULLET_JACKET_METAL_OPTIONS = ['Cu', 'Brass', 'Ni-plated', 'Al', 'Steel', 'None'] as const;
|
|
92
|
+
export const BULLET_CORE_METAL_OPTIONS = ['Pb', 'Steel'] as const;
|
|
93
|
+
export const BULLET_TYPE_OPTIONS = ['FMJ', 'TMJ', 'HP', 'WC'] as const;
|
|
94
|
+
export const BULLET_BARREL_TYPE_OPTIONS = ['Conventional', 'Polygonal'] as const;
|
|
95
|
+
export const CARTRIDGE_METAL_OPTIONS = ['Brass', 'Ni-plated', 'Al', 'Steel'] as const;
|
|
96
|
+
export const CARTRIDGE_PRIMER_TYPE_OPTIONS = ['CF', 'RF'] as const;
|
|
97
|
+
export const CARTRIDGE_FPI_SHAPE_OPTIONS = ['Circular', 'Elliptical', 'Rectangular/Square', 'Tear-drop'] as const;
|
|
98
|
+
export const CARTRIDGE_APERTURE_SHAPE_OPTIONS = ['Circular', 'Rectangular'] as const;
|
|
99
|
+
|
|
100
|
+
export const handleSelectWithCustom = (
|
|
101
|
+
value: string,
|
|
102
|
+
setValue: (nextValue: string) => void,
|
|
103
|
+
setIsCustom: (nextValue: boolean) => void,
|
|
104
|
+
) => {
|
|
105
|
+
if (value === CUSTOM) {
|
|
106
|
+
setIsCustom(true);
|
|
107
|
+
setValue('');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
setIsCustom(false);
|
|
112
|
+
setValue(value);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const isCustomValue = (value: string | undefined, knownValues: readonly string[]): boolean =>
|
|
116
|
+
value !== undefined && value !== '' && !knownValues.includes(value);
|
|
117
|
+
|
|
118
|
+
export const parseMeasurementValue = (value: string): number | null => {
|
|
119
|
+
const trimmed = value.trim();
|
|
120
|
+
if (!trimmed) return null;
|
|
121
|
+
|
|
122
|
+
const parsed = Number.parseFloat(trimmed);
|
|
123
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const formatCalculatedDiameter = (value: number): string =>
|
|
127
|
+
value.toFixed(4).replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1');
|
|
128
|
+
|
|
129
|
+
const avgWidth = (widths: string[] | undefined): number | null => {
|
|
130
|
+
if (!widths || widths.length === 0) return null;
|
|
131
|
+
const vals = widths.map(parseMeasurementValue).filter((n): n is number => n !== null);
|
|
132
|
+
if (vals.length === 0) return null;
|
|
133
|
+
return vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const buildClassDetailsSummary = (
|
|
137
|
+
bulletData: BulletAnnotationData | undefined,
|
|
138
|
+
cartridgeCaseData: CartridgeCaseAnnotationData | undefined,
|
|
139
|
+
shotshellData: ShotshellAnnotationData | undefined,
|
|
140
|
+
classType: string,
|
|
141
|
+
): string => {
|
|
142
|
+
const showBullet = classType === 'Bullet' || classType === 'Other' || classType === '';
|
|
143
|
+
const showCartridge = classType === 'Cartridge Case' || classType === 'Other' || classType === '';
|
|
144
|
+
const showShotshell = classType === 'Shotshell' || classType === 'Other' || classType === '';
|
|
145
|
+
const showHeaders = classType === 'Other' || classType === '';
|
|
146
|
+
|
|
147
|
+
const allLines: string[] = [];
|
|
148
|
+
|
|
149
|
+
const pushSection = (header: string, sectionRows: string[]) => {
|
|
150
|
+
if (sectionRows.length === 0) return;
|
|
151
|
+
if (allLines.length > 0) allLines.push('');
|
|
152
|
+
if (showHeaders) allLines.push(header);
|
|
153
|
+
allLines.push(...sectionRows);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const r = (label: string, value: string | number | undefined): string | null =>
|
|
157
|
+
value ? `${label}: ${value}` : null;
|
|
158
|
+
|
|
159
|
+
if (showBullet && bulletData) {
|
|
160
|
+
const rows: string[] = [];
|
|
161
|
+
const add = (v: string | null) => { if (v) rows.push(v); };
|
|
162
|
+
add(r('Caliber', bulletData.caliber));
|
|
163
|
+
add(r('Mass', bulletData.mass));
|
|
164
|
+
add(r('Diameter', bulletData.diameter));
|
|
165
|
+
add(r('L/G Count', bulletData.lgNumber));
|
|
166
|
+
add(r('L/G Direction', bulletData.lgDirection));
|
|
167
|
+
if (bulletData.lgNumber && bulletData.calcDiameter) {
|
|
168
|
+
const avgL = avgWidth(bulletData.lWidths);
|
|
169
|
+
const avgG = avgWidth(bulletData.gWidths);
|
|
170
|
+
if (avgL !== null) add(r('Avg L Width', `${parseFloat(avgL.toFixed(4))}"`));
|
|
171
|
+
if (avgG !== null) add(r('Avg G Width', `${parseFloat(avgG.toFixed(4))}"`));
|
|
172
|
+
add(r('Calc. Diameter', `${bulletData.calcDiameter}"`));
|
|
173
|
+
}
|
|
174
|
+
add(r('Jacket Metal', bulletData.jacketMetal));
|
|
175
|
+
add(r('Core Metal', bulletData.coreMetal));
|
|
176
|
+
add(r('Bullet Type', bulletData.bulletType));
|
|
177
|
+
add(r('Barrel Type', bulletData.barrelType));
|
|
178
|
+
pushSection('[Bullet]', rows);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (showCartridge && cartridgeCaseData) {
|
|
182
|
+
const rows: string[] = [];
|
|
183
|
+
const add = (v: string | null) => { if (v) rows.push(v); };
|
|
184
|
+
add(r('Caliber', cartridgeCaseData.caliber));
|
|
185
|
+
add(r('Brand', cartridgeCaseData.brand));
|
|
186
|
+
add(r('Metal', cartridgeCaseData.metal));
|
|
187
|
+
add(r('Primer Type', cartridgeCaseData.primerType));
|
|
188
|
+
add(r('FPI Shape', cartridgeCaseData.fpiShape));
|
|
189
|
+
add(r('Aperture Shape', cartridgeCaseData.apertureShape));
|
|
190
|
+
if (cartridgeCaseData.hasFpDrag) rows.push('FP Drag: Yes');
|
|
191
|
+
if (cartridgeCaseData.hasExtractorMarks) rows.push('Extractor Marks: Yes');
|
|
192
|
+
if (cartridgeCaseData.hasEjectorMarks) rows.push('Ejector Marks: Yes');
|
|
193
|
+
if (cartridgeCaseData.hasChamberMarks) rows.push('Chamber Marks: Yes');
|
|
194
|
+
if (cartridgeCaseData.hasMagazineLipMarks) rows.push('Magazine Lip Marks: Yes');
|
|
195
|
+
if (cartridgeCaseData.hasPrimerShear) rows.push('Primer Shear: Yes');
|
|
196
|
+
if (cartridgeCaseData.hasEjectionPortMarks) rows.push('Ejection Port Marks: Yes');
|
|
197
|
+
pushSection('[Cartridge Case]', rows);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (showShotshell && shotshellData) {
|
|
201
|
+
const rows: string[] = [];
|
|
202
|
+
const add = (v: string | null) => { if (v) rows.push(v); };
|
|
203
|
+
add(r('Gauge', shotshellData.gauge));
|
|
204
|
+
add(r('Shot Size', shotshellData.shotSize));
|
|
205
|
+
add(r('Metal', shotshellData.metal));
|
|
206
|
+
add(r('Brand', shotshellData.brand));
|
|
207
|
+
add(r('FPI Shape', shotshellData.fpiShape));
|
|
208
|
+
if (shotshellData.hasExtractorMarks) rows.push('Extractor Marks: Yes');
|
|
209
|
+
if (shotshellData.hasEjectorMarks) rows.push('Ejector Marks: Yes');
|
|
210
|
+
if (shotshellData.hasChamberMarks) rows.push('Chamber Marks: Yes');
|
|
211
|
+
pushSection('[Shotshell]', rows);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (allLines.length === 0) return '';
|
|
215
|
+
return ['--- Class Details ---', ...allLines].join('\n');
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export const calculateBulletDiameter = (
|
|
219
|
+
lgCount: number,
|
|
220
|
+
lWidths: string[],
|
|
221
|
+
gWidths: string[],
|
|
222
|
+
): number | null => {
|
|
223
|
+
if (lgCount <= 0) return null;
|
|
224
|
+
|
|
225
|
+
const lWidthValues = lWidths.slice(0, lgCount).map(parseMeasurementValue);
|
|
226
|
+
const gWidthValues = gWidths.slice(0, lgCount).map(parseMeasurementValue);
|
|
227
|
+
const hasAllMeasurements = lWidthValues.length === lgCount
|
|
228
|
+
&& gWidthValues.length === lgCount
|
|
229
|
+
&& lWidthValues.every((value) => value !== null)
|
|
230
|
+
&& gWidthValues.every((value) => value !== null);
|
|
231
|
+
|
|
232
|
+
if (!hasAllMeasurements) return null;
|
|
233
|
+
|
|
234
|
+
const lAverage = lWidthValues.reduce((sum, value) => sum + (value ?? 0), 0) / lgCount;
|
|
235
|
+
const gAverage = gWidthValues.reduce((sum, value) => sum + (value ?? 0), 0) / lgCount;
|
|
236
|
+
const circumference = (lAverage + gAverage) * lgCount;
|
|
237
|
+
|
|
238
|
+
return circumference / Math.PI;
|
|
239
|
+
};
|
|
@@ -2,8 +2,10 @@ import { useState, useEffect } from 'react';
|
|
|
2
2
|
import type { User } from 'firebase/auth';
|
|
3
3
|
import { ColorSelector } from '~/components/colors/colors';
|
|
4
4
|
import { AddlNotesModal } from './addl-notes-modal';
|
|
5
|
+
import { ClassDetailsModal } from './class-details-modal';
|
|
6
|
+
import { buildClassDetailsSummary } from './class-details-shared';
|
|
5
7
|
import { getNotes, saveNotes } from '~/components/actions/notes-manage';
|
|
6
|
-
import { type AnnotationData } from '~/types/annotations';
|
|
8
|
+
import { type AnnotationData, type BulletAnnotationData, type CartridgeCaseAnnotationData, type ShotshellAnnotationData } from '~/types/annotations';
|
|
7
9
|
import { resolveEarliestAnnotationTimestamp } from '~/utils/ui';
|
|
8
10
|
import { auditService } from '~/services/audit';
|
|
9
11
|
import styles from './notes.module.css';
|
|
@@ -19,7 +21,7 @@ interface NotesEditorFormProps {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
type SupportLevel = 'ID' | 'Exclusion' | 'Inconclusive';
|
|
22
|
-
type ClassType = 'Bullet' | 'Cartridge Case' | 'Other';
|
|
24
|
+
type ClassType = 'Bullet' | 'Cartridge Case' | 'Shotshell' | 'Other';
|
|
23
25
|
type IndexType = 'number' | 'color';
|
|
24
26
|
|
|
25
27
|
export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefresh, originalFileName, isUploading = false, showNotification: externalShowNotification }: NotesEditorFormProps) => {
|
|
@@ -41,6 +43,10 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
41
43
|
const [customClass, setCustomClass] = useState('');
|
|
42
44
|
const [classNote, setClassNote] = useState('');
|
|
43
45
|
const [hasSubclass, setHasSubclass] = useState(false);
|
|
46
|
+
const [bulletData, setBulletData] = useState<BulletAnnotationData | undefined>(undefined);
|
|
47
|
+
const [cartridgeCaseData, setCartridgeCaseData] = useState<CartridgeCaseAnnotationData | undefined>(undefined);
|
|
48
|
+
const [shotshellData, setShotshellData] = useState<ShotshellAnnotationData | undefined>(undefined);
|
|
49
|
+
const [isClassDetailsOpen, setIsClassDetailsOpen] = useState(false);
|
|
44
50
|
|
|
45
51
|
// Index state
|
|
46
52
|
const [indexType, setIndexType] = useState<IndexType>('color');
|
|
@@ -91,6 +97,9 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
91
97
|
setCustomClass(existingNotes.customClass || '');
|
|
92
98
|
setClassNote(existingNotes.classNote || '');
|
|
93
99
|
setHasSubclass(existingNotes.hasSubclass ?? false);
|
|
100
|
+
setBulletData(existingNotes.bulletData);
|
|
101
|
+
setCartridgeCaseData(existingNotes.cartridgeCaseData);
|
|
102
|
+
setShotshellData(existingNotes.shotshellData);
|
|
94
103
|
setIndexType(existingNotes.indexType || 'color');
|
|
95
104
|
setIndexNumber(existingNotes.indexNumber || '');
|
|
96
105
|
setIndexColor(existingNotes.indexColor || '');
|
|
@@ -154,6 +163,9 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
154
163
|
customClass: customClass,
|
|
155
164
|
classNote: classNote || undefined,
|
|
156
165
|
hasSubclass: hasSubclass,
|
|
166
|
+
bulletData: bulletData,
|
|
167
|
+
cartridgeCaseData: cartridgeCaseData,
|
|
168
|
+
shotshellData: shotshellData,
|
|
157
169
|
|
|
158
170
|
// Index Information
|
|
159
171
|
indexType: indexType,
|
|
@@ -362,6 +374,7 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
362
374
|
<option value="">Select class type...</option>
|
|
363
375
|
<option value="Bullet">Bullet</option>
|
|
364
376
|
<option value="Cartridge Case">Cartridge Case</option>
|
|
377
|
+
<option value="Shotshell">Shotshell</option>
|
|
365
378
|
<option value="Other">Other</option>
|
|
366
379
|
</select>
|
|
367
380
|
|
|
@@ -395,9 +408,15 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
395
408
|
</label>
|
|
396
409
|
</div>
|
|
397
410
|
|
|
398
|
-
<div className={styles.
|
|
399
|
-
<
|
|
400
|
-
|
|
411
|
+
<div className={styles.classDetailsPanel}>
|
|
412
|
+
<button
|
|
413
|
+
type="button"
|
|
414
|
+
onClick={() => setIsClassDetailsOpen(true)}
|
|
415
|
+
className={styles.classDetailsButton}
|
|
416
|
+
disabled={areInputsDisabled}
|
|
417
|
+
>
|
|
418
|
+
Enter Class Characteristic Details
|
|
419
|
+
</button>
|
|
401
420
|
</div>
|
|
402
421
|
</div>
|
|
403
422
|
</>
|
|
@@ -533,6 +552,25 @@ export const NotesEditorForm = ({ currentCase, user, imageId, onAnnotationRefres
|
|
|
533
552
|
onSave={setAdditionalNotes}
|
|
534
553
|
showNotification={notificationHandler}
|
|
535
554
|
/>
|
|
555
|
+
<ClassDetailsModal
|
|
556
|
+
isOpen={isClassDetailsOpen}
|
|
557
|
+
onClose={() => setIsClassDetailsOpen(false)}
|
|
558
|
+
classType={classType}
|
|
559
|
+
bulletData={bulletData}
|
|
560
|
+
cartridgeCaseData={cartridgeCaseData}
|
|
561
|
+
shotshellData={shotshellData}
|
|
562
|
+
onSave={(b, c, s) => {
|
|
563
|
+
if (b !== undefined) setBulletData(b);
|
|
564
|
+
if (c !== undefined) setCartridgeCaseData(c);
|
|
565
|
+
if (s !== undefined) setShotshellData(s);
|
|
566
|
+
const summary = buildClassDetailsSummary(b, c, s, classType);
|
|
567
|
+
if (summary) {
|
|
568
|
+
setAdditionalNotes((prev) => prev ? `${prev}\n${summary}` : summary);
|
|
569
|
+
}
|
|
570
|
+
}}
|
|
571
|
+
showNotification={notificationHandler}
|
|
572
|
+
isReadOnly={areInputsDisabled}
|
|
573
|
+
/>
|
|
536
574
|
</>
|
|
537
575
|
)}
|
|
538
576
|
</div>
|
|
@@ -61,10 +61,7 @@
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
.editorLayout .caseNumbers > .inputGroup + .inputGroup,
|
|
64
|
-
.editorLayout
|
|
65
|
-
.compactSectionGrid
|
|
66
|
-
> .compactHalfSection
|
|
67
|
-
+ .compactHalfSection,
|
|
64
|
+
.editorLayout .compactSectionGrid > .compactHalfSection + .compactHalfSection,
|
|
68
65
|
.classCharacteristicsColumns > .characteristicsPlaceholder {
|
|
69
66
|
border-left: none;
|
|
70
67
|
padding-left: 0;
|
|
@@ -250,6 +247,241 @@ textarea:focus {
|
|
|
250
247
|
line-height: 1.4;
|
|
251
248
|
}
|
|
252
249
|
|
|
250
|
+
/* Class Details Panel — replaces placeholder in right column */
|
|
251
|
+
|
|
252
|
+
.classDetailsPanel {
|
|
253
|
+
display: flex;
|
|
254
|
+
flex-direction: column;
|
|
255
|
+
gap: 0.75rem;
|
|
256
|
+
min-height: 150px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.classDetailsButton {
|
|
260
|
+
width: 100%;
|
|
261
|
+
padding: 0.65rem 1rem;
|
|
262
|
+
background: transparent;
|
|
263
|
+
color: var(--primary);
|
|
264
|
+
border: 1.5px solid var(--primary);
|
|
265
|
+
border-radius: 6px;
|
|
266
|
+
font-size: 0.88rem;
|
|
267
|
+
font-weight: 500;
|
|
268
|
+
cursor: pointer;
|
|
269
|
+
transition: all 0.2s;
|
|
270
|
+
text-align: center;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.classDetailsButton:hover:not(:disabled) {
|
|
274
|
+
background-color: color-mix(in lab, var(--primary) 10%, transparent);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.classDetailsButton:disabled {
|
|
278
|
+
opacity: 0.5;
|
|
279
|
+
cursor: not-allowed;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* Class Details Modal */
|
|
283
|
+
|
|
284
|
+
.classDetailsModal {
|
|
285
|
+
max-width: 680px;
|
|
286
|
+
max-height: 80vh;
|
|
287
|
+
display: flex;
|
|
288
|
+
flex-direction: column;
|
|
289
|
+
gap: 1rem;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.classDetailsContent {
|
|
293
|
+
overflow-y: auto;
|
|
294
|
+
flex: 1;
|
|
295
|
+
min-height: 0;
|
|
296
|
+
display: flex;
|
|
297
|
+
flex-direction: column;
|
|
298
|
+
gap: 1.25rem;
|
|
299
|
+
padding-right: 0.25rem;
|
|
300
|
+
padding-bottom: 0.5rem;
|
|
301
|
+
scrollbar-width: thin;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.classDetailsSection {
|
|
305
|
+
display: flex;
|
|
306
|
+
flex-direction: column;
|
|
307
|
+
gap: 1rem;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.classDetailsSectionHeader {
|
|
311
|
+
margin: 0;
|
|
312
|
+
font-size: 0.95rem;
|
|
313
|
+
font-weight: 700;
|
|
314
|
+
color: #343a40;
|
|
315
|
+
padding-bottom: 0.4rem;
|
|
316
|
+
border-bottom: 1.5px solid #dee2e6;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.classDetailsFieldGrid {
|
|
320
|
+
display: grid;
|
|
321
|
+
grid-template-columns: 1fr 1fr;
|
|
322
|
+
gap: 0.85rem 1.25rem;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.classDetailsField {
|
|
326
|
+
display: flex;
|
|
327
|
+
flex-direction: column;
|
|
328
|
+
gap: 0.25rem;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.classDetailsFieldFull {
|
|
332
|
+
grid-column: 1 / -1;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.classDetailsLabel {
|
|
336
|
+
font-size: 0.8rem;
|
|
337
|
+
font-weight: 600;
|
|
338
|
+
color: #495057;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.classDetailsInput {
|
|
342
|
+
padding: 0.5rem 0.65rem;
|
|
343
|
+
border: 1.5px solid #ced4da;
|
|
344
|
+
border-radius: 6px;
|
|
345
|
+
font-size: 0.88rem;
|
|
346
|
+
transition: border-color 0.2s;
|
|
347
|
+
width: 100%;
|
|
348
|
+
box-sizing: border-box;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.classDetailsInput:focus {
|
|
352
|
+
border-color: var(--primary);
|
|
353
|
+
outline: none;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.classDetailsInput:disabled {
|
|
357
|
+
background-color: #f8f9fa;
|
|
358
|
+
cursor: not-allowed;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.classDetailsCheckboxGroup {
|
|
362
|
+
display: flex;
|
|
363
|
+
flex-wrap: wrap;
|
|
364
|
+
gap: 0.4rem 1.25rem;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.classDetailsCheckboxLabel {
|
|
368
|
+
display: flex;
|
|
369
|
+
align-items: center;
|
|
370
|
+
gap: 0.4rem;
|
|
371
|
+
font-size: 0.85rem;
|
|
372
|
+
color: #495057;
|
|
373
|
+
cursor: pointer;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.classDetailsCheckboxLabel input[type="checkbox"] {
|
|
377
|
+
cursor: pointer;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.classDetailsCheckboxLabel input[type="checkbox"]:disabled {
|
|
381
|
+
cursor: not-allowed;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.lgWidthsSection {
|
|
385
|
+
display: flex;
|
|
386
|
+
flex-direction: column;
|
|
387
|
+
gap: 0.75rem;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.lgWidthsLayout {
|
|
391
|
+
display: flex;
|
|
392
|
+
gap: 1.25rem;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.lgWidthsColumn {
|
|
396
|
+
flex: 1;
|
|
397
|
+
display: flex;
|
|
398
|
+
flex-direction: column;
|
|
399
|
+
gap: 0.85rem;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.calculatedDiameterWrapper {
|
|
403
|
+
display: flex;
|
|
404
|
+
flex-direction: column;
|
|
405
|
+
gap: 0.4rem;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.calculatedDiameterDisplay {
|
|
409
|
+
display: flex;
|
|
410
|
+
justify-content: space-between;
|
|
411
|
+
align-items: center;
|
|
412
|
+
gap: 1rem;
|
|
413
|
+
padding: 0.85rem 1rem;
|
|
414
|
+
border: 1px solid #dee2e6;
|
|
415
|
+
border-radius: 8px;
|
|
416
|
+
background: #f8f9fa;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.calculatedDiameterValue {
|
|
420
|
+
font-size: 0.95rem;
|
|
421
|
+
font-weight: 700;
|
|
422
|
+
color: #343a40;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.calcExplanationToggle {
|
|
426
|
+
all: unset;
|
|
427
|
+
cursor: pointer;
|
|
428
|
+
font-size: 0.8rem;
|
|
429
|
+
color: var(--color-primary, #4f6ef7);
|
|
430
|
+
text-decoration: underline;
|
|
431
|
+
text-underline-offset: 2px;
|
|
432
|
+
padding: 0.1rem 0;
|
|
433
|
+
align-self: flex-start;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.calcExplanationToggle:hover {
|
|
437
|
+
color: var(--color-primary-hover, #3a56d4);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.calcExplanationPanel {
|
|
441
|
+
padding: 0.85rem 1rem;
|
|
442
|
+
border: 1px solid #dee2e6;
|
|
443
|
+
border-radius: 8px;
|
|
444
|
+
background: #f8f9fa;
|
|
445
|
+
font-size: 0.82rem;
|
|
446
|
+
color: #495057;
|
|
447
|
+
display: flex;
|
|
448
|
+
flex-direction: column;
|
|
449
|
+
gap: 0.55rem;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.calcExplanationFormula {
|
|
453
|
+
margin: 0;
|
|
454
|
+
font-family: monospace;
|
|
455
|
+
font-size: 0.88rem;
|
|
456
|
+
font-weight: 600;
|
|
457
|
+
color: #343a40;
|
|
458
|
+
letter-spacing: 0.01em;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.calcExplanationList {
|
|
462
|
+
margin: 0;
|
|
463
|
+
padding-left: 1.15rem;
|
|
464
|
+
display: flex;
|
|
465
|
+
flex-direction: column;
|
|
466
|
+
gap: 0.2rem;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.calcExplanationList li {
|
|
470
|
+
line-height: 1.5;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.calcExplanationNote {
|
|
474
|
+
margin: 0;
|
|
475
|
+
color: #6c757d;
|
|
476
|
+
font-style: italic;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.calcExplanationExample {
|
|
480
|
+
margin: 0;
|
|
481
|
+
padding-top: 0.35rem;
|
|
482
|
+
border-top: 1px solid #dee2e6;
|
|
483
|
+
}
|
|
484
|
+
|
|
253
485
|
.classCharacteristics input {
|
|
254
486
|
width: 100%;
|
|
255
487
|
padding: 0.75rem;
|