Neoconstruct

Neoconstruct Marker

Neoconstruct's ruler-and-compass marks: wedge, slash, disc, ring, plus, square, bar and star, plus a zero-padded numeral. Ships in the Neoconstruct theme.

Free, MIT licenseOpen in Browse
import * as React from "react"
import { cn } from "cn"

/**
 * Constructivist marks, drawn with ruler and compass. Each one comes from the
 * period's own vocabulary: Lissitzky's wedge into a circle, the diagonal band,
 * Suprematist disc and square, the arrow, the compass-drawn target, the
 * printer's registration mark and the heavy typographic rule.
 * Numerals live in `MarkerNumeral`.
 */
const shapes = {
  // A wedge driving into a ring.
  wedge: (
    <>
      <circle
        cx="15"
        cy="12"
        r="6.5"
        fill="none"
        stroke="currentColor"
        strokeWidth="2.5"
      />
      <polygon points="1,6.5 16,12 1,17.5" />
    </>
  ),
  // A thick band at 30 degrees.
  band: <polygon points="2,15.5 19,5.5 22,10 5,20" />,
  disc: <circle cx="12" cy="12" r="8" />,
  // Two compass-drawn rings around a dot.
  target: (
    <>
      <circle
        cx="12"
        cy="12"
        r="9"
        fill="none"
        stroke="currentColor"
        strokeWidth="2.5"
      />
      <circle cx="12" cy="12" r="3.5" />
    </>
  ),
  // The printer's registration mark: a cross through a ring.
  registration: (
    <>
      <circle
        cx="12"
        cy="12"
        r="6.5"
        fill="none"
        stroke="currentColor"
        strokeWidth="2.5"
      />
      <path d="M11 1h2v22h-2zM1 11h22v2H1z" />
    </>
  ),
  arrow: <path d="M2 10h12V5l8 7-8 7v-5H2z" />,
  square: <rect x="5" y="5" width="14" height="14" />,
  bar: <rect x="2" y="9" width="20" height="6" />,
} as const

type MarkerVariant = keyof typeof shapes

/** A marker choice for components: any shape, or "none" to render nothing. */
type MarkerProp = MarkerVariant | "none"

/**
 * A small Constructivist mark for buttons, badges and list items: a wedge,
 * band, disc, target, registration mark, arrow, square or bar. It draws in
 * the text color at `size-4` and is hidden from assistive tech.
 */
function Marker({
  variant = "wedge",
  className,
  ...props
}: React.ComponentProps<"svg"> & {
  /** Which mark to draw; "none" renders nothing. Defaults to "wedge". */
  variant?: MarkerProp
}) {
  if (variant === "none") return null

  return (
    <svg
      data-slot="marker"
      data-theme="neoconstruct"
      data-variant={variant}
      viewBox="0 0 24 24"
      fill="currentColor"
      aria-hidden="true"
      className={cn("size-4 shrink-0", className)}
      {...props}
    >
      {shapes[variant]}
    </svg>
  )
}

/**
 * A zero-padded step or issue number in the heading font: 1 shows as "01".
 * It has no size of its own, so set one with `className`. It is hidden from
 * assistive tech, so pair it with text that names the step.
 */
function MarkerNumeral({
  n,
  className,
  ...props
}: React.ComponentProps<"span"> & {
  /** The number to show, padded to two digits. */
  n: number
}) {
  return (
    <span
      data-slot="marker-numeral"
      data-theme="neoconstruct"
      aria-hidden="true"
      className={cn("font-heading leading-none tabular-nums", className)}
      {...props}
    >
      {String(n).padStart(2, "0")}
    </span>
  )
}

export { Marker, MarkerNumeral }
export type { MarkerVariant, MarkerProp }

Installation

pnpm dlx shadcn add @01orbital/neoconstruct-marker
npx shadcn add @01orbital/neoconstruct-marker
yarn shadcn add @01orbital/neoconstruct-marker
bunx --bun shadcn add @01orbital/neoconstruct-marker

Free under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.

It lands at components/neoconstruct/neoconstruct-marker.tsx, as source you own and edit.

Usage

import {
  Marker,
  MarkerNumeral,
  type MarkerVariant,
} from "@/components/neoconstruct/neoconstruct-marker"

const variants = [
  "wedge",
  "band",
  "disc",
  "target",
  "registration",
  "arrow",
  "square",
  "bar",
] as const satisfies readonly MarkerVariant[]

export default function MarkerDemo() {
  return (
    <div className="flex flex-col gap-8">
      <div className="flex flex-wrap justify-center gap-6">
        {variants.map((variant) => (
          <div key={variant} className="flex w-16 flex-col items-center gap-2">
            <Marker variant={variant} className="size-8 text-main-text" />
            <span className="font-heading text-xs tracking-wider uppercase">
              {variant}
            </span>
          </div>
        ))}
      </div>
      <div className="flex items-end gap-6">
        {[1, 2, 3].map((n) => (
          <div key={n} className="flex items-end gap-2">
            <MarkerNumeral n={n} className="text-5xl text-main-text" />
            <span className="font-heading text-sm tracking-wider uppercase">
              Step
            </span>
          </div>
        ))}
      </div>
    </div>
  )
}

Anatomy

Neoconstruct Marker is built from 2 parts. In the example they nest like this:

Marker
MarkerNumeral
  • Marker: A small Constructivist mark for buttons, badges and list items: a wedge, band, disc, target, registration mark, arrow, square or bar. It draws in the text color at size-4 and is hidden from assistive tech.
  • MarkerNumeral: A zero-padded step or issue number in the heading font: 1 shows as "01". It has no size of its own, so set one with className. It is hidden from assistive tech, so pair it with text that names the step.

API reference

Marker

A small Constructivist mark for buttons, badges and list items: a wedge, band, disc, target, registration mark, arrow, square or bar. It draws in the text color at size-4 and is hidden from assistive tech.

Prop

Type

It also accepts the HTML attributes of the element it renders.

MarkerNumeral

A zero-padded step or issue number in the heading font: 1 shows as "01". It has no size of its own, so set one with className. It is hidden from assistive tech, so pair it with text that names the step.

Prop

Type

It also accepts the HTML attributes of the element it renders.

Dependencies

npm: cn. The CLI installs them for you.

Registry pieces it installs with it:

  • font-unbounded