chore: import upstream snapshot with attribution
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* The Components section inventory: one entry per component page,
|
||||
* alphabetical. Single source for the sidebar section, the index-page
|
||||
* grid, and page titles/OG metadata. `preview` names the engine-rendered
|
||||
* tile pair in /public/components — the index grid uses the 16:9 hero
|
||||
* tiles (one representative variation per component; regenerate with
|
||||
* `zig build docs-component-previews`).
|
||||
*/
|
||||
export type ComponentPage = {
|
||||
slug: string;
|
||||
name: string;
|
||||
/** Preview tile stem: /components/<preview>-{light,dark}.webp */
|
||||
preview: string;
|
||||
/** One-line index-grid caption. */
|
||||
blurb: string;
|
||||
};
|
||||
|
||||
export const componentPages: ComponentPage[] = [
|
||||
{ slug: "accordion", name: "Accordion", preview: "accordion-hero", blurb: "Disclosure surface with a model-owned open state." },
|
||||
{ slug: "alert", name: "Alert", preview: "alert-hero", blurb: "Inline callouts with icon and variant color." },
|
||||
{ slug: "avatar", name: "Avatar", preview: "avatar-hero", blurb: "Initials fallback and runtime-registered images." },
|
||||
{ slug: "badge", name: "Badge", preview: "badge-hero", blurb: "Status labels in every variant." },
|
||||
{ slug: "breadcrumb", name: "Breadcrumb", preview: "breadcrumb-hero", blurb: "Hierarchy trail with separators." },
|
||||
{ slug: "bubble", name: "Bubble", preview: "bubble-hero", blurb: "Chat-message surfaces for either side." },
|
||||
{ slug: "button", name: "Button", preview: "button-hero", blurb: "Variants, sizes, inline icons, and states." },
|
||||
{ slug: "button-group", name: "Button Group", preview: "button-group-hero", blurb: "Attached action buttons as one segmented bar." },
|
||||
{ slug: "card", name: "Card", preview: "card-hero", blurb: "The bordered, elevated surface container." },
|
||||
{ slug: "chart", name: "Chart", preview: "chart-hero", blurb: "Line, bar, and band series (Zig builder)." },
|
||||
{ slug: "checkbox", name: "Checkbox", preview: "checkbox-hero", blurb: "Binary choice with model-owned state." },
|
||||
{ slug: "combobox", name: "Combobox", preview: "combobox-hero", blurb: "Text entry with an anchored suggestions menu." },
|
||||
{ slug: "dialog", name: "Dialog", preview: "dialog-hero", blurb: "Modal surface with model-owned dismissal." },
|
||||
{ slug: "drawer", name: "Drawer", preview: "drawer-hero", blurb: "Side-anchored modal surface." },
|
||||
{ slug: "dropdown-menu", name: "Dropdown Menu", preview: "dropdown-menu-hero", blurb: "Anchored floating menus and menu items." },
|
||||
{ slug: "icon", name: "Icon", preview: "icon-hero", blurb: "The built-in vector icon registry." },
|
||||
{ slug: "input", name: "Input", preview: "input-hero", blurb: "Single-line text entry: input, text field, search field." },
|
||||
{ slug: "input-group", name: "Input Group", preview: "input-group-hero", blurb: "One bordered field: textarea plus accessory actions." },
|
||||
{ slug: "list", name: "List", preview: "list-hero", blurb: "Rows with icons, selection, and virtualization." },
|
||||
{ slug: "markdown", name: "Markdown", preview: "markdown-hero", blurb: "GFM rendering through native widgets." },
|
||||
{ slug: "pagination", name: "Pagination", preview: "pagination-hero", blurb: "Page navigation row." },
|
||||
{ slug: "panel", name: "Panel", preview: "panel-hero", blurb: "The plain surface container." },
|
||||
{ slug: "progress", name: "Progress", preview: "progress-hero", blurb: "Determinate progress bar." },
|
||||
{ slug: "radio", name: "Radio", preview: "radio-hero", blurb: "Single choice within a radio group." },
|
||||
{ slug: "resizable", name: "Resizable", preview: "resizable-hero", blurb: "Panel with an engine-managed drag handle." },
|
||||
{ slug: "scroll", name: "Scroll", preview: "scroll-hero", blurb: "Scroll regions with model-observable offsets." },
|
||||
{ slug: "select", name: "Select", preview: "select-hero", blurb: "Trigger plus the anchored dropdown options pattern." },
|
||||
{ slug: "separator", name: "Separator", preview: "separator-hero", blurb: "Hairline rules, horizontal and vertical." },
|
||||
{ slug: "sheet", name: "Sheet", preview: "sheet-hero", blurb: "Bottom-anchored modal surface." },
|
||||
{ slug: "skeleton", name: "Skeleton", preview: "skeleton-hero", blurb: "Loading placeholders that sketch the content." },
|
||||
{ slug: "slider", name: "Slider", preview: "slider-hero", blurb: "Continuous value control." },
|
||||
{ slug: "spacer", name: "Spacer", preview: "spacer-hero", blurb: "Flexible empty space between siblings." },
|
||||
{ slug: "spinner", name: "Spinner", preview: "spinner-hero", blurb: "Indeterminate progress leaf." },
|
||||
{ slug: "split", name: "Split", preview: "split-hero", blurb: "Draggable two-pane splitter." },
|
||||
{ slug: "status-bar", name: "Status Bar", preview: "status-bar-hero", blurb: "Window-bottom status text." },
|
||||
{ slug: "stepper", name: "Stepper", preview: "stepper-hero", blurb: "Stage progress with completed/active/pending steps." },
|
||||
{ slug: "switch", name: "Switch", preview: "switch-hero", blurb: "On/off switches with model-owned state." },
|
||||
{ slug: "table", name: "Table", preview: "table-hero", blurb: "Rows and cells with hairline dividers." },
|
||||
{ slug: "tabs", name: "Tabs", preview: "tabs-hero", blurb: "Tab strip over segmented controls." },
|
||||
{ slug: "textarea", name: "Textarea", preview: "textarea-hero", blurb: "Multi-line text entry." },
|
||||
{ slug: "timeline", name: "Timeline", preview: "timeline-hero", blurb: "Ledger list with indicators and connectors." },
|
||||
{ slug: "toggle", name: "Toggle", preview: "toggle-hero", blurb: "Pressed-state toggles, toggle buttons, and groups." },
|
||||
{ slug: "tooltip", name: "Tooltip", preview: "tooltip-hero", blurb: "The floating label above the control it annotates." },
|
||||
{ slug: "tree", name: "Tree", preview: "tree-hero", blurb: "Disclosure tree with one roving focus set." },
|
||||
{ slug: "virtual-list", name: "Virtual List", preview: "virtual-list-hero", blurb: "Windowed rows: the view builds only what's visible." },
|
||||
];
|
||||
@@ -0,0 +1,106 @@
|
||||
import { componentPages } from "./components-pages";
|
||||
|
||||
export type NavItem = {
|
||||
name: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export type NavSection = {
|
||||
title: string;
|
||||
items: NavItem[];
|
||||
};
|
||||
|
||||
export const navSections: NavSection[] = [
|
||||
{
|
||||
title: "Get Started",
|
||||
items: [
|
||||
{ name: "Introduction", href: "/introduction" },
|
||||
{ name: "Quick Start", href: "/quick-start" },
|
||||
{ name: "CLI", href: "/cli" },
|
||||
{ name: "Config", href: "/app-zon" },
|
||||
{ name: "Agent Skills", href: "/skills" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Core Concepts",
|
||||
items: [
|
||||
{ name: "App Model", href: "/app-model" },
|
||||
{ name: "TypeScript Cores", href: "/typescript" },
|
||||
{ name: "Where Packages Go", href: "/typescript/packages" },
|
||||
{ name: "Native UI", href: "/native-ui" },
|
||||
{ name: "State & Data Flow", href: "/state" },
|
||||
{ name: "Theming", href: "/theming" },
|
||||
{ name: "Building Components", href: "/building-components" },
|
||||
],
|
||||
},
|
||||
{
|
||||
// One entry per built-in component page, generated from the shared
|
||||
// components-pages inventory (previews regenerate via
|
||||
// `zig build docs-component-previews`).
|
||||
title: "Components",
|
||||
items: [
|
||||
{ name: "Overview", href: "/components" },
|
||||
...componentPages.map((page) => ({ name: page.name, href: `/components/${page.slug}` })),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Native Platform",
|
||||
items: [
|
||||
{ name: "Windows", href: "/windows" },
|
||||
{ name: "Native Surfaces", href: "/native-surfaces" },
|
||||
{ name: "Menus", href: "/menus" },
|
||||
{ name: "Dialogs", href: "/dialogs" },
|
||||
{ name: "System Tray", href: "/tray" },
|
||||
{ name: "Keyboard Shortcuts", href: "/keyboard-shortcuts" },
|
||||
{ name: "Commands", href: "/commands" },
|
||||
{ name: "Native Controls", href: "/native-controls" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Automation & Testing",
|
||||
items: [
|
||||
{ name: "Automation", href: "/automation" },
|
||||
{ name: "Testing", href: "/testing" },
|
||||
{ name: "Testing in CI", href: "/testing/ci" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Packaging & Distribution",
|
||||
items: [
|
||||
{ name: "Packaging", href: "/packaging" },
|
||||
{ name: "Code Signing", href: "/packaging/signing" },
|
||||
{ name: "Updates", href: "/updates" },
|
||||
{ name: "Package Distribution", href: "/packages" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Mobile & Embedding",
|
||||
items: [{ name: "Embedded App", href: "/embed" }],
|
||||
},
|
||||
{
|
||||
title: "Web Content",
|
||||
items: [
|
||||
{ name: "Web Engines", href: "/web-engines" },
|
||||
{ name: "Web Content", href: "/frontend" },
|
||||
{ name: "Dev Server", href: "/cli/dev" },
|
||||
{ name: "Multiple WebViews", href: "/webviews" },
|
||||
{ name: "Bridge", href: "/bridge" },
|
||||
{ name: "Builtin Commands", href: "/bridge/builtin-commands" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Reference",
|
||||
items: [
|
||||
{ name: "App & Runtime", href: "/runtime" },
|
||||
{ name: "Capabilities", href: "/capabilities" },
|
||||
{ name: "Security", href: "/security" },
|
||||
{ name: "Platform Support", href: "/platform-support" },
|
||||
{ name: "Debugging", href: "/debugging" },
|
||||
{ name: "native doctor", href: "/debugging/doctor" },
|
||||
{ name: "Zig 0.16 Notes", href: "/zig" },
|
||||
{ name: "Extensions", href: "/extensions" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const allDocsPages: NavItem[] = navSections.flatMap((s) => s.items);
|
||||
@@ -0,0 +1,20 @@
|
||||
const REPO = "vercel-labs/native";
|
||||
const REVALIDATE = 86400;
|
||||
|
||||
export async function getStarCount(): Promise<string> {
|
||||
try {
|
||||
const res = await fetch(`https://api.github.com/repos/${REPO}`, {
|
||||
headers: { Accept: "application/vnd.github.v3+json" },
|
||||
next: { revalidate: REVALIDATE },
|
||||
});
|
||||
if (!res.ok) return "";
|
||||
const data = await res.json();
|
||||
const count = data.stargazers_count;
|
||||
if (typeof count !== "number") return "";
|
||||
if (count >= 1000)
|
||||
return `${(count / 1000).toFixed(count >= 10000 ? 0 : 1)}k`;
|
||||
return String(count);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
/**
|
||||
* Loader + thin typed wrapper for the live component-preview engine:
|
||||
* the Native SDK canvas runtime compiled to wasm32-freestanding
|
||||
* (`zig build docs-wasm-preview` → /public/wasm/component-preview.wasm).
|
||||
*
|
||||
* One wasm instance is shared by every preview on the page; each
|
||||
* preview is an engine-side handle (its own retained runtime + scene).
|
||||
* The module has zero imports — the page owns the clock and the canvas,
|
||||
* and `render` reports whether the retained display list actually
|
||||
* changed so an idle preview never repaints or blits.
|
||||
*/
|
||||
|
||||
interface PreviewExports {
|
||||
memory: WebAssembly.Memory;
|
||||
preview_alloc(len: number): number;
|
||||
preview_free(ptr: number, len: number): void;
|
||||
preview_instance_bytes(): number;
|
||||
preview_set_now_ms(ms: number): void;
|
||||
preview_create(namePtr: number, nameLen: number, dark: number): number;
|
||||
preview_destroy(handle: number): void;
|
||||
preview_logical_width(handle: number): number;
|
||||
preview_logical_height(handle: number): number;
|
||||
preview_set_theme(handle: number, dark: number): number;
|
||||
preview_set_theme_pack(handle: number, namePtr: number, nameLen: number): number;
|
||||
preview_pointer(handle: number, kind: number, x: number, y: number): void;
|
||||
preview_scroll(handle: number, x: number, y: number, dx: number, dy: number): void;
|
||||
preview_key(
|
||||
handle: number,
|
||||
phase: number,
|
||||
keyPtr: number,
|
||||
keyLen: number,
|
||||
textPtr: number,
|
||||
textLen: number,
|
||||
modifiers: number,
|
||||
): void;
|
||||
preview_text(handle: number, textPtr: number, textLen: number): void;
|
||||
preview_set_focused(handle: number, focused: number): void;
|
||||
preview_text_input_active(handle: number): number;
|
||||
preview_cursor(handle: number): number;
|
||||
preview_frame(handle: number): void;
|
||||
preview_pixel_width(handle: number, scale: number): number;
|
||||
preview_pixel_height(handle: number, scale: number): number;
|
||||
preview_pixel_byte_len(handle: number, scale: number): number;
|
||||
preview_render(
|
||||
handle: number,
|
||||
scale: number,
|
||||
pixelsPtr: number,
|
||||
pixelsLen: number,
|
||||
scratchPtr: number,
|
||||
scratchLen: number,
|
||||
): number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The engine's built-in theme packs by their manifest-facing names.
|
||||
* A pack is a complete token register (palette, control tables, type
|
||||
* scale) that composes with the light/dark scheme axis, so any pack ×
|
||||
* scheme combination is a live re-theme, never a scene rebuild from JS.
|
||||
*/
|
||||
export const themePacks = ["house", "geist"] as const;
|
||||
export type ThemePack = (typeof themePacks)[number];
|
||||
|
||||
export const PointerKind = {
|
||||
down: 0,
|
||||
up: 1,
|
||||
move: 2,
|
||||
drag: 3,
|
||||
cancel: 4,
|
||||
} as const;
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
export class PreviewEngine {
|
||||
constructor(private exports: PreviewExports) {}
|
||||
|
||||
create(scene: string, dark: boolean): LivePreview | null {
|
||||
const e = this.exports;
|
||||
const bytes = encoder.encode(scene);
|
||||
const ptr = e.preview_alloc(bytes.length);
|
||||
if (!ptr) return null;
|
||||
new Uint8Array(e.memory.buffer).set(bytes, ptr);
|
||||
const handle = e.preview_create(ptr, bytes.length, dark ? 1 : 0);
|
||||
e.preview_free(ptr, bytes.length);
|
||||
if (!handle) return null;
|
||||
return new LivePreview(e, handle);
|
||||
}
|
||||
}
|
||||
|
||||
export class LivePreview {
|
||||
readonly logicalWidth: number;
|
||||
readonly logicalHeight: number;
|
||||
private pixelsPtr = 0;
|
||||
private scratchPtr = 0;
|
||||
private bufferLen = 0;
|
||||
private imageData: ImageData | null = null;
|
||||
private destroyed = false;
|
||||
|
||||
constructor(
|
||||
private exports: PreviewExports,
|
||||
private handle: number,
|
||||
) {
|
||||
this.logicalWidth = exports.preview_logical_width(handle);
|
||||
this.logicalHeight = exports.preview_logical_height(handle);
|
||||
}
|
||||
|
||||
setNow(ms: number): void {
|
||||
this.exports.preview_set_now_ms(ms);
|
||||
}
|
||||
|
||||
pointer(kind: number, x: number, y: number): void {
|
||||
if (this.destroyed) return;
|
||||
this.exports.preview_pointer(this.handle, kind, x, y);
|
||||
}
|
||||
|
||||
scroll(x: number, y: number, dx: number, dy: number): void {
|
||||
if (this.destroyed) return;
|
||||
this.exports.preview_scroll(this.handle, x, y, dx, dy);
|
||||
}
|
||||
|
||||
key(phase: 0 | 1, key: string, text: string, modifiers: number): void {
|
||||
if (this.destroyed) return;
|
||||
const e = this.exports;
|
||||
const keyBytes = encoder.encode(key);
|
||||
const textBytes = encoder.encode(text);
|
||||
const len = keyBytes.length + textBytes.length;
|
||||
const ptr = len > 0 ? e.preview_alloc(len) : 0;
|
||||
if (len > 0 && !ptr) return;
|
||||
if (ptr) {
|
||||
const mem = new Uint8Array(e.memory.buffer);
|
||||
mem.set(keyBytes, ptr);
|
||||
mem.set(textBytes, ptr + keyBytes.length);
|
||||
}
|
||||
e.preview_key(this.handle, phase, ptr, keyBytes.length, ptr + keyBytes.length, textBytes.length, modifiers);
|
||||
if (ptr) e.preview_free(ptr, len);
|
||||
}
|
||||
|
||||
text(value: string): void {
|
||||
if (this.destroyed) return;
|
||||
const e = this.exports;
|
||||
const bytes = encoder.encode(value);
|
||||
if (bytes.length === 0) return;
|
||||
const ptr = e.preview_alloc(bytes.length);
|
||||
if (!ptr) return;
|
||||
new Uint8Array(e.memory.buffer).set(bytes, ptr);
|
||||
e.preview_text(this.handle, ptr, bytes.length);
|
||||
e.preview_free(ptr, bytes.length);
|
||||
}
|
||||
|
||||
textInputActive(): boolean {
|
||||
if (this.destroyed) return false;
|
||||
return this.exports.preview_text_input_active(this.handle) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The engine's cursor for the pointer's current hover target, mapped
|
||||
* to a CSS cursor keyword. The engine follows the native register:
|
||||
* arrow over controls (buttons, toggles, rows — the platform
|
||||
* convention), pointer ONLY over true hyperlinks, I-beam over text
|
||||
* entry, col-resize over split dividers and resizable edges. The
|
||||
* mirror is a straight enum-to-keyword map — it must never re-add
|
||||
* `pointer` for controls on the CSS side.
|
||||
*/
|
||||
cursor(): string {
|
||||
if (this.destroyed) return "default";
|
||||
switch (this.exports.preview_cursor(this.handle)) {
|
||||
case 1:
|
||||
return "pointer";
|
||||
case 2:
|
||||
return "text";
|
||||
case 3:
|
||||
return "col-resize";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror the canvas element's DOM focus into the engine view: a blur
|
||||
* drops the focus ring, caret, and caret-blink animation (parking the
|
||||
* render loop); a focus restores them for the retained focused widget.
|
||||
*/
|
||||
setFocused(focused: boolean): void {
|
||||
if (this.destroyed) return;
|
||||
this.exports.preview_set_focused(this.handle, focused ? 1 : 0);
|
||||
}
|
||||
|
||||
setTheme(dark: boolean): void {
|
||||
if (this.destroyed) return;
|
||||
this.exports.preview_set_theme(this.handle, dark ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the theme pack by name — the pack axis of the same in-place
|
||||
* re-theme `setTheme` performs on the scheme axis. The engine refuses
|
||||
* unknown names, so a stale page can never half-apply a pack.
|
||||
*/
|
||||
setThemePack(pack: ThemePack): void {
|
||||
if (this.destroyed) return;
|
||||
const e = this.exports;
|
||||
const bytes = encoder.encode(pack);
|
||||
const ptr = e.preview_alloc(bytes.length);
|
||||
if (!ptr) return;
|
||||
new Uint8Array(e.memory.buffer).set(bytes, ptr);
|
||||
e.preview_set_theme_pack(this.handle, ptr, bytes.length);
|
||||
e.preview_free(ptr, bytes.length);
|
||||
}
|
||||
|
||||
/** Step engine-owned frame animations (scroll momentum). */
|
||||
frame(): void {
|
||||
if (this.destroyed) return;
|
||||
this.exports.preview_frame(this.handle);
|
||||
}
|
||||
|
||||
pixelWidth(scale: number): number {
|
||||
return this.destroyed ? 0 : this.exports.preview_pixel_width(this.handle, scale);
|
||||
}
|
||||
|
||||
pixelHeight(scale: number): number {
|
||||
return this.destroyed ? 0 : this.exports.preview_pixel_height(this.handle, scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render at `scale` device pixels per logical unit. Returns the fresh
|
||||
* ImageData when the engine repainted, or null when the retained scene
|
||||
* is unchanged (skip the blit) or on error.
|
||||
*/
|
||||
render(scale: number): ImageData | null {
|
||||
if (this.destroyed) return null;
|
||||
const e = this.exports;
|
||||
const byteLen = e.preview_pixel_byte_len(this.handle, scale);
|
||||
if (!byteLen) return null;
|
||||
if (byteLen !== this.bufferLen) {
|
||||
this.releaseBuffers();
|
||||
this.pixelsPtr = e.preview_alloc(byteLen);
|
||||
this.scratchPtr = e.preview_alloc(byteLen);
|
||||
if (!this.pixelsPtr || !this.scratchPtr) {
|
||||
this.releaseBuffers();
|
||||
return null;
|
||||
}
|
||||
this.bufferLen = byteLen;
|
||||
this.imageData = null;
|
||||
}
|
||||
const status = e.preview_render(this.handle, scale, this.pixelsPtr, byteLen, this.scratchPtr, byteLen);
|
||||
if (status !== 1) return null;
|
||||
const width = e.preview_pixel_width(this.handle, scale);
|
||||
const height = e.preview_pixel_height(this.handle, scale);
|
||||
if (!width || !height) return null;
|
||||
if (!this.imageData || this.imageData.width !== width || this.imageData.height !== height) {
|
||||
this.imageData = new ImageData(width, height);
|
||||
}
|
||||
// Copy out of wasm memory: the buffer may move on memory growth, so
|
||||
// never hand long-lived views to the canvas.
|
||||
this.imageData.data.set(new Uint8ClampedArray(e.memory.buffer, this.pixelsPtr, byteLen));
|
||||
return this.imageData;
|
||||
}
|
||||
|
||||
private releaseBuffers(): void {
|
||||
const e = this.exports;
|
||||
if (this.pixelsPtr) e.preview_free(this.pixelsPtr, this.bufferLen);
|
||||
if (this.scratchPtr) e.preview_free(this.scratchPtr, this.bufferLen);
|
||||
this.pixelsPtr = 0;
|
||||
this.scratchPtr = 0;
|
||||
this.bufferLen = 0;
|
||||
this.imageData = null;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (this.destroyed) return;
|
||||
this.releaseBuffers();
|
||||
this.exports.preview_destroy(this.handle);
|
||||
this.destroyed = true;
|
||||
}
|
||||
}
|
||||
|
||||
let enginePromise: Promise<PreviewEngine | null> | null = null;
|
||||
|
||||
/**
|
||||
* Fetch + instantiate the shared engine once. Resolves null when wasm
|
||||
* is unavailable (the static webp layer simply stays).
|
||||
*/
|
||||
export function loadPreviewEngine(): Promise<PreviewEngine | null> {
|
||||
if (!enginePromise) {
|
||||
enginePromise = instantiate().catch(() => null);
|
||||
}
|
||||
return enginePromise;
|
||||
}
|
||||
|
||||
async function instantiate(): Promise<PreviewEngine | null> {
|
||||
if (typeof WebAssembly === "undefined") return null;
|
||||
const url = "/wasm/component-preview.wasm";
|
||||
let instance: WebAssembly.Instance;
|
||||
try {
|
||||
const streamed = await WebAssembly.instantiateStreaming(fetch(url), {});
|
||||
instance = streamed.instance;
|
||||
} catch {
|
||||
// Older servers without the application/wasm MIME type.
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) return null;
|
||||
const bytes = await response.arrayBuffer();
|
||||
const result = await WebAssembly.instantiate(bytes, {});
|
||||
instance = result.instance;
|
||||
}
|
||||
return new PreviewEngine(instance.exports as unknown as PreviewExports);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import vocab from "./component-vocab.json";
|
||||
import { componentPages } from "./components-pages";
|
||||
|
||||
/**
|
||||
* Turn a docs page.mdx source into clean plain markdown for "Copy Page"
|
||||
* and the /md/<slug> route. The .mdx source is the truth; this strips the
|
||||
* MDX-only parts (imports, JSX components) and replaces the data-driven
|
||||
* components with the same content they render:
|
||||
*
|
||||
* - `<AttrTable attrs={[...]} element="x" />` becomes a markdown table
|
||||
* using the same component-vocab.json lookup the component uses.
|
||||
* - `<ComponentIndexGrid />` becomes the component index as a list.
|
||||
* - `<IconGallery />` becomes the icon-name list from the vocabulary.
|
||||
* - `<ComponentPreview ... />` (an engine-rendered image) is dropped.
|
||||
* - `<CodeToggle>` wrappers are unwrapped: the fenced samples inside are
|
||||
* plain markdown already, so only the tags drop.
|
||||
* - Filenamed fences (```ts:src/core.ts) become a labeled `path`: line
|
||||
* above a plain ```ts fence.
|
||||
*
|
||||
* Inline HTML (tables, definition lists) passes through untouched —
|
||||
* it is valid markdown as written.
|
||||
*/
|
||||
export function mdxToCleanMarkdown(raw: string): string {
|
||||
const lines = raw.split("\n");
|
||||
const out: string[] = [];
|
||||
let jsxBlock: string[] | null = null;
|
||||
let inFence = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Fenced code passes through verbatim: TypeScript samples start lines
|
||||
// with `import`/`export`, which only MDX-level lines may strip. The one
|
||||
// rewrite is a filenamed opener (```ts:src/core.ts — the site renders
|
||||
// the path as a header row): copied markdown gets the path as a labeled
|
||||
// line above a plain fence, so any renderer highlights the real
|
||||
// language instead of choking on the colon.
|
||||
if (jsxBlock === null && trimmed.startsWith("```")) {
|
||||
if (!inFence) {
|
||||
const meta = trimmed.match(/^```([^\s:`]+):(\S+)$/);
|
||||
if (meta) {
|
||||
out.push(`\`${meta[2]}\`:`);
|
||||
out.push("");
|
||||
out.push("```" + meta[1]);
|
||||
inFence = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
inFence = !inFence;
|
||||
out.push(line);
|
||||
continue;
|
||||
}
|
||||
if (inFence) {
|
||||
out.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (jsxBlock === null && (trimmed.startsWith("export ") || trimmed.startsWith("import "))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// CodeToggle is the one paired-tag component: its children are ordinary
|
||||
// fenced code blocks, valid markdown as written, so drop only the tags.
|
||||
if (jsxBlock === null && (trimmed === "<CodeToggle>" || trimmed === "</CodeToggle>")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// A capitalized JSX component; every other docs component is self-closing.
|
||||
if (jsxBlock === null && /^<[A-Z]/.test(trimmed)) {
|
||||
jsxBlock = [line];
|
||||
} else if (jsxBlock !== null) {
|
||||
jsxBlock.push(line);
|
||||
} else {
|
||||
out.push(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmed.endsWith("/>")) {
|
||||
const replacement = renderJsxComponent(jsxBlock.join("\n"));
|
||||
if (replacement) out.push(replacement);
|
||||
jsxBlock = null;
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
.join("\n")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
type Doc = { name: string; doc: string };
|
||||
|
||||
function renderJsxComponent(block: string): string | null {
|
||||
if (block.startsWith("<AttrTable")) return renderAttrTable(block);
|
||||
if (block.startsWith("<ComponentIndexGrid")) return renderComponentIndex();
|
||||
if (block.startsWith("<IconGallery")) return renderIconList();
|
||||
// ComponentPreview and anything unknown: an image or purely visual
|
||||
// element with no markdown equivalent.
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The same lookup AttrTable does: scoped table first, then shared tables. */
|
||||
function lookupAttr(name: string, element?: string): Doc | undefined {
|
||||
if (element) {
|
||||
const table = (vocab.scoped as Record<string, Doc[]>)[element];
|
||||
const hit = table?.find((doc) => doc.name === name);
|
||||
if (hit) return hit;
|
||||
}
|
||||
return (
|
||||
(vocab.attributes as Doc[]).find((doc) => doc.name === name) ??
|
||||
(vocab.events as Doc[]).find((doc) => doc.name === name)
|
||||
);
|
||||
}
|
||||
|
||||
function renderAttrTable(block: string): string {
|
||||
const element = block.match(/element="([^"]+)"/)?.[1];
|
||||
const attrsSource = block.match(/attrs=\{\[([\s\S]*?)\]\}/)?.[1] ?? "";
|
||||
const attrs = [...attrsSource.matchAll(/"([^"]+)"/g)].map((m) => m[1]!);
|
||||
|
||||
const rows = attrs.map((name) => {
|
||||
const doc = lookupAttr(name, element);
|
||||
return `| \`${name}\` | ${doc ? escapeCell(doc.doc) : ""} |`;
|
||||
});
|
||||
return ["| Attribute | Description |", "| --- | --- |", ...rows].join("\n");
|
||||
}
|
||||
|
||||
function renderComponentIndex(): string {
|
||||
return componentPages
|
||||
.map((page) => `- [${page.name}](/components/${page.slug}) — ${page.blurb}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function renderIconList(): string {
|
||||
const icons = vocab.icons as string[];
|
||||
return `Built-in icon names: ${icons.map((name) => `\`${name}\``).join(", ")}.`;
|
||||
}
|
||||
|
||||
function escapeCell(text: string): string {
|
||||
return text.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { Metadata } from "next";
|
||||
import { PAGE_TITLES } from "./page-titles";
|
||||
|
||||
const DESCRIPTION =
|
||||
"Toolkit for building native desktop apps: declarative markup views, a predictable message-based state model, and its own engine rendering into real OS windows — no browser or WebView in the binary.";
|
||||
|
||||
export function pageMetadata(slug: string): Metadata {
|
||||
const title = PAGE_TITLES[slug];
|
||||
if (!title) return {};
|
||||
|
||||
const displayTitle = title.replace(/\n/g, " ");
|
||||
const fullTitle = `${displayTitle} | Native SDK`;
|
||||
const ogImageUrl = slug ? `/og/${slug}` : "/og";
|
||||
|
||||
return {
|
||||
title: displayTitle,
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
siteName: "Native SDK",
|
||||
title: fullTitle,
|
||||
description: DESCRIPTION,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: `${displayTitle} - Native SDK`,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: fullTitle,
|
||||
description: DESCRIPTION,
|
||||
images: [ogImageUrl],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { componentPages } from "./components-pages";
|
||||
|
||||
export const PAGE_TITLES: Record<string, string> = {
|
||||
components: "Components",
|
||||
// One title per component page, from the shared inventory.
|
||||
...Object.fromEntries(componentPages.map((page) => [`components/${page.slug}`, page.name])),
|
||||
"": "The Complete Toolkit\nfor Native Apps",
|
||||
introduction: "Introduction",
|
||||
"quick-start": "Quick Start",
|
||||
"app-model": "App Model",
|
||||
typescript: "TypeScript Cores",
|
||||
"typescript/packages": "Where Packages Go",
|
||||
"native-ui": "Native UI",
|
||||
state: "State & Data Flow",
|
||||
theming: "Theming",
|
||||
"building-components": "Building Components",
|
||||
runtime: "App & Runtime",
|
||||
frontend: "Frontend Projects",
|
||||
"native-surfaces": "Native Surfaces",
|
||||
windows: "Windows",
|
||||
webviews: "Multiple WebViews",
|
||||
"keyboard-shortcuts": "Keyboard Shortcuts",
|
||||
commands: "Commands",
|
||||
bridge: "Bridge",
|
||||
"bridge/builtin-commands": "Builtin Commands",
|
||||
dialogs: "Dialogs",
|
||||
menus: "Menus",
|
||||
"native-controls": "Native Controls",
|
||||
"built-in-components": "Built-in Components",
|
||||
capabilities: "Capabilities",
|
||||
"platform-support": "Platform Support",
|
||||
tray: "System Tray",
|
||||
security: "Security",
|
||||
cli: "CLI",
|
||||
"cli/dev": "Dev Server",
|
||||
skills: "Agent Skills",
|
||||
packaging: "Packaging",
|
||||
"packaging/signing": "Code Signing",
|
||||
updates: "Updates",
|
||||
"app-zon": "Config",
|
||||
debugging: "Debugging",
|
||||
"debugging/doctor": "native doctor",
|
||||
automation: "Automation",
|
||||
testing: "Testing",
|
||||
"testing/ci": "Testing in CI",
|
||||
extensions: "Extensions",
|
||||
embed: "Embedded App",
|
||||
"web-engines": "Web Engines",
|
||||
packages: "Package Distribution",
|
||||
zig: "Zig 0.16 Notes",
|
||||
};
|
||||
|
||||
export function getPageTitle(slug: string): string | null {
|
||||
return slug in PAGE_TITLES ? PAGE_TITLES[slug]! : null;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { allDocsPages } from "./docs-navigation";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
export type IndexEntry = {
|
||||
title: string;
|
||||
href: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
let cached: IndexEntry[] | null = null;
|
||||
|
||||
export async function getSearchIndex(): Promise<IndexEntry[]> {
|
||||
if (cached) return cached;
|
||||
|
||||
// The homepage lives in the site header, not the docs nav, so index it explicitly.
|
||||
const pages = [{ name: "Home", href: "/" }, ...allDocsPages];
|
||||
const entries: IndexEntry[] = await Promise.all(
|
||||
pages.map(async (item) => ({
|
||||
title: item.name,
|
||||
href: item.href,
|
||||
content: await pageContent(item.href, item.name),
|
||||
})),
|
||||
);
|
||||
|
||||
cached = entries;
|
||||
return entries;
|
||||
}
|
||||
|
||||
async function pageContent(href: string, fallback: string): Promise<string> {
|
||||
// The homepage is a TSX landing page, not MDX; index a hand-written summary.
|
||||
if (href === "/") {
|
||||
return "Toolkit for building native desktop apps. Cross-platform native UI: declarative Native markup views (.native), design tokens, TypeScript or Zig app cores, a predictable message-based state loop, hot reload, typed effects, AI agent skills and evals, automation, WebView coexistence on macOS, Linux, Windows, iOS, and Android.";
|
||||
}
|
||||
const relative = path.join(href.slice(1), "page.mdx");
|
||||
const filePath = path.join(process.cwd(), "src", "app", relative);
|
||||
try {
|
||||
const source = await readFile(filePath, "utf8");
|
||||
return stripMdx(source);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function stripMdx(source: string): string {
|
||||
return source
|
||||
.replace(/```[\s\S]*?```/g, " ")
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/[#*_`[\](){}>-]/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* The single source of truth for the project's public name and every
|
||||
* name-bearing string on the site. Rename day touches this file only —
|
||||
* all copy elsewhere is written name-neutral and pulls from here.
|
||||
*/
|
||||
export const siteName = "Native SDK";
|
||||
|
||||
/** The npm package that carries the CLI. */
|
||||
export const npmCli = "@native-sdk/cli";
|
||||
|
||||
/** The public repository. */
|
||||
export const githubUrl = "https://github.com/vercel-labs/native";
|
||||
|
||||
/** The canonical site origin (used for metadata + Open Graph). */
|
||||
export const siteUrl = "https://native-sdk.dev";
|
||||
|
||||
/** The one-line tagline used in titles and Open Graph text. */
|
||||
export const tagline = "The Complete Toolkit for Native Apps";
|
||||
|
||||
/** The one-sentence description used in metadata. */
|
||||
export const description =
|
||||
"Native SDK is the complete toolkit for building beautiful native desktop applications: declarative markup, a predictable message-based state model, a modern component library, and its own native renderer — no browser, no WebView, no compromise.";
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Reference in New Issue
Block a user