@rovula/ui 0.0.69 → 0.0.71

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 (31) hide show
  1. package/dist/cjs/bundle.css +23 -9
  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.styles.d.ts +0 -1
  5. package/dist/cjs/types/components/FocusedScrollView/FocusedScrollView.d.ts +12 -0
  6. package/dist/cjs/types/components/FocusedScrollView/FocusedScrollView.stories.d.ts +7 -0
  7. package/dist/cjs/types/index.d.ts +1 -0
  8. package/dist/components/Dropdown/Dropdown.js +2 -2
  9. package/dist/components/Dropdown/Dropdown.stories.js +1 -0
  10. package/dist/components/Dropdown/Dropdown.styles.js +6 -6
  11. package/dist/components/DropdownMenu/DropdownMenu.js +2 -2
  12. package/dist/components/FocusedScrollView/FocusedScrollView.js +67 -0
  13. package/dist/components/FocusedScrollView/FocusedScrollView.stories.js +33 -0
  14. package/dist/esm/bundle.css +23 -9
  15. package/dist/esm/bundle.js +2 -2
  16. package/dist/esm/bundle.js.map +1 -1
  17. package/dist/esm/types/components/Dropdown/Dropdown.styles.d.ts +0 -1
  18. package/dist/esm/types/components/FocusedScrollView/FocusedScrollView.d.ts +12 -0
  19. package/dist/esm/types/components/FocusedScrollView/FocusedScrollView.stories.d.ts +7 -0
  20. package/dist/esm/types/index.d.ts +1 -0
  21. package/dist/index.d.ts +13 -2
  22. package/dist/index.js +1 -0
  23. package/dist/src/theme/global.css +30 -11
  24. package/package.json +1 -1
  25. package/src/components/Dropdown/Dropdown.stories.tsx +2 -0
  26. package/src/components/Dropdown/Dropdown.styles.ts +6 -6
  27. package/src/components/Dropdown/Dropdown.tsx +4 -10
  28. package/src/components/DropdownMenu/DropdownMenu.tsx +10 -2
  29. package/src/components/FocusedScrollView/FocusedScrollView.stories.tsx +114 -0
  30. package/src/components/FocusedScrollView/FocusedScrollView.tsx +112 -0
  31. package/src/index.ts +1 -0
@@ -2,7 +2,6 @@ export declare const iconWrapperVariant: (props?: ({
2
2
  size?: "sm" | "md" | "lg" | null | undefined;
3
3
  } & import("class-variance-authority/dist/types").ClassProp) | undefined) => string;
4
4
  export declare const dropdownIconVariant: (props?: ({
5
- size?: "sm" | "md" | "lg" | null | undefined;
6
5
  disabled?: boolean | null | undefined;
7
6
  isFocus?: boolean | null | undefined;
8
7
  } & import("class-variance-authority/dist/types").ClassProp) | undefined) => string;
@@ -0,0 +1,12 @@
1
+ import React, { ReactNode, HTMLAttributes } from "react";
2
+ type ScrollAlign = "start" | "center" | "end";
3
+ type FocusedScrollViewProps = {
4
+ selectedKey?: string;
5
+ children: ReactNode;
6
+ direction?: "vertical" | "horizontal";
7
+ className?: string;
8
+ containerProps?: HTMLAttributes<HTMLDivElement>;
9
+ scrollAlign?: ScrollAlign;
10
+ };
11
+ export declare const FocusedScrollView: React.FC<FocusedScrollViewProps>;
12
+ export {};
@@ -0,0 +1,7 @@
1
+ import { Meta, StoryObj } from "@storybook/react";
2
+ import { FocusedScrollView } from "./FocusedScrollView";
3
+ declare const meta: Meta<typeof FocusedScrollView>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof FocusedScrollView>;
6
+ export declare const VerticalScroll: Story;
7
+ export declare const HorizontalScroll: Story;
@@ -33,6 +33,7 @@ export * from "./components/Toast/Toast";
33
33
  export * from "./components/Toast/Toaster";
34
34
  export * from "./components/Toast/useToast";
35
35
  export * from "./components/Tree";
36
+ export * from "./components/FocusedScrollView/FocusedScrollView";
36
37
  export type { ButtonProps } from "./components/Button/Button";
