# @hiyve/utilities

Reusable utility components, hooks, and functions for Hiyve video conferencing applications.

## Installation

```bash
npm install @hiyve/utilities
```

## Quick Start

```tsx
import {
  LiveClock,
  TooltipIconButton,
  ErrorBoundary,
  useContainerBreakpoint,
} from '@hiyve/utilities';

function Header() {
  const { isBelowBreakpoint, containerRef } = useContainerBreakpoint(800);

  return (
    <ErrorBoundary fallback={<div>Something went wrong</div>}>
      <header ref={containerRef}>
        {!isBelowBreakpoint && <LiveClock />}
        <TooltipIconButton tooltip="Copy" onClick={handleCopy}>
          <CopyIcon />
        </TooltipIconButton>
      </header>
    </ErrorBoundary>
  );
}
```

---

## Components

### LiveClock

Self-contained live time display that updates every second.

```tsx
import { LiveClock } from '@hiyve/utilities';

<LiveClock variant="h6" />

<LiveClock
  variant="body1"
  updateInterval={5000}
  labels={{ formatTime: (date) => date.format('HH:mm:ss') }}
  colors={{ text: '#ffffff' }}
/>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `variant` | `'h1' \| 'h2' \| ... \| 'caption'` | `'h5'` | Typography variant |
| `updateInterval` | `number` | `1000` | Update interval in milliseconds |
| `labels` | `Partial<LiveClockLabels>` | -- | Custom time formatting |
| `colors` | `Partial<LiveClockColors>` | -- | Custom colors |
| `styles` | `Partial<LiveClockStyles>` | -- | Custom styles (padding, textAlign) |
| `sx` | `SxProps<Theme>` | -- | MUI sx styling |

### TooltipIconButton

IconButton wrapper that correctly displays tooltips on disabled buttons.

```tsx
import { TooltipIconButton } from '@hiyve/utilities';
import SaveIcon from '@mui/icons-material/Save';

<TooltipIconButton
  tooltip="Save changes"
  disabled={!hasChanges}
  onClick={handleSave}
>
  <SaveIcon />
</TooltipIconButton>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `tooltip` | `ReactNode` | -- | Tooltip content (required) |
| `placement` | `TooltipProps['placement']` | `'top'` | Tooltip placement |
| `disabled` | `boolean` | `false` | Disabled state (tooltip still works) |
| `styles` | `Partial<TooltipIconButtonStyles>` | -- | Custom styles |
| `tooltipProps` | `Omit<TooltipProps, 'title' \| 'children' \| 'placement'>` | -- | Additional Tooltip props |
| `children` | `ReactNode` | -- | Icon element |

Extends all standard MUI `IconButtonProps` (except `title`).

### ErrorBoundary

React error boundary that catches render errors in its child tree.

```tsx
import { ErrorBoundary } from '@hiyve/utilities';

// Static fallback
<ErrorBoundary fallback={<div>Something went wrong</div>}>
  <MyComponent />
</ErrorBoundary>

// Render function fallback with error details and reset
<ErrorBoundary
  fallback={(error, reset) => (
    <div>
      <p>Error: {error.message}</p>
      <button onClick={reset}>Try Again</button>
    </div>
  )}
  onError={(error, errorInfo) => logToService(error)}
>
  <VideoGrid />
</ErrorBoundary>
```

| Prop | Type | Description |
|------|------|-------------|
| `children` | `ReactNode` | Content to render when no error has occurred |
| `fallback` | `ReactNode \| (error: Error, reset: () => void) => ReactNode` | Content to render when an error is caught |
| `onError` | `(error: Error, errorInfo: ErrorInfo) => void` | Callback invoked when an error is caught |

When `fallback` is a function, it receives the caught `Error` and a `reset` function that clears the error state and re-renders the children.

---

## Hooks

### useContainerBreakpoint

Responsive container detection using ResizeObserver. Returns whether the container is below a given width threshold.

```tsx
import { useContainerBreakpoint } from '@hiyve/utilities';

function ResponsivePanel() {
  const { isBelowBreakpoint, containerRef } = useContainerBreakpoint(600);

  return (
    <div ref={containerRef}>
      {isBelowBreakpoint ? <CompactView /> : <FullView />}
    </div>
  );
}
```

**Signature:** `useContainerBreakpoint(breakpoint: number, options?: UseContainerBreakpointOptions)`

**Returns:** `UseContainerBreakpointResult`

