Vela UI Docsvenizia.ai

Doc Typography

The borderless, auto-growing text line for paragraphs, three heading levels and quotes, plus the divider.

npx shadcn@latest add @velaui/doc-typography
npx shadcn@latest add @velaui/doc-typography

Examples

  • Default
  • Headings
  • Quote
  • Inline formatting

View code

Doc Typography · Default

Installation

pnpm dlx velaui add @velaui/doc-typography
pnpm dlx velaui add @velaui/doc-typography

Code

doc-typography-default.tsx
import * as React from "react";

import { DocDivider, DocTypography } from "@/components/ui/doc-typography";

export default function Example() {
  const [title, setTitle] = React.useState("Q4 launch plan");
  const [body, setBody] = React.useState("Everything the team needs for the launch, in one place.");
  return (
    <div className="w-full max-w-xl space-y-1 p-6">
      <DocTypography type="h1" value={title} onChange={setTitle} />
      <DocTypography value={body} onChange={setBody} />
      <DocDivider />
    </div>
  );
}

View code

Doc Typography · Headings

Installation

pnpm dlx velaui add @velaui/doc-typography
pnpm dlx velaui add @velaui/doc-typography

Code

doc-typography-headings.tsx
import * as React from "react";

import { DocTypography, type DocTypographyType } from "@/components/ui/doc-typography";

const LEVELS: [DocTypographyType, string][] = [
  ["h1", "Heading 1"],
  ["h2", "Heading 2"],
  ["h3", "Heading 3"],
  ["text", "Paragraph text"],
];

export default function Example() {
  const [values, setValues] = React.useState(() => LEVELS.map(([, v]) => v));
  return (
    <div className="w-full max-w-xl space-y-2 p-6">
      {LEVELS.map(([type], i) => (
        <DocTypography
          key={type}
          type={type}
          value={values[i]}
          onChange={(v) => setValues(values.map((x, j) => (j === i ? v : x)))}
        />
      ))}
    </div>
  );
}

View code

Doc Typography · Quote

Installation

pnpm dlx velaui add @velaui/doc-typography
pnpm dlx velaui add @velaui/doc-typography

Code

doc-typography-quote.tsx
import { DocTypography } from "@/components/ui/doc-typography";

export default function Example() {
  return (
    <div className="w-full max-w-xl p-6">
      <DocTypography type="quote" readOnly value="Ship small, ship often." />
    </div>
  );
}

View code

Doc Typography · Inline formatting

Installation

pnpm dlx velaui add @velaui/doc-typography
pnpm dlx velaui add @velaui/doc-typography

Code

doc-typography-inline-formatting.tsx
import { DocTypography } from "@/components/ui/doc-typography";

const mark = "rounded-sm px-0.5 text-text-strong";

export default function Example() {
  return (
    <div className="w-full max-w-xl space-y-2 p-6">
      <DocTypography type="h2" readOnly value="Inline formatting" />
      <ul className="list-disc space-y-1 pl-5 text-sm leading-[1.6] text-text-strong">
        <li>
          <strong>bold</strong>
        </li>
        <li>
          <em>italic</em>
        </li>
        <li>
          <s>strikethrough</s>
        </li>
        <li>
          <mark className={`${mark} bg-warning`}>highlighting</mark>{" "}
          <mark className={`${mark} bg-success`}>in a</mark> <mark className={`${mark} bg-info`}>variety</mark>{" "}
          <mark className={`${mark} bg-negative`}>of different</mark>{" "}
          <mark className={`${mark} bg-bg-surface-2`}>colors</mark>
        </li>
        <li>
          <code className="rounded-sm bg-bg-surface-2 px-1 font-mono text-[0.9em]">monospace</code>
        </li>
        <li>
          <u>underlined</u>
        </li>
        <li>
          {/* Source only, like DocMath: there is no typesetter in the catalog. */}
          <code className="font-mono text-[0.9em] italic" aria-label="LaTeX: E equals m c squared">
            E = mc^2
          </code>
        </li>
      </ul>
    </div>
  );
}

