@kopexa/grc 0.0.5 → 0.0.6

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.
@@ -0,0 +1,339 @@
1
+ "use client";
2
+
3
+ import { useSafeIntl } from "@kopexa/i18n";
4
+ import { CheckCirleIcon, EditIcon } from "@kopexa/icons";
5
+ import {
6
+ Button,
7
+ Card,
8
+ Chip,
9
+ Heading,
10
+ Separator,
11
+ Textarea,
12
+ } from "@kopexa/sight";
13
+ import { useState } from "react";
14
+ import { messages } from "./messages";
15
+ import type { RiskTreatment, RiskTreatmentValue } from "./types";
16
+
17
+ // ============================================
18
+ // Types
19
+ // ============================================
20
+
21
+ export interface RiskTreatmentCardProps {
22
+ /** The current treatment value */
23
+ value?: RiskTreatmentValue;
24
+ /** Callback when the treatment changes */
25
+ onChange?: (value: RiskTreatmentValue) => void;
26
+ /** Make the component read-only */
27
+ readOnly?: boolean;
28
+ /** Custom title for the card */
29
+ title?: string;
30
+ /** Recommended treatment based on risk level */
31
+ recommended?: RiskTreatment;
32
+ }
33
+
34
+ // ============================================
35
+ // Treatment Option Component
36
+ // ============================================
37
+
38
+ interface TreatmentOptionProps {
39
+ id: RiskTreatment;
40
+ label: string;
41
+ description: string;
42
+ hint?: string;
43
+ isSelected: boolean;
44
+ isRecommended: boolean;
45
+ recommendedLabel: string;
46
+ onSelect: (id: RiskTreatment) => void;
47
+ }
48
+
49
+ function TreatmentOption({
50
+ id,
51
+ label,
52
+ description,
53
+ hint,
54
+ isSelected,
55
+ isRecommended,
56
+ recommendedLabel,
57
+ onSelect,
58
+ }: TreatmentOptionProps) {
59
+ return (
60
+ <button
61
+ type="button"
62
+ onClick={() => onSelect(id)}
63
+ className={`
64
+ w-full text-left p-4 rounded-lg border-2 transition-all
65
+ ${
66
+ isSelected
67
+ ? "border-primary bg-primary/5"
68
+ : "border-border hover:border-primary/50 hover:bg-muted/50"
69
+ }
70
+ `}
71
+ >
72
+ <div className="flex items-start gap-3">
73
+ {/* Radio indicator */}
74
+ <div
75
+ className={`
76
+ mt-0.5 size-5 rounded-full border-2 flex items-center justify-center flex-shrink-0
77
+ ${isSelected ? "border-primary bg-primary" : "border-muted-foreground"}
78
+ `}
79
+ >
80
+ {isSelected && (
81
+ <div className="size-2 rounded-full bg-primary-foreground" />
82
+ )}
83
+ </div>
84
+
85
+ {/* Content */}
86
+ <div className="flex-1 min-w-0">
87
+ <div className="flex items-center gap-2 flex-wrap">
88
+ <span className="font-medium">{label}</span>
89
+ {isRecommended && (
90
+ <Chip size="sm" color="success" variant="flat">
91
+ {recommendedLabel}
92
+ </Chip>
93
+ )}
94
+ </div>
95
+ <p className="text-sm text-muted-foreground mt-1">{description}</p>
96
+ {hint && (
97
+ <p className="text-xs text-muted-foreground mt-2 flex items-center gap-1">
98
+ <span className="text-primary">→</span>
99
+ {hint}
100
+ </p>
101
+ )}
102
+ </div>
103
+ </div>
104
+ </button>
105
+ );
106
+ }
107
+
108
+ // ============================================
109
+ // Main Component
110
+ // ============================================
111
+
112
+ const defaultValue: RiskTreatmentValue = {
113
+ treatment: "not_defined",
114
+ rationale: undefined,
115
+ };
116
+
117
+ export function RiskTreatmentCard({
118
+ value,
119
+ onChange,
120
+ readOnly = false,
121
+ title,
122
+ recommended,
123
+ }: RiskTreatmentCardProps) {
124
+ const intl = useSafeIntl();
125
+ const [isEditing, setIsEditing] = useState(false);
126
+ const [editValues, setEditValues] = useState<RiskTreatmentValue>(
127
+ value || defaultValue,
128
+ );
129
+
130
+ // i18n labels
131
+ const t = {
132
+ title: intl.formatMessage(messages.title),
133
+ strategyLabel: intl.formatMessage(messages.strategy_label),
134
+ strategyPrompt: intl.formatMessage(messages.strategy_prompt),
135
+ rationaleLabel: intl.formatMessage(messages.rationale_label),
136
+ rationalePlaceholder: intl.formatMessage(messages.rationale_placeholder),
137
+ noRationale: intl.formatMessage(messages.no_rationale),
138
+ edit: intl.formatMessage(messages.edit),
139
+ cancel: intl.formatMessage(messages.cancel),
140
+ save: intl.formatMessage(messages.save),
141
+ recommended: intl.formatMessage(messages.recommended),
142
+ // Treatment labels
143
+ acceptLabel: intl.formatMessage(messages.accept_label),
144
+ mitigateLabel: intl.formatMessage(messages.mitigate_label),
145
+ transferLabel: intl.formatMessage(messages.transfer_label),
146
+ avoidLabel: intl.formatMessage(messages.avoid_label),
147
+ notDefinedLabel: intl.formatMessage(messages.not_defined_label),
148
+ // Treatment descriptions
149
+ acceptDescription: intl.formatMessage(messages.accept_description),
150
+ mitigateDescription: intl.formatMessage(messages.mitigate_description),
151
+ transferDescription: intl.formatMessage(messages.transfer_description),
152
+ avoidDescription: intl.formatMessage(messages.avoid_description),
153
+ // Treatment hints
154
+ acceptHint: intl.formatMessage(messages.accept_hint),
155
+ mitigateHint: intl.formatMessage(messages.mitigate_hint),
156
+ transferHint: intl.formatMessage(messages.transfer_hint),
157
+ avoidHint: intl.formatMessage(messages.avoid_hint),
158
+ };
159
+
160
+ const treatmentLabels: Record<RiskTreatment, string> = {
161
+ accept: t.acceptLabel,
162
+ mitigate: t.mitigateLabel,
163
+ transfer: t.transferLabel,
164
+ avoid: t.avoidLabel,
165
+ not_defined: t.notDefinedLabel,
166
+ };
167
+
168
+ const treatmentOptions: Array<{
169
+ id: RiskTreatment;
170
+ label: string;
171
+ description: string;
172
+ hint: string;
173
+ }> = [
174
+ {
175
+ id: "accept",
176
+ label: t.acceptLabel,
177
+ description: t.acceptDescription,
178
+ hint: t.acceptHint,
179
+ },
180
+ {
181
+ id: "mitigate",
182
+ label: t.mitigateLabel,
183
+ description: t.mitigateDescription,
184
+ hint: t.mitigateHint,
185
+ },
186
+ {
187
+ id: "transfer",
188
+ label: t.transferLabel,
189
+ description: t.transferDescription,
190
+ hint: t.transferHint,
191
+ },
192
+ {
193
+ id: "avoid",
194
+ label: t.avoidLabel,
195
+ description: t.avoidDescription,
196
+ hint: t.avoidHint,
197
+ },
198
+ ];
199
+
200
+ const cardTitle = title ?? t.title;
201
+ const currentValue = isEditing ? editValues : value || defaultValue;
202
+ const isNotDefined = currentValue.treatment === "not_defined";
203
+
204
+ const handleSave = () => {
205
+ onChange?.(editValues);
206
+ setIsEditing(false);
207
+ };
208
+
209
+ const handleCancel = () => {
210
+ setEditValues(value || defaultValue);
211
+ setIsEditing(false);
212
+ };
213
+
214
+ const handleStartEdit = () => {
215
+ setEditValues(value || defaultValue);
216
+ setIsEditing(true);
217
+ };
218
+
219
+ const handleTreatmentSelect = (treatment: RiskTreatment) => {
220
+ setEditValues((prev) => ({ ...prev, treatment }));
221
+ };
222
+
223
+ const handleRationaleChange = (rationale: string) => {
224
+ setEditValues((prev) => ({ ...prev, rationale: rationale || undefined }));
225
+ };
226
+
227
+ return (
228
+ <Card.Root variant="accent">
229
+ <Card.Header className="flex flex-row items-center justify-between">
230
+ <div className="flex items-center gap-2">
231
+ <Heading level="h3" className="text-base">
232
+ {cardTitle}
233
+ </Heading>
234
+ {isEditing && (
235
+ <Chip size="sm" color="primary">
236
+ {t.edit}
237
+ </Chip>
238
+ )}
239
+ </div>
240
+ {!readOnly &&
241
+ (!isEditing ? (
242
+ <Button
243
+ variant="ghost"
244
+ size="sm"
245
+ isIconOnly
246
+ onClick={handleStartEdit}
247
+ aria-label={t.edit}
248
+ >
249
+ <EditIcon className="size-4" />
250
+ </Button>
251
+ ) : (
252
+ <div className="flex items-center gap-2">
253
+ <Button variant="ghost" size="sm" onClick={handleCancel}>
254
+ {t.cancel}
255
+ </Button>
256
+ <Button size="sm" onClick={handleSave}>
257
+ {t.save}
258
+ </Button>
259
+ </div>
260
+ ))}
261
+ </Card.Header>
262
+
263
+ <Card.Body className="space-y-4">
264
+ {isEditing ? (
265
+ <>
266
+ {/* Edit Mode: Treatment Selection */}
267
+ <div className="space-y-3">
268
+ <p className="text-sm text-muted-foreground">
269
+ {t.strategyPrompt}
270
+ </p>
271
+ <div className="grid gap-3">
272
+ {treatmentOptions.map((option) => (
273
+ <TreatmentOption
274
+ key={option.id}
275
+ id={option.id}
276
+ label={option.label}
277
+ description={option.description}
278
+ hint={option.hint}
279
+ isSelected={editValues.treatment === option.id}
280
+ isRecommended={recommended === option.id}
281
+ recommendedLabel={t.recommended}
282
+ onSelect={handleTreatmentSelect}
283
+ />
284
+ ))}
285
+ </div>
286
+ </div>
287
+
288
+ <Separator />
289
+
290
+ {/* Edit Mode: Rationale */}
291
+ <div className="space-y-2">
292
+ <label
293
+ htmlFor="treatment-rationale"
294
+ className="text-sm font-medium"
295
+ >
296
+ {t.rationaleLabel}
297
+ </label>
298
+ <Textarea
299
+ id="treatment-rationale"
300
+ value={editValues.rationale || ""}
301
+ onChange={(e) => handleRationaleChange(e.target.value)}
302
+ placeholder={t.rationalePlaceholder}
303
+ rows={3}
304
+ className="text-sm"
305
+ />
306
+ </div>
307
+ </>
308
+ ) : (
309
+ <>
310
+ {/* View Mode: Selected Strategy */}
311
+ <div>
312
+ <p className="text-sm font-medium mb-1">{t.strategyLabel}</p>
313
+ <div className="flex items-center gap-2">
314
+ {!isNotDefined && (
315
+ <CheckCirleIcon className="size-4 text-success" />
316
+ )}
317
+ <p
318
+ className={`text-sm ${isNotDefined ? "text-muted-foreground" : ""}`}
319
+ >
320
+ {treatmentLabels[currentValue.treatment]}
321
+ </p>
322
+ </div>
323
+ </div>
324
+
325
+ <Separator />
326
+
327
+ {/* View Mode: Rationale */}
328
+ <div>
329
+ <p className="text-sm font-medium mb-1">{t.rationaleLabel}</p>
330
+ <p className="text-sm text-muted-foreground">
331
+ {currentValue.rationale || t.noRationale}
332
+ </p>
333
+ </div>
334
+ </>
335
+ )}
336
+ </Card.Body>
337
+ </Card.Root>
338
+ );
339
+ }
@@ -0,0 +1,24 @@
1
+ // ============================================
2
+ // Risk Treatment Types
3
+ // ============================================
4
+
5
+ export type RiskTreatment =
6
+ | "accept"
7
+ | "mitigate"
8
+ | "transfer"
9
+ | "avoid"
10
+ | "not_defined";
11
+
12
+ export interface RiskTreatmentValue {
13
+ /** The selected treatment strategy */
14
+ treatment: RiskTreatment;
15
+ /** Human-readable explanation for the chosen strategy */
16
+ rationale?: string;
17
+ }
18
+
19
+ export interface TreatmentOption {
20
+ id: RiskTreatment;
21
+ label: string;
22
+ description: string;
23
+ hint?: string;
24
+ }