@kopexa/grc 0.0.4 → 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.
- package/dist/chunk-AHKTFAZC.mjs +110 -0
- package/dist/chunk-BAC3SZJH.mjs +1 -0
- package/dist/chunk-GC6CS627.mjs +248 -0
- package/dist/common/index.mjs +19 -19
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +344 -2
- package/dist/index.mjs +28 -19
- package/dist/risk/index.d.mts +4 -2
- package/dist/risk/index.d.ts +4 -2
- package/dist/risk/index.js +351 -0
- package/dist/risk/index.mjs +11 -0
- package/dist/risk/messages.d.mts +96 -0
- package/dist/risk/messages.d.ts +96 -0
- package/dist/risk/messages.js +133 -0
- package/dist/risk/messages.mjs +7 -0
- package/dist/risk/risk-treatment-card.d.mts +18 -0
- package/dist/risk/risk-treatment-card.d.ts +18 -0
- package/dist/risk/risk-treatment-card.js +369 -0
- package/dist/risk/risk-treatment-card.mjs +9 -0
- package/dist/risk/types.d.mts +15 -0
- package/dist/risk/types.d.ts +15 -0
- package/dist/risk/types.js +19 -0
- package/dist/risk/types.mjs +1 -0
- package/package.json +7 -7
- package/src/index.ts +3 -0
- package/src/risk/index.ts +10 -2
- package/src/risk/messages.ts +116 -0
- package/src/risk/risk-treatment-card.tsx +339 -0
- package/src/risk/types.ts +24 -0
|
@@ -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
|
+
}
|