@iyulab/u-widgets
v0.35.3
Published
Declarative, data-driven widget system for visualization and input
Readme
u-widgets
Declarative, data-driven widget system for visualization and input.
Define your data. Map it to visual channels. The renderer does the rest.
{
"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):
npm install @iyulab/u-widgets echarts// 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
npm install @iyulab/u-widgetsimport '@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:
import { widgetEntry } from '@iyulab/u-widgets';
widgetEntry('chart.waterfall'); // { entry: '@iyulab/u-widgets/charts', element: 'uw-chart' }
widgetEntry('metric'); // undefined — the core entry renders itCDN
<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
// 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.
<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.
Chart Options
XY charts (chart.bar, chart.line, chart.area, chart.scatter) support declarative options that eliminate the need for ECharts passthrough:
Reference Lines
{
"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
{
"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 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:
{
"widget": "chart.bar",
"data": [{ "task": "Design", "start": 0, "end": 3 }],
"options": { "echarts": { "series": [{ "type": "custom", "renderItem": "…" }] } }
}Axis Label Format
{
"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
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이므로 함께 설치합니다.
npm install @iyulab/u-widgets @lit/react reactimport '@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:
npx @iyulab/u-widgets-mcpFramework 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 참고) in a client component so it registers the element and stays out of Server Components:
// 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:
// 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:
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.
videocontrols 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 대응표.
Documentation
- Widget Reference — Schema, mapping, options, theming
- MCP Server Guide — Setup for Claude Desktop, Claude Code, VS Code
License
MIT
