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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/components/Codeblock/Codeblock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,29 @@ export type CodeblockProps = React.HTMLAttributes<HTMLDivElement> & {
fromHomepage?: boolean
}

const LANGUAGE_LABELS: Record<string, string> = {
js: "JS",
javascript: "JS",
ts: "TS",
typescript: "TS",
jsx: "JSX",
tsx: "TSX",
json: "JSON",
python: "Python",
py: "Python",
solidity: "Solidity",
sol: "Solidity",
bash: "Shell",
sh: "Shell",
shell: "Shell",
yaml: "YAML",
yml: "YAML",
html: "HTML",
css: "CSS",
rust: "Rust",
go: "Go",
}

const Codeblock = async ({
children,
allowCollapse = true,
Expand Down Expand Up @@ -61,11 +84,13 @@ const Codeblock = async ({
resolvedLang !== "text" && COPY_WIDGET_LANGS.has(resolvedLang)
const shouldShowLineNumbers = resolvedLang !== "bash"
const totalLines = codeText.split("\n").length
const languageLabel = LANGUAGE_LABELS[language] ?? ""

return (
<CodeblockClient
html={html}
codeText={codeText}
languageLabel={languageLabel}
allowCollapse={allowCollapse}
shouldShowCopyWidget={shouldShowCopyWidget}
shouldShowLineNumbers={shouldShowLineNumbers}
Expand Down
128 changes: 76 additions & 52 deletions src/components/Codeblock/CodeblockClient.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
"use client"

import { useState } from "react"
import { Clipboard, ClipboardCheck } from "lucide-react"
import { Check, ChevronDown, ChevronUp, Copy } from "lucide-react"

import CopyToClipboard from "@/components/CopyToClipboard"
import { Button } from "@/components/ui/buttons/Button"
import { Flex } from "@/components/ui/flex"

import { cn } from "@/lib/utils/cn"

Expand All @@ -16,6 +14,7 @@ import { useTranslation } from "@/hooks/useTranslation"
type CodeblockClientProps = {
html: string
codeText: string
languageLabel: string
allowCollapse: boolean
shouldShowCopyWidget: boolean
shouldShowLineNumbers: boolean
Expand All @@ -27,6 +26,7 @@ type CodeblockClientProps = {
const CodeblockClient = ({
html,
codeText,
languageLabel,
allowCollapse,
shouldShowCopyWidget,
shouldShowLineNumbers,
Expand All @@ -35,66 +35,90 @@ const CodeblockClient = ({
className,
}: CodeblockClientProps) => {
const { t } = useTranslation("common")
const [isCollapsed, setIsCollapsed] = useState(allowCollapse)

const isCollapsable = totalLines - 1 > LINES_BEFORE_COLLAPSABLE
const hasTopBar = shouldShowCopyWidget || isCollapsable
const showButtons = !fromHomepage && hasTopBar
const codeLineCount = Math.max(totalLines - 1, 1)
const isCollapsable =
!fromHomepage && allowCollapse && codeLineCount > LINES_BEFORE_COLLAPSABLE
const [isCollapsed, setIsCollapsed] = useState(isCollapsable)

const showLanguageLabel = !fromHomepage && languageLabel.length > 0
const showCopy = !fromHomepage && shouldShowCopyWidget
const showCornerCollapse = isCollapsable && !isCollapsed
const hasCornerUi = showLanguageLabel || showCopy || showCornerCollapse

return (
/* Overwrites codeblocks inheriting RTL styling in Right-To-Left script languages (e.g., Arabic) */
/* Context: https://github.com/ethereum/ethereum-org-website/issues/6202 */
<div className={cn("relative", className)} dir="ltr">
/* Force LTR — codeblocks shouldn't inherit RTL from Arabic/Urdu pages.
Context: https://github.com/ethereum/ethereum-org-website/issues/6202 */
<div
dir="ltr"
className={cn(
"codeblock-shiki group/codeblock relative my-8",
hasCornerUi && "has-corner",
shouldShowLineNumbers && "line-numbers",
isCollapsable && isCollapsed && "is-collapsed",
className
)}
>
<div
className={cn(
"codeblock-shiki overflow-auto rounded bg-background-highlight text-primary",
hasTopBar && "has-top-bar",
shouldShowLineNumbers && "line-numbers"
)}
style={{
maxHeight: isCollapsed
? `calc((1.2rem * ${LINES_BEFORE_COLLAPSABLE}) + 4.185rem)`
: "none",
}}
data-code-surface
className="overflow-auto rounded-md bg-background-highlight text-primary"
dangerouslySetInnerHTML={{ __html: html }}
/>
{showButtons && (
<Flex className="absolute end-4 top-3 z-10 justify-end gap-2">
{allowCollapse && isCollapsable && (
<Button
variant="outline"
className="bg-background-highlight"
size="sm"
onClick={() => setIsCollapsed(!isCollapsed)}

{hasCornerUi && (
<div className="pointer-events-none absolute inset-e-4 top-1.5 flex items-center gap-1 font-mono text-[10px] leading-none tracking-[0.08em] text-disabled uppercase">
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<div className="pointer-events-none absolute inset-e-4 top-1.5 flex items-center gap-1 font-mono text-[10px] leading-none tracking-[0.08em] text-disabled uppercase">
<div className="pointer-events-none absolute inset-e-4 top-1.5 flex items-center gap-1 font-mono text-2xs leading-none tracking-[0.08em] text-disabled uppercase">

{showCornerCollapse && (
<button
type="button"
onClick={() => setIsCollapsed(true)}
aria-label={t("show-less")}
className="pointer-events-auto inline-flex h-6 items-center gap-1 rounded px-1.5 opacity-0 transition-opacity group-focus-within/codeblock:opacity-100 group-hover/codeblock:opacity-100 hover:text-primary focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-primary"
>
{isCollapsed ? t("show-all") : t("show-less")}
</Button>
<ChevronUp className="size-3" aria-hidden="true" />
</button>
)}
{shouldShowCopyWidget && (
<Button
variant="outline"
size="sm"
asChild
className="bg-background-highlight"
{showCopy && (
<CopyToClipboard
text={codeText}
className="pointer-events-auto inline-flex h-6 items-center gap-1 rounded px-1.5 text-disabled opacity-0 transition-opacity group-focus-within/codeblock:opacity-100 group-hover/codeblock:opacity-100 hover:text-primary focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-primary"
>
<CopyToClipboard text={codeText}>
{(isCopied) =>
!isCopied ? (
<>
{t("copy")}
<Clipboard className="size-[1em]" />
</>
{(isCopied) => (
<>
<span className="sr-only">
{isCopied ? t("copied") : t("copy")}
</span>
{isCopied ? (
<Check className="size-3" aria-hidden="true" />
) : (
<>
{t("copied")}
<ClipboardCheck className="size-[1em]" />
</>
)
}
</CopyToClipboard>
</Button>
<Copy className="size-3" aria-hidden="true" />
)}
</>
)}
</CopyToClipboard>
)}
</Flex>
{showLanguageLabel && (
<span className="px-1" aria-hidden="true">
{languageLabel}
</span>
)}
</div>
)}

{isCollapsable && isCollapsed && (
<button
type="button"
onClick={() => setIsCollapsed(false)}
aria-label={`${t("show-all")} (${codeLineCount})`}
className="codeblock-expander group/expander"
>
<span>
{t("show-all")}{" "}
<span className="text-disabled transition-colors duration-[120ms] ease-out group-hover/expander:text-primary">
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<span className="text-disabled transition-colors duration-[120ms] ease-out group-hover/expander:text-primary">
<span className="text-disabled transition-colors duration-120 ease-out group-hover/expander:text-primary">

({codeLineCount})
</span>
</span>
<ChevronDown className="size-3.5" aria-hidden="true" />
</button>
)}
</div>
)
Expand Down
93 changes: 81 additions & 12 deletions src/styles/codeblock.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/* SHIKI CODEBLOCK STYLES
Drives the highlighted output from <Codeblock /> (server-rendered via
shiki with dual themes: one-light + one-dark-pro). Shiki emits CSS
variables (--shiki-light / --shiki-dark) — we wire them up to `color`
so the theme switch works without re-highlighting on the client. */
Drives the highlighted output from <Codeblock />. Shiki emits CSS variables
(--shiki-light / --shiki-dark) so the theme switch happens without
re-highlighting on the client. */

.codeblock-shiki .shiki,
.codeblock-shiki .shiki span {
Expand All @@ -16,20 +15,35 @@
background-color: transparent;
}

.codeblock-shiki .shiki,
.codeblock-shiki .shiki code {
font-size: 0.8125rem;
line-height: 1.55;
}

.codeblock-shiki .shiki {
margin: 0;
padding: 1.5rem 1rem;
padding: 0.875rem 1rem;
width: fit-content;
min-width: 100%;
overflow: visible;
tab-size: 2;
}

/* When corner UI is overlaid, give the first line headroom so it never sits
underneath the language watermark / copy button. */
.codeblock-shiki.has-corner .shiki {
padding-top: 1.75rem;
}

.codeblock-shiki.has-top-bar .shiki {
padding-top: 2.75rem;
/* Trim shiki's trailing empty line. */
.codeblock-shiki .shiki .line:last-child:has(> span:empty:only-child),
.codeblock-shiki .shiki .line:last-child:empty {
display: none;
}

/* Line numbers via CSS counter — keeps shiki's <span class="line"> output
intact and avoids server-side post-processing. */
/* Line numbers via CSS counter. Real disabled-color column with a hairline
inner separator — readable for keyboard users, still recedes visually. */
.codeblock-shiki.line-numbers .shiki code {
counter-reset: shiki-line;
}
Expand All @@ -38,9 +52,64 @@
counter-increment: shiki-line;
content: counter(shiki-line);
display: inline-block;
width: 2rem;
margin-inline-end: 2rem;
width: 1.5rem;
padding-inline-end: 0.75rem;
margin-inline-end: 1rem;
border-inline-end: 1px solid hsla(var(--border-low-contrast));
text-align: end;
opacity: 0.4;
color: hsla(var(--disabled));
user-select: none;
font-variant-numeric: tabular-nums;
}

/* Collapsed surface: cap the code surface and flatten its bottom corners so
the expander row can sit flush beneath it. */
.codeblock-shiki.is-collapsed > [data-code-surface] {
max-height: calc((1.5rem * 8) + 1.75rem);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}

/* Fade overlay riding just above the expander row: dissolves the cut line of
code into the surface tint instead of hard-clipping it. */
.codeblock-shiki.is-collapsed::before {
content: "";
position: absolute;
inset-inline: 0;
bottom: 1.875rem;
height: 2.75rem;
background: linear-gradient(
to bottom,
transparent,
hsla(var(--background-highlight))
);
pointer-events: none;
z-index: 1;
}

/* Bottom inline expander — replaces the corner "Show all" button. Shares the
code surface tint so it reads as one continuous region. */
.codeblock-shiki .codeblock-expander {
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
width: 100%;
padding-block: 0.5rem 0.625rem;
font-size: 0.75rem;
line-height: 1;
color: hsla(var(--body-medium));
background: hsla(var(--background-highlight));
border-radius: 0 0 0.375rem 0.375rem;
cursor: pointer;
transition: color 120ms ease-out;
}

.codeblock-shiki .codeblock-expander:hover {
color: hsla(var(--primary));
}

.codeblock-shiki .codeblock-expander:focus-visible {
outline: 2px solid hsla(var(--primary));
outline-offset: 2px;
}
Loading