# @slm-solusi-digital/telescope-ui

A React component library built on top of [HeroUI](https://heroui.com) v2. Telescope-UI wraps HeroUI primitives with project-specific defaults and ships a handful of domain components (shipping/voyage status chips, RHF-integrated form inputs, modals, headers) used across the SLM Solusi Digital maritime apps.

Distributed as an ES module with a shippable `theme.css` (design tokens) and `.d.ts` files.

## Installation

```bash
npm install @slm-solusi-digital/telescope-ui @heroui/react@2.8.0 framer-motion
```

### Peer dependencies

| Package         | Version                |
| --------------- | ---------------------- |
| `react`         | `^18.0.0 \|\| ^19.0.0` |
| `react-dom`     | `^18.0.0 \|\| ^19.0.0` |
| `@heroui/react` | `2.8.0`                |
| `framer-motion` | `>=10.0.0`             |

Everything else (`react-hook-form`, `react-select`, `react-number-format`, `lucide-react`, `@internationalized/date`) is bundled into `dist/index.js`.

## Setup

### 1. Configure Tailwind CSS v4

Create a HeroUI plugin file:

```ts
// src/heroui-plugin.ts
import { heroui } from "@heroui/react";

export default heroui();
```

Wire it into your CSS entry. **This package ships no CSS** — its components are styled by your app's Tailwind build, so you must point Tailwind at both the HeroUI theme **and** this library's `dist` so the utility classes used inside the components get generated:

```css
/* src/index.css */
@import "tailwindcss";
/* Ships the design tokens (palette + text sizes) — import after tailwindcss, before the plugin */
@import "@slm-solusi-digital/telescope-ui/theme.css";
@plugin "./heroui-plugin.ts";
@source "../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}";
/* Required — without this, telescope-ui components render unstyled */
@source "../../node_modules/@slm-solusi-digital/telescope-ui/dist/**/*.js";
```

The imported `theme.css` supplies everything the components need — the full HeroUI color palette as runtime CSS vars (light **and** dark, switched by the `.dark` class) plus the extra `--text-xxs` / `--text-xxxs` sizes. You don't need to define these by hand.

To use a different brand color, override the `--primary` RGB triple (space-separated, no `#`) after the import:

```css
:root {
  --primary: 5 80 169; /* your brand color as "R G B" */
}
```

### 2. Wrap your app with `HeroUIProvider`

```tsx
import { HeroUIProvider } from "@slm-solusi-digital/telescope-ui";

function App() {
  return (
    <HeroUIProvider>
      <YourApp />
    </HeroUIProvider>
  );
}
```

## Components

All components are named exports from the package root.

### Buttons

| Export          | Notes                                                            |
| --------------- | ---------------------------------------------------------------- |
| `ButtonActions` | Three-dot actions menu trigger.                                  |
| `ButtonAdd`     | Icon-only primary "+" button. `ButtonAdd.WithText` adds a label. |
| `ButtonDelete`  | Trash-icon danger button.                                        |

```tsx
import { ButtonAdd, ButtonDelete } from "@slm-solusi-digital/telescope-ui";

<ButtonAdd onPress={onCreate} />
<ButtonAdd.WithText onPress={onCreate}>New voyage</ButtonAdd.WithText>
<ButtonDelete onPress={onDelete} />
```

### Status chips

Voyage / vessel / port-call status indicators built on HeroUI `Chip`.

| Export                                           | Purpose                                                    |
| ------------------------------------------------ | ---------------------------------------------------------- |
| `ChipBallast`, `ChipLaden`, `ChipSailing`        | Voyage leg state                                           |
| `ChipScheduled`, `ChipCompleted`, `ChipCanceled` | Lifecycle state                                            |
| `ChipPol`, `ChipPod`                             | Port of loading / discharge                                |
| `ChipVoyageStatus`                               | Router — dispatches to one of the above by `status` string |
| `ChipVesselStatus`                               | Router for vessel-level status                             |

```tsx
import { ChipVoyageStatus } from "@slm-solusi-digital/telescope-ui";

<ChipVoyageStatus status="SAILING" />;
```

### Headers

| Export             | Notes                                           |
| ------------------ | ----------------------------------------------- |
| `HeaderBack`       | Back-arrow banner for detail pages.             |
| `HeaderDetailPage` | Title + actions slot for record detail screens. |
| `HeaderForm`       | Section heading for form blocks.                |

### Inputs

Form inputs follow a **base + `.WithRHFControl`** convention: use the base for controlled usage, and `Base.WithRHFControl` (co-located in the same file) for react-hook-form. Options-based selects take an `options: { value, label }[]` prop.

| Export                       | Description                                                                                         |
| ---------------------------- | --------------------------------------------------------------------------------------------------- |
| `InputText`                  | Themed `Input` (`outside` label, white bg).                                                         |
| `InputTextarea`              | Themed `Textarea`.                                                                                  |
| `InputNumber`                | Numeric input (`react-number-format`) — thousand separators, `prefix`/`suffix`, `decimalScale`.     |
| `InputSelect`                | HeroUI `Select` dropdown, `options`-based.                                                          |
| `InputAutocomplete`          | Searchable `Autocomplete`, `options`-based (client or server filtering).                            |
| `InputCreatableAutocomplete` | Creatable combobox — pick or create. `.WithRHFControl` has `mode="string" \| "object" \| "action"`. |
| `InputSelectUnit`            | Narrow unit dropdown for an amount field's `endContent` (`place="left" \| "right"`).                |
| `InputSelectMultiple`        | `react-select` multi-select (theme-aware).                                                          |
| `InputCreatableSelectRHF`    | `react-select` creatable (supports `isMulti`); RHF-bound.                                           |
| `InputCheckbox`              | Labeled `CheckboxGroup` over an options array.                                                      |
| `InputRadioGroup`            | Labeled `RadioGroup` over an options array.                                                         |
| `InputDate`                  | Date picker. `.WithRHFControl` (`DateValue`) / `.WithRHFControlISO` (ISO string).                   |
| `InputDateRangePicker`       | Date range picker.                                                                                  |
| `InputSearch`                | Debounced search input.                                                                             |
| `InputUnit`                  | Static unit-label chip that pairs with an input.                                                    |

Every `*.WithRHFControl` adapter takes `control` + `name` (+ optional `rules`) and wires validation.

```tsx
import { InputNumber, InputText } from "@slm-solusi-digital/telescope-ui";
import { useForm } from "react-hook-form";

function VoyageForm() {
  const { control } = useForm({ defaultValues: { vesselName: "", quantity: undefined } });
  return (
    <>
      <InputText.WithRHFControl
        control={control}
        name="vesselName"
        label="Vessel name"
        isRequired
      />
      <InputNumber.WithRHFControl
        control={control}
        name="quantity"
        label="Quantity"
        suffix=" tons"
      />
    </>
  );
}
```

#### Deprecated

Superseded by the `.WithRHFControl` convention above. Still exported for now; removed in a future release (see [CHANGELOG.md](CHANGELOG.md)):

| Deprecated                       | Use instead                                                       |
| -------------------------------- | ----------------------------------------------------------------- |
| `InputSelectRHF`                 | `InputSelect.WithRHFControl` / `InputAutocomplete.WithRHFControl` |
| `InputNumericRHF`                | `InputNumber.WithRHFControl`                                      |
| `InputDateRHF`                   | `InputDate.WithRHFControl`                                        |
| `InputDateTimePickerRHF` (`.V2`) | `InputDate.WithRHFControl` / `InputDate.WithRHFControlISO`        |
| `InputSelectUnitRHF`             | `InputSelectUnit.WithRHFControl`                                  |

### Modals

| Export             | Purpose                                      |
| ------------------ | -------------------------------------------- |
| `ModalAttachments` | File-list + upload modal.                    |
| `ModalDelete`      | Confirmation dialog for destructive actions. |
| `ModalSuccess`     | Success-state confirmation.                  |

### Layout

| Export             | Notes                                                                         |
| ------------------ | ----------------------------------------------------------------------------- |
| `FormLayout`       | Composes `HeaderForm` + `FormFooter` + `FormNavigation` around form sections. |
| `FormSection`      | Anchored form-section card (scroll target via `id`).                          |
| `FormFooter`       | Sticky submit/cancel footer with submitting state.                            |
| `FormNavigation`   | Scroll-spy side nav (single or `multiple` active sections).                   |
| `DetailPageLayout` | `HeaderDetailPage` + content container.                                       |

### Navigation

| Export                | Notes                                                                    |
| --------------------- | ------------------------------------------------------------------------ |
| `TelescopePagination` | "Showing X to Y of N" text + pagination + page-size select (controlled). |

### Data display

| Export         | Notes                                      |
| -------------- | ------------------------------------------ |
| `EmptyState`   | Empty-state placeholder with illustration. |
| `FieldView`    | Read-only label/value row.                 |
| `TimelineItem` | Vertical timeline node (dot + tail).       |
| `TimeStatus`   | Absolute time + relative-time chip.        |

### Feedback & icons

| Export                               | Notes                                           |
| ------------------------------------ | ----------------------------------------------- |
| `BannerError`                        | Inline error banner.                            |
| `PageLoader`                         | Full-page loading spinner.                      |
| `NotFound`                           | 404 page (from `PageNotFound`).                 |
| `IconEstimate`                       | Styled "E" badge used to mark estimated values. |
| `IconCheck`, `IconCloud`, `IconDocs` | Standalone SVG icon components.                 |

### Utilities

Date formatters built on `@internationalized/date`, pinned to `Asia/Jakarta` / `en-GB`:

```ts
import {
  type YearRange,
  convertDateStringToLocal,
  convertDaysToDuration,
  convertParamsToDateRange,
  formatDate,
  formatDateISO,
  formatDateTime,
  formatTime,
  getYearRange,
  getYearRanges,
} from "@slm-solusi-digital/telescope-ui";
```

## Development

```bash
npm install
npm run storybook        # Storybook on http://localhost:6006 — primary dev surface
npm run build            # tsc -b && vite build → emits dist/
npm run test:unit        # Vitest (jsdom)
npm run lint             # ESLint
```

Run a single test file:

```bash
npx vitest run --project unit src/components/InputText/InputText.test.tsx
```

### Pre-commit hook

Commits run an ESLint check (including a blocking `complexity` rule) via `.husky/pre-commit`. The hook requires `core.hooksPath` to point at `.husky` — this is a per-clone git config, so set it once after cloning:

```bash
git config core.hooksPath .husky
```

Without it, commits skip the check silently.

Storybook stories doubling as headless-browser tests:

```bash
npx playwright install chromium    # one-time
npx vitest run --project storybook
```

### Adding a new component

Each component lives in a grouped folder `src/components/<group>/<Name>/` (groups: `button`, `chip`, `input`, `layout`, `modal`, `navigation`, `feedback`, `data-display`, `icon`) with three files:

```
<Name>.tsx          # default export, named after the folder
<Name>.stories.tsx  # Storybook stories under "Components/<Group>/<Name>"
<Name>.test.tsx     # Vitest + @testing-library/react
```

Re-export it from the group barrel `src/components/<group>/index.ts`; the root [src/index.ts](src/index.ts) pulls every barrel in via `export *`. Components that aren't exported are invisible to consumers. Form inputs should expose RHF support via a static `Base.WithRHFControl` (not a new `*RHF` export).

## Releasing

This is a **public scoped package** on the npm registry (`"access": "public"`). Publishing requires an npm account with publish rights on the `@slm-solusi-digital` scope. The `prepublishOnly` hook runs the build automatically, and `publishConfig.access` in `package.json` keeps the release public — no `--access` flag is needed.

**Do not bump the version or `npm publish` ad-hoc.** The full release process — branching model, versioning rules, and the bump/tag/publish steps — lives in [RELEASING.md](RELEASING.md). The short version:

```bash
npm version minor    # bumps package.json, commits, and tags vX.Y.Z (never edit version by hand)
git push --follow-tags
npm publish
```

## License

UNLICENSED — proprietary and confidential. © SLM Solusi Digital. Internal use only; not licensed for redistribution.
