@momoi-labs/kiso-react 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,6 +46,11 @@ Controls and forms
46
46
  - Input, Textarea, Search, Label, Checkbox, Switch, and FormField.
47
47
  - Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup,
48
48
  SelectLabel, and SelectSeparator.
49
+ - ChipInput, ChipInputBox, ChipInputField, ChipInputList, ChipInputOption,
50
+ ChipInputEmpty, and the chip parts Chip, ChipScope, ChipName, ChipValue,
51
+ ChipOption, ChipOptionAdd, ChipRemove, for several structured values in one
52
+ field. Each chip reads as a call: scope, name, value, then one segment per
53
+ option as name=value or name=[a, b].
49
54
  - ValidationMessage, for a field error referenced by `aria-describedby`.
50
55
 
51
56
  Data and structure
@@ -57,7 +62,8 @@ Data and structure
57
62
  - Sidebar, SidebarHeader, SidebarBody, SidebarFooter.
58
63
  - AppShell and AppShellMain, the low-level columns of a console.
59
64
  - ApplicationShell, the standard brand, navigation, header, footer, and page
60
- arrangement.
65
+ arrangement. Pass `layout="topbar"` for a single-surface frame with brand and
66
+ primary action in the top bar and no sidebar rail.
61
67
  - Split, Pane, and Splitter, a list-detail layout with a resizable divider.
62
68
  - LogView, LogViewLine, LogViewTime, and LogViewLevel.
63
69
  - Stat, StatHeader, StatLabel, StatValue, StatFoot, and StatDelta.
@@ -114,9 +120,11 @@ const notify = useToast();
114
120
  notify('error', `Could not stop ${app.name}`, details);
