@lateralus-ai/shipping-ui 1.0.4

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 (87) hide show
  1. package/README.md +108 -0
  2. package/dist/components/HelloWorld.d.ts +1 -0
  3. package/dist/components/InputPrompt.d.ts +20 -0
  4. package/dist/components/ModalPanel.d.ts +8 -0
  5. package/dist/components/PdfViewer/ImageViewer.d.ts +8 -0
  6. package/dist/components/PdfViewer/PdfViewer.d.ts +7 -0
  7. package/dist/components/PdfViewer/index.d.ts +2 -0
  8. package/dist/components/PdfViewer/usePageManagement.d.ts +5 -0
  9. package/dist/components/PdfViewer/usePanning.d.ts +15 -0
  10. package/dist/components/PdfViewer/useRefDimensions.d.ts +4 -0
  11. package/dist/components/PdfViewer/useRotation.d.ts +4 -0
  12. package/dist/components/PdfViewer/useZoom.d.ts +8 -0
  13. package/dist/components/Sidebar/Button.d.ts +7 -0
  14. package/dist/components/Sidebar/Container.d.ts +5 -0
  15. package/dist/components/Sidebar/Item.d.ts +9 -0
  16. package/dist/components/Sidebar/Layout.d.ts +7 -0
  17. package/dist/components/Sidebar/Provider.d.ts +14 -0
  18. package/dist/components/Sidebar/SecondaryItem.d.ts +9 -0
  19. package/dist/components/Sidebar/ToggleCollapseButton.d.ts +1 -0
  20. package/dist/components/Sidebar/index.d.ts +7 -0
  21. package/dist/components/icons/CloseSidebarIcon.d.ts +2 -0
  22. package/dist/components/icons/CloseSidebarMidIcon.d.ts +1 -0
  23. package/dist/components/icons/ExpandIcon.d.ts +3 -0
  24. package/dist/components/icons/SendArrowIcon.d.ts +4 -0
  25. package/dist/components/icons/SendArrowIconGreen.d.ts +4 -0
  26. package/dist/components/icons/XIcon.d.ts +3 -0
  27. package/dist/components/index.d.ts +3 -0
  28. package/dist/index.cjs +30 -0
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.esm.js +3671 -0
  31. package/dist/material-theme.d.ts +416 -0
  32. package/dist/stories/Buttons.d.ts +1 -0
  33. package/dist/stories/Checkbox.d.ts +1 -0
  34. package/dist/stories/ColorPalette.d.ts +1 -0
  35. package/dist/stories/Dropdowns.d.ts +1 -0
  36. package/dist/stories/InputPrompt.d.ts +1 -0
  37. package/dist/stories/Sidebar.d.ts +1 -0
  38. package/dist/stories/Typography.d.ts +1 -0
  39. package/dist/tailwind-theme.d.ts +197 -0
  40. package/dist/utils/cn.d.ts +7 -0
  41. package/package.json +88 -0
  42. package/src/components/HelloWorld.tsx +3 -0
  43. package/src/components/InputPrompt.tsx +96 -0
  44. package/src/components/ModalPanel.tsx +31 -0
  45. package/src/components/PdfViewer/ImageViewer.tsx +167 -0
  46. package/src/components/PdfViewer/PdfViewer.tsx +53 -0
  47. package/src/components/PdfViewer/index.ts +2 -0
  48. package/src/components/PdfViewer/usePageManagement.ts +14 -0
  49. package/src/components/PdfViewer/usePanning.ts +42 -0
  50. package/src/components/PdfViewer/useRefDimensions.ts +16 -0
  51. package/src/components/PdfViewer/useRotation.ts +13 -0
  52. package/src/components/PdfViewer/useZoom.ts +26 -0
  53. package/src/components/Sidebar/Button.tsx +20 -0
  54. package/src/components/Sidebar/Container.tsx +32 -0
  55. package/src/components/Sidebar/Item.tsx +39 -0
  56. package/src/components/Sidebar/Layout.tsx +23 -0
  57. package/src/components/Sidebar/Provider.tsx +47 -0
  58. package/src/components/Sidebar/SecondaryItem.tsx +39 -0
  59. package/src/components/Sidebar/ToggleCollapseButton.tsx +24 -0
  60. package/src/components/Sidebar/index.ts +7 -0
  61. package/src/components/icons/CloseSidebarIcon.tsx +19 -0
  62. package/src/components/icons/CloseSidebarMidIcon.tsx +19 -0
  63. package/src/components/icons/ExpandIcon.tsx +21 -0
  64. package/src/components/icons/SendArrowIcon.tsx +23 -0
  65. package/src/components/icons/SendArrowIconGreen.tsx +17 -0
  66. package/src/components/icons/XIcon.tsx +21 -0
  67. package/src/components/index.ts +3 -0
  68. package/src/index.ts +4 -0
  69. package/src/material-theme.ts +447 -0
  70. package/src/stories/Buttons.stories.tsx +15 -0
  71. package/src/stories/Buttons.tsx +52 -0
  72. package/src/stories/Checkbox.stories.tsx +15 -0
  73. package/src/stories/Checkbox.tsx +56 -0
  74. package/src/stories/ColorPalette.stories.tsx +15 -0
  75. package/src/stories/ColorPalette.tsx +72 -0
  76. package/src/stories/Dropdowns.stories.tsx +15 -0
  77. package/src/stories/Dropdowns.tsx +52 -0
  78. package/src/stories/InputPrompt.stories.tsx +15 -0
  79. package/src/stories/InputPrompt.tsx +63 -0
  80. package/src/stories/PDFViewer.stories.tsx +37 -0
  81. package/src/stories/Sidebar.stories.tsx +15 -0
  82. package/src/stories/Sidebar.tsx +94 -0
  83. package/src/stories/Typography.stories.tsx +15 -0
  84. package/src/stories/Typography.tsx +110 -0
  85. package/src/style.css +2 -0
  86. package/src/tailwind-theme.ts +219 -0
  87. package/src/utils/cn.ts +11 -0
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "@lateralus-ai/shipping-ui",
3
+ "version": "1.0.4",
4
+ "description": "Shared UI theme and components for Lateralus shipping applications",
5
+ "main": "dist/index.cjs",
6
+ "module": "dist/index.esm.js",
7
+ "types": "dist/index.d.ts",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.esm.js",
14
+ "require": "./dist/index.cjs",
15
+ "types": "./dist/index.d.ts"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "lib",
21
+ "src",
22
+ "tailwind.config.js"
23
+ ],
24
+ "scripts": {
25
+ "dev": "storybook dev -p 6006",
26
+ "build": "npx rimraf ./dist && vite build",
27
+ "storybook": "storybook dev -p 6006",
28
+ "build-storybook": "storybook build",
29
+ "release": "npx release-it"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/lateralus-ai/shipping-ui.git"
34
+ },
35
+ "keywords": [
36
+ "tailwind",
37
+ "material-tailwind",
38
+ "theme",
39
+ "shipping",
40
+ "storybook"
41
+ ],
42
+ "author": "Lateralus AI",
43
+ "license": "UNLICENSED",
44
+ "bugs": {
45
+ "url": "https://github.com/lateralus-ai/shipping-ui/issues"
46
+ },
47
+ "homepage": "https://github.com/lateralus-ai/shipping-ui#readme",
48
+ "devDependencies": {
49
+ "@chromatic-com/storybook": "^1.6.1",
50
+ "@material-tailwind/react": "^2.1.10",
51
+ "@storybook/addon-essentials": "^8.2.9",
52
+ "@storybook/addon-interactions": "^8.2.9",
53
+ "@storybook/addon-links": "^8.2.9",
54
+ "@storybook/blocks": "^8.2.9",
55
+ "@storybook/react": "^8.2.9",
56
+ "@storybook/react-vite": "^8.2.9",
57
+ "@types/react": "^18.2.0",
58
+ "@types/react-dom": "^18.2.0",
59
+ "@uidotdev/usehooks": "^2.4.1",
60
+ "@vitejs/plugin-react": "^4.2.1",
61
+ "ai": "^5.0.59",
62
+ "autoprefixer": "^10.4.16",
63
+ "clsx": "^1.2.0",
64
+ "postcss": "^8.4.32",
65
+ "react": "^18.2.0",
66
+ "react-dom": "^18.2.0",
67
+ "release-it": "^19.0.5",
68
+ "storybook": "^8.2.9",
69
+ "tailwind-merge": "^1.9.0",
70
+ "tailwindcss": "^3.4.0",
71
+ "tailwindcss-animate": "^1.0.7",
72
+ "tailwindcss-radix": "^3.0.3",
73
+ "typescript": "^5.3.3",
74
+ "vite": "^5.0.10",
75
+ "vite-plugin-dts": "^4.5.4"
76
+ },
77
+ "peerDependencies": {
78
+ "react": "^18.2.0",
79
+ "react-dom": "^18.2.0",
80
+ "react-textarea-autosize": "^8.5.9"
81
+ },
82
+ "dependencies": {
83
+ "@iconify/react": "^6.0.2",
84
+ "@react-hooks-library/core": "^0.6.2",
85
+ "react-pdf": "^10.1.0",
86
+ "react-router": "^7.9.3"
87
+ }
88
+ }
@@ -0,0 +1,3 @@
1
+ export const HelloWorld = () => {
2
+ return <div className="bg-red-800 text-white p-2">Hello world</div>;
3
+ };
@@ -0,0 +1,96 @@
1
+ import { IconButton, Tooltip } from "@material-tailwind/react";
2
+ import { useState, type KeyboardEvent, type ReactNode } from "react";
3
+ import TextareaAutosize from "react-textarea-autosize";
4
+ import { cn } from "../utils/cn";
5
+ import SendArrowIcon from "./icons/SendArrowIcon";
6
+ import SendArrowIconGreen from "./icons/SendArrowIconGreen";
7
+
8
+ interface SubmitButtonProps extends React.ComponentProps<typeof IconButton> {
9
+ tooltipContent?: string;
10
+ className?: string;
11
+ }
12
+
13
+ export const SubmitButton: React.FC<SubmitButtonProps> = ({
14
+ tooltipContent = "Send message",
15
+ className,
16
+ ...props
17
+ }) => {
18
+ const [hover, setHover] = useState(false);
19
+
20
+ return (
21
+ <Tooltip content={tooltipContent}>
22
+ <IconButton
23
+ variant="text"
24
+ color="gray"
25
+ className={cn(className, "hover:rounded-lg hover:bg-gray-100")}
26
+ type="submit"
27
+ onMouseEnter={() => setHover(true)}
28
+ onMouseLeave={() => setHover(false)}
29
+ {...props}
30
+ >
31
+ {hover ? (
32
+ <SendArrowIconGreen className="size-6 " />
33
+ ) : (
34
+ <SendArrowIcon className="size-6 " />
35
+ )}
36
+ </IconButton>
37
+ </Tooltip>
38
+ );
39
+ };
40
+
41
+ interface TextareaProps extends React.ComponentProps<typeof TextareaAutosize> {
42
+ onSubmit?: () => void;
43
+ }
44
+
45
+ export const Textarea: React.FC<TextareaProps> = ({
46
+ onSubmit = () => {},
47
+ ...props
48
+ }) => {
49
+ return (
50
+ <TextareaAutosize
51
+ autoFocus
52
+ placeholder="Write your message here"
53
+ className="pl-1 block w-full focus:outline-none max-h-32 resize-none placeholder:text-gray-400"
54
+ onKeyDown={(e: KeyboardEvent<HTMLTextAreaElement>) => {
55
+ if (e.key === "Enter" && !e.shiftKey) {
56
+ e.preventDefault();
57
+ onSubmit();
58
+ }
59
+ }}
60
+ {...props}
61
+ />
62
+ );
63
+ };
64
+
65
+ interface ContainerProps extends React.HTMLAttributes<HTMLDivElement> {
66
+ bottom?: ReactNode;
67
+ right?: ReactNode;
68
+ top?: ReactNode;
69
+ textArea?: ReactNode;
70
+ }
71
+
72
+ export const Container: React.FC<ContainerProps> = ({
73
+ bottom,
74
+ right,
75
+ top,
76
+ textArea = <Textarea />,
77
+ ...props
78
+ }) => {
79
+ return (
80
+ <div
81
+ className="border gap-2 rounded-lg bg-white shadow-[0px_2px_2px_0px_#0000000A,0px_8px_8px_-8px_#0000000A] border-gray-100"
82
+ {...props}
83
+ >
84
+ {top}
85
+
86
+ <div className="flex gap-2 p-4 rounded-lg bg-white">
87
+ <div className="grow flex flex-col items-start justify-center">
88
+ {textArea}
89
+ {bottom}
90
+ </div>
91
+
92
+ <div className="shrink flex items-end justify-end">{right}</div>
93
+ </div>
94
+ </div>
95
+ );
96
+ };
@@ -0,0 +1,31 @@
1
+ import { IconButton } from "@material-tailwind/react";
2
+ import XIcon from "./icons/XIcon";
3
+
4
+ interface HeaderProps {
5
+ onClose: () => void;
6
+ right?: React.ReactNode;
7
+ }
8
+
9
+ const Header = ({ onClose, right, children }: HeaderProps) => {
10
+ return (
11
+ <div className="bg-gray-50 flex gap-2 items-center px-2 py-3 rounded-t-xl">
12
+ <div className="grow text-subheader-em">{children}</div>
13
+ <div className="flex items-center gap-4">
14
+ {right}
15
+ <IconButton
16
+ variant="text"
17
+ size="sm"
18
+ color="gray"
19
+ className="rounded-full"
20
+ onClick={onClose}
21
+ >
22
+ <XIcon className="size-4 text-gray-600" />
23
+ </IconButton>
24
+ </div>
25
+ </div>
26
+ );
27
+ };
28
+
29
+ export const ModalPanel = {
30
+ Header,
31
+ };
@@ -0,0 +1,167 @@
1
+ import { IconButton, ButtonGroup, Tooltip } from "@material-tailwind/react";
2
+ import { ModalPanel } from "../ModalPanel";
3
+ import ExpandIcon from "../icons/ExpandIcon";
4
+ import { Icon } from "@iconify/react";
5
+ import { ChangeEvent } from "react";
6
+ import { useZoom } from "./useZoom";
7
+ import { useRotation } from "./useRotation";
8
+ import { usePanning } from "./usePanning";
9
+ import { usePageManagement } from "./usePageManagement";
10
+
11
+ interface ImageViewerProps {
12
+ onClose: () => void;
13
+ totalPages: number;
14
+ getImageSrc: (page: number) => string;
15
+ title: string;
16
+ }
17
+
18
+ export const ImageViewer = ({
19
+ onClose,
20
+ totalPages,
21
+ getImageSrc,
22
+ title = "PDF Viewer",
23
+ }: ImageViewerProps) => {
24
+ const [zoom, zoomActions] = useZoom();
25
+ const [rotation, rotationActions] = useRotation();
26
+ const [{ pan, isDragging }, panActions] = usePanning();
27
+ const [currentPage, pageActions] = usePageManagement(totalPages);
28
+
29
+ const rightButtons = (
30
+ <IconButton variant="text" color="gray">
31
+ <ExpandIcon className="size-4" />
32
+ </IconButton>
33
+ );
34
+
35
+ return (
36
+ <div className="shadow rounded-t-lg">
37
+ <ModalPanel.Header onClose={onClose} right={rightButtons}>
38
+ {title}
39
+ </ModalPanel.Header>
40
+
41
+ <div className="grid">
42
+ <div className="grid relative">
43
+ <div
44
+ className="overflow-hidden col-start-1 row-start-1 bg-gray-200 h-[600px] relative"
45
+ onMouseMove={panActions.handleMouseMove}
46
+ onMouseUp={panActions.handleMouseUp}
47
+ onMouseLeave={panActions.handleMouseUp}
48
+ onWheel={zoomActions.handleWheel}
49
+ >
50
+ <div
51
+ style={{
52
+ transform: `translate(${pan.x}px, ${pan.y}px)`,
53
+ cursor: isDragging ? "grabbing" : "grab",
54
+ width: "100%",
55
+ height: "100%",
56
+ display: "flex",
57
+ alignItems: "center",
58
+ justifyContent: "center",
59
+ }}
60
+ onMouseDown={panActions.handleMouseDown}
61
+ >
62
+ <div
63
+ style={{
64
+ transform: `scale(${zoom / 100}) rotate(${rotation}deg)`,
65
+ transformOrigin: "center",
66
+ transition: isDragging ? "none" : "transform 0.3s ease",
67
+ }}
68
+ >
69
+ <img
70
+ src={getImageSrc(currentPage)}
71
+ style={{ userSelect: "none", pointerEvents: "none" }}
72
+ />
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ <div className="col-start-1 row-start-1 self-end p-4 flex gap-2 justify-between w-full z-10">
78
+ <ButtonGroup className="divide-x-0 h-[52px]">
79
+ <IconButton
80
+ variant="filled"
81
+ color="white"
82
+ className="py-[26px]"
83
+ onClick={zoomActions.zoomOut}
84
+ >
85
+ <Icon icon="lucide:minus" />
86
+ </IconButton>
87
+ <Tooltip content="Click to reset zoom and pan">
88
+ <button
89
+ className="!bg-white text-center cursor-pointer w-[60px]"
90
+ onClick={() => {
91
+ zoomActions.reset();
92
+ panActions.reset();
93
+ }}
94
+ >
95
+ {zoom}%
96
+ </button>
97
+ </Tooltip>
98
+ <IconButton
99
+ variant="filled"
100
+ color="white"
101
+ className="py-[26px]"
102
+ onClick={zoomActions.zoomIn}
103
+ >
104
+ <Icon icon="lucide:plus" />
105
+ </IconButton>
106
+ </ButtonGroup>
107
+ <ButtonGroup className="divide-x-0 h-[52px]">
108
+ <IconButton
109
+ variant="filled"
110
+ color="white"
111
+ className="py-[26px]"
112
+ onClick={rotationActions.rotateClockwise}
113
+ >
114
+ <Icon icon="lucide:iteration-cw" />
115
+ </IconButton>
116
+
117
+ <IconButton
118
+ variant="filled"
119
+ color="white"
120
+ className="py-[26px]"
121
+ onClick={rotationActions.rotateCounterClockwise}
122
+ >
123
+ <Icon icon="lucide:iteration-ccw" />
124
+ </IconButton>
125
+ </ButtonGroup>
126
+ <ButtonGroup className="divide-x-0 ">
127
+ <IconButton
128
+ variant="filled"
129
+ color="white"
130
+ className="py-[26px]"
131
+ onClick={pageActions.prevPage}
132
+ disabled={currentPage === 1}
133
+ >
134
+ <Icon icon="lucide:chevron-left" />
135
+ </IconButton>
136
+ <input
137
+ className="bg-white pl-4 flex items-center w-[50px] text-center focus:outline-none w-[60px]"
138
+ value={currentPage}
139
+ onChange={(e: ChangeEvent<HTMLInputElement>) => {
140
+ const page = parseInt(e.target.value);
141
+ if (!isNaN(page)) {
142
+ pageActions.goToPage(page);
143
+ }
144
+ }}
145
+ type="number"
146
+ min="1"
147
+ max={totalPages}
148
+ />
149
+ <div className="flex items-center bg-white px-2">
150
+ of {totalPages}
151
+ </div>
152
+ <IconButton
153
+ variant="filled"
154
+ color="white"
155
+ className="py-[26px]"
156
+ onClick={pageActions.nextPage}
157
+ disabled={currentPage === totalPages}
158
+ >
159
+ <Icon icon="lucide:chevron-right" />
160
+ </IconButton>
161
+ </ButtonGroup>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ );
167
+ };
@@ -0,0 +1,53 @@
1
+ import { useRef } from "react";
2
+ import { IconButton } from "@material-tailwind/react";
3
+ import { ModalPanel } from "../ModalPanel";
4
+ import ExpandIcon from "../icons/ExpandIcon";
5
+ import { useRefDimensions } from "./useRefDimensions";
6
+
7
+ interface PdfViewerProps {
8
+ onClose: () => void;
9
+ src: string;
10
+ title: string;
11
+ }
12
+
13
+ export const PdfViewer = ({
14
+ onClose,
15
+ src,
16
+ title = "PDF Viewer",
17
+ }: PdfViewerProps) => {
18
+ const containerRef = useRef(null);
19
+ const dimensions = useRefDimensions(containerRef);
20
+
21
+ const rightButtons = (
22
+ <IconButton variant="text" color="gray">
23
+ <a href={src} target="_blank" rel="noopener noreferrer">
24
+ <ExpandIcon className="size-4" />
25
+ </a>
26
+ </IconButton>
27
+ );
28
+
29
+ return (
30
+ <div className="shadow rounded-t-lg">
31
+ <ModalPanel.Header onClose={onClose} right={rightButtons}>
32
+ {title}
33
+ </ModalPanel.Header>
34
+
35
+ <div className="grid">
36
+ <div className="grid relative">
37
+ <div
38
+ ref={containerRef}
39
+ className="overflow-hidden col-start-1 row-start-1 bg-gray-200 h-[600px] relative"
40
+ >
41
+ <embed
42
+ src={`${src}#view=FitH&navpanes=0&scrollbar=0`}
43
+ type="application/pdf"
44
+ width={dimensions.width}
45
+ height={dimensions.height}
46
+ style={{ border: "none" }}
47
+ />
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ );
53
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./ImageViewer";
2
+ export * from "./PdfViewer";
@@ -0,0 +1,14 @@
1
+ import { useState } from "react";
2
+
3
+ export const usePageManagement = (totalPages: number = 1) => {
4
+ const [currentPage, setCurrentPage] = useState<number>(1);
5
+
6
+ const actions = {
7
+ nextPage: () => setCurrentPage((prev) => Math.min(prev + 1, totalPages)),
8
+ prevPage: () => setCurrentPage((prev) => Math.max(prev - 1, 1)),
9
+ goToPage: (page: number) =>
10
+ setCurrentPage(Math.max(1, Math.min(page, totalPages))),
11
+ };
12
+
13
+ return [currentPage, actions] as const;
14
+ };
@@ -0,0 +1,42 @@
1
+ import { useState, MouseEvent } from "react";
2
+
3
+ interface PanState {
4
+ x: number;
5
+ y: number;
6
+ }
7
+
8
+ export const usePanning = () => {
9
+ const [pan, setPan] = useState<PanState>({ x: 0, y: 0 });
10
+ const [isDragging, setIsDragging] = useState<boolean>(false);
11
+ const [dragStart, setDragStart] = useState<PanState>({ x: 0, y: 0 });
12
+
13
+ const handleMouseDown = (e: MouseEvent) => {
14
+ setIsDragging(true);
15
+ setDragStart({
16
+ x: e.clientX - pan.x,
17
+ y: e.clientY - pan.y,
18
+ });
19
+ };
20
+
21
+ const handleMouseMove = (e: MouseEvent) => {
22
+ if (!isDragging) return;
23
+
24
+ setPan({
25
+ x: e.clientX - dragStart.x,
26
+ y: e.clientY - dragStart.y,
27
+ });
28
+ };
29
+
30
+ const handleMouseUp = () => {
31
+ setIsDragging(false);
32
+ };
33
+
34
+ const actions = {
35
+ handleMouseDown,
36
+ handleMouseMove,
37
+ handleMouseUp,
38
+ reset: () => setPan({ x: 0, y: 0 }),
39
+ };
40
+
41
+ return [{ pan, isDragging }, actions] as const;
42
+ };
@@ -0,0 +1,16 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ export const useRefDimensions = (ref) => {
4
+ const [dimensions, setDimensions] = useState({ width: 1, height: 2 });
5
+
6
+ useEffect(() => {
7
+ if (ref.current) {
8
+ const { current } = ref;
9
+ const boundingRect = current.getBoundingClientRect();
10
+ const { width, height } = boundingRect;
11
+ setDimensions({ width: Math.round(width), height: Math.round(height) });
12
+ }
13
+ }, [ref]);
14
+
15
+ return dimensions;
16
+ };
@@ -0,0 +1,13 @@
1
+ import { useState } from "react";
2
+
3
+ export const useRotation = () => {
4
+ const [rotation, setRotation] = useState<number>(0);
5
+
6
+ const actions = {
7
+ rotateClockwise: () => setRotation((prev) => (prev + 90) % 360),
8
+ rotateCounterClockwise: () =>
9
+ setRotation((prev) => (prev - 90 + 360) % 360),
10
+ };
11
+
12
+ return [rotation, actions] as const;
13
+ };
@@ -0,0 +1,26 @@
1
+ import { useState, WheelEvent } from "react";
2
+
3
+ export const useZoom = (
4
+ initialZoom = 100,
5
+ minZoom = 50,
6
+ maxZoom = 990,
7
+ step = 10,
8
+ ) => {
9
+ const [zoom, setZoom] = useState<number>(initialZoom);
10
+
11
+ const actions = {
12
+ zoomIn: () => setZoom((prev) => Math.min(prev + step, maxZoom)),
13
+ zoomOut: () => setZoom((prev) => Math.max(prev - step, minZoom)),
14
+ reset: () => setZoom(initialZoom),
15
+ zoomToValue: (value: number) =>
16
+ setZoom(Math.max(minZoom, Math.min(value, maxZoom))),
17
+ };
18
+
19
+ const handleWheel = (e: WheelEvent) => {
20
+ e.preventDefault();
21
+ const delta = e.deltaY > 0 ? -step : step;
22
+ setZoom((prev) => Math.max(minZoom, Math.min(prev + delta, maxZoom)));
23
+ };
24
+
25
+ return [zoom, { ...actions, handleWheel }] as const;
26
+ };
@@ -0,0 +1,20 @@
1
+ import { useContext, type ReactNode } from "react";
2
+ import { collapsedContext } from "./Provider";
3
+
4
+ interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
5
+ children?: ReactNode;
6
+ icon?: ReactNode;
7
+ }
8
+
9
+ export const Button = ({ icon, children, ...props }: ButtonProps) => {
10
+ const { isCollapsed, isHovered } = useContext(collapsedContext)!;
11
+
12
+ return (
13
+ <button
14
+ className="flex items-center gap-2 w-full hover:bg-gray-100 text-caption-1-em text-gray-600 hover:text-gray-900 h-[38px] rounded-lg px-2"
15
+ {...props}
16
+ >
17
+ {icon} {(!isCollapsed || isHovered) && children}
18
+ </button>
19
+ );
20
+ };
@@ -0,0 +1,32 @@
1
+ import { useContext, type ReactNode } from "react";
2
+ import { cn } from "../../utils/cn";
3
+ import { collapsedContext } from "./Provider";
4
+
5
+ export interface ContainerProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ children?: ReactNode;
7
+ }
8
+
9
+ export const Container = ({
10
+ className,
11
+ children,
12
+ ...props
13
+ }: ContainerProps) => {
14
+ const { isCollapsed, isHovered, setIsHovered } =
15
+ useContext(collapsedContext)!;
16
+
17
+ return (
18
+ <div
19
+ className={cn(
20
+ className,
21
+ isCollapsed && "shadow-lg",
22
+ isCollapsed && !isHovered ? "w-auto" : "w-[280px]",
23
+ "bg-gray-50 p-2 flex flex-col justify-between items-center rounded-lg",
24
+ )}
25
+ onMouseEnter={() => isCollapsed && setIsHovered(true)}
26
+ onMouseLeave={() => isCollapsed && setIsHovered(false)}
27
+ {...props}
28
+ >
29
+ {children}
30
+ </div>
31
+ );
32
+ };
@@ -0,0 +1,39 @@
1
+ import { useContext, type ReactNode } from "react";
2
+ import { cn } from "../../utils/cn";
3
+ import { collapsedContext } from "./Provider";
4
+
5
+ interface ItemProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ children?: ReactNode;
7
+ icon?: ReactNode;
8
+ isActive?: boolean;
9
+ trailing?: ReactNode;
10
+ }
11
+
12
+ export const Item = ({
13
+ children,
14
+ className,
15
+ icon,
16
+ isActive,
17
+ trailing,
18
+ ...props
19
+ }: ItemProps) => {
20
+ const { isCollapsed, isHovered } = useContext(collapsedContext)!;
21
+
22
+ return (
23
+ <div
24
+ className={cn(
25
+ isActive ? "!text-gray-900" : "!text-gray-600",
26
+ "flex items-center justify-between w-full hover:bg-gray-100 text-caption-1-em hover:text-gray-900 h-[38px] rounded-lg px-2 cursor-pointer group",
27
+ className,
28
+ )}
29
+ {...props}
30
+ >
31
+ <div className="flex items-center gap-2">
32
+ {icon}
33
+ {(!isCollapsed || isHovered) && children}
34
+ </div>
35
+
36
+ {(!isCollapsed || isHovered) && trailing}
37
+ </div>
38
+ );
39
+ };
@@ -0,0 +1,23 @@
1
+ import { Provider } from "./Provider";
2
+ import { Container, type ContainerProps } from "./Container";
3
+
4
+ interface LayoutProps extends ContainerProps {
5
+ isCollapsed?: boolean;
6
+ onSwitchLayout?: (isCollapsed: boolean) => void;
7
+ }
8
+
9
+ export const Layout = ({
10
+ className,
11
+ children,
12
+ isCollapsed,
13
+ onSwitchLayout,
14
+ ...props
15
+ }: LayoutProps) => {
16
+ return (
17
+ <Provider isCollapsed={isCollapsed} onSwitchLayout={onSwitchLayout}>
18
+ <Container className={className} {...props}>
19
+ {children}
20
+ </Container>
21
+ </Provider>
22
+ );
23
+ };