npm package: @gigu/ui
Description: GigU design system components for Granular.js
Latest version: 0.4.2

# @gigu/ui

GigU design system components for Granular.js projects.

**v0.2.2** — 40+ components: buttons, forms, data, overlays, charts, and more.

## Install

```bash
npm install @gigu/ui
```

For charts, also install Chart.js:

```bash
npm install chart.js
```

## Prerequisites

Add to your `index.html` before your main script:

```html
<!-- Urbane Rounded (display / heading font) -->
<link rel="stylesheet" href="https://use.typekit.net/pzw3ohz.css">

<!-- Nunito Sans (body font) -->
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,400;0,6..12,500;0,6..12,600;0,6..12,700;1,6..12,400&display=swap" rel="stylesheet">

<!-- Font Awesome Pro (icons used by components) -->
<script src="https://kit.fontawesome.com/346525b409.js" crossorigin="anonymous"></script>
```

## Quick start

```ts
import "@gigu/ui/tokens.css";
```

When the user switches theme, sync the `.dark` class so tokens apply:

```ts
import { setThemeMode } from "@granularjs/ui";

function applyTheme(isDark: boolean) {
  setThemeMode(isDark ? "dark" : "light");
  document.documentElement.classList.toggle("dark", isDark);
}
```

## Tailwind setup

```js
// tailwind.config.js
const giguPreset = require("@gigu/ui/tailwind");

export default {
  presets: [giguPreset],
  content: ["./src/**/*.{ts,html}"],
};
```

---

## Usage

### Buttons

```ts
import { Button, ActionIcon } from "@gigu/ui";

const btn = Button({ label: "Save", variant: "filled", color: "primary", size: "md" });
const icon = ActionIcon({ icon: "fa-solid fa-plus", onClick: () => {} });
```

### Badges & Alerts

```ts
import { Badge, Chip, Alert } from "@gigu/ui";

const badge = Badge({ label: "Active", color: "green" });
const chip  = Chip({ label: "TypeScript", onRemove: () => {} });
const alert = Alert({ color: "warning", title: "Heads up", message: "This action is irreversible." });
```

### Forms

```ts
import {
  TextInput, NumberInput, Textarea, SearchInput,
  GSelect, Switch, Checkbox, PasswordInput, CopyButton,
  MultiSelect, Combobox,
} from "@gigu/ui";

const input  = TextInput({ label: "Email", placeholder: "you@example.com" });
const search = SearchInput({ placeholder: "Search jobs…", onSearch: (q) => console.log(q) });
const sel    = GSelect({ label: "Status", options: [
  { value: "active", label: "Active" },
  { value: "closed", label: "Closed" },
]});
const combo  = Combobox({ items: [
  { value: "js", label: "JavaScript" },
  { value: "ts", label: "TypeScript" },
], multiple: true, placeholder: "Pick languages…" });
```

### Display

```ts
import { Avatar, Skeleton, Loading, Divider, Spinner } from "@gigu/ui";

const avatar  = Avatar({ name: "Alice Smith", size: "md" });
const skel    = Skeleton({ width: "100%", height: "40px" });
const spinner = Spinner({ variant: "ring", color: "primary", size: "md" });
// variants: "border" | "ring" | "dots" | "bars"
```

### Layout

```ts
import { Stack, Group, Flex, Col, SimpleGrid } from "@gigu/ui";

const stack = Stack({ gap: 4 }, item1, item2, item3);
const group = Group({ gap: 2 }, badge1, badge2);
const grid  = SimpleGrid({ cols: 3, gap: 6 }, card1, card2, card3);
```

### Data

```ts
import { StatCard, Table, List, DataGrid, KPICard } from "@gigu/ui";

const stat = StatCard({
  label: "Total Applications",
  value: "1,284",
  delta: "+12%",
  deltaUp: true,
});

const kpi = KPICard({
  label: "Open Gigs",
  value: "47",
  icon: "fa-solid fa-briefcase",
  iconBg: "bg-orange-100 dark:bg-orange-500/15",
  iconColor: "text-primary",
  trend: { value: "+8%", up: true, label: "vs last month" },
  testId: "kpi-open-gigs",
});

const table = Table({
  columns: [
    { key: "name",   header: "Name" },
    { key: "status", header: "Status" },
    { key: "date",   header: "Date" },
  ],
  rows: data,
});
```

### Progress & Slider

```ts
import { Progress, Slider, RangeSlider } from "@gigu/ui";

const bar    = Progress({ value: 72, label: "Completion", showValue: true, color: "primary" });
const slider = Slider({ min: 0, max: 100, value: 50, onChange: (v) => console.log(v) });
const range  = RangeSlider({ min: 0, max: 100, low: 20, high: 80, onChange: (lo, hi) => {} });
```

### Calendar

```ts
import { Calendar } from "@gigu/ui";

// Single date picker
const cal = Calendar({ mode: "single", onSelect: (date) => console.log(date) });

// Date range picker
const range = Calendar({
  mode: "range",
  onRangeSelect: (start, end) => console.log(start, end),
});
```

