@striae-org/striae 5.3.2 → 5.4.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 (112) hide show
  1. package/app/components/auth/auth.module.css +531 -0
  2. package/app/components/auth/mfa-enrollment.tsx +132 -79
  3. package/app/components/auth/mfa-totp-enrollment.tsx +231 -0
  4. package/app/components/auth/mfa-verification.tsx +162 -33
  5. package/app/components/{sidebar/cases/cases-modal.tsx → navbar/case-modals/all-cases-modal.tsx} +4 -4
  6. package/app/components/navbar/case-modals/archive-case-modal.tsx +9 -10
  7. package/app/components/navbar/case-modals/case-modal-shared.module.css +88 -0
  8. package/app/components/navbar/case-modals/delete-case-modal.tsx +9 -10
  9. package/app/components/navbar/case-modals/export-case-modal.tsx +9 -10
  10. package/app/components/navbar/case-modals/export-confirmations-modal.tsx +9 -10
  11. package/app/components/navbar/case-modals/open-case-modal.tsx +4 -4
  12. package/app/components/navbar/case-modals/rename-case-modal.tsx +9 -10
  13. package/app/components/navbar/navbar.tsx +1 -1
  14. package/app/components/sidebar/files/delete-files-modal.tsx +3 -3
  15. package/app/components/sidebar/files/files-modal.module.css +29 -0
  16. package/app/components/sidebar/notes/{class-details-fields.tsx → class-details/class-details-fields.tsx} +1 -1
  17. package/app/components/sidebar/notes/{class-details-modal.tsx → class-details/class-details-modal.tsx} +1 -1
  18. package/app/components/sidebar/notes/{class-details-sections.tsx → class-details/class-details-sections.tsx} +1 -1
  19. package/app/components/sidebar/notes/notes-editor-form.tsx +2 -2
  20. package/app/components/sidebar/notes/notes-editor-modal.tsx +6 -6
  21. package/app/components/sidebar/notes/notes.module.css +52 -0
  22. package/app/components/toolbar/toolbar-color-selector.tsx +8 -8
  23. package/app/components/toolbar/toolbar.module.css +181 -2
  24. package/app/components/user/delete-account.tsx +7 -7
  25. package/app/components/user/inactivity-warning.tsx +6 -6
  26. package/app/components/user/manage-profile.tsx +18 -1
  27. package/app/components/user/mfa-enrolled-factors.tsx +117 -0
  28. package/app/components/user/mfa-phone-update.tsx +8 -4
  29. package/app/components/user/mfa-totp-section.tsx +446 -0
  30. package/app/components/user/user.module.css +665 -0
  31. package/app/routes/striae/striae.tsx +1 -1
  32. package/app/services/audit/audit.service.ts +1 -1
  33. package/app/services/audit/builders/audit-event-builders-user-security.ts +4 -2
  34. package/app/services/firebase/errors.ts +2 -0
  35. package/app/utils/auth/mfa.ts +35 -1
  36. package/package.json +23 -28
  37. package/scripts/deploy-all.sh +166 -0
  38. package/scripts/deploy-config/modules/env-utils.sh +322 -0
  39. package/scripts/deploy-config/modules/keys.sh +404 -0
  40. package/scripts/deploy-config/modules/prompt.sh +375 -0
  41. package/scripts/deploy-config/modules/scaffolding.sh +310 -0
  42. package/scripts/deploy-config/modules/validation.sh +354 -0
  43. package/scripts/deploy-config.sh +236 -0
  44. package/scripts/deploy-pages-secrets.sh +231 -0
  45. package/scripts/deploy-pages.sh +34 -0
  46. package/scripts/deploy-primershear-emails.sh +167 -0
  47. package/scripts/deploy-worker-secrets.sh +385 -0
  48. package/scripts/dev.cjs +23 -0
  49. package/scripts/enable-totp-mfa.mjs +57 -0
  50. package/scripts/install-workers.sh +87 -0
  51. package/scripts/run-eslint.cjs +43 -0
  52. package/scripts/unenroll-totp-mfa.mjs +82 -0
  53. package/scripts/update-compatibility-dates.cjs +124 -0
  54. package/scripts/update-markdown-versions.cjs +43 -0
  55. package/workers/audit-worker/.editorconfig +12 -0
  56. package/workers/audit-worker/.prettierrc +6 -0
  57. package/workers/audit-worker/package.json +1 -1
  58. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  59. package/workers/data-worker/.editorconfig +12 -0
  60. package/workers/data-worker/.prettierrc +6 -0
  61. package/workers/data-worker/package.json +1 -1
  62. package/workers/data-worker/wrangler.jsonc.example +1 -1
  63. package/workers/image-worker/.editorconfig +12 -0
  64. package/workers/image-worker/.prettierrc +6 -0
  65. package/workers/image-worker/package.json +1 -1
  66. package/workers/image-worker/wrangler.jsonc.example +1 -1
  67. package/workers/pdf-worker/.editorconfig +12 -0
  68. package/workers/pdf-worker/.prettierrc +6 -0
  69. package/workers/pdf-worker/package.json +1 -1
  70. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  71. package/workers/user-worker/.editorconfig +12 -0
  72. package/workers/user-worker/.prettierrc +6 -0
  73. package/workers/user-worker/package.json +1 -1
  74. package/workers/user-worker/wrangler.jsonc.example +1 -1
  75. package/wrangler.toml.example +1 -1
  76. package/app/components/auth/mfa-enrollment.module.css +0 -276
  77. package/app/components/auth/mfa-verification.module.css +0 -259
  78. package/app/components/navbar/case-modals/archive-case-modal.module.css +0 -34
  79. package/app/components/navbar/case-modals/delete-case-modal.module.css +0 -9
  80. package/app/components/navbar/case-modals/export-case-modal.module.css +0 -27
  81. package/app/components/navbar/case-modals/export-confirmations-modal.module.css +0 -24
  82. package/app/components/navbar/case-modals/open-case-modal.module.css +0 -82
  83. package/app/components/navbar/case-modals/rename-case-modal.module.css +0 -9
  84. package/app/components/sidebar/files/delete-files-modal.module.css +0 -26
  85. package/app/components/sidebar/notes/notes-editor-modal.module.css +0 -49
  86. package/app/components/toolbar/toolbar-color-selector.module.css +0 -171
  87. package/app/components/user/delete-account.module.css +0 -277
  88. package/app/components/user/inactivity-warning.module.css +0 -148
  89. package/app/components/user/manage-profile.module.css +0 -192
  90. package/app/routes/auth/login.module.css +0 -523
  91. package/app/routes/auth/login.tsx +0 -705
  92. package/workers/audit-worker/worker-configuration.d.ts +0 -7448
  93. package/workers/data-worker/worker-configuration.d.ts +0 -7448
  94. package/workers/image-worker/worker-configuration.d.ts +0 -7448
  95. package/workers/pdf-worker/worker-configuration.d.ts +0 -7447
  96. package/workers/user-worker/worker-configuration.d.ts +0 -7450
  97. /package/app/components/{sidebar → navbar}/case-import/case-import.module.css +0 -0
  98. /package/app/components/{sidebar → navbar}/case-import/case-import.tsx +0 -0
  99. /package/app/components/{sidebar → navbar}/case-import/components/CasePreviewSection.tsx +0 -0
  100. /package/app/components/{sidebar → navbar}/case-import/components/ConfirmationDialog.tsx +0 -0
  101. /package/app/components/{sidebar → navbar}/case-import/components/ConfirmationPreviewSection.tsx +0 -0
  102. /package/app/components/{sidebar → navbar}/case-import/components/ExistingCaseSection.tsx +0 -0
  103. /package/app/components/{sidebar → navbar}/case-import/components/FileSelector.tsx +0 -0
  104. /package/app/components/{sidebar → navbar}/case-import/components/ProgressSection.tsx +0 -0
  105. /package/app/components/{sidebar → navbar}/case-import/hooks/useFilePreview.ts +0 -0
  106. /package/app/components/{sidebar → navbar}/case-import/hooks/useImportExecution.ts +0 -0
  107. /package/app/components/{sidebar → navbar}/case-import/hooks/useImportState.ts +0 -0
  108. /package/app/components/{sidebar → navbar}/case-import/index.ts +0 -0
  109. /package/app/components/{sidebar → navbar}/case-import/utils/file-validation.ts +0 -0
  110. /package/app/components/{sidebar/cases/cases-modal.module.css → navbar/case-modals/all-cases-modal.module.css} +0 -0
  111. /package/app/components/sidebar/notes/{class-details-shared.ts → class-details/class-details-shared.ts} +0 -0
  112. /package/app/components/sidebar/notes/{use-class-details-state.ts → class-details/use-class-details-state.ts} +0 -0
