@rovula/ui 0.1.41 → 0.1.43
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/dist/cjs/bundle.css +3 -0
- package/dist/cjs/bundle.js +4 -4
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/AutoComplete/AutoComplete.d.ts +76 -0
- package/dist/cjs/types/components/AutoComplete/AutoComplete.stories.d.ts +362 -0
- package/dist/cjs/types/components/AutoComplete/index.d.ts +2 -0
- package/dist/cjs/types/index.d.ts +3 -0
- package/dist/components/AutoComplete/AutoComplete.js +103 -0
- package/dist/components/AutoComplete/AutoComplete.stories.js +212 -0
- package/dist/components/AutoComplete/index.js +1 -0
- package/dist/components/Dialog/Dialog.js +5 -1
- package/dist/components/TextInput/TextInput.js +2 -1
- package/dist/esm/bundle.css +3 -0
- package/dist/esm/bundle.js +4 -4
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/AutoComplete/AutoComplete.d.ts +76 -0
- package/dist/esm/types/components/AutoComplete/AutoComplete.stories.d.ts +362 -0
- package/dist/esm/types/components/AutoComplete/index.d.ts +2 -0
- package/dist/esm/types/index.d.ts +3 -0
- package/dist/index.d.ts +77 -2
- package/dist/index.js +2 -0
- package/dist/src/theme/global.css +4 -0
- package/package.json +1 -1
- package/src/components/AutoComplete/AutoComplete.stories.tsx +525 -0
- package/src/components/AutoComplete/AutoComplete.tsx +379 -0
- package/src/components/AutoComplete/index.ts +2 -0
- package/src/components/Dialog/Dialog.tsx +4 -0
- package/src/components/TextInput/TextInput.tsx +13 -8
- package/src/index.ts +3 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import * as yup from "yup";
|
|
4
|
+
import AutoComplete from "./AutoComplete";
|
|
5
|
+
import Avatar from "../Avatar/Avatar";
|
|
6
|
+
import Text from "../Text/Text";
|
|
7
|
+
import Button from "../Button/Button";
|
|
8
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogBody, DialogFooter, DialogTrigger, DialogClose, } from "../Dialog/Dialog";
|
|
9
|
+
import { Form } from "../Form/Form";
|
|
10
|
+
import { Field } from "../Form/Field";
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Meta
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const meta = {
|
|
15
|
+
title: "Components/AutoComplete",
|
|
16
|
+
component: AutoComplete,
|
|
17
|
+
tags: ["autodocs"],
|
|
18
|
+
parameters: {
|
|
19
|
+
layout: "fullscreen",
|
|
20
|
+
},
|
|
21
|
+
decorators: [
|
|
22
|
+
(Story) => (_jsx("div", { className: "p-8 bg-bg-bg1", children: _jsx(Story, {}) })),
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
export default meta;
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Shared fixtures
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
const fruits = [
|
|
30
|
+
{ value: "apple", label: "Apple" },
|
|
31
|
+
{ value: "banana", label: "Banana" },
|
|
32
|
+
{ value: "blueberry", label: "Blueberry" },
|
|
33
|
+
{ value: "cherry", label: "Cherry" },
|
|
34
|
+
{ value: "grape", label: "Grape" },
|
|
35
|
+
{ value: "mango", label: "Mango" },
|
|
36
|
+
{ value: "orange", label: "Orange" },
|
|
37
|
+
{ value: "peach", label: "Peach" },
|
|
38
|
+
{ value: "pineapple", label: "Pineapple" },
|
|
39
|
+
{ value: "strawberry", label: "Strawberry" },
|
|
40
|
+
];
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Default — basic client-side filter
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
export const Default = {
|
|
45
|
+
render: () => {
|
|
46
|
+
const [value, setValue] = useState("");
|
|
47
|
+
const filtered = fruits.filter((f) => f.label.toLowerCase().includes(value.toLowerCase()));
|
|
48
|
+
return (_jsx(AutoComplete, { label: "Fruit", value: value, options: filtered, onChange: setValue, onSearch: setValue, noOptionsText: "No fruit found", fullwidth: true, "data-testid": "autocomplete-default" }));
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// showNoOptions — display "no results" message when empty (like MUI)
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
export const ShowNoOptionsMessage = {
|
|
55
|
+
name: "Show No Options Message",
|
|
56
|
+
render: () => {
|
|
57
|
+
const [value, setValue] = useState("");
|
|
58
|
+
const filtered = fruits.filter((f) => f.label.toLowerCase().includes(value.toLowerCase()));
|
|
59
|
+
return (_jsxs("div", { className: "flex flex-col gap-6 w-full", children: [_jsxs("div", { children: [_jsx(Text, { variant: "small4", className: "text-text-g-contrast-medium mb-2", children: "showNoOptions=false (default) \u2014 popover hides when empty" }), _jsx(AutoComplete, { label: "Fruit (no message)", value: value, options: filtered, onChange: setValue, onSearch: setValue, showNoOptions: false, noOptionsText: "No fruit found", fullwidth: true })] }), _jsxs("div", { children: [_jsx(Text, { variant: "small4", className: "text-text-g-contrast-medium mb-2", children: "showNoOptions=true \u2014 shows \"No fruit found\" when no match" }), _jsx(AutoComplete, { label: "Fruit (with message)", value: value, options: filtered, onChange: setValue, onSearch: setValue, showNoOptions: true, noOptionsText: "No fruit found", fullwidth: true })] })] }));
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// FreeSolo — typed value is committed even without selecting from list
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
export const FreeSolo = {
|
|
66
|
+
name: "Free Solo (typed value always committed)",
|
|
67
|
+
render: () => {
|
|
68
|
+
const [value, setValue] = useState("");
|
|
69
|
+
const [committed, setCommitted] = useState("");
|
|
70
|
+
const filtered = fruits.filter((f) => f.label.toLowerCase().includes(value.toLowerCase()));
|
|
71
|
+
return (_jsxs("div", { className: "flex flex-col gap-4 w-full", children: [_jsx(AutoComplete, { label: "Email or name", value: value, options: filtered, onChange: (v) => {
|
|
72
|
+
setValue(v);
|
|
73
|
+
setCommitted(v);
|
|
74
|
+
}, onSearch: setValue, showNoOptions: true, noOptionsText: "No match \u2014 your typed value will be used", fullwidth: true }), _jsxs(Text, { variant: "small2", className: "text-text-g-contrast-medium", children: ["Committed value: ", _jsx("strong", { children: committed || "—" })] })] }));
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Async — simulates server search with debounce
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
const allUsers = [
|
|
81
|
+
{ value: "alice@co.com", label: "alice@co.com", name: "Alice" },
|
|
82
|
+
{ value: "bob@co.com", label: "bob@co.com", name: "Bob" },
|
|
83
|
+
{ value: "charlie@co.com", label: "charlie@co.com", name: "Charlie" },
|
|
84
|
+
{ value: "diana@co.com", label: "diana@co.com", name: "Diana" },
|
|
85
|
+
];
|
|
86
|
+
const AsyncAutoComplete = () => {
|
|
87
|
+
const [value, setValue] = useState("");
|
|
88
|
+
const [options, setOptions] = useState(allUsers);
|
|
89
|
+
const [loading, setLoading] = useState(false);
|
|
90
|
+
const handleSearch = (query) => {
|
|
91
|
+
setLoading(true);
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
setOptions(query
|
|
94
|
+
? allUsers.filter((u) => u.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
95
|
+
u.value.toLowerCase().includes(query.toLowerCase()))
|
|
96
|
+
: allUsers);
|
|
97
|
+
setLoading(false);
|
|
98
|
+
}, 600);
|
|
99
|
+
};
|
|
100
|
+
return (_jsx(AutoComplete, { label: "Search user", value: value, options: options, loading: loading, onChange: setValue, onSearch: handleSearch, onSelect: (option) => setValue(option.value), filterOptions: (x) => x, showNoOptions: true, noOptionsText: "No users found", renderOption: (option) => (_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Avatar, { type: "text", text: option.name, className: "size-7 shrink-0 text-xs" }), _jsxs("div", { className: "flex flex-col min-w-0", children: [_jsx(Text, { variant: "subtitle4", className: "truncate", children: option.name }), _jsx(Text, { variant: "small2", className: "text-text-g-contrast-medium truncate", children: option.value })] })] })), fullwidth: true, "data-testid": "autocomplete-async" }));
|
|
101
|
+
};
|
|
102
|
+
export const Async = {
|
|
103
|
+
name: "Async Search (simulated API)",
|
|
104
|
+
render: () => _jsx(AsyncAutoComplete, {}),
|
|
105
|
+
};
|
|
106
|
+
const inviteSchema = yup.object({
|
|
107
|
+
email: yup
|
|
108
|
+
.string()
|
|
109
|
+
.email("Invalid email format")
|
|
110
|
+
.required("Email is required"),
|
|
111
|
+
assignee: yup.string().required("Please select an assignee"),
|
|
112
|
+
});
|
|
113
|
+
const FormIntegrationDemo = () => {
|
|
114
|
+
const [submitted, setSubmitted] = useState(null);
|
|
115
|
+
const [assigneeQuery, setAssigneeQuery] = useState("");
|
|
116
|
+
const assigneeOptions = allUsers
|
|
117
|
+
.filter((u) => !assigneeQuery ||
|
|
118
|
+
u.name.toLowerCase().includes(assigneeQuery.toLowerCase()) ||
|
|
119
|
+
u.value.toLowerCase().includes(assigneeQuery.toLowerCase()))
|
|
120
|
+
.map((u) => ({ value: u.value, label: u.name }));
|
|
121
|
+
return (_jsxs("div", { className: "flex flex-col gap-4 w-full max-w-sm", children: [_jsxs(Form, { className: "flex flex-col gap-4", defaultValues: { email: "", assignee: "" }, validationSchema: inviteSchema, mode: "onBlur", onSubmit: (values) => setSubmitted(values), children: [_jsx(Field, { name: "email", component: AutoComplete, componentProps: {
|
|
122
|
+
label: "Email",
|
|
123
|
+
placeholder: "Search by email",
|
|
124
|
+
options: allUsers
|
|
125
|
+
.filter((u) => !assigneeQuery || u.value.includes(assigneeQuery))
|
|
126
|
+
.map((u) => ({ value: u.value, label: u.value })),
|
|
127
|
+
onSearch: setAssigneeQuery,
|
|
128
|
+
showNoOptions: true,
|
|
129
|
+
noOptionsText: "No matching email",
|
|
130
|
+
required: true,
|
|
131
|
+
fullwidth: true,
|
|
132
|
+
} }), _jsx(Field, { name: "assignee", component: AutoComplete, componentProps: {
|
|
133
|
+
label: "Assignee",
|
|
134
|
+
placeholder: "Search by name",
|
|
135
|
+
options: assigneeOptions,
|
|
136
|
+
onSearch: setAssigneeQuery,
|
|
137
|
+
showNoOptions: true,
|
|
138
|
+
noOptionsText: "No users found",
|
|
139
|
+
required: true,
|
|
140
|
+
fullwidth: true,
|
|
141
|
+
} }), _jsx(Button, { type: "submit", fullwidth: true, children: "Submit" })] }), submitted && (_jsxs("div", { className: "mt-2 p-3 rounded-md bg-bg-bg2 typography-small2 text-text-g-contrast-medium", children: ["Submitted: ", _jsx("strong", { children: JSON.stringify(submitted) })] }))] }));
|
|
142
|
+
};
|
|
143
|
+
export const WithFormValidation = {
|
|
144
|
+
name: "Form — react-hook-form + yup + Field",
|
|
145
|
+
render: () => _jsx(FormIntegrationDemo, {}),
|
|
146
|
+
};
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Inside Dialog — portal + overflow escape
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
const dialogAssignees = allUsers.map((u) => ({
|
|
151
|
+
value: u.value,
|
|
152
|
+
label: u.name,
|
|
153
|
+
}));
|
|
154
|
+
const AutoCompleteInDialogDemo = () => {
|
|
155
|
+
const [query, setQuery] = useState("");
|
|
156
|
+
const [value, setValue] = useState("");
|
|
157
|
+
const options = dialogAssignees.filter((o) => !query ||
|
|
158
|
+
o.label.toLowerCase().includes(query.toLowerCase()) ||
|
|
159
|
+
o.value.toLowerCase().includes(query.toLowerCase()));
|
|
160
|
+
return (_jsxs(Dialog, { children: [_jsx(DialogTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", children: "Open Dialog" }) }), _jsxs(DialogContent, { showCloseButton: true, children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: "Assign Task" }), _jsx(DialogDescription, { children: "AutoComplete inside a Dialog \u2014 popover escapes overflow correctly." })] }), _jsx(DialogBody, { className: "gap-4 py-2", children: _jsx(AutoComplete, { label: "Assignee", placeholder: "Search by name or email", value: value, options: options, onChange: setValue, onSearch: setQuery, onSelect: (opt) => setValue(opt.value), filterOptions: (x) => x, showNoOptions: true, noOptionsText: "No users found", portal: false, required: true, fullwidth: true }) }), _jsxs(DialogFooter, { children: [_jsx(DialogClose, { asChild: true, children: _jsx(Button, { variant: "outline", children: "Cancel" }) }), _jsx(Button, { disabled: !value, children: "Confirm" })] })] })] }));
|
|
161
|
+
};
|
|
162
|
+
export const InsideDialog = {
|
|
163
|
+
name: "Inside Dialog",
|
|
164
|
+
render: () => _jsx(AutoCompleteInDialogDemo, {}),
|
|
165
|
+
};
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Sizes
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
export const Sizes = {
|
|
170
|
+
render: () => {
|
|
171
|
+
const [sm, setSm] = useState("");
|
|
172
|
+
const [md, setMd] = useState("");
|
|
173
|
+
const [lg, setLg] = useState("");
|
|
174
|
+
const getOptions = (v) => fruits.filter((f) => f.label.toLowerCase().includes(v.toLowerCase()));
|
|
175
|
+
return (_jsx("div", { className: "flex flex-col gap-6 w-full", children: [
|
|
176
|
+
{ size: "sm", value: sm, onChange: setSm },
|
|
177
|
+
{ size: "md", value: md, onChange: setMd },
|
|
178
|
+
{ size: "lg", value: lg, onChange: setLg },
|
|
179
|
+
].map(({ size, value, onChange }) => (_jsx(AutoComplete, { label: `Size: ${size}`, size: size, value: value, options: getOptions(value), onChange: onChange, onSearch: onChange, noOptionsText: "No fruit found", fullwidth: true }, size))) }));
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// States — error, disabled
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
export const States = {
|
|
186
|
+
render: () => (_jsxs("div", { className: "flex flex-col gap-6 w-full", children: [_jsx(AutoComplete, { label: "Error state", value: "", options: fruits, error: true, errorMessage: "Please enter a valid email", fullwidth: true }), _jsx(AutoComplete, { label: "Disabled state", value: "locked@example.com", options: fruits, disabled: true, fullwidth: true })] })),
|
|
187
|
+
};
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Loading state
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
export const LoadingState = {
|
|
192
|
+
name: "Loading State",
|
|
193
|
+
render: () => {
|
|
194
|
+
const [value, setValue] = useState("j");
|
|
195
|
+
return (_jsx(AutoComplete, { label: "Searching\u2026", value: value, options: [], loading: true, onChange: setValue, noOptionsText: "No results", fullwidth: true }));
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// Keyboard navigation
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
export const KeyboardNavigation = {
|
|
202
|
+
name: "Keyboard Navigation (ArrowUp/Down, Enter, Escape)",
|
|
203
|
+
render: () => {
|
|
204
|
+
const [value, setValue] = useState("");
|
|
205
|
+
const [selected, setSelected] = useState(null);
|
|
206
|
+
const filtered = fruits.filter((f) => f.label.toLowerCase().includes(value.toLowerCase()));
|
|
207
|
+
return (_jsxs("div", { className: "flex flex-col gap-4 w-full", children: [_jsx(AutoComplete, { label: "Pick a fruit (keyboard)", value: value, options: filtered, onChange: setValue, onSearch: setValue, onSelect: (opt) => {
|
|
208
|
+
setValue(opt.label);
|
|
209
|
+
setSelected(opt);
|
|
210
|
+
}, noOptionsText: "No fruit found", fullwidth: true }), selected && (_jsxs(Text, { variant: "small2", className: "text-text-g-contrast-medium", children: ["Selected: ", _jsx("strong", { children: selected.label })] }))] }));
|
|
211
|
+
},
|
|
212
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./AutoComplete";
|
|
@@ -26,7 +26,11 @@ const DialogOverlay = React.forwardRef((_a, ref) => {
|
|
|
26
26
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
|
27
27
|
const DialogContent = React.forwardRef((_a, ref) => {
|
|
28
28
|
var { className, children, showCloseButton = false, closeButtonClassName } = _a, props = __rest(_a, ["className", "children", "showCloseButton", "closeButtonClassName"]);
|
|
29
|
-
return (_jsxs(DialogPortal, { children: [_jsx(DialogOverlay, {}), _jsxs(DialogPrimitive.Content, Object.assign({ ref: ref, className: cn("fixed left-[50%] top-[50%] z-50 flex w-[calc(100%-32px)] max-w-[650px] translate-x-[-50%] translate-y-[-50%] flex-col gap-6 rounded-md bg-modal-surface p-8 text-text-g-contrast-medium shadow-[0px_12px_24px_-4px_rgba(0,0,0,0.12)] duration-200 focus:outline-none focus-visible:outline-none outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]", className) }, props, {
|
|
29
|
+
return (_jsxs(DialogPortal, { children: [_jsx(DialogOverlay, {}), _jsxs(DialogPrimitive.Content, Object.assign({ ref: ref, className: cn("fixed left-[50%] top-[50%] z-50 flex w-[calc(100%-32px)] max-w-[650px] translate-x-[-50%] translate-y-[-50%] flex-col gap-6 rounded-md bg-modal-surface p-8 text-text-g-contrast-medium shadow-[0px_12px_24px_-4px_rgba(0,0,0,0.12)] duration-200 focus:outline-none focus-visible:outline-none outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]", className) }, props, { onOpenAutoFocus: (event) => {
|
|
30
|
+
var _a;
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
(_a = props === null || props === void 0 ? void 0 : props.onOpenAutoFocus) === null || _a === void 0 ? void 0 : _a.call(props, event);
|
|
33
|
+
}, onCloseAutoFocus: (event) => {
|
|
30
34
|
var _a;
|
|
31
35
|
event.preventDefault();
|
|
32
36
|
document.body.style.pointerEvents = "auto";
|
|
@@ -118,7 +118,8 @@ export const TextInput = forwardRef((_a, ref) => {
|
|
|
118
118
|
const displayValue = format && typeof props.value === "string"
|
|
119
119
|
? format(props.value)
|
|
120
120
|
: props.value;
|
|
121
|
-
const handleClearInput = useCallback(() => {
|
|
121
|
+
const handleClearInput = useCallback((e) => {
|
|
122
|
+
e.preventDefault();
|
|
122
123
|
if (inputRef.current) {
|
|
123
124
|
inputRef.current.value = "";
|
|
124
125
|
if (props.onChange) {
|