@kenyaemr/esm-patient-clinical-view-app 5.4.2-pre.2714 → 5.4.2-pre.2722

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/.turbo/turbo-build.log +4 -4
  2. package/dist/805.js +1 -0
  3. package/dist/805.js.map +1 -0
  4. package/dist/kenyaemr-esm-patient-clinical-view-app.js +2 -2
  5. package/dist/kenyaemr-esm-patient-clinical-view-app.js.buildmanifest.json +27 -27
  6. package/dist/main.js +27 -27
  7. package/dist/main.js.map +1 -1
  8. package/dist/routes.json +1 -1
  9. package/package.json +1 -1
  10. package/src/config-schema.ts +97 -0
  11. package/src/contact-list/contact-tracing-history.component.tsx +18 -15
  12. package/src/maternal-and-child-health/partography/components/pulse-bp-graph.component.tsx +1 -0
  13. package/src/maternal-and-child-health/partography/components/temperature-graph.component.tsx +218 -0
  14. package/src/maternal-and-child-health/partography/components/uterine-contractions-graph.component.tsx +209 -0
  15. package/src/maternal-and-child-health/partography/forms/cervical-contractions-form.component.tsx +211 -0
  16. package/src/maternal-and-child-health/partography/forms/cervix-form.component.tsx +354 -0
  17. package/src/maternal-and-child-health/partography/forms/drugs-iv-fluids-form.component.tsx +321 -0
  18. package/src/maternal-and-child-health/partography/forms/fetal-heart-rate-form.component.tsx +275 -0
  19. package/src/maternal-and-child-health/partography/forms/index.ts +9 -0
  20. package/src/maternal-and-child-health/partography/forms/membrane-amniotic-fluid-form.component.tsx +330 -0
  21. package/src/maternal-and-child-health/partography/forms/oxytocin-form.component.tsx +207 -0
  22. package/src/maternal-and-child-health/partography/forms/pulse-bp-form.component.tsx +174 -0
  23. package/src/maternal-and-child-health/partography/forms/temperature-form.component.tsx +210 -0
  24. package/src/maternal-and-child-health/partography/forms/time-picker-dropdown.component.tsx +218 -0
  25. package/src/maternal-and-child-health/partography/forms/time-picker-dropdown.scss +107 -0
  26. package/src/maternal-and-child-health/partography/forms/time-picker-with-clock.component.tsx +174 -0
  27. package/src/maternal-and-child-health/partography/forms/time-picker-with-clock.scss +178 -0
  28. package/src/maternal-and-child-health/partography/forms/urine-test-form.component.tsx +255 -0
  29. package/src/maternal-and-child-health/partography/forms/useCervixData.ts +16 -0
  30. package/src/maternal-and-child-health/partography/graphs/cervical-contractions-graph.component.tsx +266 -0
  31. package/src/maternal-and-child-health/partography/graphs/cervix-graph.component.tsx +429 -0
  32. package/src/maternal-and-child-health/partography/graphs/drugs-iv-fluids-graph-wrapper.component.tsx +163 -0
  33. package/src/maternal-and-child-health/partography/graphs/drugs-iv-fluids-graph.component.tsx +82 -0
  34. package/src/maternal-and-child-health/partography/graphs/fetal-heart-rate-graph.component.tsx +359 -0
  35. package/src/maternal-and-child-health/partography/graphs/index.ts +10 -0
  36. package/src/maternal-and-child-health/partography/graphs/membrane-amniotic-fluid-graph.component.tsx +266 -0
  37. package/src/maternal-and-child-health/partography/graphs/oxytocin-graph-wrapper.component.tsx +190 -0
  38. package/src/maternal-and-child-health/partography/graphs/oxytocin-graph.component.tsx +126 -0
  39. package/src/maternal-and-child-health/partography/graphs/partograph-graph.component.tsx +266 -0
  40. package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph-wrapper.component.tsx +298 -0
  41. package/src/maternal-and-child-health/partography/graphs/pulse-bp-graph.component.tsx +267 -0
  42. package/src/maternal-and-child-health/partography/graphs/temperature-graph.component.tsx +242 -0
  43. package/src/maternal-and-child-health/partography/graphs/urine-test-graph.component.tsx +246 -0
  44. package/src/maternal-and-child-health/partography/partograph.component.tsx +2141 -118
  45. package/src/maternal-and-child-health/partography/partography-dashboard.meta.ts +8 -0
  46. package/src/maternal-and-child-health/partography/partography-data-form.scss +163 -0
  47. package/src/maternal-and-child-health/partography/partography.resource.ts +233 -326
  48. package/src/maternal-and-child-health/partography/partography.scss +1341 -3
  49. package/src/maternal-and-child-health/partography/resources/blood-pressure.resource.ts +96 -0
  50. package/src/maternal-and-child-health/partography/resources/cervical-dilation.resource.ts +109 -0
  51. package/src/maternal-and-child-health/partography/resources/cervix.resource.ts +362 -0
  52. package/src/maternal-and-child-health/partography/resources/descent-of-head.resource.ts +101 -0
  53. package/src/maternal-and-child-health/partography/resources/drugs-fluids.resource.ts +88 -0
  54. package/src/maternal-and-child-health/partography/resources/fetal-heart-rate.resource.ts +122 -0
  55. package/src/maternal-and-child-health/partography/resources/maternal-pulse.resource.ts +77 -0
  56. package/src/maternal-and-child-health/partography/resources/membrane-amniotic-fluid.resource.ts +108 -0
  57. package/src/maternal-and-child-health/partography/resources/oxytocin.resource.ts +159 -0
  58. package/src/maternal-and-child-health/partography/resources/progress-events.resource.ts +6 -0
  59. package/src/maternal-and-child-health/partography/resources/pulse-bp-combined.resource.ts +53 -0
  60. package/src/maternal-and-child-health/partography/resources/temperature.resource.ts +84 -0
  61. package/src/maternal-and-child-health/partography/resources/uterine-contractions.resource.ts +173 -0
  62. package/src/maternal-and-child-health/partography/table/temperature-table.component.tsx +99 -0
  63. package/src/maternal-and-child-health/partography/table/uterine-contractions-table.component.tsx +86 -0
  64. package/src/maternal-and-child-health/partography/types/index.ts +319 -101
  65. package/dist/397.js +0 -1
  66. package/dist/397.js.map +0 -1