37
38
  export type { InputProps } from "./components/TextInput/TextInput";
38
39
  export type { DropdownProps, Options } from "./components/Dropdown/Dropdown";
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import React__default, { ReactElement, ReactNode, CSSProperties, FC, ComponentPropsWithoutRef } from 'react';
2
+ import React__default, { ReactElement, ReactNode, CSSProperties, FC, ComponentPropsWithoutRef, HTMLAttributes } from 'react';
3
3
  import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
4
4
  import * as class_variance_authority_dist_types from 'class-variance-authority/dist/types';
5
5
  import * as LabelPrimitive from '@radix-ui/react-label';
@@ -736,6 +736,17 @@ declare const Tree: FC<TreeProps>;
736
736
 
737
737
  declare const TreeItem: FC<TreeItemProps>;
738
738
 
739
+ type ScrollAlign = "start" | "center" | "end";
740
+ type FocusedScrollViewProps = {
741
+ selectedKey?: string;
742
+ children: ReactNode;
743
+ direction?: "vertical" | "horizontal";
744
+ className?: string;
745
+ containerProps?: HTMLAttributes<HTMLDivElement>;
746
+ scrollAlign?: ScrollAlign;
747
+ };
748
+ declare const FocusedScrollView: React__default.FC<FocusedScrollViewProps>;
749
+
739
750
  declare const resloveTimestamp: (timestamp: number) => number;
740
751
  declare const getStartDateOfDay: (date: Date) => Date;
741
752
  declare const getEndDateOfDay: (date: Date) => Date;
@@ -749,4 +760,4 @@ declare function usePrevious<T>(value: T): T | undefined;
749
760
 
750
761
  declare function cn(...inputs: ClassValue[]): string;
751
762
 
752
- export { ActionButton, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, Avatar, AvatarGroup, type AvatarGroupProps, type AvatarProps, Button, type ButtonProps, Calendar, Checkbox, Collapsible, DataTable, type DataTableProps, DatePicker, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Dropdown, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, type DropdownProps, Icon, Input, InputFilter, type InputFilterProps, type InputProps, Label, Loading, Navbar, type NavbarProps, type Options$1 as Options, Popover, PopoverContent, PopoverTrigger, ProgressBar, Search, type SearchProps, Slider, type SliderProps, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, Text, TextInput, Toast$1 as Toast, ToastAction, type ToastActionElement, ToastClose, ToastDescription, type ToastProps, ToastProvider, ToastTitle, ToastViewport, Toaster, Tooltip, TooltipArrow, TooltipContent, TooltipProvider, TooltipSimple, TooltipTrigger, Tree, type TreeData, TreeItem, type TreeItemProps, type TreeProps, cn, getEndDateOfDay, getStartDateOfDay, getStartEndTimestampOfDay, getTimestampUTC, reducer, resloveTimestamp, toast, usePrevious, useToast };
763
+ export { ActionButton, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, Avatar, AvatarGroup, type AvatarGroupProps, type AvatarProps, Button, type ButtonProps, Calendar, Checkbox, Collapsible, DataTable, type DataTableProps, DatePicker, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Dropdown, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, type DropdownProps, FocusedScrollView, Icon, Input, InputFilter, type InputFilterProps, type InputProps, Label, Loading, Navbar, type NavbarProps, type Options$1 as Options, Popover, PopoverContent, PopoverTrigger, ProgressBar, Search, type SearchProps, Slider, type SliderProps, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, Text, TextInput, Toast$1 as Toast, ToastAction, type ToastActionElement, ToastClose, ToastDescription, type ToastProps, ToastProvider, ToastTitle, ToastViewport, Toaster, Tooltip, TooltipArrow, TooltipContent, TooltipProvider, TooltipSimple, TooltipTrigger, Tree, type TreeData, TreeItem, type TreeItemProps, type TreeProps, cn, getEndDateOfDay, getStartDateOfDay, getStartEndTimestampOfDay, getTimestampUTC, reducer, resloveTimestamp, toast, usePrevious, useToast };
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ export * from "./components/Toast/Toast";
35
35
  export * from "./components/Toast/Toaster";
36
36
  export * from "./components/Toast/useToast";
