@anthusai/papyrus 1.0.0-next.2 → 1.0.0-next.4

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 (58) hide show
  1. package/amplify/data/resource.js +26 -0
  2. package/amplify/data/resource.ts +30 -0
  3. package/amplify/data/schema.js +26 -0
  4. package/amplify/data/schema.ts +30 -0
  5. package/amplify/functions/content-actions/handler.py +107 -0
  6. package/amplify/functions/content-actions/requirements.txt +4 -0
  7. package/amplify/functions/content-actions/resource.js +40 -0
  8. package/amplify/functions/content-actions/resource.ts +54 -0
  9. package/amplify/functions/shared/python-bundle.js +17 -2
  10. package/amplify/functions/shared/python-bundle.ts +23 -2
  11. package/amplify/site-backend.js +10 -0
  12. package/amplify/site-backend.ts +15 -0
  13. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -2
  14. package/app/globals.css +1 -4
  15. package/app/layout.tsx +8 -12
  16. package/app/robots.ts +11 -0
  17. package/bin/papyrus-infra.mjs +46 -16
  18. package/components/news-desk-page.tsx +1 -4
  19. package/components/staging-banner.tsx +11 -0
  20. package/infra/amplify-app-shell.js +123 -80
  21. package/infra/amplify-app-shell.ts +139 -113
  22. package/infra/build-specs.js +86 -0
  23. package/infra/build-specs.ts +132 -0
  24. package/infra/index.js +2 -0
  25. package/infra/site-config.js +125 -0
  26. package/infra/site-config.ts +163 -0
  27. package/lib/cached-content-repository.ts +2 -1
  28. package/lib/content-actions-client.ts +98 -0
  29. package/lib/content-repository.ts +3 -0
  30. package/lib/content-source-context.ts +16 -0
  31. package/lib/empty-theme.css +1 -0
  32. package/lib/graphql-content-repository.ts +182 -37
  33. package/lib/layout-scenarios.ts +8 -8
  34. package/lib/site-brand-tenant.ts +3 -22
  35. package/lib/site-brand.ts +44 -42
  36. package/lib/site-env.ts +52 -0
  37. package/lib/staging-gate.ts +37 -0
  38. package/middleware.ts +46 -34
  39. package/package.json +6 -1
  40. package/renderers/pretext/article-page.tsx +1 -6
  41. package/renderers/pretext/presentation-shell.tsx +2 -9
  42. package/routes.manifest.json +1 -1
  43. package/src/with-papyrus.mjs +15 -2
  44. package/publications/anth_us/brand.ts +0 -68
  45. package/publications/anth_us/theme.css +0 -66
  46. package/publications/pilobol_us/brand.ts +0 -37
  47. package/publications/pilobol_us/theme.css +0 -140
  48. package/publications/threat_intelligence/blog-defense/graph.ts +0 -636
  49. package/publications/threat_intelligence/blog-defense/layout.ts +0 -934
  50. package/publications/threat_intelligence/blog-defense/page-background.tsx +0 -590
  51. package/publications/threat_intelligence/brand.ts +0 -40
  52. package/publications/threat_intelligence/pictograms/art.tsx +0 -608
  53. package/publications/threat_intelligence/pictograms/figure.tsx +0 -82
  54. package/publications/threat_intelligence/pictograms/registry.ts +0 -30
  55. package/publications/threat_intelligence/pictograms/system.tsx +0 -305
  56. package/publications/threat_intelligence/seed/seed-edition-content.json +0 -629
  57. package/publications/threat_intelligence/theme.css +0 -1440
  58. /package/{publications/papyrus/theme.css → app/papyrus-theme.css} +0 -0
