@rovula/ui 0.0.29 → 0.0.31

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 (77) hide show
  1. package/dist/cjs/bundle.css +139 -69
  2. package/dist/cjs/bundle.js +3 -3
  3. package/dist/cjs/bundle.js.map +1 -1
  4. package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +12 -0
  5. package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +18 -0
  6. package/dist/cjs/types/components/InputFilter/InputFilter.d.ts +64 -0
  7. package/dist/cjs/types/components/InputFilter/InputFilter.stories.d.ts +370 -0
  8. package/dist/cjs/types/components/InputFilter/InputFilter.styles.d.ts +8 -0
  9. package/dist/cjs/types/components/Search/Search.d.ts +2 -20
  10. package/dist/cjs/types/components/Search/Search.stories.d.ts +26 -38
  11. package/dist/cjs/types/components/TextInput/TextInput.d.ts +10 -0
  12. package/dist/cjs/types/components/TextInput/TextInput.stories.d.ts +10 -0
  13. package/dist/cjs/types/components/TextInput/TextInput.styles.d.ts +1 -0
  14. package/dist/cjs/types/index.d.ts +1 -0
  15. package/dist/components/ActionButton/ActionButton.stories.js +1 -1
  16. package/dist/components/Checkbox/Checkbox.js +3 -3
  17. package/dist/components/Checkbox/Checkbox.stories.js +1 -1
  18. package/dist/components/DatePicker/DatePicker.js +2 -1
  19. package/dist/components/Dropdown/Dropdown.js +34 -19
  20. package/dist/components/Dropdown/Dropdown.stories.js +1 -0
  21. package/dist/components/Dropdown/Dropdown.styles.js +1 -1
  22. package/dist/components/InputFilter/InputFilter.js +124 -0
  23. package/dist/components/InputFilter/InputFilter.stories.js +34 -0
  24. package/dist/components/InputFilter/InputFilter.styles.js +65 -0
  25. package/dist/components/RadioGroup/RadioGroup.js +5 -2
  26. package/dist/components/RadioGroup/RadioGroup.stories.js +1 -1
  27. package/dist/components/Search/Search.js +1 -1
  28. package/dist/components/Search/Search.stories.js +2 -1
  29. package/dist/components/TextInput/TextInput.js +23 -6
  30. package/dist/components/TextInput/TextInput.styles.js +94 -20
  31. package/dist/esm/bundle.css +139 -69
  32. package/dist/esm/bundle.js +3 -3
  33. package/dist/esm/bundle.js.map +1 -1
  34. package/dist/esm/types/components/Dropdown/Dropdown.d.ts +12 -0
  35. package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +18 -0
  36. package/dist/esm/types/components/InputFilter/InputFilter.d.ts +64 -0
  37. package/dist/esm/types/components/InputFilter/InputFilter.stories.d.ts +370 -0
  38. package/dist/esm/types/components/InputFilter/InputFilter.styles.d.ts +8 -0
  39. package/dist/esm/types/components/Search/Search.d.ts +2 -20
  40. package/dist/esm/types/components/Search/Search.stories.d.ts +26 -38
  41. package/dist/esm/types/components/TextInput/TextInput.d.ts +10 -0
  42. package/dist/esm/types/components/TextInput/TextInput.stories.d.ts +10 -0
  43. package/dist/esm/types/components/TextInput/TextInput.styles.d.ts +1 -0
  44. package/dist/esm/types/index.d.ts +1 -0
  45. package/dist/index.d.ts +82 -16
  46. package/dist/index.js +1 -0
  47. package/dist/src/theme/global.css +278 -161
  48. package/dist/theme/presets/colors.js +21 -0
  49. package/dist/theme/themes/xspector/color.css +13 -0
  50. package/dist/theme/themes/xspector/components/action-button.css +44 -42
  51. package/dist/theme/themes/xspector/state.css +1 -1
  52. package/dist/theme/tokens/color.css +13 -0
  53. package/dist/theme/tokens/components/action-button.css +42 -42
  54. package/package.json +1 -1
  55. package/src/components/ActionButton/ActionButton.stories.tsx +1 -1
  56. package/src/components/Checkbox/Checkbox.stories.tsx +1 -1
  57. package/src/components/Checkbox/Checkbox.tsx +4 -4
  58. package/src/components/DatePicker/DatePicker.tsx +4 -2
  59. package/src/components/Dropdown/Dropdown.stories.tsx +1 -0
  60. package/src/components/Dropdown/Dropdown.styles.ts +1 -1
  61. package/src/components/Dropdown/Dropdown.tsx +69 -38
  62. package/src/components/InputFilter/InputFilter.stories.tsx +72 -0
  63. package/src/components/InputFilter/InputFilter.styles.ts +74 -0
  64. package/src/components/InputFilter/InputFilter.tsx +314 -0
  65. package/src/components/RadioGroup/RadioGroup.stories.tsx +1 -1
  66. package/src/components/RadioGroup/RadioGroup.tsx +7 -9
  67. package/src/components/Search/Search.stories.tsx +3 -2
  68. package/src/components/Search/Search.tsx +13 -2
  69. package/src/components/TextInput/TextInput.styles.ts +94 -20
  70. package/src/components/TextInput/TextInput.tsx +53 -11
  71. package/src/index.ts +1 -0
  72. package/src/theme/presets/colors.js +21 -0
  73. package/src/theme/themes/xspector/color.css +13 -0
  74. package/src/theme/themes/xspector/components/action-button.css +44 -42
  75. package/src/theme/themes/xspector/state.css +1 -1
  76. package/src/theme/tokens/color.css +13 -0
  77. package/src/theme/tokens/components/action-button.css +42 -42