37
37
  export * from "./components/Tree";
38
+ export * from "./components/FocusedScrollView/FocusedScrollView";
38
39
  // UTILS
39
40
  export { resloveTimestamp, getStartDateOfDay, getEndDateOfDay, getStartEndTimestampOfDay, getTimestampUTC, } from "./utils/datetime";
40
41
  // Hooks
@@ -2361,6 +2361,11 @@ input[type=number] {
2361
2361
  margin-right: -0.5rem;
2362
2362
  }
2363
2363
 
2364
+ .mx-2 {
2365
+ margin-left: 0.5rem;
2366
+ margin-right: 0.5rem;
2367
+ }
2368
+
2364
2369
  .mx-4 {
2365
2370
  margin-left: 1rem;
2366
2371
  margin-right: 1rem;
@@ -2452,6 +2457,10 @@ input[type=number] {
2452
2457
  display: block;
2453
2458
  }
2454
2459
 
2460
+ .inline-block {
2461
+ display: inline-block;
2462
+ }
2463
+
2455
2464
  .flex {
2456
2465
  display: flex;
2457
2466
  }
@@ -2663,6 +2672,10 @@ input[type=number] {
2663
2672
  width: 0.5rem;
2664
2673
  }
2665
2674
 
2675
+ .w-28 {
2676
+ width: 7rem;
2677
+ }
2678
+
2666
2679
  .w-3 {
2667
2680
  width: 0.75rem;
2668
2681
  }
@@ -2753,6 +2766,10 @@ input[type=number] {
2753
2766
  min-width: fit-content;
2754
2767
  }
2755
2768
 
2769
+ .max-w-full {
2770
+ max-width: 100%;
2771
+ }
2772
+
2756
2773
  .max-w-lg {
2757
2774
  max-width: 32rem;
2758
2775
  }
@@ -3027,6 +3044,10 @@ input[type=number] {
3027
3044
  text-overflow: ellipsis;
3028
3045
  }
3029
3046
 
3047
+ .whitespace-nowrap {
3048
+ white-space: nowrap;
3049
+ }
3050
+
3030
3051
  .rounded {
3031
3052
  border-radius: 0.25rem;
3032
3053
  }
@@ -4889,6 +4910,11 @@ input[type=number] {
4889
4910
  background-color: color-mix(in srgb, var(--other-transparency-warning-8) calc(100% * var(--tw-bg-opacity, 1)), transparent);
4890
4911
  }
4891
4912
 
4913
+ .bg-white {
4914
+ --tw-bg-opacity: 1;
4915
+ background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
4916
+ }
4917
+
4892
4918
  .bg-white-transparent-12 {
4893
4919
  --tw-bg-opacity: 1;
4894
4920
  background-color: color-mix(in srgb, var(--other-transparency-white-12) calc(100% * var(--tw-bg-opacity, 1)), transparent);
@@ -5545,6 +5571,10 @@ input[type=number] {
5545
5571
  text-transform: capitalize;
5546
5572
  }
5547
5573
 
5574
+ .leading-\[3rem\] {
5575
+ line-height: 3rem;
5576
+ }
5577
+
5548
5578
  .leading-none {
5549
5579
  line-height: 1;
5550
5580
  }
@@ -5936,12 +5966,6 @@ input[type=number] {
5936
5966
  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
5937
5967
  }
5938
5968
 
5939
- .shadow-\[0px_2px_24px_0px_rgba\(145\2c _158\2c _171\2c _0\.24\)\] {
5940
- --tw-shadow: 0px 2px 24px 0px rgba(145, 158, 171, 0.24);
5941
- --tw-shadow-colored: 0px 2px 24px 0px var(--tw-shadow-color);
5942
- box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
5943
- }
5944
-
5945
5969
  .shadow-lg {
5946
5970
  --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
5947
5971
  --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
@@ -5954,11 +5978,6 @@ input[type=number] {
5954
5978
  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
5955
5979
  }
5956
5980
 
5957
- .shadow-\[var\(--dropdown-menu-shadow\)\] {
5958
- --tw-shadow-color: var(--dropdown-menu-shadow);
5959
- --tw-shadow: var(--tw-shadow-colored);
5960
- }
5961
-
5962
5981
  .outline-none {
5963
5982
  outline: 2px solid transparent;
5964
5983
  outline-offset: 2px;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rovula/ui",
3
- "version": "0.0.69",
3
+ "version": "0.0.71",
4
4
  "main": "dist/cjs/bundle.js",
5
5
  "module": "dist/esm/bundle.js",
6
6
  "types": "dist/index.d.ts",
@@ -141,6 +141,8 @@ export const CustomOption = {
141
141
  fullwidth: true,
142
142
  options: customOptions,
143
143
  filterMode: true,
144
+
145
+ // iconMode: "flat",
144
146
  },
145
147
  render: (args) => {
146
148
  console.log("args ", args);
@@ -19,11 +19,12 @@ export const iconWrapperVariant = cva(
19
19
 
20
20
  export const dropdownIconVariant = cva(["transition-all"], {
21
21
  variants: {
22
- size: {
23
- sm: "size-[14px]",
24
- md: "size-5",
25
- lg: "size-6",
26
- },
22
+ // Controll by text-input
23
+ // size: {
24
+ // sm: "size-[14px]",
25
+ // md: "size-5",
26
+ // lg: "size-6",
27
+ // },
27
28
  disabled: {
28
29
  true: "fill-input-text-disabled",
29
30
  false: "fill-inherit",
@@ -34,7 +35,6 @@ export const dropdownIconVariant = cva(["transition-all"], {
34
35
  },
35
36
  },
36
37
  defaultVariants: {
37
- size: "md",
38
38
  disabled: false,
39
39
  isFocus: false,
40
40
  },
@@ -12,11 +12,7 @@ import React, {
12
12
  } from "react";
13
13
  import * as Portal from "@radix-ui/react-portal";
14
14
  import TextInput, { InputProps } from "../TextInput/TextInput";
15
- import {
16
- customInputVariant,
17
- dropdownIconVariant,
18
- iconWrapperVariant,
19
- } from "./Dropdown.styles";
15
+ import { customInputVariant, dropdownIconVariant } from "./Dropdown.styles";
20
16
 
21
17
  import { ChevronDownIcon } from "@heroicons/react/16/solid";
22
18
  import { cn } from "@/utils/cn";
@@ -339,11 +335,9 @@ const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
339
335
  <TextInput
340
336
  hasClearIcon={false}
341
337
  endIcon={
342
- <div className={iconWrapperVariant({ size })}>
343
- <ChevronDownIcon
344
- className={dropdownIconVariant({ size, isFocus: isFocused })}
345
- />
346
- </div>
338
+ <ChevronDownIcon
339
+ className={dropdownIconVariant({ isFocus: isFocused })}
340
+ />
347
341
  }
348
342
  {...props}
349
343
  ref={inputRef}
@@ -52,10 +52,14 @@ const DropdownMenuSubContent = React.forwardRef<
52
52
  <DropdownMenuPrimitive.SubContent
53
53
  ref={ref}
54
54
  className={cn(
55
- "z-50 min-w-[154px] overflow-hidden rounded-md bg-base-popup text-base-popup-foreground shadow-[var(--dropdown-menu-shadow)] 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
55
+ "z-50 min-w-[154px] overflow-hidden rounded-md bg-base-popup text-base-popup-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
56
56
  className
57
57
  )}
58
58
  {...props}
59
+ style={{
60
+ boxShadow: "var(--dropdown-menu-shadow)",
61
+ ...props.style,
62
+ }}
59
63
  />
60
64
  ));
61
65
  DropdownMenuSubContent.displayName =
@@ -70,10 +74,14 @@ const DropdownMenuContent = React.forwardRef<
70
74
  ref={ref}
71
75
  sideOffset={sideOffset}
72
76
  className={cn(
73
- "z-50 min-w-[154px] overflow-hidden rounded-md bg-base-popup text-base-popup-foreground shadow-[0px_2px_24px_0px_rgba(145,_158,_171,_0.24)] 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
77
+ "z-50 min-w-[154px] overflow-hidden rounded-md bg-base-popup text-base-popup-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
74
78
  className
75
79
  )}
76
80
  {...props}
81
+ style={{
82
+ boxShadow: "var(--dropdown-menu-shadow)",
83
+ ...props.style,
84
+ }}
77
85
  />
78
86
  </DropdownMenuPrimitive.Portal>
79
87
  ));
@@ -0,0 +1,114 @@
1
+ import { Meta, StoryObj } from "@storybook/react";
2
+ import React, { useState } from "react";
3
+ import { FocusedScrollView } from "./FocusedScrollView";
4
+
5
+ const meta: Meta<typeof FocusedScrollView> = {
6
+ title: "Components/FocusedScrollView",
7
+ component: FocusedScrollView,
8
+ args: {
9
+ direction: "vertical",
10
+ scrollAlign: "center",
11
+ className: "border border-input-default-stroke rounded",
12
+ },
13
+ };
14
+
15
+ export default meta;
16
+
17
+ type Story = StoryObj<typeof FocusedScrollView>;
18
+
19
+ const items = Array.from({ length: 30 }, (_, i) => `Item ${i + 1}`);
20
+
21
+ export const VerticalScroll: Story = {
22
+ render: (args) => {
23
+ const [selected, setSelected] = useState("item-5");
24
+
25
+ return (
26
+ <div className="p-6 space-y-4">
27
+ <div className="flex gap-2">
28
+ <button
29
+ onClick={() => setSelected("item-5")}
30
+ className="px-3 py-1 bg-primary text-white rounded"
31
+ >
32
+ Scroll to Item 5
33
+ </button>
34
+ <button
35
+ onClick={() => setSelected("item-20")}
36
+ className="px-3 py-1 bg-primary text-white rounded"
37
+ >
38
+ Scroll to Item 20
39
+ </button>
40
+ <button
41
+ onClick={() => setSelected("item-29")}
42
+ className="px-3 py-1 bg-primary text-white rounded"
43
+ >
44
+ Scroll to Item 30
45
+ </button>
46
+ </div>
47
+
48
+ <FocusedScrollView {...args} selectedKey={selected}>
49
+ {items.map((item, index) => (
50
+ <div
51
+ key={`item-${index + 1}`}
52
+ className={`px-4 py-2 ${
53
+ selected === `item-${index + 1}`
54
+ ? "bg-secondary text-secondary-foreground"
55
+ : "bg-white"
56
+ }`}
57
+ >
58
+ {item}
59
+ </div>
60
+ ))}
61
+ </FocusedScrollView>
62
+ </div>
63
+ );
64
+ },
65
+ };
66
+
67
+ export const HorizontalScroll: Story = {
68
+ args: {
69
+ direction: "horizontal",
70
+ },
71
+ render: (args) => {
72
+ const [selected, setSelected] = useState("item-15");
73
+
74
+ return (
75
+ <div className="p-6 space-y-4">
76
+ <div className="flex gap-2">
77
+ <button
78
+ onClick={() => setSelected("item-5")}
79
+ className="px-3 py-1 bg-primary text-white rounded"
80
+ >
81
+ Scroll to Item 5
82
+ </button>
83
+ <button
84
+ onClick={() => setSelected("item-15")}
85
+ className="px-3 py-1 bg-primary text-white rounded"
86
+ >
87
+ Scroll to Item 15
88
+ </button>
89
+ <button
90
+ onClick={() => setSelected("item-25")}
91
+ className="px-3 py-1 bg-primary text-white rounded"
92
+ >
93
+ Scroll to Item 25
94
+ </button>
95
+ </div>
96
+
97
+ <FocusedScrollView {...args} selectedKey={selected}>
98
+ {items.map((item, index) => (
99
+ <div
100
+ key={`item-${index + 1}`}
101
+ className={`inline-block w-28 h-12 mx-2 text-center leading-[3rem] rounded ${
102
+ selected === `item-${index + 1}`
103
+ ? "bg-secondary text-secondary-foreground"
104
+ : "bg-gray-200"
105
+ }`}
106
+ >
107
+ {item}
108
+ </div>
109
+ ))}
110
+ </FocusedScrollView>
111
+ </div>
112
+ );
113
+ },
114
+ };
@@ -0,0 +1,112 @@
1
+ import React, {
2
+ useRef,
3
+ useEffect,
4
+ cloneElement,
5
+ ReactNode,
6
+ isValidElement,
7
+ HTMLAttributes,
8
+ } from "react";
9
+
10
+ type ScrollAlign = "start" | "center" | "end";
11
+
12
+ type FocusedScrollViewProps = {
13
+ selectedKey?: string;
14
+ children: ReactNode;
15
+ direction?: "vertical" | "horizontal";
16
+ className?: string;
17
+ containerProps?: HTMLAttributes<HTMLDivElement>;
18
+ scrollAlign?: ScrollAlign;
19
+ };
20
+
21
+ export const FocusedScrollView: React.FC<FocusedScrollViewProps> = ({
22
+ selectedKey,
23
+ children,
24
+ direction = "vertical",
25
+ className,
26
+ containerProps,
27
+ scrollAlign = "center",
28
+ }) => {
29
+ const containerRef = useRef<HTMLDivElement>(null);
30
+ const itemRefs = useRef<Record<string, HTMLDivElement | null>>({});
31
+
32
+ const scrollToItem = (key: string) => {
33
+ const container = containerRef.current;
34
+ const item = itemRefs.current[key];
35
+
36
+ if (container && item) {
37
+ if (direction === "vertical") {
38
+ const containerTop = container.getBoundingClientRect().top;
39
+ const itemTop = item.getBoundingClientRect().top;
40
+ const offset = itemTop - containerTop + container.scrollTop;
41
+
42
+ const containerHeight = container.clientHeight;
43
+ const itemHeight = item.offsetHeight;
44
+
45
+ let targetTop = offset;
46
+
47
+ if (scrollAlign === "center") {
48
+ targetTop = offset - (containerHeight / 2 - itemHeight / 2);
49
+ } else if (scrollAlign === "end") {
50
+ targetTop = offset + (itemHeight - containerHeight);
51
+ }
52
+
53
+ container.scrollTo({
54
+ top: targetTop,
55
+ behavior: "smooth",
56
+ });
57
+ } else if (direction === "horizontal") {
58
+ const containerLeft = container.getBoundingClientRect().left;
59
+ const itemLeft = item.getBoundingClientRect().left;
60
+ const offset = itemLeft - containerLeft + container.scrollLeft;
61
+
62
+ const containerWidth = container.clientWidth;
63
+ const itemWidth = item.offsetWidth;
64
+
65
+ let targetLeft = offset;
66
+
67
+ if (scrollAlign === "center") {
68
+ targetLeft = offset - (containerWidth / 2 - itemWidth / 2);
69
+ } else if (scrollAlign === "end") {
70
+ targetLeft = offset + (itemWidth - containerWidth);
71
+ }
72
+
73
+ container.scrollTo({
74
+ left: targetLeft,
75
+ behavior: "smooth",
76
+ });
77
+ }
78
+ }
79
+ };
80
+
81
+ useEffect(() => {
82
+ if (selectedKey) {
83
+ scrollToItem(selectedKey);
84
+ }
85
+ }, [selectedKey, direction, scrollAlign]);
86
+
87
+ const enhancedChildren = Array.isArray(children)
88
+ ? children.map((child) => {
89
+ if (isValidElement(child) && child.key) {
90
+ return cloneElement(child, {
91
+ // @ts-ignore
92
+ ref: (el: HTMLDivElement) => {
93
+ itemRefs.current[String(child.key)] = el;
94
+ },
95
+ });
96
+ }
97
+ return child;
98
+ })
99
+ : children;
100
+
101
+ return (
102
+ <div
103
+ ref={containerRef}
104
+ className={`overflow-auto ${
105
+ direction === "vertical" ? "max-h-60" : "max-w-full whitespace-nowrap"
106
+ } ${className ?? ""}`}
107
+ {...containerProps}
108
+ >
109
+ {enhancedChildren}
110
+ </div>
111
+ );
112
+ };
package/src/index.ts CHANGED
@@ -40,6 +40,7 @@ export * from "./components/Toast/Toast";
40
40
  export * from "./components/Toast/Toaster";
41
41
  export * from "./components/Toast/useToast";
42
42
  export * from "./components/Tree";
43
+ export * from "./components/FocusedScrollView/FocusedScrollView";
43
44
 
44
45
  // Export component types
45
46
  export type { ButtonProps } from "./components/Button/Button";