Glrk UI

UI Component Wrappers

Overview

These are opinionated wrappers built on top of Base UI and Shadcn primitives. Directly composing raw primitives throughout an application leads to verbose, repetitive code — these wrappers give you a clean, consistent API while keeping full access to the underlying Base UI or Shadcn UI components when you need advanced control.

Key Benefits

  • Reduced Boilerplate: Eliminate repetitive primitive composition patterns
  • Type Safety: Built-in TypeScript support for common value types
  • Consistent API: Standardized interface across similar components
  • Flexibility: Preserve access to underlying Base UI and shadcn primitives for advanced use cases

Prerequest

  • shadcn base setup
  • add following to your components.json at registries block
{
  "registries": {
    "@glrk-ui": "https://ui.glrk.dev/registry/{name}.json"
  }
}

Type Definitions

Core Types

The wrapper system supports three primitive value types that cover the majority of form and selection use cases:

types/general.d.ts
type allowedPrimitiveT = string | number | boolean

type itemT = {
  label: React.ReactNode
  value: allowedPrimitiveT
  className?: string
  disabled?: boolean
}

type groupT = {
  group: string
  items: (allowedPrimitiveT | itemT)[]
  className?: string
}

type itemsT = (allowedPrimitiveT | itemT | groupT)[]

type indicatorAtT = 'right' | 'left' | ''

Design Rationale

Primitive Type Restriction: The system intentionally limits allowed values to string, number, and boolean. These types handle the vast majority of form scenarios.

Empty Values: Use empty strings ("") for empty values, or omit the property entirely. Extend the type system as needed for your specific requirements.


Utility Functions

lib/utils.ts
import { isValidElement, type ReactNode } from 'react'
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

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

export function isAllowedPrimitive(value: unknown): value is allowedPrimitiveT {
  return ['string', 'number', 'boolean'].includes(typeof value)
}

export function parseAllowedPrimitive(value: allowedPrimitiveT): allowedPrimitiveT {
  if (typeof value !== 'string') return value

  const trimmed = value.trim()

  if (trimmed === 'true') return true
  if (trimmed === 'false') return false
  if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed)

  return trimmed
}

export function itemTypeChecker<T>(key: keyof T) {
  return (option: any): option is T => !!option && typeof option === 'object' && key in option
}

export const isSeparator = (item: any) => item === '---'
export const isOption = itemTypeChecker<itemT>('value')
export const isGroup = itemTypeChecker<groupT>('group')

export const getValue = (item: allowedPrimitiveT | itemT) =>
  typeof item === 'object' ? item.value : item
export const getLabel = (item: allowedPrimitiveT | itemT) =>
  typeof item === 'object' ? item.label : `${item}`

export function getKey(item: allowedPrimitiveT | itemT, i: number): string {
  const val = getValue(item)
  if (typeof val === 'boolean') return `key-${val}`
  if (val === '---') return `---${i}`
  return `${val}`
}

export function extractText(node: ReactNode): string {
  if (node == null || typeof node === 'boolean') return ''
  if (typeof node === 'string' || typeof node === 'number') return String(node)
  if (isValidElement(node)) return extractText((node.props as { children?: ReactNode }).children)
  if (Array.isArray(node)) return node.map(extractText).join('')
  return ''
}

Items Configuration

Flexible Item Formats

The wrapper system supports multiple item formats to accommodate different use cases:

const items: itemsT = [
  // Simple primitive values (label equals value)
  "Data 1",
  false,
  12,
  
  // Visual separator
  "---",
  
  // Object format with custom label
  {
    label: "Obj 1",
    value: "obj-1", 
  },
  
  // Object with custom styling
  {
    label: "Obj 2",
    value: "obj-2",
    className: "bg-red-50",
  },
  
  // Rich content with React nodes
  {
    value: "apple",
    label: <><Apple className="mr-2" /> Apple</>
  },
  
  "---",
  
  // Grouped items
  {
    group: "Group 1",
    items: [
      "grp 1",
      21,
      true,
      { value: "banana", label: <><Banana className="mr-2" /> Banana</> }
    ],
  },
  
  // Grouped items with styling
  {
    group: "Group 2",
    items: ["grp 2", 22],
    className: "bg-amber-50",
  },
]

Item Format Guidelines

  • Primitive values: Use directly when the display label matches the value
  • Separators: Use "---" string for visual separation between items
  • Object format: Use when you need custom labels, styling, or rich content
  • Grouped items: Organize related items under labeled groups
  • Custom styling: Apply Tailwind classes via className for visual distinction

Value Conversion Pattern

Understanding String Conversion

Controlled components use string values internally. The wrapper system handles conversion between strings and primitive types transparently. Some components directly support type conversion. If a component doesn't support this feature, use the following guide.

Implementation Patterns

// ❌ Incorrect: Type mismatch
const [value, setValue] = useState(true)

<SelectWrapper
  value={value}
  onValueChange={setValue}
/>

// ✅ Correct: Convert to string
const [value, setValue] = useState("true")

<SelectWrapper
  value={value}
  onValueChange={setValue}
/>

// ✅ Recommended: Preserve primitive types
const [value, setValue] = useState<allowedPrimitiveT>(true)

<SelectWrapper
  value={`${value}`}
  onValueChange={v => setValue(parseAllowedPrimitive(v))}
/>

Best Practices

  1. String State: Simplest approach for string-only values
  2. Primitive State with Conversion: Use parseAllowedPrimitive to maintain type fidelity
  3. Template Literals: Use `${value}` for type-safe string conversion
  4. Type Annotations: Explicitly type state as allowedPrimitiveT when using conversion

Next Steps

With this foundation in place, you can use the wrapper components for common UI elements. Checkout the wrappers.