@striae-org/striae 3.0.4

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 (223) hide show
  1. package/.env.example +100 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +18 -0
  4. package/README.md +133 -0
  5. package/app/components/actions/case-export/core-export.ts +328 -0
  6. package/app/components/actions/case-export/data-processing.ts +167 -0
  7. package/app/components/actions/case-export/download-handlers.ts +900 -0
  8. package/app/components/actions/case-export/index.ts +41 -0
  9. package/app/components/actions/case-export/metadata-helpers.ts +107 -0
  10. package/app/components/actions/case-export/types-constants.ts +56 -0
  11. package/app/components/actions/case-export/validation-utils.ts +25 -0
  12. package/app/components/actions/case-export.ts +4 -0
  13. package/app/components/actions/case-import/annotation-import.ts +35 -0
  14. package/app/components/actions/case-import/confirmation-import.ts +363 -0
  15. package/app/components/actions/case-import/image-operations.ts +61 -0
  16. package/app/components/actions/case-import/index.ts +39 -0
  17. package/app/components/actions/case-import/orchestrator.ts +420 -0
  18. package/app/components/actions/case-import/storage-operations.ts +270 -0
  19. package/app/components/actions/case-import/validation.ts +189 -0
  20. package/app/components/actions/case-import/zip-processing.ts +413 -0
  21. package/app/components/actions/case-manage.ts +524 -0
  22. package/app/components/actions/case-review.ts +4 -0
  23. package/app/components/actions/confirm-export.ts +351 -0
  24. package/app/components/actions/generate-pdf.ts +210 -0
  25. package/app/components/actions/image-manage.ts +385 -0
  26. package/app/components/actions/notes-manage.ts +33 -0
  27. package/app/components/actions/signout.module.css +15 -0
  28. package/app/components/actions/signout.tsx +50 -0
  29. package/app/components/audit/user-audit-viewer.tsx +975 -0
  30. package/app/components/audit/user-audit.module.css +568 -0
  31. package/app/components/auth/auth-provider.tsx +78 -0
  32. package/app/components/auth/mfa-enrollment.module.css +268 -0
  33. package/app/components/auth/mfa-enrollment.tsx +398 -0
  34. package/app/components/auth/mfa-verification.module.css +251 -0
  35. package/app/components/auth/mfa-verification.tsx +295 -0
  36. package/app/components/button/button.module.css +63 -0
  37. package/app/components/button/button.tsx +46 -0
  38. package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
  39. package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
  40. package/app/components/canvas/canvas.module.css +314 -0
  41. package/app/components/canvas/canvas.tsx +449 -0
  42. package/app/components/canvas/confirmation/confirmation.module.css +187 -0
  43. package/app/components/canvas/confirmation/confirmation.tsx +214 -0
  44. package/app/components/colors/colors.module.css +59 -0
  45. package/app/components/colors/colors.tsx +68 -0
  46. package/app/components/form/base-form.tsx +21 -0
  47. package/app/components/form/form-button.tsx +28 -0
  48. package/app/components/form/form-field.tsx +53 -0
  49. package/app/components/form/form-message.tsx +17 -0
  50. package/app/components/form/form-toggle.tsx +23 -0
  51. package/app/components/form/form.module.css +427 -0
  52. package/app/components/form/index.ts +6 -0
  53. package/app/components/icon/icon.module.css +3 -0
  54. package/app/components/icon/icon.tsx +27 -0
  55. package/app/components/icon/icons.svg +102 -0
  56. package/app/components/icon/manifest.json +110 -0
  57. package/app/components/sidebar/case-export/case-export.module.css +386 -0
  58. package/app/components/sidebar/case-export/case-export.tsx +317 -0
  59. package/app/components/sidebar/case-import/case-import.module.css +626 -0
  60. package/app/components/sidebar/case-import/case-import.tsx +404 -0
  61. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
  62. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
  63. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
  64. package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
  65. package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
  66. package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
  67. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
  68. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
  69. package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
  70. package/app/components/sidebar/case-import/index.ts +18 -0
  71. package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
  72. package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
  73. package/app/components/sidebar/cases/cases-modal.module.css +166 -0
  74. package/app/components/sidebar/cases/cases-modal.tsx +201 -0
  75. package/app/components/sidebar/cases/cases.module.css +713 -0
  76. package/app/components/sidebar/files/files-modal.module.css +209 -0
  77. package/app/components/sidebar/files/files-modal.tsx +239 -0
  78. package/app/components/sidebar/hash/hash-utility.module.css +366 -0
  79. package/app/components/sidebar/hash/hash-utility.tsx +982 -0
  80. package/app/components/sidebar/notes/notes-modal.tsx +51 -0
  81. package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
  82. package/app/components/sidebar/notes/notes.module.css +360 -0
  83. package/app/components/sidebar/sidebar-container.tsx +149 -0
  84. package/app/components/sidebar/sidebar.module.css +321 -0
  85. package/app/components/sidebar/sidebar.tsx +215 -0
  86. package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
  87. package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
  88. package/app/components/theme-provider/theme-provider.tsx +131 -0
  89. package/app/components/theme-provider/theme.ts +155 -0
  90. package/app/components/toast/toast.module.css +137 -0
  91. package/app/components/toast/toast.tsx +56 -0
  92. package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
  93. package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
  94. package/app/components/toolbar/toolbar.module.css +42 -0
  95. package/app/components/toolbar/toolbar.tsx +167 -0
  96. package/app/components/user/delete-account.module.css +274 -0
  97. package/app/components/user/delete-account.tsx +471 -0
  98. package/app/components/user/inactivity-warning.module.css +145 -0
  99. package/app/components/user/inactivity-warning.tsx +84 -0
  100. package/app/components/user/manage-profile.module.css +190 -0
  101. package/app/components/user/manage-profile.tsx +253 -0
  102. package/app/components/user/mfa-phone-update.tsx +739 -0
  103. package/app/config-example/admin-service.json +13 -0
  104. package/app/config-example/config.json +17 -0
  105. package/app/config-example/firebase.ts +21 -0
  106. package/app/config-example/inactivity.ts +13 -0
  107. package/app/config-example/meta-config.json +6 -0
  108. package/app/contexts/auth.context.ts +12 -0
  109. package/app/entry.client.tsx +12 -0
  110. package/app/entry.server.tsx +44 -0
  111. package/app/hooks/useInactivityTimeout.ts +110 -0
  112. package/app/root.tsx +170 -0
  113. package/app/routes/_index.tsx +16 -0
  114. package/app/routes/auth/emailActionHandler.module.css +232 -0
  115. package/app/routes/auth/emailActionHandler.tsx +405 -0
  116. package/app/routes/auth/emailVerification.tsx +120 -0
  117. package/app/routes/auth/login.module.css +523 -0
  118. package/app/routes/auth/login.tsx +654 -0
  119. package/app/routes/auth/passwordReset.module.css +274 -0
  120. package/app/routes/auth/passwordReset.tsx +154 -0
  121. package/app/routes/auth/route.ts +16 -0
  122. package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
  123. package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
  124. package/app/routes/mobile-prevented/route.ts +14 -0
  125. package/app/routes/striae/striae.module.css +30 -0
  126. package/app/routes/striae/striae.tsx +417 -0
  127. package/app/services/audit-export.service.ts +755 -0
  128. package/app/services/audit.service.ts +1454 -0
  129. package/app/services/firebase-errors.ts +106 -0
  130. package/app/services/firebase.ts +15 -0
  131. package/app/styles/legal-pages.module.css +113 -0
  132. package/app/styles/root.module.css +146 -0
  133. package/app/tailwind.css +225 -0
  134. package/app/types/annotations.ts +45 -0
  135. package/app/types/audit.ts +301 -0
  136. package/app/types/case.ts +90 -0
  137. package/app/types/export.ts +8 -0
  138. package/app/types/file.ts +30 -0
  139. package/app/types/import.ts +107 -0
  140. package/app/types/index.ts +24 -0
  141. package/app/types/user.ts +38 -0
  142. package/app/utils/SHA256.ts +461 -0
  143. package/app/utils/annotation-timestamp.ts +25 -0
  144. package/app/utils/audit-export-signature.ts +117 -0
  145. package/app/utils/auth-action-settings.ts +48 -0
  146. package/app/utils/auth.ts +34 -0
  147. package/app/utils/batch-operations.ts +135 -0
  148. package/app/utils/confirmation-signature.ts +193 -0
  149. package/app/utils/data-operations.ts +871 -0
  150. package/app/utils/device-detection.ts +5 -0
  151. package/app/utils/html-sanitizer.ts +80 -0
  152. package/app/utils/id-generator.ts +36 -0
  153. package/app/utils/meta.ts +48 -0
  154. package/app/utils/mfa-phone.ts +97 -0
  155. package/app/utils/mfa.ts +79 -0
  156. package/app/utils/password-policy.ts +28 -0
  157. package/app/utils/permissions.ts +562 -0
  158. package/app/utils/signature-utils.ts +160 -0
  159. package/app/utils/style.ts +83 -0
  160. package/app/utils/version.ts +5 -0
  161. package/firebase.json +11 -0
  162. package/functions/[[path]].ts +10 -0
  163. package/package.json +138 -0
  164. package/postcss.config.js +6 -0
  165. package/public/.well-known/publickey.info@striae.org.asc +17 -0
  166. package/public/.well-known/security.txt +7 -0
  167. package/public/_headers +28 -0
  168. package/public/_routes.json +13 -0
  169. package/public/assets/striae.jpg +0 -0
  170. package/public/clear.jpg +0 -0
  171. package/public/favicon.ico +0 -0
  172. package/public/favicon.svg +9 -0
  173. package/public/icon-256.png +0 -0
  174. package/public/icon-512.png +0 -0
  175. package/public/logo-dark.png +0 -0
  176. package/public/manifest.json +25 -0
  177. package/public/oin-badge.png +0 -0
  178. package/public/shortcut.png +0 -0
  179. package/public/social-image.png +0 -0
  180. package/public/striae-ascii.txt +10 -0
  181. package/scripts/deploy-all.sh +100 -0
  182. package/scripts/deploy-config.sh +940 -0
  183. package/scripts/deploy-pages.sh +34 -0
  184. package/scripts/deploy-worker-secrets.sh +215 -0
  185. package/scripts/dev.cjs +23 -0
  186. package/scripts/install-workers.sh +88 -0
  187. package/scripts/run-eslint.cjs +35 -0
  188. package/scripts/update-compatibility-dates.cjs +124 -0
  189. package/scripts/update-markdown-versions.cjs +43 -0
  190. package/tailwind.config.ts +22 -0
  191. package/tsconfig.json +33 -0
  192. package/vite.config.ts +35 -0
  193. package/worker-configuration.d.ts +7490 -0
  194. package/workers/audit-worker/package.json +17 -0
  195. package/workers/audit-worker/src/audit-worker.example.ts +195 -0
  196. package/workers/audit-worker/worker-configuration.d.ts +7448 -0
  197. package/workers/audit-worker/wrangler.jsonc.example +29 -0
  198. package/workers/data-worker/package.json +17 -0
  199. package/workers/data-worker/src/data-worker.example.ts +267 -0
  200. package/workers/data-worker/src/signature-utils.ts +79 -0
  201. package/workers/data-worker/src/signing-payload-utils.ts +290 -0
  202. package/workers/data-worker/worker-configuration.d.ts +7448 -0
  203. package/workers/data-worker/wrangler.jsonc.example +30 -0
  204. package/workers/image-worker/package.json +17 -0
  205. package/workers/image-worker/src/image-worker.example.ts +180 -0
  206. package/workers/image-worker/worker-configuration.d.ts +7447 -0
  207. package/workers/image-worker/wrangler.jsonc.example +22 -0
  208. package/workers/keys-worker/package.json +17 -0
  209. package/workers/keys-worker/src/keys.example.ts +66 -0
  210. package/workers/keys-worker/src/keys.ts +66 -0
  211. package/workers/keys-worker/worker-configuration.d.ts +7447 -0
  212. package/workers/keys-worker/wrangler.jsonc.example +22 -0
  213. package/workers/pdf-worker/package.json +17 -0
  214. package/workers/pdf-worker/src/format-striae.ts +534 -0
  215. package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
  216. package/workers/pdf-worker/src/report-types.ts +69 -0
  217. package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
  218. package/workers/pdf-worker/wrangler.jsonc.example +26 -0
  219. package/workers/user-worker/package.json +17 -0
  220. package/workers/user-worker/src/user-worker.example.ts +636 -0
  221. package/workers/user-worker/worker-configuration.d.ts +7448 -0
  222. package/workers/user-worker/wrangler.jsonc.example +29 -0
  223. package/wrangler.toml.example +8 -0
