@rovula/ui 0.0.69 → 0.0.70

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.
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rovula/ui",
3
- "version": "0.0.69",
3
+ "version": "0.0.70",
4
4
  "main": "dist/cjs/bundle.js",
5
5
  "module": "dist/esm/bundle.js",
6
6
  "types": "dist/index.d.ts",
@@ -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";