@@ -0,0 +1,72 @@
1
+ import React, { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { InputFilter, Options } from "./InputFilter";
4
+
5
+ const meta = {
6
+ title: "Components/InputFilter",
7
+ component: InputFilter,
8
+ tags: ["autodocs"],
9
+ parameters: {
10
+ layout: "fullscreen",
11
+ },
12
+ decorators: [
13
+ (Story) => (
14
+ <div className="p-5 flex w-full bg-base-bg2">
15
+ <Story />
16
+ </div>
17
+ ),
18
+ ],
19
+ } satisfies Meta<typeof InputFilter>;
20
+
21
+ export default meta;
22
+
23
+ const options: Options[] = new Array(100).fill("").map((__, index) => ({
24
+ value: `option${index + 1}`,
25
+ label: `Option ${index + 1}`,
26
+ }));
27
+
28
+ export const Default = {
29
+ args: {
30
+ label: "Choose an option:",
31
+ fullwidth: true,
32
+ disabled: true,
33
+ options,
34
+ },
35
+ render: (args) => {
36
+ const [value, setValue] = useState<Options[] | undefined>([]);
37
+
38
+ const handleOnSelect = (values: Options[]) => {
39
+ setValue(values);
40
+ };
41
+
42
+ return (
43
+ <div className="flex flex-row gap-4 w-full">
44
+ <InputFilter
45
+ id="1"
46
+ size="lg"
47
+ options={options}
48
+ values={value}
49
+ onSelect={handleOnSelect}
50
+ optionContainerClassName="h-[400px]"
51
+ {...args}
52
+ />
53
+ <InputFilter
54
+ id="2"
55
+ size="md"
56
+ options={options}
57
+ values={value}
58
+ onSelect={handleOnSelect}
59
+ {...args}
60
+ />
61
+ <InputFilter
62
+ id="3"
63
+ size="sm"
64
+ options={options}
65
+ values={value}
66
+ onSelect={handleOnSelect}
67
+ {...args}
68
+ />
69
+ </div>
70
+ );
71
+ },
72
+ } satisfies StoryObj;
@@ -0,0 +1,74 @@
1
+ import React from "react";
2
+ import { cva } from "class-variance-authority";
3
+
4
+ export const filterIconVariant = cva(
5
+ [
6
+ // Base styles
7
+ "absolute contents items-center justify-center cursor-pointer",
8
+
9
+ // Border styles
10
+ "border-l border-l-input-default-stroke",
11
+ "peer-hover:border-l-input-active-stroke",
12
+ "peer-focus:border-l-input-active-stroke",
13
+
14
+ // Fill styles
15
+ "fill-primary",
16
+ "peer-hover:fill-input-filled-text",
17
+ "peer-focus:fill-input-filled-text",
18
+
19
+ // Stroke styles
20
+ "stroke-input-default-stroke",
21
+ "peer-hover:stroke-input-active-stroke",
22
+ "peer-focus:stroke-input-filled-text",
23
+ ],
24
+ {
25
+ variants: {
26
+ size: {
27
+ sm: "p-1 size-[30px]",
28
+ md: "p-2 size-[38px]",
29
+ lg: "p-3 size-14",
30
+ },
31
+ rounded: {
32
+ none: "rounded-r-none",
33
+ normal: "rounded-r-xl",
34
+ full: "rounded-r-full",
35
+ },
36
+ error: {
37
+ true: "border-l-input-error",
38
+ },
39
+ position: {
40
+ start: "inset-y-0 left-0",
41
+ end: "inset-y-0 right-0",
42
+ },
43
+ active: {
44
+ false: "",
45
+ true: [
46
+ // Fill styles
47
+ "fill-primary-default",
48
+ "peer-hover:fill-primary-default", // TODO wait for refactor color after change function button colors
49
+ "peer-focus:fill-primary-hover", // TODO wait for refactor color after change function button colors
50
+
51
+ // Stroke styles
52
+ "stroke-primary-default",
53
+ "peer-hover:stroke-primary-default",
54
+ "peer-focus:stroke-primary-hover",
55
+ ],
56
+ },
57
+ disabled: {
58
+ true: [
59
+ "border-l-input-disable-stroke",
60
+ "fill-input-disable-stroke",
61
+ "stroke-input-disable-stroke",
62
+ ],
63
+ },
64
+ },
65
+ defaultVariants: {
66
+ size: "md",
67
+ rounded: "normal",
68
+ error: false,
69
+ position: "end",
70
+ active: false,
71
+ disabled: false,
72
+ },
73
+ }
74
+ );
@@ -0,0 +1,314 @@
1
+ import React, {
2
+ Fragment,
3
+ ReactNode,
4
+ forwardRef,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from "react";
11
+
12
+ import TextInput, { InputProps } from "../TextInput/TextInput";
13
+ import { cn } from "@/utils/cn";
14
+ import { customInputVariant } from "../Dropdown/Dropdown.styles";
15
+ import Icon from "../Icon/Icon";
16
+ import { filterIconVariant } from "./InputFilter.styles";
17
+ import { Checkbox } from "../Checkbox/Checkbox";
18
+
19
+ type RenderLabelCallbackArg = {
20
+ value: string;
21
+ label: string;
22
+ handleOnClick: () => void;
23
+ className: string;
24
+ };
25
+
26
+ export type Options = {
27
+ value: string;
28
+ label: string;
29
+ renderLabel?: (config: RenderLabelCallbackArg) => ReactNode;
30
+ };
31
+
32
+ export type InputFilterProps = {
33
+ id?: string;
34
+ label?: string;
35
+ size?: "sm" | "md" | "lg";
36
+ rounded?: "none" | "normal" | "full";
37
+ variant?: "flat" | "outline" | "underline";
38
+ helperText?: string;
39
+ errorMessage?: string;
40
+ filterMode?: boolean;
41
+ fullwidth?: boolean;
42
+ disabled?: boolean;
43
+ error?: boolean;
44
+ required?: boolean;
45
+ className?: string;
46
+ optionContainerClassName?: string;
47
+ options: Options[];
48
+ values?: Options[];
49
+ onChangeText?: InputProps["onChange"];
50
+ onSelect?: (values: Options[]) => void;
51
+ renderOptions?: (value: {
52
+ optionsFiltered: Options[];
53
+ selectedOptions: Options[];
54
+ onClick: (option: Options) => void;
55
+ }) => ReactNode;
56
+ } & Omit<InputProps, "value" | "onSelect">;
57
+
58
+ const InputFilter = forwardRef<HTMLInputElement, InputFilterProps>(
59
+ (
60
+ {
61
+ id,
62
+ options = [],
63
+ values = [],
64
+ label,
65
+ size = "md",
66
+ rounded = "normal",
67
+ variant = "outline",
68
+ helperText,
69
+ errorMessage,
70
+ fullwidth = true,
71
+ disabled = false,
72
+ error = false,
73
+ filterMode = false,
74
+ required = true,
75
+ onChangeText,
76
+ onSelect,
77
+ renderOptions: customRenderOptions,
78
+ optionContainerClassName,
79
+ ...props
80
+ },
81
+ ref
82
+ ) => {
83
+ const _id = id || `${label}-select`;
84
+
85
+ const [isFocused, setIsFocused] = useState(false);
86
+ const [selectedOptions, setSelectedOptions] = useState<Options[]>(values);
87
+ const [textValue, setTextValue] = useState("");
88
+ const keyCode = useRef("");
89
+ const containerRef = useRef<HTMLDivElement>(null);
90
+
91
+ useEffect(() => {
92
+ setSelectedOptions(values ?? []);
93
+ setTextValue(values?.map((option) => option.label).join(", ") ?? "");
94
+ }, [values]);
95
+
96
+ useEffect(() => {
97
+ const handleClickOutside = (event: MouseEvent) => {
98
+ if (
99
+ containerRef.current &&
100
+ !containerRef.current.contains(event.target as Node)
101
+ ) {
102
+ setIsFocused(false);
103
+ }
104
+ };
105
+
106
+ document.addEventListener("mousedown", handleClickOutside);
107
+ return () =>
108
+ document.removeEventListener("mousedown", handleClickOutside);
109
+ }, []);
110
+
111
+ const handleOnChangeText = useCallback(
112
+ (event: React.ChangeEvent<HTMLInputElement>) => {
113
+ onChangeText?.(event);
114
+ setTextValue(event.target.value);
115
+
116
+ if (!event.target.value) {
117
+ clearMismatchValues(event as any);
118
+ }
119
+ },
120
+ [onChangeText]
121
+ );
122
+
123
+ const handleOptionClick = useCallback(
124
+ (option: Options) => {
125
+ const isSelected = selectedOptions.some(
126
+ (selected) => selected.value === option.value
127
+ );
128
+ let newSelectedOptions = [...selectedOptions];
129
+
130
+ if (isSelected) {
131
+ newSelectedOptions = newSelectedOptions.filter(
132
+ (selected) => selected.value !== option.value
133
+ );
134
+ } else {
135
+ newSelectedOptions.push(option);
136
+ }
137
+
138
+ setSelectedOptions(newSelectedOptions);
139
+ setTextValue(
140
+ newSelectedOptions.map((option) => option.label).join(", ")
141
+ );
142
+ onSelect?.(newSelectedOptions);
143
+ },
144
+ [selectedOptions, onSelect]
145
+ );
146
+
147
+ const optionsFiltered = useMemo(() => {
148
+ const lastText = textValue?.split(",").pop();
149
+ const filterText = lastText ?? "";
150
+
151
+ return options.filter(
152
+ (option) =>
153
+ !filterMode ||
154
+ option.label?.toLowerCase().includes(filterText?.toLowerCase())
155
+ );
156
+ }, [options, filterMode, textValue]);
157
+
158
+ const renderOptions = () => {
159
+ if (customRenderOptions) {
160
+ return customRenderOptions({
161
+ optionsFiltered,
162
+ selectedOptions,
163
+ onClick: handleOptionClick,
164
+ });
165
+ }
166
+
167
+ return (
168
+ <ul
169
+ className={cn(
170
+ "absolute mt-1 w-full bg-base-popup border border-base-popup text-base-popup-foreground rounded-md shadow-md z-10 max-h-60 overflow-y-auto",
171
+ optionContainerClassName
172
+ )}
173
+ >
174
+ {optionsFiltered.map((option) => {
175
+ if (option.renderLabel) {
176
+ return (
177
+ <Fragment key={option.value}>
178
+ {option.renderLabel({
179
+ value: option.value,
180
+ label: option.label,
181
+ handleOnClick: () => handleOptionClick(option),
182
+ className: `p-4 typography-subtitile4 hover:bg-gray-100 cursor-pointer flex items-center gap-3 ${
183
+ selectedOptions.some(
184
+ (selected) => selected.value === option.value
185
+ )
186
+ ? "bg-gray-200"
187
+ : ""
188
+ }`,
189
+ })}
190
+ </Fragment>
191
+ );
192
+ }
193
+ return (
194
+ <li
195
+ key={option.value}
196
+ onMouseDown={() => handleOptionClick(option)}
197
+ className={`p-4 typography-subtitile4 hover:bg-primary-hover-bg cursor-pointer flex items-center gap-3 ${
198
+ selectedOptions.some(
199
+ (selected) => selected.value === option.value
200
+ )
201
+ ? "bg-base-popup-highlight"
202
+ : ""
203
+ }`}
204
+ >
205
+ <Checkbox
206
+ checked={selectedOptions.some(
207
+ (selected) => selected.value === option.value
208
+ )}
209
+ />
210
+ {option.label}
211
+ </li>
212
+ );
213
+ })}
214
+ {optionsFiltered.length === 0 && (
215
+ <li className="px-4 py-14 text-center text-input-text">
216
+ Not found
217
+ </li>
218
+ )}
219
+ </ul>
220
+ );
221
+ };
222
+
223
+ const handleOnFocus = useCallback(
224
+ (e: React.FocusEvent<HTMLInputElement, Element>) => {
225
+ setIsFocused(true);
226
+ props?.onFocus?.(e);
227
+ },
228
+ [props?.onFocus]
229
+ );
230
+
231
+ const clearMismatchValues = useCallback(
232
+ (e: React.FocusEvent<HTMLInputElement, Element>) => {
233
+ const matchSelectedValues = optionsFiltered.filter(
234
+ (opt) =>
235
+ opt.value === e.target?.value || opt.label === e.target?.value
236
+ );
237
+
238
+ if (keyCode.current === "Enter") {
239
+ return;
240
+ }
241
+
242
+ setSelectedOptions(matchSelectedValues);
243
+ setTextValue(
244
+ matchSelectedValues.map((option) => option.label).join(", ")
245
+ );
246
+ onSelect?.(matchSelectedValues);
247
+ },
248
+ [optionsFiltered, textValue]
249
+ );
250
+
251
+ const handleOnKeyDown = useCallback(
252
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
253
+ keyCode.current = e.code;
254
+ props?.onKeyDown?.(e);
255
+ },
256
+ [props?.onKeyDown]
257
+ );
258
+
259
+ const filterIconClassName = filterIconVariant({
260
+ size: size,
261
+ rounded: rounded,
262
+ error: error,
263
+ active: !!values.length,
264
+ disabled,
265
+ });
266
+
267
+ return (
268
+ <div
269
+ ref={containerRef}
270
+ className={`relative ${fullwidth ? "w-full" : ""}`}
271
+ >
272
+ <TextInput
273
+ hasClearIcon={false}
274
+ endIcon={
275
+ <div className={filterIconClassName}>
276
+ <Icon
277
+ type="heroicons"
278
+ name="adjustments-horizontal"
279
+ variant="outline"
280
+ color="inherit"
281
+ stroke="inherit"
282
+ fill="transparent"
283
+ />
284
+ </div>
285
+ }
286
+ {...props}
287
+ ref={ref}
288
+ readOnly={!filterMode}
289
+ value={textValue}
290
+ onChange={handleOnChangeText}
291
+ label={label}
292
+ placeholder=" "
293
+ type="text"
294
+ rounded={rounded}
295
+ variant={variant}
296
+ helperText={helperText}
297
+ errorMessage={errorMessage}
298
+ fullwidth={fullwidth}
299
+ error={error}
300
+ required={required}
301
+ id={_id}
302
+ disabled={disabled}
303
+ size={size}
304
+ className={customInputVariant({ size })}
305
+ onFocus={handleOnFocus}
306
+ onKeyDown={handleOnKeyDown}
307
+ />
308
+ {isFocused && renderOptions()}
309
+ </div>
310
+ );
311
+ }
312
+ );
313
+
314
+ export { InputFilter };
@@ -11,7 +11,7 @@ const meta = {
11
11
  },
12
12
  decorators: [
13
13
  (Story) => (
14
- <div className="p-5 flex w-full">
14
+ <div className="p-5 flex w-full bg-base-bg2">
15
15
  <Story />
16
16
  </div>
17
17
  ),
@@ -1,8 +1,5 @@
1
- "use client";
2
-
3
1
  import * as React from "react";
4
2
  import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
5
-
6
3
  import { cn } from "@/utils/cn";
7
4
 
8
5
  const RadioGroup = React.forwardRef<
@@ -27,18 +24,19 @@ const RadioGroupItem = React.forwardRef<
27
24
  <RadioGroupPrimitive.Item
28
25
  ref={ref}
29
26
  className={cn(
30
- "aspect-square box-border size-4 rounded-full border border-primary text-primary",
31
- "hover:border-primary-hover",
27
+ "aspect-square box-border size-4 rounded-full border border-function-default-solid text-function-default-solid",
28
+ "hover:border-function-default-hover",
32
29
  "focus:outline-none",
33
- "data-[state=checked]:border-secondary data-[state=checked]:text-secondary",
34
- "hover:data-[state=checked]:border-secondary-hover hover:data-[state=checked]:text-secondary-hover",
35
- "data-[disabled]:border-state-disable-solid data-[disabled]:fill-state-disable-solid data-[disabled]:cursor-not-allowed data-[disabled]:pointer-events-none data-[disabled]:text-state-disable-solid",
30
+ // Disabled state styles
31
+ "data-[disabled]:!border-state-disable-solid data-[disabled]:!fill-state-disable-solid data-[disabled]:!cursor-not-allowed data-[disabled]:!pointer-events-none data-[disabled]:!text-state-disable-solid",
32
+ // Checked state styles
33
+ "data-[state=checked]:border-function-active-solid data-[state=checked]:text-function-active-solid",
34
+ "hover:data-[state=checked]:border-function-active-hover hover:data-[state=checked]:text-function-active-hover",
36
35
  className
37
36
  )}
38
37
  {...props}
39
38
  >
40
39
  <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
41
- {/* <div className="bg-primary size-2.5 rounded-full" /> */}
42
40
  <svg
43
41
  width="10"
44
42
  height="10"
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import React, { useState } from "react";
2
2
  import type { Meta, StoryObj } from "@storybook/react";
3
3
  import { Search } from "./Search";
4
4
  import { Options } from "../Dropdown/Dropdown";
@@ -12,7 +12,7 @@ const meta = {
12
12
  },
13
13
  decorators: [
14
14
  (Story) => (
15
- <div className="p-5 flex w-full">
15
+ <div className="p-5 flex w-full bg-base-bg2">
16
16
  <Story />
17
17
  </div>
18
18
  ),
@@ -30,6 +30,7 @@ export const Default = {
30
30
  args: {
31
31
  label: "Choose an option:",
32
32
  fullwidth: true,
33
+ size: "md",
33
34
  options,
34
35
  },
35
36
  render: (args) => {
@@ -1,18 +1,29 @@
1
1
  import React, { forwardRef } from "react";
2
2
  import Dropdown, { DropdownProps } from "../Dropdown/Dropdown";
3
3
 
4
- export type SearchProps = DropdownProps;
4
+ export type SearchProps = Omit<
5
+ DropdownProps,
6
+ | "isFloatingLabel"
7
+ | "keepCloseIconOnValue"
8
+ | "hasClearIcon"
9
+ | "hasSearchIcon"
10
+ | "endIcon"
11
+ | "filterMode"
12
+ | "isFloatingLabel"
13
+ >;
5
14
 
6
15
  const Search = forwardRef<HTMLInputElement, SearchProps>((props, ref) => {
7
16
  return (
8
17
  <Dropdown
18
+ label="Search"
9
19
  {...props}
10
20
  ref={ref}
21
+ keepCloseIconOnValue
11
22
  hasClearIcon
12
23
  hasSearchIcon
13
- label="Search"
14
24
  endIcon={null}
15
25
  filterMode
26
+ isFloatingLabel={false}
16
27
  />
17
28
  );
18
29
  });