@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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/browser/decentrys-ui.js +1733 -0
  4. package/dist/browser/decentrys-ui.mjs +1681 -0
  5. package/dist/browser/react-dom-global.d.ts +6 -0
  6. package/dist/browser/react-dom-global.d.ts.map +1 -0
  7. package/dist/browser/react-dom-global.js +18 -0
  8. package/dist/browser/react-dom-global.js.map +1 -0
  9. package/dist/browser/react-global.d.ts +25 -0
  10. package/dist/browser/react-global.d.ts.map +1 -0
  11. package/dist/browser/react-global.js +39 -0
  12. package/dist/browser/react-global.js.map +1 -0
  13. package/dist/browser/react-jsx-runtime.d.ts +17 -0
  14. package/dist/browser/react-jsx-runtime.d.ts.map +1 -0
  15. package/dist/browser/react-jsx-runtime.js +27 -0
  16. package/dist/browser/react-jsx-runtime.js.map +1 -0
  17. package/dist/components/AddressRiskBadge.d.ts +39 -0
  18. package/dist/components/AddressRiskBadge.d.ts.map +1 -0
  19. package/dist/components/AddressRiskBadge.js +26 -0
  20. package/dist/components/AddressRiskBadge.js.map +1 -0
  21. package/dist/components/ApprovalWarning.d.ts +29 -0
  22. package/dist/components/ApprovalWarning.d.ts.map +1 -0
  23. package/dist/components/ApprovalWarning.js +18 -0
  24. package/dist/components/ApprovalWarning.js.map +1 -0
  25. package/dist/components/ContractFacts.d.ts +27 -0
  26. package/dist/components/ContractFacts.d.ts.map +1 -0
  27. package/dist/components/ContractFacts.js +15 -0
  28. package/dist/components/ContractFacts.js.map +1 -0
  29. package/dist/components/RiskDetailsModal.d.ts +35 -0
  30. package/dist/components/RiskDetailsModal.d.ts.map +1 -0
  31. package/dist/components/RiskDetailsModal.js +74 -0
  32. package/dist/components/RiskDetailsModal.js.map +1 -0
  33. package/dist/components/ThreatSignalList.d.ts +28 -0
  34. package/dist/components/ThreatSignalList.d.ts.map +1 -0
  35. package/dist/components/ThreatSignalList.js +28 -0
  36. package/dist/components/ThreatSignalList.js.map +1 -0
  37. package/dist/components/TokenSecurityPanel.d.ts +28 -0
  38. package/dist/components/TokenSecurityPanel.d.ts.map +1 -0
  39. package/dist/components/TokenSecurityPanel.js +26 -0
  40. package/dist/components/TokenSecurityPanel.js.map +1 -0
  41. package/dist/components/TransactionRiskBanner.d.ts +36 -0
  42. package/dist/components/TransactionRiskBanner.d.ts.map +1 -0
  43. package/dist/components/TransactionRiskBanner.js +20 -0
  44. package/dist/components/TransactionRiskBanner.js.map +1 -0
  45. package/dist/context.d.ts +51 -0
  46. package/dist/context.d.ts.map +1 -0
  47. package/dist/context.js +47 -0
  48. package/dist/context.js.map +1 -0
  49. package/dist/copy.d.ts +72 -0
  50. package/dist/copy.d.ts.map +1 -0
  51. package/dist/copy.js +77 -0
  52. package/dist/copy.js.map +1 -0
  53. package/dist/index.d.ts +57 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +107 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/presentation.d.ts +316 -0
  58. package/dist/presentation.d.ts.map +1 -0
  59. package/dist/presentation.js +533 -0
  60. package/dist/presentation.js.map +1 -0
  61. package/dist/primitives.d.ts +139 -0
  62. package/dist/primitives.d.ts.map +1 -0
  63. package/dist/primitives.js +150 -0
  64. package/dist/primitives.js.map +1 -0
  65. package/dist/styles.d.ts +15 -0
  66. package/dist/styles.d.ts.map +1 -0
  67. package/dist/styles.js +285 -0
  68. package/dist/styles.js.map +1 -0
  69. package/dist/theme.d.ts +52 -0
  70. package/dist/theme.d.ts.map +1 -0
  71. package/dist/theme.js +90 -0
  72. package/dist/theme.js.map +1 -0
  73. package/package.json +70 -0
  74. package/src/browser/react-dom-global.ts +20 -0
  75. package/src/browser/react-global.ts +42 -0
  76. package/src/browser/react-jsx-runtime.ts +23 -0
  77. package/src/components/AddressRiskBadge.tsx +94 -0
  78. package/src/components/ApprovalWarning.tsx +144 -0
  79. package/src/components/ContractFacts.tsx +119 -0
  80. package/src/components/RiskDetailsModal.tsx +229 -0
  81. package/src/components/ThreatSignalList.tsx +76 -0
  82. package/src/components/TokenSecurityPanel.tsx +168 -0
  83. package/src/components/TransactionRiskBanner.tsx +123 -0
  84. package/src/context.tsx +89 -0
  85. package/src/copy.test.ts +214 -0
  86. package/src/copy.ts +151 -0
  87. package/src/index.ts +84 -0
  88. package/src/presentation.test.ts +521 -0
  89. package/src/presentation.ts +846 -0
  90. package/src/primitives.tsx +384 -0
  91. package/src/styles.ts +281 -0
  92. package/src/theme.test.ts +105 -0
  93. 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
+ `;