Initial commit

This commit is contained in:
c4ch3c4d3
2026-03-22 16:17:20 -06:00
commit 3bafa35460
55 changed files with 1885894 additions and 0 deletions
+526
View File
@@ -0,0 +1,526 @@
import { notFound } from "next/navigation";
import { micromark } from "micromark";
import { LabContent } from "~/components/labs/LabContent";
import { getLabDocument, getLabSummaries } from "~/lib/labs";
type ObjectiveStyle = "divider" | "cards" | "rail";
const objectiveStyles = new Set<ObjectiveStyle>(["divider", "cards", "rail"]);
type StepStyle = "underline" | "pills" | "blocks";
const stepStyles = new Set<StepStyle>(["underline", "pills", "blocks"]);
type BreakoutStyle = "none" | "panel" | "workflow" | "command-pills" | "instruction-rails";
const breakoutStyles = new Set<BreakoutStyle>([
"none",
"panel",
"workflow",
"command-pills",
"instruction-rails",
]);
function normalizeObjectiveStyle(style: unknown): ObjectiveStyle | null {
if (typeof style !== "string") return null;
const normalized = style.trim().toLowerCase() as ObjectiveStyle;
return objectiveStyles.has(normalized) ? normalized : null;
}
function normalizeStepStyle(style: unknown): StepStyle | null {
if (typeof style !== "string") return null;
const normalized = style.trim().toLowerCase() as StepStyle;
return stepStyles.has(normalized) ? normalized : null;
}
function normalizeBreakoutStyle(style: unknown): BreakoutStyle | null {
if (typeof style !== "string") return null;
const normalized = style.trim().toLowerCase() as BreakoutStyle;
return breakoutStyles.has(normalized) ? normalized : null;
}
function extractObjectiveStyleDirective(markdown: string) {
const styleDirectivePattern = /<!--\s*objective-style:\s*([a-z-]+)\s*-->/i;
const match = styleDirectivePattern.exec(markdown);
const style = normalizeObjectiveStyle(match?.[1]);
if (!match) {
return { markdown, style };
}
const cleanedMarkdown = markdown.replace(styleDirectivePattern, "").trimStart();
return { markdown: cleanedMarkdown, style };
}
function extractBreakoutStyleDirective(markdown: string) {
const styleDirectivePattern = /<!--\s*breakout-style:\s*([a-z-]+)\s*-->/i;
const match = styleDirectivePattern.exec(markdown);
const style = normalizeBreakoutStyle(match?.[1]);
if (!match) {
return { markdown, style };
}
const cleanedMarkdown = markdown.replace(styleDirectivePattern, "").trimStart();
return { markdown: cleanedMarkdown, style };
}
function extractStepStyleDirective(markdown: string) {
const styleDirectivePattern = /<!--\s*step-style:\s*([a-z-]+)\s*-->/i;
const match = styleDirectivePattern.exec(markdown);
const style = normalizeStepStyle(match?.[1]);
if (!match) {
return { markdown, style };
}
const cleanedMarkdown = markdown.replace(styleDirectivePattern, "").trimStart();
return { markdown: cleanedMarkdown, style };
}
function extractPlainText(htmlText: string) {
return htmlText
.replace(/<[^>]+>/g, "")
.replace(/&nbsp;/g, " ")
.trim();
}
function isObjectiveHeading(headingHtml: string) {
const plainText = extractPlainText(headingHtml);
return /^objective\b/i.test(plainText);
}
function stripObjectiveDividers(html: string) {
return html.replace(
/<hr\s*\/?>\s*(?=<h2(?:\s+[^>]*)?>\s*Objective\b)/gi,
"",
);
}
function transformOutsideDetails(
html: string,
transform: (safeHtml: string) => string,
) {
const detailsPattern = /<details(?:\s+[^>]*)?>[\s\S]*?<\/details>/gi;
const detailsBlocks: string[] = [];
const maskedHtml = html.replace(detailsPattern, (detailsBlock) => {
const token = `__DETAILS_BLOCK_${detailsBlocks.length}__`;
detailsBlocks.push(detailsBlock);
return token;
});
const transformedHtml = transform(maskedHtml);
return transformedHtml.replace(/__DETAILS_BLOCK_(\d+)__/g, (_, indexText: string) => {
const index = Number(indexText);
return detailsBlocks[index] ?? "";
});
}
function segmentObjectiveSections(html: string) {
const headingPattern = /<h2(?:\s+[^>]*)?>([\s\S]*?)<\/h2>/gi;
const headings: Array<{ index: number; text: string }> = [];
let match = headingPattern.exec(html);
while (match) {
headings.push({ index: match.index, text: match[1] ?? "" });
match = headingPattern.exec(html);
}
if (headings.length === 0) return html;
let output = "";
let cursor = 0;
for (let i = 0; i < headings.length; i++) {
const heading = headings[i];
if (!isObjectiveHeading(heading.text)) continue;
const nextObjectiveHeading = headings.slice(i + 1).find((nextHeading) => {
return isObjectiveHeading(nextHeading.text);
});
const nextHeadingIndex = nextObjectiveHeading?.index ?? html.length;
output += html.slice(cursor, heading.index);
output += '<section class="objective-segment">';
output += html.slice(heading.index, nextHeadingIndex);
output += "</section>";
cursor = nextHeadingIndex;
}
output += html.slice(cursor);
return output;
}
function isStepHeading(headingHtml: string) {
const plainText = extractPlainText(headingHtml);
if (!plainText) return false;
return (
/^step\b/i.test(plainText) ||
/^\d+(?:\.\d+)*(?:[.):])?\s+/.test(plainText) ||
/\b(explore|execute|checkpoint|review)\b/i.test(plainText)
);
}
function getStepMode(headingHtml: string) {
const plainText = extractPlainText(headingHtml).toLowerCase();
if (plainText.includes("execute")) return "execute";
if (plainText.includes("explore")) return "explore";
if (plainText.includes("checkpoint")) return "checkpoint";
if (plainText.includes("review")) return "review";
return null;
}
function looksLikeCommandBlock(codeHtml: string) {
const codeText = codeHtml
.replace(/<[^>]+>/g, "")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&amp;/g, "&")
.replace(/&#39;/g, "'")
.replace(/&quot;/g, '"');
return (
/(^|\n)\s*(\$|sudo\s|git\s|python3?\s|pip\s|npm\s|pnpm\s|yarn\s|llama-|ollama\s|curl\s|wget\s|apt\s|cd\s|ls\s|cat\s|cp\s|mv\s|chmod\s|make\s)/i.test(
codeText,
) || /--[a-z0-9-]+/i.test(codeText)
);
}
function commandLinesToHtml(codeHtml: string) {
const lines = codeHtml
.split("\n")
.map((line) => line.trimEnd())
.filter((line) => line.length > 0);
if (lines.length === 0) {
return `<span class="cmd-pill">${codeHtml}</span>`;
}
return lines.map((line) => `<span class="cmd-pill">${line}</span>`).join("\n");
}
function markExplicitInstructionElements(
html: string,
options?: {
commandPills?: boolean;
},
) {
const renderCommandPills = options?.commandPills ?? false;
const imperativeLead =
/^(?:\d+\.\s*)?(?:open|go to|navigate to|click|select|run|enter|type|copy|paste|create|clone|convert|inspect|execute|use|download|install|review|confirm|kill|rerun|quit|start|stop)\b/i;
let markedHtml = html.replace(
/<pre([^>]*)>\s*<code([^>]*)>([\s\S]*?)<\/code>\s*<\/pre>/gi,
(fullMatch, rawPreAttrs: string, rawCodeAttrs: string, codeHtml: string) => {
if (!looksLikeCommandBlock(codeHtml)) return fullMatch;
const preAttrs = addClassAttribute(rawPreAttrs.trim(), "explicit-command-block");
const codeAttrs = addClassAttribute(rawCodeAttrs.trim(), "explicit-command");
const commandContent = renderCommandPills ? commandLinesToHtml(codeHtml) : codeHtml;
return `<pre${preAttrs ? ` ${preAttrs}` : ""}><code${codeAttrs ? ` ${codeAttrs}` : ""}>${commandContent}</code></pre>`;
},
);
markedHtml = markedHtml.replace(
/<(p|li)([^>]*)>([\s\S]*?)<\/\1>/gi,
(fullMatch, tagName: string, rawAttrs: string, innerHtml: string) => {
const plainText = extractPlainText(innerHtml);
if (!imperativeLead.test(plainText)) return fullMatch;
const attrs = addClassAttribute(rawAttrs.trim(), "explicit-instruction");
return `<${tagName}${attrs ? ` ${attrs}` : ""}>${innerHtml}</${tagName}>`;
},
);
return markedHtml;
}
function classifyStepKind(sectionHtml: string, mode: string | null) {
const hasExplicitInstruction = /class="[^"]*\bexplicit-instruction\b[^"]*"/i.test(sectionHtml);
const hasCommandBlock = /class="[^"]*\bexplicit-command-block\b[^"]*"/i.test(sectionHtml);
const hasOrderedList = /<ol>/i.test(sectionHtml);
if (mode === "execute" || mode === "checkpoint") return "instruction";
if (mode === "explore" || mode === "review") {
return hasCommandBlock ? "instruction" : "explanation";
}
if (hasCommandBlock || hasExplicitInstruction) return "instruction";
if (hasOrderedList) return "mixed";
return "explanation";
}
function findObjectiveSegmentOpenStart(html: string, headingIndex: number) {
const lookbackStart = Math.max(0, headingIndex - 120);
const beforeHeading = html.slice(lookbackStart, headingIndex);
const openTagMatch = /<section class="objective-segment">\s*$/i.exec(beforeHeading);
if (!openTagMatch) return headingIndex;
return headingIndex - openTagMatch[0].length;
}
function segmentStepSections(html: string) {
const headingPattern = /<h([2-4])([^>]*)>([\s\S]*?)<\/h\1>/gi;
const headings: Array<{
index: number;
level: number;
attrs: string;
text: string;
isStep: boolean;
}> = [];
let match = headingPattern.exec(html);
while (match) {
const level = Number(match[1] ?? "0");
const attrs = match[2] ?? "";
const text = match[3] ?? "";
const hasStepClass = /\bclass\s*=\s*"[^"]*\blab-step-title\b/i.test(attrs);
const isObjectiveLevelTwo = level === 2 && isObjectiveHeading(text);
const supportsStepSegmentation = level >= 3 || (level === 2 && !isObjectiveLevelTwo);
headings.push({
index: match.index,
level,
attrs,
text,
isStep: supportsStepSegmentation && (hasStepClass || isStepHeading(text)),
});
match = headingPattern.exec(html);
}
if (headings.length === 0) return html;
let output = "";
let cursor = 0;
for (let i = 0; i < headings.length; i++) {
const heading = headings[i];
if (!heading.isStep) continue;
let nextIndex = html.length;
for (let j = i + 1; j < headings.length; j++) {
const nextHeading = headings[j];
if (nextHeading.level === 2 || nextHeading.isStep) {
nextIndex = findObjectiveSegmentOpenStart(html, nextHeading.index);
break;
}
}
const sectionHtml = html.slice(heading.index, nextIndex);
const modeFromAttrs = /data-step-mode\s*=\s*"([^"]+)"/i.exec(heading.attrs)?.[1] ?? null;
const mode = modeFromAttrs ?? getStepMode(heading.text);
const kind = classifyStepKind(sectionHtml, mode);
const modeAttribute = mode ? ` data-step-mode="${mode}"` : "";
output += html.slice(cursor, heading.index);
output += `<section class="step-segment" data-step-kind="${kind}"${modeAttribute}>${sectionHtml}</section>`;
cursor = nextIndex;
}
output += html.slice(cursor);
return output;
}
function addClassAttribute(attrs: string, className: string) {
const classPattern = /class\s*=\s*"([^"]*)"/i;
if (classPattern.test(attrs)) {
return attrs.replace(classPattern, (_, classList: string) => {
const classes = classList.split(/\s+/).filter(Boolean);
if (!classes.includes(className)) classes.push(className);
return `class="${classes.join(" ")}"`;
});
}
return `${attrs} class="${className}"`.trim();
}
function addDataAttribute(attrs: string, name: string, value: string) {
const attrPattern = new RegExp(`\\b${name}\\s*=`, "i");
if (attrPattern.test(attrs)) return attrs;
return `${attrs} ${name}="${value}"`.trim();
}
function stripStepOrdinalPrefix(headingHtml: string) {
return headingHtml.replace(/^(\s*)(?:\d+(?:\.\d+)*(?:[.):])?\s+)/, "$1");
}
function annotateStepHeadings(
html: string,
options?: {
stripOrdinals?: boolean;
},
) {
const stripOrdinals = options?.stripOrdinals ?? false;
return html.replace(
/<h([2-4])([^>]*)>([\s\S]*?)<\/h\1>/gi,
(fullMatch, level: string, rawAttrs: string, headingHtml: string) => {
const numericLevel = Number(level);
if (numericLevel === 2 && isObjectiveHeading(headingHtml)) return fullMatch;
if (!isStepHeading(headingHtml)) return fullMatch;
let attrs = rawAttrs.trim();
attrs = addClassAttribute(attrs, "lab-step-title");
const mode = getStepMode(headingHtml);
if (mode) {
attrs = addDataAttribute(attrs, "data-step-mode", mode);
}
const displayHeadingHtml = stripOrdinals
? stripStepOrdinalPrefix(headingHtml)
: headingHtml;
return `<h${level}${attrs ? ` ${attrs}` : ""}>${displayHeadingHtml}</h${level}>`;
},
);
}
function splitTableRow(line: string) {
let row = line.trim();
if (row.startsWith("|")) row = row.slice(1);
if (row.endsWith("|")) row = row.slice(0, -1);
return row.split("|").map((cell) => cell.trim());
}
function isTableDivider(line: string) {
const cells = splitTableRow(line);
return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
}
function isTableRow(line: string) {
if (!line.includes("|")) return false;
return splitTableRow(line).length > 1;
}
function tableAlignment(dividerCell: string) {
if (/^:-+:$/.test(dividerCell)) return ' style="text-align:center;"';
if (/^-+:$/.test(dividerCell)) return ' style="text-align:right;"';
return "";
}
function renderInlineMarkdown(markdown: string) {
const html = micromark(markdown, { allowDangerousHtml: false }).trim();
if (html.startsWith("<p>") && html.endsWith("</p>")) {
return html.slice(3, -4);
}
return html;
}
function convertGfmTables(markdown: string) {
const lines = markdown.split("\n");
const out: string[] = [];
let inFence = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i] ?? "";
const fenceMatch = /^(```|~~~)/.exec(line.trim());
if (fenceMatch) {
inFence = !inFence;
out.push(line);
continue;
}
if (inFence) {
out.push(line);
continue;
}
const next = lines[i + 1];
if (next && isTableRow(line) && isTableDivider(next)) {
const header = splitTableRow(line);
const divider = splitTableRow(next);
const rows: string[][] = [];
let j = i + 2;
while (j < lines.length && isTableRow(lines[j] ?? "") && !isTableDivider(lines[j] ?? "")) {
rows.push(splitTableRow(lines[j]!));
j++;
}
const alignAttrs = header.map((_, idx) => tableAlignment(divider[idx] ?? ""));
const headHtml = header
.map((cell, idx) => `<th${alignAttrs[idx] ?? ""}>${renderInlineMarkdown(cell)}</th>`)
.join("");
const bodyHtml = rows
.map((row) => {
const cells = header.map((_, idx) => renderInlineMarkdown(row[idx] ?? ""));
return `<tr>${cells.map((cell, idx) => `<td${alignAttrs[idx] ?? ""}>${cell}</td>`).join("")}</tr>`;
})
.join("");
out.push("<table>");
out.push(`<thead><tr>${headHtml}</tr></thead>`);
out.push(`<tbody>${bodyHtml}</tbody>`);
out.push("</table>");
i = j - 1;
continue;
}
out.push(line);
}
return out.join("\n");
}
function markdownToHtml(markdown: string) {
return micromark(convertGfmTables(markdown), { allowDangerousHtml: true });
}
export async function generateStaticParams() {
return getLabSummaries().map((lab) => ({ slug: lab.slug }));
}
export default async function LabPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const lab = getLabDocument(slug);
if (!lab) {
return notFound();
}
const { content, data } = lab;
const { markdown: markdownWithoutObjectiveDirective, style: objectiveDirectiveStyle } =
extractObjectiveStyleDirective(content);
const { markdown: markdownWithoutStepDirective, style: stepDirectiveStyle } =
extractStepStyleDirective(markdownWithoutObjectiveDirective);
const { markdown, style: breakoutDirectiveStyle } = extractBreakoutStyleDirective(
markdownWithoutStepDirective,
);
const styleConfig = data as {
objectiveStyle?: unknown;
stepStyle?: unknown;
breakoutStyle?: unknown;
};
const objectiveStyle =
normalizeObjectiveStyle(styleConfig.objectiveStyle) ?? objectiveDirectiveStyle ?? "divider";
const stepStyle = normalizeStepStyle(styleConfig.stepStyle) ?? stepDirectiveStyle ?? "underline";
const breakoutStyle =
normalizeBreakoutStyle(styleConfig.breakoutStyle) ?? breakoutDirectiveStyle ?? "none";
const objectiveSegmentedHtml = segmentObjectiveSections(
stripObjectiveDividers(markdownToHtml(markdown)),
);
const baseHtml = transformOutsideDetails(objectiveSegmentedHtml, (safeHtml) =>
annotateStepHeadings(safeHtml, {
stripOrdinals: breakoutStyle === "instruction-rails",
}),
);
const htmlContent =
breakoutStyle === "none"
? baseHtml
: transformOutsideDetails(baseHtml, (safeHtml) =>
segmentStepSections(markExplicitInstructionElements(safeHtml, {
commandPills: breakoutStyle === "command-pills",
})),
);
return (
<main className="mx-auto w-full max-w-5xl px-6 py-10">
<div className="mx-auto max-w-4xl rounded-lg border border-[#f6d5a5] bg-white p-6 md:p-8">
<h1 className="mb-6 text-3xl font-bold text-[#004E78]">{lab.title}</h1>
<LabContent
className={`lab-content max-w-none objective-style-${objectiveStyle} step-style-${stepStyle} breakout-style-${breakoutStyle}`}
html={htmlContent}
/>
</div>
</main>
);
}
+29
View File
@@ -0,0 +1,29 @@
import Link from "next/link";
import { getLabSummaries } from "~/lib/labs";
export default function LabsIndex() {
const labs = getLabSummaries();
return (
<main className="mx-auto w-full max-w-5xl px-6 py-10">
<div className="mb-6">
<h1 className="text-3xl font-bold text-[#004E78]">Open Security Labs</h1>
<p className="mt-2 text-slate-600">Browse converted markdown and MDX lab content.</p>
</div>
<div className="grid gap-4">
{labs.map((lab) => (
<Link
key={lab.slug}
href={`/labs/${lab.slug}`}
className="block rounded-lg border border-slate-200 bg-white p-6 transition hover:border-[#F89C27] hover:shadow-sm"
>
<h2 className="text-xl font-semibold text-[#004E78]">{lab.title}</h2>
{lab.description ? <p className="mt-2 text-slate-600">{lab.description}</p> : null}
</Link>
))}
</div>
</main>
);
}
+23
View File
@@ -0,0 +1,23 @@
import "~/styles/globals.css";
import type { Metadata } from "next";
import type { ReactNode } from "react";
import { SiteHeader } from "~/components/SiteHeader";
export const metadata: Metadata = {
title: "Open Security Labs",
description: "Open Security lab content and notebook conversions",
icons: [{ rel: "icon", url: "/logo.png" }],
};
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body className="bg-white text-slate-900">
<SiteHeader />
{children}
</body>
</html>
);
}
+56
View File
@@ -0,0 +1,56 @@
import Link from "next/link";
import { getLabSummaries } from "~/lib/labs";
export default function HomePage() {
const labs = getLabSummaries();
return (
<main className="mx-auto w-full max-w-5xl px-6 py-10">
<section className="rounded-xl border border-[#f6d5a5] bg-white p-8 shadow-sm">
<h1 className="mb-3 text-3xl font-bold text-[#004E78]">
Open Security Labs
</h1>
<p className="max-w-3xl text-slate-700">
Markdown-first lab workspace for Open Security notebook conversions.
</p>
<div className="mt-6 flex flex-wrap gap-3">
<Link
href="/labs"
className="inline-flex items-center rounded-md bg-[#004E78] px-4 py-2 text-sm font-semibold text-white hover:bg-[#003a5a]"
>
Browse all labs
</Link>
<Link
href="https://discord.gg/Ma9UZNBxvh"
className="inline-flex items-center rounded-md border border-[#F89C27] px-4 py-2 text-sm font-semibold text-[#004E78] hover:bg-[#F89C27] hover:text-white"
>
Open Security Discord
</Link>
</div>
</section>
<section className="mt-8">
<h2 className="mb-4 text-xl font-semibold text-[#004E78]">
Recent Labs
</h2>
<div className="grid gap-4 md:grid-cols-2">
{labs.slice(0, 6).map((lab) => (
<Link
key={lab.slug}
href={`/labs/${lab.slug}`}
className="block rounded-lg border border-slate-200 bg-white p-5 transition hover:border-[#F89C27] hover:shadow-sm"
>
<h3 className="text-lg font-semibold text-[#004E78]">
{lab.title}
</h3>
{lab.description ? (
<p className="mt-2 text-sm text-slate-600">{lab.description}</p>
) : null}
</Link>
))}
</div>
</section>
</main>
);
}
+29
View File
@@ -0,0 +1,29 @@
import Image from "next/image";
import Link from "next/link";
export function SiteHeader() {
return (
<header className="sticky top-0 z-20 border-b border-[#f8c27a] bg-white/95 shadow-sm backdrop-blur">
<div className="mx-auto flex w-full max-w-5xl items-center justify-between px-6 py-3">
<Link href="/" className="flex items-center" aria-label="Open Security home">
<Image src="/logo-full.png" alt="Open Security" width={150} height={40} priority />
</Link>
<nav className="flex items-center gap-5 text-sm font-semibold text-[#004E78]">
<Link href="/" className="hover:text-[#F89C27]">
Home
</Link>
<Link href="/labs" className="hover:text-[#F89C27]">
Labs
</Link>
<Link
href="https://discord.gg/Ma9UZNBxvh"
className="rounded-md border border-[#F89C27] px-3 py-1.5 text-[#004E78] hover:bg-[#F89C27] hover:text-white"
>
Discord
</Link>
</nav>
</div>
</header>
);
}
+241
View File
@@ -0,0 +1,241 @@
"use client";
import { useEffect, useRef, useState } from "react";
type LabContentProps = {
className: string;
html: string;
};
const cliLanguagePattern = /\b(language-(bash|sh|shell|zsh|console|terminal)|bash|shell|zsh)\b/i;
const cliCommandPattern =
/(^|\n)\s*(\$|sudo\s|git\s|python3?\s|pip\s|npm\s|pnpm\s|yarn\s|llama-|ollama\s|curl\s|wget\s|apt\s|cd\s|ls\s|cat\s|cp\s|mv\s|chmod\s|make\s)/i;
const promptLanguagePattern = /\b(language-(text|plaintext|md|markdown)|text|plaintext|markdown)\b/i;
const promptSignalPattern =
/\b(you are|guidelines|follow these|example|when provided|system prompt|tasked with)\b/i;
type ParsedSetting = {
key: string;
value: string;
};
type ZoomedImageState = {
src: string;
alt: string;
};
function looksLikeCliCommand(commandText: string, className: string) {
if (cliLanguagePattern.test(className)) return true;
return cliCommandPattern.test(commandText) || /--[a-z0-9-]+/i.test(commandText);
}
function looksLikePromptTextBlock(text: string, className: string) {
if (looksLikeCliCommand(text, className)) return false;
const normalizedText = text.trim();
if (!normalizedText) return false;
const lineCount = normalizedText.split("\n").length;
if (promptLanguagePattern.test(className) && normalizedText.length > 80) return true;
if (lineCount >= 4 && promptSignalPattern.test(normalizedText)) return true;
if (lineCount >= 6 && /(^|\n)\s*[*-]\s+/.test(normalizedText)) return true;
return false;
}
function escapeRegex(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function escapeHtml(value: string) {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function parseSettingListItem(item: HTMLLIElement): ParsedSetting | null {
const keyElement = item.querySelector("code");
if (!keyElement) return null;
const key = (keyElement.textContent ?? "").replace(/\s+/g, " ").trim();
if (!key || key.length > 40) return null;
const text = (item.textContent ?? "").replace(/\s+/g, " ").trim();
const match = new RegExp(`^${escapeRegex(key)}\\s*(?:-||—|:|=)\\s*(.+)$`).exec(text);
if (!match) return null;
const value = (match[1] ?? "").replace(/\s+/g, " ").trim();
if (!value || value.length > 36) return null;
if (/[.;]/.test(value) && value.length > 16) return null;
return { key, value };
}
function enhanceSettingsLists(root: HTMLElement) {
const lists = root.querySelectorAll<HTMLUListElement>("ul");
for (const list of lists) {
if (list.dataset.settingsEnhanced === "true") continue;
const items = Array.from(list.children).filter((node): node is HTMLLIElement => {
return node.tagName === "LI";
});
if (items.length < 2) continue;
const parsedItems = items.map((item) => parseSettingListItem(item));
if (parsedItems.some((parsedItem) => parsedItem === null)) continue;
const settings = parsedItems as ParsedSetting[];
const compactValueCount = settings.filter((setting) => setting.value.length <= 20).length;
if (compactValueCount < Math.max(2, Math.ceil(settings.length * 0.66))) continue;
list.dataset.settingsEnhanced = "true";
list.classList.add("lab-settings-list");
for (let i = 0; i < items.length; i++) {
const item = items[i];
const setting = settings[i];
if (!item || !setting) continue;
item.classList.add("lab-settings-item");
item.innerHTML =
`<span class="lab-setting-key">${escapeHtml(setting.key)}</span>` +
`<span class="lab-setting-value">${escapeHtml(setting.value)}</span>`;
}
}
}
function ensureCopyButton(pre: HTMLPreElement, label: string) {
if (pre.dataset.copyEnhanced === "true") return;
pre.dataset.copyEnhanced = "true";
const copyButton = document.createElement("button");
copyButton.type = "button";
copyButton.className = "lab-copy-button";
copyButton.textContent = label;
copyButton.dataset.defaultLabel = label;
copyButton.setAttribute("aria-label", "Copy block to clipboard");
pre.appendChild(copyButton);
}
export function LabContent({ className, html }: LabContentProps) {
const containerRef = useRef<HTMLElement>(null);
const [zoomedImage, setZoomedImage] = useState<ZoomedImageState | null>(null);
useEffect(() => {
const root = containerRef.current;
if (!root) return;
const preBlocks = root.querySelectorAll<HTMLPreElement>("pre");
for (const pre of preBlocks) {
const code = pre.querySelector<HTMLElement>("code");
if (!code) continue;
const blockText = code.textContent ?? "";
if (looksLikeCliCommand(blockText, code.className)) {
pre.classList.add("lab-cli-shell");
ensureCopyButton(pre, "Copy");
continue;
}
if (looksLikePromptTextBlock(blockText, code.className)) {
pre.classList.add("lab-prompt-card");
ensureCopyButton(pre, "Copy Text");
}
}
enhanceSettingsLists(root);
const handleRootClick = (event: Event) => {
const target = event.target as HTMLElement;
const button = target.closest<HTMLButtonElement>("button.lab-copy-button");
if (button) {
const pre = button.closest("pre");
const code = pre?.querySelector("code");
const commandText = code?.textContent?.trimEnd();
if (!commandText) return;
const defaultLabel = button.dataset.defaultLabel ?? "Copy";
void navigator.clipboard.writeText(commandText).then(() => {
button.textContent = "Copied";
button.classList.add("is-copied");
window.setTimeout(() => {
button.textContent = defaultLabel;
button.classList.remove("is-copied");
}, 1200);
}).catch(() => {
button.textContent = "Failed";
window.setTimeout(() => {
button.textContent = defaultLabel;
}, 1200);
});
return;
}
const image = target.closest<HTMLImageElement>("img");
if (!image || !root.contains(image)) return;
const src = image.getAttribute("src");
if (!src) return;
event.preventDefault();
event.stopPropagation();
setZoomedImage({
src,
alt: image.getAttribute("alt") ?? "",
});
};
root.addEventListener("click", handleRootClick);
return () => {
root.removeEventListener("click", handleRootClick);
};
}, [html]);
useEffect(() => {
if (!zoomedImage) return;
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
const activeElement = document.activeElement;
const previousFocusedElement = activeElement instanceof HTMLElement ? activeElement : null;
const handleEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setZoomedImage(null);
}
};
window.addEventListener("keydown", handleEscape);
return () => {
window.removeEventListener("keydown", handleEscape);
document.body.style.overflow = previousOverflow;
previousFocusedElement?.focus();
};
}, [zoomedImage]);
return (
<>
<article
ref={containerRef}
className={className}
dangerouslySetInnerHTML={{ __html: html }}
/>
{zoomedImage ? (
<div
className="lab-image-modal"
role="presentation"
onClick={() => setZoomedImage(null)}
>
<div className="lab-image-modal__surface" onClick={(event) => event.stopPropagation()}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img className="lab-image-modal__image" src={zoomedImage.src} alt={zoomedImage.alt} />
</div>
</div>
) : null}
</>
);
}
+100
View File
@@ -0,0 +1,100 @@
import fs from "fs";
import path from "path";
import matter from "gray-matter";
const CONTENT_DIR = path.join(process.cwd(), "content", "labs");
const CONTENT_EXTENSIONS = [".md", ".mdx"] as const;
export type LabSummary = {
slug: string;
title: string;
description: string;
fileName: string;
};
export type LabDocument = LabSummary & {
content: string;
data: Record<string, unknown>;
};
function toTitleCaseFromSlug(slug: string) {
return slug.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
}
function getSlugFromFileName(fileName: string) {
return fileName.replace(/\.(md|mdx)$/i, "");
}
function hasSupportedExtension(fileName: string) {
return CONTENT_EXTENSIONS.some((ext) => fileName.toLowerCase().endsWith(ext));
}
export function listLabFiles() {
if (!fs.existsSync(CONTENT_DIR)) {
return [];
}
return fs
.readdirSync(CONTENT_DIR)
.filter((fileName) => hasSupportedExtension(fileName))
.sort((a, b) => a.localeCompare(b));
}
export function getLabSummaries() {
return listLabFiles().map((fileName) => {
const filePath = path.join(CONTENT_DIR, fileName);
const source = fs.readFileSync(filePath, "utf8");
const { data } = matter(source);
const slug = getSlugFromFileName(fileName);
const title =
typeof data.title === "string" && data.title.trim().length > 0
? data.title
: toTitleCaseFromSlug(slug);
const description =
typeof data.description === "string" && data.description.trim().length > 0
? data.description
: "";
return {
slug,
title,
description,
fileName,
} satisfies LabSummary;
});
}
export function getLabDocument(slug: string): LabDocument | null {
const fileName = listLabFiles().find((candidateFileName) => {
return getSlugFromFileName(candidateFileName) === slug;
});
if (!fileName) {
return null;
}
const filePath = path.join(CONTENT_DIR, fileName);
const source = fs.readFileSync(filePath, "utf8");
const { content, data } = matter(source);
const title =
typeof data.title === "string" && data.title.trim().length > 0
? data.title
: toTitleCaseFromSlug(slug);
const description =
typeof data.description === "string" && data.description.trim().length > 0
? data.description
: "";
return {
slug,
title,
description,
fileName,
content,
data,
};
}
+854
View File
@@ -0,0 +1,854 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Base CSS Element Custom Styles */
h1 {
font-size: 2.25rem;
line-height: 2.5rem;
margin-bottom: 10px;
color: #004E78;
}
h2 {
font-size: 1.875rem;
line-height: 2.25rem;
margin-bottom: 10px;
color: #004E78;
}
h3 {
font-size: 1.5rem;
line-height: 2rem;
margin-bottom: 10px;
color: #004E78;
}
h4 {
font-size: 1.25rem;
line-height: 1.75rem;
margin-bottom: 10px;
color: #004E78;
}
h5 {
font-size: 1.125rem;
line-height: 1.75rem;
margin-bottom: 10px;
color: #004E78;
}
p {
font-size: 1rem;
line-height: 1.5rem;
margin-bottom: 5px;
}
ul {
list-style-type: disc;
margin-left: 20px;
}
ol {
list-style-type: number;
margin-left: 20px;
}
/* End Basic Customizations */
@layer base {
:root {
--background: none;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
/* #F89C27 / 34, 94%, 56% // #004E78 / 201 100% 24% */
--primary: 34, 94%, 56%;
--primary-foreground: 0 0% 98%;
--secondary: 201 100% 24%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 34, 94%, 56%;
--input: 0 0% 89.8%;
--ring: 34, 94%, 56%;
/* Keeping original chart colors */
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
/* Keeping original chart colors */
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
border-color: #e2e8f0;
}
body {
background-color: #ffffff;
color: #0f172a;
}
}
/* Keyframes for landing page */
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.blur {
background: radial-gradient(circle, transparent 100%, black);
mix-blend-mode: multiply;
}
.bg-fiber-carbon {
background:
radial-gradient(black 10%, transparent 1%) 0 0,
radial-gradient(rgba(255, 255, 255, 0.1) 15%, transparent 10%) 8px 19px;
background-color: #ffff;
background-size: 36px 36px;
}
.progress {
animation: progress 1s infinite linear;
}
.left-right {
transform-origin: 0% 50%;
}
@keyframes progress {
0% {
transform: translateX(0) scaleX(0);
}
40% {
transform: translateX(0) scaleX(0.4);
}
100% {
transform: translateX(100%) scaleX(0.5);
}
}
.lab-content table {
width: 100%;
margin: 1rem 0;
border-collapse: collapse;
}
.lab-content th,
.lab-content td {
border: 1px solid #d1d5db;
padding: 0.55rem 0.7rem;
vertical-align: top;
}
.lab-content th {
background-color: #f3f4f6;
text-align: left;
}
.lab-content img {
cursor: zoom-in;
}
.lab-image-modal {
position: fixed;
inset: 0;
z-index: 90;
display: flex;
align-items: center;
justify-content: center;
padding: 1.25rem;
background: rgba(75, 85, 99, 0.82);
}
.lab-image-modal__surface {
max-width: 95vw;
max-height: 95vh;
}
.lab-image-modal__image {
display: block;
width: auto;
height: auto;
max-width: 95vw;
max-height: 95vh;
border-radius: 10px;
box-shadow: 0 18px 56px rgba(17, 24, 39, 0.5);
}
.lab-content .lab-callout {
margin: 1rem 0;
padding: 0.75rem 1rem;
border-left: 4px solid;
border-radius: 0.25rem;
}
.lab-content .lab-callout--warning {
border-left-color: #dc2626;
background-color: #fef2f2;
}
.lab-content .lab-callout--info {
border-left-color: #2563eb;
background-color: #eff6ff;
}
.lab-content .lab-callout--checkpoint {
border-left-color: #15803d;
background-color: #f0fdf4;
}
.lab-content pre.lab-cli-shell {
position: relative;
margin: 1rem 0;
padding: 1rem 1rem 0.85rem;
border: 1px solid #c8d9e8;
border-left: 5px solid #004e78;
border-radius: 10px;
background: #f4f9ff;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.65);
overflow: auto;
}
.lab-content pre.lab-cli-shell::before {
content: "CLI";
position: absolute;
top: 0.42rem;
left: 0.7rem;
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.08em;
color: #4a6477;
}
.lab-content pre.lab-cli-shell code {
display: block;
margin-top: 0.45rem;
font-size: 0.9rem;
line-height: 1.38;
}
.lab-content pre.lab-cli-shell .lab-copy-button {
position: absolute;
top: 0.36rem;
right: 0.4rem;
border: 1px solid #c3d4e5;
border-radius: 8px;
background: #ffffff;
color: #294e69;
font-size: 0.74rem;
font-weight: 600;
line-height: 1;
padding: 0.34rem 0.56rem;
cursor: pointer;
}
.lab-content pre.lab-cli-shell .lab-copy-button:hover {
background: #eef5fb;
}
.lab-content pre.lab-cli-shell .lab-copy-button.is-copied {
border-color: #88c09e;
color: #0f5d33;
background: #eaf8ef;
}
.lab-content pre.lab-prompt-card {
position: relative;
margin: 1rem 0;
padding: 1rem 1rem 0.92rem;
border: 1px solid #d7c7a7;
border-left: 5px solid #b77400;
border-radius: 10px;
background: #fffaf2;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
overflow: auto;
}
.lab-content pre.lab-prompt-card::before {
content: "Card Description";
position: absolute;
top: 0.42rem;
left: 0.7rem;
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.08em;
color: #6c4a12;
}
.lab-content pre.lab-prompt-card code {
display: block;
margin-top: 0.82rem;
font-size: 0.9rem;
line-height: 1.4;
white-space: pre-wrap;
}
.lab-content pre.lab-prompt-card .lab-copy-button {
position: absolute;
top: 0.34rem;
right: 0.4rem;
border: 1px solid #9b5a00;
border-radius: 999px;
background: linear-gradient(180deg, #ffd18d, #f3a743);
color: #3d2401;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
line-height: 1;
padding: 0.36rem 0.62rem;
cursor: pointer;
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.35), 0 1px 2px rgba(61, 36, 1, 0.18);
}
.lab-content pre.lab-prompt-card .lab-copy-button:hover {
background: linear-gradient(180deg, #ffdb9f, #f5b459);
}
.lab-content pre.lab-prompt-card .lab-copy-button.is-copied {
border-color: #4f8d5f;
color: #08361a;
background: linear-gradient(180deg, #c8f2d4, #9edcb3);
}
.lab-content ul.lab-settings-list {
list-style: none;
margin: 0.9rem 0 1.2rem;
margin-left: 0;
padding: 0.22rem 0;
border: 1px solid #ccdeec;
border-left: 5px solid #0b72ba;
border-radius: 12px;
background: linear-gradient(180deg, #f9fcff, #f4f9fe);
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04);
}
.lab-content ul.lab-settings-list > li.lab-settings-item {
margin: 0;
padding: 0.5rem 0.85rem;
border: none;
border-bottom: 1px dashed #d5e3ef;
border-radius: 0;
background: transparent;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 0.75rem;
}
.lab-content ul.lab-settings-list > li.lab-settings-item:last-child {
border-bottom: none;
}
.lab-content ul.lab-settings-list .lab-setting-key {
font-weight: 600;
color: #0b4e77;
letter-spacing: 0.01em;
}
.lab-content ul.lab-settings-list .lab-setting-value {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
font-size: 0.86rem;
font-weight: 600;
color: #1f425f;
border: none;
border-radius: 0;
background: transparent;
padding: 0;
white-space: nowrap;
}
.lab-content hr {
margin: 2rem 0 1.4rem;
border-color: #d7dee6;
}
.lab-content .objective-segment {
position: relative;
margin: 1.75rem 0;
}
.lab-content .objective-segment > :first-child {
margin-top: 0;
}
.lab-content .objective-segment > :last-child {
margin-bottom: 0;
}
.lab-content .objective-segment > h2 {
scroll-margin-top: 2rem;
}
.lab-content.objective-style-divider .objective-segment {
padding-top: 1.55rem;
}
.lab-content.objective-style-divider .objective-segment::before {
content: "";
position: absolute;
top: 0;
left: 0.5rem;
right: 0.5rem;
height: 1px;
background: linear-gradient(
90deg,
rgba(0, 78, 120, 0.08),
rgba(0, 78, 120, 0.45) 12%,
rgba(0, 78, 120, 0.45) 88%,
rgba(0, 78, 120, 0.08)
);
}
.lab-content.objective-style-cards .objective-segment {
margin: 1.5rem 0;
padding: 1rem 1.2rem 1.2rem;
border: 1px solid #d8e1ea;
border-left: 6px solid #f89c27;
border-radius: 12px;
background: #f9fbfd;
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04);
}
.lab-content.objective-style-cards .objective-segment > h2 {
margin-bottom: 0.75rem;
}
.lab-content.objective-style-rail .objective-segment {
margin: 1.8rem 0;
padding: 0.4rem 0 0.5rem 1.15rem;
border-left: 4px solid #004e78;
background: linear-gradient(90deg, rgba(0, 78, 120, 0.08), rgba(0, 78, 120, 0));
}
.lab-content.objective-style-rail .objective-segment > h2 {
margin-bottom: 0.75rem;
}
.lab-content.objective-style-rail .objective-segment > h2::after {
content: "";
display: block;
width: 4.5rem;
height: 0.2rem;
margin-top: 0.45rem;
border-radius: 999px;
background-color: #f89c27;
}
.lab-content .lab-step-title {
margin-top: 1.2rem;
margin-bottom: 0.55rem;
line-height: 1.35;
color: #003f61;
letter-spacing: 0.01em;
}
.lab-content .lab-step-title + p,
.lab-content .lab-step-title + ul,
.lab-content .lab-step-title + ol,
.lab-content .lab-step-title + pre,
.lab-content .lab-step-title + blockquote,
.lab-content .lab-step-title + figure {
margin-top: 0.45rem;
}
.lab-content.step-style-underline .lab-step-title {
padding-bottom: 0.35rem;
border-bottom: 1px solid #d9e3ec;
}
.lab-content.step-style-underline .lab-step-title[data-step-mode="execute"] {
border-bottom-color: #f8cc8f;
}
.lab-content.step-style-underline .lab-step-title[data-step-mode="explore"] {
border-bottom-color: #9bc9ee;
}
.lab-content.step-style-underline .lab-step-title[data-step-mode="checkpoint"] {
border-bottom-color: #86d4a4;
}
.lab-content.step-style-pills .lab-step-title {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
padding: 0.42rem 0.8rem;
border-radius: 999px;
border: 1px solid #d8e2eb;
background: #f7fbff;
}
.lab-content.step-style-pills .lab-step-title::before {
content: attr(data-step-mode);
text-transform: capitalize;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.08em;
padding: 0.2rem 0.5rem;
border-radius: 999px;
color: #1a4f72;
background: #e2eef8;
}
.lab-content.step-style-pills .lab-step-title:not([data-step-mode])::before {
content: none;
}
.lab-content.step-style-pills .lab-step-title[data-step-mode="execute"]::before {
color: #8a4d00;
background: #fee7c7;
}
.lab-content.step-style-pills .lab-step-title[data-step-mode="explore"]::before {
color: #0f4970;
background: #d9ebfb;
}
.lab-content.step-style-pills .lab-step-title[data-step-mode="checkpoint"]::before {
color: #0e5e35;
background: #d9f7e4;
}
.lab-content.step-style-blocks .lab-step-title {
padding: 0.7rem 0.9rem;
border: 1px solid #d8e2eb;
border-left: 5px solid #004e78;
border-radius: 10px;
background: #f9fbfd;
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04);
}
.lab-content.step-style-blocks .lab-step-title[data-step-mode="execute"] {
border-left-color: #cc7a00;
background: #fffbf5;
}
.lab-content.step-style-blocks .lab-step-title[data-step-mode="explore"] {
border-left-color: #0b72ba;
background: #f6fbff;
}
.lab-content.step-style-blocks .lab-step-title[data-step-mode="checkpoint"] {
border-left-color: #198754;
background: #f6fffa;
}
.lab-content .step-segment {
position: relative;
margin: 1rem 0 1.35rem;
}
.lab-content .step-segment > :first-child {
margin-top: 0;
}
.lab-content .step-segment > :last-child {
margin-bottom: 0;
}
.lab-content .step-segment[data-step-kind]::before {
display: inline-block;
font-size: 0.63rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.lab-content.breakout-style-panel .step-segment {
padding: 0.75rem 0.9rem 0.9rem;
border: 1px solid #dce7f1;
border-radius: 12px;
background: #f8fbfe;
}
.lab-content.breakout-style-panel .step-segment[data-step-kind="explanation"] {
border-left: 5px solid #2b7fbf;
background: #f5fafe;
}
.lab-content.breakout-style-panel .step-segment[data-step-kind="instruction"] {
border-left: 5px solid #d48806;
background: #fffaf2;
}
.lab-content.breakout-style-panel .step-segment[data-step-kind="mixed"] {
border-left: 5px solid #0e7490;
background: linear-gradient(90deg, #f6fbff, #fffbf5);
}
.lab-content.breakout-style-panel .step-segment[data-step-kind]::before {
color: #315168;
content: attr(data-step-kind);
}
.lab-content.breakout-style-workflow .step-segment {
padding: 0.45rem 0 0.65rem 1rem;
border-left: 2px dashed #c6d5e3;
}
.lab-content.breakout-style-workflow .step-segment[data-step-kind="explanation"] {
border-left-color: #6da9d8;
}
.lab-content.breakout-style-workflow .step-segment[data-step-kind="instruction"] {
border-left-color: #de9a2e;
}
.lab-content.breakout-style-workflow .step-segment[data-step-kind="mixed"] {
border-left-color: #4a95ab;
}
.lab-content.breakout-style-workflow .step-segment[data-step-kind]::before {
width: 1.2rem;
text-indent: -9999px;
overflow: hidden;
border-radius: 999px;
margin-left: -1.4rem;
margin-right: 0.35rem;
vertical-align: middle;
background: #6da9d8;
content: "";
}
.lab-content.breakout-style-workflow .step-segment[data-step-kind="instruction"]::before {
background: #de9a2e;
}
.lab-content.breakout-style-workflow .step-segment[data-step-kind="mixed"]::before {
background: #4a95ab;
}
.lab-content .explicit-command-block {
margin: 0.8rem 0;
}
.lab-content .explicit-command .cmd-pill {
display: block;
}
.lab-content.breakout-style-command-pills .step-segment {
padding: 0.3rem 0 0.45rem;
}
.lab-content.breakout-style-command-pills .step-segment[data-step-kind="instruction"] {
border-left: 3px solid #f0b45f;
padding-left: 0.75rem;
}
.lab-content.breakout-style-command-pills .step-segment[data-step-kind="explanation"] {
border-left: 3px solid #8dc1e7;
padding-left: 0.75rem;
}
.lab-content.breakout-style-command-pills .step-segment[data-step-kind="mixed"] {
border-left: 3px solid #6db0bf;
padding-left: 0.75rem;
}
.lab-content.breakout-style-command-pills .step-segment[data-step-kind]::before {
color: #4a6477;
content: attr(data-step-kind);
}
.lab-content.breakout-style-instruction-rails .step-segment {
padding: 0.3rem 0 0.45rem 0.9rem;
border-left: none;
overflow: clip;
}
.lab-content.breakout-style-instruction-rails .step-segment::after {
content: "";
position: absolute;
left: 0;
top: 0.45rem;
height: calc(100% - 1.05rem);
width: 4px;
border-radius: 999px;
background: #6db0bf;
}
.lab-content.breakout-style-instruction-rails .step-segment[data-step-kind="instruction"]::after {
background: #f0b45f;
}
.lab-content.breakout-style-instruction-rails .step-segment[data-step-kind="explanation"]::after {
background: #8dc1e7;
}
.lab-content.breakout-style-instruction-rails .step-segment[data-step-kind]::before {
color: #4a6477;
content: attr(data-step-kind);
}
.lab-content.breakout-style-instruction-rails .lab-step-title {
font-size: 1.25rem;
line-height: 1.7rem;
}
.lab-content.breakout-style-command-pills p.explicit-instruction,
.lab-content.breakout-style-command-pills li.explicit-instruction {
border-radius: 999px;
border: 1px solid #f2d2a0;
background: #fff8ec;
padding: 0.22rem 0.68rem;
}
.lab-content.breakout-style-command-pills ol > li.explicit-instruction::marker,
.lab-content.breakout-style-command-pills ul > li.explicit-instruction::marker {
color: #a66300;
font-weight: 700;
}
.lab-content.breakout-style-command-pills .explicit-command-block {
padding: 0.25rem 0;
border: none;
background: transparent;
}
.lab-content.breakout-style-command-pills .explicit-command {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
white-space: normal;
background: transparent;
padding: 0;
}
.lab-content.breakout-style-command-pills .explicit-command .cmd-pill {
display: inline-flex;
align-items: center;
border-radius: 999px;
border: 1px solid #e5bf85;
background: #fff1d8;
padding: 0.2rem 0.58rem;
line-height: 1.25;
font-size: 0.86rem;
}
.lab-content ul.concept-pill-list {
list-style: none;
margin: 0.9rem 0 1.2rem;
margin-left: 0;
padding: 0;
display: grid;
gap: 0.6rem;
}
.lab-content ul.concept-pill-list > li {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.55rem;
margin: 0;
padding: 0.48rem 0.78rem;
border: 1px solid #d5e2ee;
border-radius: 999px;
background: linear-gradient(180deg, #f9fcff, #f4f9fe);
}
.lab-content .concept-pill-label {
display: inline;
color: #0f4f76;
font-size: 0.86rem;
font-weight: 700;
letter-spacing: 0.01em;
text-transform: none;
line-height: 1.25;
}
@media (max-width: 640px) {
.lab-content.objective-style-cards .objective-segment {
padding: 0.9rem 1rem 1rem;
}
.lab-content.objective-style-rail .objective-segment {
padding-left: 0.85rem;
}
.lab-content.step-style-pills .lab-step-title {
border-radius: 12px;
padding: 0.45rem 0.65rem;
}
.lab-content.step-style-blocks .lab-step-title {
padding: 0.62rem 0.75rem;
}
.lab-content.breakout-style-panel .step-segment {
padding: 0.68rem 0.72rem 0.78rem;
}
.lab-content.breakout-style-workflow .step-segment,
.lab-content.breakout-style-command-pills .step-segment,
.lab-content.breakout-style-instruction-rails .step-segment {
padding-left: 0.6rem;
}
.lab-content ul.lab-settings-list > li.lab-settings-item {
grid-template-columns: 1fr;
gap: 0.28rem;
align-items: start;
padding: 0.52rem 0.75rem;
}
.lab-content ul.lab-settings-list .lab-setting-value {
justify-self: start;
}
.lab-content ul.concept-pill-list > li {
border-radius: 16px;
}
}