| Return Field | Type | Description |
|--------------|------|-------------|
| `isBelowBreakpoint` | `boolean` | Whether the container width is below the breakpoint |
| `containerRef` | `RefObject<HTMLElement>` | Ref to attach to the container element |

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `initialValue` | `boolean` | `false` | Initial value before the first measurement |

### useDebounce

Delays updating a value until a specified time has passed since the last change. Useful for search inputs and API call throttling.

```tsx
import { useDebounce } from '@hiyve/utilities';

function SearchComponent() {
  const [searchInput, setSearchInput] = useState('');
  const debouncedSearch = useDebounce(searchInput, 300);

  useEffect(() => {
    performSearch(debouncedSearch);
  }, [debouncedSearch]);

  return (
    <input
      value={searchInput}
      onChange={(e) => setSearchInput(e.target.value)}
    />
  );
}
```

**Signature:** `useDebounce<T>(value: T, delay: number): T`

| Parameter | Type | Description |
|-----------|------|-------------|
| `value` | `T` | The value to debounce |
| `delay` | `number` | Debounce delay in milliseconds |

Returns the debounced value.

### useValidation

Form validation hook with built-in validator factories.

```tsx
import { useValidation, validators } from '@hiyve/utilities';

function PollQuestionInput() {
  const [question, setQuestion] = useState('');
  const { error, validate, isValid, clearError } = useValidation(question, [
    validators.required('Question is required'),
    validators.maxLength(500, 'Question must be 500 characters or less'),
  ]);

  const handleSubmit = () => {
    if (validate()) {
      submitPoll(question);
    }
  };

  return (
    <TextField
      value={question}
      onChange={(e) => setQuestion(e.target.value)}
      error={!!error}
      helperText={error}
    />
  );
}
```

**Signature:** `useValidation<T>(value: T, validators: Validator<T>[]): UseValidationResult`

| Return Field | Type | Description |
|--------------|------|-------------|
| `error` | `string \| null` | Current error message, or null if valid |
| `validate` | `() => boolean` | Run validators and update error state; returns true if valid |
| `isValid` | `boolean` | Whether the value is currently valid |
| `clearError` | `() => void` | Clear the current error |
| `setError` | `(message: string \| null) => void` | Set a custom error message |

#### Built-in Validators

The `validators` object provides factory functions that return `Validator` functions:

| Validator | Signature | Description |
|-----------|-----------|-------------|
| `validators.required` | `(message?: string)` | Value must be a non-empty string |
| `validators.minLength` | `(min: number, message?: string)` | Minimum string length |
| `validators.maxLength` | `(max: number, message?: string)` | Maximum string length |
| `validators.pattern` | `(regex: RegExp, message: string)` | Value must match a regex pattern |
| `validators.noSpecialChars` | `(message?: string)` | Only letters, numbers, spaces, hyphens, underscores |
| `validators.range` | `(min: number, max: number, message?: string)` | Numeric value within a range |
| `validators.email` | `(message?: string)` | Valid email address format |
| `validators.url` | `(message?: string)` | Valid URL format |
| `validators.custom` | `<T>(fn: (value: T) => boolean, message: string)` | Custom validation logic |

### usePersistedState

Persists state to localStorage with the same API as `useState`. Values survive page reloads and are automatically synchronized across browser tabs.

```tsx
import { usePersistedState } from '@hiyve/utilities';

function JoinForm() {
  const [userName, setUserName] = usePersistedState('hiyve-userName', '');
  const [roomName, setRoomName] = usePersistedState('hiyve-roomName', '');

  return (
    <>
      <input value={userName} onChange={(e) => setUserName(e.target.value)} />
      <input value={roomName} onChange={(e) => setRoomName(e.target.value)} />
    </>
  );
}
```

**Signature:** `usePersistedState<T>(key: string, defaultValue: T): [T, (value: T | ((prev: T) => T)) => void]`

| Parameter | Type | Description |
|-----------|------|-------------|
| `key` | `string` | The localStorage key to persist under |
| `defaultValue` | `T` | The initial value when no stored value exists |

Returns a `[value, setValue]` tuple identical to `useState`. The setter supports both direct values and functional updates. Works in SSR environments by falling back to the default value when `window` is unavailable. Corrupted or unparseable stored values fall back to the default gracefully.

---

## Utilities

### throttle

Creates a throttled version of a function that executes at most once every `wait` milliseconds. The first call executes immediately; subsequent calls within the wait period are queued.

