@anthusai/papyrus 1.0.0-next.2 → 1.0.0-next.3
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/amplify/data/resource.js +26 -0
- package/amplify/data/resource.ts +30 -0
- package/amplify/data/schema.js +26 -0
- package/amplify/data/schema.ts +30 -0
- package/amplify/functions/content-actions/handler.py +107 -0
- package/amplify/functions/content-actions/requirements.txt +4 -0
- package/amplify/functions/content-actions/resource.js +40 -0
- package/amplify/functions/content-actions/resource.ts +54 -0
- package/amplify/functions/shared/python-bundle.js +17 -2
- package/amplify/functions/shared/python-bundle.ts +23 -2
- package/amplify/site-backend.js +10 -0
- package/amplify/site-backend.ts +15 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -2
- package/app/globals.css +1 -4
- package/app/layout.tsx +4 -12
- package/components/news-desk-page.tsx +1 -4
- package/lib/content-actions-client.ts +98 -0
- package/lib/empty-theme.css +1 -0
- package/lib/layout-scenarios.ts +8 -8
- package/lib/site-brand-tenant.ts +3 -22
- package/lib/site-brand.ts +44 -42
- package/middleware.ts +10 -35
- package/package.json +2 -1
- package/renderers/pretext/article-page.tsx +1 -6
- package/renderers/pretext/presentation-shell.tsx +2 -9
- package/routes.manifest.json +1 -1
- package/src/with-papyrus.mjs +15 -2
- package/publications/anth_us/brand.ts +0 -68
- package/publications/anth_us/theme.css +0 -66
- package/publications/pilobol_us/brand.ts +0 -37
- package/publications/pilobol_us/theme.css +0 -140
- package/publications/threat_intelligence/blog-defense/graph.ts +0 -636
- package/publications/threat_intelligence/blog-defense/layout.ts +0 -934
- package/publications/threat_intelligence/blog-defense/page-background.tsx +0 -590
- package/publications/threat_intelligence/brand.ts +0 -40
- package/publications/threat_intelligence/pictograms/art.tsx +0 -608
- package/publications/threat_intelligence/pictograms/figure.tsx +0 -82
- package/publications/threat_intelligence/pictograms/registry.ts +0 -30
- package/publications/threat_intelligence/pictograms/system.tsx +0 -305
- package/publications/threat_intelligence/seed/seed-edition-content.json +0 -629
- package/publications/threat_intelligence/theme.css +0 -1440
- /package/{publications/papyrus/theme.css → app/papyrus-theme.css} +0 -0
|
@@ -1,590 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { motion } from "motion/react";
|
|
4
|
-
import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type RefObject } from "react";
|
|
5
|
-
import {
|
|
6
|
-
BLOG_DEFENSE_CORE_NODE_ID,
|
|
7
|
-
buildCompromiseSequenceFromPath,
|
|
8
|
-
buildVisibleAttackPath,
|
|
9
|
-
type BlogDefenseCompromiseStep,
|
|
10
|
-
} from "./graph";
|
|
11
|
-
import { layoutDefenseGraph, type LayoutDefenseNode } from "./layout";
|
|
12
|
-
import { SITE_BRAND } from "../../../lib/site-brand";
|
|
13
|
-
import { PICTOGRAM_CYCLE_MS } from "../pictograms/registry";
|
|
14
|
-
import { useResolvedPapyrusTheme } from "../../../components/use-resolved-papyrus-theme";
|
|
15
|
-
|
|
16
|
-
const THROB_MS = 190;
|
|
17
|
-
const COMPROMISE_MS = 120;
|
|
18
|
-
const STEP_GAP_MS = 36;
|
|
19
|
-
const START_DELAY_MS = 40;
|
|
20
|
-
const COMPROMISE_TRANSITION_S = COMPROMISE_MS / 1000;
|
|
21
|
-
const THROB_TRANSITION_S = 0.21;
|
|
22
|
-
const BREACH_RING_DELAYS_S = [0, 0.6, 1.2];
|
|
23
|
-
const BREACH_RING_DURATION_S = 2.4;
|
|
24
|
-
|
|
25
|
-
type PictogramColors = {
|
|
26
|
-
edge: string;
|
|
27
|
-
node: string;
|
|
28
|
-
throb: string;
|
|
29
|
-
coreGlow: string;
|
|
30
|
-
compromised: string;
|
|
31
|
-
compromisedStroke: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const FALLBACK_COLORS: Record<"light" | "dark", PictogramColors> = {
|
|
35
|
-
light: {
|
|
36
|
-
edge: "var(--slate-8)",
|
|
37
|
-
node: "var(--slate-8)",
|
|
38
|
-
throb: "var(--tomato-8)",
|
|
39
|
-
coreGlow: "var(--ti-alarm-red)",
|
|
40
|
-
compromised: "var(--tomato-11)",
|
|
41
|
-
compromisedStroke: "var(--sand-1)",
|
|
42
|
-
},
|
|
43
|
-
dark: {
|
|
44
|
-
edge: "var(--slate-6)",
|
|
45
|
-
node: "var(--slate-5)",
|
|
46
|
-
throb: "var(--tomato-8)",
|
|
47
|
-
coreGlow: "var(--ti-alarm-red)",
|
|
48
|
-
compromised: "var(--tomato-9)",
|
|
49
|
-
compromisedStroke: "var(--sand-2)",
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
type Size = {
|
|
54
|
-
width: number;
|
|
55
|
-
height: number;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
type ObstacleRect = {
|
|
59
|
-
x: number;
|
|
60
|
-
y: number;
|
|
61
|
-
width: number;
|
|
62
|
-
height: number;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
type BlogPageBackgroundProps = {
|
|
66
|
-
pageRef: RefObject<HTMLElement | null>;
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
function intersectRect(source: DOMRect, bounds: DOMRect): ObstacleRect | null {
|
|
70
|
-
const left = Math.max(source.left, bounds.left);
|
|
71
|
-
const top = Math.max(source.top, bounds.top);
|
|
72
|
-
const right = Math.min(source.right, bounds.right);
|
|
73
|
-
const bottom = Math.min(source.bottom, bounds.bottom);
|
|
74
|
-
const width = right - left;
|
|
75
|
-
const height = bottom - top;
|
|
76
|
-
if (width <= 0 || height <= 0) return null;
|
|
77
|
-
return {
|
|
78
|
-
x: left - bounds.left,
|
|
79
|
-
y: top - bounds.top,
|
|
80
|
-
width,
|
|
81
|
-
height,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function textFragmentRects(element: HTMLElement, bounds: DOMRect): ObstacleRect[] {
|
|
86
|
-
const ownerDocument = element.ownerDocument;
|
|
87
|
-
const elementRect = element.getBoundingClientRect();
|
|
88
|
-
const computedStyle = ownerDocument.defaultView?.getComputedStyle(element);
|
|
89
|
-
const context = ownerDocument.createElement("canvas").getContext("2d");
|
|
90
|
-
if (context && computedStyle) {
|
|
91
|
-
context.font = computedStyle.font;
|
|
92
|
-
}
|
|
93
|
-
const walker = ownerDocument.createTreeWalker(
|
|
94
|
-
element,
|
|
95
|
-
NodeFilter.SHOW_TEXT,
|
|
96
|
-
{
|
|
97
|
-
acceptNode(node) {
|
|
98
|
-
return /\S/.test(node.textContent ?? "")
|
|
99
|
-
? NodeFilter.FILTER_ACCEPT
|
|
100
|
-
: NodeFilter.FILTER_REJECT;
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
);
|
|
104
|
-
const range = ownerDocument.createRange();
|
|
105
|
-
const rects: ObstacleRect[] = [];
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
let node = walker.nextNode();
|
|
109
|
-
while (node) {
|
|
110
|
-
const text = node.textContent ?? "";
|
|
111
|
-
const tokenPattern = /\S+/g;
|
|
112
|
-
let match: RegExpExecArray | null;
|
|
113
|
-
while ((match = tokenPattern.exec(text)) !== null) {
|
|
114
|
-
range.setStart(node, match.index);
|
|
115
|
-
range.setEnd(node, match.index + match[0].length);
|
|
116
|
-
for (const fragmentRect of Array.from(range.getClientRects())) {
|
|
117
|
-
const measuredWidth = context?.measureText(match[0]).width ?? 0;
|
|
118
|
-
const textWidth = measuredWidth > 0 && measuredWidth < (fragmentRect.width * 0.98)
|
|
119
|
-
? measuredWidth
|
|
120
|
-
: fragmentRect.width;
|
|
121
|
-
const sourceRect = new DOMRect(
|
|
122
|
-
fragmentRect.left,
|
|
123
|
-
elementRect.top,
|
|
124
|
-
textWidth,
|
|
125
|
-
elementRect.height,
|
|
126
|
-
);
|
|
127
|
-
const rect = intersectRect(sourceRect, bounds);
|
|
128
|
-
if (rect) rects.push(rect);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
node = walker.nextNode();
|
|
132
|
-
}
|
|
133
|
-
} finally {
|
|
134
|
-
range.detach();
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return rects;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function elementObstacleRects(element: HTMLElement, bounds: DOMRect): ObstacleRect[] {
|
|
141
|
-
const fragments = textFragmentRects(element, bounds);
|
|
142
|
-
if (fragments.length > 0) return fragments;
|
|
143
|
-
return Array.from(element.getClientRects())
|
|
144
|
-
.map((lineRect) => intersectRect(lineRect, bounds))
|
|
145
|
-
.filter((rect): rect is ObstacleRect => Boolean(rect));
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function readPictogramColors(container: HTMLElement): PictogramColors {
|
|
149
|
-
const style = getComputedStyle(container);
|
|
150
|
-
return {
|
|
151
|
-
edge: style.getPropertyValue("--ti-pictogram-edge").trim() || FALLBACK_COLORS.light.edge,
|
|
152
|
-
node: style.getPropertyValue("--ti-pictogram-node").trim() || FALLBACK_COLORS.light.node,
|
|
153
|
-
throb: style.getPropertyValue("--ti-pictogram-throb").trim() || FALLBACK_COLORS.light.throb,
|
|
154
|
-
coreGlow: style.getPropertyValue("--ti-pictogram-core-glow").trim() || FALLBACK_COLORS.light.coreGlow,
|
|
155
|
-
compromised: style.getPropertyValue("--ti-pictogram-compromised").trim() || FALLBACK_COLORS.light.compromised,
|
|
156
|
-
compromisedStroke: style.getPropertyValue("--ti-pictogram-compromised-stroke").trim() || FALLBACK_COLORS.light.compromisedStroke,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function stepDurationMs(): number {
|
|
161
|
-
return THROB_MS + COMPROMISE_MS + STEP_GAP_MS;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function isSameThrobStep(a: BlogDefenseCompromiseStep | null, b: BlogDefenseCompromiseStep | null): boolean {
|
|
165
|
-
if (!a || !b || a.kind !== b.kind) return false;
|
|
166
|
-
if (a.kind === "node" && b.kind === "node") return a.nodeId === b.nodeId;
|
|
167
|
-
if (a.kind === "edge" && b.kind === "edge") return a.edgeId === b.edgeId;
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function CoreBreachEffect({
|
|
172
|
-
coreNode,
|
|
173
|
-
colors,
|
|
174
|
-
filterId,
|
|
175
|
-
prefersReducedMotion,
|
|
176
|
-
scale,
|
|
177
|
-
layer,
|
|
178
|
-
}: {
|
|
179
|
-
coreNode: LayoutDefenseNode;
|
|
180
|
-
colors: PictogramColors;
|
|
181
|
-
filterId: string;
|
|
182
|
-
prefersReducedMotion: boolean;
|
|
183
|
-
scale: number;
|
|
184
|
-
layer: "rings" | "halo";
|
|
185
|
-
}) {
|
|
186
|
-
const ringStartRadius = coreNode.radius * 1.2;
|
|
187
|
-
const ringEndRadius = coreNode.radius * 3;
|
|
188
|
-
const haloRadius = coreNode.radius * 1.85;
|
|
189
|
-
|
|
190
|
-
if (layer === "rings") {
|
|
191
|
-
return (
|
|
192
|
-
<g className="blog-page-background__breach-rings" style={{ color: colors.coreGlow }}>
|
|
193
|
-
{BREACH_RING_DELAYS_S.map((delay) => (
|
|
194
|
-
<motion.circle
|
|
195
|
-
animate={
|
|
196
|
-
prefersReducedMotion
|
|
197
|
-
? { fillOpacity: 0.22, r: ringEndRadius * 0.75 }
|
|
198
|
-
: {
|
|
199
|
-
fillOpacity: [0.45, 0],
|
|
200
|
-
r: [ringStartRadius, ringEndRadius],
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
cx={coreNode.x}
|
|
204
|
-
cy={coreNode.y}
|
|
205
|
-
fill="currentColor"
|
|
206
|
-
initial={false}
|
|
207
|
-
key={`breach-ring-${delay}`}
|
|
208
|
-
transition={
|
|
209
|
-
prefersReducedMotion
|
|
210
|
-
? { duration: 0 }
|
|
211
|
-
: {
|
|
212
|
-
delay,
|
|
213
|
-
duration: BREACH_RING_DURATION_S,
|
|
214
|
-
ease: "easeOut",
|
|
215
|
-
repeat: Infinity,
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
/>
|
|
219
|
-
))}
|
|
220
|
-
</g>
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const blurStdDeviation = Math.max(5, scale * 0.45);
|
|
225
|
-
return (
|
|
226
|
-
<g className="blog-page-background__breach-halo" style={{ color: colors.coreGlow }}>
|
|
227
|
-
<motion.circle
|
|
228
|
-
animate={
|
|
229
|
-
prefersReducedMotion
|
|
230
|
-
? { fillOpacity: 0.42, r: haloRadius }
|
|
231
|
-
: {
|
|
232
|
-
fillOpacity: [0.35, 0.7, 0.35],
|
|
233
|
-
r: haloRadius,
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
cx={coreNode.x}
|
|
237
|
-
cy={coreNode.y}
|
|
238
|
-
fill="currentColor"
|
|
239
|
-
filter={`url(#${filterId})`}
|
|
240
|
-
initial={false}
|
|
241
|
-
transition={
|
|
242
|
-
prefersReducedMotion
|
|
243
|
-
? { duration: 0 }
|
|
244
|
-
: {
|
|
245
|
-
duration: BREACH_RING_DURATION_S,
|
|
246
|
-
ease: "easeInOut",
|
|
247
|
-
repeat: Infinity,
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
/>
|
|
251
|
-
<defs>
|
|
252
|
-
<filter height="300%" id={filterId} width="300%" x="-100%" y="-100%">
|
|
253
|
-
<feGaussianBlur result="blur" stdDeviation={blurStdDeviation} />
|
|
254
|
-
<feMerge>
|
|
255
|
-
<feMergeNode in="blur" />
|
|
256
|
-
<feMergeNode in="SourceGraphic" />
|
|
257
|
-
</feMerge>
|
|
258
|
-
</filter>
|
|
259
|
-
</defs>
|
|
260
|
-
</g>
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
export function BlogPageBackground({ pageRef }: BlogPageBackgroundProps) {
|
|
265
|
-
const svgId = useId().replace(/:/g, "-");
|
|
266
|
-
const coreBreachFilterId = `${svgId}-core-breach-glow`;
|
|
267
|
-
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
268
|
-
const resolvedTheme = useResolvedPapyrusTheme();
|
|
269
|
-
const [colors, setColors] = useState<PictogramColors>(FALLBACK_COLORS[resolvedTheme]);
|
|
270
|
-
const [size, setSize] = useState<Size>({ width: 0, height: 0 });
|
|
271
|
-
const [obstacles, setObstacles] = useState<ObstacleRect[]>([]);
|
|
272
|
-
const [enableStochasticPath, setEnableStochasticPath] = useState(false);
|
|
273
|
-
const [attackCycle, setAttackCycle] = useState(0);
|
|
274
|
-
const [compromisedNodes, setCompromisedNodes] = useState<Set<string>>(() => new Set());
|
|
275
|
-
const [compromisedEdges, setCompromisedEdges] = useState<Set<string>>(() => new Set());
|
|
276
|
-
const [throbbingStep, setThrobbingStep] = useState<BlogDefenseCompromiseStep | null>(null);
|
|
277
|
-
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
|
|
278
|
-
|
|
279
|
-
useLayoutEffect(() => {
|
|
280
|
-
const container = containerRef.current;
|
|
281
|
-
if (!container) return;
|
|
282
|
-
|
|
283
|
-
const update = () => {
|
|
284
|
-
const rect = container.getBoundingClientRect();
|
|
285
|
-
const width = Math.ceil(rect.width);
|
|
286
|
-
const height = Math.ceil(rect.height);
|
|
287
|
-
if (width > 0 && height > 0) {
|
|
288
|
-
setSize((current) => (current.width === width && current.height === height ? current : { width, height }));
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
update();
|
|
293
|
-
const observer = new ResizeObserver(update);
|
|
294
|
-
observer.observe(container);
|
|
295
|
-
window.addEventListener("resize", update);
|
|
296
|
-
|
|
297
|
-
return () => {
|
|
298
|
-
observer.disconnect();
|
|
299
|
-
window.removeEventListener("resize", update);
|
|
300
|
-
};
|
|
301
|
-
}, []);
|
|
302
|
-
|
|
303
|
-
useLayoutEffect(() => {
|
|
304
|
-
const container = containerRef.current;
|
|
305
|
-
if (!container) return;
|
|
306
|
-
|
|
307
|
-
let page = pageRef.current ?? container.closest<HTMLElement>("main.presentation-page--blog");
|
|
308
|
-
let raf = 0;
|
|
309
|
-
let tries = 0;
|
|
310
|
-
|
|
311
|
-
const update = () => {
|
|
312
|
-
if (!page) {
|
|
313
|
-
page = pageRef.current ?? container.closest<HTMLElement>("main.presentation-page--blog");
|
|
314
|
-
}
|
|
315
|
-
if (!page) return false;
|
|
316
|
-
|
|
317
|
-
const containerRect = container.getBoundingClientRect();
|
|
318
|
-
const padding = containerRect.width < 420 ? 6 : containerRect.width < 780 ? 9 : 12;
|
|
319
|
-
const selectors = [
|
|
320
|
-
".presentation-header h1 span",
|
|
321
|
-
".presentation-header__meta",
|
|
322
|
-
".presentation-header__subtitle",
|
|
323
|
-
".presentation-header__date",
|
|
324
|
-
".presentation-header__tagline",
|
|
325
|
-
".presentation-section-nav a",
|
|
326
|
-
];
|
|
327
|
-
const next: ObstacleRect[] = [];
|
|
328
|
-
for (const selector of selectors) {
|
|
329
|
-
const elements = Array.from(page.querySelectorAll<HTMLElement>(selector));
|
|
330
|
-
for (const element of elements) {
|
|
331
|
-
for (const rect of elementObstacleRects(element, containerRect)) {
|
|
332
|
-
next.push({
|
|
333
|
-
x: Math.max(0, rect.x - padding),
|
|
334
|
-
y: Math.max(0, rect.y - padding),
|
|
335
|
-
width: rect.width + padding * 2,
|
|
336
|
-
height: rect.height + padding * 2,
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
const unique = next.filter((candidate, index) => {
|
|
342
|
-
return !next.slice(0, index).some((existing) => (
|
|
343
|
-
Math.abs(existing.x - candidate.x) < 1
|
|
344
|
-
&& Math.abs(existing.y - candidate.y) < 1
|
|
345
|
-
&& Math.abs(existing.width - candidate.width) < 1
|
|
346
|
-
&& Math.abs(existing.height - candidate.height) < 1
|
|
347
|
-
));
|
|
348
|
-
});
|
|
349
|
-
setObstacles(unique);
|
|
350
|
-
return unique.length > 0;
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
const ensureInitial = () => {
|
|
354
|
-
const found = update();
|
|
355
|
-
tries += 1;
|
|
356
|
-
if (!found && tries < 30) {
|
|
357
|
-
raf = window.requestAnimationFrame(ensureInitial);
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
ensureInitial();
|
|
361
|
-
|
|
362
|
-
const observer = new ResizeObserver(() => {
|
|
363
|
-
void update();
|
|
364
|
-
});
|
|
365
|
-
observer.observe(container);
|
|
366
|
-
if (page) observer.observe(page);
|
|
367
|
-
window.addEventListener("resize", update);
|
|
368
|
-
void document.fonts?.ready.then(() => {
|
|
369
|
-
void update();
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
return () => {
|
|
373
|
-
if (raf) window.cancelAnimationFrame(raf);
|
|
374
|
-
observer.disconnect();
|
|
375
|
-
window.removeEventListener("resize", update);
|
|
376
|
-
};
|
|
377
|
-
}, [pageRef]);
|
|
378
|
-
|
|
379
|
-
useEffect(() => {
|
|
380
|
-
setEnableStochasticPath(true);
|
|
381
|
-
}, []);
|
|
382
|
-
|
|
383
|
-
useEffect(() => {
|
|
384
|
-
if (!enableStochasticPath) return;
|
|
385
|
-
const interval = window.setInterval(() => {
|
|
386
|
-
setAttackCycle((current) => current + 1);
|
|
387
|
-
}, PICTOGRAM_CYCLE_MS);
|
|
388
|
-
return () => {
|
|
389
|
-
window.clearInterval(interval);
|
|
390
|
-
};
|
|
391
|
-
}, [enableStochasticPath]);
|
|
392
|
-
|
|
393
|
-
const graphLayout = useMemo(() => {
|
|
394
|
-
if (!size.width || !size.height) return null;
|
|
395
|
-
return layoutDefenseGraph({
|
|
396
|
-
width: size.width,
|
|
397
|
-
height: size.height,
|
|
398
|
-
obstacles,
|
|
399
|
-
});
|
|
400
|
-
}, [obstacles, size.height, size.width]);
|
|
401
|
-
|
|
402
|
-
const animationLayoutKey = `${size.width}:${size.height}`;
|
|
403
|
-
const compromiseSequence = useMemo(() => {
|
|
404
|
-
void attackCycle; // force recompute on attack cycle change
|
|
405
|
-
if (!graphLayout) return [];
|
|
406
|
-
const visiblePath = buildVisibleAttackPath({
|
|
407
|
-
nodes: graphLayout.nodes,
|
|
408
|
-
edges: graphLayout.edges,
|
|
409
|
-
coreId: BLOG_DEFENSE_CORE_NODE_ID,
|
|
410
|
-
stochastic: enableStochasticPath,
|
|
411
|
-
});
|
|
412
|
-
return buildCompromiseSequenceFromPath(visiblePath.nodeIds, graphLayout.edges);
|
|
413
|
-
}, [attackCycle, enableStochasticPath, graphLayout]);
|
|
414
|
-
const coreNode = useMemo(
|
|
415
|
-
() => graphLayout?.nodes.find((node) => node.id === BLOG_DEFENSE_CORE_NODE_ID),
|
|
416
|
-
[graphLayout?.nodes],
|
|
417
|
-
);
|
|
418
|
-
const coreBreached = compromisedNodes.has(BLOG_DEFENSE_CORE_NODE_ID);
|
|
419
|
-
|
|
420
|
-
useEffect(() => {
|
|
421
|
-
const container = containerRef.current;
|
|
422
|
-
if (!container) return;
|
|
423
|
-
setColors(readPictogramColors(container));
|
|
424
|
-
}, [resolvedTheme, animationLayoutKey]);
|
|
425
|
-
|
|
426
|
-
useEffect(() => {
|
|
427
|
-
if (!graphLayout || compromiseSequence.length === 0) return;
|
|
428
|
-
|
|
429
|
-
const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
430
|
-
setPrefersReducedMotion(motionQuery.matches);
|
|
431
|
-
setCompromisedNodes(new Set());
|
|
432
|
-
setCompromisedEdges(new Set());
|
|
433
|
-
setThrobbingStep(null);
|
|
434
|
-
|
|
435
|
-
if (motionQuery.matches) {
|
|
436
|
-
const nodes = new Set<string>();
|
|
437
|
-
const edges = new Set<string>();
|
|
438
|
-
for (const step of compromiseSequence) {
|
|
439
|
-
if (step.kind === "node") nodes.add(step.nodeId);
|
|
440
|
-
else edges.add(step.edgeId);
|
|
441
|
-
}
|
|
442
|
-
setCompromisedNodes(nodes);
|
|
443
|
-
setCompromisedEdges(edges);
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const timeouts: number[] = [];
|
|
448
|
-
const perStepMs = stepDurationMs();
|
|
449
|
-
compromiseSequence.forEach((step, index) => {
|
|
450
|
-
const stepStartMs = START_DELAY_MS + index * perStepMs;
|
|
451
|
-
const compromiseAtMs = stepStartMs + THROB_MS;
|
|
452
|
-
|
|
453
|
-
timeouts.push(window.setTimeout(() => {
|
|
454
|
-
setThrobbingStep(step);
|
|
455
|
-
}, stepStartMs));
|
|
456
|
-
|
|
457
|
-
timeouts.push(window.setTimeout(() => {
|
|
458
|
-
setThrobbingStep((current) => (isSameThrobStep(current, step) ? null : current));
|
|
459
|
-
if (step.kind === "node") {
|
|
460
|
-
setCompromisedNodes((current) => new Set([...current, step.nodeId]));
|
|
461
|
-
} else {
|
|
462
|
-
setCompromisedEdges((current) => new Set([...current, step.edgeId]));
|
|
463
|
-
}
|
|
464
|
-
}, compromiseAtMs));
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
return () => {
|
|
468
|
-
timeouts.forEach((timeout) => window.clearTimeout(timeout));
|
|
469
|
-
};
|
|
470
|
-
}, [animationLayoutKey, attackCycle, compromiseSequence, graphLayout]);
|
|
471
|
-
|
|
472
|
-
if (SITE_BRAND.id !== "threat-intelligence") return null;
|
|
473
|
-
|
|
474
|
-
return (
|
|
475
|
-
<div
|
|
476
|
-
aria-hidden="true"
|
|
477
|
-
className="blog-page-background"
|
|
478
|
-
data-blog-page-background="true"
|
|
479
|
-
data-blog-page-background-obstacles={JSON.stringify(obstacles)}
|
|
480
|
-
ref={containerRef}
|
|
481
|
-
>
|
|
482
|
-
{graphLayout ? (
|
|
483
|
-
<svg
|
|
484
|
-
className="blog-page-background__svg"
|
|
485
|
-
preserveAspectRatio="xMaxYMin meet"
|
|
486
|
-
viewBox={`0 0 ${size.width} ${size.height}`}
|
|
487
|
-
>
|
|
488
|
-
<g className="blog-page-background__edges">
|
|
489
|
-
{graphLayout.edges.map((edge) => {
|
|
490
|
-
const isCompromised = compromisedEdges.has(edge.id);
|
|
491
|
-
const isThrobbing = throbbingStep?.kind === "edge" && throbbingStep.edgeId === edge.id;
|
|
492
|
-
const baseStrokeWidth = Math.max(1.4, graphLayout.scale * 1.2);
|
|
493
|
-
return (
|
|
494
|
-
<motion.line
|
|
495
|
-
animate={{
|
|
496
|
-
stroke: isCompromised
|
|
497
|
-
? colors.compromised
|
|
498
|
-
: isThrobbing
|
|
499
|
-
? [colors.edge, colors.throb, colors.edge]
|
|
500
|
-
: colors.edge,
|
|
501
|
-
strokeWidth: baseStrokeWidth,
|
|
502
|
-
}}
|
|
503
|
-
data-blog-edge-id={edge.id}
|
|
504
|
-
initial={false}
|
|
505
|
-
key={edge.id}
|
|
506
|
-
stroke={isCompromised ? colors.compromised : colors.edge}
|
|
507
|
-
strokeLinecap="round"
|
|
508
|
-
strokeWidth={baseStrokeWidth}
|
|
509
|
-
transition={
|
|
510
|
-
isThrobbing
|
|
511
|
-
? { duration: THROB_TRANSITION_S, ease: "easeInOut", repeat: 1 }
|
|
512
|
-
: { duration: COMPROMISE_TRANSITION_S, ease: "easeOut" }
|
|
513
|
-
}
|
|
514
|
-
x1={edge.x1}
|
|
515
|
-
x2={edge.x2}
|
|
516
|
-
y1={edge.y1}
|
|
517
|
-
y2={edge.y2}
|
|
518
|
-
/>
|
|
519
|
-
);
|
|
520
|
-
})}
|
|
521
|
-
</g>
|
|
522
|
-
{coreBreached && coreNode ? (
|
|
523
|
-
<CoreBreachEffect
|
|
524
|
-
colors={colors}
|
|
525
|
-
coreNode={coreNode}
|
|
526
|
-
filterId={coreBreachFilterId}
|
|
527
|
-
layer="rings"
|
|
528
|
-
prefersReducedMotion={prefersReducedMotion}
|
|
529
|
-
scale={graphLayout.scale}
|
|
530
|
-
/>
|
|
531
|
-
) : null}
|
|
532
|
-
<g className="blog-page-background__nodes">
|
|
533
|
-
{graphLayout.nodes.map((node) => {
|
|
534
|
-
const isCompromised = compromisedNodes.has(node.id);
|
|
535
|
-
const isThrobbing = throbbingStep?.kind === "node" && throbbingStep.nodeId === node.id;
|
|
536
|
-
const isCore = node.role === "core";
|
|
537
|
-
const coreBreachedNode = isCore && coreBreached;
|
|
538
|
-
const coreStrokeWidth = isCore ? Math.max(4, graphLayout.scale * 1.5) : 0;
|
|
539
|
-
const breachedCoreStrokeWidth = isCore
|
|
540
|
-
? Math.max(coreStrokeWidth * 1.35, graphLayout.scale * 2)
|
|
541
|
-
: 0;
|
|
542
|
-
return (
|
|
543
|
-
<motion.circle
|
|
544
|
-
animate={{
|
|
545
|
-
fill: isCompromised
|
|
546
|
-
? colors.compromised
|
|
547
|
-
: isThrobbing
|
|
548
|
-
? [colors.node, colors.throb, colors.node]
|
|
549
|
-
: colors.node,
|
|
550
|
-
r: node.radius,
|
|
551
|
-
stroke: coreBreachedNode || (isCompromised && isCore)
|
|
552
|
-
? colors.compromisedStroke
|
|
553
|
-
: "transparent",
|
|
554
|
-
strokeWidth: coreBreachedNode
|
|
555
|
-
? breachedCoreStrokeWidth
|
|
556
|
-
: isCompromised && isCore
|
|
557
|
-
? coreStrokeWidth
|
|
558
|
-
: 0,
|
|
559
|
-
}}
|
|
560
|
-
cx={node.x}
|
|
561
|
-
cy={node.y}
|
|
562
|
-
data-blog-node-id={node.id}
|
|
563
|
-
fill={isCompromised ? colors.compromised : colors.node}
|
|
564
|
-
initial={false}
|
|
565
|
-
key={node.id}
|
|
566
|
-
r={node.radius}
|
|
567
|
-
transition={
|
|
568
|
-
isThrobbing
|
|
569
|
-
? { duration: THROB_TRANSITION_S, ease: "easeInOut", repeat: 1 }
|
|
570
|
-
: { duration: COMPROMISE_TRANSITION_S, ease: "easeOut" }
|
|
571
|
-
}
|
|
572
|
-
/>
|
|
573
|
-
);
|
|
574
|
-
})}
|
|
575
|
-
</g>
|
|
576
|
-
{coreBreached && coreNode ? (
|
|
577
|
-
<CoreBreachEffect
|
|
578
|
-
colors={colors}
|
|
579
|
-
coreNode={coreNode}
|
|
580
|
-
filterId={coreBreachFilterId}
|
|
581
|
-
layer="halo"
|
|
582
|
-
prefersReducedMotion={prefersReducedMotion}
|
|
583
|
-
scale={graphLayout.scale}
|
|
584
|
-
/>
|
|
585
|
-
) : null}
|
|
586
|
-
</svg>
|
|
587
|
-
) : null}
|
|
588
|
-
</div>
|
|
589
|
-
);
|
|
590
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { SiteBrand } from "../../lib/site-brand";
|
|
2
|
-
import { DEFAULT_THEME_PACK_TOKENS } from "../../lib/site-stack";
|
|
3
|
-
|
|
4
|
-
export const threatIntelligenceBrand: SiteBrand = {
|
|
5
|
-
id: "threat-intelligence",
|
|
6
|
-
appTitle: "Threat Intelligence",
|
|
7
|
-
appDescription: "ANTHUS THREAT INTELLIGENCE from Anthus AI Solutions.",
|
|
8
|
-
mastheadTitle: "THREAT INTELLIGENCE",
|
|
9
|
-
mastheadSubtitle: "from Anthus AI Solutions",
|
|
10
|
-
mastheadTagline: "Practical advice for staying secure as the threat landscape shifts.",
|
|
11
|
-
backToHomeLabel: "Back to Threat Intelligence",
|
|
12
|
-
articleTitleSuffix: "Threat Intelligence",
|
|
13
|
-
placeholderByline: "Anthus AI Solutions",
|
|
14
|
-
defaultPresentation: "blog",
|
|
15
|
-
forcedPresentation: "blog",
|
|
16
|
-
textFont: 'system-ui, -apple-system, "Segoe UI", "Helvetica Neue", Arial, sans-serif',
|
|
17
|
-
footerTitle: "ANTHUS THREAT INTELLIGENCE",
|
|
18
|
-
footerSubtitleOverride: "",
|
|
19
|
-
mastheadWordSplit: true,
|
|
20
|
-
mastheadDateFormat: "formatted",
|
|
21
|
-
mastheadSource: "brand",
|
|
22
|
-
sectionLinkStrategy: "anchor",
|
|
23
|
-
defaultVideoCredit: "Anthus Threat Intelligence video",
|
|
24
|
-
themePack: "threat-intelligence",
|
|
25
|
-
themeTokens: {
|
|
26
|
-
...DEFAULT_THEME_PACK_TOKENS,
|
|
27
|
-
paper: "#f5f0e6",
|
|
28
|
-
moss: "#1c1917",
|
|
29
|
-
ochre: "#ea580c",
|
|
30
|
-
ink: "#44403c",
|
|
31
|
-
},
|
|
32
|
-
renderer: { kind: "pretext" },
|
|
33
|
-
hosting: { kind: "amplify-ssr" },
|
|
34
|
-
opsChrome: "app",
|
|
35
|
-
corpusKey: "threat-intelligence",
|
|
36
|
-
steeringConfigPath: "corpora/papyrus-steering.yml",
|
|
37
|
-
newsroomSectionsConfigPath: "corpora/papyrus-newsroom-sections.yml",
|
|
38
|
-
analysisProfilesPath: "corpora/papyrus-analysis-profiles.yml",
|
|
39
|
-
publicationName: "Anthus Threat Intelligence",
|
|
40
|
-
};
|