# u-widgets

[![npm](https://img.shields.io/npm/v/@iyulab/u-widgets)](https://www.npmjs.com/package/@iyulab/u-widgets)
[![npm MCP](https://img.shields.io/npm/v/@iyulab/u-widgets-mcp?label=mcp)](https://www.npmjs.com/package/@iyulab/u-widgets-mcp)
[![license](https://img.shields.io/npm/l/@iyulab/u-widgets)](LICENSE)
[![jsdelivr](https://data.jsdelivr.com/v1/package/npm/@iyulab/u-widgets/badge)](https://www.jsdelivr.com/package/npm/@iyulab/u-widgets)

Declarative, data-driven widget system for visualization and input.

Define your data. Map it to visual channels. The renderer does the rest.

```json
{
  "widget": "chart.bar",
  "data": [
    { "name": "A", "value": 30 },
    { "name": "B", "value": 70 },
    { "name": "C", "value": 45 }
  ]
}
```

That's a complete bar chart. Mapping is auto-inferred from data shape.

## Quick Start

A first page with a metric and a bar chart (Vite — `index.html` loads `src/main.ts` as a module):

```bash
npm install @iyulab/u-widgets echarts
```

```ts
// src/main.ts
import '@iyulab/u-widgets';
import '@iyulab/u-widgets/charts';
import type { UWidgetSpec } from '@iyulab/u-widgets';

const specs: UWidgetSpec[] = [
  { widget: 'metric', data: { value: 1284, unit: 'EA', change: 12.5, trend: 'up' } },
  {
    widget: 'chart.bar',
    data: [
      { name: 'A', value: 30 },
      { name: 'B', value: 70 },
      { name: 'C', value: 45 },
    ],
  },
];

for (const spec of specs) {
  const widget = document.createElement('u-widget');
  widget.spec = spec;
  widget.style.cssText = 'display: block; max-width: 480px; margin: 16px';
  document.body.append(widget);
}

document.addEventListener('u-widget-event', (e) => console.log(e.detail));
```

## Installation

```bash
npm install @iyulab/u-widgets
```

```ts
import '@iyulab/u-widgets';
// For chart support (requires echarts peer dependency):
// import '@iyulab/u-widgets/charts';
// For math expression support (requires katex peer dependency):
// import '@iyulab/u-widgets/math';
// Korean for the text widgets write themselves (pagination, validation, region names):
// import '@iyulab/u-widgets/locales/ko';
```

`spec` is a property, not an attribute — set it from script (`el.spec = {…}`) or with a framework binding
(Lit `.spec=${…}`, React `spec={…}` via `@iyulab/u-widgets/react`).

A `chart.*` or `math` widget rendered without its entry imported shows a "Widget module not loaded"
card naming the import, and — if the entry is still missing five seconds later — warns once per widget
type in the console. It renders as soon as the entry is imported, including by a later dynamic `import()`,
so a host that loads the entry lazily sees no warning. To check this at build time, `widgetEntry(type)`
returns the entry a widget type needs:

```ts
import { widgetEntry } from '@iyulab/u-widgets';

widgetEntry('chart.waterfall'); // { entry: '@iyulab/u-widgets/charts', element: 'uw-chart' }
widgetEntry('metric');          // undefined — the core entry renders it
```

### CDN

```html
<script src="https://cdn.jsdelivr.net/npm/@iyulab/u-widgets/dist/u-widgets.global.js"></script>
```

## Widget Types

| Widget | Purpose |
|---|---|
| `chart.bar`, `chart.line`, `chart.area`, `chart.pie`, `chart.scatter`, `chart.radar` | Data visualization |
| `metric`, `stat-group` | KPI numbers |
| `gauge`, `progress` | Value within range |
| `table`, `list`, `kv` | Structured data |
| `code`, `citation`, `status`, `steps`, `rating`, `video`, `gallery` | Content display |
| `math` | LaTeX math expressions (requires `katex`) |
| `form`, `confirm` | User input |
| `compose` | Widget composition |

## Quick Examples

```json
// Metric
{ "widget": "metric", "data": { "value": 1284, "unit": "EA", "change": 12.5, "trend": "up" } }

// Table (columns auto-inferred)
{ "widget": "table", "data": [{ "name": "Alice", "role": "Engineer" }, { "name": "Bob", "role": "Designer" }] }

// Form
{ "widget": "form", "fields": [{ "field": "name", "type": "text", "required": true }], "actions": [{ "label": "Submit", "action": "submit" }] }

// Math (requires u-widgets/math entry point)
{ "widget": "math", "data": { "expression": "E = mc^2" } }

// Compose
{ "widget": "compose", "layout": "grid", "children": [{ "widget": "metric", "data": { "value": 42 } }, { "widget": "gauge", "data": { "value": 73 } }] }
```

## Sizing

Widgets grow to fit their content. Give the host a height when it must fit a fixed box and the
constraint reaches the widget: content that runs on — a table’s rows, a list’s items, a prose box, the
field area of a form — scrolls, so none of it becomes unreachable; a chart fills the box and switches to
a compact layout when it is small; media (`image`, `video`, `gauge`) scales down whole, keeping its
aspect ratio.

```html
<u-widget style="height: 400px"></u-widget>    <!-- chart fills the cell -->
<u-widget style="max-height: 240px"></u-widget> <!-- table rows scroll -->
```

Full contract: [docs/widgets.md](docs/widgets.md#sizing).

## Chart Options

XY charts (`chart.bar`, `chart.line`, `chart.area`, `chart.scatter`) support declarative options that eliminate the need for ECharts passthrough:

### Reference Lines

```json
{
  "widget": "chart.line",
  "data": [{ "month": "Jan", "rate": 82 }, { "month": "Feb", "rate": 105 }],
  "options": {
    "referenceLines": [
      { "axis": "y", "value": 100, "label": "Target", "color": "#16a34a", "style": "dashed" }
    ]
  }
}
```

### Per-Series Styles

```json
{
  "widget": "chart.line",
  "data": [{ "month": "Jan", "orders": 150, "qa": 80, "support": 45 }],
  "mapping": { "x": "month", "y": ["orders", "qa", "support"] },
  "options": {
    "series": [
      { "color": "#f97316" },
      { "color": "#8b5cf6" },
      { "color": "#3b82f6", "lineStyle": "dashed" }
    ]
  }
}
```

Each entry in `options.series` maps 1:1 to the `mapping.y` array. Supported overrides: `color`, `lineStyle`, `symbol`, `label`, `type`, `yAxisIndex`. `lineStyle` takes the keyword `referenceLines` uses (`'solid'`, `'dashed'`, `'dotted'`) or an ECharts `lineStyle` object for finer control (`{ "type": "dashed", "width": 2 }`); any other value is ignored with a console warning.

The `type` override (and `options.echarts.series[].type` passthrough) may only request a registered series: `bar`, `line`, `pie`, `scatter`, `radar`, `heatmap`, `boxplot`, `funnel`, `treemap`, `custom`. Any other type (`graph`, `sankey`, `gauge`, `sunburst`, …) merges into the option but does not render — u-widgets emits a dev warning pointing to [`@iyulab/flex-chart`](https://www.npmjs.com/package/@iyulab/flex-chart) for advanced chart types (this is the same tree-shakeable boundary as heavy interaction components like `dataZoom`/`toolbox`).

`custom` (ECharts custom series, e.g. Gantt/timeline) is registered and renders directly, **but supply it via `options.echarts` passthrough, not `options.series`** — the `options.series` per-entry override only carries the style keys listed above (`color`/`lineStyle`/`symbol`/`label`/`type`/`yAxisIndex`), so a custom series' `renderItem`/`encode` would be dropped:

```json
{
  "widget": "chart.bar",
  "data": [{ "task": "Design", "start": 0, "end": 3 }],
  "options": { "echarts": { "series": [{ "type": "custom", "renderItem": "…" }] } }
}
```

### Axis Label Format

```json
{
  "widget": "chart.bar",
  "data": [{ "month": "Jan", "revenue": 12000000 }],
  "options": {
    "yFormat": { "type": "currency", "currency": "KRW" },
    "locale": "ko-KR"
  }
}
```

`xFormat` / `yFormat` accept: `type` (`number`, `currency`, `percent`, `date`, `bytes`), `prefix`, `suffix`, `currency`, `decimals`.

Without them, a value axis and the tooltip write numbers in the widget locale (`de`: `50.000` · `2,5`). A formatter set
through `options.echarts` (`yAxis.axisLabel.formatter`, `tooltip.valueFormatter`) still wins.

## Events

```js
widget.addEventListener('u-widget-event', (e) => {
  console.log(e.detail); // { type, widget, id?, action?, data? }
});
```

## React

`@iyulab/u-widgets/react` 서브패스가 `<u-widget>`의 일급 React 래퍼 `UWidget`을 제공합니다. `spec`을 문자열로 직렬화(`JSON.stringify`)하지 않고 객체 prop으로 그대로 넘길 수 있으며, `u-widget-event`는 `onWidgetEvent` 콜백 prop으로 노출됩니다.

베이스 패키지를 별도로 import해 커스텀 엘리먼트를 등록해야 합니다. `@lit/react`·`react`는 peerDependency이므로 함께 설치합니다.

```bash
npm install @iyulab/u-widgets @lit/react react
```

```tsx
import '@iyulab/u-widgets';                        // <u-widget> 등록 (side-effect)
import { UWidget } from '@iyulab/u-widgets/react';
import type { UWidgetSpec } from '@iyulab/u-widgets/react';

function Dashboard() {
  return (
    <UWidget
      spec={{ widget: 'stat-group', data: [{ label: 'Active', value: 42 }, { label: 'Errors', value: 3 }] }}
      onWidgetEvent={(e) => console.log(e.detail)} // e.detail: UWidgetEvent
    />
  );
}
```

- `UWidgetSpec`/`UWidgetEvent`/`UWidgetAction` 타입도 함께 re-export됩니다. `onWidgetEvent`은 `CustomEvent`를 받고 `e.detail`이 `UWidgetEvent` 페이로드입니다.
- 손수 `<u-widget spec={JSON.stringify(spec)} />` 래퍼를 만들 필요가 없습니다 — 이 공식 래퍼가 객체 spec·타입드 이벤트를 처리합니다.

## MCP Server

AI assistants can use u-widgets via [MCP server](docs/mcp-server.md):

```bash
npx @iyulab/u-widgets-mcp
```

## Framework Integration

u-widgets is a browser-only Web Component library — it requires DOM APIs (`customElements`, `HTMLElement`) and cannot run in Node.js or server-side environments.

### Next.js (App Router)

Wrap the official `UWidget` React wrapper (위 [React](#react) 참고) in a client component so it registers the element and stays out of Server Components:

```tsx
// components/widget.tsx
"use client";
import "@iyulab/u-widgets";                        // <u-widget> 등록
import { UWidget } from "@iyulab/u-widgets/react";
import type { UWidgetSpec } from "@iyulab/u-widgets/react";

export function Widget({ spec }: { spec: UWidgetSpec }) {
  return <UWidget spec={spec} />;
}
```

Use the wrapper from Server Components:

```tsx
// app/dashboard/page.tsx
import { Widget } from "@/components/widget";

export default async function Page() {
  const data = await fetchData();
  return <Widget spec={{ widget: "stat-group", data }} />;
}
```

For pages where SSR hydration mismatch is a concern, use dynamic import:

```tsx
import dynamic from "next/dynamic";
const Widget = dynamic(
  () => import("@/components/widget").then((m) => m.Widget),
  { ssr: false }
);
```

## Accessibility

The baseline is **WCAG 2.2**. The table lists what this package **measures in tests** — it is not a
conformance claim for the success criteria it does not list.

| Success criterion | Guarantee | Measured by |
|---|---|---|
| SC 2.5.8 Target Size (Minimum) | Every pointer target a widget renders in its own shadow tree — buttons, links, form controls (measured by their activating label), sortable headers, table rows and list items, the interactive rating's icons, citation links, collapsible sections — is at least 24×24 CSS px. Links inside `markdown` prose use the inline exception | `e2e/target-size.spec.ts` (real Chromium, on the demo page) |
| SC 2.1.1 Keyboard (pointer-cursor check) | Nothing a widget renders shows a pointer cursor without also being a focusable, interactive element — a clickable `div` cannot slip in | `e2e/target-size.spec.ts` |
| SC 3.2.5 Change on Request (new tabs) | Citation links and `markdown` links open a new tab and end their accessible name with a visually hidden "(opens in a new tab)" (`opensInNewTab` locale string) | `tests/browser/new-tab-link-name.browser.test.ts` |

What the gate does **not** cover:

- **Spacing is not measured** — where a widget sits relative to other targets on the page is up to
  the host application, so only size is checked.
- **Chart interactions** (legend toggles, tooltips) are drawn on a canvas and have no DOM target to
  measure.
- **`video` controls** are the browser's native controls (the user-agent exception).
- Widgets that render no pointer targets — `chart`, `gallery`, `gauge`, `kv`, `math`, `metric`,
  `status`, `steps` — are listed by the gate as unmeasured rather than counted as passing.

For **KWCAG 2.2** (the Korean web accessibility standard), the `@iyulab/components` README has a table of all 33 check items — which are guaranteed by a test across the sibling packages, which are shared with the app, and which do not apply: [KWCAG 2.2 대응표](https://github.com/iyulab/node-components#kwcag-22-대응표).

## Documentation

- [Widget Reference](docs/widgets.md) — Schema, mapping, options, theming
- [MCP Server Guide](docs/mcp-server.md) — Setup for Claude Desktop, Claude Code, VS Code

## License

MIT