@@ -1,608 +0,0 @@
1
- "use client";
2
-
3
- import { motion } from "motion/react";
4
- import type { CSSProperties, ReactElement } from "react";
5
- import type { ThreatIntelligencePictogramSlug } from "./registry";
6
- import {
7
- Box,
8
- Line,
9
- Node,
10
- PictogramSvg,
11
- pointsPath,
12
- type PictogramMotion,
13
- type PictogramPalette,
14
- } from "./system";
15
-
16
- export type RegisteredPictogramProps = {
17
- alt: string;
18
- timing: PictogramMotion;
19
- palette: PictogramPalette;
20
- };
21
-
22
- export type PictogramRegistryEntry = {
23
- aspectRatio?: number;
24
- render: (props: RegisteredPictogramProps) => ReactElement;
25
- };
26
-
27
- function BalanceShiftPictogram({ alt, palette, timing }: RegisteredPictogramProps) {
28
- const scalePalette = {
29
- ...palette,
30
- edge: "var(--foreground-strong)",
31
- node: "var(--foreground-strong)",
32
- muted: "color-mix(in srgb, var(--foreground-strong) 54%, var(--background) 46%)",
33
- };
34
- const nodeTransition = timing.prefersReducedMotion
35
- ? { duration: 0 }
36
- : {
37
- duration: timing.cycleS,
38
- repeat: Infinity,
39
- ease: "easeInOut",
40
- times: [0, 0.24, 0.42, 0.62, 1],
41
- delay: timing.delayS(900),
42
- };
43
- const animationDelay = `${timing.delayS()}s`;
44
- const beamStyle = {
45
- animation: timing.prefersReducedMotion ? "none" : `ti-scale-beam ${timing.cycleS}s ease-in-out ${animationDelay} infinite`,
46
- transform: timing.prefersReducedMotion ? "rotate(7deg)" : undefined,
47
- transformBox: "view-box",
48
- transformOrigin: "160px 118px",
49
- } as CSSProperties;
50
- const leftPanStyle = {
51
- animation: timing.prefersReducedMotion ? "none" : `ti-scale-left-pan ${timing.cycleS}s ease-in-out ${animationDelay} infinite`,
52
- transform: timing.prefersReducedMotion ? "translateY(-10px)" : undefined,
53
- } as CSSProperties;
54
- const rightPanStyle = {
55
- animation: timing.prefersReducedMotion ? "none" : `ti-scale-right-pan ${timing.cycleS}s ease-in-out ${animationDelay} infinite`,
56
- transform: timing.prefersReducedMotion ? "translateY(12px)" : undefined,
57
- } as CSSProperties;
58
-
59
- return (
60
- <PictogramSvg alt={alt} viewBox="0 0 320 320">
61
- <style>
62
- {`
63
- @keyframes ti-scale-beam {
64
- 0%, 16%, 100% { transform: rotate(0deg); }
65
- 38%, 82% { transform: rotate(7deg); }
66
- }
67
-
68
- @keyframes ti-scale-left-pan {
69
- 0%, 16%, 100% { transform: translateY(0); }
70
- 38%, 82% { transform: translateY(-10px); }
71
- }
72
-
73
- @keyframes ti-scale-right-pan {
74
- 0%, 16%, 100% { transform: translateY(0); }
75
- 38%, 82% { transform: translateY(12px); }
76
- }
77
- `}
78
- </style>
79
- <g transform="translate(160 166) scale(1.14) translate(-160 -166)">
80
- <Line d="M 160 86 V 246" palette={scalePalette} width={8} />
81
- <Line d="M 124 246 H 196" palette={scalePalette} width={9} />
82
- <g>
83
- <g style={beamStyle}>
84
- <path
85
- d="M 72 118 L 248 118"
86
- fill="none"
87
- stroke={scalePalette.edge}
88
- strokeLinecap="round"
89
- strokeLinejoin="round"
90
- strokeWidth={8}
91
- />
92
- <path
93
- d="M 160 118 L 248 118"
94
- fill="none"
95
- stroke={palette.accent}
96
- strokeLinecap="round"
97
- strokeLinejoin="round"
98
- strokeWidth={8}
99
- />
100
- </g>
101
- <g style={leftPanStyle}>
102
- <path
103
- d="M 78 118 L 42 198 H 128 L 78 118 Z"
104
- fill="none"
105
- stroke={scalePalette.edge}
106
- strokeLinejoin="round"
107
- strokeWidth={6}
108
- />
109
- <circle cx={70} cy={180} fill={scalePalette.node} opacity={0.88} r={15} />
110
- <circle cx={92} cy={190} fill={scalePalette.muted} opacity={0.86} r={11} />
111
- </g>
112
- <g style={rightPanStyle}>
113
- <path
114
- d="M 246 118 L 190 212 H 288 L 246 118 Z"
115
- fill="none"
116
- stroke={palette.accent}
117
- strokeLinejoin="round"
118
- strokeWidth={7}
119
- />
120
- <motion.circle
121
- animate={timing.prefersReducedMotion ? { opacity: 0.78, scale: 1 } : { opacity: [0.42, 0.42, 0.62, 0.82, 0.82], scale: [1, 1, 1.02, 1.04, 1.04] }}
122
- cx={208}
123
- cy={198}
124
- fill={palette.accent}
125
- initial={false}
126
- opacity={0.78}
127
- r={13}
128
- transition={nodeTransition as any}
129
- />
130
- <motion.circle
131
- animate={timing.prefersReducedMotion ? { opacity: 0.9, scale: 1 } : { opacity: [0.5, 0.5, 0.76, 0.95, 0.95], scale: [1, 1, 1.03, 1.07, 1.07] }}
132
- cx={232}
133
- cy={188}
134
- fill={palette.accent}
135
- initial={false}
136
- opacity={0.9}
137
- r={14}
138
- transition={{ ...nodeTransition, delay: timing.delayS(1200) } as any}
139
- />
140
- <motion.circle
141
- animate={timing.prefersReducedMotion ? { opacity: 1, scale: 1 } : { opacity: [0.55, 0.55, 0.84, 1, 1], scale: [1, 1, 1.03, 1.1, 1.1] }}
142
- cx={254}
143
- cy={200}
144
- fill={palette.accent}
145
- initial={false}
146
- opacity={1}
147
- r={16}
148
- transition={{ ...nodeTransition, delay: timing.delayS(1550) } as any}
149
- />
150
- <motion.circle
151
- animate={timing.prefersReducedMotion ? { opacity: 0.82, scale: 1 } : { opacity: [0.38, 0.38, 0.68, 0.88, 0.88], scale: [1, 1, 1.02, 1.06, 1.06] }}
152
- cx={274}
153
- cy={189}
154
- fill={palette.accent}
155
- initial={false}
156
- opacity={0.82}
157
- r={12}
158
- transition={{ ...nodeTransition, delay: timing.delayS(1900) } as any}
159
- />
160
- </g>
161
- </g>
162
- <circle cx={160} cy={118} fill={palette.frame} r={23} stroke={scalePalette.edge} strokeWidth={7} />
163
- </g>
164
- </PictogramSvg>
165
- );
166
- }
167
-
168
- function NewsroomFlowPictogram({ alt, palette, timing }: RegisteredPictogramProps) {
169
- const newsroomPalette = {
170
- ...palette,
171
- edge: "var(--foreground-strong)",
172
- node: "var(--foreground-strong)",
173
- muted: "color-mix(in srgb, var(--foreground-strong) 54%, var(--background) 46%)",
174
- };
175
- const animationDelay = `${timing.delayS()}s`;
176
- const wheelStyle = {
177
- animation: timing.prefersReducedMotion ? "none" : `ti-newsroom-wheel ${timing.cycleS}s ease-in-out ${animationDelay} infinite`,
178
- transform: timing.prefersReducedMotion ? "rotate(7deg)" : undefined,
179
- transformBox: "view-box",
180
- transformOrigin: "160px 70px",
181
- } as CSSProperties;
182
- const dotStyle = {
183
- animation: timing.prefersReducedMotion ? "none" : `ti-newsroom-dot ${timing.cycleS}s linear ${timing.delayS(1200)}s infinite`,
184
- transform: timing.prefersReducedMotion ? "translate(196px, 0)" : undefined,
185
- } as CSSProperties;
186
- const activeLineStyle = {
187
- animation: timing.prefersReducedMotion ? "none" : `ti-newsroom-flow ${timing.cycleS}s linear ${timing.delayS(1200)}s infinite`,
188
- opacity: timing.prefersReducedMotion ? 0.85 : undefined,
189
- strokeDashoffset: timing.prefersReducedMotion ? -224 : undefined,
190
- } as CSSProperties;
191
- const flowCenters = [
192
- [62, 214],
193
- [160, 160],
194
- [258, 214],
195
- [160, 264],
196
- [62, 214],
197
- ] as Array<[number, number]>;
198
- const flowPath = pointsPath(flowCenters);
199
- const states = [
200
- { cx: 62, cy: 214, r: 30, inner: "M 48 214 H 76" },
201
- { cx: 160, cy: 160, r: 32, inner: "M 146 152 H 174 M 146 168 H 174" },
202
- { cx: 258, cy: 214, r: 30, inner: "M 246 204 H 270 M 246 214 H 270 M 246 224 H 270" },
203
- { cx: 160, cy: 264, r: 30, inner: "M 148 256 L 160 268 L 176 250" },
204
- ];
205
-
206
- return (
207
- <PictogramSvg alt={alt} viewBox="0 0 320 320">
208
- <style>
209
- {`
210
- @keyframes ti-newsroom-wheel {
211
- 0%, 16%, 100% { transform: rotate(-5deg); }
212
- 34% { transform: rotate(8deg); }
213
- 54% { transform: rotate(-3deg); }
214
- 72% { transform: rotate(6deg); }
215
- }
216
-
217
- @keyframes ti-newsroom-dot {
218
- 0%, 100% { transform: translate(0, 0); }
219
- 18% { transform: translate(98px, -54px); }
220
- 36% { transform: translate(196px, 0); }
221
- 54% { transform: translate(98px, 50px); }
222
- 72% { transform: translate(0, 0); }
223
- }
224
-
225
- @keyframes ti-newsroom-flow {
226
- 0% { opacity: 0.2; stroke-dashoffset: 0; }
227
- 18% { opacity: 0.95; stroke-dashoffset: -112; }
228
- 36% { opacity: 0.95; stroke-dashoffset: -224; }
229
- 54% { opacity: 0.95; stroke-dashoffset: -334; }
230
- 72%, 100% { opacity: 0.2; stroke-dashoffset: -444; }
231
- }
232
- `}
233
- </style>
234
- <g transform="translate(160 172) scale(1.12) translate(-160 -172)">
235
- <g style={wheelStyle}>
236
- <circle cx={160} cy={70} fill="none" r={44} stroke={newsroomPalette.edge} strokeWidth={8} />
237
- <circle cx={160} cy={70} fill={palette.frame} r={8} stroke={newsroomPalette.edge} strokeWidth={5} />
238
- <Line d="M 160 70 V 28" palette={newsroomPalette} width={5} />
239
- <Line d="M 160 70 L 124 94" palette={newsroomPalette} width={5} />
240
- <Line d="M 160 70 L 196 94" palette={newsroomPalette} width={5} />
241
- </g>
242
- <Line d="M 160 116 V 128" opacity={0.7} palette={newsroomPalette} tone="muted" width={4} />
243
- <Line d={flowPath} opacity={0.42} palette={newsroomPalette} tone="muted" width={5} />
244
- <path
245
- d={flowPath}
246
- fill="none"
247
- stroke={palette.accent}
248
- strokeDasharray="44 440"
249
- strokeLinecap="round"
250
- strokeLinejoin="round"
251
- strokeWidth={5}
252
- style={activeLineStyle}
253
- />
254
- {states.map((state, index) => (
255
- <g key={`${state.cx}-${state.cy}`}>
256
- <circle cx={state.cx} cy={state.cy} fill={palette.frame} r={state.r} stroke={newsroomPalette.edge} strokeWidth={6} />
257
- <Line d={state.inner} opacity={index === 2 ? 0.9 : 0.72} palette={index === 2 ? palette : newsroomPalette} tone={index === 2 ? "accent" : "muted"} width={4} />
258
- <Node palette={index === 2 ? palette : newsroomPalette} tone={index === 2 ? "accent" : "node"} x={state.cx} y={state.cy} />
259
- </g>
260
- ))}
261
- <circle cx={62} cy={214} fill={palette.accent} r={7} style={dotStyle} />
262
- </g>
263
- </PictogramSvg>
264
- );
265
- }
266
-
267
- function AwsDiscoveryPictogram({ alt, palette, timing }: RegisteredPictogramProps) {
268
- const awsPalette = {
269
- ...palette,
270
- edge: "var(--foreground-strong)",
271
- node: "var(--foreground-strong)",
272
- muted: "color-mix(in srgb, var(--foreground-strong) 54%, var(--background) 46%)",
273
- };
274
- const riskStyle = (offsetMs: number, resting = false) =>
275
- ({
276
- animation: timing.prefersReducedMotion ? "none" : `ti-aws-risk ${timing.cycleS}s ease-in-out ${timing.delayS(offsetMs)}s infinite`,
277
- opacity: timing.prefersReducedMotion && resting ? 1 : timing.prefersReducedMotion ? 0.86 : 0.18,
278
- transformBox: "fill-box",
279
- transformOrigin: "center",
280
- }) as CSSProperties;
281
- const buckets = [
282
- { x: 92, y: 74, risk: null },
283
- { x: 160, y: 74, risk: null },
284
- { x: 228, y: 74, risk: "warning", offset: 2200, markerDx: 15, markerDy: -20 },
285
- { x: 92, y: 150, risk: "warning", offset: 4200 },
286
- { x: 160, y: 150, risk: null },
287
- { x: 228, y: 150, risk: null },
288
- { x: 92, y: 226, risk: null },
289
- { x: 160, y: 226, risk: "accent", offset: 6200, markerDx: 24, markerDy: -16, markerR: 20 },
290
- { x: 228, y: 226, risk: "accent", offset: 8200, markerDx: 12, markerDy: -24 },
291
- ];
292
-
293
- return (
294
- <PictogramSvg alt={alt} viewBox="0 0 320 320">
295
- <style>
296
- {`
297
- @keyframes ti-aws-risk {
298
- 0%, 18%, 100% { opacity: 0.18; transform: scale(0.9); }
299
- 26% { opacity: 1; transform: scale(1.14); }
300
- 34%, 78% { opacity: 1; transform: scale(1); }
301
- 88% { opacity: 0.42; transform: scale(0.96); }
302
- }
303
- `}
304
- </style>
305
- <g transform="translate(160 160) scale(1.14) translate(-160 -150)">
306
- {buckets.map((bucket) => {
307
- const stroke = bucket.risk === "accent" ? palette.accent : bucket.risk === "warning" ? palette.warning : awsPalette.edge;
308
- const markerX = bucket.x + (bucket.markerDx ?? 20);
309
- const markerY = bucket.y + (bucket.markerDy ?? -19);
310
- const markerR = bucket.markerR ?? 10;
311
- return (
312
- <g key={`aws-bucket-${bucket.x}-${bucket.y}`}>
313
- <path
314
- d={`M ${bucket.x - 22} ${bucket.y - 16} C ${bucket.x - 22} ${bucket.y - 27}, ${bucket.x + 22} ${bucket.y - 27}, ${bucket.x + 22} ${bucket.y - 16} V ${bucket.y + 18} C ${bucket.x + 22} ${bucket.y + 29}, ${bucket.x - 22} ${bucket.y + 29}, ${bucket.x - 22} ${bucket.y + 18} Z`}
315
- fill={palette.frame}
316
- stroke={awsPalette.edge}
317
- strokeLinejoin="round"
318
- strokeWidth={5}
319
- />
320
- <path
321
- d={`M ${bucket.x - 22} ${bucket.y - 16} C ${bucket.x - 22} ${bucket.y - 5}, ${bucket.x + 22} ${bucket.y - 5}, ${bucket.x + 22} ${bucket.y - 16}`}
322
- fill="none"
323
- opacity={0.72}
324
- stroke={awsPalette.muted}
325
- strokeLinecap="round"
326
- strokeWidth={4}
327
- />
328
- <Node palette={awsPalette} x={bucket.x} y={bucket.y + 2} />
329
- {bucket.risk ? (
330
- <g style={riskStyle(bucket.offset ?? 0, bucket.risk === "accent")}>
331
- <circle cx={markerX} cy={markerY} fill={stroke} r={markerR} />
332
- <path d={`M ${markerX - markerR * 0.8} ${markerY} H ${markerX + markerR * 0.8} M ${markerX} ${markerY - markerR * 0.8} V ${markerY + markerR * 0.8}`} stroke={palette.frame} strokeLinecap="round" strokeWidth={markerR >= 16 ? 4.8 : 3.4} />
333
- </g>
334
- ) : null}
335
- </g>
336
- );
337
- })}
338
- </g>
339
- </PictogramSvg>
340
- );
341
- }
342
-
343
- function AzureBlastRadiusPictogram({ alt, palette, timing }: RegisteredPictogramProps) {
344
- const azurePalette = {
345
- ...palette,
346
- edge: "var(--foreground-strong)",
347
- node: "var(--foreground-strong)",
348
- muted: "color-mix(in srgb, var(--foreground-strong) 54%, var(--background) 46%)",
349
- };
350
- const animationDelay = `${timing.delayS(1000)}s`;
351
- const pathStyle = {
352
- animation: timing.prefersReducedMotion ? "none" : `ti-azure-path ${timing.cycleS}s ease-in-out ${animationDelay} infinite`,
353
- opacity: timing.prefersReducedMotion ? 0.95 : undefined,
354
- strokeDashoffset: timing.prefersReducedMotion ? -270 : undefined,
355
- } as CSSProperties;
356
- const riskStyle = (offsetMs: number) =>
357
- ({
358
- animation: timing.prefersReducedMotion ? "none" : `ti-azure-risk ${timing.cycleS}s ease-in-out ${timing.delayS(offsetMs)}s infinite`,
359
- opacity: timing.prefersReducedMotion ? 0.95 : 0.18,
360
- transformBox: "fill-box",
361
- transformOrigin: "center",
362
- }) as CSSProperties;
363
-
364
- return (
365
- <PictogramSvg alt={alt} viewBox="0 0 320 320">
366
- <style>
367
- {`
368
- @keyframes ti-azure-path {
369
- 0%, 16%, 100% { opacity: 0.18; stroke-dashoffset: 0; }
370
- 34%, 66% { opacity: 0.95; stroke-dashoffset: -270; }
371
- 78% { opacity: 0.28; stroke-dashoffset: -270; }
372
- }
373
-
374
- @keyframes ti-azure-risk {
375
- 0%, 20%, 100% { opacity: 0.18; transform: scale(0.9); }
376
- 32%, 70% { opacity: 1; transform: scale(1.08); }
377
- 82% { opacity: 0.35; transform: scale(1); }
378
- }
379
- `}
380
- </style>
381
- <g transform="translate(160 160) scale(1.1) translate(-160 -160)">
382
- <rect fill={palette.frame} height={214} rx={12} stroke={azurePalette.edge} strokeWidth={7} width={222} x={49} y={54} />
383
- <Line d="M 82 88 H 238 M 82 232 H 238" opacity={0.38} palette={azurePalette} tone="muted" width={4} />
384
- <circle cx={104} cy={112} fill={palette.frame} r={34} stroke={azurePalette.edge} strokeWidth={6} />
385
- <Line d="M 92 118 C 94 102, 114 102, 116 118" palette={azurePalette} tone="muted" width={4} />
386
- <Node palette={azurePalette} x={104} y={104} />
387
- <rect fill={palette.frame} height={52} rx={8} stroke={azurePalette.edge} strokeWidth={6} width={74} x={184} y={86} />
388
- <rect fill={palette.frame} height={52} rx={8} stroke={azurePalette.edge} strokeWidth={6} width={74} x={184} y={154} />
389
- <rect fill={palette.frame} height={40} rx={8} stroke={azurePalette.edge} strokeWidth={6} width={64} x={96} y={196} />
390
- <Line d="M 198 112 H 244 M 198 180 H 244 M 108 216 H 148" opacity={0.72} palette={azurePalette} tone="muted" width={4} />
391
- <Line d={pointsPath([[138, 112], [184, 112], [184, 180], [160, 216]])} opacity={0.34} palette={azurePalette} tone="muted" width={4} />
392
- <path
393
- d={pointsPath([[104, 112], [142, 112], [184, 112], [222, 112], [222, 180], [160, 216]])}
394
- fill="none"
395
- stroke={palette.accent}
396
- strokeDasharray="44 270"
397
- strokeLinecap="round"
398
- strokeLinejoin="round"
399
- strokeWidth={6}
400
- style={pathStyle}
401
- />
402
- <Node palette={azurePalette} x={104} y={112} />
403
- <Node palette={azurePalette} x={222} y={112} />
404
- <Node palette={azurePalette} x={222} y={180} />
405
- <Node palette={azurePalette} x={160} y={216} />
406
- <g style={riskStyle(3400)}>
407
- <circle cx={222} cy={112} fill={palette.warning} r={12} />
408
- <path d="M 213 112 H 231 M 222 103 V 121" stroke={palette.frame} strokeLinecap="round" strokeWidth={4} />
409
- </g>
410
- <g style={riskStyle(6200)}>
411
- <circle cx={160} cy={216} fill={palette.accent} r={18} />
412
- <path d="M 146 216 H 174 M 160 202 V 230" stroke={palette.frame} strokeLinecap="round" strokeWidth={5} />
413
- </g>
414
- </g>
415
- </PictogramSvg>
416
- );
417
- }
418
-
419
- function OpenAIInfraPictogram({ alt, palette, timing }: RegisteredPictogramProps) {
420
- const aiPalette = {
421
- ...palette,
422
- edge: "var(--foreground-strong)",
423
- node: "var(--foreground-strong)",
424
- muted: "color-mix(in srgb, var(--foreground-strong) 54%, var(--background) 46%)",
425
- };
426
- const animationDelay = `${timing.delayS(1400)}s`;
427
- const pathStyle = {
428
- animation: timing.prefersReducedMotion ? "none" : `ti-ai-path ${timing.cycleS}s ease-in-out ${animationDelay} infinite`,
429
- opacity: timing.prefersReducedMotion ? 0.95 : undefined,
430
- strokeDashoffset: timing.prefersReducedMotion ? -280 : undefined,
431
- } as CSSProperties;
432
- const riskStyle = (offsetMs: number) =>
433
- ({
434
- animation: timing.prefersReducedMotion ? "none" : `ti-ai-risk ${timing.cycleS}s ease-in-out ${timing.delayS(offsetMs)}s infinite`,
435
- opacity: timing.prefersReducedMotion ? 0.95 : 0.16,
436
- transformBox: "fill-box",
437
- transformOrigin: "center",
438
- }) as CSSProperties;
439
-
440
- return (
441
- <PictogramSvg alt={alt} viewBox="0 0 320 320">
442
- <style>
443
- {`
444
- @keyframes ti-ai-path {
445
- 0%, 16%, 100% { opacity: 0.16; stroke-dashoffset: 0; }
446
- 34%, 68% { opacity: 0.95; stroke-dashoffset: -280; }
447
- 82% { opacity: 0.28; stroke-dashoffset: -280; }
448
- }
449
-
450
- @keyframes ti-ai-risk {
451
- 0%, 20%, 100% { opacity: 0.16; transform: scale(0.9); }
452
- 30%, 72% { opacity: 1; transform: scale(1.08); }
453
- 84% { opacity: 0.34; transform: scale(1); }
454
- }
455
- `}
456
- </style>
457
- <g transform="translate(160 160) scale(1.1) translate(-160 -160)">
458
- <rect fill={palette.frame} height={72} rx={12} stroke={aiPalette.edge} strokeWidth={7} width={94} x={113} y={44} />
459
- <Line d="M 130 68 H 190 M 130 92 H 190" opacity={0.72} palette={aiPalette} tone="muted" width={4} />
460
- <Node palette={aiPalette} x={160} y={80} />
461
- <rect fill={palette.frame} height={54} rx={9} stroke={aiPalette.edge} strokeWidth={6} width={78} x={44} y={154} />
462
- <rect fill={palette.frame} height={54} rx={9} stroke={aiPalette.edge} strokeWidth={6} width={78} x={198} y={154} />
463
- <rect fill={palette.frame} height={48} rx={9} stroke={aiPalette.edge} strokeWidth={6} width={72} x={124} y={226} />
464
- <Line d={pointsPath([[160, 116], [83, 154], [83, 181], [160, 226]])} opacity={0.32} palette={aiPalette} tone="muted" width={4} />
465
- <Line d={pointsPath([[160, 116], [237, 154], [237, 181], [160, 226]])} opacity={0.32} palette={aiPalette} tone="muted" width={4} />
466
- <path
467
- d={pointsPath([[83, 181], [120, 142], [160, 80], [200, 142], [237, 181], [160, 250]])}
468
- fill="none"
469
- stroke={palette.accent}
470
- strokeDasharray="44 280"
471
- strokeLinecap="round"
472
- strokeLinejoin="round"
473
- strokeWidth={6}
474
- style={pathStyle}
475
- />
476
- <Line d="M 58 181 H 108 M 212 181 H 262 M 140 250 H 180" opacity={0.72} palette={aiPalette} tone="muted" width={4} />
477
- <Node palette={aiPalette} x={83} y={181} />
478
- <Node palette={aiPalette} x={237} y={181} />
479
- <Node palette={aiPalette} x={160} y={250} />
480
- <g style={riskStyle(2600)}>
481
- <circle cx={83} cy={181} fill={palette.warning} r={12} />
482
- <path d="M 74 181 H 92 M 83 172 V 190" stroke={palette.frame} strokeLinecap="round" strokeWidth={4} />
483
- </g>
484
- <g style={riskStyle(5600)}>
485
- <circle cx={237} cy={181} fill={palette.accent} r={14} />
486
- <path d="M 226 181 H 248 M 237 170 V 192" stroke={palette.frame} strokeLinecap="round" strokeWidth={4.5} />
487
- </g>
488
- <g style={riskStyle(8200)}>
489
- <circle cx={160} cy={250} fill={palette.accent} r={18} />
490
- <path d="M 146 250 H 174 M 160 236 V 264" stroke={palette.frame} strokeLinecap="round" strokeWidth={5} />
491
- </g>
492
- </g>
493
- </PictogramSvg>
494
- );
495
- }
496
-
497
- function GamingIsolationPictogram({ alt, palette, timing }: RegisteredPictogramProps) {
498
- const gamingPalette = {
499
- ...palette,
500
- edge: "var(--foreground-strong)",
501
- node: "var(--foreground-strong)",
502
- muted: "color-mix(in srgb, var(--foreground-strong) 54%, var(--background) 46%)",
503
- };
504
- const animationDelay = `${timing.delayS(1200)}s`;
505
- const attemptStyle = {
506
- animation: timing.prefersReducedMotion ? "none" : `ti-gaming-attempt ${timing.cycleS}s ease-in-out ${animationDelay} infinite`,
507
- opacity: timing.prefersReducedMotion ? 0.86 : undefined,
508
- } as CSSProperties;
509
- const barrierStyle = {
510
- animation: timing.prefersReducedMotion ? "none" : `ti-gaming-barrier ${timing.cycleS}s ease-in-out ${animationDelay} infinite`,
511
- } as CSSProperties;
512
- const blockedDotStyle = {
513
- animation: timing.prefersReducedMotion ? "none" : `ti-gaming-blocked-dot ${timing.cycleS}s ease-in-out ${animationDelay} infinite`,
514
- opacity: timing.prefersReducedMotion ? 1 : undefined,
515
- } as CSSProperties;
516
-
517
- return (
518
- <PictogramSvg alt={alt} viewBox="0 0 320 320">
519
- <style>
520
- {`
521
- @keyframes ti-gaming-attempt {
522
- 0%, 16%, 100% { opacity: 0.18; }
523
- 34%, 58% { opacity: 0.95; }
524
- 72% { opacity: 0.3; }
525
- }
526
-
527
- @keyframes ti-gaming-barrier {
528
- 0%, 20%, 100% { opacity: 0.62; }
529
- 38%, 62% { opacity: 1; }
530
- }
531
-
532
- @keyframes ti-gaming-blocked-dot {
533
- 0%, 24%, 100% { opacity: 0; }
534
- 38%, 64% { opacity: 1; }
535
- 76% { opacity: 0.24; }
536
- }
537
- `}
538
- </style>
539
- <g transform="translate(160 160) scale(1.12) translate(-160 -160)">
540
- <rect fill={palette.frame} height={152} rx={10} stroke={gamingPalette.edge} strokeWidth={7} width={92} x={202} y={84} />
541
- <Line d="M 160 58 V 262" opacity={0.72} palette={gamingPalette} tone="muted" width={5} />
542
- <path
543
- d="M 160 58 V 262"
544
- fill="none"
545
- stroke={palette.accent}
546
- strokeDasharray="10 13"
547
- strokeLinecap="round"
548
- strokeWidth={5}
549
- style={barrierStyle}
550
- />
551
-
552
- <g transform="translate(34 67) scale(0.47)">
553
- <path
554
- d="M 24 170 C 31 136, 58 122, 88 128 C 98 130, 101 135, 106 135 H 114 C 119 135, 122 130, 132 128 C 162 122, 189 136, 196 170 L 207 200 C 216 228, 194 246, 170 236 C 154 230, 142 214, 132 198 L 125 187 H 95 L 88 198 C 78 214, 66 230, 50 236 C 26 246, 4 228, 13 200 Z"
555
- fill={palette.frame}
556
- stroke={gamingPalette.edge}
557
- strokeLinejoin="round"
558
- strokeWidth={7}
559
- />
560
- <Line d="M 52 176 H 86 M 69 159 V 193" opacity={0.9} palette={gamingPalette} tone="muted" width={7} />
561
- <circle cx={151} cy={166} fill={gamingPalette.node} r={8} />
562
- <circle cx={173} cy={184} fill={gamingPalette.muted} r={8} />
563
- <circle cx={173} cy={148} fill={gamingPalette.muted} r={8} />
564
- </g>
565
- <Node palette={palette} tone="warning" x={102} y={136} />
566
-
567
- <path
568
- d={pointsPath([
569
- [102, 136],
570
- [124, 150],
571
- [142, 160],
572
- [151, 160],
573
- ])}
574
- fill="none"
575
- stroke={palette.accent}
576
- strokeLinecap="round"
577
- strokeLinejoin="round"
578
- strokeWidth={6}
579
- style={attemptStyle}
580
- />
581
- <g style={blockedDotStyle}>
582
- <Line d="M 151 151 L 169 169 M 169 151 L 151 169" palette={palette} tone="accent" width={5} />
583
- </g>
584
-
585
- <Box fillOpacity={0.04} height={34} palette={gamingPalette} strokeTone="edge" width={50} x={224} y={110} />
586
- <Box fillOpacity={0.04} height={34} palette={gamingPalette} strokeTone="edge" width={50} x={224} y={156} />
587
- <Box fillOpacity={0.04} height={34} palette={gamingPalette} strokeTone="edge" width={50} x={224} y={202} />
588
- <Line d="M 236 127 H 262 M 236 173 H 262 M 236 219 H 262" opacity={0.72} palette={gamingPalette} tone="muted" width={4} />
589
- <Node palette={gamingPalette} x={249} y={127} />
590
- <Node palette={gamingPalette} x={249} y={173} />
591
- <Node palette={gamingPalette} x={249} y={219} />
592
- </g>
593
- </PictogramSvg>
594
- );
595
- }
596
-
597
- export const THREAT_INTELLIGENCE_PICTOGRAM_REGISTRY: Record<ThreatIntelligencePictogramSlug, PictogramRegistryEntry> = {
598
- "the-balance-of-power-is-shifting": { aspectRatio: 1, render: BalanceShiftPictogram },
599
- "how-our-newsroom-learns": { aspectRatio: 1, render: NewsroomFlowPictogram },
600
- "audit-aws-exposure-before-attackers-do": { aspectRatio: 1, render: AwsDiscoveryPictogram },
601
- "audit-azure-blast-radius-before-attackers-do": { aspectRatio: 1, render: AzureBlastRadiusPictogram },
602
- "treat-openai-accounts-like-production-infrastructure": { aspectRatio: 1, render: OpenAIInfraPictogram },
603
- "how-to-play-games-securely": { aspectRatio: 1, render: GamingIsolationPictogram },
604
- };
605
-
606
- export function getPictogramRegistryEntry(slug: ThreatIntelligencePictogramSlug): PictogramRegistryEntry {
607
- return THREAT_INTELLIGENCE_PICTOGRAM_REGISTRY[slug];
608
- }
@@ -1,82 +0,0 @@
1
- "use client";
2
-
3
- import Image from "next/image";
4
- import type { ArticleImageLayout, ArticleImageThemeVariants } from "../../../lib/articles";
5
- import { shouldBypassImageOptimization } from "../../../lib/image-url";
6
- import { resolveThemedImageSrc } from "../../../lib/themed-image";
7
- import {
8
- getThreatIntelligencePictogramPhaseOffset,
9
- isThreatIntelligencePictogramSlug,
10
- } from "./registry";
11
- import { useResolvedPapyrusTheme } from "../../../components/use-resolved-papyrus-theme";
12
- import { THREAT_INTELLIGENCE_PICTOGRAM_REGISTRY } from "./art";
13
- import { PICTOGRAM_PALETTE, PictogramFrame, usePictogramMotion } from "./system";
14
-
15
- type PictogramFigureProps = {
16
- alt: string;
17
- caption?: string;
18
- credit: string;
19
- figureClassName: string;
20
- height: number;
21
- layout?: ArticleImageLayout;
22
- priority?: boolean;
23
- sizes: string;
24
- slug: string;
25
- src?: string;
26
- themeVariants?: ArticleImageThemeVariants;
27
- width: number;
28
- };
29
-
30
- export function PictogramFigure({
31
- alt,
32
- caption,
33
- credit,
34
- figureClassName,
35
- height,
36
- layout,
37
- priority = false,
38
- sizes,
39
- slug,
40
- src = "",
41
- themeVariants,
42
- width,
43
- }: PictogramFigureProps) {
44
- const resolvedTheme = useResolvedPapyrusTheme();
45
- const themedImageSrc = resolveThemedImageSrc(src, themeVariants, resolvedTheme);
46
- const registeredSlug = isThreatIntelligencePictogramSlug(slug) ? slug : null;
47
- const motionState = usePictogramMotion(registeredSlug ? getThreatIntelligencePictogramPhaseOffset(registeredSlug) : 0);
48
-
49
- if (!registeredSlug) {
50
- if (!themedImageSrc) {
51
- return null;
52
- }
53
- return (
54
- <figure className={figureClassName}>
55
- <Image
56
- src={themedImageSrc}
57
- alt={alt}
58
- width={width}
59
- height={height}
60
- sizes={sizes}
61
- priority={priority}
62
- unoptimized={shouldBypassImageOptimization(themedImageSrc)}
63
- />
64
- <figcaption>{caption ?? credit}</figcaption>
65
- </figure>
66
- );
67
- }
68
-
69
- const entry = THREAT_INTELLIGENCE_PICTOGRAM_REGISTRY[registeredSlug];
70
- const palette = PICTOGRAM_PALETTE;
71
- const aspectRatio = layout?.aspectRatio ?? entry.aspectRatio ?? 1;
72
- const Pictogram = entry.render;
73
-
74
- return (
75
- <figure className={`${figureClassName} pictogram-figure`} data-pictogram-slug={slug}>
76
- <PictogramFrame aspectRatio={aspectRatio}>
77
- <Pictogram alt={alt} palette={palette} timing={motionState} />
78
- </PictogramFrame>
79
- <figcaption>{caption ?? credit}</figcaption>
80
- </figure>
81
- );
82
- }