```tsx
import { throttle } from '@hiyve/utilities';

const throttledUpdate = throttle((data) => {
  broadcastUpdate(data);
}, 50);

canvas.on('object:modified', () => throttledUpdate(canvas.toJSON()));

// Clean up
throttledUpdate.cancel();
```

**Signature:** `throttle<T>(fn: T, wait: number): T & { cancel: () => void }`

Returns a throttled function with a `cancel()` method to clear any pending execution.

### Avatar Utilities

Functions for generating consistent user display elements (initials and colors).

```ts
import { getInitials, getUserColor, DEFAULT_AVATAR_PALETTE } from '@hiyve/utilities';

getInitials('John Doe');     // 'JD'
getInitials('jane_smith');   // 'JS'
getInitials('admin');        // 'AD'
getInitials('');             // '?'

getUserColor('user-123');                          // consistent color from default palette
getUserColor('user-123', ['#f00', '#0f0', '#00f']); // consistent color from custom palette
```

| Export | Signature | Description |
|--------|-----------|-------------|
| `getInitials` | `(name: string) => string` | Extracts 1-2 uppercase initials from a name. Splits on spaces, hyphens, and underscores. Returns `'?'` for empty strings. |
| `getUserColor` | `(userId: string \| undefined, palette?: string[]) => string` | Returns a deterministic color for a given user ID. The same ID always produces the same color. |
| `DEFAULT_AVATAR_PALETTE` | `string[]` | Array of 12 distinct colors designed for participant differentiation. |

### Retry Utilities

Async retry with exponential backoff, jitter, and customizable retry conditions.

```ts
import { withRetry, isNetworkErrorRetryable, sleep } from '@hiyve/utilities';

// Basic retry
const data = await withRetry(() => fetchData('/api/data'), {
  maxRetries: 3,
});

// Custom retry logic
const result = await withRetry(() => uploadFile(file), {
  maxRetries: 5,
  initialDelay: 500,
  isRetryable: isNetworkErrorRetryable,
  onRetry: (attempt, error, delay) => {
    console.log(`Retry ${attempt} after ${delay}ms`);
  },
});

// Standalone delay
await sleep(2000);
```

| Export | Signature | Description |
|--------|-----------|-------------|
| `withRetry` | `<T>(fn: () => Promise<T>, options?: RetryOptions) => Promise<T>` | Execute an async function with exponential backoff retry. Throws the last error if all retries are exhausted. |
| `calculateBackoffDelay` | `(attempt: number, options?) => number` | Calculate the delay in milliseconds for a given attempt (0-indexed). |
| `sleep` | `(ms: number) => Promise<void>` | Promise-based delay. |
| `isNetworkErrorRetryable` | `(error: unknown) => boolean` | Returns true for transient network errors (fetch failures, HTTP 5xx, 429, 408). |
| `DEFAULT_RETRY_OPTIONS` | `object` | Default configuration: `{ maxRetries: 3, initialDelay: 1000, maxDelay: 10000, backoffMultiplier: 2, jitter: true }` |

**RetryOptions:**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `maxRetries` | `number` | `3` | Maximum number of retry attempts |
| `initialDelay` | `number` | `1000` | Initial delay in milliseconds |
| `maxDelay` | `number` | `10000` | Maximum delay in milliseconds |
| `backoffMultiplier` | `number` | `2` | Multiplier for exponential backoff |
| `jitter` | `boolean` | `true` | Add randomized jitter to prevent thundering herd |
| `isRetryable` | `(error: unknown) => boolean` | all errors | Determine if an error should be retried |
| `onRetry` | `(attempt, error, nextDelay) => void` | -- | Callback invoked before each retry |

### Debug Utilities

Namespaced debug logging with level filtering and global configuration.

```ts
import { createDebugLogger, configureDebug, isDebugEnabled } from '@hiyve/utilities';

// Enable debug logging at startup
configureDebug({ enabled: true });

// Create a scoped logger
const debug = createDebugLogger('hiyve:my-component');

debug.log('Component mounted');
debug.warn('Deprecated prop used');
debug.error('Failed to load data', error);

// Check enabled state
if (debug.enabled) {
  debug.log('Detailed info:', expensiveComputation());
}
```

| Export | Signature | Description |
|--------|-----------|-------------|
| `createDebugLogger` | `(namespace: string) => DebugLogger` | Create a namespaced logger. Messages are prefixed with `[namespace]`. |
| `configureDebug` | `(config: DebugConfig) => void` | Configure global debug settings (enabled, filter, minLevel). |
| `isDebugEnabled` | `() => boolean` | Check if debug logging is globally enabled. |
| `noopLogger` | `DebugLogger` | Logger instance that does nothing. Useful as a default or fallback. |
| `hiyveDebug` | `DebugLogger` | Pre-configured logger with the `'hiyve'` namespace. |

