@kopexa/grc 0.0.2

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 (80) hide show
  1. package/LICENSE +201 -0
  2. package/dist/asset/index.d.mts +2 -0
  3. package/dist/asset/index.d.ts +2 -0
  4. package/dist/asset/index.js +19 -0
  5. package/dist/asset/index.mjs +1 -0
  6. package/dist/chunk-7754RETD.mjs +57 -0
  7. package/dist/chunk-B47KDUYY.mjs +26 -0
  8. package/dist/chunk-BFZPRJQT.mjs +1 -0
  9. package/dist/chunk-GF3WJZVI.mjs +141 -0
  10. package/dist/chunk-GFABGXAO.mjs +1 -0
  11. package/dist/chunk-KNGEZZFI.mjs +157 -0
  12. package/dist/chunk-TICWEZUI.mjs +1 -0
  13. package/dist/chunk-TW3S4OE2.mjs +251 -0
  14. package/dist/common/compliance/compliance-badge.d.mts +33 -0
  15. package/dist/common/compliance/compliance-badge.d.ts +33 -0
  16. package/dist/common/compliance/compliance-badge.js +103 -0
  17. package/dist/common/compliance/compliance-badge.mjs +13 -0
  18. package/dist/common/compliance/index.d.mts +2 -0
  19. package/dist/common/compliance/index.d.ts +2 -0
  20. package/dist/common/compliance/index.js +104 -0
  21. package/dist/common/compliance/index.mjs +13 -0
  22. package/dist/common/compliance/messages.d.mts +20 -0
  23. package/dist/common/compliance/messages.d.ts +20 -0
  24. package/dist/common/compliance/messages.js +49 -0
  25. package/dist/common/compliance/messages.mjs +7 -0
  26. package/dist/common/impact/impact-card.d.mts +35 -0
  27. package/dist/common/impact/impact-card.d.ts +35 -0
  28. package/dist/common/impact/impact-card.js +551 -0
  29. package/dist/common/impact/impact-card.mjs +10 -0
  30. package/dist/common/impact/index.d.mts +5 -0
  31. package/dist/common/impact/index.d.ts +5 -0
  32. package/dist/common/impact/index.js +564 -0
  33. package/dist/common/impact/index.mjs +24 -0
  34. package/dist/common/impact/messages.d.mts +128 -0
  35. package/dist/common/impact/messages.d.ts +128 -0
  36. package/dist/common/impact/messages.js +164 -0
  37. package/dist/common/impact/messages.mjs +7 -0
  38. package/dist/common/impact/scales.d.mts +46 -0
  39. package/dist/common/impact/scales.d.ts +46 -0
  40. package/dist/common/impact/scales.js +319 -0
  41. package/dist/common/impact/scales.mjs +16 -0
  42. package/dist/common/index.d.mts +6 -0
  43. package/dist/common/index.d.ts +6 -0
  44. package/dist/common/index.js +640 -0
  45. package/dist/common/index.mjs +35 -0
  46. package/dist/control/index.d.mts +2 -0
  47. package/dist/control/index.d.ts +2 -0
  48. package/dist/control/index.js +19 -0
  49. package/dist/control/index.mjs +1 -0
  50. package/dist/incident/index.d.mts +2 -0
  51. package/dist/incident/index.d.ts +2 -0
  52. package/dist/incident/index.js +19 -0
  53. package/dist/incident/index.mjs +1 -0
  54. package/dist/index.d.mts +6 -0
  55. package/dist/index.d.ts +6 -0
  56. package/dist/index.js +640 -0
  57. package/dist/index.mjs +35 -0
  58. package/dist/risk/index.d.mts +2 -0
  59. package/dist/risk/index.d.ts +2 -0
  60. package/dist/risk/index.js +19 -0
  61. package/dist/risk/index.mjs +1 -0
  62. package/dist/vendor/index.d.mts +2 -0
  63. package/dist/vendor/index.d.ts +2 -0
  64. package/dist/vendor/index.js +19 -0
  65. package/dist/vendor/index.mjs +1 -0
  66. package/package.json +66 -0
  67. package/src/asset/index.ts +4 -0
  68. package/src/common/compliance/compliance-badge.tsx +110 -0
  69. package/src/common/compliance/index.ts +8 -0
  70. package/src/common/compliance/messages.ts +20 -0
  71. package/src/common/impact/impact-card.tsx +367 -0
  72. package/src/common/impact/index.ts +14 -0
  73. package/src/common/impact/messages.ts +141 -0
  74. package/src/common/impact/scales.ts +191 -0
  75. package/src/common/index.ts +4 -0
  76. package/src/control/index.ts +4 -0
  77. package/src/incident/index.ts +4 -0
  78. package/src/index.ts +20 -0
  79. package/src/risk/index.ts +4 -0
  80. package/src/vendor/index.ts +4 -0
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,2 @@
1
+
2
+ export { }
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __copyProps = (to, from, except, desc) => {
8
+ if (from && typeof from === "object" || typeof from === "function") {
9
+ for (let key of __getOwnPropNames(from))
10
+ if (!__hasOwnProp.call(to, key) && key !== except)
11
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
12
+ }
13
+ return to;
14
+ };
15
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
16
+
17
+ // src/vendor/index.ts
18
+ var vendor_exports = {};
19
+ module.exports = __toCommonJS(vendor_exports);
@@ -0,0 +1 @@
1
+ "use client";
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@kopexa/grc",
3
+ "version": "0.0.2",
4
+ "description": "GRC (Governance, Risk, Compliance) components for Kopexa",
5
+ "sideEffects": false,
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.mjs",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.js"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/kopexa-grc/sight.git",
24
+ "directory": "packages/grc"
25
+ },
26
+ "keywords": [
27
+ "kopexa",
28
+ "grc",
29
+ "governance",
30
+ "risk",
31
+ "compliance",
32
+ "react",
33
+ "components"
34
+ ],
35
+ "author": "Kopexa GmbH",
36
+ "license": "MIT",
37
+ "bugs": {
38
+ "url": "https://github.com/kopexa-grc/sight/issues"
39
+ },
40
+ "homepage": "https://github.com/kopexa-grc/sight#readme",
41
+ "peerDependencies": {
42
+ "react": ">=19.0.0-rc.0",
43
+ "react-dom": ">=19.0.0-rc.0",
44
+ "react-intl": "^7.1.14",
45
+ "@kopexa/theme": "17.6.0",
46
+ "@kopexa/sight": "17.1.0"
47
+ },
48
+ "dependencies": {
49
+ "@kopexa/react-utils": "17.0.12",
50
+ "@kopexa/shared-utils": "17.0.12",
51
+ "@kopexa/icons": "17.2.0",
52
+ "@kopexa/i18n": "17.1.0"
53
+ },
54
+ "clean-package": "../../clean-package.config.json",
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "scripts": {
59
+ "build": "tsup src --dts",
60
+ "build:fast": "tsup src",
61
+ "dev": "pnpm build:fast --watch",
62
+ "clean": "rimraf dist .turbo",
63
+ "typecheck": "tsc --noEmit",
64
+ "i18n:extract": "formatjs extract \"src/**/*.{ts,tsx}\" --ignore \"**/*.test.*\" \"**/*.stories.*\" --format simple --out-file ../core/i18n/tmp/$npm_package_name.en.json"
65
+ }
66
+ }
@@ -0,0 +1,4 @@
1
+ // Asset management components
2
+ // TODO: Add components as they are extracted from the demo
3
+
4
+ export {};
@@ -0,0 +1,110 @@
1
+ "use client";
2
+
3
+ import { useSafeIntl } from "@kopexa/i18n";
4
+ import { DORAIcon, NIS2Icon } from "@kopexa/icons";
5
+ import { Tooltip } from "@kopexa/sight";
6
+ import { complianceBadge } from "@kopexa/theme";
7
+ import { messages } from "./messages";
8
+
9
+ // ============================================
10
+ // Types
11
+ // ============================================
12
+
13
+ export interface DoraBadgeProps {
14
+ /** Custom tooltip content */
15
+ tooltip?: string;
16
+ /** Icon size */
17
+ iconSize?: number;
18
+ /** Additional class names */
19
+ className?: string;
20
+ }
21
+
22
+ export interface Nis2BadgeProps {
23
+ /** Custom tooltip content */
24
+ tooltip?: string;
25
+ /** Icon size */
26
+ iconSize?: number;
27
+ /** Additional class names */
28
+ className?: string;
29
+ }
30
+
31
+ export interface ComplianceBadgesProps {
32
+ /** Show DORA Critical badge */
33
+ doraCritical?: boolean;
34
+ /** Show NIS2 Significant badge */
35
+ nis2Significant?: boolean;
36
+ /** Icon size for badges */
37
+ iconSize?: number;
38
+ /** Additional class names for container */
39
+ className?: string;
40
+ }
41
+
42
+ // ============================================
43
+ // DORA Badge Component
44
+ // ============================================
45
+
46
+ export function DoraBadge({
47
+ tooltip,
48
+ iconSize = 12,
49
+ className,
50
+ }: DoraBadgeProps) {
51
+ const intl = useSafeIntl();
52
+ const styles = complianceBadge({ color: "dora" });
53
+
54
+ const tooltipContent = tooltip ?? intl.formatMessage(messages.dora_tooltip);
55
+ const label = intl.formatMessage(messages.dora_label);
56
+
57
+ return (
58
+ <Tooltip content={tooltipContent}>
59
+ <span className={styles.root({ className })}>
60
+ <DORAIcon size={iconSize} className={styles.icon()} />
61
+ <span className={styles.label()}>{label}</span>
62
+ </span>
63
+ </Tooltip>
64
+ );
65
+ }
66
+
67
+ // ============================================
68
+ // NIS2 Badge Component
69
+ // ============================================
70
+
71
+ export function Nis2Badge({
72
+ tooltip,
73
+ iconSize = 12,
74
+ className,
75
+ }: Nis2BadgeProps) {
76
+ const intl = useSafeIntl();
77
+ const styles = complianceBadge({ color: "nis2" });
78
+
79
+ const tooltipContent = tooltip ?? intl.formatMessage(messages.nis2_tooltip);
80
+ const label = intl.formatMessage(messages.nis2_label);
81
+
82
+ return (
83
+ <Tooltip content={tooltipContent}>
84
+ <span className={styles.root({ className })}>
85
+ <NIS2Icon size={iconSize} className={styles.icon()} />
86
+ <span className={styles.label()}>{label}</span>
87
+ </span>
88
+ </Tooltip>
89
+ );
90
+ }
91
+
92
+ // ============================================
93
+ // Combined Compliance Badges Component
94
+ // ============================================
95
+
96
+ export function ComplianceBadges({
97
+ doraCritical,
98
+ nis2Significant,
99
+ iconSize = 12,
100
+ className,
101
+ }: ComplianceBadgesProps) {
102
+ if (!doraCritical && !nis2Significant) return null;
103
+
104
+ return (
105
+ <div className={className ?? "flex items-center gap-1"}>
106
+ {doraCritical && <DoraBadge iconSize={iconSize} />}
107
+ {nis2Significant && <Nis2Badge iconSize={iconSize} />}
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ ComplianceBadges,
3
+ type ComplianceBadgesProps,
4
+ DoraBadge,
5
+ type DoraBadgeProps,
6
+ Nis2Badge,
7
+ type Nis2BadgeProps,
8
+ } from "./compliance-badge";
@@ -0,0 +1,20 @@
1
+ import { defineMessages } from "@kopexa/i18n";
2
+
3
+ export const messages = defineMessages({
4
+ dora_label: {
5
+ id: "grc.compliance.dora.label",
6
+ defaultMessage: "DORA",
7
+ },
8
+ dora_tooltip: {
9
+ id: "grc.compliance.dora.tooltip",
10
+ defaultMessage: "DORA Critical - Affects critical ICT functions",
11
+ },
12
+ nis2_label: {
13
+ id: "grc.compliance.nis2.label",
14
+ defaultMessage: "NIS2",
15
+ },
16
+ nis2_tooltip: {
17
+ id: "grc.compliance.nis2.tooltip",
18
+ defaultMessage: "NIS2 Significant - All-hazards risk",
19
+ },
20
+ });
@@ -0,0 +1,367 @@
1
+ "use client";
2
+
3
+ import { useSafeIntl } from "@kopexa/i18n";
4
+ import { EditIcon } from "@kopexa/icons";
5
+ import { Button, Card, Chip, Heading, Select, Textarea } from "@kopexa/sight";
6
+ import { impactCard } from "@kopexa/theme";
7
+ import { useState } from "react";
8
+ import { messages } from "./messages";
9
+ import {
10
+ getScale,
11
+ type ImpactLevel,
12
+ type ImpactScaleConfig,
13
+ type ImpactScalePreset,
14
+ impactLevels,
15
+ } from "./scales";
16
+
17
+ // ============================================
18
+ // Types (aligned with Go ImpactMixin schema)
19
+ // ============================================
20
+
21
+ export interface ImpactValue {
22
+ /** Rating 0-5. Vertraulichkeit (Confidentiality). */
23
+ impactConfidentiality: ImpactLevel;
24
+ /** Rating 0-5. Integrität (Integrity). */
25
+ impactIntegrity: ImpactLevel;
26
+ /** Rating 0-5. Verfügbarkeit (Availability) - DORA Critical! */
27
+ impactAvailability: ImpactLevel;
28
+ /** Rating 0-5. Authentizität (Authenticity/Accountability). */
29
+ impactAuthenticity?: ImpactLevel;
30
+ /** Human-readable explanation for the chosen impact scores. Focus on the highest score. */
31
+ impactJustification?: string;
32
+ }
33
+
34
+ // ============================================
35
+ // Impact Item Row Component
36
+ // ============================================
37
+
38
+ interface ImpactItemRowProps {
39
+ label: string;
40
+ shortLabel: string;
41
+ value: ImpactLevel;
42
+ isEditing: boolean;
43
+ scale: ImpactScaleConfig;
44
+ formatLabel: (level: ImpactLevel) => string;
45
+ onLevelChange: (level: ImpactLevel) => void;
46
+ }
47
+
48
+ function ImpactItemRow({
49
+ label,
50
+ shortLabel,
51
+ value,
52
+ isEditing,
53
+ scale,
54
+ formatLabel,
55
+ onLevelChange,
56
+ }: ImpactItemRowProps) {
57
+ const config = scale[value];
58
+ const isUnrated = value === 0;
59
+ const percentage = isUnrated ? 0 : (value / 5) * 100;
60
+ const styles = impactCard({ unrated: isUnrated });
61
+
62
+ return (
63
+ <div className={styles.row()}>
64
+ <div className={styles.rowContent()}>
65
+ {/* Icon */}
66
+ <span className={styles.rowIcon()}>{shortLabel}</span>
67
+
68
+ {/* Content */}
69
+ <div className={styles.rowBody()}>
70
+ <div className={styles.rowHeader()}>
71
+ <span className={styles.rowLabel()}>{label}</span>
72
+ {isEditing ? (
73
+ <Select
74
+ value={String(value)}
75
+ onValueChange={(val) =>
76
+ onLevelChange(Number(val) as ImpactLevel)
77
+ }
78
+ size="sm"
79
+ >
80
+ <Select.Trigger className="w-36">
81
+ <Select.Value />
82
+ </Select.Trigger>
83
+ <Select.Content>
84
+ {impactLevels.map((level) => (
85
+ <Select.Item key={level} value={String(level)}>
86
+ <span className="flex items-center gap-2">
87
+ <span className="text-xs text-muted-foreground w-3">
88
+ {level}
89
+ </span>
90
+ {formatLabel(level)}
91
+ </span>
92
+ </Select.Item>
93
+ ))}
94
+ </Select.Content>
95
+ </Select>
96
+ ) : (
97
+ <div className={styles.rowValue()}>
98
+ {!isUnrated && (
99
+ <span className={styles.rowValueNumber()}>{value}</span>
100
+ )}
101
+ <span
102
+ className={styles.rowValueBadge({
103
+ className: isUnrated
104
+ ? undefined
105
+ : `${config.color} ${config.bgColor}`,
106
+ })}
107
+ >
108
+ {formatLabel(value)}
109
+ </span>
110
+ </div>
111
+ )}
112
+ </div>
113
+
114
+ {/* Progress Bar */}
115
+ {!isUnrated && !isEditing && (
116
+ <div className={styles.progressContainer()}>
117
+ <div
118
+ className={styles.progressBar({ className: config.barColor })}
119
+ style={{ width: `${percentage}%` }}
120
+ />
121
+ </div>
122
+ )}
123
+ </div>
124
+ </div>
125
+ </div>
126
+ );
127
+ }
128
+
129
+ // ============================================
130
+ // Impact Card Component
131
+ // ============================================
132
+
133
+ export interface ImpactCardProps {
134
+ /** The impact data */
135
+ value?: ImpactValue;
136
+ /** Callback when the impact changes */
137
+ onChange?: (impact: ImpactValue) => void;
138
+ /** Show justification field */
139
+ showJustification?: boolean;
140
+ /** Show authenticity as 4th dimension (CIAA) */
141
+ showAuthenticity?: boolean;
142
+ /** Make the component read-only */
143
+ readOnly?: boolean;
144
+ /** Scale preset or custom scale config */
145
+ scale?: ImpactScalePreset | ImpactScaleConfig;
146
+ /** Custom title for the card */
147
+ title?: string;
148
+ }
149
+
150
+ const defaultImpact: ImpactValue = {
151
+ impactConfidentiality: 0,
152
+ impactIntegrity: 0,
153
+ impactAvailability: 0,
154
+ impactAuthenticity: 0,
155
+ };
156
+
157
+ export function ImpactCard({
158
+ value,
159
+ onChange,
160
+ showJustification = false,
161
+ showAuthenticity = false,
162
+ readOnly = false,
163
+ scale = "risk",
164
+ title,
165
+ }: ImpactCardProps) {
166
+ const intl = useSafeIntl();
167
+ const [isEditing, setIsEditing] = useState(false);
168
+ const [editValues, setEditValues] = useState<ImpactValue>(
169
+ value || defaultImpact,
170
+ );
171
+
172
+ const styles = impactCard({ editing: isEditing });
173
+
174
+ // Resolve scale config
175
+ const scaleConfig: ImpactScaleConfig =
176
+ typeof scale === "string" ? getScale(scale) : scale;
177
+
178
+ // i18n helper for scale labels
179
+ const formatLabel = (level: ImpactLevel): string => {
180
+ const config = scaleConfig[level];
181
+ return intl.formatMessage(config.message);
182
+ };
183
+
184
+ // i18n labels
185
+ const t = {
186
+ titleCia: intl.formatMessage(messages.title_cia),
187
+ titleCiaa: intl.formatMessage(messages.title_ciaa),
188
+ confidentiality: intl.formatMessage(messages.confidentiality),
189
+ integrity: intl.formatMessage(messages.integrity),
190
+ availability: intl.formatMessage(messages.availability),
191
+ authenticity: intl.formatMessage(messages.authenticity),
192
+ justification: intl.formatMessage(messages.justification),
193
+ justificationPlaceholder: intl.formatMessage(
194
+ messages.justification_placeholder,
195
+ ),
196
+ noJustification: intl.formatMessage(messages.no_justification),
197
+ edit: intl.formatMessage(messages.edit),
198
+ cancel: intl.formatMessage(messages.cancel),
199
+ save: intl.formatMessage(messages.save),
200
+ };
201
+
202
+ // Derive default title based on authenticity
203
+ const defaultTitle = showAuthenticity ? t.titleCiaa : t.titleCia;
204
+ const cardTitle = title ?? defaultTitle;
205
+
206
+ const handleSave = () => {
207
+ onChange?.(editValues);
208
+ setIsEditing(false);
209
+ };
210
+
211
+ const handleCancel = () => {
212
+ setEditValues(value || defaultImpact);
213
+ setIsEditing(false);
214
+ };
215
+
216
+ const handleStartEdit = () => {
217
+ setEditValues(value || defaultImpact);
218
+ setIsEditing(true);
219
+ };
220
+
221
+ const currentImpact = isEditing ? editValues : value || defaultImpact;
222
+
223
+ const handleLevelChange =
224
+ (
225
+ key:
226
+ | "impactConfidentiality"
227
+ | "impactIntegrity"
228
+ | "impactAvailability"
229
+ | "impactAuthenticity",
230
+ ) =>
231
+ (level: ImpactLevel) => {
232
+ setEditValues((prev) => ({
233
+ ...prev,
234
+ [key]: level,
235
+ }));
236
+ };
237
+
238
+ const handleJustificationChange = (justification: string) => {
239
+ setEditValues((prev) => ({
240
+ ...prev,
241
+ impactJustification: justification || undefined,
242
+ }));
243
+ };
244
+
245
+ // Calculate highest impact for justification hint
246
+ const highestImpact = Math.max(
247
+ currentImpact.impactConfidentiality,
248
+ currentImpact.impactIntegrity,
249
+ currentImpact.impactAvailability,
250
+ currentImpact.impactAuthenticity ?? 0,
251
+ ) as ImpactLevel;
252
+
253
+ const highestLabel = formatLabel(highestImpact);
254
+ const justificationHint = intl.formatMessage(messages.justification_hint, {
255
+ level: highestLabel,
256
+ });
257
+
258
+ return (
259
+ <Card.Root className={styles.root()}>
260
+ <Card.Header className="flex flex-row items-center justify-between">
261
+ <div className="flex items-center gap-2">
262
+ <Heading level="h4" className="text-sm font-medium">
263
+ {cardTitle}
264
+ </Heading>
265
+ {isEditing && (
266
+ <Chip size="sm" color="primary">
267
+ {t.edit}
268
+ </Chip>
269
+ )}
270
+ </div>
271
+ {!readOnly &&
272
+ (!isEditing ? (
273
+ <button
274
+ type="button"
275
+ onClick={handleStartEdit}
276
+ className={styles.editButton()}
277
+ aria-label={t.edit}
278
+ >
279
+ <EditIcon className="size-4" />
280
+ </button>
281
+ ) : (
282
+ <div className="flex items-center gap-2">
283
+ <Button variant="ghost" size="sm" onClick={handleCancel}>
284
+ {t.cancel}
285
+ </Button>
286
+ <Button size="sm" onClick={handleSave}>
287
+ {t.save}
288
+ </Button>
289
+ </div>
290
+ ))}
291
+ </Card.Header>
292
+ <Card.Body className="space-y-3">
293
+ <ImpactItemRow
294
+ label={t.confidentiality}
295
+ shortLabel="C"
296
+ value={currentImpact.impactConfidentiality}
297
+ isEditing={isEditing}
298
+ scale={scaleConfig}
299
+ formatLabel={formatLabel}
300
+ onLevelChange={handleLevelChange("impactConfidentiality")}
301
+ />
302
+ <ImpactItemRow
303
+ label={t.integrity}
304
+ shortLabel="I"
305
+ value={currentImpact.impactIntegrity}
306
+ isEditing={isEditing}
307
+ scale={scaleConfig}
308
+ formatLabel={formatLabel}
309
+ onLevelChange={handleLevelChange("impactIntegrity")}
310
+ />
311
+ <ImpactItemRow
312
+ label={t.availability}
313
+ shortLabel="A"
314
+ value={currentImpact.impactAvailability}
315
+ isEditing={isEditing}
316
+ scale={scaleConfig}
317
+ formatLabel={formatLabel}
318
+ onLevelChange={handleLevelChange("impactAvailability")}
319
+ />
320
+ {showAuthenticity && (
321
+ <ImpactItemRow
322
+ label={t.authenticity}
323
+ shortLabel="Au"
324
+ value={currentImpact.impactAuthenticity ?? 0}
325
+ isEditing={isEditing}
326
+ scale={scaleConfig}
327
+ formatLabel={formatLabel}
328
+ onLevelChange={handleLevelChange("impactAuthenticity")}
329
+ />
330
+ )}
331
+
332
+ {/* Justification - single field for all dimensions */}
333
+ {showJustification && (
334
+ <div className={styles.justificationSection()}>
335
+ <label
336
+ htmlFor="impact-justification"
337
+ className={styles.justificationLabel()}
338
+ >
339
+ {t.justification}
340
+ {highestImpact > 0 && (
341
+ <span className={styles.justificationHint()}>
342
+ {justificationHint}
343
+ </span>
344
+ )}
345
+ </label>
346
+ {isEditing ? (
347
+ <Textarea
348
+ id="impact-justification"
349
+ value={currentImpact.impactJustification || ""}
350
+ onChange={(e) => handleJustificationChange(e.target.value)}
351
+ placeholder={t.justificationPlaceholder}
352
+ rows={3}
353
+ className="text-sm"
354
+ />
355
+ ) : currentImpact.impactJustification ? (
356
+ <p className={styles.justificationText()}>
357
+ {currentImpact.impactJustification}
358
+ </p>
359
+ ) : (
360
+ <p className={styles.justificationEmpty()}>{t.noJustification}</p>
361
+ )}
362
+ </div>
363
+ )}
364
+ </Card.Body>
365
+ </Card.Root>
366
+ );
367
+ }
@@ -0,0 +1,14 @@
1
+ export type { ImpactCardProps, ImpactValue } from "./impact-card";
2
+ export { ImpactCard } from "./impact-card";
3
+ export { messages as impactMessages } from "./messages";
4
+ export {
5
+ assetScale,
6
+ getScale,
7
+ type ImpactLevel,
8
+ type ImpactLevelConfig,
9
+ type ImpactScaleConfig,
10
+ type ImpactScalePreset,
11
+ impactLevels,
12
+ processScale,
13
+ riskScale,
14
+ } from "./scales";