Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/components/mdx/A.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
interface Props {
label?: string;
quality?: 'strong' | 'partial' | 'weak';
}

const { label = 'Candidate', quality } = Astro.props;

const qualityConfig: Record<string, { icon: string; text: string; color: string }> = {
strong: { icon: '✓', text: 'strong answer', color: 'var(--iv-quality-strong)' },
partial: { icon: '◐', text: 'partial answer', color: 'var(--iv-quality-partial)' },
weak: { icon: '✗', text: 'weak answer', color: 'var(--iv-quality-weak)' },
};

const q = quality ? qualityConfig[quality] : null;
---

<div class="iv-turn iv-turn-a">
<div class="iv-turn-meta iv-turn-meta-a">
{q && (
<span class="iv-quality-badge" style={`color: ${q.color}; border-color: color-mix(in oklch, ${q.color} 35%, transparent); background: color-mix(in oklch, ${q.color} 10%, transparent);`}>
<span aria-hidden="true">{q.icon}</span>
{q.text}
</span>
)}
<span class="iv-speaker iv-speaker-a">
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="8" r="5"/>
<path d="M20 21a8 8 0 1 0-16 0"/>
</svg>
{label}
</span>
</div>
<div class="iv-bubble iv-bubble-a">
<slot />
</div>
</div>

<style is:global>
:root {
--iv-quality-strong: oklch(50% 0.16 155);
--iv-quality-partial: oklch(52% 0.16 75);
--iv-quality-weak: oklch(50% 0.18 25);
}

[data-theme="dark"] {
--iv-quality-strong: oklch(72% 0.14 155);
--iv-quality-partial: oklch(74% 0.14 75);
--iv-quality-weak: oklch(72% 0.16 25);
}

.iv-turn-meta-a {
flex-direction: row-reverse;
}

.iv-speaker-a {
color: var(--theme-accent, oklch(55% 0.18 260));
}

.iv-bubble-a {
padding: 0.6rem 0.875rem;
background: color-mix(in oklch, var(--theme-accent, oklch(55% 0.18 260)) 8%, var(--theme-bg));
border: 1px solid color-mix(in oklch, var(--theme-accent, oklch(55% 0.18 260)) 25%, transparent);
border-radius: 8px 0 8px 8px;
max-width: 88%;
align-self: flex-end;
}

.iv-quality-badge {
font-size: 0.67rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
padding: 0.2em 0.55em;
border-radius: 4px;
border: 1px solid;
display: inline-flex;
align-items: center;
gap: 0.3em;
}
</style>
240 changes: 240 additions & 0 deletions src/components/mdx/AttackSurface.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
---
interface Props {
title?: string;
caption?: string;
}

const { title, caption } = Astro.props;

const layers = [
{
id: 'dom',
label: 'Layer 1 — DOM & Browser',
sublabel: 'Injection, execution context, browser trust model',
posts: ['Post 1 · XSS Anatomy', 'Post 2 · Content Security Policy'],
color: 'red',
icon: '🌐',
},
{
id: 'network',
label: 'Layer 2 — Network & Origins',
sublabel: 'Same-Origin Policy, cross-origin requests, CORS',
posts: ['Post 3 · CORS Deep Dive'],
color: 'orange',
icon: '🔀',
},
{
id: 'auth',
label: 'Layer 3 — Auth & Cookies',
sublabel: 'Session cookies, cross-site requests, state mutations',
posts: ['Post 4 · CSRF & SameSite'],
color: 'amber',
icon: '🍪',
},
{
id: 'supply',
label: 'Layer 4 — Supply Chain & Third-Party Trust',
sublabel: 'npm dependency tree, CDN scripts, subresource integrity',
posts: ['Post 5 · Supply Chain Attacks', 'Post 6 · Subresource Integrity'],
color: 'purple',
icon: '📦',
},
] as const;

type LayerColor = 'red' | 'orange' | 'amber' | 'purple';

const COLOR_VARS: Record<LayerColor, { bg: string; border: string; badge: string; badgeText: string }> = {
red: { bg: 'rgba(239,68,68,0.07)', border: 'rgba(239,68,68,0.35)', badge: 'rgba(239,68,68,0.15)', badgeText: '#f87171' },
orange: { bg: 'rgba(249,115,22,0.07)', border: 'rgba(249,115,22,0.35)', badge: 'rgba(249,115,22,0.15)', badgeText: '#fb923c' },
amber: { bg: 'rgba(245,158,11,0.07)', border: 'rgba(245,158,11,0.35)', badge: 'rgba(245,158,11,0.15)', badgeText: '#fbbf24' },
purple: { bg: 'rgba(168,85,247,0.07)', border: 'rgba(168,85,247,0.35)', badge: 'rgba(168,85,247,0.15)', badgeText: '#c084fc' },
};
---

<div class="as-wrap">
{title && <div class="as-header">{title}</div>}
<div class="as-body">
<div class="as-app-label">DevForum</div>
<div class="as-layers">
{layers.map((layer) => {
const c = COLOR_VARS[layer.color];
return (
<div
class="as-layer"
style={`background:${c.bg};border-color:${c.border};`}
>
<div class="as-layer-left">
<span class="as-icon" aria-hidden="true">{layer.icon}</span>
<div class="as-layer-text">
<div class="as-layer-label">{layer.label}</div>
<div class="as-layer-sub">{layer.sublabel}</div>
</div>
</div>
<div class="as-badges">
{layer.posts.map(post => (
<span
class="as-badge"
style={`background:${c.badge};color:${c.badgeText};`}
>
{post}
</span>
))}
</div>
</div>
);
})}
</div>
<div class="as-footer-label">
<span class="as-footer-dot as-dot-red" /> Browser Attacks &nbsp;·&nbsp;
<span class="as-footer-dot as-dot-orange" /> Network &nbsp;·&nbsp;
<span class="as-footer-dot as-dot-amber" /> Auth &nbsp;·&nbsp;
<span class="as-footer-dot as-dot-purple" /> Supply Chain
</div>
</div>
{caption && <div class="as-caption">{caption}</div>}
</div>

