@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.
Files changed (66) hide show
  1. package/app/components/actions/case-import/confirmation-import.ts +20 -1
  2. package/app/components/actions/case-import/orchestrator.ts +3 -0
  3. package/app/components/actions/case-manage.ts +5 -1
  4. package/app/components/actions/confirm-export.ts +12 -3
  5. package/app/components/audit/viewer/audit-entries-list.tsx +20 -2
  6. package/app/components/audit/viewer/use-audit-viewer-export.ts +2 -2
  7. package/app/components/audit/viewer/use-audit-viewer-filters.ts +11 -1
  8. package/app/components/canvas/canvas.tsx +2 -1
  9. package/app/components/navbar/case-modals/archive-case-modal.module.css +0 -76
  10. package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -8
  11. package/app/components/navbar/case-modals/case-modal-shared.module.css +94 -0
  12. package/app/components/navbar/case-modals/delete-case-modal.module.css +9 -0
  13. package/app/components/navbar/case-modals/delete-case-modal.tsx +79 -0
  14. package/app/components/navbar/case-modals/open-case-modal.module.css +2 -1
  15. package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -72
  16. package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -8
  17. package/app/components/navbar/navbar.module.css +11 -0
  18. package/app/components/navbar/navbar.tsx +38 -19
  19. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +2 -0
  20. package/app/components/sidebar/cases/case-sidebar.tsx +27 -3
  21. package/app/components/sidebar/cases/cases-modal.module.css +312 -10
  22. package/app/components/sidebar/cases/cases-modal.tsx +690 -110
  23. package/app/components/sidebar/cases/cases.module.css +23 -0
  24. package/app/components/sidebar/files/delete-files-modal.module.css +26 -0
  25. package/app/components/sidebar/files/delete-files-modal.tsx +94 -0
  26. package/app/components/sidebar/files/files-modal.module.css +285 -44
  27. package/app/components/sidebar/files/files-modal.tsx +452 -145
  28. package/app/components/sidebar/notes/class-details-fields.tsx +146 -0
  29. package/app/components/sidebar/notes/class-details-modal.tsx +147 -0
  30. package/app/components/sidebar/notes/class-details-sections.tsx +561 -0
  31. package/app/components/sidebar/notes/class-details-shared.ts +239 -0
  32. package/app/components/sidebar/notes/notes-editor-form.tsx +43 -5
  33. package/app/components/sidebar/notes/notes.module.css +236 -4
  34. package/app/components/sidebar/notes/use-class-details-state.ts +371 -0
  35. package/app/components/sidebar/sidebar-container.tsx +2 -0
  36. package/app/components/sidebar/sidebar.tsx +8 -1
  37. package/app/hooks/useCaseListPreferences.ts +99 -0
  38. package/app/hooks/useFileListPreferences.ts +106 -0
  39. package/app/routes/striae/striae.tsx +45 -1
  40. package/app/services/audit/audit-export-csv.ts +4 -2
  41. package/app/services/audit/audit-export-report.ts +36 -4
  42. package/app/services/audit/audit.service.ts +2 -0
  43. package/app/services/audit/builders/audit-entry-builder.ts +1 -0
  44. package/app/services/audit/builders/audit-event-builders-workflow.ts +8 -2
  45. package/app/types/annotations.ts +48 -1
  46. package/app/types/audit.ts +1 -0
  47. package/app/utils/data/case-filters.ts +127 -0
  48. package/app/utils/data/confirmation-summary/summary-core.ts +18 -2
  49. package/app/utils/data/file-filters.ts +201 -0
  50. package/app/utils/forensics/confirmation-signature.ts +20 -5
  51. package/functions/api/image/[[path]].ts +4 -0
  52. package/package.json +3 -4
  53. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  54. package/workers/data-worker/src/signing-payload-utils.ts +5 -0
  55. package/workers/data-worker/wrangler.jsonc.example +1 -1
  56. package/workers/image-worker/wrangler.jsonc.example +1 -1
  57. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  58. package/workers/pdf-worker/src/formats/format-striae.ts +84 -118
  59. package/workers/pdf-worker/src/pdf-worker.example.ts +28 -10
  60. package/workers/pdf-worker/src/report-layout.ts +227 -0
  61. package/workers/pdf-worker/src/report-types.ts +20 -0
  62. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  63. package/workers/user-worker/wrangler.jsonc.example +1 -1
  64. package/wrangler.toml.example +1 -1
  65. package/workers/pdf-worker/src/assets/icon-256.png +0 -0
  66. /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.characteristicsPlaceholder}>
399
- <h6 className={styles.placeholderTitle}>Characteristics Details</h6>
400
- <p className={styles.placeholderText}>This section is reserved for future development.</p>
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;