### Overlays

```ts
import { createModal, createMenu, Accordion, createDrawer, createPopover } from "@gigu/ui";

// Modal
const { trigger, open } = createModal({
  title: "Confirm deletion",
  content: () => { /* return body element */ },
  footer: (close) => { /* return footer element */ },
});

// Drawer
const drawer = createDrawer({
  direction: "right",
  title: "Applicant details",
  size: "md",
  content: () => { /* return body element */ },
});
drawer.open();

// Popover
const popover = createPopover({
  trigger: myButton,
  content: myContentEl,
  side: "bottom",
  align: "start",
});
popover.toggle();

// Dropdown menu
const menu = createMenu({
  trigger: menuBtn,
  items: [
    { label: "Edit", icon: "fa-solid fa-pen", onClick: () => {} },
    { label: "Delete", icon: "fa-solid fa-trash", onClick: () => {}, danger: true },
  ],
});
```

### Toast

```ts
import { toast } from "@gigu/ui";

toast.success("Profile saved");
toast.error("Upload failed", { description: "File too large" });
toast.promise(fetch("/api/submit"), {
  loading: "Submitting…",
  success: "Done!",
  error: "Failed",
});
```

### Tooltip

```ts
import { attachTooltip } from "@gigu/ui";

attachTooltip(myButton, "Click to submit", "top");
```

### Navigation

```ts
import { NavLink, Sidebar, AppShell, Breadcrumbs, NavTabs } from "@gigu/ui";

const crumbs = Breadcrumbs([
  { label: "Dashboard", href: "/" },
  { label: "Jobs", href: "/jobs" },
  { label: "Edit" },
]);

const tabs = NavTabs({
  items: [
    { label: "Overview", value: "overview" },
    { label: "Applications", value: "apps" },
  ],
  active: "overview",
  onChange: (val) => {},
});
```

### ScrollArea

A scrollable container with GigU-styled thin custom scrollbars (6px, hover-reveal, dark-mode aware).

```ts
import { ScrollArea } from "@gigu/ui";

// Vertical scroll with preset height
const area = ScrollArea(
  { size: "lg", scrollbars: "vertical" },
  item1, item2, item3,
);

// Custom max-height
const custom = ScrollArea(
  { maxHeight: "400px" },
  longContent,
);

// Horizontal scroll
const hscroll = ScrollArea(
  { scrollbars: "horizontal", maxHeight: "auto" },
  wideTable,
);
```

Size presets: `sm` (120px) · `md` (200px) · `lg` (320px) · `auto` (no limit) · any CSS value via `maxHeight`.

### Image

An `<img>` wrapper with an optional loading skeleton and error fallback.

```ts
import { GigUImage } from "@gigu/ui";

// Avatar
const avatar = GigUImage({
  src: "/uploads/avatar.jpg",
  alt: "User avatar",
  width: 80,
  height: 80,
  radius: "full",
  fit: "cover",
});

// Card banner with skeleton
const banner = GigUImage({
  src: "/uploads/banner.jpg",
  alt: "Company banner",
  width: "100%",
  height: "200px",
  radius: "lg",
  withPlaceholder: true,
});

// Custom error icon
const img = GigUImage({
  src: url,
  alt: "Logo",
  width: 120,
  height: 120,
  radius: "md",
  fallbackIcon: "fa-solid fa-building",
});
```

Radius options: `none` · `sm` · `md` · `lg` · `xl` · `2xl` · `full` · any CSS value.
Fit options: `cover` · `contain` · `fill` · `none` · `scale-down`.

### Empty States

```ts
import { EmptyState, InlineEmpty } from "@gigu/ui";

const empty = EmptyState({
  icon: "fa-solid fa-inbox",
  iconColor: "text-gray-400",
  iconBg: "bg-gray-100 dark:bg-gray-800",
  title: "No applications yet",
  description: "Applications will appear here once candidates apply.",
  action: { label: "Post a gig", icon: "fa-solid fa-plus" },
});

const inline = InlineEmpty("fa-solid fa-magnifying-glass", "No results found");
```

### Pagination

```ts
import { Pagination, PaginationBar } from "@gigu/ui";

const nav = Pagination({
  totalPages: 12,
  currentPage: 1,
  onPageChange: (page) => loadPage(page),
});

const bar = PaginationBar({
  total: 247,
  rowsPerPage: 25,
  onPageChange: (page) => loadPage(page),
  onRowsChange: (rows) => setPageSize(rows),
});
```

---

## Charts

`@gigu/ui` ships theme-aware Chart.js helpers that automatically apply the GigU visual style in both light and dark mode.

### Setup

```ts
import Chart from "chart.js/auto";
import "@gigu/ui/tokens.css";
```

### Color palette

```ts
import { GIGU_COLORS, GIGU_PALETTE } from "@gigu/ui";

// Named colors
GIGU_COLORS.primary  // "#FF5900"
GIGU_COLORS.blue     // "#3B82F6"
GIGU_COLORS.green    // "#22C55E"
GIGU_COLORS.amber    // "#F59E0B"
GIGU_COLORS.purple   // "#8B5CF6"
GIGU_COLORS.pink     // "#EC4899"

// Array for cycling over multiple datasets
GIGU_PALETTE  // [primary, blue, green, amber, purple, pink]
```

