mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-08 01:38:17 -10:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
069d57b5e5 | ||
|
|
e023211c02 | ||
|
|
a48006640d | ||
|
|
764fd0eddf |
No files matched your search
@@ -3,6 +3,8 @@ import React from "react";
|
||||
|
||||
type AsciinemaPlayerProps = {
|
||||
src: string;
|
||||
className?: string;
|
||||
hideControls?: boolean;
|
||||
|
||||
cols?: string;
|
||||
rows?: string;
|
||||
@@ -18,10 +20,67 @@ type AsciinemaPlayerProps = {
|
||||
fontSize?: string;
|
||||
};
|
||||
|
||||
function AsciinemaPlayer({ src, ...asciinemaOptions }: AsciinemaPlayerProps) {
|
||||
function AsciinemaPlayer({
|
||||
src,
|
||||
className,
|
||||
hideControls,
|
||||
cols,
|
||||
rows,
|
||||
autoPlay,
|
||||
preload,
|
||||
loop,
|
||||
startAt,
|
||||
speed,
|
||||
idleTimeLimit,
|
||||
theme,
|
||||
poster,
|
||||
fit,
|
||||
fontSize,
|
||||
}: AsciinemaPlayerProps) {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const [prefersReducedMotion, setPrefersReducedMotion] = React.useState(false);
|
||||
const [player, setPlayer] =
|
||||
React.useState<typeof import("asciinema-player")>();
|
||||
const asciinemaOptions = React.useMemo(
|
||||
() => ({
|
||||
autoPlay: prefersReducedMotion ? false : autoPlay,
|
||||
cols,
|
||||
controls: hideControls ? false : undefined,
|
||||
fit,
|
||||
fontSize,
|
||||
idleTimeLimit,
|
||||
loop: prefersReducedMotion ? false : loop,
|
||||
poster,
|
||||
preload,
|
||||
rows,
|
||||
speed,
|
||||
startAt,
|
||||
theme,
|
||||
}),
|
||||
[
|
||||
autoPlay,
|
||||
cols,
|
||||
fit,
|
||||
fontSize,
|
||||
hideControls,
|
||||
idleTimeLimit,
|
||||
loop,
|
||||
poster,
|
||||
preload,
|
||||
prefersReducedMotion,
|
||||
rows,
|
||||
speed,
|
||||
startAt,
|
||||
theme,
|
||||
],
|
||||
);
|
||||
React.useEffect(() => {
|
||||
const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)");
|
||||
const syncPreference = () => setPrefersReducedMotion(mediaQuery.matches);
|
||||
syncPreference();
|
||||
mediaQuery.addEventListener("change", syncPreference);
|
||||
return () => mediaQuery.removeEventListener("change", syncPreference);
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
import("asciinema-player").then((p) => {
|
||||
setPlayer(p);
|
||||
@@ -35,7 +94,17 @@ function AsciinemaPlayer({ src, ...asciinemaOptions }: AsciinemaPlayerProps) {
|
||||
};
|
||||
}, [src, player, asciinemaOptions]);
|
||||
|
||||
return <div ref={ref} />;
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={[
|
||||
className,
|
||||
hideControls ? "sliver-asciinema-controls-hidden" : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default AsciinemaPlayer;
|
||||
@@ -60,11 +60,8 @@ const CodeViewer = (props: CodeViewerProps) => {
|
||||
props.script.source_code
|
||||
);
|
||||
const [fontSize, setFontSize] = React.useState(props.fontSize || 13);
|
||||
const editorContainerClassName = React.useMemo(() => {
|
||||
return theme === Themes.DARK
|
||||
? "col-span-12 mt-4 rounded-2xl overflow-hidden"
|
||||
: "col-span-12 mt-4 rounded-2xl overflow-hidden border border-gray-300";
|
||||
}, [theme]);
|
||||
const editorContainerClassName =
|
||||
"col-span-12 mt-4 overflow-hidden rounded-2xl border border-separator bg-surface-secondary";
|
||||
|
||||
return (
|
||||
<div className={editorContainerClassName}>
|
||||
|
||||
@@ -3,9 +3,9 @@ import React from "react";
|
||||
|
||||
const LoadingState: React.FC = () => {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full items-center justify-center gap-3 text-foreground">
|
||||
<div className="flex min-h-[55vh] w-full items-center justify-center gap-3 text-muted">
|
||||
<Spinner aria-label="Loading" />
|
||||
<span>Loading...</span>
|
||||
<span className="text-sm">Loading documentation…</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ import Youtube from "./youtube";
|
||||
|
||||
export type MarkdownProps = {
|
||||
markdown: string;
|
||||
demoteTopLevelHeading?: boolean;
|
||||
};
|
||||
|
||||
type MarkdownAsciiCast = {
|
||||
@@ -121,12 +122,12 @@ const slugify = (value: string) => {
|
||||
};
|
||||
|
||||
const headingClassNames: Record<HeadingLevel, string> = {
|
||||
1: "mt-12 text-3xl font-semibold tracking-tight text-slate-900 first:mt-0 dark:text-slate-100",
|
||||
2: "mt-10 text-2xl font-semibold tracking-tight text-slate-900 dark:text-slate-100",
|
||||
3: "mt-8 text-xl font-semibold tracking-tight text-slate-900 dark:text-slate-100",
|
||||
4: "mt-6 text-lg font-semibold tracking-tight text-slate-900 dark:text-slate-100",
|
||||
5: "mt-6 text-base font-semibold tracking-tight text-slate-900 dark:text-slate-100",
|
||||
6: "mt-6 text-base font-semibold uppercase tracking-wide text-slate-700 dark:text-slate-300",
|
||||
1: "mt-12 text-3xl font-semibold tracking-tight text-foreground first:mt-0",
|
||||
2: "mt-12 text-2xl font-semibold tracking-tight text-foreground",
|
||||
3: "mt-9 text-xl font-semibold tracking-tight text-foreground",
|
||||
4: "mt-8 text-lg font-semibold tracking-tight text-foreground",
|
||||
5: "mt-7 text-base font-semibold tracking-tight text-foreground",
|
||||
6: "mt-7 text-base font-semibold tracking-tight text-foreground",
|
||||
};
|
||||
|
||||
const MarkdownViewer = (props: MarkdownProps) => {
|
||||
@@ -218,7 +219,9 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
}
|
||||
|
||||
const anchor = anchorRef.current;
|
||||
const HeadingTag = `h${level}`;
|
||||
const renderedLevel =
|
||||
level === 1 && props.demoteTopLevelHeading ? 2 : level;
|
||||
const HeadingTag = `h${renderedLevel}`;
|
||||
|
||||
return createElement(
|
||||
HeadingTag,
|
||||
@@ -226,7 +229,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
...rest,
|
||||
id: anchor || undefined,
|
||||
className: mergeClassNames(
|
||||
headingClassNames[level],
|
||||
headingClassNames[renderedLevel],
|
||||
"scroll-mt-[70px]",
|
||||
className
|
||||
),
|
||||
@@ -236,8 +239,8 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
{anchor && (
|
||||
<a
|
||||
href={`#${anchor}`}
|
||||
aria-label="Copy link to section"
|
||||
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-slate-400 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2 hover:text-primary dark:text-slate-500"
|
||||
aria-label={`Link to ${textContent}`}
|
||||
className="-my-2 inline-flex size-10 items-center justify-center rounded-xl text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 hover:bg-surface-secondary hover:text-accent"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@@ -283,12 +286,12 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
h5: createHeadingComponent(5),
|
||||
h6: createHeadingComponent(6),
|
||||
};
|
||||
}, [getAnchor]);
|
||||
}, [getAnchor, props.demoteTopLevelHeading]);
|
||||
|
||||
const proseClassName = mergeClassNames(
|
||||
"markdown-body prose max-w-none leading-7",
|
||||
theme === Themes.DARK ? "dark:prose-invert prose-slate" : "prose-slate",
|
||||
"prose-pre:bg-slate-950/90 prose-pre:text-slate-100 prose-code:font-mono prose-img:rounded-xl"
|
||||
"prose-code:font-mono prose-img:rounded-2xl"
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -307,7 +310,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
{...rest}
|
||||
href={href}
|
||||
className={mergeClassNames(
|
||||
"font-medium text-primary transition-colors duration-150 hover:text-primary-dark",
|
||||
"font-medium text-accent underline-offset-4 hover:underline",
|
||||
className
|
||||
)}
|
||||
onClick={(e) => {
|
||||
@@ -342,7 +345,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
}
|
||||
|
||||
const anchorClassName = mergeClassNames(
|
||||
"font-medium text-primary transition-colors duration-150 underline-offset-4 hover:text-primary-dark",
|
||||
"font-medium text-accent underline-offset-4 hover:underline",
|
||||
className
|
||||
);
|
||||
|
||||
@@ -376,7 +379,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<p
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"my-6 text-base leading-7 text-slate-700 dark:text-slate-300",
|
||||
"my-6 text-base leading-7 text-foreground/80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -391,7 +394,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<ul
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"my-6 list-disc space-y-2 pl-6 text-slate-700 marker:text-primary dark:text-slate-300",
|
||||
"my-6 list-disc space-y-2 pl-6 text-foreground/80 marker:text-accent",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -406,7 +409,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<ol
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"my-6 list-decimal space-y-2 pl-6 text-slate-700 marker:text-primary dark:text-slate-300",
|
||||
"my-6 list-decimal space-y-2 pl-6 text-foreground/80 marker:text-accent",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -421,7 +424,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<li
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"leading-6 text-slate-700 dark:text-slate-300",
|
||||
"leading-6 text-foreground/80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -436,7 +439,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<blockquote
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"my-8 border-l-4 border-slate-200 bg-slate-50 px-6 py-4 text-base italic text-slate-700 shadow-sm dark:border-slate-700 dark:bg-slate-900/50 dark:text-slate-300",
|
||||
"my-8 rounded-r-2xl border-l-4 border-accent/40 bg-surface-secondary px-6 py-4 text-base italic text-foreground/80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -451,7 +454,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<hr
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"my-12 border-t border-slate-200 dark:border-slate-800",
|
||||
"my-12 border-t border-separator",
|
||||
className
|
||||
)}
|
||||
/>
|
||||
@@ -461,11 +464,11 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
table(tableProps) {
|
||||
const { className, children, ...rest } = tableProps;
|
||||
return (
|
||||
<div className="my-8 overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-950/40">
|
||||
<div className="sliver-scrollbar my-8 overflow-x-auto rounded-2xl border border-separator bg-surface">
|
||||
<table
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"w-full min-w-max divide-y divide-slate-200 text-left text-sm dark:divide-slate-800",
|
||||
"w-full min-w-max divide-y divide-separator text-left text-sm",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -481,7 +484,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<thead
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"bg-slate-50 text-sm font-semibold uppercase tracking-wide text-slate-600 dark:bg-slate-900/60 dark:text-slate-300",
|
||||
"bg-surface-secondary text-sm font-semibold text-muted",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -496,7 +499,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<tbody
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"divide-y divide-slate-200 dark:divide-slate-800",
|
||||
"divide-y divide-separator",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -511,7 +514,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<tr
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"transition-colors hover:bg-slate-50 dark:hover:bg-slate-900/60",
|
||||
"bg-transparent",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -526,7 +529,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<th
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"px-4 py-3 text-left text-sm font-semibold text-slate-600 dark:text-slate-200",
|
||||
"px-4 py-3 text-left text-sm font-semibold text-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -541,7 +544,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<td
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"px-4 py-3 align-top text-sm text-slate-700 dark:text-slate-300",
|
||||
"px-4 py-3 align-top text-sm text-foreground/80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -556,7 +559,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<strong
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"font-semibold text-slate-900 dark:text-slate-100",
|
||||
"font-semibold text-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -571,7 +574,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<em
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"text-slate-700 dark:text-slate-300",
|
||||
"text-foreground/80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -605,7 +608,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<pre
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"my-6 overflow-x-auto rounded-xl border border-slate-200 bg-slate-50 p-4 text-[13px] leading-6 text-slate-900 shadow-inner dark:border-slate-800 dark:bg-slate-950/90 dark:text-slate-100",
|
||||
"sliver-scrollbar my-6 overflow-x-auto rounded-2xl border border-separator bg-surface-secondary p-4 text-[13px] leading-6 text-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -625,7 +628,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
width={1200}
|
||||
height={720}
|
||||
className={mergeClassNames(
|
||||
"my-8 w-full rounded-xl border border-slate-200 object-contain shadow-sm dark:border-slate-800",
|
||||
"my-8 w-full rounded-2xl border border-separator/70 object-contain",
|
||||
className
|
||||
)}
|
||||
/>
|
||||
@@ -655,7 +658,11 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
|
||||
if (normalizedLang === "youtube") {
|
||||
const embedId = sourceCode || "";
|
||||
return <Youtube embedId={embedId.trim()} />;
|
||||
return (
|
||||
<div className="not-prose my-8 overflow-hidden rounded-2xl">
|
||||
<Youtube embedId={embedId.trim()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (normalizedLang === "asciinema") {
|
||||
@@ -668,15 +675,18 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<AsciinemaPlayer
|
||||
src={srcUrl.toString()}
|
||||
rows={asciiCast.rows || "18"}
|
||||
cols={asciiCast.cols || "75"}
|
||||
idleTimeLimit={asciiCast.idleTimeLimit || 2}
|
||||
preload={true}
|
||||
autoPlay={true}
|
||||
loop={true}
|
||||
/>
|
||||
<div className="sliver-terminal-frame not-prose my-8 w-full max-w-full overflow-x-auto rounded-2xl bg-[#111315]">
|
||||
<AsciinemaPlayer
|
||||
className="min-w-[42rem]"
|
||||
src={srcUrl.toString()}
|
||||
rows={asciiCast.rows || "18"}
|
||||
cols={asciiCast.cols || "75"}
|
||||
idleTimeLimit={asciiCast.idleTimeLimit || 2}
|
||||
preload={true}
|
||||
autoPlay={true}
|
||||
loop={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -694,7 +704,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
<code
|
||||
{...rest}
|
||||
className={mergeClassNames(
|
||||
"rounded-md bg-slate-100 px-1.5 py-0.5 font-mono text-[13px] text-slate-700 dark:bg-slate-800/80 dark:text-slate-200",
|
||||
"rounded-md bg-surface-secondary px-1.5 py-0.5 font-mono text-[13px] text-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -752,7 +762,7 @@ const MarkdownViewer = (props: MarkdownProps) => {
|
||||
const formattedSourceCode = sourceCode.replace(/\n$/, "");
|
||||
|
||||
const preWrapperClassName = mergeClassNames(
|
||||
"not-prose mt-4 overflow-x-auto rounded-2xl border border-slate-200 bg-slate-50 px-4 py-4 text-[13px] leading-6 text-slate-900 shadow-sm dark:border-slate-800 dark:bg-slate-950/90 dark:text-slate-100",
|
||||
"sliver-scrollbar not-prose mt-4 overflow-x-auto rounded-2xl border border-separator bg-surface-secondary px-4 py-4 text-[13px] leading-6 text-foreground",
|
||||
className
|
||||
);
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ const Mermaid = ({ diagram, minHeight }: MermaidProps) => {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="not-prose my-6 text-sm text-rose-700 dark:text-rose-300">
|
||||
<div className="not-prose my-6 rounded-2xl bg-danger-soft p-4 text-sm text-danger-soft-foreground">
|
||||
Failed to render Mermaid diagram: {error}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,24 +5,28 @@ import { Themes } from "@/util/themes";
|
||||
import { faDiscord, faGithub } from "@fortawesome/free-brands-svg-icons";
|
||||
import {
|
||||
faBars,
|
||||
faHome,
|
||||
faMoon,
|
||||
faSearch,
|
||||
faSun,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, SearchField, Tooltip } from "@heroui/react";
|
||||
import { Button, Drawer, SearchField, Tooltip } from "@heroui/react";
|
||||
import { useTheme } from "next-themes";
|
||||
import NextLink from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
const routes = [
|
||||
{ href: "/docs", label: "Docs" },
|
||||
{ href: "/tutorials", label: "Tutorials" },
|
||||
{ href: "/talks", label: "Talks" },
|
||||
{ href: "/docs", label: "Docs" },
|
||||
];
|
||||
|
||||
const discordUrl =
|
||||
"https://discord.com/channels/791066041198968873/1339996286514106409";
|
||||
const githubUrl = "https://github.com/BishopFox/sliver";
|
||||
|
||||
export default function TopNavbar() {
|
||||
const router = useRouter();
|
||||
const { theme, setTheme } = useTheme();
|
||||
@@ -30,9 +34,28 @@ export default function TopNavbar() {
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
|
||||
|
||||
const activeTheme = theme || Themes.DARK;
|
||||
const lightDarkModeIcon = React.useMemo(() => {
|
||||
return activeTheme === Themes.DARK ? faSun : faMoon;
|
||||
}, [activeTheme]);
|
||||
const isDarkTheme = activeTheme === Themes.DARK;
|
||||
const lightDarkModeIcon = isDarkTheme ? faSun : faMoon;
|
||||
const themeToggleLabel = isDarkTheme
|
||||
? "Switch to light mode"
|
||||
: "Switch to dark mode";
|
||||
|
||||
const toggleTheme = React.useCallback(() => {
|
||||
setTheme(isDarkTheme ? Themes.LIGHT : Themes.DARK);
|
||||
}, [isDarkTheme, setTheme]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const desktopBreakpoint = window.matchMedia("(min-width: 768px)");
|
||||
const closeMobileMenu = (event: MediaQueryListEvent) => {
|
||||
if (event.matches) {
|
||||
setIsMobileMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
desktopBreakpoint.addEventListener("change", closeMobileMenu);
|
||||
return () =>
|
||||
desktopBreakpoint.removeEventListener("change", closeMobileMenu);
|
||||
}, []);
|
||||
|
||||
const handleSearchSubmit = React.useCallback(
|
||||
(value = query) => {
|
||||
@@ -41,7 +64,11 @@ export default function TopNavbar() {
|
||||
if (searchQuery.length === 0) {
|
||||
return;
|
||||
}
|
||||
router.push({ pathname: "/search/", query: { search: searchQuery } });
|
||||
|
||||
void router.push({
|
||||
pathname: "/search/",
|
||||
query: { search: searchQuery },
|
||||
});
|
||||
setQuery("");
|
||||
setIsMobileMenuOpen(false);
|
||||
},
|
||||
@@ -58,12 +85,13 @@ export default function TopNavbar() {
|
||||
[],
|
||||
);
|
||||
|
||||
const renderSearchInput = (wrapperClassName?: string) => (
|
||||
const renderSearchInput = (className?: string) => (
|
||||
<SearchField
|
||||
aria-label="Search documentation"
|
||||
className={wrapperClassName}
|
||||
className={className}
|
||||
fullWidth
|
||||
value={query}
|
||||
variant="secondary"
|
||||
onChange={setQuery}
|
||||
onSubmit={handleSearchSubmit}
|
||||
>
|
||||
@@ -71,8 +99,10 @@ export default function TopNavbar() {
|
||||
<SearchField.SearchIcon>
|
||||
<FontAwesomeIcon icon={faSearch} />
|
||||
</SearchField.SearchIcon>
|
||||
<SearchField.Input placeholder="Search..." />
|
||||
{query.length > 0 ? <SearchField.ClearButton aria-label="Clear search" /> : null}
|
||||
<SearchField.Input placeholder="Search documentation…" />
|
||||
{query.length > 0 ? (
|
||||
<SearchField.ClearButton aria-label="Clear search" />
|
||||
) : null}
|
||||
</SearchField.Group>
|
||||
</SearchField>
|
||||
);
|
||||
@@ -82,82 +112,80 @@ export default function TopNavbar() {
|
||||
|
||||
return (
|
||||
<li key={href}>
|
||||
<Button
|
||||
variant={isActive ? "secondary" : "ghost"}
|
||||
onPress={() => router.push(href)}
|
||||
<NextLink
|
||||
href={href}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={`inline-flex h-10 items-center justify-center rounded-xl px-4 text-sm font-medium no-underline outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ${
|
||||
isActive
|
||||
? "bg-surface-secondary text-foreground shadow-surface"
|
||||
: "text-muted hover:bg-surface-secondary hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
</NextLink>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMobileRoute = (href: string, label: string) => {
|
||||
const isActive = href === "/" ? router.pathname === "/" : router.pathname.startsWith(href);
|
||||
const isActive =
|
||||
href === "/"
|
||||
? router.pathname === "/"
|
||||
: router.pathname.startsWith(href);
|
||||
|
||||
return (
|
||||
<Button
|
||||
<NextLink
|
||||
key={href}
|
||||
fullWidth
|
||||
variant={isActive ? "primary" : "ghost"}
|
||||
onPress={() => {
|
||||
router.push(href);
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
href={href}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={`inline-flex min-h-10 w-full items-center rounded-xl px-3 py-2 text-sm font-medium no-underline outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ${
|
||||
isActive
|
||||
? "bg-surface-secondary text-foreground shadow-surface"
|
||||
: "text-muted hover:bg-surface-secondary hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
{href === "/" ? <FontAwesomeIcon fixedWidth icon={faHome} /> : null}
|
||||
{label}
|
||||
</Button>
|
||||
</NextLink>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className="sliver-navbar-glass sticky top-0 z-40 px-4 shadow-sm md:px-6">
|
||||
<div className="flex h-16 items-center gap-4">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-2 text-foreground"
|
||||
onClick={() => {
|
||||
router.push("/");
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
aria-label="Sliver C2 home"
|
||||
<nav
|
||||
aria-label="Primary navigation"
|
||||
className="sticky top-0 z-40 h-16 border-b border-separator bg-background/85 backdrop-blur-xl"
|
||||
>
|
||||
<div className="mx-auto flex h-16 w-full max-w-[1440px] items-center gap-3 px-4 sm:px-6 lg:px-8">
|
||||
<NextLink
|
||||
href="/"
|
||||
aria-label="Sliver documentation home"
|
||||
className="-ml-2 inline-flex h-10 shrink-0 items-center gap-2 rounded-xl px-2 text-sm font-semibold text-foreground no-underline outline-none hover:bg-surface-secondary focus-visible:ring-2 focus-visible:ring-accent/40"
|
||||
>
|
||||
<SliversIcon />
|
||||
<span className="hidden font-bold text-inherit sm:block">
|
||||
Sliver C2
|
||||
<span className="flex size-8 items-center justify-center rounded-xl bg-accent text-accent-foreground">
|
||||
<SliversIcon height={18} />
|
||||
</span>
|
||||
</button>
|
||||
<span className="hidden items-baseline gap-1.5 sm:flex">
|
||||
<span>Sliver C2</span>
|
||||
</span>
|
||||
</NextLink>
|
||||
|
||||
<ul className="hidden items-center gap-2 md:flex">
|
||||
<li>
|
||||
{renderTooltip(
|
||||
"Home",
|
||||
<Button
|
||||
isIconOnly
|
||||
aria-label="Home"
|
||||
variant={router.pathname === "/" ? "secondary" : "ghost"}
|
||||
onPress={() => router.push("/")}
|
||||
>
|
||||
<FontAwesomeIcon fixedWidth icon={faHome} />
|
||||
</Button>,
|
||||
)}
|
||||
</li>
|
||||
{routes.map((route) => renderDesktopRoute(route.href, route.label))}
|
||||
<ul className="hidden items-center gap-1 md:flex">
|
||||
{routes.map((route) =>
|
||||
renderDesktopRoute(route.href, route.label),
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<div className="ml-auto hidden items-center gap-2 md:flex">
|
||||
{renderSearchInput("w-64")}
|
||||
<div className="ml-auto hidden items-center gap-1 md:flex">
|
||||
{renderSearchInput("mr-2 w-56 xl:w-72")}
|
||||
|
||||
{renderTooltip(
|
||||
activeTheme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode",
|
||||
themeToggleLabel,
|
||||
<Button
|
||||
isIconOnly
|
||||
aria-label={activeTheme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode"}
|
||||
aria-label={themeToggleLabel}
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
setTheme(activeTheme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
|
||||
}}
|
||||
onPress={toggleTheme}
|
||||
>
|
||||
<FontAwesomeIcon icon={lightDarkModeIcon} />
|
||||
</Button>,
|
||||
@@ -165,62 +193,55 @@ export default function TopNavbar() {
|
||||
|
||||
{renderTooltip(
|
||||
"Join Discord",
|
||||
<Button
|
||||
isIconOnly
|
||||
<a
|
||||
href={discordUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="Join Discord"
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://discord.com/channels/791066041198968873/1339996286514106409",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}}
|
||||
className="inline-flex size-10 items-center justify-center rounded-xl text-muted no-underline outline-none hover:bg-surface-secondary hover:text-foreground focus-visible:ring-2 focus-visible:ring-accent/40"
|
||||
>
|
||||
<FontAwesomeIcon icon={faDiscord} />
|
||||
</Button>,
|
||||
</a>,
|
||||
)}
|
||||
|
||||
{renderTooltip(
|
||||
"View on GitHub",
|
||||
<Button
|
||||
isIconOnly
|
||||
<a
|
||||
href={githubUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label="View on GitHub"
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://github.com/BishopFox/sliver",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}}
|
||||
className="inline-flex size-10 items-center justify-center rounded-xl text-muted no-underline outline-none hover:bg-surface-secondary hover:text-foreground focus-visible:ring-2 focus-visible:ring-accent/40"
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} />
|
||||
</Button>,
|
||||
</a>,
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2 md:hidden">
|
||||
<div className="ml-auto flex items-center gap-1 md:hidden">
|
||||
{renderTooltip(
|
||||
activeTheme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode",
|
||||
themeToggleLabel,
|
||||
<Button
|
||||
isIconOnly
|
||||
aria-label={activeTheme === Themes.DARK ? "Switch to light mode" : "Switch to dark mode"}
|
||||
aria-label={themeToggleLabel}
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
setTheme(activeTheme === Themes.DARK ? Themes.LIGHT : Themes.DARK);
|
||||
}}
|
||||
onPress={toggleTheme}
|
||||
>
|
||||
<FontAwesomeIcon icon={lightDarkModeIcon} />
|
||||
</Button>,
|
||||
)}
|
||||
|
||||
{renderTooltip(
|
||||
isMobileMenuOpen ? "Close menu" : "Open menu",
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="ghost"
|
||||
aria-controls="mobile-navigation"
|
||||
aria-expanded={isMobileMenuOpen}
|
||||
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
|
||||
onPress={() => setIsMobileMenuOpen((current) => !current)}
|
||||
variant="ghost"
|
||||
onPress={() =>
|
||||
setIsMobileMenuOpen((current) => !current)
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={isMobileMenuOpen ? faXmark : faBars} />
|
||||
</Button>,
|
||||
@@ -229,43 +250,56 @@ export default function TopNavbar() {
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{isMobileMenuOpen ? (
|
||||
<div className="sliver-navbar-glass px-4 pb-4 shadow-sm md:hidden">
|
||||
<div className="mt-3">{renderSearchInput("w-full")}</div>
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
{renderMobileRoute("/", "Home")}
|
||||
{routes.map((route) => renderMobileRoute(route.href, route.label))}
|
||||
<Button
|
||||
fullWidth
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://discord.com/channels/791066041198968873/1339996286514106409",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDiscord} />
|
||||
Discord
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://github.com/BishopFox/sliver",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} />
|
||||
GitHub
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<Drawer.Backdrop
|
||||
isOpen={isMobileMenuOpen}
|
||||
variant="blur"
|
||||
onOpenChange={setIsMobileMenuOpen}
|
||||
>
|
||||
<Drawer.Content placement="right">
|
||||
<Drawer.Dialog
|
||||
id="mobile-navigation"
|
||||
className="w-[min(24rem,calc(100vw-1.5rem))]"
|
||||
>
|
||||
<Drawer.CloseTrigger />
|
||||
<Drawer.Header>
|
||||
<Drawer.Heading>Navigate Sliver Docs</Drawer.Heading>
|
||||
</Drawer.Header>
|
||||
<Drawer.Body className="flex flex-col gap-5 pt-2">
|
||||
{renderSearchInput("w-full")}
|
||||
|
||||
<nav
|
||||
aria-label="Mobile navigation"
|
||||
className="flex flex-col gap-1"
|
||||
>
|
||||
{renderMobileRoute("/", "Home")}
|
||||
{routes.map((route) =>
|
||||
renderMobileRoute(route.href, route.label),
|
||||
)}
|
||||
</nav>
|
||||
</Drawer.Body>
|
||||
<Drawer.Footer className="grid grid-cols-2 gap-2">
|
||||
<a
|
||||
href={discordUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-xl border border-separator px-4 text-sm font-medium text-foreground no-underline outline-none hover:bg-surface-secondary focus-visible:ring-2 focus-visible:ring-accent/40"
|
||||
>
|
||||
<FontAwesomeIcon icon={faDiscord} />
|
||||
Discord
|
||||
</a>
|
||||
<a
|
||||
href={githubUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex h-10 w-full items-center justify-center gap-2 rounded-xl border border-separator px-4 text-sm font-medium text-foreground no-underline outline-none hover:bg-surface-secondary focus-visible:ring-2 focus-visible:ring-accent/40"
|
||||
>
|
||||
<FontAwesomeIcon icon={faGithub} />
|
||||
GitHub
|
||||
</a>
|
||||
</Drawer.Footer>
|
||||
</Drawer.Dialog>
|
||||
</Drawer.Content>
|
||||
</Drawer.Backdrop>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import AsciinemaPlayer from "./asciinema";
|
||||
|
||||
import { faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import styles from "./TutorialCard.module.css";
|
||||
|
||||
export type TutorialCardCardProps = {
|
||||
name: string;
|
||||
@@ -13,6 +12,7 @@ export type TutorialCardCardProps = {
|
||||
rows?: string;
|
||||
cols?: string;
|
||||
idleTimeLimit?: number;
|
||||
hideControls?: boolean;
|
||||
|
||||
italicDescription?: boolean;
|
||||
onPress?: () => void;
|
||||
@@ -21,55 +21,48 @@ export type TutorialCardCardProps = {
|
||||
};
|
||||
|
||||
export default function TutorialCard(props: TutorialCardCardProps) {
|
||||
const actionButtonClassName =
|
||||
"ml-auto h-auto min-h-0 shrink-0 px-0 py-0 text-sm font-semibold [--button-bg-hover:transparent] [--button-bg-pressed:transparent] [--button-bg:transparent] [--button-fg:var(--warning)]";
|
||||
|
||||
return (
|
||||
<Card className="relative z-0 gap-0 overflow-hidden p-0">
|
||||
<Card.Header className="absolute left-3 right-3 top-3 z-10 flex-col items-end p-0 text-right">
|
||||
<p className="text-sm font-bold uppercase leading-none text-white/70">
|
||||
{props.name}
|
||||
</p>
|
||||
</Card.Header>
|
||||
|
||||
<div className={`${styles["hide-control-bar"]} overflow-hidden`}>
|
||||
<Card className="relative h-full min-w-0 gap-0 overflow-hidden bg-black p-0 shadow-surface">
|
||||
<div className="sliver-terminal-frame relative w-full max-w-full overflow-x-auto bg-[#111315]">
|
||||
<AsciinemaPlayer
|
||||
className="min-w-[36rem]"
|
||||
src={props.asciiCast}
|
||||
rows={props.rows ? props.rows : "18"}
|
||||
cols={props.cols ? props.cols : "75"}
|
||||
idleTimeLimit={props.idleTimeLimit ? props.idleTimeLimit : 2}
|
||||
hideControls={props.hideControls}
|
||||
rows={props.rows || "18"}
|
||||
cols={props.cols || "75"}
|
||||
idleTimeLimit={props.idleTimeLimit || 2}
|
||||
preload={true}
|
||||
autoPlay={true}
|
||||
loop={true}
|
||||
/>
|
||||
|
||||
<div className="pointer-events-none absolute inset-x-3 top-3 z-10 flex justify-end">
|
||||
<h3 className="max-w-[80%] rounded-xl border border-white/15 bg-black/55 px-3 py-2 text-right text-sm font-semibold text-white/85 shadow-lg backdrop-blur-xl">
|
||||
{props.name}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card.Footer className="absolute bottom-0 left-0 right-0 z-10 border-t border-white/20 bg-black/40 p-3 backdrop-blur-md">
|
||||
<div className="flex min-h-10 w-full flex-row items-center gap-4">
|
||||
<p
|
||||
className={
|
||||
props.italicDescription
|
||||
? "min-w-0 flex-1 text-sm leading-snug text-white/80 italic"
|
||||
: "min-w-0 flex-1 text-sm leading-snug text-white/80"
|
||||
}
|
||||
>
|
||||
{props.description}
|
||||
</p>
|
||||
<Card.Footer className="absolute inset-x-3 bottom-10 z-10 flex-row items-center gap-3 rounded-2xl border border-white/15 bg-black/55 px-3 py-2.5 text-white shadow-xl backdrop-blur-xl">
|
||||
<p
|
||||
className={`min-w-0 flex-1 text-sm leading-5 text-white/70 ${
|
||||
props.italicDescription ? "italic" : ""
|
||||
}`}
|
||||
>
|
||||
{props.description}
|
||||
</p>
|
||||
|
||||
{props.showButton ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onPress={props.onPress}
|
||||
className={actionButtonClassName}
|
||||
>
|
||||
{props.buttonText ? props.buttonText : "Read Tutorial"}
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</Button>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
{props.showButton ? (
|
||||
<Button
|
||||
className="shrink-0 text-white hover:bg-white/10"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onPress={props.onPress}
|
||||
>
|
||||
{props.buttonText || "Read tutorial"}
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</Button>
|
||||
) : null}
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { fetchDocs as fetchDocsContent, fetchTutorials as fetchTutorialsContent
|
||||
import { Themes } from "@/util/themes";
|
||||
import { faExternalLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { RouterProvider } from "@heroui/react";
|
||||
import {
|
||||
HydrationBoundary,
|
||||
QueryClient,
|
||||
@@ -15,9 +16,19 @@ import {
|
||||
} from "@tanstack/react-query";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import type { AppProps } from "next/app";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const navigate = React.useCallback(
|
||||
(path: string) => {
|
||||
void router.push(path);
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
// Initialize search
|
||||
const [search] = React.useState(() => new SearchCtx());
|
||||
|
||||
@@ -101,54 +112,50 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HydrationBoundary state={pageProps.dehydratedState}>
|
||||
<SearchContext.Provider value={search}>
|
||||
<Navbar />
|
||||
<Component {...pageProps} />
|
||||
<div className="mb-16 md:mb-20"></div>
|
||||
<footer className="z-20 w-full border-t border-separator bg-surface/80 px-3 py-4 text-center shadow backdrop-blur-md dark:bg-black/75 md:fixed md:bottom-0 md:left-0 md:right-0 md:flex md:items-center md:justify-between md:px-6 md:py-4">
|
||||
<div className="flex flex-col items-center gap-2 text-xs leading-snug text-muted sm:text-sm md:flex-row md:items-center md:gap-3 md:text-left">
|
||||
<span>
|
||||
© {new Date().getFullYear()}
|
||||
<a
|
||||
href="https://bishopfox.com/"
|
||||
className="hover:underline"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Bishop Fox
|
||||
</a>
|
||||
</span>
|
||||
<span className="hidden md:inline" aria-hidden="true">
|
||||
·
|
||||
</span>
|
||||
<span className="max-w-xs md:max-w-none">
|
||||
<a
|
||||
href="https://github.com/BishopFox/sliver/pulls"
|
||||
rel="noreferrer"
|
||||
className="hover:underline"
|
||||
target="_blank"
|
||||
>
|
||||
Help improve this documentation
|
||||
<span className="ml-1 inline-block">
|
||||
<FontAwesomeIcon icon={faExternalLink} />
|
||||
<RouterProvider navigate={navigate}>
|
||||
<SearchContext.Provider value={search}>
|
||||
<div className="flex min-h-dvh flex-col">
|
||||
<Navbar />
|
||||
<main className="sliver-page-content flex-1">
|
||||
<Component {...pageProps} />
|
||||
</main>
|
||||
<footer className="sliver-fixed-footer fixed inset-x-0 bottom-0 z-30 border-t border-separator/70 bg-background/70 backdrop-blur-xl">
|
||||
<div className="mx-auto flex min-h-[var(--sliver-footer-height)] w-full max-w-7xl flex-col items-center justify-center gap-1.5 px-4 py-3 text-center text-sm text-muted sm:px-6 md:flex-row md:justify-between md:text-left lg:px-8">
|
||||
<span>
|
||||
© {new Date().getFullYear()} {" "}
|
||||
<a
|
||||
href="https://bishopfox.com/"
|
||||
className="font-medium text-foreground no-underline hover:text-accent"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Bishop Fox
|
||||
</a>
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-5 gap-y-2">
|
||||
<a
|
||||
href="https://github.com/BishopFox/sliver/pulls"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1.5 no-underline hover:text-foreground"
|
||||
target="_blank"
|
||||
>
|
||||
Improve these docs
|
||||
<FontAwesomeIcon className="text-xs" icon={faExternalLink} />
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/BishopFox/sliver/blob/master/LICENSE"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="no-underline hover:text-foreground"
|
||||
>
|
||||
GPLv3
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
<ul className="mt-3 flex flex-wrap items-center justify-center gap-3 text-xs font-medium text-muted md:mt-0 md:gap-6 md:text-sm">
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/BishopFox/sliver/blob/master/LICENSE"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="hover:underline"
|
||||
>
|
||||
GPLv3 License
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
</SearchContext.Provider>
|
||||
</SearchContext.Provider>
|
||||
</RouterProvider>
|
||||
</HydrationBoundary>
|
||||
</QueryClientProvider>
|
||||
</NextThemesProvider>
|
||||
|
||||
@@ -3,14 +3,18 @@ import LoadingState from "@/components/loading-state";
|
||||
import { Docs } from "@/util/docs";
|
||||
import { PREBUILD_VERSION } from "@/util/__generated__/prebuild-version";
|
||||
import { fetchDocs as fetchDocsContent } from "@/util/content-fetchers";
|
||||
import { faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faBookOpen,
|
||||
faChevronRight,
|
||||
faSearch,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
ListBox,
|
||||
ScrollShadow,
|
||||
SearchField,
|
||||
Separator,
|
||||
} from "@heroui/react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import Fuse from "fuse.js";
|
||||
@@ -20,6 +24,15 @@ import { useSearchParams } from "next/navigation";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
const featuredDocNames = [
|
||||
"Getting Started",
|
||||
"Compile from Source",
|
||||
"Architecture",
|
||||
"Armory",
|
||||
"Stagers",
|
||||
"Troubleshooting",
|
||||
];
|
||||
|
||||
const DocsIndexPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -70,13 +83,20 @@ const DocsIndexPage: NextPage = () => {
|
||||
}, [docs, name, visibleDocs]);
|
||||
|
||||
const listboxClasses =
|
||||
"p-0 gap-0 divide-y divide-separator bg-surface overflow-visible rounded-lg shadow-sm";
|
||||
"flex flex-col gap-1 overflow-visible bg-transparent p-0";
|
||||
|
||||
const featuredDocs = React.useMemo(() => {
|
||||
return featuredDocNames
|
||||
.map((featuredName) => docs?.docs.find((doc) => doc.name === featuredName))
|
||||
.filter((doc) => doc !== undefined);
|
||||
}, [docs]);
|
||||
|
||||
const renderFilterInput = (className?: string) => (
|
||||
<SearchField
|
||||
aria-label="Filter documents"
|
||||
className={className}
|
||||
fullWidth
|
||||
variant="secondary"
|
||||
value={filterValue}
|
||||
onChange={setFilterValue}
|
||||
>
|
||||
@@ -99,87 +119,144 @@ const DocsIndexPage: NextPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Sliver Docs: {name}</title>
|
||||
<title>{name ? `${name} · Sliver Docs` : "Sliver Documentation"}</title>
|
||||
</Head>
|
||||
<div className="px-4 pt-4 lg:hidden">
|
||||
<label
|
||||
htmlFor="docs-mobile-selector"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
Select a document
|
||||
</label>
|
||||
<div className="mt-2">{renderFilterInput()}</div>
|
||||
<select
|
||||
id="docs-mobile-selector"
|
||||
className="mt-3 w-full rounded-lg border border-separator bg-surface p-2 text-sm"
|
||||
value={name}
|
||||
onChange={(event) => {
|
||||
const selectedName = event.target.value;
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: selectedName ? { name: selectedName } : undefined,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="">Browse documents…</option>
|
||||
{mobileDocs.map((doc) => (
|
||||
<option key={doc.name} value={doc.name}>
|
||||
{doc.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12 lg:gap-8">
|
||||
<aside className="hidden lg:block lg:col-span-3">
|
||||
<div className="sticky top-16 ml-4 flex flex-col gap-3">
|
||||
{renderFilterInput("mt-2")}
|
||||
<ScrollShadow className="max-h-[calc(100vh-15rem)] sliver-scrollbar overflow-y-auto pr-1 rounded-lg">
|
||||
<ListBox
|
||||
aria-label="Toolbox Menu"
|
||||
className={listboxClasses}
|
||||
onAction={(key) => {
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: { name: String(key) },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{visibleDocs.map((doc) => (
|
||||
<ListBox.Item
|
||||
key={doc.name}
|
||||
id={doc.name}
|
||||
textValue={doc.name}
|
||||
className="h-12 rounded-none px-3 first:rounded-t-lg last:rounded-b-lg"
|
||||
>
|
||||
{doc.name}
|
||||
</ListBox.Item>
|
||||
))}
|
||||
</ListBox>
|
||||
</ScrollShadow>
|
||||
<div className="mx-auto w-full max-w-[90rem] px-4 pt-8 sm:px-6 lg:px-8 lg:pt-10">
|
||||
<div className="mb-8 lg:hidden">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex size-9 items-center justify-center rounded-2xl bg-surface-secondary text-accent">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">Documentation</p>
|
||||
<p className="text-xs text-muted">Browse the reference library</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div className="px-4 pb-8 lg:col-span-9 lg:px-8">
|
||||
{name !== "" ? (
|
||||
<Card className="mt-2">
|
||||
<Card.Header>
|
||||
<span className="text-3xl">{name}</span>
|
||||
</Card.Header>
|
||||
<Separator />
|
||||
<Card.Content>
|
||||
<div className="mt-5">{renderFilterInput()}</div>
|
||||
<label
|
||||
htmlFor="docs-mobile-selector"
|
||||
className="mt-4 block text-sm font-medium text-foreground"
|
||||
>
|
||||
Document
|
||||
</label>
|
||||
<select
|
||||
id="docs-mobile-selector"
|
||||
className="mt-2 w-full rounded-2xl border border-transparent bg-surface-secondary px-3 py-2.5 text-sm text-foreground outline-none focus:border-accent"
|
||||
value={name}
|
||||
onChange={(event) => {
|
||||
const selectedName = event.target.value;
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: selectedName ? { name: selectedName } : undefined,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="">Browse documents…</option>
|
||||
{mobileDocs.map((doc) => (
|
||||
<option key={doc.name} value={doc.name}>
|
||||
{doc.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid min-w-0 grid-cols-1 gap-8 lg:grid-cols-[17rem_minmax(0,1fr)] lg:gap-10">
|
||||
<aside className="sliver-sticky-sidebar hidden border-r border-separator/70 pr-6 lg:block">
|
||||
<div className="flex h-full min-h-0 flex-col gap-5">
|
||||
<div className="flex items-center gap-3 px-1">
|
||||
<span className="flex size-9 items-center justify-center rounded-2xl bg-surface-secondary text-accent">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">Documentation</p>
|
||||
<p className="text-xs text-muted">{docs.docs.length} articles</p>
|
||||
</div>
|
||||
</div>
|
||||
{renderFilterInput()}
|
||||
<ScrollShadow className="sliver-scrollbar min-h-0 flex-1 overscroll-contain overflow-y-auto pr-2">
|
||||
<ListBox
|
||||
aria-label="Documentation"
|
||||
className={listboxClasses}
|
||||
selectedKeys={name ? [name] : []}
|
||||
selectionMode="single"
|
||||
>
|
||||
{visibleDocs.map((doc) => (
|
||||
<ListBox.Item
|
||||
key={doc.name}
|
||||
id={doc.name}
|
||||
href={`/docs/?name=${encodeURIComponent(doc.name)}`}
|
||||
textValue={doc.name}
|
||||
className={`min-h-10 rounded-xl px-3 py-2 text-sm ${
|
||||
doc.name === name
|
||||
? "bg-surface font-medium text-foreground shadow-surface"
|
||||
: "text-muted"
|
||||
}`}
|
||||
>
|
||||
{doc.name}
|
||||
</ListBox.Item>
|
||||
))}
|
||||
</ListBox>
|
||||
</ScrollShadow>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div
|
||||
key={name || "documentation-overview"}
|
||||
className="min-w-0 pb-16 lg:pr-4"
|
||||
>
|
||||
{name !== "" ? (
|
||||
<article className="mx-auto w-full max-w-4xl">
|
||||
<header className="mb-8 border-b border-separator/70 pb-8">
|
||||
<p className="text-sm font-medium text-accent">Reference</p>
|
||||
<h1 className="mt-2 text-balance text-4xl font-semibold tracking-tight text-foreground sm:text-5xl">
|
||||
{name}
|
||||
</h1>
|
||||
</header>
|
||||
<MarkdownViewer
|
||||
key={name}
|
||||
markdown={markdown || ""}
|
||||
demoteTopLevelHeading
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="mt-8 text-center text-2xl text-foreground/90">
|
||||
Welcome to the Sliver Wiki!
|
||||
<div className="mt-2 text-xl text-muted">
|
||||
Please select a document
|
||||
</article>
|
||||
) : (
|
||||
<div className="mx-auto w-full max-w-5xl pt-4 lg:pt-8">
|
||||
<h1 className="text-sm font-medium text-accent">
|
||||
Sliver reference
|
||||
</h1>
|
||||
<p className="mt-4 max-w-2xl text-lg leading-8 text-muted">
|
||||
Browse configuration, transports, extensions, payloads, and
|
||||
troubleshooting guidance for the Sliver framework.
|
||||
</p>
|
||||
|
||||
<div className="mt-10 grid gap-4 sm:grid-cols-2">
|
||||
{featuredDocs.map((doc) => (
|
||||
<Card key={doc.name} className="h-full">
|
||||
<Card.Header>
|
||||
<Card.Title>{doc.name}</Card.Title>
|
||||
<Card.Description>
|
||||
Open this topic in the Sliver reference.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Footer className="mt-auto">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: { name: doc.name },
|
||||
});
|
||||
}}
|
||||
>
|
||||
Open article
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
+190
-122
@@ -1,144 +1,212 @@
|
||||
import AsciinemaPlayer from "@/components/asciinema";
|
||||
import { SliversIcon } from "@/components/icons/slivers";
|
||||
import TutorialCard from "@/components/tutorial-card";
|
||||
import { Themes } from "@/util/themes";
|
||||
import { faDownload, faExternalLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faDownload,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, Card, Separator } from "@heroui/react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Button, Card, buttonVariants } from "@heroui/react";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const isDarkTheme = React.useMemo(() => {
|
||||
return (resolvedTheme || Themes.DARK) !== Themes.LIGHT;
|
||||
}, [resolvedTheme]);
|
||||
|
||||
return (
|
||||
<div className="mt-6 flex flex-col gap-6 px-4 sm:px-6 lg:grid lg:grid-cols-12 lg:px-12">
|
||||
<div className="lg:col-span-6">
|
||||
<div className="w-full overflow-hidden rounded-xl border border-separator bg-surface shadow-sm">
|
||||
<div className="w-full overflow-x-auto lg:overflow-visible">
|
||||
<AsciinemaPlayer
|
||||
src="/asciinema/intro.cast"
|
||||
rows="18"
|
||||
cols="75"
|
||||
idleTimeLimit={60}
|
||||
preload={true}
|
||||
autoPlay={true}
|
||||
loop={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:col-span-6 lg:ml-2">
|
||||
<Card className="mx-auto max-w-3xl lg:mx-0">
|
||||
<Card.Header>
|
||||
<div className="flex items-center">
|
||||
<SliversIcon className="mr-2" height={28} />
|
||||
<span className="text-2xl">Sliver Command & Control</span>
|
||||
<>
|
||||
<Head>
|
||||
<title>Sliver C2 Documentation</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Documentation, tutorials, and community resources for the Sliver command and control framework."
|
||||
/>
|
||||
</Head>
|
||||
|
||||
<div className="relative overflow-hidden">
|
||||
<div className="sliver-grid-surface pointer-events-none absolute inset-x-0 top-0 h-[34rem] opacity-55" />
|
||||
|
||||
<div className="relative mx-auto w-full max-w-7xl px-4 pb-20 pt-8 sm:px-6 sm:pt-12 lg:px-8 lg:pt-16">
|
||||
<section className="grid min-w-0 grid-cols-1 items-stretch gap-6 lg:grid-cols-12">
|
||||
<div className="min-w-0 lg:col-span-7">
|
||||
<Card className="relative h-full min-w-0 gap-0 overflow-hidden rounded-3xl bg-black p-0 shadow-surface">
|
||||
<div className="absolute right-3 top-3 z-10 max-w-[calc(100%_-_1.5rem)]">
|
||||
<Link
|
||||
href="/tutorials"
|
||||
aria-label="Browse Sliver tutorials"
|
||||
className="inline-flex min-w-0 cursor-pointer items-center gap-2.5 rounded-xl border border-white/15 bg-black/55 px-3 py-2 text-white no-underline shadow-lg outline-none backdrop-blur-xl hover:bg-black/70 focus-visible:ring-2 focus-visible:ring-white/70"
|
||||
>
|
||||
<span className="flex size-7 shrink-0 items-center justify-center rounded-lg bg-white/10 text-white/90">
|
||||
<SliversIcon height={16} />
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-xs font-semibold">
|
||||
Sliver operator console
|
||||
</p>
|
||||
<p className="truncate text-[11px] text-white/60">
|
||||
Interactive command tour
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="sliver-terminal-frame min-h-[20rem] flex-1 overflow-x-auto bg-[#111315] sm:min-h-[24rem]">
|
||||
<AsciinemaPlayer
|
||||
className="sliver-asciinema-bottom-aligned h-full w-full sm:min-w-[35rem]"
|
||||
src="/asciinema/intro.cast"
|
||||
fit="width"
|
||||
hideControls
|
||||
rows="18"
|
||||
cols="75"
|
||||
idleTimeLimit={60}
|
||||
preload={true}
|
||||
autoPlay={true}
|
||||
loop={true}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Separator />
|
||||
<Card.Content>
|
||||
<p className={isDarkTheme ? "prose dark:prose-invert" : "prose prose-slate"}>
|
||||
Sliver is a powerful command and control (C2) framework designed
|
||||
to provide advanced capabilities for covertly managing and
|
||||
controlling remote systems. With Sliver, security professionals,
|
||||
red teams, and penetration testers can easily establish a secure
|
||||
and reliable communication channel over Mutual TLS, HTTP(S), DNS,
|
||||
or Wireguard with target machines. Enabling them to execute
|
||||
commands, gather information, and perform various
|
||||
post-exploitation activities. The framework offers a user-friendly
|
||||
console interface, extensive functionality, and support for
|
||||
multiple operating systems as well as multiple CPU architectures,
|
||||
making it an indispensable tool for conducting comprehensive
|
||||
offensive security operations.
|
||||
</p>
|
||||
<div className="mt-4 flex w-full gap-3">
|
||||
|
||||
<Card className="h-full bg-surface/80 shadow-surface backdrop-blur-2xl lg:col-span-5">
|
||||
<Card.Header className="pb-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-accent">
|
||||
Operator documentation
|
||||
</p>
|
||||
<h1 className="mt-1 text-balance text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
|
||||
Sliver Command & Control
|
||||
</h1>
|
||||
</div>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content className="pt-2">
|
||||
<p className="text-pretty text-base leading-7 text-muted">
|
||||
Sliver is a cross-platform command and control framework for
|
||||
professional red teams. Operate over mTLS, WireGuard, HTTP(S),
|
||||
and DNS from one console, with broad operating-system and CPU
|
||||
architecture support.
|
||||
</p>
|
||||
<p className="mt-4 text-sm leading-6 text-muted">
|
||||
Start with the operator guide, download the latest release, or
|
||||
extend your workflow with community packages from the Armory.
|
||||
</p>
|
||||
</Card.Content>
|
||||
|
||||
<Card.Footer className="mt-auto flex-col items-stretch gap-3 pt-5">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<Button
|
||||
fullWidth
|
||||
variant="primary"
|
||||
onPress={() =>
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: { name: "Getting Started" },
|
||||
})
|
||||
}
|
||||
>
|
||||
Get started
|
||||
</Button>
|
||||
<Button
|
||||
className="[--button-bg-hover:var(--color-purple-700)] [--button-bg-pressed:var(--color-purple-700)] [--button-bg:var(--color-purple-600)] [--button-fg:var(--color-white)]"
|
||||
fullWidth
|
||||
variant="tertiary"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://github.com/BishopFox/sliver/releases/latest",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
Latest release
|
||||
</Button>
|
||||
</div>
|
||||
<a
|
||||
className={buttonVariants({
|
||||
fullWidth: true,
|
||||
variant: "secondary",
|
||||
})}
|
||||
href="https://github.com/sliverarmory"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Browse the Armory
|
||||
<FontAwesomeIcon
|
||||
className="text-xs"
|
||||
icon={faArrowUpRightFromSquare}
|
||||
/>
|
||||
</a>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<section
|
||||
aria-labelledby="quick-guides-heading"
|
||||
className="mt-12 border-t border-separator/70 pt-10"
|
||||
>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<h2
|
||||
id="quick-guides-heading"
|
||||
className="text-sm font-medium text-accent"
|
||||
>
|
||||
Quick guides
|
||||
</h2>
|
||||
<blockquote className="mt-1 max-w-3xl text-xl font-medium italic leading-snug tracking-tight text-foreground sm:text-2xl">
|
||||
Bred as living shields, these slivers have proven unruly—they
|
||||
know they cannot be caught.
|
||||
</blockquote>
|
||||
</div>
|
||||
<Button
|
||||
className="flex-1"
|
||||
variant="primary"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://github.com/BishopFox/sliver/releases/latest",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
Download Latest Release
|
||||
</Button>
|
||||
<Button
|
||||
className="sliver-armory-ghost flex-1"
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
window.open(
|
||||
"https://github.com/sliverarmory",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}}
|
||||
onPress={() => router.push("/tutorials")}
|
||||
>
|
||||
Visit the Armory
|
||||
<FontAwesomeIcon icon={faExternalLink} />
|
||||
Browse all tutorials
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 mt-8">
|
||||
<Separator />
|
||||
</div>
|
||||
<div className="mt-6 grid min-w-0 grid-cols-1 gap-5 xl:grid-cols-2">
|
||||
<TutorialCard
|
||||
name="Getting Started"
|
||||
description="Install Sliver and open your first operator workflow."
|
||||
asciiCast="/asciinema/install-1.cast"
|
||||
cols="133"
|
||||
rows="32"
|
||||
idleTimeLimit={1}
|
||||
hideControls
|
||||
showButton={true}
|
||||
buttonText="Read guide"
|
||||
onPress={() => {
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: { name: "Getting Started" },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="col-span-12 mt-8">
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-9">
|
||||
<div className="sm:col-span-1 lg:col-span-3">
|
||||
<TutorialCard
|
||||
name="Getting Started"
|
||||
description="A quick start guide to get you up and running"
|
||||
asciiCast="/asciinema/install-1.cast"
|
||||
cols="133"
|
||||
rows="32"
|
||||
idleTimeLimit={1}
|
||||
showButton={true}
|
||||
buttonText="Read Docs"
|
||||
onPress={() => {
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: { name: "Getting Started" },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-1 lg:col-span-3">
|
||||
<TutorialCard
|
||||
name="Compile From Source"
|
||||
description="How to compile Sliver from source"
|
||||
asciiCast="/asciinema/compile-from-source.cast"
|
||||
cols="133"
|
||||
rows="32"
|
||||
idleTimeLimit={1}
|
||||
showButton={true}
|
||||
buttonText="Read Docs"
|
||||
onPress={() => {
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: { name: "Compile from Source" },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<TutorialCard
|
||||
name="Compile from Source"
|
||||
description="Build a development copy and understand the toolchain."
|
||||
asciiCast="/asciinema/compile-from-source.cast"
|
||||
cols="133"
|
||||
rows="32"
|
||||
idleTimeLimit={1}
|
||||
hideControls
|
||||
showButton={true}
|
||||
buttonText="Read guide"
|
||||
onPress={() => {
|
||||
router.push({
|
||||
pathname: "/docs",
|
||||
query: { name: "Compile from Source" },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-12 mb-8"></div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,66 +1,165 @@
|
||||
import MarkdownViewer from "@/components/markdown";
|
||||
import { useSearchContext } from "@/util/search-context";
|
||||
import { faChevronCircleRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import LoadingState from "@/components/loading-state";
|
||||
import { PREBUILD_VERSION } from "@/util/__generated__/prebuild-version";
|
||||
import { fetchDocs as fetchDocsContent } from "@/util/content-fetchers";
|
||||
import { SearchCtx } from "@/util/search-context";
|
||||
import {
|
||||
faChevronRight,
|
||||
faMagnifyingGlass,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, Card, Separator } from "@heroui/react";
|
||||
import { Button, Card, SearchField } from "@heroui/react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
export type SearchPageProps = {};
|
||||
|
||||
const createExcerpt = (markdown: string) => {
|
||||
return markdown
|
||||
.replace(/```[\s\S]*?```/g, " ")
|
||||
.replace(/!\[[^\]]*\]\([^)]*\)/g, " ")
|
||||
.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
|
||||
.replace(/[#>*_`~|-]/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.slice(0, 240);
|
||||
};
|
||||
|
||||
const SearchPage: NextPage = (props: SearchPageProps) => {
|
||||
const router = useRouter();
|
||||
const search = useSearchContext();
|
||||
const query = useSearchParams().get("search");
|
||||
const query = useSearchParams().get("search")?.trim() || "";
|
||||
const { data: docs, isLoading } = useQuery({
|
||||
queryKey: ["docs", PREBUILD_VERSION],
|
||||
queryFn: () => fetchDocsContent(PREBUILD_VERSION),
|
||||
});
|
||||
|
||||
const docsSearch = React.useMemo(() => {
|
||||
const search = new SearchCtx();
|
||||
if (docs) {
|
||||
search.addDocs(docs);
|
||||
}
|
||||
return search;
|
||||
}, [docs]);
|
||||
|
||||
const searchResults = React.useMemo(() => {
|
||||
if (query) {
|
||||
return search.searchDocs(query);
|
||||
return docsSearch.searchDocs(query);
|
||||
}
|
||||
return [];
|
||||
}, [query, search]);
|
||||
}, [docsSearch, query]);
|
||||
|
||||
if (isLoading || !docs) {
|
||||
return <LoadingState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-4 pb-8 pt-4">
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col gap-4">
|
||||
<div className="text-3xl">
|
||||
Search: "{query?.slice(0, 50)}"
|
||||
<div className="text-sm text-gray-500">
|
||||
{searchResults.length} Results
|
||||
<>
|
||||
<Head>
|
||||
<title>{query ? `Search: ${query} · Sliver Docs` : "Search · Sliver Docs"}</title>
|
||||
</Head>
|
||||
<div className="mx-auto w-full max-w-5xl px-4 pb-20 pt-12 sm:px-6 lg:px-8 lg:pt-16">
|
||||
<header className="max-w-3xl">
|
||||
<p className="text-sm font-medium text-accent">Search the reference</p>
|
||||
<h1 className="mt-2 text-balance text-4xl font-semibold tracking-tight text-foreground sm:text-5xl">
|
||||
Find exactly what you need.
|
||||
</h1>
|
||||
<p className="mt-4 text-lg leading-8 text-muted">
|
||||
Search configuration, workflows, transports, extensions, and
|
||||
troubleshooting notes across the Sliver documentation.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<SearchField
|
||||
key={query}
|
||||
aria-label="Search documentation"
|
||||
className="mt-8 max-w-2xl"
|
||||
defaultValue={query}
|
||||
fullWidth
|
||||
onClear={() => router.push("/search")}
|
||||
onSubmit={(value) => {
|
||||
const nextQuery = value.trim();
|
||||
if (nextQuery) {
|
||||
router.push({ pathname: "/search", query: { search: nextQuery } });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SearchField.Group>
|
||||
<SearchField.SearchIcon>
|
||||
<FontAwesomeIcon icon={faMagnifyingGlass} />
|
||||
</SearchField.SearchIcon>
|
||||
<SearchField.Input placeholder="Search documentation…" />
|
||||
<SearchField.ClearButton aria-label="Clear search" />
|
||||
</SearchField.Group>
|
||||
</SearchField>
|
||||
|
||||
<div className="mt-10 flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-foreground">
|
||||
{query ? `Results for “${query.slice(0, 50)}”` : "Search results"}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm tabular-nums text-muted">
|
||||
{searchResults.length} {searchResults.length === 1 ? "result" : "results"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{searchResults.map((doc) => (
|
||||
<Card key={doc.name}>
|
||||
<Card.Content className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<span className="text-xl">{doc.name}</span>
|
||||
<div className="flex-1"></div>
|
||||
{searchResults.length > 0 ? (
|
||||
<div className="mt-5 grid gap-4">
|
||||
{searchResults.map((doc) => (
|
||||
<Card key={doc.name}>
|
||||
<Card.Header>
|
||||
<Card.Title>{doc.name}</Card.Title>
|
||||
<Card.Description className="mt-1 max-w-3xl leading-6">
|
||||
{createExcerpt(doc.content) || "Open this article in the Sliver reference."}
|
||||
{doc.content.length > 240 ? "…" : ""}
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Footer>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
router.push({ pathname: "/docs", query: { name: doc.name } });
|
||||
}}
|
||||
>
|
||||
Open article
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Card className="mt-5">
|
||||
<Card.Content className="flex flex-col items-start gap-3 p-8">
|
||||
<span className="flex size-10 items-center justify-center rounded-2xl bg-surface-secondary text-accent">
|
||||
<FontAwesomeIcon icon={faMagnifyingGlass} />
|
||||
</span>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
{query ? "No matching documentation" : "Start with a keyword"}
|
||||
</h2>
|
||||
<p className="mt-1 max-w-xl text-sm leading-6 text-muted">
|
||||
{query
|
||||
? "Try a command name, transport, feature, or a shorter phrase."
|
||||
: "Search for topics like staging, WireGuard, profiles, or extensions."}
|
||||
</p>
|
||||
</div>
|
||||
{query ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="w-full sm:w-[150px]"
|
||||
onPress={() => {
|
||||
router.push(`/docs?name=${doc.name}`);
|
||||
}}
|
||||
variant="outline"
|
||||
onPress={() => router.push("/docs")}
|
||||
>
|
||||
Full Doc
|
||||
<FontAwesomeIcon icon={faChevronCircleRight} />
|
||||
Browse all documentation
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="max-h-[200px] overflow-hidden">
|
||||
<MarkdownViewer markdown={doc.content} />
|
||||
</div>
|
||||
) : null}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Youtube from "@/components/youtube";
|
||||
import { faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Accordion, Button, Card } from "@heroui/react";
|
||||
import { Accordion, Card } from "@heroui/react";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import React from "react";
|
||||
@@ -21,7 +21,7 @@ type TalkSection = {
|
||||
|
||||
const workshopTalks: Talk[] = [
|
||||
{
|
||||
title:"Getting Started with Sliver v1.6",
|
||||
title: "Getting Started with Sliver v1.6",
|
||||
description: "Introductory workshop covering basics and new features.",
|
||||
url: "https://www.youtube.com/watch?v=IOiyXYp1lDc",
|
||||
},
|
||||
@@ -47,7 +47,7 @@ const generalTradecraftTalks: Talk[] = [
|
||||
title: "The Sliver C2 Framework",
|
||||
description: "General discussion of C2 design.",
|
||||
url: "https://www.youtube.com/watch?v=tkjMZuZ_8nw",
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const communityTalks: Talk[] = [
|
||||
@@ -100,81 +100,124 @@ const talkSections: TalkSection[] = [
|
||||
];
|
||||
|
||||
const TalksIndexPage: NextPage = () => {
|
||||
const [footerHidden, setFooterHidden] = React.useState<Record<string, boolean>>(
|
||||
{}
|
||||
);
|
||||
const actionButtonClassName =
|
||||
"ml-auto h-auto min-h-0 shrink-0 px-0 py-0 text-sm font-semibold [--button-bg-hover:transparent] [--button-bg-pressed:transparent] [--button-bg:transparent] [--button-fg:var(--warning)]";
|
||||
const [overlayHidden, setOverlayHidden] = React.useState<
|
||||
Record<string, boolean>
|
||||
>({});
|
||||
|
||||
const hideFooterFor = React.useCallback((url: string) => {
|
||||
setFooterHidden((prev) => (prev[url] ? prev : { ...prev, [url]: true }));
|
||||
const hideOverlayFor = React.useCallback((url: string) => {
|
||||
setOverlayHidden((current) =>
|
||||
current[url] ? current : { ...current, [url]: true },
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Sliver Talks</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Sliver workshops, technical talks, and community videos."
|
||||
/>
|
||||
</Head>
|
||||
<div className="mt-6 px-4 pb-8 sm:px-6 lg:px-12">
|
||||
<Accordion
|
||||
allowsMultipleExpanded
|
||||
defaultExpandedKeys={["workshops", "general-tradecraft"]}
|
||||
hideSeparator
|
||||
className="px-0"
|
||||
>
|
||||
{talkSections.map((section) => (
|
||||
<Accordion.Item
|
||||
key={section.key}
|
||||
id={section.key}
|
||||
>
|
||||
<Accordion.Heading>
|
||||
<Accordion.Trigger>
|
||||
<span className="flex flex-col items-start text-left">
|
||||
<span className="text-lg font-semibold">{section.title}</span>
|
||||
<span className="text-sm text-muted">{section.description}</span>
|
||||
</span>
|
||||
<Accordion.Indicator className="ml-auto" />
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Heading>
|
||||
<Accordion.Panel>
|
||||
<Accordion.Body>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-9">
|
||||
{section.talks.map((talk) => (
|
||||
<div key={talk.url} className="sm:col-span-1 lg:col-span-3">
|
||||
<Card className="relative z-0 gap-0 overflow-hidden p-0">
|
||||
<Youtube
|
||||
className="w-full"
|
||||
url={talk.url}
|
||||
title={talk.title}
|
||||
onPlay={() => hideFooterFor(talk.url)}
|
||||
/>
|
||||
|
||||
{footerHidden[talk.url] ? null : (
|
||||
<Card.Footer className="absolute bottom-0 left-0 right-0 z-10 border-t border-white/20 bg-black/40 p-3 backdrop-blur-md">
|
||||
<div className="flex min-h-10 w-full items-center gap-4">
|
||||
<p className="min-w-0 flex-1 text-sm leading-snug text-white/80">{talk.description}</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className={actionButtonClassName}
|
||||
onPress={() => {
|
||||
window.open(talk.url, "_blank", "noopener,noreferrer");
|
||||
}}
|
||||
>
|
||||
Watch <FontAwesomeIcon icon={faChevronRight} />
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Footer>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Accordion.Body>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion>
|
||||
<div className="relative overflow-hidden">
|
||||
<div className="sliver-grid-surface pointer-events-none absolute inset-x-0 top-0 h-72 opacity-35" />
|
||||
|
||||
<div className="relative mx-auto w-full max-w-7xl px-4 pb-20 pt-8 sm:px-6 sm:pt-10 lg:px-8 lg:pt-12">
|
||||
<header className="mb-6 max-w-3xl">
|
||||
<p className="text-sm font-medium text-accent">Talks & workshops</p>
|
||||
<h1 className="mt-1 text-balance text-3xl font-semibold tracking-tight text-foreground sm:text-4xl">
|
||||
Watch Sliver in the field.
|
||||
</h1>
|
||||
<p className="mt-3 max-w-2xl text-base leading-7 text-muted">
|
||||
Practical workshops, technical deep dives, and community sessions
|
||||
collected in one compact library.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<Accordion
|
||||
allowsMultipleExpanded
|
||||
defaultExpandedKeys={["workshops", "general-tradecraft"]}
|
||||
hideSeparator
|
||||
className="w-full px-0"
|
||||
>
|
||||
{talkSections.map((section) => (
|
||||
<Accordion.Item key={section.key} id={section.key}>
|
||||
<Accordion.Heading>
|
||||
<Accordion.Trigger>
|
||||
<span className="flex flex-col items-start text-left">
|
||||
<span className="flex items-center gap-2 text-lg font-semibold text-foreground">
|
||||
{section.title}
|
||||
<span className="text-xs font-normal tabular-nums text-muted">
|
||||
{section.talks.length}
|
||||
</span>
|
||||
</span>
|
||||
<span className="mt-1 text-sm text-muted">
|
||||
{section.description}
|
||||
</span>
|
||||
</span>
|
||||
<Accordion.Indicator className="ml-auto" />
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Heading>
|
||||
|
||||
<Accordion.Panel>
|
||||
<Accordion.Body>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{section.talks.map((talk) => (
|
||||
<Card
|
||||
key={talk.url}
|
||||
className="relative z-0 gap-0 overflow-hidden bg-black p-0 shadow-surface"
|
||||
>
|
||||
<Youtube
|
||||
className="w-full"
|
||||
url={talk.url}
|
||||
title={talk.title}
|
||||
onInteract={() => hideOverlayFor(talk.url)}
|
||||
onPlay={() => hideOverlayFor(talk.url)}
|
||||
/>
|
||||
|
||||
{overlayHidden[talk.url] ? null : (
|
||||
<>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-32 bg-gradient-to-t from-black/85 via-black/35 to-transparent"
|
||||
/>
|
||||
<div className="pointer-events-none absolute inset-x-3 bottom-3 z-10">
|
||||
<div className="flex items-end gap-3 rounded-2xl border border-white/15 bg-black/55 p-3 text-white shadow-xl backdrop-blur-xl">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-sm font-semibold leading-snug text-white">
|
||||
{talk.title}
|
||||
</h3>
|
||||
<p className="mt-0.5 truncate text-xs text-white/65">
|
||||
{talk.description}
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href={talk.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
aria-label={`Watch ${talk.title} on YouTube`}
|
||||
className="pointer-events-auto inline-flex min-h-9 shrink-0 items-center gap-1.5 rounded-xl px-2.5 text-xs font-semibold text-white no-underline outline-none transition-colors hover:bg-white/10 focus-visible:ring-2 focus-visible:ring-white/70"
|
||||
>
|
||||
Watch
|
||||
<FontAwesomeIcon
|
||||
className="text-[10px]"
|
||||
icon={faArrowUpRightFromSquare}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</Accordion.Body>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -3,14 +3,18 @@ import LoadingState from "@/components/loading-state";
|
||||
import { Tutorials } from "@/util/tutorials";
|
||||
import { PREBUILD_VERSION } from "@/util/__generated__/prebuild-version";
|
||||
import { fetchTutorials as fetchTutorialsContent } from "@/util/content-fetchers";
|
||||
import { faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faChevronRight,
|
||||
faGraduationCap,
|
||||
faSearch,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
ListBox,
|
||||
ScrollShadow,
|
||||
SearchField,
|
||||
Separator,
|
||||
} from "@heroui/react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import Fuse from "fuse.js";
|
||||
@@ -70,13 +74,14 @@ const TutorialsIndexPage: NextPage = () => {
|
||||
}, [name, tutorials, visibleTutorials]);
|
||||
|
||||
const listboxClasses =
|
||||
"p-0 gap-0 divide-y divide-separator bg-surface overflow-visible rounded-lg shadow-sm";
|
||||
"flex flex-col gap-1 overflow-visible bg-transparent p-0";
|
||||
|
||||
const renderFilterInput = (className?: string) => (
|
||||
<SearchField
|
||||
aria-label="Filter tutorials"
|
||||
className={className}
|
||||
fullWidth
|
||||
variant="secondary"
|
||||
value={filterValue}
|
||||
onChange={setFilterValue}
|
||||
>
|
||||
@@ -99,87 +104,146 @@ const TutorialsIndexPage: NextPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Sliver Tutorial: {name}</title>
|
||||
<title>{name ? `${name} · Sliver Tutorials` : "Sliver Tutorials"}</title>
|
||||
</Head>
|
||||
<div className="px-4 pt-4 lg:hidden">
|
||||
<label
|
||||
htmlFor="tutorials-mobile-selector"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
Select a tutorial
|
||||
</label>
|
||||
<div className="mt-2">{renderFilterInput()}</div>
|
||||
<select
|
||||
id="tutorials-mobile-selector"
|
||||
className="mt-3 w-full rounded-lg border border-separator bg-surface p-2 text-sm"
|
||||
value={name}
|
||||
onChange={(event) => {
|
||||
const selectedName = event.target.value;
|
||||
router.push({
|
||||
pathname: "/tutorials",
|
||||
query: selectedName ? { name: selectedName } : undefined,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="">Browse tutorials…</option>
|
||||
{mobileTutorials.map((tutorial) => (
|
||||
<option key={tutorial.name} value={tutorial.name}>
|
||||
{tutorial.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-12 lg:gap-8">
|
||||
<aside className="hidden lg:block lg:col-span-3">
|
||||
<div className="sticky top-16 ml-4 flex flex-col gap-3">
|
||||
{renderFilterInput("mt-2")}
|
||||
<ScrollShadow className="max-h-[calc(100vh-6rem)] sliver-scrollbar overflow-y-auto pr-1 rounded-lg">
|
||||
<ListBox
|
||||
aria-label="Toolbox Menu"
|
||||
className={listboxClasses}
|
||||
onAction={(key) => {
|
||||
router.push({
|
||||
pathname: "/tutorials",
|
||||
query: { name: String(key) },
|
||||
});
|
||||
}}
|
||||
>
|
||||
{visibleTutorials.map((tutorial) => (
|
||||
<ListBox.Item
|
||||
key={tutorial.name}
|
||||
id={tutorial.name}
|
||||
textValue={tutorial.name}
|
||||
className="h-12 rounded-none px-3 first:rounded-t-lg last:rounded-b-lg"
|
||||
>
|
||||
{tutorial.name}
|
||||
</ListBox.Item>
|
||||
))}
|
||||
</ListBox>
|
||||
</ScrollShadow>
|
||||
<div className="mx-auto w-full max-w-[90rem] px-4 pt-8 sm:px-6 lg:px-8 lg:pt-10">
|
||||
<div className="mb-8 lg:hidden">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex size-9 items-center justify-center rounded-2xl bg-surface-secondary text-accent">
|
||||
<FontAwesomeIcon icon={faGraduationCap} />
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">Tutorials</p>
|
||||
<p className="text-xs text-muted">Follow a guided workflow</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div className="px-4 pb-8 lg:col-span-9 lg:px-8">
|
||||
{name !== "" ? (
|
||||
<Card className="mt-2">
|
||||
<Card.Header>
|
||||
<span className="text-3xl">{name}</span>
|
||||
</Card.Header>
|
||||
<Separator />
|
||||
<Card.Content>
|
||||
<div className="mt-5">{renderFilterInput()}</div>
|
||||
<label
|
||||
htmlFor="tutorials-mobile-selector"
|
||||
className="mt-4 block text-sm font-medium text-foreground"
|
||||
>
|
||||
Tutorial
|
||||
</label>
|
||||
<select
|
||||
id="tutorials-mobile-selector"
|
||||
className="mt-2 w-full rounded-2xl border border-transparent bg-surface-secondary px-3 py-2.5 text-sm text-foreground outline-none focus:border-accent"
|
||||
value={name}
|
||||
onChange={(event) => {
|
||||
const selectedName = event.target.value;
|
||||
router.push({
|
||||
pathname: "/tutorials",
|
||||
query: selectedName ? { name: selectedName } : undefined,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="">Browse tutorials…</option>
|
||||
{mobileTutorials.map((tutorial) => (
|
||||
<option key={tutorial.name} value={tutorial.name}>
|
||||
{tutorial.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid min-w-0 grid-cols-1 gap-8 lg:grid-cols-[17rem_minmax(0,1fr)] lg:gap-10">
|
||||
<aside className="sliver-sticky-sidebar hidden border-r border-separator/70 pr-6 lg:block">
|
||||
<div className="flex h-full min-h-0 flex-col gap-5">
|
||||
<div className="flex items-center gap-3 px-1">
|
||||
<span className="flex size-9 items-center justify-center rounded-2xl bg-surface-secondary text-accent">
|
||||
<FontAwesomeIcon icon={faGraduationCap} />
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-foreground">Tutorials</p>
|
||||
<p className="text-xs text-muted">
|
||||
{tutorials.tutorials.length} chapters
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{renderFilterInput()}
|
||||
<ScrollShadow className="sliver-scrollbar min-h-0 flex-1 overscroll-contain overflow-y-auto pr-2">
|
||||
<ListBox
|
||||
aria-label="Tutorials"
|
||||
className={listboxClasses}
|
||||
selectedKeys={name ? [name] : []}
|
||||
selectionMode="single"
|
||||
>
|
||||
{visibleTutorials.map((tutorial) => (
|
||||
<ListBox.Item
|
||||
key={tutorial.name}
|
||||
id={tutorial.name}
|
||||
href={`/tutorials/?name=${encodeURIComponent(tutorial.name)}`}
|
||||
textValue={tutorial.name}
|
||||
className={`min-h-10 rounded-xl px-3 py-2 text-sm ${
|
||||
tutorial.name === name
|
||||
? "bg-surface font-medium text-foreground shadow-surface"
|
||||
: "text-muted"
|
||||
}`}
|
||||
>
|
||||
{tutorial.name}
|
||||
</ListBox.Item>
|
||||
))}
|
||||
</ListBox>
|
||||
</ScrollShadow>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div
|
||||
key={name || "tutorials-overview"}
|
||||
className="min-w-0 pb-16 lg:pr-4"
|
||||
>
|
||||
{name !== "" ? (
|
||||
<article className="mx-auto w-full max-w-4xl">
|
||||
<header className="mb-8 border-b border-separator/70 pb-8">
|
||||
<p className="text-sm font-medium text-accent">Tutorial</p>
|
||||
<h1 className="mt-2 text-balance text-4xl font-semibold tracking-tight text-foreground sm:text-5xl">
|
||||
{name}
|
||||
</h1>
|
||||
</header>
|
||||
<MarkdownViewer
|
||||
key={name}
|
||||
markdown={markdown || ""}
|
||||
demoteTopLevelHeading
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="mt-8 text-center text-2xl text-foreground/90">
|
||||
Welcome to the Sliver Tutorials!
|
||||
<div className="mt-2 text-xl text-muted">
|
||||
Please select a chapter
|
||||
</article>
|
||||
) : (
|
||||
<div className="mx-auto w-full max-w-5xl pt-4 lg:pt-8">
|
||||
<h1 className="text-sm font-medium text-accent">
|
||||
Guided learning
|
||||
</h1>
|
||||
<p className="mt-4 max-w-2xl text-lg leading-8 text-muted">
|
||||
Follow practical walkthroughs covering setup, sessions,
|
||||
staging, pivots, scripting, and post-exploitation workflows.
|
||||
</p>
|
||||
|
||||
<div className="mt-10 grid gap-4 sm:grid-cols-2">
|
||||
{tutorials.tutorials.slice(0, 6).map((tutorial) => (
|
||||
<Card key={tutorial.name} className="h-full">
|
||||
<Card.Header>
|
||||
<Card.Title>{tutorial.name}</Card.Title>
|
||||
<Card.Description>
|
||||
Continue through this guided Sliver workflow.
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Footer className="mt-auto">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
router.push({
|
||||
pathname: "/tutorials",
|
||||
query: { name: tutorial.name },
|
||||
});
|
||||
}}
|
||||
>
|
||||
Start tutorial
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</Button>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -6,14 +6,39 @@
|
||||
:root {
|
||||
--font-mono: "Fira Code", ui-monospace, SFMono-Regular, Menlo, Monaco,
|
||||
Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--sliver-navbar-height: 4rem;
|
||||
--sliver-footer-height: 4.75rem;
|
||||
}
|
||||
|
||||
/*
|
||||
Keep html as the single scrolling element. Setting overflow on both html
|
||||
and body breaks sticky sidebar behavior on docs/tutorial pages.
|
||||
*/
|
||||
/* Keep html as the single page scroller so content can pass beneath the shell. */
|
||||
html {
|
||||
overflow-y: scroll;
|
||||
scroll-padding-top: calc(var(--sliver-navbar-height) + 1.5rem);
|
||||
scroll-padding-bottom: calc(
|
||||
var(--sliver-footer-height) + env(safe-area-inset-bottom)
|
||||
);
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100dvh;
|
||||
background:
|
||||
radial-gradient(circle at 12% -10%, color-mix(in oklab, var(--accent) 12%, transparent), transparent 28rem),
|
||||
radial-gradient(circle at 88% 4%, color-mix(in oklab, var(--surface-tertiary) 55%, transparent), transparent 26rem),
|
||||
var(--background);
|
||||
}
|
||||
|
||||
.sliver-page-content {
|
||||
padding-bottom: calc(
|
||||
var(--sliver-footer-height) + env(safe-area-inset-bottom)
|
||||
);
|
||||
}
|
||||
|
||||
.sliver-fixed-footer {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: color-mix(in oklab, var(--accent) 32%, transparent);
|
||||
}
|
||||
|
||||
/* Fallback for engines that do not support scrollbar-gutter. */
|
||||
@@ -31,6 +56,22 @@ html {
|
||||
html {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.sliver-sticky-sidebar {
|
||||
position: sticky;
|
||||
top: calc(var(--sliver-navbar-height) + 2.5rem);
|
||||
align-self: start;
|
||||
height: calc(
|
||||
100dvh - var(--sliver-navbar-height) - var(--sliver-footer-height) -
|
||||
env(safe-area-inset-bottom) - 3.5rem
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
:root {
|
||||
--sliver-footer-height: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
code,
|
||||
@@ -44,7 +85,7 @@ pre {
|
||||
|
||||
.sliver-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(148, 163, 184, 0.7) transparent;
|
||||
scrollbar-color: color-mix(in oklab, var(--muted) 58%, transparent) transparent;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
@@ -57,33 +98,35 @@ pre {
|
||||
}
|
||||
|
||||
.sliver-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(148, 163, 184, 0.7);
|
||||
background-color: color-mix(in oklab, var(--muted) 58%, transparent);
|
||||
border-radius: 9999px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
.sliver-navbar-glass {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--separator);
|
||||
background-color: color-mix(in oklab, var(--surface) 72%, transparent);
|
||||
-webkit-backdrop-filter: blur(18px) saturate(1.18);
|
||||
backdrop-filter: blur(18px) saturate(1.18);
|
||||
.sliver-grid-surface {
|
||||
background-image:
|
||||
linear-gradient(color-mix(in oklab, var(--separator) 35%, transparent) 1px, transparent 1px),
|
||||
linear-gradient(90deg, color-mix(in oklab, var(--separator) 35%, transparent) 1px, transparent 1px);
|
||||
background-size: 32px 32px;
|
||||
mask-image: linear-gradient(to bottom, black, transparent 92%);
|
||||
}
|
||||
|
||||
.sliver-armory-ghost {
|
||||
--button-bg: transparent;
|
||||
--button-bg-hover: rgb(109 40 217 / 0.16);
|
||||
--button-bg-pressed: rgb(109 40 217 / 0.24);
|
||||
--button-fg: #a78bfa;
|
||||
.sliver-terminal-frame .ap-wrapper {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.dark .sliver-scrollbar {
|
||||
scrollbar-color: rgba(79, 97, 125, 0.8) transparent;
|
||||
.sliver-asciinema-bottom-aligned .ap-wrapper {
|
||||
align-items: flex-end;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.dark .sliver-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(79, 97, 125, 0.8);
|
||||
.sliver-terminal-frame .asciinema-player {
|
||||
background: #111315;
|
||||
}
|
||||
|
||||
.sliver-asciinema-controls-hidden .ap-control-bar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.asciinema-player {
|
||||
@@ -94,3 +137,19 @@ pre {
|
||||
.asciinema-player .asciinema-terminal {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.markdown-body :where(h2, h3, h4) {
|
||||
scroll-margin-top: calc(var(--sliver-navbar-height) + 1.5rem);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user