<style>
.as-wrap {
margin: 2rem 0;
border: 1px solid var(--theme-line);
border-radius: 12px;
overflow: hidden;
}

.as-header {
padding: 0.625rem 1.25rem;
background: var(--theme-bg-raised);
border-bottom: 1px solid var(--theme-line);
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--theme-fg-faint);
}

.as-body {
background: var(--theme-bg-inset);
padding: 1.25rem;
}

.as-app-label {
text-align: center;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--theme-fg-ghost);
margin-bottom: 0.75rem;
}

.as-layers {
display: flex;
flex-direction: column;
gap: 0.5rem;
}

.as-layer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.75rem 1rem;
border: 1px solid;
border-radius: 8px;
flex-wrap: wrap;
row-gap: 0.5rem;
}

.as-layer-left {
display: flex;
align-items: center;
gap: 0.75rem;
min-width: 0;
}

.as-icon {
font-size: 1.25rem;
flex-shrink: 0;
line-height: 1;
}

.as-layer-text {
min-width: 0;
}

.as-layer-label {
font-size: 0.875rem;
font-weight: 600;
color: var(--theme-fg-default);
white-space: nowrap;
}

.as-layer-sub {
font-size: 0.7rem;
color: var(--theme-fg-faint);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
margin-top: 0.15rem;
white-space: nowrap;
}

.as-badges {
display: flex;
flex-wrap: wrap;
gap: 0.375rem;
flex-shrink: 0;
}

.as-badge {
font-size: 0.65rem;
font-weight: 600;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
padding: 0.2rem 0.5rem;
border-radius: 4px;
white-space: nowrap;
}

.as-footer-label {
margin-top: 0.875rem;
text-align: center;
font-size: 0.65rem;
color: var(--theme-fg-ghost);
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
flex-wrap: wrap;
}

.as-footer-dot {
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
}

.as-dot-red { background: #ef4444; }
.as-dot-orange { background: #f97316; }
.as-dot-amber { background: #f59e0b; }
.as-dot-purple { background: #a855f7; }

.as-caption {
padding: 0.5rem 1.25rem;
font-size: 0.75rem;
color: var(--theme-fg-faint);
font-style: italic;
text-align: center;
border-top: 1px solid var(--theme-line);
background: var(--theme-bg-raised);
}

@media (max-width: 600px) {
.as-layer-sub {
white-space: normal;
}
.as-layer-label {
white-space: normal;
}
}
</style>
38 changes: 33 additions & 5 deletions src/components/mdx/BrowserSupport.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,36 @@ type Status = 'full' | 'partial' | 'flag' | 'none';
type Browser = 'chrome' | 'firefox' | 'safari' | 'edge' | 'node';

interface BrowserEntry {
browser: Browser;
browser?: Browser;
name?: string;
version?: string;
status: Status;
status?: Status;
supported?: boolean;
note?: string;
}

interface Props {
feature: string;
data: BrowserEntry[];
data?: BrowserEntry[];
browsers?: BrowserEntry[];
note?: string;
}

const { feature, data } = Astro.props;
const { feature, data, browsers, note: topLevelNote } = Astro.props;

const nameToKey: Record<string, Browser> = {
chrome: 'chrome', firefox: 'firefox', safari: 'safari', edge: 'edge',
'node.js': 'node', node: 'node',
};

const rawEntries = data ?? browsers ?? [];
const entries = rawEntries.map((e) => {
const browserKey: Browser =
e.browser ?? nameToKey[(e.name ?? '').toLowerCase()] ?? 'chrome';
const status: Status =
e.status ?? (e.supported === false ? 'none' : 'full');
return { browser: browserKey, version: e.version, status, note: e.note };
});

const browserMeta: Record<Browser, { name: string; icon: string }> = {
chrome: {
Expand Down Expand Up @@ -50,7 +68,7 @@ const statusMeta: Record<Status, { symbol: string; label: string; cls: string }>
<div class="bs-block">
<div class="bs-header">{feature}</div>
<div class="bs-grid">
{data.map((entry) => {
{entries.map((entry) => {
const bm = browserMeta[entry.browser];
const sm = statusMeta[entry.status];
return (
Expand Down Expand Up @@ -88,6 +106,7 @@ const statusMeta: Record<Status, { symbol: string; label: string; cls: string }>
<span><span class="bs-flag">⚑</span> Flag</span>
<span><span class="bs-none">✗</span> None</span>
</div>
{topLevelNote && <div class="bs-top-note">{topLevelNote}</div>}
</div>

<style>
Expand Down Expand Up @@ -177,4 +196,13 @@ const statusMeta: Record<Status, { symbol: string; label: string; cls: string }>
align-items: center;
gap: 0.3rem;
}

.bs-top-note {
padding: 0.625rem 1.25rem;
border-top: 1px solid var(--theme-line);
font-size: 0.75rem;
color: var(--theme-fg-faint);
line-height: 1.5;
background: var(--theme-bg-raised);
}
</style>
Loading
Loading