awa-ui 0.3.0

Web UI and JSON API for the Awa job queue
Documentation
"use client"

import { use } from "react"
import type {
  ButtonProps,
  DisclosureGroupProps,
  DisclosurePanelProps,
  DisclosureProps,
} from "react-aria-components"
import {
  Button,
  composeRenderProps,
  DisclosureStateContext,
  Heading,
  Disclosure as PrimitiveDisclosure,
  DisclosureGroup as PrimitiveDisclosureGroup,
  DisclosurePanel as PrimitiveDisclosurePanel,
} from "react-aria-components"
import { twJoin, twMerge } from "tailwind-merge"
import { cx } from "@/lib/primitive"

export function DisclosureGroup({ className, ...props }: DisclosureGroupProps) {
  return (
    <PrimitiveDisclosureGroup
      className={cx(
        [
          "[--disclosure-gutter-x:--spacing(4)]",
          "[--disclosure-radius:var(--radius-lg)]",
          "[--disclosure-collapsed-border:var(--color-border)]",
          "[--disclosure-expanded-border:var(--color-muted-fg)]/30",
          "[--disclosure-collapsed-bg:var(--color-bg)]",
          "[--disclosure-collapsed-fg:var(--color-muted-fg)]",
          "[--disclosure-expanded-bg:var(--color-secondary)]/20",
          "[--disclosure-expanded-fg:var(--color-fg)]",
          "flex flex-col gap-y-2",
        ],
        className,
      )}
      {...props}
    />
  )
}

export function Disclosure({ className, ...props }: DisclosureProps) {
  return (
    <PrimitiveDisclosure
      className={composeRenderProps(className, (className, { isExpanded, isFocusVisibleWithin }) =>
        twMerge(
          "group/disclosure-item inset-ring inset-ring-(--disclosure-collapsed-border,transparent) w-full rounded-(--disclosure-radius,--spacing(0)) bg-(--disclosure-collapsed-bg,transparent) duration-200",
          (isExpanded || isFocusVisibleWithin) &&
            "inset-ring-(--disclosure-expanded-border,transparent) bg-(--disclosure-expanded-bg)",
          "has-data-hovered:inset-ring-(--disclosure-expanded-border,transparent) has-data-hovered:bg-(--disclosure-expanded-bg)",
          className,
        ),
      )}
      {...props}
    />
  )
}

export interface DisclosureTriggerProps extends ButtonProps {
  ref?: React.Ref<HTMLButtonElement>
  triggerIndicator?: boolean
}

export function DisclosureTrigger({
  ref,
  className,
  triggerIndicator = true,
  ...props
}: DisclosureTriggerProps) {
  const state = use(DisclosureStateContext)!
  return (
    <Heading>
      <Button
        {...props}
        ref={ref}
        slot="trigger"
        className={cx(
          [
            "relative isolate flex w-full cursor-default items-center justify-between px-(--disclosure-gutter-x,--spacing(0)) py-[calc(var(--disclosure-gutter-x,--spacing(0))-(--spacing(1)))] text-start font-medium text-sm/6 outline-hidden",
            "**:data-[slot=icon]:shrink-0 [&_[data-slot='icon']:not([class*='size-'])]:size-5 sm:[&_[data-slot='icon']:not([class*='size-'])]:size-4",
            "disabled:opacity-50",
            state.isExpanded
              ? "rounded-t-(--disclosure-radius) rounded-b-none text-(--disclosure-expanded-fg)"
              : "rounded-(--disclosure-radius) text-(--disclosure-collapsed-fg) hover:text-(--disclosure-expanded-fg)",
          ],
          className,
        )}
      >
        {(values) => (
          <>
            {typeof props.children === "function" ? props.children(values) : props.children}
            {triggerIndicator && <DisclosureIndicator />}
          </>
        )}
      </Button>
    </Heading>
  )
}

export function DisclosureIndicator({ className, ...props }: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="disclosure-indicator"
      className={twMerge(
        "pointer-events-none relative ms-(--disclosure-gutter-x,--spacing(0)) -me-[calc(var(--disclosure-gutter-x,--spacing(0))-(--spacing(2)))] flex size-6 shrink-0 items-center justify-center [--width:--spacing(2.5)]",
        className,
      )}
      {...props}
    >
      <span
        className={twJoin([
          "absolute h-[1.5px] w-(--width) origin-center bg-current transition-transform duration-300",
          "rotate-90 group-expanded/disclosure-item:rotate-0 group-expanded:rotate-0",
        ])}
      />
      <span className="absolute h-[1.5px] w-(--width) origin-center bg-current transition-transform duration-300" />
    </span>
  )
}

export function DisclosurePanel({ className, ...props }: DisclosurePanelProps) {
  return (
    <PrimitiveDisclosurePanel
      data-slot="disclosure-panel"
      className={cx(
        "h-(--disclosure-panel-height) overflow-clip text-sm/6 transition-[height] duration-200",
        className,
      )}
    >
      <div
        data-slot="disclosure-panel-content"
        className="justify-start self-stretch text-pretty px-(--disclosure-gutter-x,--spacing(0)) pt-2 pb-(--disclosure-gutter-x,--spacing(0)) text-(--disclosure-collapsed-fg)"
      >
        {props.children}
      </div>
    </PrimitiveDisclosurePanel>
  )
}