Tawny

Dashboard

Status select

The Board status menu. A 32px trigger, grey status icons, and the open row in tawny.

components/status-select

NOR-142

Empty state for archived projects

Status

Source

Copy the files below into your project.

apps/web/components/landing/showcases/status-select-demo.tsx
'use client'

import { useState } from 'react'
import { ScaledDesignCanvas } from '@/components/landing/scaled-design-canvas'

type Status = 'todo' | 'progress' | 'done'

const STATUSES: { id: Status; label: string }[] = [
  { id: 'todo', label: 'Todo' },
  { id: 'progress', label: 'In progress' },
  { id: 'done', label: 'Done' },
]

/**
 * The status menu from the Board issue detail: a 32px trigger,
 * grey status icons, and the open row in tawny.
 */
export function StatusSelect({
  initial = 'progress',
  defaultOpen = false,
}: {
  initial?: Status
  defaultOpen?: boolean
}) {
  const [status, setStatus] = useState<Status>(initial)
  const [open, setOpen] = useState(defaultOpen)
  const current = STATUSES.find((entry) => entry.id === status) ?? STATUSES[0]

  return (
    <div className="w-72 tracking-[-0.01em]">
      <p className="text-[11px] text-muted-foreground">NOR-142</p>
      <h2 className="mt-1 text-[15px] font-semibold leading-5 tracking-tight text-foreground">
        Empty state for archived projects
      </h2>
      <div className="relative mt-4">
        <p className="text-[11px] text-muted-foreground">Status</p>
        <button
          type="button"
          aria-expanded={open}
          aria-haspopup="listbox"
          onClick={() => setOpen((value) => !value)}
          className="mt-1 flex h-8 w-full items-center gap-2 rounded-md border border-border bg-background px-2 text-left text-[13px] text-foreground transition-colors duration-[80ms] ease-out hover:bg-accent"
        >
          <StatusIcon status={current.id} />
          <span className="min-w-0 flex-1 truncate">{current.label}</span>
        </button>
        {open ? (
          <ul
            role="listbox"
            aria-label="Status"
            className="absolute top-full z-10 mt-1 w-full overflow-hidden rounded-md border border-border bg-popover py-1"
          >
            {STATUSES.map((entry) => {
              const active = entry.id === status
              return (
                <li key={entry.id}>
                  <button
                    type="button"
                    role="option"
                    aria-selected={active}
                    onClick={() => {
                      setStatus(entry.id)
                      setOpen(false)
                    }}
                    className={
                      active
                        ? 'flex w-full items-center gap-2 bg-tawny/15 px-2 py-1.5 text-left text-[13px] text-foreground'
                        : 'flex w-full items-center gap-2 px-2 py-1.5 text-left text-[13px] text-foreground transition-colors duration-[80ms] ease-out hover:bg-accent'
                    }
                  >
                    <StatusIcon status={entry.id} />
                    {entry.label}
                  </button>
                </li>
              )
            })}
          </ul>
        ) : null}
      </div>
    </div>
  )
}

function StatusIcon({ status }: { status: Status }) {
  const color = 'var(--muted-foreground)'
  if (status === 'done') {
    return (
      <svg width={16} height={16} viewBox="0 0 16 16" aria-hidden="true">
        <circle cx="8" cy="8" r="6" fill="none" stroke={color} strokeWidth="1.5" />
        <path d="M5 8.2 7.1 10.2 11 6.1" fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
      </svg>
    )
  }
  if (status === 'progress') {
    return (
      <svg width={16} height={16} viewBox="0 0 16 16" aria-hidden="true">
        <circle cx="8" cy="8" r="6" fill="none" stroke={color} strokeWidth="1.5" />
        <path d="M8 2.2a5.8 5.8 0 0 1 0 11.6Z" fill={color} />
      </svg>
    )
  }
  return (
    <svg width={16} height={16} viewBox="0 0 16 16" aria-hidden="true">
      <circle cx="8" cy="8" r="6" fill="none" stroke={color} strokeWidth="1.5" />
    </svg>
  )
}

/**
 * Open menu, centered on both axes. The dropdown is out of flow, so the
 * padding below the card is the menu's height and keeps the pair centered.
 */
function SelectStage() {
  return (
    <div className="flex h-full items-center justify-center bg-background">
      <div className="pb-[5.5rem]">
        <div className="rounded-xl border border-border bg-card p-5">
          <StatusSelect defaultOpen />
        </div>
      </div>
    </div>
  )
}

/** Gallery card: tighter canvas so the menu fills the 16:9 crop, like Motion Primitives. */
export function StatusSelectDemo() {
  return (
    <div className="h-full">
      <ScaledDesignCanvas width={560} height={315}>
        <SelectStage />
      </ScaledDesignCanvas>
    </div>
  )
}

/** Detail page: the same control, larger, centered, and clickable. */
export function StatusSelectDetail() {
  return (
    <div className="flex h-full items-center justify-center bg-background">
      <div className="origin-center scale-150">
        <div className="pb-[5.5rem]">
          <div className="rounded-xl border border-border bg-card p-5">
            <StatusSelect defaultOpen />
          </div>
        </div>
      </div>
    </div>
  )
}