@@ -1,7 +1,6 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
2
  import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
3
- import sharedStyles from './case-modal-shared.module.css';
4
- import styles from './rename-case-modal.module.css';
3
+ import styles from './case-modal-shared.module.css';
5
4
 
6
5
  interface RenameCaseModalProps {
7
6
  isOpen: boolean;
@@ -60,22 +59,22 @@ export const RenameCaseModal = ({
60
59
 
61
60
  return (
62
61
  <div
63
- className={sharedStyles.overlay}
62
+ className={styles.overlay}
64
63
  aria-label="Close rename case dialog"
65
64
  {...overlayProps}
66
65
  >
67
- <div className={`${sharedStyles.modal} ${styles.modal}`} role="dialog" aria-modal="true" aria-label="Rename Case">
66
+ <div className={`${styles.modal} ${styles.modalStandard}`} role="dialog" aria-modal="true" aria-label="Rename Case">
68
67
  <button {...getCloseButtonProps({ ariaLabel: 'Close rename case dialog' })}>
69
68
  ×
70
69
  </button>
71
- <h3 className={sharedStyles.title}>Rename Case</h3>
72
- <p className={sharedStyles.subtitle}>Current case: {currentCase}</p>
70
+ <h3 className={styles.title}>Rename Case</h3>
71
+ <p className={styles.subtitle}>Current case: {currentCase}</p>
73
72
  <input
74
73
  ref={inputRef}
75
74
  type="text"
76
75
  value={newCaseName}
77
76
  onChange={(event) => setNewCaseName(event.target.value)}
78
- className={sharedStyles.input}
77
+ className={styles.input}
79
78
  placeholder="New case number"
80
79
  disabled={isSubmitting}
81
80
  onKeyDown={(event) => {
@@ -84,10 +83,10 @@ export const RenameCaseModal = ({
84
83
  }
85
84
  }}
86
85
  />
87
- <div className={sharedStyles.actions}>
86
+ <div className={styles.actions}>
88
87
  <button
89
88
  type="button"
90
- className={sharedStyles.cancelButton}
89
+ className={styles.cancelButton}
91
90
  onClick={requestClose}
92
91
  disabled={isCloseBlocked}
93
92
  >
@@ -95,7 +94,7 @@ export const RenameCaseModal = ({
95
94
  </button>
96
95
  <button
97
96
  type="button"
98
- className={`${sharedStyles.confirmButton} ${styles.confirmButton}`}
97
+ className={`${styles.confirmButton} ${styles.confirmButtonWarning}`}
99
98
  onClick={() => void handleSubmit()}
100
99
  disabled={isSubmitting || !newCaseName.trim()}
101
100
  >
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState, useContext } from 'react';
2
2
  import styles from './navbar.module.css';
3
3
  import { SignOut } from '../actions/signout';
4
4
  import { ManageProfile } from '../user/manage-profile';
5
- import { CaseImport } from '../sidebar/case-import/case-import';
5
+ import { CaseImport } from './case-import/case-import';
6
6
  import { AuthContext } from '~/contexts/auth.context';
7
7
  import { getUserData } from '~/utils/data';
8
8
  import { type ImportResult, type ConfirmationImportResult } from '~/types';
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
2
2
  import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
3
3
  import { type FileData } from '~/types';
4
4
  import sharedStyles from '~/components/navbar/case-modals/case-modal-shared.module.css';
5
- import styles from './delete-files-modal.module.css';
5
+ import styles from './files-modal.module.css';
6
6
 
7
7
  interface DeleteFilesModalProps {
8
8
  isOpen: boolean;
@@ -45,7 +45,7 @@ export const DeleteFilesModal = ({
45
45
 
46
46
  return (
47
47
  <div className={sharedStyles.overlay} aria-label="Close delete files dialog" {...overlayProps}>
48
- <div className={`${sharedStyles.modal} ${styles.modal}`} role="dialog" aria-modal="true" aria-label="Delete Selected Files">
48
+ <div className={`${sharedStyles.modal} ${styles.deleteFilesModal}`} role="dialog" aria-modal="true" aria-label="Delete Selected Files">
49
49
  <button {...getCloseButtonProps({ ariaLabel: 'Close delete files dialog' })}>×</button>
50
50
 
51
51
  <h3 className={sharedStyles.title}>Delete Selected Files</h3>
@@ -79,7 +79,7 @@ export const DeleteFilesModal = ({
79
79
  </button>
80
80
  <button
81
81
  type="button"
82
- className={`${sharedStyles.confirmButton} ${styles.confirmButton}`}
82
+ className={`${sharedStyles.confirmButton} ${styles.deleteFilesConfirmButton}`}
83
83
  onClick={() => {
84
84
  void onSubmit();
85
85
  }}
@@ -457,6 +457,35 @@
457
457
  );
458
458
  }
459
459
 
460
+ /* ─── Delete Files Modal ────────────────────────────────────────────────── */
461
+
462
+ .deleteFilesModal {
463
+ width: min(620px, calc(100vw - 2rem));
464
+ }
465
+
466
+ .filePreviewList {
467
+ margin: 0.6rem 0 0;
468
+ padding-left: 1.1rem;
469
+ color: #3f2a2e;
470
+ font-size: 0.86rem;
471
+ }
472
+
473
+ .filePreviewList li + li {
474
+ margin-top: 0.3rem;
475
+ }
476
+
477
+ .remainingNote {
478
+ margin-top: 0.5rem;
479
+ font-size: 0.84rem;
480
+ color: #6b2f35;
481
+ }
482
+
483
+ .deleteFilesConfirmButton {
484
+ background: #dc3545;
485
+ color: #ffffff;
486
+ border-color: #c82333;
487
+ }
488
+
460
489
  @media (max-width: 980px) {
461
490
  .controlsSection {
462
491
  grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -1,5 +1,5 @@
1
1
  import type { ReactNode } from 'react';
2
- import styles from './notes.module.css';
2
+ import styles from '../notes.module.css';
3
3
  import { CUSTOM, handleSelectWithCustom } from './class-details-shared';
4
4
 
5
5
  interface BaseFieldProps {
@@ -9,7 +9,7 @@ import {
9
9
  } from './class-details-shared';
10
10
  import { BulletSection, CartridgeCaseSection, ShotshellSection } from './class-details-sections';
11
11
  import { useClassDetailsState } from './use-class-details-state';
12
- import styles from './notes.module.css';
12
+ import styles from '../notes.module.css';
13
13
 
14
14
  interface ClassDetailsModalProps {
15
15
  isOpen: boolean;
@@ -1,5 +1,5 @@
1
1
  import { useState } from 'react';
2
- import styles from './notes.module.css';
2
+ import styles from '../notes.module.css';
3
3
  import {
4
4
  BULLET_BARREL_TYPE_OPTIONS,
5
5
  BULLET_CORE_METAL_OPTIONS,
@@ -2,8 +2,8 @@ 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
+ import { ClassDetailsModal } from './class-details/class-details-modal';
6
+ import { buildClassDetailsSummary } from './class-details/class-details-shared';
7
7
  import { getNotes, saveNotes } from '~/components/actions/notes-manage';
8
8
  import { type AnnotationData, type BulletAnnotationData, type CartridgeCaseAnnotationData, type ShotshellAnnotationData } from '~/types/annotations';
9
9
  import { resolveEarliestAnnotationTimestamp } from '~/utils/ui';
@@ -1,7 +1,7 @@
1
1
  import type { User } from 'firebase/auth';
2
2
  import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
3
3
  import { NotesEditorForm } from './notes-editor-form';
4
- import styles from './notes-editor-modal.module.css';
4
+ import styles from './notes.module.css';
5
5
 
6
6
  interface NotesEditorModalProps {
7
7
  isOpen: boolean;
@@ -40,14 +40,14 @@ export const NotesEditorModal = ({
40
40
 
41
41
  return (
42
42
  <div className={styles.overlay} aria-label="Close image notes dialog" {...overlayProps}>
43
- <div className={styles.modal} role="dialog" aria-modal="true" aria-label="Image Notes">
44
- <div className={styles.header}>
45
- <h2 className={styles.title}>Image Notes</h2>
46
- <button className={styles.closeButton} {...getCloseButtonProps({ ariaLabel: 'Close image notes dialog' })}>
43
+ <div className={styles.editorModal} role="dialog" aria-modal="true" aria-label="Image Notes">
44
+ <div className={styles.editorModalHeader}>
45
+ <h2 className={styles.editorModalTitle}>Image Notes</h2>
46
+ <button className={styles.editorModalCloseButton} {...getCloseButtonProps({ ariaLabel: 'Close image notes dialog' })}>
47
47
  ×
48
48
  </button>
49
49
  </div>
50
- <div className={styles.content}>
50
+ <div className={styles.editorModalContent}>
51
51
  <NotesEditorForm
52
52
  currentCase={currentCase}
53
53
  user={user}
@@ -773,6 +773,58 @@ textarea:focus {
773
773
  animation: fadeIn 0.3s ease-in;
774
774
  }
775
775
 
776
+ /* ─── Notes Editor Modal ────────────────────────────────────────────────── */
777
+
778
+ .overlay {
779
+ position: fixed;
780
+ inset: 0;
781
+ background-color: color-mix(in lab, var(--background) 56%, transparent);
782
+ display: flex;
783
+ justify-content: center;
784
+ align-items: center;
785
+ z-index: var(--zIndex5);
786
+ }
787
+
788
+ .editorModal {
789
+ position: relative;
790
+ width: min(900px, calc(100vw - 2rem));
791
+ max-height: calc(100vh - 4rem);
792
+ background: var(--backgroundLight);
793
+ border-radius: var(--spaceXS);
794
+ box-shadow: 0 var(--spaceXS) var(--spaceL)
795
+ color-mix(in lab, var(--black) 16%, transparent);
796
+ display: flex;
797
+ flex-direction: column;
798
+ overflow: hidden;
799
+ }
800
+
801
+ .editorModalHeader {
802
+ display: flex;
803
+ align-items: center;
804
+ justify-content: space-between;
805
+ padding: var(--spaceM) var(--spaceL);
806
+ border-bottom: 1px solid color-mix(in lab, var(--text) 12%, transparent);
807
+ }
808
+
809
+ .editorModalTitle {
810
+ margin: 0;
811
+ color: var(--textTitle);
812
+ font-size: var(--fontSizeBodyM);
813
+ font-weight: var(--fontWeightMedium);
814
+ }
815
+
816
+ .editorModalCloseButton {
817
+ background: none;
818
+ border: none;
819
+ color: var(--textLight);
820
+ }
821
+
822
+ .editorModalContent {
823
+ padding: var(--spaceM) var(--spaceL);
824
+ overflow-y: auto;
825
+ max-height: calc(100vh - 11rem);
826
+ }
827
+
776
828
  @keyframes fadeIn {
777
829
  from {
778
830
  opacity: 0;
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect, useCallback } from 'react';
2
- import styles from './toolbar-color-selector.module.css';
2
+ import styles from './toolbar.module.css';
3
3
 
4
4
  interface ToolbarColorSelectorProps {
5
5
  selectedColor: string;
@@ -66,15 +66,15 @@ export const ToolbarColorSelector = ({
66
66
 
67
67
  return (
68
68
  <div className={styles.toolbarColorSelector}>
69
- <div className={styles.header}>
70
- <span className={styles.title}>Select Box Color</span>
69
+ <div className={styles.colorSelectorHeader}>
70
+ <span className={styles.colorSelectorTitle}>Select Box Color</span>
71
71
  </div>
72
72
 
73
- <div className={styles.content}>
73
+ <div className={styles.colorSelectorContent}>
74
74
  <div className={styles.toggleSection}>
75
75
  <button
76
76
  onClick={() => setShowColorWheel(!showColorWheel)}
77
- className={styles.toggleButton}
77
+ className={styles.colorToggleButton}
78
78
  >
79
79
  {showColorWheel ? 'Presets' : 'Custom'}
80
80
  </button>
@@ -106,11 +106,11 @@ export const ToolbarColorSelector = ({
106
106
  )}
107
107
  </div>
108
108
 
109
- <div className={styles.actions}>
109
+ <div className={styles.colorSelectorActions}>
110
110
  <button
111
111
  type="button"
112
112
  onClick={handleConfirm}
113
- className={`${styles.actionButton} ${styles.confirmButton}`}
113
+ className={`${styles.actionButton} ${styles.colorSelectorConfirmButton}`}
114
114
  title="Apply selected color"
115
115
  >
116
116
  ✓ Apply Color
@@ -118,7 +118,7 @@ export const ToolbarColorSelector = ({
118
118
  <button
119
119
  type="button"
120
120
  onClick={handleCancel}
121
- className={`${styles.actionButton} ${styles.cancelButton}`}
121
+ className={`${styles.actionButton} ${styles.colorSelectorCancelButton}`}
122
122
  title="Cancel color selection"
123
123
  >
124
124
  ✕ Cancel
@@ -9,7 +9,8 @@
9
9
  padding: var(--spaceS);
10
10
  background: var(--background);
11
11
  border-radius: 5px;
12
- box-shadow: 0 var(--spaceXS) var(--spaceM) color-mix(in lab, var(--black) 10%, transparent);
12
+ box-shadow: 0 var(--spaceXS) var(--spaceM)
13
+ color-mix(in lab, var(--black) 10%, transparent);
13
14
  height: auto;
14
15
  overflow: hidden;
15
16
  transition: opacity var(--durationM) var(--bezierFastoutSlowin);
@@ -39,4 +40,182 @@
39
40
  pointer-events: none;
40
41
  height: 0;
41
42
  overflow: hidden;
42
- }
43
+ }
44
+
45
+ /* ─── Color Selector ────────────────────────────────────────────────────── */
46
+
47
+ .toolbarColorSelector {
48
+ position: absolute;
49
+ left: calc(100% + var(--spaceM));
50
+ bottom: 0;
51
+ background: var(--background);
52
+ border-radius: 5px;
53
+ box-shadow: 0 var(--spaceXS) var(--spaceM)
54
+ color-mix(in lab, var(--black) 10%, transparent);
55
+ padding: var(--spaceS);
56
+ width: 200px;
57
+ z-index: 50;
58
+ }
59
+
60
+ .colorSelectorHeader {
61
+ display: flex;
62
+ justify-content: flex-start;
63
+ align-items: center;
64
+ margin-bottom: var(--spaceS);
65
+ padding-bottom: var(--spaceXS);
66
+ border-bottom: 1px solid var(--border);
67
+ }
68
+
69
+ .colorSelectorTitle {
70
+ font-size: var(--fontSizeS);
71
+ font-weight: 600;
72
+ color: var(--foreground);
73
+ }
74
+
75
+ .colorSelectorActions {
76
+ display: flex;
77
+ flex-direction: column;
78
+ gap: var(--spaceXS);
79
+ margin-top: var(--spaceS);
80
+ padding-top: var(--spaceS);
81
+ border-top: 1px solid var(--border);
82
+ }
83
+
84
+ .actionButton {
85
+ width: 100%;
86
+ min-height: 38px;
87
+ border: none;
88
+ border-radius: 4px;
89
+ display: flex;
90
+ align-items: center;
91
+ justify-content: center;
92
+ cursor: pointer;
93
+ font-size: var(--fontSizeS);
94
+ font-weight: 600;
95
+ color: white;
96
+ transition: background-color var(--durationS) var(--bezierFastoutSlowin);
97
+ }
98
+
99
+ .colorSelectorConfirmButton {
100
+ background: var(--success);
101
+ }
102
+
103
+ .colorSelectorConfirmButton:hover {
104
+ background: color-mix(in lab, var(--success) 80%, var(--black));
105
+ }
106
+
107
+ .colorSelectorCancelButton {
108
+ background: var(--error);
109
+ }
110
+
111
+ .colorSelectorCancelButton:hover {
112
+ background: color-mix(in lab, var(--error) 80%, var(--black));
113
+ }
114
+
115
+ .colorSelectorContent {
116
+ display: flex;
117
+ flex-direction: column;
118
+ gap: var(--spaceS);
119
+ }
120
+
121
+ .toggleSection {
122
+ display: flex;
123
+ justify-content: center;
124
+ }
125
+
126
+ .colorToggleButton {
127
+ background: var(--backgroundSecondary);
128
+ color: var(--foreground);
129
+ border: 1px solid var(--border);
130
+ border-radius: 3px;
131
+ padding: var(--spaceXS) var(--spaceS);
132
+ cursor: pointer;
133
+ font-size: var(--fontSizeBodyXS);
134
+ }
135
+
136
+ .colorToggleButton:hover {
137
+ background: var(--backgroundTertiary);
138
+ }
139
+
140
+ .colorWheelSection {
141
+ display: flex;
142
+ justify-content: center;
143
+ }
144
+
145
+ .colorWheel {
146
+ width: 60px;
147
+ height: 40px;
148
+ border: 1px solid var(--border);
149
+ border-radius: 3px;
150
+ cursor: pointer;
151
+ }
152
+
153
+ .colorGrid {
154
+ display: grid;
155
+ grid-template-columns: repeat(5, 1fr);
156
+ gap: var(--spaceXS);
157
+ padding: var(--spaceXS);
158
+ }
159
+
160
+ .colorSwatch {
161
+ width: 28px;
162
+ height: 28px;
163
+ border: 2px solid var(--border);
164
+ border-radius: 3px;
165
+ cursor: pointer;
166
+ position: relative;
167
+ transition: all 0.2s ease;
168
+ }
169
+
170
+ .colorSwatch:hover {
171
+ transform: scale(1.1);
172
+ border-color: var(--foreground);
173
+ }
174
+
175
+ .colorSwatch.selected {
176
+ border-color: var(--foreground);
177
+ border-width: 3px;
178
+ box-shadow:
179
+ 0 0 0 2px var(--background),
180
+ 0 0 0 4px var(--foreground);
181
+ transform: scale(1.15);
182
+ position: relative;
183
+ }
184
+
185
+ .colorSwatch.selected::after {
186
+ content: "✓";
187
+ position: absolute;
188
+ top: 50%;
189
+ left: 50%;
190
+ transform: translate(-50%, -50%);
191
+ color: var(--foreground);
192
+ font-weight: bold;
193
+ font-size: 14px;
194
+ text-shadow:
195
+ 0 0 3px var(--background),
196
+ 0 0 6px var(--background);
197
+ pointer-events: none;
198
+ }
199
+
200
+ .preview {
201
+ display: flex;
202
+ align-items: center;
203
+ gap: var(--spaceS);
204
+ padding: var(--spaceXS);
205
+ background: var(--backgroundSecondary);
206
+ border-radius: 3px;
207
+ }
208
+
209
+ .previewLabel {
210
+ font-size: var(--fontSizeBodyXS);
211
+ color: var(--foreground);
212
+ font-weight: 500;
213
+ }
214
+
215
+ .previewSwatch {
216
+ width: 20px;
217
+ height: 20px;
218
+ border: 1px solid var(--border);
219
+ border-radius: 2px;
220
+ flex-shrink: 0;
221
+ }
@@ -4,7 +4,7 @@ import { auth } from '~/services/firebase';
4
4
  import { fetchUserApi } from '~/utils/api';
5
5
  import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
6
6
  import { auditService } from '~/services/audit';
7
- import styles from './delete-account.module.css';
7
+ import styles from './user.module.css';
8
8
 
9
9
  interface DeletionProgress {
10
10
  totalCases: number;
@@ -323,17 +323,17 @@ export const DeleteAccount = ({ isOpen, onClose, user, company }: DeleteAccountP
323
323
  aria-label="Close delete account dialog"
324
324
  >
325
325
  <div
326
- className={styles.modal}
326
+ className={styles.deleteAccountModal}
327
327
  role="dialog"
328
328
  aria-modal="true"
329
329
  aria-labelledby="modal-title"
330
330
  >
331
331
  {/* Header */}
332
- <header className={styles.modalHeader}>
332
+ <header className={styles.dangerModalHeader}>
333
333
  <h1 id="modal-title" className={styles.dangerTitle}>Delete Striae Account</h1>
334
334
  <button
335
335
  onClick={onClose}
336
- className={styles.closeButton}
336
+ className={styles.dangerCloseButton}
337
337
  aria-label="Close modal"
338
338
  >
339
339
  &times;
@@ -421,7 +421,7 @@ export const DeleteAccount = ({ isOpen, onClose, user, company }: DeleteAccountP
421
421
  {/* Confirmation Form */}
422
422
  {!success && (
423
423
  <form className={styles.confirmationForm}>
424
- <div className={styles.formGroup}>
424
+ <div className={styles.deleteFormGroup}>
425
425
  <label htmlFor="uid-confirmation" className={styles.formLabel}>
426
426
  Enter UID to confirm account deletion:
427
427
  </label>
@@ -436,7 +436,7 @@ export const DeleteAccount = ({ isOpen, onClose, user, company }: DeleteAccountP
436
436
  />
437
437
  </div>
438
438
 
439
- <div className={styles.formGroup}>
439
+ <div className={styles.deleteFormGroup}>
440
440
  <label htmlFor="email-confirmation" className={styles.formLabel}>
441
441
  Enter your email address to confirm account deletion:
442
442
  </label>
@@ -455,7 +455,7 @@ export const DeleteAccount = ({ isOpen, onClose, user, company }: DeleteAccountP
455
455
  <button
456
456
  type="button"
457
457
  onClick={handleDeleteAccount}
458
- className={styles.deleteButton}
458
+ className={styles.deleteAccountButton}
459
459
  disabled={!isConfirmationValid || isDeleting}
460
460
  >
461
461
  {isDeleting ? 'Deleting Account...' : 'Delete Striae Account'}
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect } from 'react';
2
2
  import { useOverlayDismiss } from '~/hooks/useOverlayDismiss';
3
- import styles from './inactivity-warning.module.css';
3
+ import styles from './user.module.css';
4
4
 
5
5
  interface InactivityWarningProps {
6
6
  isOpen: boolean;
@@ -56,16 +56,16 @@ export const InactivityWarning = ({
56
56
  const seconds = countdown % 60;
57
57
 
58
58
  return (
59
- <div className={styles.overlay} aria-label="Close inactivity warning" {...overlayProps}>
60
- <div className={styles.modal}>
59
+ <div className={styles.inactivityOverlay} aria-label="Close inactivity warning" {...overlayProps}>
60
+ <div className={styles.inactivityModal}>
61
61
  <button className={styles.closeButton} {...getCloseButtonProps({ ariaLabel: 'Close inactivity warning' })}>
62
62
  ×
63
63
  </button>
64
- <div className={styles.header}>
64
+ <div className={styles.inactivityHeader}>
65
65
  <h3>Session Timeout Warning</h3>
66
66
  </div>
67
67
 
68
- <div className={styles.content}>
68
+ <div className={styles.inactivityContent}>
69
69
  <p>
70
70
  Your session will expire due to inactivity in:
71
71
  </p>
@@ -77,7 +77,7 @@ export const InactivityWarning = ({
77
77
  </p>
78
78
  </div>
79
79
 
80
- <div className={styles.actions}>
80
+ <div className={styles.inactivityActions}>
81
81
  <button
82
82
  onClick={requestClose}
83
83
  className={styles.extendButton}
@@ -11,7 +11,9 @@ import { handleAuthError, ERROR_MESSAGES } from '~/services/firebase/errors';
11
11
  import { FormField, FormButton } from '../form';
12
12
  import { Toast } from '~/components/toast/toast';
13
13
  import { MfaPhoneUpdateSection } from './mfa-phone-update';
14
- import styles from './manage-profile.module.css';
14
+ import { MfaTotpSection } from './mfa-totp-section';
15
+ import { MfaEnrolledFactors } from './mfa-enrolled-factors';
16
+ import styles from './user.module.css';
15
17
 
16
18
  interface ManageProfileProps {
17
19
  isOpen: boolean;
@@ -27,6 +29,7 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
27
29
  const [email, setEmail] = useState('');
28
30
  const [isLoading, setIsLoading] = useState(false);
29
31
  const [isMfaBusy, setIsMfaBusy] = useState(false);
32
+ const [enrolledFactorsRefreshKey, setEnrolledFactorsRefreshKey] = useState(0);
30
33
  const [showToast, setShowToast] = useState(false);
31
34
  const [toastMessage, setToastMessage] = useState('');
32
35
  const [toastType, setToastType] = useState<'success' | 'error'>('success');
@@ -298,6 +301,20 @@ export const ManageProfile = ({ isOpen, onClose }: ManageProfileProps) => {
298
301
 
299
302
  <MfaPhoneUpdateSection user={user} isOpen={isOpen} onBusyChange={handleMfaBusyChange} />
300
303
 
304
+ <MfaTotpSection
305
+ user={user}
306
+ isOpen={isOpen}
307
+ onBusyChange={handleMfaBusyChange}
308
+ onTotpEnrolled={() => setEnrolledFactorsRefreshKey((k) => k + 1)}
309
+ />
310
+
311
+ <MfaEnrolledFactors
312
+ user={user}
313
+ refreshKey={enrolledFactorsRefreshKey}
314
+ onFactorRemoved={() => setEnrolledFactorsRefreshKey((k) => k + 1)}
315
+ onBusyChange={handleMfaBusyChange}
316
+ />
317
+
301
318
  <div className={styles.buttonGroup}>
302
319
  <FormButton variant="primary" type="submit" isLoading={isLoading} loadingText="Updating...">
303
320
  Update Profile