Usage

import * as React from "react";

import { DocDivider, DocTypography } from "@/components/ui/doc-typography";

export default function Example() {
  const [title, setTitle] = React.useState("Q4 launch plan");
  const [body, setBody] = React.useState("Everything the team needs for the launch, in one place.");
  return (
    <div className="w-full max-w-xl space-y-1 p-6">
      <DocTypography type="h1" value={title} onChange={setTitle} />
      <DocTypography value={body} onChange={setBody} />
      <DocDivider />
    </div>
  );
}

Props

DocTypography

PropTypeDefault
highlightsstring[]—
onChange(value: string, caret: number) => void—
typeDocTypographyType"text"
valuerequiredstring—

Takes these and every prop of DocTypographyProps.

DocDivider

Takes every prop of React.ComponentProps<"hr">.

Dependencies

npm
none besides React
Installs with it
Textarea

Source

components/ui/doc-typography.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";

import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";

export type DocTypographyType = "text" | "h1" | "h2" | "h3" | "quote";

export const DOC_TYPOGRAPHY_STYLE: Record<DocTypographyType, string> = {
  text: "text-sm leading-[1.6]",
  h1: "text-2xl font-semibold leading-[1.3]",
  h2: "text-xl font-semibold leading-[1.3]",
  h3: "text-lg font-semibold leading-[1.3]",
  quote:
    "border-l-2 border-border-default pl-3 text-sm leading-[1.6] text-text-muted",
};

const LABEL: Record<DocTypographyType, string> = {
  text: "Paragraph",
  h1: "Heading 1",
  h2: "Heading 2",
  h3: "Heading 3",
  quote: "Quote",
};

/** A borderless text line that grows with its content; the block text every doc-* component builds on. */
export interface DocTypographyProps
  extends Omit<React.ComponentProps<"textarea">, "value" | "onChange"> {
  type?: DocTypographyType;
  value: string;
  /** Called with the new text and where the caret is in it. */
  onChange?: (value: string, caret: number) => void;
  /** Exact pieces of `value` to tint, as mentions are: the text is unchanged, a coloured chip shows behind it. */
  highlights?: string[];
}

const escape = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

function DocTypography({
  type = "text",
  value,
  onChange,
  highlights,
  className,
  ...props
}: DocTypographyProps) {
  const pieces = highlights?.length ? value.split(new RegExp(`(${highlights.map(escape).join("|")})`)) : undefined;
  const field = (
    <Textarea
      rows={1}
      variant="fill"
      size="sm"
      aria-label={LABEL[type]}
      value={value}
      onChange={(e) => onChange?.(e.target.value, e.target.selectionStart)}
      className={cn(
        pieces && "relative",
        "min-h-0 resize-none rounded-none border-0 bg-transparent p-0 text-text-strong hover:bg-transparent focus-visible:border-0 focus-visible:bg-transparent focus-visible:ring-0",
        DOC_TYPOGRAPHY_STYLE[type],
        className,
      )}
      {...props}
    />
  );
  // The chips live in a copy of the text behind the field: same type, same wrapping, invisible text, tinted marks.
  return pieces ? (
    <div className="relative">
      <div
        aria-hidden
        className={cn("pointer-events-none absolute inset-0 break-words whitespace-pre-wrap text-transparent", DOC_TYPOGRAPHY_STYLE[type], className)}
      >
        {pieces.map((piece, i) =>
          i % 2 ? (
            <mark key={i} className="rounded-sm bg-info text-transparent">
              {piece}
            </mark>
          ) : (
            piece
          ),
        )}
      </div>
      {field}
    </div>
  ) : (
    field
  );
}

function DocDivider({ className, ...props }: React.ComponentProps<"hr">) {
  return (
    <hr
      aria-label="Divider"
      className={cn("my-3 border-t border-border-default", className)}
      {...props}
    />
  );
}

export { DocTypography, DocDivider };