**DebugLogger interface:**

| Method/Property | Type | Description |
|-----------------|------|-------------|
| `log` | `(...args: unknown[]) => void` | Log a standard message |
| `warn` | `(...args: unknown[]) => void` | Log a warning |
| `error` | `(...args: unknown[]) => void` | Log an error |
| `debug` | `(...args: unknown[]) => void` | Log a debug-level message |
| `enabled` | `boolean` (readonly) | Whether this logger is currently active |

**DebugConfig options:**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `enabled` | `boolean` | `false` in production | Enable or disable debug logging globally |
| `filter` | `string \| RegExp` | -- | Namespace filter (e.g. `'hiyve:*'` or `/^hiyve:(client|cloud)/`) |
| `minLevel` | `DebugLevel` | `'log'` | Minimum log level: `'debug'`, `'log'`, `'warn'`, or `'error'` |

### Error Formatting

Maps raw error messages to user-friendly strings using case-insensitive substring matching. Useful in `onError` callbacks to display clear, actionable messages to end users.

```tsx
import { formatHiyveError } from '@hiyve/utilities';

<HiyveProvider
  onError={(err) => setError(formatHiyveError(err.message || String(err)))}
>
```

With custom messages that extend the defaults:

```ts
import { formatHiyveError, DEFAULT_HIYVE_ERROR_MESSAGES } from '@hiyve/utilities';

const friendly = formatHiyveError(error, {
  'rate limit': 'Too many requests. Please wait a moment and try again.',
});
```

| Export | Signature | Description |
|--------|-----------|-------------|
| `formatHiyveError` | `(error: string \| Error, customMessages?: Record<string, string>) => string` | Returns a user-friendly message if the error matches a known pattern, or the original message if no match is found. Accepts a raw string or an `Error` object. Optional `customMessages` extend and override the defaults. |
| `DEFAULT_HIYVE_ERROR_MESSAGES` | `Record<string, string>` | Built-in mapping of error substrings to friendly messages. Covers common room-not-found, expired token/session, and incorrect password scenarios. Spread into a new object to extend with your own mappings. |

**Default mappings included:**

| Error substring | Friendly message |
|-----------------|------------------|
| `'does not exist'`, `'not found'`, `'no room'` | Unable to join room. The room name may be incorrect or the host hasn't started the meeting yet. |
| `'token'` | Your invite link has expired or is invalid. Please request a new one. |
| `'expired'` | Your session has expired. Please rejoin the room. |
| `'password'` | Incorrect password. Please try again. |

---

## Video Types & Layouts

Framework-agnostic types and layout algorithms shared between `@hiyve/video-tile`, `@hiyve/video-grid`, and their Angular counterparts. These are the single source of truth — the framework-specific packages re-export them for backward compatibility.

```ts
import {
  type OverlayPosition,
  type TilePosition,
  type MuteStatus,
  type MoodType,
  type MoodData,
  type BuiltInLayoutMode,
  type LayoutMode,
  getGridColumns,
  getGridClass,
  getPositionStyles,
  calculateSpeakerLayout,
  calculateSidebarLayout,
} from '@hiyve/utilities';
```

### Angular / non-React consumers

Angular packages should import from the `@hiyve/utilities/video` subpath. This entry point is a standalone bundle (~2KB) with zero React or MUI dependencies:

```ts
import { type MuteStatus, getGridColumns } from '@hiyve/utilities/video';
```

### Shared Types