### Theme-aware defaults

`giguChartDefaults()` reads the current dark-mode state from `<html>` and returns a full Chart.js options object:

```ts
import Chart from "chart.js/auto";
import { ChartCanvas, ChartCard, giguChartDefaults } from "@gigu/ui";

const { defs, xy, r } = giguChartDefaults();
// defs  — responsive + legend + tooltip defaults
// xy    — x/y scale defaults (gridlines, tick colors)
// r     — radar scale defaults

const canvas = ChartCanvas("my-chart");
new Chart(canvas, {
  type: "bar",
  data: { labels: ["Jan","Feb","Mar"], datasets: [{ label: "Revenue", data: [42,78,65] }] },
  options: { ...defs, scales: xy },
});

const card = ChartCard({ title: "Revenue", subtitle: "Monthly breakdown", canvas });
document.body.appendChild(card);
```

### Preset chart configs

These return a complete `ChartConfiguration` object ready to pass to `new Chart()`:

```ts
import Chart from "chart.js/auto";
import { ChartCanvas, barChartConfig, lineChartConfig, pieChartConfig, radarChartConfig } from "@gigu/ui";

// Bar — single, grouped, stacked, or horizontal
const barCfg = barChartConfig({
  labels: ["Jan","Feb","Mar","Apr","May","Jun"],
  datasets: [
    { label: "Applications", data: [42,78,65,90,82,110] },
    { label: "Hires",        data: [10,18,15,24,21, 32], color: "#3B82F6" },
  ],
  stacked: false,     // true for stacked bars
  horizontal: false,  // true for horizontal bar (indexAxis: "y")
});
new Chart(ChartCanvas("bar"), barCfg);

// Line — fill optional per dataset
const lineCfg = lineChartConfig({
  labels: ["Jan","Feb","Mar","Apr","May","Jun"],
  datasets: [
    { label: "Active Gigs", data: [8,12,10,15,14,18], fill: true },
    { label: "Filled",      data: [5, 9, 8,12,11,15], fill: false },
  ],
});
new Chart(ChartCanvas("line"), lineCfg);

// Pie or donut
const pieCfg = pieChartConfig({
  labels: ["Part-time","Full-time","Internship","Contract"],
  data:   [45, 25, 20, 10],
  donut:  true,    // false for pie
  cutout: "65%",   // donut hole size
});
new Chart(ChartCanvas("pie"), pieCfg);

// Radar
const radarCfg = radarChartConfig({
  labels: ["Speed","Quality","Reliability","Communication","Flexibility"],
  datasets: [
    { label: "Avg Student",    data: [75,80,70,85,78] },
    { label: "Top Performers", data: [90,92,88,95,90] },
  ],
});
new Chart(ChartCanvas("radar"), radarCfg);
```

### Tabbed live chart card

```ts
import Chart from "chart.js/auto";
import { LiveChartCard, giguChartDefaults, GIGU_COLORS } from "@gigu/ui";

const { card, canvas, ref } = LiveChartCard(
  "Unique Applicants",
  "Applications received per period",
  [
    { label: "Day",   labels: ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"], data: [18,32,27,45,38,22,14] },
    { label: "Week",  labels: ["Wk 1","Wk 2","Wk 3","Wk 4","Wk 5","Wk 6"], data: [82,110,96,140,128,155] },
    { label: "Month", labels: ["Jan","Feb","Mar","Apr","May","Jun"],         data: [42,78,65,90,82,110] },
  ],
  "chart-applicants",
);

document.body.appendChild(card);

const { defs, xy, tooltipDefaults } = giguChartDefaults();
ref.chart = new Chart(canvas, {
  type: "bar",
  data: {
    labels: ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],
    datasets: [{ label: "Applicants", data: [18,32,27,45,38,22,14], backgroundColor: `${GIGU_COLORS.primary}CC`, borderRadius: 6 }],
  },
  options: {
    ...defs,
    scales: xy,
    plugins: { legend: { display: false }, tooltip: tooltipDefaults },
    animation: { duration: 300 },
  },
});
```

---

## CSS tokens reference

| Variable | Value | Usage |
|----------|-------|-------|
| `--gigu-flame`   | `#FF5900` | Primary / CTA |
| `--gigu-ember`   | `#E64A19` | Primary hover |
| `--gigu-spark`   | `#FF7F50` | Accent / emphasized |
| `--gigu-cream`   | `#FFE5D0` | Primary subtle bg |
| `--gigu-ash`     | `#6B6B6B` | Body text |
| `--gigu-fog`     | `#8A9099` | Secondary / descriptions |
| `--gigu-snow`    | `#C7CCD6` | Borders / dividers |
| `--font-sans`    | Nunito Sans     | Body text |
| `--font-display` | Urbane Rounded  | Headings / titles |
| `--font-mono`    | Geist Mono      | Code blocks |