Component Preview

code-block and preview of the code

Component Preview - Preview

1 function InstallDropdown({ slug }: { slug: string }) {2const [open, setOpen] = useState(false);3const [selected, setSelected] = useState<PackageManager>("npx");4const [copied, setCopied] = useState(false);5const ref = useRef<HTMLDivElement>(null);6      7useEffect(() => {8  function handleOutside(e: MouseEvent) {9    if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);10  }11  document.addEventListener("mousedown", handleOutside);12  return () => document.removeEventListener("mousedown", handleOutside);13}, []);14      15const command = packageCommands[selected](slug);16      17const handleCopy = async () => {18  if (copied) return;19  try {20    if (navigator.clipboard && window.isSecureContext) {21      await navigator.clipboard.writeText(command);22    } else {23      const el = document.createElement("textarea");24      el.value = command;25      el.style.cssText = "position:absolute;left:-9999px;top:-9999px";26      document.body.appendChild(el);27      el.focus();28      el.select();29      document.execCommand("copy");30      document.body.removeChild(el);31    }32    setCopied(true);33    setTimeout(() => setCopied(false), 3000);34  } catch (err) {35    console.error("Copy failed:", err);36  }37};
1 function InstallDropdown({ slug }: { slug: string }) {2const [open, setOpen] = useState(false);3const [selected, setSelected] = useState<PackageManager>("npx");4const [copied, setCopied] = useState(false);5const ref = useRef<HTMLDivElement>(null);6      7useEffect(() => {8  function handleOutside(e: MouseEvent) {9    if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);10  }11  document.addEventListener("mousedown", handleOutside);12  return () => document.removeEventListener("mousedown", handleOutside);13}, []);14      15const command = packageCommands[selected](slug);16      17const handleCopy = async () => {18  if (copied) return;19  try {20    if (navigator.clipboard && window.isSecureContext) {21      await navigator.clipboard.writeText(command);22    } else {23      const el = document.createElement("textarea");24      el.value = command;25      el.style.cssText = "position:absolute;left:-9999px;top:-9999px";26      document.body.appendChild(el);27      el.focus();28      el.select();29      document.execCommand("copy");30      document.body.removeChild(el);31    }32    setCopied(true);33    setTimeout(() => setCopied(false), 3000);34  } catch (err) {35    console.error("Copy failed:", err);36  }37};

Manual Installation

1

Install dependencies

Terminal
bash
npm i clsx tailwind-merge react-icons shaki
2

Add the util file

lib/utils.ts
ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
3

Copy the source code

components/lokalhost_io/code-blocks/component-preview.tsx
tsx
"use client";

import React, { forwardRef, useSyncExternalStore, useEffect, useState } from "react";

function cx(...classes: Array<string | false | null | undefined>) {
return classes.filter(Boolean).join(" ");
}
export type Platform = "mac" | "windows" | "linux" | "custom";
export type ButtonSize = "sm" | "md" | "lg" | "xl";

export interface PlatformConfig {
id: Platform;
label: string;
icon?: React.ReactNode;
downloadUrl?: string;
fileName?: string;
}

export interface DownloadButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
platform?: Platform;

label?: React.ReactNode;

icon?: React.ReactNode;
iconPosition?: "left" | "right";
showIcon?: boolean;

as?: "button" | "a";
href?: string;
fileName?: string;
target?: string;
rel?: string;

size?: ButtonSize;

/** State */
isLoading?: boolean;
loadingText?: React.ReactNode;
loadingIcon?: React.ReactNode;
disabled?: boolean;

className?: string;
wrapperClassName?: string;
colorClassName?: string;

wrapperColorClassName?: string;

iconClassName?: string;

/** Callbacks */
onDownloadStart?: (platform: Platform) => void;
onDownloadComplete?: (platform: Platform) => void;
onDownloadError?: (error: Error) => void;

children?: React.ReactNode;
}

/* ------------------------------------------------------------------ */
/* Size config                                                        */
/* ------------------------------------------------------------------ */

