@supertype.ai/foundations 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +369 -0
  3. package/bin/foundations.mjs +713 -0
  4. package/dist/blocks/accordion.d.ts +23 -0
  5. package/dist/blocks/accordion.js +59 -0
  6. package/dist/blocks/callout.d.ts +57 -0
  7. package/dist/blocks/callout.js +61 -0
  8. package/dist/blocks/card.d.ts +34 -0
  9. package/dist/blocks/card.js +56 -0
  10. package/dist/blocks/index.d.ts +7 -0
  11. package/dist/blocks/index.js +7 -0
  12. package/dist/blocks/interactive-accordion.d.ts +13 -0
  13. package/dist/blocks/interactive-accordion.js +27 -0
  14. package/dist/blocks/segment.d.ts +37 -0
  15. package/dist/blocks/segment.js +37 -0
  16. package/dist/blocks/steps.d.ts +10 -0
  17. package/dist/blocks/steps.js +13 -0
  18. package/dist/blocks/tabs.d.ts +32 -0
  19. package/dist/blocks/tabs.js +69 -0
  20. package/dist/cjs/eslint.js +146 -0
  21. package/dist/cjs/package.json +3 -0
  22. package/dist/cn.d.ts +2 -0
  23. package/dist/cn.js +5 -0
  24. package/dist/contrast.d.ts +47 -0
  25. package/dist/contrast.js +255 -0
  26. package/dist/eslint.d.ts +74 -0
  27. package/dist/eslint.js +138 -0
  28. package/dist/essay/contents.d.ts +10 -0
  29. package/dist/essay/contents.js +17 -0
  30. package/dist/essay/essay.d.ts +125 -0
  31. package/dist/essay/essay.js +92 -0
  32. package/dist/essay/index.d.ts +7 -0
  33. package/dist/essay/index.js +9 -0
  34. package/dist/essay/layout.d.ts +72 -0
  35. package/dist/essay/layout.js +77 -0
  36. package/dist/essay/rail.d.ts +15 -0
  37. package/dist/essay/rail.js +26 -0
  38. package/dist/essay/reading.d.ts +17 -0
  39. package/dist/essay/reading.js +31 -0
  40. package/dist/essay/scroll.d.ts +8 -0
  41. package/dist/essay/scroll.js +78 -0
  42. package/dist/essay/toc.d.ts +23 -0
  43. package/dist/essay/toc.js +50 -0
  44. package/dist/index.d.ts +2 -0
  45. package/dist/index.js +33 -0
  46. package/dist/injection.d.ts +8 -0
  47. package/dist/injection.js +1 -0
  48. package/dist/mdx.d.ts +47 -0
  49. package/dist/mdx.js +68 -0
  50. package/dist/og.d.ts +18 -0
  51. package/dist/og.js +50 -0
  52. package/dist/rehype.d.ts +18 -0
  53. package/dist/rehype.js +41 -0
  54. package/dist/seo.d.ts +174 -0
  55. package/dist/seo.js +152 -0
  56. package/dist/typography/as.d.ts +15 -0
  57. package/dist/typography/as.js +8 -0
  58. package/dist/typography/header.d.ts +44 -0
  59. package/dist/typography/header.js +119 -0
  60. package/dist/typography/highlight.d.ts +33 -0
  61. package/dist/typography/highlight.js +98 -0
  62. package/dist/typography/index.d.ts +4 -0
  63. package/dist/typography/index.js +3 -0
  64. package/dist/typography/paragraph.d.ts +157 -0
  65. package/dist/typography/paragraph.js +229 -0
  66. package/llms.txt +125 -0
  67. package/package.json +140 -0
  68. package/src/prose.css +12 -0
  69. package/src/shiki.css +23 -0
  70. package/src/theme.css +272 -0
  71. package/src/tokens.css +43 -0
  72. package/src/type.css +73 -0
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { cn } from "../cn.js";
4
+ import { Rail, RailLink } from "./rail.js";
5
+ import { useReadingProgress, useScrollSpy } from "./scroll.js";
6
+ /**
7
+ * Hairline progress bar, rendered even where the rail is hidden. A CSS
8
+ * transition, not a spring: the value only feeds a transform, and this keeps the
9
+ * essay layer from dragging in an animation runtime.
10
+ */
11
+ export function ReadingProgressBar({ className }) {
12
+ const progress = useReadingProgress();
13
+ return (_jsx("div", { "aria-hidden": true, className: cn("fixed inset-x-0 top-0 z-[60] h-0.5 origin-left bg-primary", "transition-transform duration-150 ease-out", className), style: { transform: `scaleX(${progress})` } }));
14
+ }
15
+ /** Circular percentage indicator at the head of the rail. */
16
+ function ProgressDonut({ progress }) {
17
+ const radius = 16;
18
+ const circumference = 2 * Math.PI * radius;
19
+ return (_jsxs("div", { className: "relative grid size-10 place-items-center", children: [_jsxs("svg", { viewBox: "0 0 40 40", className: "size-10 -rotate-90", "aria-hidden": true, children: [_jsx("circle", { cx: "20", cy: "20", r: radius, fill: "none", strokeWidth: "3", className: "stroke-border" }), _jsx("circle", { cx: "20", cy: "20", r: radius, fill: "none", strokeWidth: "3", strokeLinecap: "round", className: "stroke-primary transition-[stroke-dashoffset] duration-150 ease-out", strokeDasharray: circumference, strokeDashoffset: circumference * (1 - progress) })] }), _jsx("span", { className: "absolute font-mono text-3xs font-medium tabular-nums text-muted-foreground", children: Math.round(progress * 100) })] }));
20
+ }
21
+ /**
22
+ * Sticky rail with live scroll-spy. Both it and `ReadingProgressBar` read shared
23
+ * stores, so mounting them together costs one scroll subscription, not two.
24
+ */
25
+ export function ReadingRail({ headings, className, }) {
26
+ const progress = useReadingProgress();
27
+ const active = useScrollSpy(headings.map((h) => h.id));
28
+ if (headings.length === 0)
29
+ return null;
30
+ return (_jsxs("nav", { "aria-label": "On this page", className: cn("flex flex-col gap-4", className), children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(ProgressDonut, { progress: progress }), _jsx("p", { className: "text-xs font-medium uppercase tracking-widest text-muted-foreground", children: "On this page" })] }), _jsx(Rail, { children: headings.map(({ id, label, depth }) => (_jsx(RailLink, { href: `#${id}`, active: active === id, nested: depth === 3, children: label }, id))) })] }));
31
+ }
@@ -0,0 +1,8 @@
1
+ export declare function useReadingProgress(): number;
2
+ /**
3
+ * One IntersectionObserver across all headings, not one each — the observer
4
+ * already reports what changed. Topmost wins when several are on screen.
5
+ */
6
+ export declare function useScrollSpy(ids: string[], { rootMargin }?: {
7
+ rootMargin?: string;
8
+ }): string;
@@ -0,0 +1,78 @@
1
+ "use client";
2
+ import { useEffect, useState, useSyncExternalStore } from "react";
3
+ /**
4
+ * Reading progress 0→1 from one shared listener: a page mounts both the bar and
5
+ * the rail, and hook-local state would double every subscription. Reads coalesce
6
+ * to a frame, since `scrollHeight` forces layout.
7
+ */
8
+ let progress = 0;
9
+ let frame = 0;
10
+ const listeners = new Set();
11
+ function measure() {
12
+ frame = 0;
13
+ const scrollable = document.documentElement.scrollHeight - window.innerHeight;
14
+ const next = scrollable > 0 ? window.scrollY / scrollable : 0;
15
+ const clamped = Math.min(1, Math.max(0, next));
16
+ if (clamped === progress)
17
+ return;
18
+ progress = clamped;
19
+ listeners.forEach((l) => l());
20
+ }
21
+ function schedule() {
22
+ if (frame)
23
+ return;
24
+ frame = requestAnimationFrame(measure);
25
+ }
26
+ function subscribe(listener) {
27
+ if (listeners.size === 0) {
28
+ window.addEventListener("scroll", schedule, { passive: true });
29
+ window.addEventListener("resize", schedule);
30
+ measure();
31
+ }
32
+ listeners.add(listener);
33
+ return () => {
34
+ listeners.delete(listener);
35
+ if (listeners.size === 0) {
36
+ window.removeEventListener("scroll", schedule);
37
+ window.removeEventListener("resize", schedule);
38
+ if (frame)
39
+ cancelAnimationFrame(frame);
40
+ frame = 0;
41
+ }
42
+ };
43
+ }
44
+ export function useReadingProgress() {
45
+ // The server snapshot is 0, an unscrolled document. That is also the client's
46
+ // value on first paint, so the two agree.
47
+ return useSyncExternalStore(subscribe, () => progress, () => 0);
48
+ }
49
+ /**
50
+ * One IntersectionObserver across all headings, not one each — the observer
51
+ * already reports what changed. Topmost wins when several are on screen.
52
+ */
53
+ export function useScrollSpy(ids, { rootMargin = "-15% 0px -75% 0px" } = {}) {
54
+ const [active, setActive] = useState("");
55
+ // Depend on the joined ids rather than the array: a caller that maps headings
56
+ // inline passes a new array identity every render, which would tear the
57
+ // observer down and rebuild it on each one.
58
+ const key = ids.join("|");
59
+ useEffect(() => {
60
+ const elements = key
61
+ .split("|")
62
+ .filter(Boolean)
63
+ .map((id) => document.getElementById(id))
64
+ .filter((el) => el !== null);
65
+ if (elements.length === 0)
66
+ return;
67
+ const observer = new IntersectionObserver((entries) => {
68
+ const visible = entries
69
+ .filter((e) => e.isIntersecting)
70
+ .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
71
+ if (visible[0])
72
+ setActive(visible[0].target.id);
73
+ }, { rootMargin });
74
+ elements.forEach((el) => observer.observe(el));
75
+ return () => observer.disconnect();
76
+ }, [key, rootMargin]);
77
+ return active;
78
+ }
@@ -0,0 +1,23 @@
1
+ /** A heading in the table of contents. */
2
+ export interface TocHeading {
3
+ /** 2 = h2 (top level), 3 = h3 (nested). */
4
+ depth: 2 | 3;
5
+ /** Matches the id rehype-slug stamps on the heading, so anchors line up. */
6
+ id: string;
7
+ label: string;
8
+ }
9
+ /**
10
+ * GitHub's slug algorithm, reimplemented. Must agree exactly with rehype-slug or
11
+ * the rail's anchors point at headings that do not exist.
12
+ */
13
+ export declare function createSlugger(): (value: string) => string;
14
+ /**
15
+ * h2/h3 from raw markdown. Fenced code is skipped — a `# comment` in a shell
16
+ * block is not a heading. Works on source, so it needs no DOM or compiled MDX.
17
+ */
18
+ export declare function extractHeadings(markdown: string): TocHeading[];
19
+ /**
20
+ * Computed, never stored: a hand-declared `readingMinutes` is correct once.
21
+ * 200 wpm, code blocks excluded — counting them inflates every technical post.
22
+ */
23
+ export declare function readingTime(source: string, wordsPerMinute?: number): number;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * GitHub's slug algorithm, reimplemented. Must agree exactly with rehype-slug or
3
+ * the rail's anchors point at headings that do not exist.
4
+ */
5
+ export function createSlugger() {
6
+ const seen = new Map();
7
+ return (value) => {
8
+ const base = value
9
+ .toLowerCase()
10
+ .trim()
11
+ .replace(/[^\w\s-]/g, "")
12
+ .replace(/\s+/g, "-");
13
+ const count = seen.get(base) ?? 0;
14
+ seen.set(base, count + 1);
15
+ return count === 0 ? base : `${base}-${count}`;
16
+ };
17
+ }
18
+ /**
19
+ * h2/h3 from raw markdown. Fenced code is skipped — a `# comment` in a shell
20
+ * block is not a heading. Works on source, so it needs no DOM or compiled MDX.
21
+ */
22
+ export function extractHeadings(markdown) {
23
+ const slug = createSlugger();
24
+ const headings = [];
25
+ let inFence = false;
26
+ for (const line of markdown.split("\n")) {
27
+ if (/^\s*(```|~~~)/.test(line)) {
28
+ inFence = !inFence;
29
+ continue;
30
+ }
31
+ if (inFence)
32
+ continue;
33
+ const match = /^(#{2,3})\s+(.+?)\s*$/.exec(line);
34
+ if (!match)
35
+ continue;
36
+ const depth = match[1].length;
37
+ const label = match[2].replace(/[*_`]/g, "").trim();
38
+ headings.push({ depth, id: slug(label), label });
39
+ }
40
+ return headings;
41
+ }
42
+ /**
43
+ * Computed, never stored: a hand-declared `readingMinutes` is correct once.
44
+ * 200 wpm, code blocks excluded — counting them inflates every technical post.
45
+ */
46
+ export function readingTime(source, wordsPerMinute = 200) {
47
+ const prose = source.replace(/```[\s\S]*?```/g, " ");
48
+ const words = prose.trim() ? prose.trim().split(/\s+/).length : 0;
49
+ return Math.max(1, Math.round(words / wordsPerMinute));
50
+ }
@@ -0,0 +1,2 @@
1
+ export { cn } from "./cn.js";
2
+ export * from "./typography/index.js";
package/dist/index.js ADDED
@@ -0,0 +1,33 @@
1
+ export { cn } from "./cn.js";
2
+ export * from "./typography/index.js";
3
+ // NOTE: blocks, the MDX map, and the Shiki plugin are all deliberately absent
4
+ // from this barrel.
5
+ //
6
+ // A barrel's transitive dependencies are paid by every name it exports. mdx.tsx
7
+ // imports `next/image`, and a bare subpath like that fails to resolve from
8
+ // inside node_modules under a plain Node ESM loader, which is the loader a
9
+ // consumer's test runner uses. Pulling it in here made every test that touches a
10
+ // Typography component fail to import.
11
+ //
12
+ // blocks/ carries the same hazard one dependency over: interactive-accordion.tsx
13
+ // and tabs.tsx pull `@base-ui/react`, so re-exporting them made a bare
14
+ // `import { TypographyH2 }` resolve Base UI. They live at
15
+ // "@supertype.ai/foundations/blocks" for that reason, not for tree-shaking — the
16
+ // bundlers already handle that.
17
+ //
18
+ // What the split does NOT buy is a plain-Node-importable root. Measured, not
19
+ // assumed: `node -e "import('@supertype.ai/foundations')"` from a consumer fails on
20
+ // ERR_MODULE_NOT_FOUND for next/link, because TypographyLink imports
21
+ // `next-view-transitions`, whose dist/index.js:3 imports `next/link` — the same
22
+ // unresolvable bare subpath as next/image above, one package further out. The
23
+ // blocks entry fails identically through card.tsx.
24
+ //
25
+ // This is survivable because the runner that matters resolves it: both consumers'
26
+ // vitest suites import typography freely and pass. Do not "fix" it by reinstating
27
+ // injection — that was tried, and paragraph.tsx documents the call site it
28
+ // silently left unbound. Isolating it would mean a /next subpath for
29
+ // TypographyLink and Card, against ~230 and ~100 call sites.
30
+ //
31
+ // The Shiki plugin has build-time consumers (source.config.ts, next.config) that
32
+ // run in bare Node where React is not resolvable. Import it from
33
+ // "@supertype.ai/foundations/rehype".
@@ -0,0 +1,8 @@
1
+ import type { ComponentType } from "react";
2
+ /**
3
+ * A component the app injects — its Link, its Image. Untyped in its props on
4
+ * purpose: no hand-written signature accepts `next/link`, `next/image` and
5
+ * react-router at once, and tightening it only moves the cast to every consumer.
6
+ * What must hold is checked where these are rendered, against a fixed prop set.
7
+ */
8
+ export type InjectedComponent = ComponentType<any>;
@@ -0,0 +1 @@
1
+ export {};
package/dist/mdx.d.ts ADDED
@@ -0,0 +1,47 @@
1
+ import type { ComponentProps } from "react";
2
+ import { Disclosure, DisclosureGroup } from "./blocks/accordion.js";
3
+ import { Callout } from "./blocks/callout.js";
4
+ import { Card, Cards } from "./blocks/card.js";
5
+ import { Step, Steps } from "./blocks/steps.js";
6
+ import { TabGroup, Tab } from "./blocks/tabs.js";
7
+ /**
8
+ * Rendered from markdown syntax, so there is no call site and no knob — a knob
9
+ * here is one every project turns differently. Headings carry variants and this
10
+ * map binds them; retune in CSS by moving the `--text-*` rung, or with
11
+ * `.editorial` over the subtree.
12
+ *
13
+ * A constant rather than a factory: the router and the image component are the
14
+ * package's now, so there is nothing left for a consumer to inject.
15
+ */
16
+ export declare const proseMdxComponents: {
17
+ Card: typeof Card;
18
+ Cards: typeof Cards;
19
+ Accordions: typeof DisclosureGroup;
20
+ Accordion: typeof Disclosure;
21
+ Banner: (props: ComponentProps<typeof Callout>) => import("react").JSX.Element;
22
+ Tabs: typeof TabGroup;
23
+ Tab: typeof Tab;
24
+ Steps: typeof Steps;
25
+ Step: typeof Step;
26
+ h1: (props: ComponentProps<"h1">) => import("react").JSX.Element;
27
+ h2: (props: ComponentProps<"h2">) => import("react").JSX.Element;
28
+ h3: (props: ComponentProps<"h3">) => import("react").JSX.Element;
29
+ h4: (props: ComponentProps<"h4">) => import("react").JSX.Element;
30
+ p: (props: ComponentProps<"p">) => import("react").JSX.Element;
31
+ ul: (props: ComponentProps<"ul">) => import("react").JSX.Element;
32
+ ol: (props: ComponentProps<"ol">) => import("react").JSX.Element;
33
+ li: (props: ComponentProps<"li">) => import("react").JSX.Element;
34
+ blockquote: ({ className, ...props }: ComponentProps<"blockquote">) => import("react").JSX.Element;
35
+ a: ({ href, children }: ComponentProps<"a">) => import("react").JSX.Element;
36
+ /**
37
+ * The frame only — never set `color`, or it beats shiki's token spans.
38
+ * `tabIndex` keeps a horizontally scrolling block reachable by keyboard.
39
+ */
40
+ pre: ({ className, ...props }: ComponentProps<"pre">) => import("react").JSX.Element;
41
+ img: ({ alt, src, width, height, ...props }: ComponentProps<"img">) => import("react").JSX.Element;
42
+ hr: () => import("react").JSX.Element;
43
+ strong: (props: ComponentProps<"strong">) => import("react").JSX.Element;
44
+ table: (props: ComponentProps<"table">) => import("react").JSX.Element;
45
+ th: (props: ComponentProps<"th">) => import("react").JSX.Element;
46
+ td: (props: ComponentProps<"td">) => import("react").JSX.Element;
47
+ };
package/dist/mdx.js ADDED
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { TypographyH1, TypographyH2, TypographyH3, TypographyH4, } from "./typography/header.js";
3
+ import { cn } from "./cn.js";
4
+ import Image from "next/image";
5
+ import { Disclosure, DisclosureGroup } from "./blocks/accordion.js";
6
+ import { Callout } from "./blocks/callout.js";
7
+ import { Card, Cards } from "./blocks/card.js";
8
+ import { Step, Steps } from "./blocks/steps.js";
9
+ import { TabGroup, Tab } from "./blocks/tabs.js";
10
+ import { TypographyProse, TypographyProseList, TypographyLink, } from "./typography/paragraph.js";
11
+ /**
12
+ * Rendered from markdown syntax, so there is no call site and no knob — a knob
13
+ * here is one every project turns differently. Headings carry variants and this
14
+ * map binds them; retune in CSS by moving the `--text-*` rung, or with
15
+ * `.editorial` over the subtree.
16
+ *
17
+ * A constant rather than a factory: the router and the image component are the
18
+ * package's now, so there is nothing left for a consumer to inject.
19
+ */
20
+ export const proseMdxComponents = {
21
+ // Authorable blocks. Unlike the elements below these ARE called by hand in
22
+ // MDX, so they take props — but never a `className`: an author writing
23
+ // <Card> is choosing a component, not restyling one.
24
+ Card,
25
+ Cards,
26
+ Accordions: DisclosureGroup,
27
+ Accordion: Disclosure,
28
+ Banner: (props) => (_jsx(Callout, { density: "editorial", ...props })),
29
+ Tabs: TabGroup,
30
+ Tab,
31
+ Steps,
32
+ Step,
33
+ h1: (props) => (_jsx(TypographyH1, { variant: "display", ...props })),
34
+ h2: (props) => _jsx(TypographyH2, { ...props }),
35
+ h3: (props) => _jsx(TypographyH3, { ...props }),
36
+ h4: (props) => _jsx(TypographyH4, { ...props }),
37
+ p: (props) => _jsx(TypographyProse, { ...props }),
38
+ ul: (props) => _jsx(TypographyProseList, { ...props }),
39
+ ol: (props) => (_jsx(TypographyProseList, { ordered: true, ...props })),
40
+ li: (props) => (_jsx("li", { className: "[&>ul]:mt-2 [&>ol]:mt-2", ...props })),
41
+ // No quotemark glyph: a markdown `>` block is already marked as a quote by
42
+ // its rule and its indent, and a mark on top of that reads as decoration.
43
+ blockquote: ({ className, ...props }) => (_jsx("blockquote", { className: cn("my-6 border-l-[3px] border-border pl-5 text-lg italic leading-relaxed text-foreground", className), ...props })),
44
+ a: ({ href = "", children }) => (_jsx(TypographyLink, { href: href, children: children })),
45
+ /**
46
+ * The frame only — never set `color`, or it beats shiki's token spans.
47
+ * `tabIndex` keeps a horizontally scrolling block reachable by keyboard.
48
+ */
49
+ pre: ({ className, ...props }) => (_jsx("pre", { tabIndex: 0, className: cn("my-6 overflow-x-auto rounded-xl border border-border p-4 text-sm leading-relaxed", "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", className), ...props })),
50
+ img: ({ alt = "", src, width, height, ...props }) => {
51
+ const className = "my-6 h-auto max-w-full rounded-xl border border-border";
52
+ // `next/image` needs a real src and intrinsic dimensions, which a markdown
53
+ // `![](…)` lacks unless a remark plugin measured them. The plain `<img>` is
54
+ // the fallback for when it did not.
55
+ const w = Number(width);
56
+ const h = Number(height);
57
+ if (typeof src === "string" && w > 0 && h > 0) {
58
+ return (_jsx(Image, { src: src, width: w, height: h, alt: alt, className: className, ...props }));
59
+ }
60
+ // eslint-disable-next-line @next/next/no-img-element
61
+ return (_jsx("img", { src: typeof src === "string" ? src : undefined, width: width, height: height, alt: alt, className: className, ...props }));
62
+ },
63
+ hr: () => _jsx("hr", { className: "my-12 border-border" }),
64
+ strong: (props) => (_jsx("strong", { className: "font-semibold text-foreground", ...props })),
65
+ table: (props) => (_jsx("div", { className: "mt-6 overflow-x-auto rounded-xl border border-border", children: _jsx("table", { className: "w-full border-collapse text-sm", ...props }) })),
66
+ th: (props) => (_jsx("th", { className: "border-b border-border bg-muted/40 px-4 py-2.5 text-left font-semibold text-foreground", ...props })),
67
+ td: (props) => (_jsx("td", { className: "border-b border-border/60 px-4 py-2.5 align-top text-muted-foreground", ...props })),
68
+ };
package/dist/og.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { ReactElement } from "react";
2
+ /** Open Graph card dimensions. The 1.91:1 ratio every major network crops to. */
3
+ export declare const OG_SIZE: {
4
+ readonly width: 1200;
5
+ readonly height: 630;
6
+ };
7
+ export interface OgCardOptions {
8
+ title: string;
9
+ description?: string;
10
+ /** The site or brand name, set small at the foot of the card. */
11
+ site?: string;
12
+ /** Accent colour for the rule and the site name. Any CSS colour. */
13
+ accent?: string;
14
+ background?: string;
15
+ foreground?: string;
16
+ muted?: string;
17
+ }
18
+ export declare function ogCard({ title, description, site, accent, background, foreground, muted, }: OgCardOptions): ReactElement;
package/dist/og.js ADDED
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** Open Graph card dimensions. The 1.91:1 ratio every major network crops to. */
3
+ export const OG_SIZE = { width: 1200, height: 630 };
4
+ /** Measured, not guessed: ~45 chars/line at 62px over three lines, two at 28px. */
5
+ const TITLE_MAX = 135;
6
+ const DESCRIPTION_MAX = 160;
7
+ /**
8
+ * A tree, not an image, so the package never imports `next/og`. Rendered by
9
+ * satori: no stylesheets, no classes, no default block layout — hence inline
10
+ * styles and an explicit `display: flex` on everything. Text is truncated in JS
11
+ * because `-webkit-line-clamp` needs a display mode that contradicts the flex.
12
+ */
13
+ /** Truncates on a word boundary, so a cut title does not end mid-word. */
14
+ function truncate(text, max) {
15
+ if (text.length <= max)
16
+ return text;
17
+ const cut = text.slice(0, max);
18
+ const lastSpace = cut.lastIndexOf(" ");
19
+ return `${(lastSpace > max * 0.6 ? cut.slice(0, lastSpace) : cut).trimEnd()}…`;
20
+ }
21
+ export function ogCard({ title, description, site, accent = "#b1976b", background = "#0c0a09", foreground = "#fafaf9", muted = "#a8a29e", }) {
22
+ return (_jsxs("div", { style: {
23
+ width: "100%",
24
+ height: "100%",
25
+ display: "flex",
26
+ flexDirection: "column",
27
+ justifyContent: "space-between",
28
+ background,
29
+ padding: "72px",
30
+ fontFamily: "sans-serif",
31
+ }, children: [_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "28px" }, children: [_jsx("div", { style: { display: "flex", width: "88px", height: "6px", background: accent } }), _jsx("div", { style: {
32
+ display: "flex",
33
+ fontSize: 62,
34
+ fontWeight: 700,
35
+ lineHeight: 1.15,
36
+ letterSpacing: "-0.02em",
37
+ color: foreground,
38
+ }, children: truncate(title, TITLE_MAX) }), description ? (_jsx("div", { style: {
39
+ display: "flex",
40
+ fontSize: 28,
41
+ lineHeight: 1.4,
42
+ color: muted,
43
+ }, children: truncate(description, DESCRIPTION_MAX) })) : null] }), site ? (_jsx("div", { style: {
44
+ display: "flex",
45
+ fontSize: 26,
46
+ fontWeight: 600,
47
+ letterSpacing: "0.04em",
48
+ color: accent,
49
+ }, children: site })) : null] }));
50
+ }
@@ -0,0 +1,18 @@
1
+ import rehypeShiki, { type RehypeShikiOptions } from "@shikijs/rehype";
2
+ /**
3
+ * Explicit rather than Shiki's full set — every grammar is build-time parsing.
4
+ * Anything unlisted falls back to `text`: content uses labels that are not real
5
+ * grammars (`tree`, `spark-defaults`), and dying over a fence label is a bad trade.
6
+ */
7
+ export declare const PROSE_LANGS: readonly ["bash", "csv", "diff", "docker", "json", "python", "sql", "toml", "tsx", "typescript", "javascript", "yaml"];
8
+ export declare const PROSE_THEMES: {
9
+ readonly light: "github-light";
10
+ readonly dark: "github-dark";
11
+ };
12
+ /**
13
+ * `defaultColor: false` emits `--shiki-light` / `--shiki-dark` per token instead
14
+ * of a baked colour, so one compiled document serves both themes; `shiki.css`
15
+ * maps them. A `[plugin, options]` tuple, so it drops into `rehypePlugins`.
16
+ */
17
+ export declare const proseCodeOptions: RehypeShikiOptions;
18
+ export declare const rehypeProseCode: [typeof rehypeShiki, RehypeShikiOptions];
package/dist/rehype.js ADDED
@@ -0,0 +1,41 @@
1
+ import rehypeShiki, {} from "@shikijs/rehype";
2
+ /**
3
+ * Explicit rather than Shiki's full set — every grammar is build-time parsing.
4
+ * Anything unlisted falls back to `text`: content uses labels that are not real
5
+ * grammars (`tree`, `spark-defaults`), and dying over a fence label is a bad trade.
6
+ */
7
+ export const PROSE_LANGS = [
8
+ "bash",
9
+ "csv",
10
+ "diff",
11
+ "docker",
12
+ "json",
13
+ "python",
14
+ "sql",
15
+ "toml",
16
+ "tsx",
17
+ "typescript",
18
+ "javascript",
19
+ "yaml",
20
+ ];
21
+ export const PROSE_THEMES = {
22
+ light: "github-light",
23
+ dark: "github-dark",
24
+ };
25
+ /**
26
+ * `defaultColor: false` emits `--shiki-light` / `--shiki-dark` per token instead
27
+ * of a baked colour, so one compiled document serves both themes; `shiki.css`
28
+ * maps them. A `[plugin, options]` tuple, so it drops into `rehypePlugins`.
29
+ */
30
+ export const proseCodeOptions = {
31
+ themes: PROSE_THEMES,
32
+ defaultColor: false,
33
+ langs: [...PROSE_LANGS],
34
+ fallbackLanguage: "text",
35
+ };
36
+ // Typed as a mutable tuple, not `as const`: unified's `Pluggable` does not
37
+ // accept a readonly tuple, and a readonly one fails to assign in consumers.
38
+ export const rehypeProseCode = [
39
+ rehypeShiki,
40
+ proseCodeOptions,
41
+ ];