Typography.
Long-form text styles: headings, paragraphs, lead, lists, quotes, inline code and tables as small components, plus a Prose wrapper that styles raw HTML or rendered markdown.
Publishing your first hub site
Every in-game computer can serve a small website to the rest of the server. Here is how to put one up.
Seed the computer
Run lantern seed once. It writes a .lantern folder with your site name and a key that proves the site is yours.
Keep the seed file. Lose it and the name goes back to the directory after a week.
What goes in a site
- An index page, written in plain Lua or markdown.
- Optional pages, one file each.
- A guestbook, if you want visitors to sign.
Limits
| Resource | Limit |
|---|---|
| Pages per site | 64 |
| Total size | 512 KB |
| Guestbook entries | 1,000 |
Limits are per computer, not per player.
import {
TypographyBlockquote,
TypographyH1,
TypographyH2,
TypographyH3,
TypographyInlineCode,
TypographyLead,
TypographyList,
TypographyMuted,
TypographyP,
TypographySmall,
TypographyTable,
} from "@/components/ui/typography";
export default function TypographyDemo() {
return (
<article className="w-full max-w-2xl">
<TypographySmall>Guide / 4 min read</TypographySmall>
<TypographyH1 className="mt-4">Publishing your first hub site</TypographyH1>
<TypographyLead className="mt-5">
Every in-game computer can serve a small website to the rest of the server. Here is how to put one up.
</TypographyLead>
<TypographyH2 className="mt-10">Seed the computer</TypographyH2>
<TypographyP>
Run <TypographyInlineCode>lantern seed</TypographyInlineCode> once. It writes a{" "}
<TypographyInlineCode>.lantern</TypographyInlineCode> folder with your site name and a key that proves the site
is yours.
</TypographyP>
<TypographyBlockquote>Keep the seed file. Lose it and the name goes back to the directory after a week.</TypographyBlockquote>
<TypographyH3>What goes in a site</TypographyH3>
<TypographyList>
<li>An index page, written in plain Lua or markdown.</li>
<li>Optional pages, one file each.</li>
<li>A guestbook, if you want visitors to sign.</li>
</TypographyList>
<TypographyH3>Limits</TypographyH3>
<TypographyTable>
<thead>
<tr>
<th>Resource</th>
<th>Limit</th>
</tr>
</thead>
<tbody>
<tr>
<td>Pages per site</td>
<td>64</td>
</tr>
<tr>
<td>Total size</td>
<td>512 KB</td>
</tr>
<tr>
<td>Guestbook entries</td>
<td>1,000</td>
</tr>
</tbody>
</TypographyTable>
<TypographyMuted>Limits are per computer, not per player.</TypographyMuted>
</article>
);
}Installation#
With the CLI
$npx lanterncn add typographyThis also installs the Lantern theme and any Lantern UI components it depends on. The shadcn CLI works directly too: npx shadcn@latest add httptim/lantern-ui/typography. See Installation if your project is not set up yet.
Manually
Copy the source into your project
components/ui/typography.tsximport * as React from "react"; import { cn } from "@/lib/utils"; function TypographyH1({ className, ...props }: React.ComponentProps<"h1">) { return ( <h1 data-slot="typography-h1" className={cn("scroll-m-20 font-display text-4xl leading-[1.08] font-medium tracking-[-0.05em] text-balance sm:text-5xl", className)} {...props} /> ); } function TypographyH2({ className, ...props }: React.ComponentProps<"h2">) { return ( <h2 data-slot="typography-h2" className={cn( "mt-10 scroll-m-20 border-b pb-2 font-display text-3xl leading-tight font-medium tracking-[-0.04em] first:mt-0", className, )} {...props} /> ); } function TypographyH3({ className, ...props }: React.ComponentProps<"h3">) { return ( <h3 data-slot="typography-h3" className={cn("mt-8 scroll-m-20 font-display text-2xl leading-snug font-medium tracking-tight", className)} {...props} /> ); } function TypographyH4({ className, ...props }: React.ComponentProps<"h4">) { return ( <h4 data-slot="typography-h4" className={cn("mt-6 scroll-m-20 font-display text-lg leading-snug font-medium tracking-tight", className)} {...props} /> ); } function TypographyP({ className, ...props }: React.ComponentProps<"p">) { return ( <p data-slot="typography-p" className={cn("leading-[1.8] text-foreground/85 [&:not(:first-child)]:mt-5", className)} {...props} /> ); } function TypographyBlockquote({ className, ...props }: React.ComponentProps<"blockquote">) { return ( <blockquote data-slot="typography-blockquote" className={cn("mt-6 border-l-2 border-primary bg-card/60 py-2 pr-4 pl-5 text-foreground/90 italic", className)} {...props} /> ); } function TypographyList({ className, ordered = false, ...props }: React.HTMLAttributes<HTMLElement> & { ordered?: boolean }) { const Comp: React.ElementType = ordered ? "ol" : "ul"; return ( <Comp data-slot="typography-list" className={cn( "my-5 ml-6 space-y-2 leading-[1.7] text-foreground/85 [&>li]:pl-1.5 [&>li]:marker:font-mono [&>li]:marker:text-[0.8em] [&>li]:marker:text-success", ordered ? "list-decimal" : "list-[square]", className, )} {...props} /> ); } function TypographyInlineCode({ className, ...props }: React.ComponentProps<"code">) { return ( <code data-slot="typography-inline-code" className={cn( "rounded-sm border border-terminal-border/60 bg-terminal px-[0.35em] py-[0.1em] font-mono text-[0.85em] text-terminal-foreground", className, )} {...props} /> ); } function TypographyLead({ className, ...props }: React.ComponentProps<"p">) { return <p data-slot="typography-lead" className={cn("text-lg leading-[1.7] text-muted-foreground sm:text-xl", className)} {...props} />; } function TypographyLarge({ className, ...props }: React.ComponentProps<"div">) { return <div data-slot="typography-large" className={cn("font-display text-lg font-medium tracking-tight", className)} {...props} />; } function TypographySmall({ className, ...props }: React.ComponentProps<"small">) { return ( <small data-slot="typography-small" className={cn("font-mono text-[10px] leading-none tracking-[0.2em] text-success uppercase", className)} {...props} /> ); } function TypographyMuted({ className, ...props }: React.ComponentProps<"p">) { return <p data-slot="typography-muted" className={cn("text-sm text-muted-foreground", className)} {...props} />; } function TypographyTable({ className, ...props }: React.ComponentProps<"table">) { return ( <div data-slot="typography-table" className="my-6 w-full overflow-x-auto rounded-lg border"> <table className={cn( "w-full border-collapse text-sm", "[&_th]:border-b [&_th]:bg-secondary/60 [&_th]:px-4 [&_th]:py-2.5 [&_th]:text-left [&_th]:font-mono [&_th]:text-[10px] [&_th]:font-normal [&_th]:tracking-[0.2em] [&_th]:text-muted-foreground [&_th]:uppercase", "[&_td]:border-b [&_td]:px-4 [&_td]:py-2.5 [&_tr:last-child>td]:border-b-0 [&_tbody_tr:nth-child(even)]:bg-card/50", className, )} {...props} /> </div> ); } /** * Styles raw HTML or rendered markdown children (h1 to h4, paragraphs, links, lists, blockquotes, * code, rules and tables) without @tailwindcss/typography. Content inside a .not-prose element is left alone. */ function Prose({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="prose" className={cn( "max-w-[68ch] text-[15px] leading-[1.8] text-foreground/85", // Headings "[&_:is(h1,h2,h3,h4):not(:where(.not-prose,.not-prose_*))]:scroll-m-20 [&_:is(h1,h2,h3,h4):not(:where(.not-prose,.not-prose_*))]:font-display [&_:is(h1,h2,h3,h4):not(:where(.not-prose,.not-prose_*))]:font-medium [&_:is(h1,h2,h3,h4):not(:where(.not-prose,.not-prose_*))]:text-foreground", "[&_h1:not(:where(.not-prose_*))]:mb-6 [&_h1:not(:where(.not-prose_*))]:text-4xl [&_h1:not(:where(.not-prose_*))]:leading-[1.08] [&_h1:not(:where(.not-prose_*))]:tracking-[-0.05em] sm:[&_h1:not(:where(.not-prose_*))]:text-5xl", "[&_h2:not(:where(.not-prose_*))]:mt-12 [&_h2:not(:where(.not-prose_*))]:mb-4 [&_h2:not(:where(.not-prose_*))]:border-b [&_h2:not(:where(.not-prose_*))]:pb-2 [&_h2:not(:where(.not-prose_*))]:text-[1.75rem] [&_h2:not(:where(.not-prose_*))]:leading-tight [&_h2:not(:where(.not-prose_*))]:tracking-[-0.04em]", "[&_h3:not(:where(.not-prose_*))]:mt-9 [&_h3:not(:where(.not-prose_*))]:mb-3 [&_h3:not(:where(.not-prose_*))]:text-xl [&_h3:not(:where(.not-prose_*))]:tracking-tight", "[&_h4:not(:where(.not-prose_*))]:mt-7 [&_h4:not(:where(.not-prose_*))]:mb-2 [&_h4:not(:where(.not-prose_*))]:text-base [&_h4:not(:where(.not-prose_*))]:tracking-tight", "[&>:first-child]:mt-0", // Text "[&_p:not(:where(.not-prose_*))]:my-5", "[&_a:not(:where(.not-prose_*))]:text-primary [&_a:not(:where(.not-prose_*))]:underline [&_a:not(:where(.not-prose_*))]:decoration-primary/40 [&_a:not(:where(.not-prose_*))]:underline-offset-4 [&_a:not(:where(.not-prose_*))]:hover:decoration-primary", "[&_strong:not(:where(.not-prose_*))]:font-semibold [&_strong:not(:where(.not-prose_*))]:text-foreground", // Lists "[&_:is(ul,ol):not(:where(.not-prose_*))]:my-5 [&_:is(ul,ol):not(:where(.not-prose_*))]:ml-6 [&_:is(ul,ol):not(:where(.not-prose_*))]:space-y-2", "[&_ul:not(:where(.not-prose_*))]:list-[square] [&_ol:not(:where(.not-prose_*))]:list-decimal", "[&_li:not(:where(.not-prose_*))]:pl-1.5 [&_li:not(:where(.not-prose_*))]:marker:font-mono [&_li:not(:where(.not-prose_*))]:marker:text-[0.8em] [&_li:not(:where(.not-prose_*))]:marker:text-success", "[&_li_:is(ul,ol)]:my-2", // Quotes and rules "[&_blockquote:not(:where(.not-prose_*))]:my-6 [&_blockquote:not(:where(.not-prose_*))]:border-l-2 [&_blockquote:not(:where(.not-prose_*))]:border-primary [&_blockquote:not(:where(.not-prose_*))]:bg-card/60 [&_blockquote:not(:where(.not-prose_*))]:py-1 [&_blockquote:not(:where(.not-prose_*))]:pr-4 [&_blockquote:not(:where(.not-prose_*))]:pl-5 [&_blockquote:not(:where(.not-prose_*))]:text-foreground/90 [&_blockquote:not(:where(.not-prose_*))]:italic", "[&_blockquote_p]:my-3", "[&_hr:not(:where(.not-prose_*))]:my-10 [&_hr:not(:where(.not-prose_*))]:border-t [&_hr:not(:where(.not-prose_*))]:border-dashed [&_hr:not(:where(.not-prose_*))]:border-input", // Code "[&_code:not(:where(.not-prose_*))]:font-mono [&_code:not(:where(.not-prose_*))]:text-[0.85em] [&_code:not(:where(.not-prose_*))]:text-terminal-foreground", "[&_:not(pre)>code:not(:where(.not-prose_*))]:rounded-sm [&_:not(pre)>code:not(:where(.not-prose_*))]:border [&_:not(pre)>code:not(:where(.not-prose_*))]:border-terminal-border/60 [&_:not(pre)>code:not(:where(.not-prose_*))]:bg-terminal [&_:not(pre)>code:not(:where(.not-prose_*))]:px-[0.35em] [&_:not(pre)>code:not(:where(.not-prose_*))]:py-[0.1em]", "[&_pre:not(:where(.not-prose_*))]:my-6 [&_pre:not(:where(.not-prose_*))]:overflow-x-auto [&_pre:not(:where(.not-prose_*))]:rounded-lg [&_pre:not(:where(.not-prose_*))]:border [&_pre:not(:where(.not-prose_*))]:border-terminal-border [&_pre:not(:where(.not-prose_*))]:bg-terminal [&_pre:not(:where(.not-prose_*))]:px-5 [&_pre:not(:where(.not-prose_*))]:py-4 [&_pre:not(:where(.not-prose_*))]:text-[13px] [&_pre:not(:where(.not-prose_*))]:leading-[1.7] [&_pre:not(:where(.not-prose_*))]:text-terminal-foreground [&_pre:not(:where(.not-prose_*))]:shadow-block-sm", "[&_pre_code]:text-[1em]", // Tables "[&_table:not(:where(.not-prose_*))]:my-6 [&_table:not(:where(.not-prose_*))]:w-full [&_table:not(:where(.not-prose_*))]:border-collapse [&_table:not(:where(.not-prose_*))]:text-sm", "[&_th:not(:where(.not-prose_*))]:border-b [&_th:not(:where(.not-prose_*))]:border-input [&_th:not(:where(.not-prose_*))]:px-3 [&_th:not(:where(.not-prose_*))]:py-2 [&_th:not(:where(.not-prose_*))]:text-left [&_th:not(:where(.not-prose_*))]:font-mono [&_th:not(:where(.not-prose_*))]:text-[10px] [&_th:not(:where(.not-prose_*))]:font-normal [&_th:not(:where(.not-prose_*))]:tracking-[0.2em] [&_th:not(:where(.not-prose_*))]:text-muted-foreground [&_th:not(:where(.not-prose_*))]:uppercase", "[&_td:not(:where(.not-prose_*))]:border-b [&_td:not(:where(.not-prose_*))]:px-3 [&_td:not(:where(.not-prose_*))]:py-2 [&_td:not(:where(.not-prose_*))]:align-top [&_td:not(:where(.not-prose_*))]:break-words", // Media "[&_img:not(:where(.not-prose_*))]:my-6 [&_img:not(:where(.not-prose_*))]:rounded-lg [&_img:not(:where(.not-prose_*))]:border", className, )} {...props} /> ); } export { TypographyH1, TypographyH2, TypographyH3, TypographyH4, TypographyP, TypographyBlockquote, TypographyList, TypographyInlineCode, TypographyLead, TypographyLarge, TypographySmall, TypographyMuted, TypographyTable, Prose, };Update the import paths to match your project
The source imports
cnfrom@/lib/utils.
Usage#
import {
TypographyH1,
TypographyH2,
TypographyH3,
TypographyH4,
TypographyP,
TypographyBlockquote,
TypographyList,
TypographyInlineCode,
TypographyLead,
TypographyLarge,
TypographySmall,
TypographyMuted,
TypographyTable,
Prose,
} from "@/components/ui/typography";<TypographyH1>Publishing a hub site</TypographyH1>
<TypographyP>Run <TypographyInlineCode>lantern seed</TypographyInlineCode> once.</TypographyP>
<Prose dangerouslySetInnerHTML={{ __html: html }} />Prose styles descendant h1 to h4, p, a, strong, ul, ol, blockquote, code, pre, hr, table and img with plain Tailwind classes, so it does not need @tailwindcss/typography. Wrap part of the content in an element with the not-prose class to leave everything inside it unstyled.
Links are orange, list markers are green mono, blockquotes get an orange left rule and code uses the terminal colors.
Examples#
Prose wrapper
Relay changelog
Turtle relays now retry a dropped hop before giving up. Read the relay guide for setup.
Changed
- Hops time out after
3sinstead of10s. - Relays report fuel to the status page.
Upgrade
- Stop the relay.
- Update the program.
- Start it again.
lantern relay stop
lantern update relay
lantern relay start --hops 4
Relays on the old version keep working, but cannot retry.
Known issues
| Issue | Status |
|---|---|
| Relays in unloaded chunks stall | Investigating |
| Fuel reads 0 after restart | Fixed in 0.3.2 |
import { Prose } from "@/components/ui/typography";
// Stand-in for HTML rendered from markdown by your CMS or a markdown library.
const html = `
<h2>Relay changelog</h2>
<p>Turtle relays now retry a dropped hop before giving up. Read the <a href="#">relay guide</a> for setup.</p>
<h3>Changed</h3>
<ul>
<li>Hops time out after <code>3s</code> instead of <code>10s</code>.</li>
<li>Relays report fuel to the <strong>status page</strong>.</li>
</ul>
<h3>Upgrade</h3>
<ol>
<li>Stop the relay.</li>
<li>Update the program.</li>
<li>Start it again.</li>
</ol>
<pre><code>lantern relay stop
lantern update relay
lantern relay start --hops 4</code></pre>
<blockquote><p>Relays on the old version keep working, but cannot retry.</p></blockquote>
<hr />
<h4>Known issues</h4>
<table>
<thead><tr><th>Issue</th><th>Status</th></tr></thead>
<tbody>
<tr><td>Relays in unloaded chunks stall</td><td>Investigating</td></tr>
<tr><td>Fuel reads 0 after restart</td><td>Fixed in 0.3.2</td></tr>
</tbody>
</table>
`;
export default function TypographyProse() {
return <Prose className="w-full" dangerouslySetInnerHTML={{ __html: html }} />;
}