export const sizeConfig: Record<
ButtonSize,
{
  outerHeight: string;
  outerMinWidth: string;
  innerMinWidth: string;
  text: string;
  gap: string;
  iconSize: string;
}
> = {
sm: {
  outerHeight: "h-8",
  outerMinWidth: "min-w-[108px]",
  innerMinWidth: "min-w-[96px]",
  text: "text-xs",
  gap: "gap-1.5",
  iconSize: "[&_svg]:size-3.5",
},
md: {
  outerHeight: "h-9",
  outerMinWidth: "min-w-[128px]",
  innerMinWidth: "min-w-[116px]",
  text: "text-sm",
  gap: "gap-2",
  iconSize: "[&_svg]:size-4",
},
lg: {
  outerHeight: "h-11",
  outerMinWidth: "min-w-[148px]",
  innerMinWidth: "min-w-[136px]",
  text: "text-base",
  gap: "gap-2",
  iconSize: "[&_svg]:size-[18px]",
},
xl: {
  outerHeight: "h-12",
  outerMinWidth: "min-w-[168px]",
  innerMinWidth: "min-w-[156px]",
  text: "text-lg",
  gap: "gap-2.5",
  iconSize: "[&_svg]:size-5",
},
};

/* ------------------------------------------------------------------ */
/* Default styling — split into STRUCTURE and COLOR                   */
/* ------------------------------------------------------------------ */

export const structuralButtonClassName =
"inline-flex items-center rounded-lg px-1 text-sm font-medium font-sans " +
"overflow-hidden transition-colors duration-150 " +
"disabled:opacity-50 disabled:cursor-not-allowed " +
"shadow-[0px_0px_0px_1px_rgba(0,0,0,0.06),0px_1px_1px_-0.5px_rgba(0,0,0,0.06),0px_3px_3px_-1.5px_rgba(0,0,0,0.06),0px_6px_6px_-3px_rgba(0,0,0,0.06),0px_12px_12px_-6px_rgba(0,0,0,0.06),0px_24px_24px_-12px_rgba(0,0,0,0.06)]";

/** Default color classes for the outer shell — fully replaceable via "colorClassName". */
export const defaultButtonColorClassName =
"bg-neutral-800 dark:bg-neutral-200 text-neutral-100 dark:text-neutral-900 " +
"border border-neutral-700 dark:border-neutral-300 " +
"hover:bg-neutral-950 dark:hover:bg-neutral-100";

export const structuralWrapperClassName =
"inline-flex items-center rounded-sm px-2 py-[3.5px] shadow-sm transition-colors duration-150";

/** Default color classes for the inner wrapper — fully replaceable via "wrapperColorClassName". */
export const defaultWrapperColorClassName =
"bg-neutral-950 dark:bg-neutral-100 text-neutral-100 dark:text-neutral-900 " +
"hover:bg-neutral-800 dark:hover:bg-neutral-200";

export const defaultButtonClassName = cx(
structuralButtonClassName,
sizeConfig.md.outerHeight,
sizeConfig.md.text,
defaultButtonColorClassName
);

export const defaultWrapperClassName = cx(
structuralWrapperClassName,
sizeConfig.md.gap,
defaultWrapperColorClassName
);

/* ------------------------------------------------------------------ */
/* Default platform config (data-driven — edit/extend freely)         */
/* ------------------------------------------------------------------ */

export const defaultPlatformConfig: Record<Platform, PlatformConfig> = {
mac: {
  id: "mac",
  label: "Download for Mac",
  icon: null, // plug in your own icon component/svg
  downloadUrl: "",
  fileName: "",
},
windows: {
  id: "windows",
  label: "Download for Windows",
  icon: null,
  downloadUrl: "",
  fileName: "",
},
linux: {
  id: "linux",
  label: "Download for Linux",
  icon: null,
  downloadUrl: "",
  fileName: "",
},
custom: {
  id: "custom",
  label: "Download",
  icon: null,
},
};

// No real browser event to subscribe to (UA doesn't change at runtime),
// so subscribe is a no-op — we only need getSnapshot/getServerSnapshot.
function subscribeNoop() {
return () => {};
}

function getPlatformSnapshot(): Platform | null {
const ua = window.navigator.userAgent.toLowerCase();
// check android before linux — Android UAs contain "linux" too
if (ua.includes("android")) return null;
if (ua.includes("mac")) return "mac";
if (ua.includes("win")) return "windows";
if (ua.includes("linux")) return "linux";
return null;
}

function getPlatformServerSnapshot(): Platform | null {
return null;
}

export function useDetectedPlatform(): Platform | null {
return useSyncExternalStore(
  subscribeNoop,
  getPlatformSnapshot,
  getPlatformServerSnapshot
);
}

/* ------------------------------------------------------------------ */
/* Hover animation — slides content up, teleports to bottom, slides in */
/* ------------------------------------------------------------------ */

