@decentrys/ui-sdk 0.1.0
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/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/browser/decentrys-ui.js +1733 -0
- package/dist/browser/decentrys-ui.mjs +1681 -0
- package/dist/browser/react-dom-global.d.ts +6 -0
- package/dist/browser/react-dom-global.d.ts.map +1 -0
- package/dist/browser/react-dom-global.js +18 -0
- package/dist/browser/react-dom-global.js.map +1 -0
- package/dist/browser/react-global.d.ts +25 -0
- package/dist/browser/react-global.d.ts.map +1 -0
- package/dist/browser/react-global.js +39 -0
- package/dist/browser/react-global.js.map +1 -0
- package/dist/browser/react-jsx-runtime.d.ts +17 -0
- package/dist/browser/react-jsx-runtime.d.ts.map +1 -0
- package/dist/browser/react-jsx-runtime.js +27 -0
- package/dist/browser/react-jsx-runtime.js.map +1 -0
- package/dist/components/AddressRiskBadge.d.ts +39 -0
- package/dist/components/AddressRiskBadge.d.ts.map +1 -0
- package/dist/components/AddressRiskBadge.js +26 -0
- package/dist/components/AddressRiskBadge.js.map +1 -0
- package/dist/components/ApprovalWarning.d.ts +29 -0
- package/dist/components/ApprovalWarning.d.ts.map +1 -0
- package/dist/components/ApprovalWarning.js +18 -0
- package/dist/components/ApprovalWarning.js.map +1 -0
- package/dist/components/ContractFacts.d.ts +27 -0
- package/dist/components/ContractFacts.d.ts.map +1 -0
- package/dist/components/ContractFacts.js +15 -0
- package/dist/components/ContractFacts.js.map +1 -0
- package/dist/components/RiskDetailsModal.d.ts +35 -0
- package/dist/components/RiskDetailsModal.d.ts.map +1 -0
- package/dist/components/RiskDetailsModal.js +74 -0
- package/dist/components/RiskDetailsModal.js.map +1 -0
- package/dist/components/ThreatSignalList.d.ts +28 -0
- package/dist/components/ThreatSignalList.d.ts.map +1 -0
- package/dist/components/ThreatSignalList.js +28 -0
- package/dist/components/ThreatSignalList.js.map +1 -0
- package/dist/components/TokenSecurityPanel.d.ts +28 -0
- package/dist/components/TokenSecurityPanel.d.ts.map +1 -0
- package/dist/components/TokenSecurityPanel.js +26 -0
- package/dist/components/TokenSecurityPanel.js.map +1 -0
- package/dist/components/TransactionRiskBanner.d.ts +36 -0
- package/dist/components/TransactionRiskBanner.d.ts.map +1 -0
- package/dist/components/TransactionRiskBanner.js +20 -0
- package/dist/components/TransactionRiskBanner.js.map +1 -0
- package/dist/context.d.ts +51 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +47 -0
- package/dist/context.js.map +1 -0
- package/dist/copy.d.ts +72 -0
- package/dist/copy.d.ts.map +1 -0
- package/dist/copy.js +77 -0
- package/dist/copy.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +107 -0
- package/dist/index.js.map +1 -0
- package/dist/presentation.d.ts +316 -0
- package/dist/presentation.d.ts.map +1 -0
- package/dist/presentation.js +533 -0
- package/dist/presentation.js.map +1 -0
- package/dist/primitives.d.ts +139 -0
- package/dist/primitives.d.ts.map +1 -0
- package/dist/primitives.js +150 -0
- package/dist/primitives.js.map +1 -0
- package/dist/styles.d.ts +15 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +285 -0
- package/dist/styles.js.map +1 -0
- package/dist/theme.d.ts +52 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +90 -0
- package/dist/theme.js.map +1 -0
- package/package.json +70 -0
- package/src/browser/react-dom-global.ts +20 -0
- package/src/browser/react-global.ts +42 -0
- package/src/browser/react-jsx-runtime.ts +23 -0
- package/src/components/AddressRiskBadge.tsx +94 -0
- package/src/components/ApprovalWarning.tsx +144 -0
- package/src/components/ContractFacts.tsx +119 -0
- package/src/components/RiskDetailsModal.tsx +229 -0
- package/src/components/ThreatSignalList.tsx +76 -0
- package/src/components/TokenSecurityPanel.tsx +168 -0
- package/src/components/TransactionRiskBanner.tsx +123 -0
- package/src/context.tsx +89 -0
- package/src/copy.test.ts +214 -0
- package/src/copy.ts +151 -0
- package/src/index.ts +84 -0
- package/src/presentation.test.ts +521 -0
- package/src/presentation.ts +846 -0
- package/src/primitives.tsx +384 -0
- package/src/styles.ts +281 -0
- package/src/theme.test.ts +105 -0
- package/src/theme.ts +117 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared rendering pieces.
|
|
3
|
+
*
|
|
4
|
+
* Every visual decision in here comes out of `presentation.ts`; nothing in
|
|
5
|
+
* this file decides what a thing means, only how the description it was given
|
|
6
|
+
* turns into elements. That is why the tests target the presentation module
|
|
7
|
+
* rather than markup — the mapping is where the rule can be broken, and it is
|
|
8
|
+
* not in this file.
|
|
9
|
+
*
|
|
10
|
+
* Two invariants hold throughout:
|
|
11
|
+
*
|
|
12
|
+
* - Every row carries its kind as a **word** (`Fact`, `Capability`,
|
|
13
|
+
* `Threat signal`, `Unknown`) and as a **rule style**, before colour is
|
|
14
|
+
* considered at all. A user who cannot distinguish amber from grey can
|
|
15
|
+
* still tell an observation from an accusation.
|
|
16
|
+
* - No control here is ever rendered disabled. These components inform; the
|
|
17
|
+
* integrator's own UI decides what a user may do.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
21
|
+
import type {
|
|
22
|
+
Evidence, HistoryStatus, ObservedFact, RiskLevel, TechnicalCapability,
|
|
23
|
+
ThreatSignal, UnknownField,
|
|
24
|
+
} from '@decentrys/protect';
|
|
25
|
+
import { DecentrysUiStyles } from './context';
|
|
26
|
+
import type { DecentrysTheme } from './theme';
|
|
27
|
+
import type { UiCopy } from './copy';
|
|
28
|
+
import {
|
|
29
|
+
availabilityPresentation, capabilityPresentation, evidencePresentation, factPresentation,
|
|
30
|
+
historyPresentation, kindPresentation, levelPresentation, signalPresentation,
|
|
31
|
+
unknownPresentation, type LevelPresentation,
|
|
32
|
+
} from './presentation';
|
|
33
|
+
import type { Assessment } from '@decentrys/protect';
|
|
34
|
+
|
|
35
|
+
/** Props every component in this package accepts. */
|
|
36
|
+
export interface CommonProps {
|
|
37
|
+
/** Applied alongside `dcy` on the component's own root. */
|
|
38
|
+
className?: string;
|
|
39
|
+
style?: CSSProperties;
|
|
40
|
+
/** Token overrides, applied inline so they beat the default stylesheet. */
|
|
41
|
+
theme?: DecentrysTheme;
|
|
42
|
+
/** Copy overrides, including translation. */
|
|
43
|
+
copy?: Partial<UiCopy>;
|
|
44
|
+
/** Risk-level label overrides, for translation or house style. */
|
|
45
|
+
levelLabels?: Partial<Record<RiskLevel, string>>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function cx(...parts: Array<string | false | undefined | null>): string {
|
|
49
|
+
return parts.filter(Boolean).join(' ');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function StyleGate({ emit }: { emit: boolean }): ReactNode {
|
|
53
|
+
return emit ? <DecentrysUiStyles /> : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Badges and chips
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
export function LevelBadge({ level }: { level: LevelPresentation }): ReactNode {
|
|
61
|
+
return (
|
|
62
|
+
<span className="dcy-badge" data-dcy-tone={level.tone} data-dcy-level={level.level}>
|
|
63
|
+
{/* Decorative only — the label beside it carries the meaning, so a screen
|
|
64
|
+
reader is not made to announce a row of exclamation marks. */}
|
|
65
|
+
<span className="dcy-glyph" aria-hidden="true">{level.glyph}</span>
|
|
66
|
+
<span>{level.label}</span>
|
|
67
|
+
</span>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function Chip({ children, title }: { children: ReactNode; title?: string }): ReactNode {
|
|
72
|
+
return <span className="dcy-chip" title={title}>{children}</span>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function Mono({ children }: { children: ReactNode }): ReactNode {
|
|
76
|
+
return <span className="dcy-mono">{children}</span>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Visible to assistive technology, not to the eye.
|
|
81
|
+
*
|
|
82
|
+
* Used to carry a level's full meaning alongside a short badge label, so the
|
|
83
|
+
* compact form is short for sighted users without being *less* informative for
|
|
84
|
+
* anyone using a screen reader.
|
|
85
|
+
*/
|
|
86
|
+
export function ScreenReaderText({ children }: { children: ReactNode }): ReactNode {
|
|
87
|
+
return <span className="dcy-sr">{children}</span>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Sections
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
export function Section({
|
|
95
|
+
title, note, children,
|
|
96
|
+
}: { title: string; note?: string; children: ReactNode }): ReactNode {
|
|
97
|
+
return (
|
|
98
|
+
<section className="dcy-section">
|
|
99
|
+
<h4 className="dcy-section-title">{title}</h4>
|
|
100
|
+
{note ? <p className="dcy-note">{note}</p> : null}
|
|
101
|
+
{children}
|
|
102
|
+
</section>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* An explicit statement of emptiness.
|
|
108
|
+
*
|
|
109
|
+
* A blank panel is indistinguishable from a clean result, and the difference
|
|
110
|
+
* between "we looked and found nothing" and "there is nothing to find" is the
|
|
111
|
+
* difference this product is built on. So emptiness is written down.
|
|
112
|
+
*/
|
|
113
|
+
export function EmptyNote({ children }: { children: ReactNode }): ReactNode {
|
|
114
|
+
return <p className="dcy-note" data-dcy-empty="">{children}</p>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Rows
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
export function FactRow({ fact, copy }: { fact: ObservedFact; copy: UiCopy }): ReactNode {
|
|
122
|
+
const kind = kindPresentation('fact', copy);
|
|
123
|
+
const p = factPresentation(fact);
|
|
124
|
+
return (
|
|
125
|
+
<li
|
|
126
|
+
className="dcy-row"
|
|
127
|
+
data-dcy-kind="fact"
|
|
128
|
+
data-dcy-tone={p.tone}
|
|
129
|
+
data-dcy-rule={kind.rule}
|
|
130
|
+
>
|
|
131
|
+
<span className="dcy-glyph" aria-hidden="true">{kind.glyph}</span>
|
|
132
|
+
<div className="dcy-row-body">
|
|
133
|
+
<p className="dcy-row-statement">
|
|
134
|
+
<Chip>{kind.label}</Chip> {p.statement}
|
|
135
|
+
</p>
|
|
136
|
+
<p className="dcy-row-meta">
|
|
137
|
+
<span>{copy.sourceLabel}: {p.source}</span>
|
|
138
|
+
<span>{copy.observedLabel}: {p.observedAt}</span>
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
</li>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function CapabilityRow({
|
|
146
|
+
capability, copy,
|
|
147
|
+
}: { capability: TechnicalCapability; copy: UiCopy }): ReactNode {
|
|
148
|
+
const kind = kindPresentation('capability', copy);
|
|
149
|
+
const p = capabilityPresentation(capability);
|
|
150
|
+
return (
|
|
151
|
+
<li
|
|
152
|
+
className="dcy-row"
|
|
153
|
+
data-dcy-kind="capability"
|
|
154
|
+
data-dcy-tone={p.tone}
|
|
155
|
+
data-dcy-rule={kind.rule}
|
|
156
|
+
data-dcy-severity={p.severity}
|
|
157
|
+
>
|
|
158
|
+
<span className="dcy-glyph" aria-hidden="true">{kind.glyph}</span>
|
|
159
|
+
<div className="dcy-row-body">
|
|
160
|
+
<p className={cx('dcy-row-statement', p.emphasis === 'strong' && 'dcy-row-strong')}>
|
|
161
|
+
<Chip>{kind.label}</Chip> <Chip>{p.severityLabel}</Chip> {p.statement}
|
|
162
|
+
</p>
|
|
163
|
+
{p.grantedBy ? (
|
|
164
|
+
<p className="dcy-row-meta">
|
|
165
|
+
<span>{copy.grantedByLabel}: <Mono>{p.grantedBy}</Mono></span>
|
|
166
|
+
</p>
|
|
167
|
+
) : null}
|
|
168
|
+
</div>
|
|
169
|
+
</li>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function UnknownRow({
|
|
174
|
+
unknown, copy,
|
|
175
|
+
}: { unknown: UnknownField; copy: UiCopy }): ReactNode {
|
|
176
|
+
const kind = kindPresentation('unknown', copy);
|
|
177
|
+
const p = unknownPresentation(unknown);
|
|
178
|
+
return (
|
|
179
|
+
<li
|
|
180
|
+
className="dcy-row"
|
|
181
|
+
data-dcy-kind="unknown"
|
|
182
|
+
data-dcy-tone={p.tone}
|
|
183
|
+
data-dcy-rule={kind.rule}
|
|
184
|
+
>
|
|
185
|
+
<span className="dcy-glyph" aria-hidden="true">{kind.glyph}</span>
|
|
186
|
+
<div className="dcy-row-body">
|
|
187
|
+
<p className="dcy-row-statement">
|
|
188
|
+
<Chip>{kind.label}</Chip> {p.statement}
|
|
189
|
+
</p>
|
|
190
|
+
<p className="dcy-row-meta">
|
|
191
|
+
<span>{p.field}</span>
|
|
192
|
+
<span>{p.reasonLabel}</span>
|
|
193
|
+
</p>
|
|
194
|
+
</div>
|
|
195
|
+
</li>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Threat signals
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
export function EvidenceList({
|
|
204
|
+
evidence, copy,
|
|
205
|
+
}: { evidence: Evidence[]; copy: UiCopy }): ReactNode {
|
|
206
|
+
if (evidence.length === 0) return null;
|
|
207
|
+
return (
|
|
208
|
+
<div className="dcy-section">
|
|
209
|
+
<h5 className="dcy-section-title">{copy.evidenceHeading}</h5>
|
|
210
|
+
{evidence.map((item) => {
|
|
211
|
+
const e = evidencePresentation(item, copy);
|
|
212
|
+
return (
|
|
213
|
+
<dl className="dcy-kv" key={e.id}>
|
|
214
|
+
<dt>{copy.sourceLabel}</dt>
|
|
215
|
+
<dd>{e.source}</dd>
|
|
216
|
+
<dt>Type</dt>
|
|
217
|
+
<dd>{e.type}</dd>
|
|
218
|
+
<dt>{copy.observedLabel}</dt>
|
|
219
|
+
<dd>{e.observedAt}</dd>
|
|
220
|
+
<dt>{copy.confidenceLabel}</dt>
|
|
221
|
+
<dd>{e.confidenceLabel}</dd>
|
|
222
|
+
{e.reference ? (
|
|
223
|
+
<>
|
|
224
|
+
<dt>Reference</dt>
|
|
225
|
+
<dd><Mono>{e.reference}</Mono></dd>
|
|
226
|
+
</>
|
|
227
|
+
) : null}
|
|
228
|
+
<dt>Attribution</dt>
|
|
229
|
+
<dd>{e.attribution}</dd>
|
|
230
|
+
<dt>Evidence id</dt>
|
|
231
|
+
<dd><Mono>{e.id}</Mono></dd>
|
|
232
|
+
</dl>
|
|
233
|
+
);
|
|
234
|
+
})}
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function SignalCard({
|
|
240
|
+
signal, copy, subjectNoun, showEvidence = false,
|
|
241
|
+
}: {
|
|
242
|
+
signal: ThreatSignal;
|
|
243
|
+
copy: UiCopy;
|
|
244
|
+
subjectNoun?: string;
|
|
245
|
+
showEvidence?: boolean;
|
|
246
|
+
}): ReactNode {
|
|
247
|
+
const kind = kindPresentation('signal', copy);
|
|
248
|
+
const p = signalPresentation(signal, copy, { subjectNoun });
|
|
249
|
+
return (
|
|
250
|
+
<li
|
|
251
|
+
className="dcy-signal"
|
|
252
|
+
data-dcy-kind="signal"
|
|
253
|
+
data-dcy-tone={p.tone}
|
|
254
|
+
data-dcy-severity={p.severity}
|
|
255
|
+
data-dcy-raising={String(p.raising)}
|
|
256
|
+
data-dcy-hops={String(p.hops)}
|
|
257
|
+
>
|
|
258
|
+
<div className="dcy-signal-head">
|
|
259
|
+
<span className="dcy-glyph" aria-hidden="true">{kind.glyph}</span>
|
|
260
|
+
<Chip>{kind.label}</Chip>
|
|
261
|
+
<Chip>{p.severityLabel} severity</Chip>
|
|
262
|
+
<Chip title={copy.confidenceCaveat}>{p.confidenceLabel} {copy.confidenceLabel.toLowerCase()}</Chip>
|
|
263
|
+
<Chip>{p.hopLabel}</Chip>
|
|
264
|
+
{p.active ? null : <Chip>{p.statusLabel}</Chip>}
|
|
265
|
+
</div>
|
|
266
|
+
<p className="dcy-row-statement dcy-row-strong">{p.explanation}</p>
|
|
267
|
+
{/* Hop distance in a sentence, not just a badge. A "2" tells nobody that
|
|
268
|
+
they are reading an inference about somebody else's counterparty. */}
|
|
269
|
+
<p className="dcy-note">{p.hopStatement}</p>
|
|
270
|
+
{p.inertStatement ? <p className="dcy-note">{p.inertStatement}</p> : null}
|
|
271
|
+
<p className="dcy-note">{p.attribution}</p>
|
|
272
|
+
{showEvidence ? <EvidenceList evidence={signal.evidence} copy={copy} /> : null}
|
|
273
|
+
</li>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function SignalList({
|
|
278
|
+
signals, copy, subjectNoun, showEvidence,
|
|
279
|
+
}: {
|
|
280
|
+
signals: ThreatSignal[];
|
|
281
|
+
copy: UiCopy;
|
|
282
|
+
subjectNoun?: string;
|
|
283
|
+
showEvidence?: boolean;
|
|
284
|
+
}): ReactNode {
|
|
285
|
+
if (signals.length === 0) return <EmptyNote>{copy.noSignals}</EmptyNote>;
|
|
286
|
+
return (
|
|
287
|
+
<ul className="dcy-list">
|
|
288
|
+
{signals.map((signal, index) => (
|
|
289
|
+
<SignalCard
|
|
290
|
+
key={`${signal.type}:${signal.createdAt}:${index}`}
|
|
291
|
+
signal={signal}
|
|
292
|
+
copy={copy}
|
|
293
|
+
subjectNoun={subjectNoun}
|
|
294
|
+
showEvidence={showEvidence}
|
|
295
|
+
/>
|
|
296
|
+
))}
|
|
297
|
+
</ul>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Standing notes
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* History, always neutral.
|
|
307
|
+
*
|
|
308
|
+
* `LIMITED` is the ordinary state of anything deployed this week. It gets the
|
|
309
|
+
* same typography as `ESTABLISHED`, no alert role, no severity tone and no
|
|
310
|
+
* icon that means trouble — the presentation layer will not even hand this
|
|
311
|
+
* component a warning tone to render.
|
|
312
|
+
*/
|
|
313
|
+
export function HistoryNote({
|
|
314
|
+
status, copy,
|
|
315
|
+
}: { status: HistoryStatus; copy: UiCopy }): ReactNode {
|
|
316
|
+
const h = historyPresentation(status);
|
|
317
|
+
return (
|
|
318
|
+
<p className="dcy-note" data-dcy-tone={h.tone} data-dcy-history={h.status}>
|
|
319
|
+
<Chip>{h.label}</Chip> {h.statement}
|
|
320
|
+
</p>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Our own outage, stated as ours. Never rendered as a finding about the user. */
|
|
325
|
+
export function AvailabilityNote({ assessment }: { assessment: Assessment }): ReactNode {
|
|
326
|
+
const a = availabilityPresentation(assessment);
|
|
327
|
+
if (!a.unavailable) return null;
|
|
328
|
+
return (
|
|
329
|
+
<p className="dcy-note" data-dcy-tone={a.tone} data-dcy-unavailable="">
|
|
330
|
+
{a.statement}
|
|
331
|
+
</p>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Coverage signals the SDK refused to treat as risk.
|
|
337
|
+
*
|
|
338
|
+
* Shown rather than dropped, because an integrator should be able to watch the
|
|
339
|
+
* rule being applied to their own traffic.
|
|
340
|
+
*/
|
|
341
|
+
export function DemotedNote({
|
|
342
|
+
demotedSignals, copy,
|
|
343
|
+
}: { demotedSignals?: string[]; copy: UiCopy }): ReactNode {
|
|
344
|
+
if (!demotedSignals || demotedSignals.length === 0) return null;
|
|
345
|
+
return (
|
|
346
|
+
<Section title={copy.demotedHeading} note={copy.demotedExplanation}>
|
|
347
|
+
<p className="dcy-row-meta">
|
|
348
|
+
{demotedSignals.map((type) => <Chip key={type}>{type}</Chip>)}
|
|
349
|
+
</p>
|
|
350
|
+
</Section>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function ExplanationList({ explanation }: { explanation: string[] }): ReactNode {
|
|
355
|
+
if (explanation.length === 0) return null;
|
|
356
|
+
return (
|
|
357
|
+
<ul className="dcy-list" data-dcy-explanation="">
|
|
358
|
+
{explanation.map((line, index) => (
|
|
359
|
+
<li className="dcy-note" key={`${index}:${line.slice(0, 24)}`}>{line}</li>
|
|
360
|
+
))}
|
|
361
|
+
</ul>
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function LevelHeader({
|
|
366
|
+
assessment, copy, levelLabels, children,
|
|
367
|
+
}: {
|
|
368
|
+
assessment: Assessment;
|
|
369
|
+
copy: UiCopy;
|
|
370
|
+
levelLabels?: Partial<Record<RiskLevel, string>>;
|
|
371
|
+
children?: ReactNode;
|
|
372
|
+
}): ReactNode {
|
|
373
|
+
const level = levelPresentation(assessment.riskLevel, { labels: levelLabels });
|
|
374
|
+
return (
|
|
375
|
+
<div className="dcy-head">
|
|
376
|
+
<div className="dcy-head-main">
|
|
377
|
+
<h3 className="dcy-title">{level.label}</h3>
|
|
378
|
+
<p className="dcy-meaning">{level.meaning}</p>
|
|
379
|
+
</div>
|
|
380
|
+
<LevelBadge level={level} />
|
|
381
|
+
{children}
|
|
382
|
+
</div>
|
|
383
|
+
);
|
|
384
|
+
}
|
package/src/styles.ts
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default stylesheet, as a string.
|
|
3
|
+
*
|
|
4
|
+
* It is exported so an integrator with a strict CSP, a shadow root or a
|
|
5
|
+
* server-rendered `<head>` can place it themselves rather than having a
|
|
6
|
+
* `<style>` tag appear inside their component tree. Everything visual in it
|
|
7
|
+
* resolves through a `--dcy-*` custom property, so overriding the palette
|
|
8
|
+
* never requires touching or overriding these rules.
|
|
9
|
+
*
|
|
10
|
+
* There is no styling dependency, and there will not be one. A package that
|
|
11
|
+
* lands inside somebody else's wallet build does not get to bring Tailwind, a
|
|
12
|
+
* CSS-in-JS runtime or a class-name convention along with it.
|
|
13
|
+
*/
|
|
14
|
+
export const DECENTRYS_UI_CSS = `
|
|
15
|
+
.dcy {
|
|
16
|
+
--dcy-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
17
|
+
--dcy-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
|
18
|
+
--dcy-radius: 8px;
|
|
19
|
+
--dcy-gap: 12px;
|
|
20
|
+
|
|
21
|
+
--dcy-surface: #ffffff;
|
|
22
|
+
--dcy-surface-raised: #f5f6f8;
|
|
23
|
+
--dcy-border: #dde1e7;
|
|
24
|
+
--dcy-text: #14171c;
|
|
25
|
+
--dcy-text-muted: #545d6b;
|
|
26
|
+
--dcy-text-faint: #737d8c;
|
|
27
|
+
--dcy-focus: #2f6fed;
|
|
28
|
+
|
|
29
|
+
--dcy-tone-neutral: #545d6b;
|
|
30
|
+
--dcy-tone-neutral-bg: #f2f4f6;
|
|
31
|
+
--dcy-tone-info: #2f5f96;
|
|
32
|
+
--dcy-tone-info-bg: #eef3f9;
|
|
33
|
+
--dcy-tone-caution: #7d6113;
|
|
34
|
+
--dcy-tone-caution-bg: #faf4e4;
|
|
35
|
+
--dcy-tone-elevated: #97500f;
|
|
36
|
+
--dcy-tone-elevated-bg: #fbefe3;
|
|
37
|
+
--dcy-tone-high: #a83c18;
|
|
38
|
+
--dcy-tone-high-bg: #fbece6;
|
|
39
|
+
--dcy-tone-critical: #98211d;
|
|
40
|
+
--dcy-tone-critical-bg: #fbe9e9;
|
|
41
|
+
--dcy-tone-fact: #6a7382;
|
|
42
|
+
--dcy-tone-fact-bg: transparent;
|
|
43
|
+
--dcy-tone-capability: #554d8c;
|
|
44
|
+
--dcy-tone-capability-bg: #f2f1fa;
|
|
45
|
+
--dcy-tone-unknown: #77808f;
|
|
46
|
+
--dcy-tone-unknown-bg: transparent;
|
|
47
|
+
--dcy-tone-inactive: #8d97a4;
|
|
48
|
+
--dcy-tone-inactive-bg: transparent;
|
|
49
|
+
|
|
50
|
+
font-family: var(--dcy-font);
|
|
51
|
+
font-size: 14px;
|
|
52
|
+
line-height: 1.5;
|
|
53
|
+
color: var(--dcy-text);
|
|
54
|
+
box-sizing: border-box;
|
|
55
|
+
}
|
|
56
|
+
.dcy *, .dcy *::before, .dcy *::after { box-sizing: inherit; }
|
|
57
|
+
|
|
58
|
+
@media (prefers-color-scheme: dark) {
|
|
59
|
+
.dcy {
|
|
60
|
+
--dcy-surface: #14171c;
|
|
61
|
+
--dcy-surface-raised: #1c2027;
|
|
62
|
+
--dcy-border: #2c323b;
|
|
63
|
+
--dcy-text: #e8ebef;
|
|
64
|
+
--dcy-text-muted: #a4adba;
|
|
65
|
+
--dcy-text-faint: #808a98;
|
|
66
|
+
--dcy-focus: #7aa5ff;
|
|
67
|
+
|
|
68
|
+
--dcy-tone-neutral: #a4adba;
|
|
69
|
+
--dcy-tone-neutral-bg: #1e232a;
|
|
70
|
+
--dcy-tone-info: #86b0e6;
|
|
71
|
+
--dcy-tone-info-bg: #17202b;
|
|
72
|
+
--dcy-tone-caution: #d8b463;
|
|
73
|
+
--dcy-tone-caution-bg: #241f14;
|
|
74
|
+
--dcy-tone-elevated: #e0a065;
|
|
75
|
+
--dcy-tone-elevated-bg: #261c13;
|
|
76
|
+
--dcy-tone-high: #eb8c68;
|
|
77
|
+
--dcy-tone-high-bg: #271812;
|
|
78
|
+
--dcy-tone-critical: #ef7d78;
|
|
79
|
+
--dcy-tone-critical-bg: #291414;
|
|
80
|
+
--dcy-tone-fact: #97a1ae;
|
|
81
|
+
--dcy-tone-capability: #a79fe0;
|
|
82
|
+
--dcy-tone-capability-bg: #1d1b2a;
|
|
83
|
+
--dcy-tone-unknown: #8b95a3;
|
|
84
|
+
--dcy-tone-inactive: #79828f;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
/*
|
|
90
|
+
* Tone resolution happens here, in the stylesheet, rather than in inline
|
|
91
|
+
* styles on the elements. An inline colour would beat any rule an integrator
|
|
92
|
+
* wrote against these components without \`!important\`, which is not a
|
|
93
|
+
* position to put somebody in who only wanted to match their own palette.
|
|
94
|
+
*/
|
|
95
|
+
[data-dcy-tone="neutral"] { --dcy-tone: var(--dcy-tone-neutral); --dcy-tone-bg: var(--dcy-tone-neutral-bg); }
|
|
96
|
+
[data-dcy-tone="info"] { --dcy-tone: var(--dcy-tone-info); --dcy-tone-bg: var(--dcy-tone-info-bg); }
|
|
97
|
+
[data-dcy-tone="caution"] { --dcy-tone: var(--dcy-tone-caution); --dcy-tone-bg: var(--dcy-tone-caution-bg); }
|
|
98
|
+
[data-dcy-tone="elevated"] { --dcy-tone: var(--dcy-tone-elevated); --dcy-tone-bg: var(--dcy-tone-elevated-bg); }
|
|
99
|
+
[data-dcy-tone="high"] { --dcy-tone: var(--dcy-tone-high); --dcy-tone-bg: var(--dcy-tone-high-bg); }
|
|
100
|
+
[data-dcy-tone="critical"] { --dcy-tone: var(--dcy-tone-critical); --dcy-tone-bg: var(--dcy-tone-critical-bg); }
|
|
101
|
+
[data-dcy-tone="fact"] { --dcy-tone: var(--dcy-tone-fact); --dcy-tone-bg: var(--dcy-tone-fact-bg); }
|
|
102
|
+
[data-dcy-tone="capability"] { --dcy-tone: var(--dcy-tone-capability); --dcy-tone-bg: var(--dcy-tone-capability-bg); }
|
|
103
|
+
[data-dcy-tone="unknown"] { --dcy-tone: var(--dcy-tone-unknown); --dcy-tone-bg: var(--dcy-tone-unknown-bg); }
|
|
104
|
+
[data-dcy-tone="inactive"] { --dcy-tone: var(--dcy-tone-inactive); --dcy-tone-bg: var(--dcy-tone-inactive-bg); }
|
|
105
|
+
|
|
106
|
+
[data-dcy-rule="solid"] { border-left-style: solid; }
|
|
107
|
+
[data-dcy-rule="dashed"] { border-left-style: dashed; }
|
|
108
|
+
[data-dcy-rule="dotted"] { border-left-style: dotted; }
|
|
109
|
+
|
|
110
|
+
.dcy-row[data-dcy-kind="signal"] { border-left-width: 4px; }
|
|
111
|
+
|
|
112
|
+
.dcy :focus-visible {
|
|
113
|
+
outline: 2px solid var(--dcy-focus);
|
|
114
|
+
outline-offset: 2px;
|
|
115
|
+
border-radius: 4px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.dcy-card {
|
|
119
|
+
border: 1px solid var(--dcy-border);
|
|
120
|
+
border-radius: var(--dcy-radius);
|
|
121
|
+
background: var(--dcy-surface);
|
|
122
|
+
padding: var(--dcy-gap);
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
gap: var(--dcy-gap);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.dcy-head { display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap; }
|
|
129
|
+
.dcy-head-main { flex: 1 1 14rem; min-width: 0; }
|
|
130
|
+
.dcy-title { margin: 0; font-size: 14px; font-weight: 600; }
|
|
131
|
+
.dcy-meaning { margin: 4px 0 0; color: var(--dcy-text-muted); font-size: 13px; }
|
|
132
|
+
|
|
133
|
+
.dcy-badge {
|
|
134
|
+
display: inline-flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 6px;
|
|
137
|
+
padding: 2px 8px;
|
|
138
|
+
border-radius: 999px;
|
|
139
|
+
border: 1px solid currentColor;
|
|
140
|
+
font-size: 12px;
|
|
141
|
+
font-weight: 600;
|
|
142
|
+
line-height: 1.6;
|
|
143
|
+
white-space: nowrap;
|
|
144
|
+
color: var(--dcy-tone, var(--dcy-text-muted));
|
|
145
|
+
background: var(--dcy-tone-bg, transparent);
|
|
146
|
+
}
|
|
147
|
+
.dcy-glyph {
|
|
148
|
+
font-family: var(--dcy-font-mono);
|
|
149
|
+
font-weight: 700;
|
|
150
|
+
letter-spacing: -0.05em;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.dcy-chip {
|
|
154
|
+
display: inline-block;
|
|
155
|
+
padding: 1px 6px;
|
|
156
|
+
border-radius: 4px;
|
|
157
|
+
border: 1px solid var(--dcy-border);
|
|
158
|
+
background: var(--dcy-surface-raised);
|
|
159
|
+
color: var(--dcy-text-muted);
|
|
160
|
+
font-size: 11px;
|
|
161
|
+
font-weight: 600;
|
|
162
|
+
text-transform: uppercase;
|
|
163
|
+
letter-spacing: 0.04em;
|
|
164
|
+
white-space: nowrap;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.dcy-section { display: flex; flex-direction: column; gap: 8px; }
|
|
168
|
+
.dcy-section-title {
|
|
169
|
+
margin: 0;
|
|
170
|
+
font-size: 11px;
|
|
171
|
+
font-weight: 700;
|
|
172
|
+
text-transform: uppercase;
|
|
173
|
+
letter-spacing: 0.06em;
|
|
174
|
+
color: var(--dcy-text-faint);
|
|
175
|
+
}
|
|
176
|
+
.dcy-note { margin: 0; font-size: 12px; color: var(--dcy-text-muted); }
|
|
177
|
+
.dcy-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
178
|
+
|
|
179
|
+
/*
|
|
180
|
+
* Kind rows.
|
|
181
|
+
*
|
|
182
|
+
* The left rule's *style* and the row's text label are what separate a fact
|
|
183
|
+
* from a threat signal; the colour is the third carrier, not the only one.
|
|
184
|
+
*/
|
|
185
|
+
.dcy-row {
|
|
186
|
+
display: flex;
|
|
187
|
+
gap: 8px;
|
|
188
|
+
padding: 6px 0 6px 10px;
|
|
189
|
+
border-left-style: solid;
|
|
190
|
+
border-left-width: 2px;
|
|
191
|
+
border-left-color: var(--dcy-tone, var(--dcy-tone-fact));
|
|
192
|
+
}
|
|
193
|
+
.dcy-row-body { min-width: 0; flex: 1 1 auto; }
|
|
194
|
+
.dcy-row-statement { margin: 0; }
|
|
195
|
+
.dcy-row-strong { font-weight: 600; }
|
|
196
|
+
.dcy-row-meta {
|
|
197
|
+
margin: 3px 0 0;
|
|
198
|
+
font-size: 12px;
|
|
199
|
+
color: var(--dcy-text-muted);
|
|
200
|
+
display: flex;
|
|
201
|
+
flex-wrap: wrap;
|
|
202
|
+
gap: 4px 10px;
|
|
203
|
+
}
|
|
204
|
+
.dcy-mono { font-family: var(--dcy-font-mono); font-size: 12px; word-break: break-all; }
|
|
205
|
+
|
|
206
|
+
.dcy-signal {
|
|
207
|
+
border: 1px solid var(--dcy-border);
|
|
208
|
+
border-left-width: 4px;
|
|
209
|
+
border-left-color: var(--dcy-tone, var(--dcy-border));
|
|
210
|
+
border-radius: var(--dcy-radius);
|
|
211
|
+
padding: 10px;
|
|
212
|
+
display: flex;
|
|
213
|
+
flex-direction: column;
|
|
214
|
+
gap: 6px;
|
|
215
|
+
}
|
|
216
|
+
.dcy-signal-head { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
217
|
+
|
|
218
|
+
.dcy-kv { display: grid; grid-template-columns: minmax(6rem, auto) 1fr; gap: 4px 12px; font-size: 13px; }
|
|
219
|
+
.dcy-kv dt { color: var(--dcy-text-muted); }
|
|
220
|
+
.dcy-kv dd { margin: 0; }
|
|
221
|
+
|
|
222
|
+
.dcy-btn {
|
|
223
|
+
font: inherit;
|
|
224
|
+
font-size: 13px;
|
|
225
|
+
font-weight: 600;
|
|
226
|
+
color: var(--dcy-text);
|
|
227
|
+
background: var(--dcy-surface-raised);
|
|
228
|
+
border: 1px solid var(--dcy-border);
|
|
229
|
+
border-radius: 6px;
|
|
230
|
+
padding: 5px 12px;
|
|
231
|
+
cursor: pointer;
|
|
232
|
+
}
|
|
233
|
+
.dcy-btn:hover { background: var(--dcy-border); }
|
|
234
|
+
.dcy-btn-quiet { background: transparent; border-color: transparent; text-decoration: underline; padding-left: 0; }
|
|
235
|
+
|
|
236
|
+
.dcy-backdrop {
|
|
237
|
+
position: fixed;
|
|
238
|
+
inset: 0;
|
|
239
|
+
background: rgba(8, 10, 14, 0.55);
|
|
240
|
+
display: flex;
|
|
241
|
+
align-items: flex-start;
|
|
242
|
+
justify-content: center;
|
|
243
|
+
padding: 24px 16px;
|
|
244
|
+
overflow: auto;
|
|
245
|
+
z-index: 2147483000;
|
|
246
|
+
}
|
|
247
|
+
.dcy-modal {
|
|
248
|
+
width: min(40rem, 100%);
|
|
249
|
+
max-height: calc(100vh - 48px);
|
|
250
|
+
overflow: auto;
|
|
251
|
+
background: var(--dcy-surface);
|
|
252
|
+
border: 1px solid var(--dcy-border);
|
|
253
|
+
border-radius: var(--dcy-radius);
|
|
254
|
+
padding: 16px;
|
|
255
|
+
display: flex;
|
|
256
|
+
flex-direction: column;
|
|
257
|
+
gap: var(--dcy-gap);
|
|
258
|
+
}
|
|
259
|
+
.dcy-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
260
|
+
|
|
261
|
+
.dcy-inline {
|
|
262
|
+
display: inline-flex;
|
|
263
|
+
align-items: center;
|
|
264
|
+
gap: 6px;
|
|
265
|
+
font-family: var(--dcy-font);
|
|
266
|
+
font-size: 12px;
|
|
267
|
+
}
|
|
268
|
+
.dcy-sr {
|
|
269
|
+
position: absolute;
|
|
270
|
+
width: 1px;
|
|
271
|
+
height: 1px;
|
|
272
|
+
padding: 0;
|
|
273
|
+
margin: -1px;
|
|
274
|
+
overflow: hidden;
|
|
275
|
+
clip: rect(0 0 0 0);
|
|
276
|
+
clip-path: inset(50%);
|
|
277
|
+
white-space: nowrap;
|
|
278
|
+
border: 0;
|
|
279
|
+
}
|
|
280
|
+
.dcy-hr { border: none; border-top: 1px solid var(--dcy-border); margin: 0; }
|
|
281
|
+
`;
|