@@ -0,0 +1,214 @@
1
+ import { useState, useEffect, useContext } from 'react';
2
+ import { ConfirmationData } from '~/types/annotations';
3
+ import { AuthContext } from '~/contexts/auth.context';
4
+ import { generateConfirmationId } from '~/utils/id-generator';
5
+ import styles from './confirmation.module.css';
6
+
7
+ interface ConfirmationModalProps {
8
+ isOpen: boolean;
9
+ onClose: () => void;
10
+ onConfirm?: (confirmationData: ConfirmationData) => void;
11
+ company?: string;
12
+ existingConfirmation?: ConfirmationData | null;
13
+ }
14
+
15
+ // Format current date and time in readable format
16
+ const formatTimestamp = (): string => {
17
+ const now = new Date();
18
+ return now.toLocaleString('en-US', {
19
+ year: 'numeric',
20
+ month: 'long',
21
+ day: 'numeric',
22
+ hour: 'numeric',
23
+ minute: '2-digit',
24
+ second: '2-digit',
25
+ hour12: true
26
+ });
27
+ };
28
+
29
+ export const ConfirmationModal = ({ isOpen, onClose, onConfirm, company, existingConfirmation }: ConfirmationModalProps) => {
30
+ const { user } = useContext(AuthContext);
31
+ const [badgeId, setBadgeId] = useState('');
32
+ const [error, setError] = useState('');
33
+ const [isConfirming, setIsConfirming] = useState(false);
34
+
35
+ const fullName = user?.displayName || user?.email || 'Unknown User';
36
+ const userEmail = user?.email || 'No email available';
37
+ const labCompany = company || 'Not specified';
38
+ const timestamp = formatTimestamp();
39
+ const confirmationId = generateConfirmationId();
40
+
41
+ // Check if this is an existing confirmation
42
+ const hasExistingConfirmation = !!existingConfirmation;
43
+
44
+ // Handle Escape key to close modal
45
+ useEffect(() => {
46
+ const handleEscapeKey = (event: KeyboardEvent) => {
47
+ if (event.key === 'Escape' && isOpen) {
48
+ onClose();
49
+ }
50
+ };
51
+
52
+ if (isOpen) {
53
+ document.addEventListener('keydown', handleEscapeKey);
54
+ }
55
+
56
+ return () => {
57
+ document.removeEventListener('keydown', handleEscapeKey);
58
+ };
59
+ }, [isOpen, onClose]);
60
+
61
+ // Reset form when modal opens
62
+ useEffect(() => {
63
+ if (isOpen) {
64
+ if (existingConfirmation) {
65
+ setBadgeId(existingConfirmation.badgeId);
66
+ } else {
67
+ setBadgeId('');
68
+ }
69
+ setError('');
70
+ setIsConfirming(false);
71
+ }
72
+ }, [isOpen, existingConfirmation]);
73
+
74
+ if (!isOpen) return null;
75
+
76
+ const handleConfirm = async () => {
77
+ if (!badgeId.trim()) {
78
+ setError('Badge/ID is required');
79
+ return;
80
+ }
81
+
82
+ setIsConfirming(true);
83
+ setError('');
84
+
85
+ try {
86
+ const confirmationData: ConfirmationData = {
87
+ fullName,
88
+ badgeId: badgeId.trim(),
89
+ timestamp,
90
+ confirmationId,
91
+ confirmedBy: user?.uid || '',
92
+ confirmedByEmail: user?.email || '',
93
+ confirmedByCompany: labCompany,
94
+ confirmedAt: new Date().toISOString()
95
+ };
96
+
97
+ onConfirm?.(confirmationData);
98
+ onClose();
99
+ } catch (error) {
100
+ console.error('Confirmation failed:', error);
101
+ setError('Confirmation failed. Please try again.');
102
+ } finally {
103
+ setIsConfirming(false);
104
+ }
105
+ };
106
+
107
+ const handleOverlayClick = (e: React.MouseEvent) => {
108
+ if (e.target === e.currentTarget) {
109
+ onClose();
110
+ }
111
+ };
112
+
113
+ return (
114
+ <div className={styles.overlay} onClick={handleOverlayClick}>
115
+ <div className={styles.modal}>
116
+ <div className={styles.header}>
117
+ <h2 className={styles.title}>
118
+ {hasExistingConfirmation ? 'Confirmation Details' : 'Confirm Identification'}
119
+ </h2>
120
+ <button
121
+ className={styles.closeButton}
122
+ onClick={onClose}
123
+ aria-label="Close modal"
124
+ >
125
+ ×
126
+ </button>
127
+ </div>
128
+
129
+ {hasExistingConfirmation && (
130
+ <div className={styles.existingConfirmationBanner}>
131
+ ✓ This image has already been confirmed
132
+ </div>
133
+ )}
134
+
135
+ <div className={styles.content}>
136
+ <div className={styles.fieldGroup}>
137
+ <div className={styles.field}>
138
+ <label className={styles.label}>Name:</label>
139
+ <div className={styles.readOnlyValue}>
140
+ {hasExistingConfirmation ? existingConfirmation.fullName : fullName}
141
+ </div>
142
+ </div>
143
+
144
+ <div className={styles.field}>
145
+ <label className={styles.label} htmlFor="badgeId">Badge/ID: *</label>
146
+ <input
147
+ id="badgeId"
148
+ type="text"
149
+ className={styles.input}
150
+ value={badgeId}
151
+ onChange={(e) => {
152
+ setBadgeId(e.target.value);
153
+ if (error) setError('');
154
+ }}
155
+ placeholder="Enter your badge or ID number"
156
+ disabled={isConfirming || hasExistingConfirmation}
157
+ autoFocus={!hasExistingConfirmation}
158
+ />
159
+ </div>
160
+
161
+ <div className={styles.field}>
162
+ <label className={styles.label}>Email:</label>
163
+ <div className={styles.readOnlyValue}>
164
+ {hasExistingConfirmation ? existingConfirmation.confirmedByEmail : userEmail}
165
+ </div>
166
+ </div>
167
+
168
+ <div className={styles.field}>
169
+ <label className={styles.label}>Lab/Company:</label>
170
+ <div className={styles.readOnlyValue}>
171
+ {hasExistingConfirmation ? existingConfirmation.confirmedByCompany : labCompany}
172
+ </div>
173
+ </div>
174
+
175
+ <div className={styles.field}>
176
+ <label className={styles.label}>Timestamp:</label>
177
+ <div className={styles.readOnlyValue}>
178
+ {hasExistingConfirmation ? existingConfirmation.timestamp : timestamp}
179
+ </div>
180
+ </div>
181
+
182
+ <div className={styles.field}>
183
+ <label className={styles.label}>Confirmation ID:</label>
184
+ <div className={styles.readOnlyValue}>
185
+ {hasExistingConfirmation ? existingConfirmation.confirmationId : confirmationId}
186
+ </div>
187
+ </div>
188
+ </div>
189
+
190
+ {error && <div className={styles.error}>{error}</div>}
191
+ </div>
192
+
193
+ <div className={styles.footer}>
194
+ <button
195
+ className={styles.cancelButton}
196
+ onClick={onClose}
197
+ disabled={isConfirming}
198
+ >
199
+ {hasExistingConfirmation ? 'Close' : 'Cancel'}
200
+ </button>
201
+ {!hasExistingConfirmation && (
202
+ <button
203
+ className={styles.confirmButton}
204
+ onClick={handleConfirm}
205
+ disabled={isConfirming || !badgeId.trim()}
206
+ >
207
+ {isConfirming ? 'Confirming...' : 'Confirm'}
208
+ </button>
209
+ )}
210
+ </div>
211
+ </div>
212
+ </div>
213
+ );
214
+ };
@@ -0,0 +1,59 @@
1
+ .colorSelector {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 0.75rem;
5
+ }
6
+
7
+ .colorHeader {
8
+ display: flex;
9
+ justify-content: space-between;
10
+ align-items: center;
11
+ }
12
+
13
+ .toggleButton {
14
+ padding: 0.25rem 0.5rem;
15
+ font-size: 0.8rem;
16
+ border: 1px solid #ced4da;
17
+ border-radius: 4px;
18
+ background: white;
19
+ cursor: pointer;
20
+ transition: all 0.2s;
21
+ }
22
+
23
+ .toggleButton:hover {
24
+ background: #f8f9fa;
25
+ border-color: #adb5bd;
26
+ }
27
+
28
+ .colorWheel {
29
+ width: 100%;
30
+ height: 40px;
31
+ padding: 0;
32
+ border: 2px solid #ced4da;
33
+ border-radius: 4px;
34
+ cursor: pointer;
35
+ }
36
+
37
+ .colorGrid {
38
+ display: grid;
39
+ grid-template-columns: repeat(5, 1fr);
40
+ gap: 0.5rem;
41
+ }
42
+
43
+ .colorSwatch {
44
+ width: 30px;
45
+ height: 30px;
46
+ border: 2px solid #ced4da;
47
+ border-radius: 4px;
48
+ cursor: pointer;
49
+ transition: transform 0.2s;
50
+ }
51
+
52
+ .colorSwatch:hover {
53
+ transform: scale(1.1);
54
+ }
55
+
56
+ .colorSwatch.selected {
57
+ border-color: #0d6efd;
58
+ box-shadow: 0 0 0 2px rgba(13,110,253,.25);
59
+ }
@@ -0,0 +1,68 @@
1
+ import { useState } from 'react';
2
+ import styles from './colors.module.css';
3
+
4
+ interface ColorSelectorProps {
5
+ selectedColor: string;
6
+ onColorSelect: (color: string) => void;
7
+ }
8
+
9
+ interface ColorOption {
10
+ value: string;
11
+ label: string;
12
+ }
13
+
14
+ const commonColors: ColorOption[] = [
15
+ { value: '#ff0000', label: 'Red' },
16
+ { value: '#ff8000', label: 'Orange' },
17
+ { value: '#ffde21', label: 'Yellow' },
18
+ { value: '#00ff00', label: 'Green' },
19
+ { value: '#00ffff', label: 'Cyan' },
20
+ { value: '#0000ff', label: 'Blue' },
21
+ { value: '#8000ff', label: 'Purple' },
22
+ { value: '#ff00ff', label: 'Magenta' },
23
+ { value: '#000000', label: 'Black' },
24
+ { value: '#ffffff', label: 'White' }
25
+ ];
26
+
27
+ export const ColorSelector = ({ selectedColor, onColorSelect }: ColorSelectorProps) => {
28
+ const [showColorWheel, setShowColorWheel] = useState(false);
29
+
30
+ return (
31
+ <div className={styles.colorSelector}>
32
+ <div className={styles.colorHeader}>
33
+ <span className={styles.colorLabel}>Select color</span>
34
+ <button
35
+ onClick={() => setShowColorWheel(!showColorWheel)}
36
+ className={styles.toggleButton}
37
+ >
38
+ {showColorWheel ? 'Presets' : 'Color Wheel'}
39
+ </button>
40
+ </div>
41
+
42
+ {showColorWheel ? (
43
+ <>
44
+ <input
45
+ type="color"
46
+ value={selectedColor}
47
+ onChange={(e) => onColorSelect(e.target.value)}
48
+ className={styles.colorWheel}
49
+ title="Choose a color"
50
+ />
51
+ </>
52
+ ) : (
53
+ <div className={styles.colorGrid}>
54
+ {commonColors.map((color) => (
55
+ <button
56
+ key={color.value}
57
+ className={`${styles.colorSwatch} ${color.value === selectedColor ? styles.selected : ''}`}
58
+ style={{ backgroundColor: color.value }}
59
+ onClick={() => onColorSelect(color.value)}
60
+ aria-label={`Select ${color.label}`}
61
+ title={color.label}
62
+ />
63
+ ))}
64
+ </div>
65
+ )}
66
+ </div>
67
+ );
68
+ };
@@ -0,0 +1,21 @@
1
+ import { Form as RemixForm } from '@remix-run/react';
2
+ import styles from './form.module.css';
3
+
4
+ interface BaseFormProps {
5
+ children: React.ReactNode;
6
+ method?: 'get' | 'post' | 'put' | 'delete';
7
+ className?: string;
8
+ onSubmit?: React.FormEventHandler<HTMLFormElement>;
9
+ }
10
+
11
+ export const BaseForm = ({ children, method = 'post', className, onSubmit }: BaseFormProps) => {
12
+ return (
13
+ <RemixForm
14
+ method={method}
15
+ className={`${styles.form} ${className || ''}`}
16
+ onSubmit={onSubmit}
17
+ >
18
+ {children}
19
+ </RemixForm>
20
+ );
21
+ };
@@ -0,0 +1,28 @@
1
+ import styles from './form.module.css';
2
+
3
+ interface FormButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
4
+ variant?: 'primary' | 'secondary' | 'success' | 'error';
5
+ isLoading?: boolean;
6
+ loadingText?: string;
7
+ children: React.ReactNode;
8
+ }
9
+
10
+ export const FormButton = ({
11
+ variant = 'primary',
12
+ isLoading = false,
13
+ loadingText = 'Loading...',
14
+ children,
15
+ className,
16
+ disabled,
17
+ ...props
18
+ }: FormButtonProps) => {
19
+ return (
20
+ <button
21
+ className={`${styles.button} ${styles[`button${variant.charAt(0).toUpperCase() + variant.slice(1)}`]} ${className || ''}`}
22
+ disabled={disabled || isLoading}
23
+ {...props}
24
+ >
25
+ {isLoading ? loadingText : children}
26
+ </button>
27
+ );
28
+ };
@@ -0,0 +1,53 @@
1
+ import { forwardRef } from 'react';
2
+ import styles from './form.module.css';
3
+
4
+ interface FormFieldProps extends React.InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement> {
5
+ label?: string;
6
+ error?: string;
7
+ component?: 'input' | 'textarea' | 'select';
8
+ children?: React.ReactNode; // For select options
9
+ }
10
+
11
+ export const FormField = forwardRef<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, FormFieldProps>(
12
+ ({ label, error, component = 'input', className, children, ...props }, ref) => {
13
+ const baseClassName = component === 'textarea' ? styles.textarea : component === 'select' ? styles.select : styles.input;
14
+
15
+ return (
16
+ <div className={styles.fieldWrapper}>
17
+ {label && (
18
+ <label className={styles.label} htmlFor={props.id || props.name}>
19
+ {label}
20
+ </label>
21
+ )}
22
+ {component === 'input' && (
23
+ <input
24
+ ref={ref as React.Ref<HTMLInputElement>}
25
+ className={`${baseClassName} ${className || ''}`}
26
+ {...(props as React.InputHTMLAttributes<HTMLInputElement>)}
27
+ />
28
+ )}
29
+ {component === 'textarea' && (
30
+ <textarea
31
+ ref={ref as React.Ref<HTMLTextAreaElement>}
32
+ className={`${baseClassName} ${className || ''}`}
33
+ {...(props as React.TextareaHTMLAttributes<HTMLTextAreaElement>)}
34
+ />
35
+ )}
36
+ {component === 'select' && (
37
+ <select
38
+ ref={ref as React.Ref<HTMLSelectElement>}
39
+ className={`${baseClassName} ${className || ''}`}
40
+ {...(props as React.SelectHTMLAttributes<HTMLSelectElement>)}
41
+ >
42
+ {children}
43
+ </select>
44
+ )}
45
+ {error && (
46
+ <p className={styles.error}>{error}</p>
47
+ )}
48
+ </div>
49
+ );
50
+ }
51
+ );
52
+
53
+ FormField.displayName = 'FormField';
@@ -0,0 +1,17 @@
1
+ import styles from './form.module.css';
2
+
3
+ interface FormMessageProps {
4
+ type: 'error' | 'success';
5
+ title?: string;
6
+ message: string;
7
+ className?: string;
8
+ }
9
+
10
+ export const FormMessage = ({ type, title, message, className }: FormMessageProps) => {
11
+ return (
12
+ <div className={`${styles[`${type}Message`]} ${className || ''}`}>
13
+ {title && <h2><strong>{title}</strong></h2>}
14
+ <p>{message}</p>
15
+ </div>
16
+ );
17
+ };
@@ -0,0 +1,23 @@
1
+ import styles from './form.module.css';
2
+
3
+ interface FormToggleProps extends React.InputHTMLAttributes<HTMLInputElement> {
4
+ label: string | React.ReactNode;
5
+ error?: string;
6
+ }
7
+
8
+ export const FormToggle = ({ label, error, className, ...props }: FormToggleProps) => {
9
+ return (
10
+ <div className={styles.toggleWrapper}>
11
+ <label className={`${styles.toggle} ${className || ''}`}>
12
+ <input
13
+ type="checkbox"
14
+ {...props}
15
+ />
16
+ <span>{label}</span>
17
+ </label>
18
+ {error && (
19
+ <p className={styles.error}>{error}</p>
20
+ )}
21
+ </div>
22
+ );
23
+ };