@rovula/ui 0.1.40 → 0.1.42

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.
Files changed (30) hide show
  1. package/dist/cjs/bundle.css +3 -0
  2. package/dist/cjs/bundle.js +4 -4
  3. package/dist/cjs/bundle.js.map +1 -1
  4. package/dist/cjs/types/components/AutoComplete/AutoComplete.d.ts +74 -0
  5. package/dist/cjs/types/components/AutoComplete/AutoComplete.stories.d.ts +361 -0
  6. package/dist/cjs/types/components/AutoComplete/index.d.ts +2 -0
  7. package/dist/cjs/types/index.d.ts +3 -0
  8. package/dist/components/AutoComplete/AutoComplete.js +103 -0
  9. package/dist/components/AutoComplete/AutoComplete.stories.js +212 -0
  10. package/dist/components/AutoComplete/index.js +1 -0
  11. package/dist/components/Dialog/Dialog.js +5 -1
  12. package/dist/components/TextInput/TextInput.js +2 -1
  13. package/dist/esm/bundle.css +3 -0
  14. package/dist/esm/bundle.js +4 -4
  15. package/dist/esm/bundle.js.map +1 -1
  16. package/dist/esm/types/components/AutoComplete/AutoComplete.d.ts +74 -0
  17. package/dist/esm/types/components/AutoComplete/AutoComplete.stories.d.ts +361 -0
  18. package/dist/esm/types/components/AutoComplete/index.d.ts +2 -0
  19. package/dist/esm/types/index.d.ts +3 -0
  20. package/dist/index.d.ts +75 -2
  21. package/dist/index.js +2 -0
  22. package/dist/src/theme/global.css +35 -31
  23. package/package.json +1 -1
  24. package/src/components/AutoComplete/AutoComplete.stories.tsx +525 -0
  25. package/src/components/AutoComplete/AutoComplete.tsx +374 -0
  26. package/src/components/AutoComplete/index.ts +2 -0
  27. package/src/components/Dialog/Dialog.tsx +4 -0
  28. package/src/components/TextInput/TextInput.tsx +13 -8
  29. package/src/index.ts +3 -0
  30. package/src/theme/themes/variable.css +31 -31
@@ -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, { onCloseAutoFocus: (event) => {
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) {
@@ -1307,6 +1307,9 @@ input[type=number] {
1307
1307
  .max-w-md{
1308
1308
  max-width: 28rem;
1309
1309
  }
1310
+ .max-w-sm{
1311
+ max-width: 24rem;
1312
+ }
1310
1313
  .flex-1{
1311
1314
  flex: 1 1 0%;
1312
1315
  }