const BTN_HOVER_CSS = `
@keyframes keep-btn-enter {
0% { transform: translateY(0); opacity: 1; }
30% { transform: translateY(-100%); opacity: 0; }
32% { transform: translateY(100%); opacity: 0; }
80% { transform: translateY(0); opacity: 1; }
100% { transform: translateY(0); opacity: 1; }
}
@keyframes keep-btn-leave {
0% { transform: translateY(0); opacity: 1; }
30% { transform: translateY(100%); opacity: 0; }
32% { transform: translateY(-100%); opacity: 0; }
80% { transform: translateY(0); opacity: 1; }
100% { transform: translateY(0); opacity: 1; }
}
.keep-btn-enter {
animation: keep-btn-enter 0.45s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.keep-btn-leave {
animation: keep-btn-leave 0.45s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
`;

let keepBtnCssInjected = false;

function injectKeepBtnCSS() {
if (keepBtnCssInjected) return;
keepBtnCssInjected = true;
if (typeof document !== "undefined") {
  const s = document.createElement("style");
  s.textContent = BTN_HOVER_CSS;
  document.head.appendChild(s);
}
}

/* ------------------------------------------------------------------ */
/* Component                                                          */
/* ------------------------------------------------------------------ */

export const DownloadButton = forwardRef<
HTMLButtonElement | HTMLAnchorElement,
DownloadButtonProps
>(
(
  {
    platform = "custom",
    label,
    icon,
    iconPosition = "left",
    showIcon = true,

    as = "button",
    href,
    fileName,
    target,
    rel,

    size = "md",

    isLoading = false,
    loadingText,
    loadingIcon,
    disabled = false,

    className,
    colorClassName,
    iconClassName = "",
    wrapperClassName,
    wrapperColorClassName,

    onDownloadStart,
    onDownloadComplete,
    onDownloadError,

    children,
    onClick,
    onMouseEnter: onMouseEnterProp,
    onMouseLeave: onMouseLeaveProp,
    ...rest
  },
  ref
) => {
  const config = defaultPlatformConfig[platform];
  const sizing = sizeConfig[size];

  useEffect(() => { injectKeepBtnCSS(); }, []);

  const [animClass, setAnimClass] = useState<"keep-btn-enter" | "keep-btn-leave" | "">("");

  const resolvedLabel = children ?? label ?? config.label;
  const resolvedIcon = icon ?? config.icon;
  const resolvedHref = href ?? config.downloadUrl;
  const resolvedFileName = fileName ?? config.fileName;

  const handleClick = (
    e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>
  ) => {
    if (disabled || isLoading) {
      e.preventDefault();
      return;
    }
    try {
      onDownloadStart?.(platform);
      onClick?.(e as React.MouseEvent<HTMLButtonElement>);
      onDownloadComplete?.(platform);
    } catch (err) {
      onDownloadError?.(err instanceof Error ? err : new Error(String(err)));
    }
  };

  const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
    onMouseEnterProp?.(e as React.MouseEvent<HTMLButtonElement>);
    setAnimClass("keep-btn-enter");
  };

  const handleMouseLeave = (e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => {
    onMouseLeaveProp?.(e as React.MouseEvent<HTMLButtonElement>);
    setAnimClass("keep-btn-leave");
  };

  const handleAnimEnd = () => {
    setAnimClass("");
  };

  const iconNode = showIcon
    ? isLoading
      ? loadingIcon ?? resolvedIcon
      : resolvedIcon
    : null;

  const labelNode = isLoading ? loadingText ?? resolvedLabel : resolvedLabel;

  // Inner wrapper: full override wins; otherwise compose structure + size + color.
  const resolvedWrapperClassName =
    wrapperClassName ??
    cx(
      animClass,
      structuralWrapperClassName,
      sizing.gap,
      sizing.innerMinWidth,
      "justify-center",
      wrapperColorClassName ?? defaultWrapperColorClassName
    );

  const content = (
    <span className={resolvedWrapperClassName} data-slot="content-wrapper" onAnimationEnd={handleAnimEnd}>
      {iconPosition === "left" && iconNode ? (
        <span className={cx(sizing.iconSize, iconClassName)} data-slot="icon">
          {iconNode}
        </span>
      ) : null}
      <span data-slot="label">{labelNode}</span>
      {iconPosition === "right" && iconNode ? (
        <span className={cx(sizing.iconSize, iconClassName)} data-slot="icon">
          {iconNode}
        </span>
      ) : null}
    </span>
  );

  // Outer shell: full override wins; otherwise compose structure + size + color.
  const resolvedClassName =
    className ??
    cx(
      structuralButtonClassName,
      sizing.outerHeight,
      sizing.outerMinWidth,
      sizing.text,
      "justify-center",
      colorClassName ?? defaultButtonColorClassName
    );

  const sharedProps = {
    className: resolvedClassName,
    onMouseEnter: handleMouseEnter,
    onMouseLeave: handleMouseLeave,
    "data-platform": platform,
    "data-size": size,
    "data-loading": isLoading || undefined,
    "data-disabled": disabled || undefined,
    "aria-disabled": disabled || isLoading || undefined,
    "aria-busy": isLoading || undefined,
  };

  if (as === "a") {
    return (
      <a
        ref={ref as React.Ref<HTMLAnchorElement>}
        href={disabled || isLoading ? undefined : resolvedHref}
        download={resolvedFileName || true}
        target={target}
        rel={rel ?? (target === "_blank" ? "noopener noreferrer" : undefined)}
        onClick={handleClick}
        {...sharedProps}
        {...(rest as React.AnchorHTMLAttributes<HTMLAnchorElement>)}
      >
        {content}
      </a>
    );
  }

  return (
    <button
      ref={ref as React.Ref<HTMLButtonElement>}
      type="button"
      disabled={disabled || isLoading}
      onClick={handleClick}
      {...sharedProps}
      {...rest}
    >
      {content}
    </button>
  );
}
);