115
121
  ```
116
122
 
117
- ApplicationShell takes navigation groups as data. Destinations keep routing in
118
- the product through `{ href, active, onClick? }`; the package owns the repeated
119
- Sidebar, Navigation, Header, and main arrangement.
123
+ ApplicationShell takes navigation groups as data in the default sidebar layout.
124
+ Destinations keep routing in the product through `{ href, active, onClick? }`;
125
+ the package owns the repeated Sidebar, Navigation, Header, and main
126
+ arrangement. With `layout="topbar"`, omit navigation and footer: brand and
127
+ primary action move into Header beside the existing header slot.
120
128
 
121
129
  Navigation, Sidebar, and Link know nothing about routing. Mark the current
122
130
  destination with `active`, and pass `asChild` to render a router's own link
@@ -176,7 +184,7 @@ import { BrandMark, TerminalIcon } from '@momoi-labs/kiso-react';
176
184
  ## Development
177
185
 
178
186
  From the repository root, run `npm ci`, then `npm run prototype`. The component
179
- gallery opens at <http://127.0.0.1:5173/#components> and covers all 43 Kiso
187
+ gallery opens at <http://127.0.0.1:5173/#components> and covers all 44 Kiso
180
188
  catalog entries, each rendering the component this package exports. Its sample
181
189
  data is simulated in memory.
182
190
 
@@ -14,15 +14,25 @@ type ApplicationShellGroup = {
14
14
  destinations: readonly ApplicationShellDestination[];
15
15
  empty?: React.ReactNode;
16
16
  };
17
- type ApplicationShellProps = Omit<React.ComponentProps<typeof AppShell>, "children"> & {
17
+ type ApplicationShellSharedProps = Omit<React.ComponentProps<typeof AppShell>, "children"> & {
18
18
  brand: React.ReactNode;
19
19
  primaryAction?: React.ReactNode;
20
+ header?: React.ReactNode;
21
+ children: React.ReactNode;
22
+ };
23
+ type ApplicationShellSidebarProps = ApplicationShellSharedProps & {
24
+ layout?: "sidebar";
20
25
  navigation: readonly ApplicationShellGroup[];
21
26
  navigationLabel?: string;
22
27
  footer?: React.ReactNode;
23
- header?: React.ReactNode;
24
- children: React.ReactNode;
25
28
  };
26
- declare function ApplicationShell({ brand, primaryAction, navigation, navigationLabel, footer, header, children, ...props }: ApplicationShellProps): React.JSX.Element;
29
+ type ApplicationShellTopbarProps = ApplicationShellSharedProps & {
30
+ layout: "topbar";
31
+ navigation?: never;
32
+ navigationLabel?: never;
33
+ footer?: never;
34
+ };
35
+ type ApplicationShellProps = ApplicationShellSidebarProps | ApplicationShellTopbarProps;
36
+ declare function ApplicationShell(props: ApplicationShellProps): React.JSX.Element;
27
37
  export { AppShell, AppShellMain, ApplicationShell };
28
38
  export type { ApplicationShellDestination, ApplicationShellGroup, ApplicationShellProps, };
package/dist/app-shell.js CHANGED
@@ -11,12 +11,20 @@ function AppShell({ className, ...props }) {
11
11
  function AppShellMain({ className, ...props }) {
12
12
  return (_jsx("main", { "data-slot": "app-shell-main", className: cn("grow", className), ...props }));
13
13
  }
14
- function ApplicationShell({ brand, primaryAction, navigation, navigationLabel = "Primary", footer, header, children, ...props }) {
15
- return (_jsxs(AppShell, { ...props, children: [_jsxs(Sidebar, { children: [_jsxs(SidebarHeader, { children: [brand, primaryAction] }), _jsx(SidebarBody, { children: _jsx(Navigation, { "aria-label": navigationLabel, children: navigation.map((group, groupIndex) => (_jsx(NavigationGroup, { label: group.label, children: group.destinations.length > 0 ? (_jsx(NavigationList, { children: group.destinations.map((destination, destinationIndex) => (_jsx(NavigationItem, { children: _jsxs(NavigationLink, { href: destination.href, active: destination.active, onClick: (event) => {
16
- if (!destination.onClick)
17
- return;
18
- event.preventDefault();
19
- destination.onClick();
20
- }, children: [destination.leading, _jsx("span", { className: "grow truncate", children: destination.label }), destination.trailing] }) }, `${destination.href}-${destinationIndex}`))) })) : group.empty }, group.label ?? groupIndex))) }) }), footer && _jsx(SidebarFooter, { children: footer })] }), _jsxs(AppShellMain, { children: [header && _jsx(Header, { children: header }), children] })] }));
14
+ function ApplicationShellNavigation({ navigation, navigationLabel, }) {
15
+ return (_jsx(Navigation, { "aria-label": navigationLabel, children: navigation.map((group, groupIndex) => (_jsx(NavigationGroup, { label: group.label, children: group.destinations.length > 0 ? (_jsx(NavigationList, { children: group.destinations.map((destination, destinationIndex) => (_jsx(NavigationItem, { children: _jsxs(NavigationLink, { href: destination.href, active: destination.active, onClick: (event) => {
16
+ if (!destination.onClick)
17
+ return;
18
+ event.preventDefault();
19
+ destination.onClick();
20
+ }, children: [destination.leading, _jsx("span", { className: "grow truncate", children: destination.label }), destination.trailing] }) }, `${destination.href}-${destinationIndex}`))) })) : (group.empty) }, group.label ?? groupIndex))) }));
21
+ }
22
+ function ApplicationShell(props) {
23
+ if (props.layout === "topbar") {
24
+ const { brand, primaryAction, header, children, layout: _layout, ...shellProps } = props;
25
+ return (_jsx(AppShell, { ...shellProps, "data-layout": "topbar", children: _jsxs(AppShellMain, { children: [_jsxs(Header, { children: [brand, primaryAction, header] }), children] }) }));
26
+ }
27
+ const { brand, primaryAction, navigation, navigationLabel = "Primary", footer, header, children, layout: _layout, ...shellProps } = props;
28
+ return (_jsxs(AppShell, { ...shellProps, children: [_jsxs(Sidebar, { children: [_jsxs(SidebarHeader, { children: [brand, primaryAction] }), _jsx(SidebarBody, { children: _jsx(ApplicationShellNavigation, { navigation: navigation, navigationLabel: navigationLabel }) }), footer && _jsx(SidebarFooter, { children: footer })] }), _jsxs(AppShellMain, { children: [header && _jsx(Header, { children: header }), children] })] }));
21
29
  }
22
30
  export { AppShell, AppShellMain, ApplicationShell };
@@ -0,0 +1,43 @@
1
+ import * as React from "react";
2
+ declare function ChipInput({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
3
+ declare function ChipInputBox({ className, disabled, onMouseDown, ...props }: React.ComponentProps<"div"> & {
4
+ disabled?: boolean;
5
+ }): React.JSX.Element;
6
+ declare function ChipInputField({ className, onKeyDown, onRemoveLast, ...props }: React.ComponentProps<"input"> & {
7
+ onRemoveLast?: () => void;
8
+ }): React.JSX.Element;
9
+ declare function ChipInputList({ className, "aria-label": ariaLabel, ...props }: React.ComponentProps<"div">): React.JSX.Element;
10
+ declare function ChipInputOption({ className, disabled, onSelect, onClick, ...props }: React.ComponentProps<"div"> & {
11
+ disabled?: boolean;
12
+ onSelect?: () => void;
13
+ }): React.JSX.Element;
14
+ declare function ChipInputEmpty({ className, ...props }: React.ComponentProps<"p">): React.JSX.Element;
15
+ declare function Chip({ className, invalid, ...props }: React.ComponentProps<"span"> & {
16
+ invalid?: boolean;
17
+ }): React.JSX.Element;
18
+ declare function ChipScope({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element;
19
+ declare function ChipName({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element;
20
+ declare function EditableSegment({ slot, className, value, placeholder, display, onCommit, editable, editLabel, confirmLabel, maxWidth, ...props }: Omit<React.ComponentProps<"button">, "value"> & {
21
+ slot: string;
22
+ value: string;
23
+ placeholder?: string;
24
+ display?: React.ReactNode;
25
+ onCommit?: (value: string) => void;
26
+ editable?: boolean;
27
+ editLabel?: string;
28
+ confirmLabel?: string;
29
+ maxWidth?: number;
30
+ }): React.JSX.Element;
31
+ declare function ChipValue({ className, value, editLabel, confirmLabel, ...props }: Omit<React.ComponentProps<typeof EditableSegment>, "slot"> & {
32
+ value: string;
33
+ }): React.JSX.Element;
34
+ declare function ChipOption({ className, name, value, label, editLabel, confirmLabel, ...props }: Omit<React.ComponentProps<typeof EditableSegment>, "slot" | "value" | "display"> & {
35
+ name: string;
36
+ value: string | string[];
37
+ label?: string;
38
+ }): React.JSX.Element;
39
+ declare function ChipOptionAdd({ className, label, placeholder, editLabel, confirmLabel, ...props }: Omit<React.ComponentProps<typeof EditableSegment>, "slot" | "value" | "display"> & {
40
+ label: string;
41
+ }): React.JSX.Element;
42
+ declare function ChipRemove({ className, ...props }: React.ComponentProps<"button">): React.JSX.Element;
43
+ export { Chip, ChipInput, ChipInputBox, ChipInputEmpty, ChipInputField, ChipInputList, ChipInputOption, ChipName, ChipOption, ChipOptionAdd, ChipRemove, ChipScope, ChipValue, };
@@ -0,0 +1,174 @@
1
+ "use client";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { clsx as cn } from "clsx";
5
+ // One field holding several structured values. The component owns the box, the
6
+ // chips, and the combobox keys; the product owns what a chip means, which
7
+ // suggestions match, and which options a chip accepts.
8
+ const ChipInputContext = React.createContext(null);
9
+ function useChipInput() {
10
+ const context = React.useContext(ChipInputContext);
11
+ if (!context) {
12
+ throw new Error("ChipInput parts must be used inside <ChipInput>");
13
+ }
14
+ return context;
15
+ }
16
+ function options(list) {
17
+ return Array.from(list?.querySelectorAll('[data-slot="chip-input-option"]:not([aria-disabled="true"])') ?? []);
18
+ }
19
+ function ChipInput({ className, ...props }) {
20
+ const [activeId, setActiveId] = React.useState("");
21
+ const [open, setOpen] = React.useState(false);
22
+ const listId = React.useId();
23
+ const listRef = React.useRef(null);
24
+ const fieldRef = React.useRef(null);
25
+ const value = React.useMemo(() => ({ activeId, setActiveId, listId, listRef, fieldRef, open, setOpen }), [activeId, listId, open]);
26
+ return (_jsx(ChipInputContext.Provider, { value: value, children: _jsx("div", { "data-slot": "chip-input", className: cn("chip-input", className), ...props }) }));
27
+ }
28
+ // The box is the field: clicking its empty space puts the caret back where
29
+ // typing continues, the way a single-line input behaves.
30
+ function ChipInputBox({ className, disabled = false, onMouseDown, ...props }) {
31
+ const { fieldRef } = useChipInput();
32
+ return (_jsx("div", { "data-slot": "chip-input-box", "data-disabled": disabled || undefined, className: cn("chip-input-box", className), onMouseDown: (event) => {
33
+ if (event.target === event.currentTarget && !disabled) {
34
+ event.preventDefault();
35
+ fieldRef.current?.focus();
36
+ }
37
+ onMouseDown?.(event);
38
+ }, ...props }));
39
+ }
40
+ function ChipInputField({ className, onKeyDown, onRemoveLast, ...props }) {
41
+ const { activeId, setActiveId, listId, listRef, fieldRef, open } = useChipInput();
42
+ // Typing changes the list under the highlight, so re-anchor it every render
43
+ // instead of tracking an index the filter invalidates.
44
+ React.useEffect(() => {
45
+ const visible = options(listRef.current);
46
+ if (!visible.some((option) => option.id === activeId)) {
47
+ setActiveId(visible[0]?.id ?? "");
48
+ }
49
+ });
50
+ function keys(event) {
51
+ const visible = options(listRef.current);
52
+ const index = visible.findIndex((option) => option.id === activeId);
53
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
54
+ event.preventDefault();
55
+ if (!visible.length)
56
+ return;
57
+ const step = event.key === "ArrowDown" ? 1 : -1;
58
+ const next = visible[(index + step + visible.length) % visible.length];
59
+ setActiveId(next.id);
60
+ next.scrollIntoView({ block: "nearest" });
61
+ }
62
+ else if ((event.key === "Enter" || event.key === "Tab") && index >= 0) {
63
+ // Tab only commits a highlighted suggestion; with none it still leaves
64
+ // the field, so the box never traps the keyboard. Enter with no
65
+ // highlight falls through to the product, which may take the query.
66
+ event.preventDefault();
67
+ visible[index]?.click();
68
+ }
69
+ else if (event.key === "Backspace" &&
70
+ event.currentTarget.value === "" &&
71
+ onRemoveLast) {
72
+ event.preventDefault();
73
+ onRemoveLast();
74
+ }
75
+ onKeyDown?.(event);
76
+ }
77
+ return (_jsx("input", { ref: fieldRef, "data-slot": "chip-input-field", role: "combobox", "aria-expanded": open, "aria-controls": listId, "aria-activedescendant": activeId || undefined, "aria-autocomplete": "list", autoComplete: "off", onKeyDown: keys, className: cn("chip-input-field", className), ...props }));
78
+ }
79
+ function ChipInputList({ className, "aria-label": ariaLabel = "Suggestions", ...props }) {
80
+ const { listId, listRef, setOpen } = useChipInput();
81
+ React.useEffect(() => {
82
+ setOpen(true);
83
+ return () => setOpen(false);
84
+ }, [setOpen]);
85
+ return (_jsx("div", { id: listId, ref: listRef, role: "listbox", "aria-label": ariaLabel, "data-slot": "chip-input-list", className: cn("menu chip-input-list", className), ...props }));
86
+ }
87
+ function ChipInputOption({ className, disabled = false, onSelect, onClick, ...props }) {
88
+ const id = React.useId();
89
+ const { activeId, setActiveId } = useChipInput();
90
+ return (_jsx("div", { id: id, role: "option", "data-slot": "chip-input-option", "aria-selected": activeId === id, "aria-disabled": disabled || undefined, className: cn("menu-item", className), onPointerMove: () => !disabled && setActiveId(id), onMouseDown: (event) => event.preventDefault(), onClick: (event) => {
91
+ if (disabled)
92
+ return;
93
+ onSelect?.();
94
+ onClick?.(event);
95
+ }, ...props }));
96
+ }
97
+ function ChipInputEmpty({ className, ...props }) {
98
+ return (_jsx("p", { "data-slot": "chip-input-empty", className: cn("menu-item muted", className), ...props }));
99
+ }
100
+ function Chip({ className, invalid = false, ...props }) {
101
+ return (_jsx("span", { "data-slot": "chip", "data-state": invalid ? "invalid" : undefined, className: cn("chip", className), ...props }));
102
+ }
103
+ // The leading segment: what kind of value this is, when that is a separate
104
+ // fact from its name. Reads as the caller in `npm(t3, latest)`.
105
+ function ChipScope({ className, ...props }) {
106
+ return (_jsx("span", { "data-slot": "chip-scope", className: cn("chip-scope", className), ...props }));
107
+ }
108
+ function ChipName({ className, ...props }) {
109
+ return (_jsx("span", { "data-slot": "chip-name", className: cn("chip-name truncate", className), ...props }));
110
+ }
111
+ // Every editable segment behaves the same: press it, it becomes an input
112
+ // sized to its text, Enter or the check confirms, Escape restores.
113
+ function EditableSegment({ slot, className, value, placeholder, display, onCommit, editable = true, editLabel, confirmLabel, maxWidth = 48, ...props }) {
114
+ const [draft, setDraft] = React.useState(null);
115
+ if (!editable || !onCommit) {
116
+ return (_jsx("span", { "data-slot": slot, className: cn(className), children: value ? (display ?? value) : placeholder }));
117
+ }
118
+ function commit(next) {
119
+ if (next !== null)
120
+ onCommit?.(next.trim());
121
+ setDraft(null);
122
+ }
123
+ if (draft !== null) {
124
+ return (_jsxs(_Fragment, { children: [_jsx("input", { "data-slot": `${slot}-input`, className: cn(className, "is-editing"), "aria-label": editLabel, value: draft, autoFocus: true, size: 1, style: {
125
+ width: `${Math.min(Math.max(draft.length + 1, 6), maxWidth)}ch`,
126
+ }, onChange: (event) => setDraft(event.target.value), onBlur: () => commit(draft), onKeyDown: (event) => {
127
+ if (event.key === "Enter") {
128
+ event.preventDefault();
129
+ commit(draft);
130
+ }
131
+ else if (event.key === "Escape") {
132
+ event.preventDefault();
133
+ setDraft(null);
134
+ }
135
+ event.stopPropagation();
136
+ } }), _jsx("button", { type: "button", className: "chip-action", "aria-label": confirmLabel,
137
+ // Commit before the input's blur can unmount this button.
138
+ onMouseDown: (event) => {
139
+ event.preventDefault();
140
+ commit(draft);
141
+ }, children: _jsx(CheckIcon, {}) })] }));
142
+ }
143
+ return (_jsx("button", { type: "button", "data-slot": slot, "data-empty": value ? undefined : "true", className: cn(className), "aria-label": editLabel, onClick: () => setDraft(value), ...props, children: value ? (display ?? value) : placeholder }));
144
+ }
145
+ // The version is the part people revise most, so it edits in place instead of
146
+ // forcing a remove and a retype.
147
+ function ChipValue({ className, value, editLabel, confirmLabel = "Confirm value", ...props }) {
148
+ return (_jsx(EditableSegment, { slot: "chip-value", className: cn("chip-value", className), value: value, editLabel: editLabel ?? `Edit value, currently ${value}`, confirmLabel: confirmLabel, maxWidth: 16, ...props }));
149
+ }
150
+ // One segment per option, divided like the rest, written the way the config
151
+ // file writes it. A list keeps its brackets so `os=linux` and
152
+ // `allow_builds=[node-pty, esbuild]` read as the same kind of fact.
153
+ function ChipOption({ className, name, value, label, editLabel, confirmLabel, ...props }) {
154
+ const list = Array.isArray(value);
155
+ const shown = list ? `[${value.join(", ")}]` : value;
156
+ const text = `${name}=${shown}`;
157
+ const where = label ? ` on ${label}` : "";
158
+ return (_jsx(EditableSegment, { slot: "chip-option", className: cn("chip-option", className), value: text, display: _jsxs(_Fragment, { children: [_jsx("span", { className: "chip-option-name", children: name }), _jsx("span", { className: "chip-option-mark", children: "=" }), list ? _jsx("span", { className: "chip-option-mark", children: "[" }) : null, _jsx("span", { className: "chip-option-value", children: list ? value.join(", ") : value }), list ? _jsx("span", { className: "chip-option-mark", children: "]" }) : null] }), editLabel: editLabel ?? `Edit ${name}${where}, currently ${shown}`, confirmLabel: confirmLabel ?? `Confirm ${name}${where}`, ...props }));
159
+ }
160
+ // The segment that takes a new option. It stays at the end of the chip so the
161
+ // place to add one is where the last one ended.
162
+ function ChipOptionAdd({ className, label, placeholder = "+", editLabel, confirmLabel, ...props }) {
163
+ return (_jsx(EditableSegment, { slot: "chip-option-add", className: cn("chip-option", className), value: "", placeholder: placeholder, editLabel: editLabel ?? `Add an option to ${label}`, confirmLabel: confirmLabel ?? `Confirm the new option on ${label}`, ...props }));
164
+ }
165
+ function ChipRemove({ className, ...props }) {
166
+ return (_jsx("button", { type: "button", "data-slot": "chip-remove", className: cn("chip-action", className), ...props, children: _jsx(CloseIcon, {}) }));
167
+ }
168
+ function CheckIcon() {
169
+ return (_jsx("svg", { className: "icon", viewBox: "0 0 16 16", "aria-hidden": "true", children: _jsx("path", { d: "m3.5 8.5 3 3 6-7" }) }));
170
+ }
171
+ function CloseIcon() {
172
+ return (_jsx("svg", { className: "icon", viewBox: "0 0 16 16", "aria-hidden": "true", children: _jsx("path", { d: "m4 4 8 8M12 4l-8 8" }) }));
173
+ }
174
+ export { Chip, ChipInput, ChipInputBox, ChipInputEmpty, ChipInputField, ChipInputList, ChipInputOption, ChipName, ChipOption, ChipOptionAdd, ChipRemove, ChipScope, ChipValue, };
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./breadcrumb.js";
7
7
  export * from "./button.js";
8
8
  export * from "./card.js";
9
9
  export * from "./checkbox.js";
10
+ export * from "./chip-input.js";
10
11
  export * from "./command-palette.js";
11
12
  export * from "./dialog.js";
12
13
  export * from "./dot.js";
@@ -29,6 +30,7 @@ export * from "./select.js";
29
30
  export * from "./separator.js";
30
31
  export * from "./sidebar.js";
31
32
  export * from "./skeleton.js";
33
+ export * from "./sparkline.js";
32
34
  export * from "./spinner.js";
33
35
  export * from "./split.js";
34
36
  export * from "./stat.js";
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export * from "./breadcrumb.js";
8
8
  export * from "./button.js";
9
9
  export * from "./card.js";
10
10
  export * from "./checkbox.js";
11
+ export * from "./chip-input.js";
11
12
  export * from "./command-palette.js";
12
13
  export * from "./dialog.js";
13
14
  export * from "./dot.js";
@@ -30,6 +31,7 @@ export * from "./select.js";
30
31
  export * from "./separator.js";
31
32
  export * from "./sidebar.js";
32
33
  export * from "./skeleton.js";
34
+ export * from "./sparkline.js";
33
35
  export * from "./spinner.js";
34
36
  export * from "./split.js";
35
37
  export * from "./stat.js";
@@ -0,0 +1,13 @@
1
+ import * as React from "react";
2
+ export type SparklineProps = Omit<React.ComponentProps<"div">, "ref"> & {
3
+ values: number[];
4
+ height?: number;
5
+ tone?: "neutral" | "primary";
6
+ fill?: boolean;
7
+ min?: number;
8
+ max?: number;
9
+ label?: string;
10
+ ref?: React.Ref<HTMLDivElement>;
11
+ };
12
+ declare function Sparkline({ values, height, tone, fill, min, max, label, className, style, ref, ...props }: SparklineProps): React.JSX.Element | null;
13
+ export { Sparkline };
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Area, AreaChart, ResponsiveContainer, YAxis } from "recharts";
4
+ import { clsx as cn } from "clsx";
5
+ function Sparkline({ values, height = 24, tone = "neutral", fill = false, min, max, label, className, style, ref, ...props }) {
6
+ // Below two samples there is no shape to show, and a flat rule across a
7
+ // cell reads as a border rather than as a measurement.
8
+ if (values.length < 2)
9
+ return null;
10
+ // Default domain is the data's own extent; pass min and max to make
11
+ // sibling sparklines share one scale, so rows compare honestly.
12
+ let lo = min ?? Math.min(...values);
13
+ let hi = max ?? Math.max(...values);
14
+ const pad = lo === hi ? Math.max(1, Math.abs(lo) * 0.1) : 0;
15
+ lo -= pad;
16
+ hi += pad;
17
+ return (_jsx("div", { ref: ref, "data-slot": "sparkline", "data-tone": tone, role: label ? "img" : undefined, "aria-label": label, "aria-hidden": label ? undefined : true, className: cn("sparkline", tone === "primary" && "primary", className), style: { height, ...style }, ...props, children: _jsx(ResponsiveContainer, { width: "100%", height: "100%", children: _jsxs(AreaChart, { data: values.map((value) => ({ value })), margin: { top: 2, right: 0, bottom: 2, left: 0 }, children: [_jsx(YAxis, { hide: true, domain: [lo, hi] }), _jsx(Area, { type: "monotone", dataKey: "value", stroke: "currentColor", strokeWidth: 1.5, fill: fill ? "currentColor" : "none", fillOpacity: fill ? 0.12 : 0, isAnimationActive: false, dot: false, activeDot: false })] }) }) }));
18
+ }
19
+ export { Sparkline };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momoi-labs/kiso-react",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Kiso React components built with shadcn/ui and Radix",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,10 +32,12 @@
32
32
  "prepack": "npm run build"
33
33
  },
34
34
  "dependencies": {
35
- "@momoi-labs/kiso": "^0.7.1",
35
+ "@momoi-labs/kiso": "^0.9.0",
36
36
  "class-variance-authority": "0.7.1",
37
37
  "clsx": "2.1.1",
38
- "radix-ui": "1.6.7"
38
+ "radix-ui": "1.6.7",
39
+ "react-is": "^19.3.0",
40
+ "recharts": "^3.10.1"
39
41
  },
40
42
  "peerDependencies": {
41
43
  "react": "^19.0.0",