@momoi-labs/kiso-react 0.4.1 → 0.5.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
@@ -176,7 +181,7 @@ import { BrandMark, TerminalIcon } from '@momoi-labs/kiso-react';
176
181
  ## Development
177
182
 
178
183
  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
184
+ gallery opens at <http://127.0.0.1:5173/#components> and covers all 44 Kiso
180
185
  catalog entries, each rendering the component this package exports. Its sample
181
186
  data is simulated in memory.
182
187
 
@@ -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.5.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.8.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",