@@ -0,0 +1,218 @@
1
+ import React, { useMemo } from 'react';
2
+ import { Select, SelectItem, FormGroup } from '@carbon/react';
3
+ import styles from './time-picker-dropdown.scss';
4
+
5
+ interface TimePickerDropdownProps {
6
+ id: string;
7
+ labelText: string;
8
+ value: string;
9
+ onChange: (value: string) => void;
10
+ invalid?: boolean;
11
+ invalidText?: string;
12
+ existingTimeEntries?: Array<{ hour: number; time: string }>;
13
+ }
14
+
15
+ const TimePickerDropdown: React.FC<TimePickerDropdownProps> = ({
16
+ id,
17
+ labelText,
18
+ value,
19
+ onChange,
20
+ invalid = false,
21
+ invalidText,
22
+ existingTimeEntries = [],
23
+ }) => {
24
+ const [hours, minutes] = value && value.includes(':') ? value.split(':') : ['', ''];
25
+
26
+ // Memoize sorted entries and latest entry to avoid re-sorting on every render
27
+ const latestEntry = useMemo(() => {
28
+ if (!existingTimeEntries || existingTimeEntries.length === 0) {
29
+ return null;
30
+ }
31
+ const sorted = existingTimeEntries.slice().sort((a, b) => {
32
+ const aTime = a.time.split(':').map(Number);
33
+ const bTime = b.time.split(':').map(Number);
34
+ const aTimeInMinutes = aTime[0] * 60 + aTime[1];
35
+ const bTimeInMinutes = bTime[0] * 60 + bTime[1];
36
+ return aTimeInMinutes - bTimeInMinutes;
37
+ });
38
+ return sorted[sorted.length - 1];
39
+ }, [existingTimeEntries]);
40
+
41
+ const isTimeDisabled = (hour: string, minute: string) => {
42
+ if (!latestEntry) {
43
+ return false;
44
+ }
45
+ const currentHour = parseInt(hour);
46
+ const currentMinute = parseInt(minute);
47
+ const currentTimeInMinutes = currentHour * 60 + currentMinute;
48
+ const [latestHour, latestMinute] = latestEntry.time.split(':').map(Number);
49
+ const latestTimeInMinutes = latestHour * 60 + latestMinute;
50
+ return currentTimeInMinutes <= latestTimeInMinutes;
51
+ };
52
+
53
+ // Memoize options generation so we don't recreate arrays on every render
54
+ const hourOptions = useMemo(() => {
55
+ return Array.from({ length: 24 }, (_, i) => {
56
+ const hour = i.toString().padStart(2, '0');
57
+ let isDisabled = false;
58
+ let disableReason = '';
59
+ if (existingTimeEntries && existingTimeEntries.length > 0 && latestEntry) {
60
+ const [latestHour] = latestEntry.time.split(':').map(Number);
61
+ if (i < latestHour) {
62
+ isDisabled = true;
63
+ disableReason = 'before latest entry';
64
+ }
65
+ }
66
+ const displayText = isDisabled ? `${hour} ${disableReason}` : hour;
67
+ return {
68
+ value: hour,
69
+ text: displayText,
70
+ disabled: isDisabled,
71
+ reason: disableReason,
72
+ };
73
+ });
74
+ }, [existingTimeEntries, latestEntry]);
75
+
76
+ const minuteOptions = useMemo(() => {
77
+ return Array.from({ length: 12 }, (_, i) => {
78
+ const minute = (i * 5).toString().padStart(2, '0');
79
+ let isDisabled = false;
80
+ let disableReason = '';
81
+ if (hours && existingTimeEntries.length > 0 && latestEntry) {
82
+ const currentHour = parseInt(hours);
83
+ const currentMinute = i * 5;
84
+ const [latestHour, latestMinute] = latestEntry.time.split(':').map(Number);
85
+ if (currentHour === latestHour && currentMinute <= latestMinute) {
86
+ isDisabled = true;
87
+ disableReason = `≤ ${latestMinute} min`;
88
+ }
89
+ }
90
+ const displayText = isDisabled ? `${minute} ${disableReason}` : minute;
91
+ return { value: minute, text: displayText, disabled: isDisabled };
92
+ });
93
+ }, [hours, existingTimeEntries, latestEntry]);
94
+
95
+ const handleHourChange = (selectedHour: string) => {
96
+ if (!selectedHour || selectedHour === '') {
97
+ return;
98
+ }
99
+ const hourOption = hourOptions.find((opt) => opt.value === selectedHour);
100
+ if (hourOption && hourOption.disabled) {
101
+ alert(`This hour (${selectedHour}) cannot be selected as it is before a previous entry.`);
102
+ return;
103
+ }
104
+ let defaultMinute = '00';
105
+ if (existingTimeEntries.length > 0 && latestEntry) {
106
+ const [latestHour, latestMinute] = latestEntry.time.split(':').map(Number);
107
+ const selectedHourInt = parseInt(selectedHour);
108
+ if (selectedHourInt === latestHour) {
109
+ const nextValidMinute = Math.ceil((latestMinute + 1) / 5) * 5;
110
+ if (nextValidMinute < 60) {
111
+ defaultMinute = nextValidMinute.toString().padStart(2, '0');
112
+ } else {
113
+ alert(`Hour ${selectedHour} has no available time slots after ${latestEntry.time}`);
114
+ return;
115
+ }
116
+ }
117
+ }
118
+ const newTime = `${selectedHour}:${defaultMinute}`;
119
+ onChange(newTime);
120
+ };
121
+
122
+ const handleMinuteChange = (selectedMinute: string) => {
123
+ const newTime = `${hours || '00'}:${selectedMinute}`;
124
+ if (existingTimeEntries.length > 0 && latestEntry && hours) {
125
+ const currentHour = parseInt(hours);
126
+ const currentMinute = parseInt(selectedMinute);
127
+ const [latestHour, latestMinute] = latestEntry.time.split(':').map(Number);
128
+ if (currentHour === latestHour && currentMinute <= latestMinute) {
129
+ alert(`Cannot select ${selectedMinute} minutes. Must select a time after ${latestEntry.time}`);
130
+ return;
131
+ }
132
+ }
133
+ onChange(newTime);
134
+ };
135
+
136
+ return (
137
+ <div className={styles.timePickerContainer}>
138
+ <FormGroup legendText={labelText} invalid={invalid}>
139
+ <div className={styles.timeInputsWrapper}>
140
+ <div className={styles.timeInput}>
141
+ <Select
142
+ id={`${id}-hours`}
143
+ labelText="Hours (HH)"
144
+ value={hours}
145
+ onChange={(e) => handleHourChange((e.target as HTMLSelectElement).value)}
146
+ invalid={invalid}>
147
+ <SelectItem value="" text="HH" />
148
+ {hourOptions.map((option) => (
149
+ <SelectItem
150
+ key={option.value}
151
+ value={option.disabled ? '' : option.value}
152
+ text={option.text}
153
+ disabled={option.disabled}
154
+ className={option.disabled ? styles.disabledOption : undefined}
155
+ />
156
+ ))}
157
+ </Select>
158
+ </div>
159
+
160
+ <div className={styles.separator}>:</div>
161
+
162
+ <div className={styles.timeInput}>
163
+ <Select
164
+ id={`${id}-minutes`}
165
+ labelText="Minutes (MM)"
166
+ value={minutes}
167
+ onChange={(e) => handleMinuteChange((e.target as HTMLSelectElement).value)}
168
+ invalid={invalid}>
169
+ <SelectItem value="" text="MM" />
170
+ {minuteOptions.map((option) => (
171
+ <SelectItem
172
+ key={option.value}
173
+ value={option.value}
174
+ text={option.text}
175
+ disabled={option.disabled}
176
+ className={option.disabled ? styles.disabledOption : undefined}
177
+ />
178
+ ))}
179
+ </Select>
180
+ </div>
181
+ </div>
182
+
183
+ {invalid && invalidText && <div className={styles.errorText}>{invalidText}</div>}
184
+
185
+ {latestEntry && (
186
+ <div className={styles.timeConstraint}>
187
+ Latest entry: <strong>{latestEntry.time}</strong> - Next measurement must be after this time
188
+ </div>
189
+ )}
190
+
191
+ {existingTimeEntries.length > 0 && (
192
+ <div className={styles.usedTimesIndicator}>
193
+ <strong>Time Restrictions:</strong>
194
+ <br />
195
+ <strong>Used Times:</strong> {existingTimeEntries.map((entry) => entry.time).join(', ')}
196
+ <br />
197
+ <strong>Blocked Hours:</strong>{' '}
198
+ {hourOptions
199
+ .filter((opt) => opt.disabled)
200
+ .map((opt) => opt.value)
201
+ .join(', ')}{' '}
202
+ (grayed out)
203
+ <br />
204
+ <small>You can only select times after the latest entry</small>
205
+ </div>
206
+ )}
207
+
208
+ {value && (
209
+ <div className={styles.timePreview}>
210
+ Selected Time: <strong>{value}</strong>
211
+ </div>
212
+ )}
213
+ </FormGroup>
214
+ </div>
215
+ );
216
+ };
217
+
218
+ export default TimePickerDropdown;
@@ -0,0 +1,107 @@
1
+ @use '~@openmrs/esm-styleguide/src/vars' as *;
2
+ @use '~@carbon/styles/scss/spacing' as *;
3
+ @use '~@carbon/styles/scss/type' as *;
4
+ @use '~@carbon/styles/scss/colors' as *;
5
+ @use '~@carbon/styles/scss/theme' as *;
6
+
7
+ .timePickerContainer {
8
+ width: 100%;
9
+ margin-bottom: $spacing-05;
10
+ }
11
+
12
+ .timeInputsWrapper {
13
+ display: flex;
14
+ align-items: flex-end;
15
+ gap: $spacing-03;
16
+ margin-top: $spacing-03;
17
+ }
18
+
19
+ .timeInput {
20
+ flex: 1;
21
+ min-width: 80px;
22
+ }
23
+
24
+ .separator {
25
+ @include type-style('heading-compact-01');
26
+ color: var(--cds-text-primary);
27
+ padding-bottom: $spacing-05;
28
+ font-weight: 600;
29
+ font-size: 1.25rem;
30
+ align-self: center;
31
+ margin-top: $spacing-02;
32
+ }
33
+
34
+ .timePreview {
35
+ @include type-style('body-compact-01');
36
+ color: var(--cds-text-secondary);
37
+ margin-top: $spacing-03;
38
+ padding: $spacing-03;
39
+ background-color: var(--cds-layer-01);
40
+ border-radius: 4px;
41
+ border-left: 3px solid var(--cds-support-success);
42
+ }
43
+
44
+ .timeConstraint {
45
+ @include type-style('body-compact-01');
46
+ color: var(--cds-text-secondary);
47
+ margin-top: $spacing-03;
48
+ padding: $spacing-03;
49
+ background-color: var(--cds-layer-01);
50
+ border-radius: 4px;
51
+ border-left: 3px solid var(--cds-support-warning);
52
+ }
53
+
54
+ .usedTimesIndicator {
55
+ @include type-style('body-compact-01');
56
+ color: var(--cds-text-secondary);
57
+ margin-top: $spacing-03;
58
+ padding: $spacing-03;
59
+ background-color: var(--cds-layer-accent);
60
+ border-radius: 4px;
61
+ border-left: 3px solid var(--cds-support-error);
62
+
63
+ small {
64
+ @include type-style('caption-01');
65
+ color: var(--cds-text-secondary);
66
+ font-style: italic;
67
+ }
68
+ }
69
+
70
+ .errorText {
71
+ @include type-style('caption-01');
72
+ color: var(--cds-support-error);
73
+ margin-top: $spacing-02;
74
+ }
75
+
76
+ // Disabled option styling for used times
77
+ .disabledOption {
78
+ color: var(--cds-text-disabled) !important;
79
+ background-color: var(--cds-layer-disabled) !important;
80
+ opacity: 0.5 !important;
81
+ font-style: italic;
82
+ cursor: not-allowed !important;
83
+
84
+ &:hover {
85
+ background-color: var(--cds-layer-disabled) !important;
86
+ color: var(--cds-text-disabled) !important;
87
+ }
88
+ }
89
+
90
+ // Additional styling for disabled select items in Carbon
91
+ :global(.cds--list-box__menu-item--disabled) {
92
+ color: var(--cds-text-disabled) !important;
93
+ background-color: var(--cds-layer-disabled) !important;
94
+ opacity: 0.5 !important;
95
+ font-style: italic;
96
+ cursor: not-allowed !important;
97
+
98
+ &:hover {
99
+ background-color: var(--cds-layer-disabled) !important;
100
+ color: var(--cds-text-disabled) !important;
101
+ }
102
+
103
+ &::after {
104
+ content: ' 🚫';
105
+ font-style: normal;
106
+ }
107
+ }
@@ -0,0 +1,174 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { TextInput, Layer } from '@carbon/react';
3
+ import { Time } from '@carbon/react/icons';
4
+ import styles from './time-picker-with-clock.scss';
5
+
6
+ interface TimePickerWithClockProps {
7
+ id: string;
8
+ labelText: string;
9
+ placeholder?: string;
10
+ value: string;
11
+ onChange: (value: string) => void;
12
+ invalid?: boolean;
13
+ invalidText?: string;
14
+ }
15
+
16
+ const TimePickerWithClock: React.FC<TimePickerWithClockProps> = ({
17
+ id,
18
+ labelText,
19
+ placeholder = 'HH:MM',
20
+ value,
21
+ onChange,
22
+ invalid = false,
23
+ invalidText,
24
+ }) => {
25
+ const [isClockOpen, setIsClockOpen] = useState(false);
26
+ const [selectedHour, setSelectedHour] = useState<number | null>(null);
27
+ const [selectedMinute, setSelectedMinute] = useState<number | null>(null);
28
+ const clockRef = useRef<HTMLDivElement>(null);
29
+ const inputRef = useRef<HTMLInputElement>(null);
30
+
31
+ useEffect(() => {
32
+ if (value && value.includes(':')) {
33
+ const [hours, minutes] = value.split(':').map(Number);
34
+ if (!isNaN(hours) && !isNaN(minutes)) {
35
+ setSelectedHour(hours);
36
+ setSelectedMinute(minutes);
37
+ }
38
+ }
39
+ }, [value]);
40
+
41
+ useEffect(() => {
42
+ const handleClickOutside = (event: MouseEvent) => {
43
+ if (clockRef.current && !clockRef.current.contains(event.target as Node)) {
44
+ setIsClockOpen(false);
45
+ }
46
+ };
47
+ if (isClockOpen) {
48
+ document.addEventListener('mousedown', handleClickOutside);
49
+ }
50
+ return () => {
51
+ document.removeEventListener('mousedown', handleClickOutside);
52
+ };
53
+ }, [isClockOpen]);
54
+
55
+ const handleInputClick = () => {
56
+ setIsClockOpen(true);
57
+ };
58
+
59
+ const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
60
+ const inputValue = event.target.value;
61
+ onChange(inputValue);
62
+ };
63
+
64
+ const handleHourClick = (hour: number) => {
65
+ setSelectedHour(hour);
66
+ if (selectedMinute !== null) {
67
+ const timeString = `${hour.toString().padStart(2, '0')}:${selectedMinute.toString().padStart(2, '0')}`;
68
+ onChange(timeString);
69
+ setIsClockOpen(false);
70
+ }
71
+ };
72
+
73
+ const handleMinuteClick = (minute: number) => {
74
+ setSelectedMinute(minute);
75
+ if (selectedHour !== null) {
76
+ const timeString = `${selectedHour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
77
+ onChange(timeString);
78
+ setIsClockOpen(false);
79
+ }
80
+ };
81
+
82
+ const renderClockFace = () => {
83
+ if (selectedHour === null) {
84
+ return (
85
+ <div className={styles.clockFace}>
86
+ <div className={styles.clockTitle}>Select Hour</div>
87
+ <div className={styles.clockNumbers}>
88
+ {Array.from({ length: 24 }, (_, i) => (
89
+ <button
90
+ key={i}
91
+ type="button"
92
+ className={`${styles.clockNumber} ${styles.hourNumber}`}
93
+ onClick={() => handleHourClick(i)}
94
+ style={{
95
+ transform: `rotate(${i * 15}deg) translate(0, -80px) rotate(-${i * 15}deg)`,
96
+ }}>
97
+ {i.toString().padStart(2, '0')}
98
+ </button>
99
+ ))}
100
+ </div>
101
+ </div>
102
+ );
103
+ } else {
104
+ return (
105
+ <div className={styles.clockFace}>
106
+ <div className={styles.clockTitle}>
107
+ Select Minute
108
+ <button type="button" className={styles.backButton} onClick={() => setSelectedHour(null)}>
109
+ ← Back to Hours
110
+ </button>
111
+ </div>
112
+ <div className={styles.clockNumbers}>
113
+ {Array.from({ length: 12 }, (_, i) => {
114
+ const minute = i * 5;
115
+ return (
116
+ <button
117
+ key={minute}
118
+ type="button"
119
+ className={`${styles.clockNumber} ${styles.minuteNumber}`}
120
+ onClick={() => handleMinuteClick(minute)}
121
+ style={{
122
+ transform: `rotate(${i * 30}deg) translate(0, -80px) rotate(-${i * 30}deg)`,
123
+ }}>
124
+ {minute.toString().padStart(2, '0')}
125
+ </button>
126
+ );
127
+ })}
128
+ </div>
129
+ </div>
130
+ );
131
+ }
132
+ };
133
+
134
+ return (
135
+ <div className={styles.timePickerContainer}>
136
+ <div
137
+ className={styles.inputWrapper}
138
+ onClick={handleInputClick}
139
+ onKeyDown={(e) => {
140
+ if (e.key === 'Enter' || e.key === ' ') {
141
+ e.preventDefault();
142
+ handleInputClick();
143
+ }
144
+ }}
145
+ role="button"
146
+ tabIndex={0}>
147
+ <TextInput
148
+ ref={inputRef}
149
+ id={id}
150
+ labelText={labelText}
151
+ placeholder={placeholder}
152
+ value={value}
153
+ onChange={handleInputChange}
154
+ invalid={invalid}
155
+ invalidText={invalidText}
156
+ readOnly
157
+ />
158
+ <div className={styles.clockIcon}>
159
+ <Time size={16} />
160
+ </div>
161
+ </div>
162
+
163
+ {isClockOpen && (
164
+ <Layer>
165
+ <div ref={clockRef} className={styles.clockDropdown}>
166
+ {renderClockFace()}
167
+ </div>
168
+ </Layer>
169
+ )}
170
+ </div>
171
+ );
172
+ };
173
+
174
+ export default TimePickerWithClock;
@@ -0,0 +1,178 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @use '@carbon/styles/scss/type';
3
+ @use '@carbon/colors';
4
+ @use '~@openmrs/esm-styleguide/src/vars' as *;
5
+
6
+ .timePickerContainer {
7
+ position: relative;
8
+ width: 100%;
9
+ }
10
+
11
+ .inputWrapper {
12
+ position: relative;
13
+ cursor: pointer;
14
+ input {
15
+ cursor: pointer;
16
+ caret-color: transparent;
17
+ }
18
+ }
19
+
20
+ .clockIcon {
21
+ position: absolute;
22
+ right: spacing.$spacing-05;
23
+ top: 50%;
24
+ transform: translateY(-50%);
25
+ pointer-events: none;
26
+ color: $text-02;
27
+ z-index: 1;
28
+ }
29
+
30
+ .clockDropdown {
31
+ position: absolute;
32
+ top: 100%;
33
+ left: 0;
34
+ right: 0;
35
+ background: $ui-background;
36
+ border: 1px solid $ui-03;
37
+ border-radius: 4px;
38
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
39
+ z-index: 9999;
40
+ padding: spacing.$spacing-05;
41
+ margin-top: spacing.$spacing-02;
42
+ min-height: 240px;
43
+ }
44
+
45
+ .clockFace {
46
+ display: flex;
47
+ flex-direction: column;
48
+ align-items: center;
49
+ width: 100%;
50
+ }
51
+
52
+ .clockTitle {
53
+ @include type.type-style('heading-compact-02');
54
+ color: $text-02;
55
+ margin-bottom: spacing.$spacing-05;
56
+ text-align: center;
57
+ display: flex;
58
+ flex-direction: column;
59
+ gap: spacing.$spacing-03;
60
+ }
61
+
62
+ .backButton {
63
+ @include type.type-style('body-compact-01');
64
+ background: none;
65
+ border: none;
66
+ color: colors.$blue-60;
67
+ cursor: pointer;
68
+ text-decoration: underline;
69
+ padding: 0;
70
+
71
+ &:hover {
72
+ color: colors.$blue-70;
73
+ }
74
+ }
75
+
76
+ .clockNumbers {
77
+ position: relative;
78
+ width: 180px;
79
+ height: 180px;
80
+ border-radius: 50%;
81
+ border: 2px solid $ui-03;
82
+ background: $ui-background;
83
+ }
84
+
85
+ .clockNumber {
86
+ position: absolute;
87
+ top: 50%;
88
+ left: 50%;
89
+ transform-origin: 0 0;
90
+ width: 32px;
91
+ height: 32px;
92
+ border: 1px solid $ui-03;
93
+ border-radius: 50%;
94
+ background: $ui-background;
95
+ color: $text-02;
96
+ cursor: pointer;
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ @include type.type-style('body-compact-01');
101
+ transition: all 0.2s ease;
102
+
103
+ &:hover {
104
+ background: $ui-02;
105
+ border-color: colors.$blue-60;
106
+ transform: scale(1.1);
107
+ }
108
+
109
+ &:active {
110
+ background: colors.$blue-60;
111
+ color: colors.$white;
112
+ }
113
+ }
114
+
115
+ .hourNumber {
116
+ font-weight: 600;
117
+
118
+ &:hover {
119
+ background: colors.$green-50;
120
+ color: colors.$white;
121
+ }
122
+ }
123
+
124
+ .minuteNumber {
125
+ background: $ui-02;
126
+
127
+ &:hover {
128
+ background: colors.$purple-60;
129
+ color: colors.$white;
130
+ }
131
+ }
132
+
133
+ @media (max-width: 672px) {
134
+ .clockDropdown {
135
+ position: fixed;
136
+ top: 50%;
137
+ left: 50%;
138
+ right: auto;
139
+ transform: translate(-50%, -50%);
140
+ width: 90vw;
141
+ max-width: 300px;
142
+ border-radius: 8px;
143
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
144
+ }
145
+
146
+ .clockNumbers {
147
+ width: 160px;
148
+ height: 160px;
149
+ }
150
+
151
+ .clockNumber {
152
+ width: 28px;
153
+ height: 28px;
154
+ @include type.type-style('body-compact-01');
155
+ }
156
+ }
157
+
158
+ @media (prefers-color-scheme: dark) {
159
+ .clockDropdown {
160
+ background: $ui-02;
161
+ border-color: $ui-04;
162
+ }
163
+
164
+ .clockNumbers {
165
+ border-color: $ui-04;
166
+ background: $ui-background;
167
+ }
168
+
169
+ .clockNumber {
170
+ background: $ui-02;
171
+ border-color: $ui-04;
172
+ color: $text-02;
173
+
174
+ &:hover {
175
+ background: $ui-03;
176
+ }
177
+ }
178
+ }