DownloadButton.displayName = "DownloadButton";

/* ------------------------------------------------------------------ */
/* Group wrapper — renders Mac / Windows / Linux together             */
/* ------------------------------------------------------------------ */

export interface DownloadButtonGroupProps {
platforms?: Platform[];
config?: Partial<Record<Platform, Partial<PlatformConfig>>>;
highlightDetected?: boolean;
as?: "button" | "a";
size?: ButtonSize;
className?: string;
colorClassName?: string;
wrapperColorClassName?: string;
buttonProps?: Partial<DownloadButtonProps>;
}

export function DownloadButtonGroup({
platforms = ["mac", "windows", "linux"],
config,
highlightDetected = false,
as = "a",
size = "md",
className = "",
colorClassName,
wrapperColorClassName,
buttonProps,
}: DownloadButtonGroupProps) {
const detected = useDetectedPlatform();
const merged: Record<Platform, PlatformConfig> = platforms.reduce(
  (acc, p) => {
    acc[p] = { ...defaultPlatformConfig[p], ...config?.[p] };
    return acc;
  },
  {} as Record<Platform, PlatformConfig>
);

return (
  <div className={className} data-slot="download-group">
    {platforms.map((p) => {
      const cfg = merged[p];
      const isDetected = highlightDetected && detected === p;

      return (
        <DownloadButton
          key={p}
          platform={p}
          as={as}
          size={size}
          colorClassName={colorClassName}
          wrapperColorClassName={wrapperColorClassName}
          href={cfg.downloadUrl}
          fileName={cfg.fileName}
          data-detected={isDetected || undefined}
          {...buttonProps}
        />
      );
    })}
  </div>
);
}

Props

PropTypeDescription
link*stringIdentifies this preview and derives the install slug (last path segment) used for CLI/AI-prompt commands and the iframe preview URL.
childrenReact.ReactNodeThe live component to render in the Preview tab. Omit for a code-only entry — shows a fallback message instead of an empty panel.
codestringSource code shown in the Code tab, syntax-highlighted via Shiki. Omit to show a "no code provided" message instead.
languagestringShiki language id used to highlight `code`. Defaults to "tsx".
classNamestringExtra classes applied to the inner white/black panel (border, radius, background).
frameClassNamestringClasses for the outer padded frame behind the panel — the "outer-layer" background color. Defaults to "bg-neutral-100 dark:bg-neutral-900".
useIframebooleanRenders the Preview tab as an iframe pointed at `{prePath}/preview/{link}` instead of rendering `children` directly.
compactbooleanReduces the Preview tab's minimum height from 500px to 120px. Ignored when `isBlock` is true.
isBlockbooleanRemoves the default padding around `children` in the Preview tab, for components that manage their own spacing.
commentstring[]Optional list of short tags rendered as pills below the frame (e.g. notes like "client component", "requires Framer Motion").
lightTheme
github-light-defaultone-lightcatppuccin-latte
Shiki theme used in the Code tab when the app is in light mode. Defaults to "github-light-default".
darkTheme
one-dark-progithub-dark-defaultdraculatokyo-night
Shiki theme used in the Code tab when the app is in dark mode. Defaults to "one-dark-pro".
showLineNumbersbooleanShows a line-number gutter in the Code tab. Defaults to true.
maxCodeHeightnumberMax height in px of the Code tab before it scrolls. Defaults to 500.
defaultTab
PreviewCode
Which tab is active on first render. Defaults to "Preview".
onCopyCode(code: string) => voidCalled after the code is successfully copied — hook up analytics or a toast here.