| Type | Description |
|------|-------------|
| `OverlayPosition` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` |
| `TilePosition` | `{ left: number; top: number; width: number; height: number }` |
| `MuteStatus` | `{ audio: boolean; video: boolean; output?: boolean }` |
| `MoodType` | `'neutral' \| 'happy' \| 'sad' \| 'angry' \| 'fearful' \| 'disgusted' \| 'surprised'` |
| `MoodData` | `{ mood: MoodType; confidence?: number; engagement?: number; timestamp?: number }` |
| `BuiltInLayoutMode` | `'grid' \| 'speaker' \| 'sidebar'` |
| `LayoutMode` | `string` (any layout name, including custom layouts) |

### Layout Functions

| Export | Signature | Description |
|--------|-----------|-------------|
| `getGridColumns` | `(count: number) => number` | Number of grid columns for a participant count |
| `getGridClass` | `(count: number) => string` | CSS class name for grid styling (e.g. `'grid-2x2'`) |
| `getPositionStyles` | `(position: OverlayPosition) => Record<string, string>` | CSS properties for positioning an overlay element |
| `calculateSpeakerLayout` | `(params) => Record<string, TilePosition>` | Compute tile positions for speaker layout (dominant + filmstrip) |
| `calculateSidebarLayout` | `(params) => Record<string, TilePosition>` | Compute tile positions for sidebar layout (dominant + side panel) |

---

## Types

All TypeScript interfaces and types are exported for use in custom implementations.

| Type | Description |
|------|-------------|
| `LiveClockProps` | Props for the LiveClock component |
| `LiveClockLabels` | Customizable time format labels |
| `LiveClockStyles` | Style configuration (padding, textAlign) |
| `LiveClockColors` | Color configuration (text) |
| `TooltipIconButtonProps` | Props for TooltipIconButton (extends IconButtonProps) |
| `TooltipIconButtonStyles` | Style configuration (wrapperDisplay) |
| `ErrorBoundaryProps` | Props for ErrorBoundary |
| `UseContainerBreakpointResult` | Return type of useContainerBreakpoint |
| `UseContainerBreakpointOptions` | Options for useContainerBreakpoint |
| `Validator<T>` | Validator function type: `(value: T) => string \| null` |
| `UseValidationResult` | Return type of useValidation |
| `UseThrottleOptions` | Options type with `leading` and `trailing` flags |
| `RetryOptions` | Options for withRetry |
| `DebugLevel` | `'log' \| 'warn' \| 'error' \| 'debug'` |
| `DebugLogger` | Debug logger interface |
| `DebugConfig` | Configuration for configureDebug |
| `OverlayPosition` | Video tile overlay corner position |
| `TilePosition` | Absolute position and size for a video tile |
| `MuteStatus` | Audio/video/output mute state |
| `MoodType` | 7-emotion sentiment category |
| `MoodData` | Mood analysis result with confidence and engagement |
| `BuiltInLayoutMode` | Built-in layout mode union |
| `LayoutMode` | Layout mode (any string, including custom) |

---

## Defaults

Default configuration objects and merge helpers for component customization.

| Export | Description |
|--------|-------------|
| `defaultLiveClockLabels` | Default time format: `'MMMM D, YYYY h:mm:ss A'` |
| `defaultLiveClockStyles` | Default styles: `{ padding: 2, textAlign: 'center' }` |
| `defaultLiveClockColors` | Default colors: `{ text: 'inherit' }` |
| `defaultTooltipIconButtonStyles` | Default styles: `{ wrapperDisplay: 'inline-flex' }` |
| `mergeLiveClockLabels` | `(partial?) => LiveClockLabels` -- merge partial labels with defaults |
| `mergeLiveClockStyles` | `(partial?) => LiveClockStyles` -- merge partial styles with defaults |
| `mergeLiveClockColors` | `(partial?) => LiveClockColors` -- merge partial colors with defaults |
| `mergeTooltipIconButtonStyles` | `(partial?) => TooltipIconButtonStyles` -- merge partial styles with defaults |

```tsx
import { defaultLiveClockLabels, LiveClock } from '@hiyve/utilities';

// Override only the time format, keeping other defaults
<LiveClock
  labels={{
    ...defaultLiveClockLabels,
    formatTime: (date) => date.format('HH:mm:ss'),
  }}
/>
```

---

## Features

- Zero/minimal external dependencies (MUI peer dependency for components)
- All utilities can be used independently
- Shared video types and layout algorithms for React and Angular packages
- Standalone `@hiyve/utilities/video` entry point with no React/MUI dependencies
- Built-in form validators for common patterns
- Retry with exponential backoff and jitter
- Scoped debug logging with level and namespace filtering
- Deterministic avatar colors for consistent user identification
- Error boundary with reset capability

## Requirements

**Peer dependencies** (for component exports):

- `react` ^18.0.0
- `@mui/material` ^5.0.0 || ^6.0.0
- `@mui/icons-material` ^5.0.0 || ^6.0.0
- `@emotion/react` ^11.0.0
- `@emotion/styled` ^11.0.0

Non-component exports (hooks, utilities, avatar, debug, retry, throttle) have no peer dependency requirements beyond React.

The `@hiyve/utilities/video` subpath (video types and layout algorithms) has **no peer dependencies at all** and can be used from Angular or any other framework.

## License

Proprietary - Hiyve SDK
