@lateralus-ai/shipping-ui 1.1.1 → 1.2.0
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.
- package/dist/components/ModalPanel.d.ts +4 -0
- package/dist/components/PdfViewer/ImageViewer.d.ts +2 -2
- package/dist/components/PdfViewer/PdfViewer.d.ts +3 -3
- package/dist/components/PdfViewer/usePageManagement.d.ts +5 -1
- package/dist/components/SearchModal.d.ts +24 -0
- package/dist/components/Tabs.d.ts +13 -0
- package/dist/components/icons/SettingsIcon.d.ts +3 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/defect-report.pdf +0 -0
- package/dist/example.pdf +0 -0
- package/dist/index.cjs +21 -10
- package/dist/index.esm.js +17861 -1053
- package/dist/material-theme.d.ts +26 -0
- package/dist/stories/SearchModal.d.ts +1 -0
- package/dist/style.css +1 -0
- package/dist/tailwind-theme.d.ts +12 -0
- package/package.json +2 -1
- package/src/components/ModalPanel.tsx +13 -0
- package/src/components/PdfViewer/ImageViewer.tsx +11 -6
- package/src/components/PdfViewer/PdfViewer.tsx +142 -19
- package/src/components/PdfViewer/usePageManagement.ts +22 -7
- package/src/components/SearchModal.tsx +320 -0
- package/src/components/Tabs.tsx +43 -0
- package/src/components/icons/SettingsIcon.tsx +33 -0
- package/src/components/index.ts +1 -0
- package/src/material-theme.ts +30 -0
- package/src/stories/ModalHeader.stories.tsx +36 -0
- package/src/stories/PDFViewer.stories.tsx +5 -5
- package/src/stories/SearchModal.stories.tsx +132 -0
- package/src/stories/SearchModal.tsx +82 -0
- package/src/stories/Tabs.stories.tsx +51 -0
- package/src/styles/tabs.css +55 -0
- package/src/tailwind-theme.ts +12 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { Dialog, DialogBody } from "@material-tailwind/react";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useHotkeys } from "react-hotkeys-hook";
|
|
4
|
+
import { useMediaQuery } from "@uidotdev/usehooks";
|
|
5
|
+
import { cn } from "../utils/cn";
|
|
6
|
+
|
|
7
|
+
export const formatDateReport = (date: string | Date) => {
|
|
8
|
+
const formattedDate = new Date(date);
|
|
9
|
+
return formattedDate.toLocaleDateString("en-US", {
|
|
10
|
+
month: "long",
|
|
11
|
+
day: "numeric",
|
|
12
|
+
year: "numeric",
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const highlightText = (text: string, searchTerm: string) => {
|
|
17
|
+
if (!searchTerm) return text;
|
|
18
|
+
|
|
19
|
+
const regex = new RegExp(`(\\b${searchTerm}\\b)`, "gi");
|
|
20
|
+
const parts = text.split(regex);
|
|
21
|
+
const firstMatchIndex = parts.findIndex((part) => regex.test(part));
|
|
22
|
+
|
|
23
|
+
if (firstMatchIndex === -1) return text;
|
|
24
|
+
|
|
25
|
+
if (firstMatchIndex === 0) {
|
|
26
|
+
return parts.map((part, i) =>
|
|
27
|
+
regex.test(part) ? (
|
|
28
|
+
<span key={i} className="text-gray-900 font-medium">
|
|
29
|
+
{part}
|
|
30
|
+
</span>
|
|
31
|
+
) : (
|
|
32
|
+
part
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const matchedPart = parts[firstMatchIndex];
|
|
38
|
+
const beforeMatch = parts.slice(0, firstMatchIndex).join("");
|
|
39
|
+
const afterMatch = parts.slice(firstMatchIndex + 1).join("");
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
<span key="match" className="text-gray-900 font-medium">
|
|
44
|
+
{beforeMatch && <>...</>}
|
|
45
|
+
{matchedPart}
|
|
46
|
+
</span>
|
|
47
|
+
{afterMatch && <span key="after"> {afterMatch}</span>}
|
|
48
|
+
</>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export interface SearchItem {
|
|
53
|
+
id: string;
|
|
54
|
+
title: string;
|
|
55
|
+
subtitle?: string;
|
|
56
|
+
icon?: React.ReactNode;
|
|
57
|
+
metadata?: Record<string, any>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SearchModalProps {
|
|
61
|
+
open: boolean;
|
|
62
|
+
onClose: () => void;
|
|
63
|
+
items?: SearchItem[];
|
|
64
|
+
loading?: boolean;
|
|
65
|
+
placeholder?: string;
|
|
66
|
+
onSearch?: (query: string) => void;
|
|
67
|
+
onSelect?: (item: SearchItem) => void;
|
|
68
|
+
filterOptions?: string[];
|
|
69
|
+
selectedFilter?: string;
|
|
70
|
+
onFilterChange?: (filter: string) => void;
|
|
71
|
+
recentlyViewedLabel?: string;
|
|
72
|
+
noResultsText?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const SearchFilters = ({
|
|
76
|
+
filters,
|
|
77
|
+
selected,
|
|
78
|
+
onChange,
|
|
79
|
+
}: {
|
|
80
|
+
filters: string[];
|
|
81
|
+
selected: string;
|
|
82
|
+
onChange: (filter: string) => void;
|
|
83
|
+
}) => {
|
|
84
|
+
return (
|
|
85
|
+
<div className="flex gap-2 px-2">
|
|
86
|
+
{filters.map((filter) => (
|
|
87
|
+
<button
|
|
88
|
+
key={filter}
|
|
89
|
+
onClick={() => onChange(filter)}
|
|
90
|
+
className={cn(
|
|
91
|
+
"px-3 py-1 text-sm rounded-full transition-colors",
|
|
92
|
+
selected === filter
|
|
93
|
+
? "bg-brand-600 text-white"
|
|
94
|
+
: "bg-gray-100 text-gray-600 hover:bg-gray-200",
|
|
95
|
+
)}
|
|
96
|
+
>
|
|
97
|
+
{filter}
|
|
98
|
+
</button>
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const SearchSkeleton = () => {
|
|
105
|
+
return (
|
|
106
|
+
<div className="p-4 flex flex-col gap-[6px]">
|
|
107
|
+
{[...Array(8)].map((_, i) => (
|
|
108
|
+
<div key={i} className="flex items-center gap-3 animate-pulse py-2">
|
|
109
|
+
<div className="size-3 bg-gray-100 rounded-full" />
|
|
110
|
+
<div className={cn(i % 2 === 0 ? "w-[250px]" : "w-[320px]","h-3 bg-gray-100 rounded-full")} />
|
|
111
|
+
</div>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const SearchItemComponent = ({
|
|
118
|
+
item,
|
|
119
|
+
isActive,
|
|
120
|
+
onClick,
|
|
121
|
+
searchTerm,
|
|
122
|
+
}: {
|
|
123
|
+
item: SearchItem;
|
|
124
|
+
isActive: boolean;
|
|
125
|
+
onClick: () => void;
|
|
126
|
+
searchTerm: string;
|
|
127
|
+
}) => {
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
onClick={onClick}
|
|
131
|
+
className={cn(
|
|
132
|
+
"flex items-center gap-3 p-2 rounded-lg cursor-pointer transition-colors",
|
|
133
|
+
isActive ? "bg-gray-100" : "hover:bg-gray-50",
|
|
134
|
+
)}
|
|
135
|
+
>
|
|
136
|
+
{item.icon && <div className="shrink-0 text-gray-600">{item.icon}</div>}
|
|
137
|
+
<div className="flex-1 min-w-0">
|
|
138
|
+
<div className="text-sm font-medium text-gray-900 truncate">
|
|
139
|
+
{item.title}
|
|
140
|
+
</div>
|
|
141
|
+
{item.subtitle && (
|
|
142
|
+
<div className="text-sm text-gray-600 truncate">
|
|
143
|
+
{typeof item.subtitle === "string"
|
|
144
|
+
? highlightText(item.subtitle, searchTerm)
|
|
145
|
+
: item.subtitle}
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const SearchModal = ({
|
|
154
|
+
open,
|
|
155
|
+
onClose,
|
|
156
|
+
items = [],
|
|
157
|
+
loading = false,
|
|
158
|
+
placeholder = "Search...",
|
|
159
|
+
onSearch,
|
|
160
|
+
onSelect,
|
|
161
|
+
filterOptions = [],
|
|
162
|
+
selectedFilter = "All",
|
|
163
|
+
onFilterChange,
|
|
164
|
+
recentlyViewedLabel = "Recently viewed",
|
|
165
|
+
noResultsText = "No results found.",
|
|
166
|
+
}: SearchModalProps) => {
|
|
167
|
+
const [searchValue, setSearchValue] = useState("");
|
|
168
|
+
const [activeIndex, setActiveIndex] = useState(-1);
|
|
169
|
+
const isMobile = useMediaQuery("only screen and (max-width: 768px)");
|
|
170
|
+
const itemRefs = useRef<HTMLDivElement[]>([]);
|
|
171
|
+
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
itemRefs.current = itemRefs.current.slice(0, items.length);
|
|
174
|
+
}, [items]);
|
|
175
|
+
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (activeIndex > items.length) {
|
|
178
|
+
setActiveIndex(-1);
|
|
179
|
+
}
|
|
180
|
+
}, [items, activeIndex]);
|
|
181
|
+
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
const currentItem = itemRefs.current[activeIndex];
|
|
184
|
+
currentItem?.scrollIntoView({
|
|
185
|
+
behavior: "smooth",
|
|
186
|
+
block: "nearest",
|
|
187
|
+
});
|
|
188
|
+
}, [activeIndex]);
|
|
189
|
+
|
|
190
|
+
useHotkeys(
|
|
191
|
+
"ArrowUp",
|
|
192
|
+
() => {
|
|
193
|
+
if (items.length === 0) return;
|
|
194
|
+
setActiveIndex((prev) => (prev <= 0 ? items.length - 1 : prev - 1));
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
preventDefault: true,
|
|
198
|
+
enableOnFormTags: true,
|
|
199
|
+
enabled: open,
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
useHotkeys(
|
|
204
|
+
"ArrowDown",
|
|
205
|
+
() => {
|
|
206
|
+
if (items.length === 0) return;
|
|
207
|
+
setActiveIndex((prev) => (prev >= items.length - 1 ? 0 : prev + 1));
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
preventDefault: true,
|
|
211
|
+
enableOnFormTags: true,
|
|
212
|
+
enabled: open,
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
useHotkeys(
|
|
217
|
+
"Enter",
|
|
218
|
+
() => {
|
|
219
|
+
if (activeIndex >= 0 && activeIndex < items.length) {
|
|
220
|
+
handleItemSelect(items[activeIndex]);
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
enableOnFormTags: true,
|
|
225
|
+
preventDefault: true,
|
|
226
|
+
enabled: open,
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const handleSearchChange = (value: string) => {
|
|
231
|
+
setSearchValue(value);
|
|
232
|
+
onSearch?.(value);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const handleItemSelect = (item: SearchItem) => {
|
|
236
|
+
onSelect?.(item);
|
|
237
|
+
handleClose();
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const handleClose = () => {
|
|
241
|
+
setSearchValue("");
|
|
242
|
+
setActiveIndex(-1);
|
|
243
|
+
onClose();
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const showFilters = filterOptions.length > 0 && searchValue;
|
|
247
|
+
const showRecentlyViewed = !searchValue && items.length > 0;
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<Dialog
|
|
251
|
+
open={open}
|
|
252
|
+
size="lg"
|
|
253
|
+
handler={handleClose}
|
|
254
|
+
className={cn(
|
|
255
|
+
isMobile && "w-screen m-0",
|
|
256
|
+
"focus:outline-none 2xl:min-w-px",
|
|
257
|
+
)}
|
|
258
|
+
>
|
|
259
|
+
<DialogBody className="relative px-0 py-2">
|
|
260
|
+
<div>
|
|
261
|
+
<div className="border-b border-b-gray-100 px-6 pb-2">
|
|
262
|
+
<input
|
|
263
|
+
type="text"
|
|
264
|
+
placeholder={placeholder}
|
|
265
|
+
value={searchValue}
|
|
266
|
+
onChange={(e) => handleSearchChange(e.target.value)}
|
|
267
|
+
className="w-full bg-transparent p-2 px-0 text-sm font-medium text-gray-600 placeholder-gray-400 outline-none focus:outline-none"
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
{loading ? (
|
|
272
|
+
<SearchSkeleton />
|
|
273
|
+
) : (
|
|
274
|
+
<div className="p-4 flex flex-col gap-2">
|
|
275
|
+
{showRecentlyViewed && (
|
|
276
|
+
<p className="text-sm font-normal text-gray-600 px-2">
|
|
277
|
+
{recentlyViewedLabel}
|
|
278
|
+
</p>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
{showFilters && (
|
|
282
|
+
<SearchFilters
|
|
283
|
+
filters={filterOptions}
|
|
284
|
+
selected={selectedFilter}
|
|
285
|
+
onChange={(filter) => onFilterChange?.(filter)}
|
|
286
|
+
/>
|
|
287
|
+
)}
|
|
288
|
+
|
|
289
|
+
<div className="overflow-y-auto max-h-[320px]" tabIndex={0}>
|
|
290
|
+
{items.map((item, index) => (
|
|
291
|
+
<div
|
|
292
|
+
key={item.id}
|
|
293
|
+
ref={(el) => {
|
|
294
|
+
if (el) itemRefs.current[index] = el;
|
|
295
|
+
}}
|
|
296
|
+
>
|
|
297
|
+
<SearchItemComponent
|
|
298
|
+
item={item}
|
|
299
|
+
isActive={activeIndex === index}
|
|
300
|
+
onClick={() => handleItemSelect(item)}
|
|
301
|
+
searchTerm={searchValue}
|
|
302
|
+
/>
|
|
303
|
+
</div>
|
|
304
|
+
))}
|
|
305
|
+
|
|
306
|
+
{!loading && items.length === 0 && (
|
|
307
|
+
<div className="text-sm text-gray-900 font-medium">
|
|
308
|
+
{noResultsText}
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
</DialogBody>
|
|
316
|
+
</Dialog>
|
|
317
|
+
);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export default SearchModal;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import "../styles/tabs.css";
|
|
3
|
+
import {
|
|
4
|
+
Tabs,
|
|
5
|
+
TabPanel,
|
|
6
|
+
TabsBody,
|
|
7
|
+
TabsHeader,
|
|
8
|
+
Tab,
|
|
9
|
+
} from "@material-tailwind/react";
|
|
10
|
+
import { Icon } from "@iconify/react";
|
|
11
|
+
import { cn } from "../utils/cn";
|
|
12
|
+
import { SettingsIcon } from "./icons/SettingsIcon";
|
|
13
|
+
|
|
14
|
+
interface Tab {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
content: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface TabsProps {
|
|
21
|
+
tabs: Tab[];
|
|
22
|
+
defaultTab?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const TabsContainer: React.FC<TabsProps> = () => {
|
|
26
|
+
return (
|
|
27
|
+
<Tabs className="w-[800px]">
|
|
28
|
+
<TabsHeader>
|
|
29
|
+
<Tab value="chats">Chats</Tab>
|
|
30
|
+
<Tab value="issues">Issues</Tab>
|
|
31
|
+
|
|
32
|
+
<button className="cursor-pointer mb-2 ml-auto rounded p-1 hover:bg-gray-100">
|
|
33
|
+
<SettingsIcon className="size-4" />
|
|
34
|
+
</button>
|
|
35
|
+
</TabsHeader>
|
|
36
|
+
|
|
37
|
+
<TabsBody>
|
|
38
|
+
<TabPanel value="chats">ChatsPanel</TabPanel>
|
|
39
|
+
<TabPanel value="issues">IssuesPanel</TabPanel>
|
|
40
|
+
</TabsBody>
|
|
41
|
+
</Tabs>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const SettingsIcon = ({ ...props }) => {
|
|
2
|
+
return (
|
|
3
|
+
<svg
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="16"
|
|
6
|
+
height="16"
|
|
7
|
+
viewBox="0 0 16 16"
|
|
8
|
+
fill="none"
|
|
9
|
+
{...props}
|
|
10
|
+
>
|
|
11
|
+
<path
|
|
12
|
+
fill-rule="evenodd"
|
|
13
|
+
clip-rule="evenodd"
|
|
14
|
+
d="M3.16699 7.7334C4.91574 7.73357 6.33301 9.1516 6.33301 10.9004C6.33283 12.3905 5.30337 13.6386 3.91699 13.9756V15.25C3.91685 15.664 3.58098 15.9998 3.16699 16C2.75287 16 2.41713 15.6641 2.41699 15.25V13.9756C1.0303 13.6387 0.000177983 12.3906 0 10.9004C0 9.15149 1.41809 7.7334 3.16699 7.7334ZM3.16699 9.2334C2.24652 9.2334 1.5 9.97992 1.5 10.9004C1.50021 11.8207 2.24665 12.5664 3.16699 12.5664C4.08719 12.5662 4.8328 11.8206 4.83301 10.9004C4.83301 9.98002 4.08732 9.23357 3.16699 9.2334Z"
|
|
15
|
+
fill="currentColor"
|
|
16
|
+
/>
|
|
17
|
+
<path
|
|
18
|
+
d="M11.8662 9.66699C12.2804 9.66699 12.6162 10.0028 12.6162 10.417V15.25C12.616 15.6641 12.2803 16 11.8662 16C11.4523 15.9998 11.1164 15.6639 11.1162 15.25V10.417C11.1162 10.0029 11.4522 9.66722 11.8662 9.66699Z"
|
|
19
|
+
fill="currentColor"
|
|
20
|
+
/>
|
|
21
|
+
<path
|
|
22
|
+
fill-rule="evenodd"
|
|
23
|
+
clip-rule="evenodd"
|
|
24
|
+
d="M11.8662 0C12.2804 0 12.6162 0.335786 12.6162 0.75V2.02344C14.0032 2.36025 15.0332 3.60999 15.0332 5.10059C15.0329 6.84904 13.6157 8.26633 11.8672 8.2666C10.1185 8.2666 8.70052 6.84921 8.7002 5.10059C8.7002 3.6104 9.7297 2.36066 11.1162 2.02344V0.75C11.1162 0.335929 11.4522 0.000230806 11.8662 0ZM11.8672 3.43359C10.9467 3.43359 10.2002 4.18011 10.2002 5.10059C10.2005 6.02078 10.9469 6.7666 11.8672 6.7666C12.7872 6.76633 13.5329 6.02062 13.5332 5.10059C13.5332 4.18028 12.7874 3.43387 11.8672 3.43359Z"
|
|
25
|
+
fill="currentColor"
|
|
26
|
+
/>
|
|
27
|
+
<path
|
|
28
|
+
d="M3.16699 0C3.58107 0.000164881 3.91699 0.335888 3.91699 0.75V5.58301C3.91699 5.99712 3.58107 6.33284 3.16699 6.33301C2.75278 6.33301 2.41699 5.99722 2.41699 5.58301V0.75C2.41699 0.335786 2.75278 0 3.16699 0Z"
|
|
29
|
+
fill="currentColor"
|
|
30
|
+
/>
|
|
31
|
+
</svg>
|
|
32
|
+
);
|
|
33
|
+
};
|
package/src/components/index.ts
CHANGED
package/src/material-theme.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { tabPanel } from "@material-tailwind/react";
|
|
2
|
+
|
|
1
3
|
export const materialTheme = {
|
|
2
4
|
button: {
|
|
3
5
|
defaultProps: {
|
|
@@ -444,4 +446,32 @@ export const materialTheme = {
|
|
|
444
446
|
},
|
|
445
447
|
},
|
|
446
448
|
},
|
|
449
|
+
tabsHeader: {
|
|
450
|
+
defaultProps: {
|
|
451
|
+
className:
|
|
452
|
+
"grow gap-6 rounded-none bg-transparent !p-0 border-b border-b-gray-100 flex items-end px-0 w-full mb-6",
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
tab: {
|
|
456
|
+
defaultProps: {
|
|
457
|
+
className:
|
|
458
|
+
"min-w-fit max-w-fit justify-start font-medium text-gray-600 pl-0 pr-2 h-12",
|
|
459
|
+
},
|
|
460
|
+
styles: {
|
|
461
|
+
base: {
|
|
462
|
+
indicator: {
|
|
463
|
+
bg: "bg-transparent border-b-2 border-gray-900 !transform-none",
|
|
464
|
+
borderRadius: "radius-none",
|
|
465
|
+
boxShadow: "shadow-none",
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
tabPanel: {
|
|
471
|
+
styles: {
|
|
472
|
+
base: {
|
|
473
|
+
p: "px-0",
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
},
|
|
447
477
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { ModalPanel } from "../components/ModalPanel";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ModalPanel.Header> = {
|
|
5
|
+
title: "Design System/ModalPanel",
|
|
6
|
+
component: ModalPanel.Header,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: "centered",
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj<typeof ModalPanel.Header>;
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
args: {
|
|
17
|
+
onClose: () => console.log("Close button clicked"),
|
|
18
|
+
children: "Modal Panel Example",
|
|
19
|
+
},
|
|
20
|
+
decorators: [
|
|
21
|
+
(Story) => (
|
|
22
|
+
<div className="w-[500px] border border-gray-200 rounded-xl">
|
|
23
|
+
<Story />
|
|
24
|
+
<ModalPanel.Body className="h-64 p-4">
|
|
25
|
+
<p className="text-body">
|
|
26
|
+
This is the modal body content. The height is controlled by the h-64 class,
|
|
27
|
+
and the width fills the container automatically.
|
|
28
|
+
</p>
|
|
29
|
+
<p className="text-body mt-4">
|
|
30
|
+
You can add any content here and control the height through the className prop.
|
|
31
|
+
</p>
|
|
32
|
+
</ModalPanel.Body>
|
|
33
|
+
</div>
|
|
34
|
+
),
|
|
35
|
+
],
|
|
36
|
+
};
|
|
@@ -13,11 +13,11 @@ const StoryPage = () => {
|
|
|
13
13
|
}
|
|
14
14
|
/>
|
|
15
15
|
</div>
|
|
16
|
-
<div
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/>
|
|
16
|
+
<div
|
|
17
|
+
className="w-[600px] h-[800px] p-8 flex flex-col"
|
|
18
|
+
title="Document Viewer"
|
|
19
|
+
>
|
|
20
|
+
<PdfViewer src="/defect-report.pdf" />
|
|
21
21
|
</div>
|
|
22
22
|
</div>
|
|
23
23
|
);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import SearchModal from "../components/SearchModal";
|
|
4
|
+
import type { SearchItem } from "../components/SearchModal";
|
|
5
|
+
import { Icon } from "@iconify/react";
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof SearchModal> = {
|
|
8
|
+
title: "Design System/SearchModal",
|
|
9
|
+
component: SearchModal,
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: "centered",
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof SearchModal>;
|
|
17
|
+
|
|
18
|
+
const mockItems: SearchItem[] = [
|
|
19
|
+
{
|
|
20
|
+
id: "1",
|
|
21
|
+
title: "Engine maintenance report",
|
|
22
|
+
subtitle: "Created by John Doe on January 15, 2025",
|
|
23
|
+
icon: <Icon icon="lucide:users" />,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "2",
|
|
27
|
+
title: "Navigation system update",
|
|
28
|
+
subtitle: "Updated yesterday at 3:45 PM",
|
|
29
|
+
icon: <Icon icon="lucide:message-circle" />,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "3",
|
|
33
|
+
title: "Crew training schedule",
|
|
34
|
+
subtitle: "Due date: February 1, 2025",
|
|
35
|
+
icon: <Icon icon="lucide:file" />,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "4",
|
|
39
|
+
title: "Fuel consumption analysis",
|
|
40
|
+
subtitle: "Last reviewed 3 days ago",
|
|
41
|
+
icon: <Icon icon="lucide:file-lock" />,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "5",
|
|
45
|
+
title: "Weather routing optimization",
|
|
46
|
+
subtitle: "Active route to Singapore",
|
|
47
|
+
icon: <Icon icon="lucide:file-lock" />,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const SearchModalWrapper = (args: any) => {
|
|
52
|
+
const [open, setOpen] = useState(true);
|
|
53
|
+
const [items, setItems] = useState(mockItems);
|
|
54
|
+
const [loading, setLoading] = useState(false);
|
|
55
|
+
const [selectedFilter, setSelectedFilter] = useState("All");
|
|
56
|
+
|
|
57
|
+
const handleSearch = (query: string) => {
|
|
58
|
+
setLoading(true);
|
|
59
|
+
setTimeout(() => {
|
|
60
|
+
const filtered = mockItems.filter(
|
|
61
|
+
(item) =>
|
|
62
|
+
item.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
63
|
+
item.subtitle?.toLowerCase().includes(query.toLowerCase()),
|
|
64
|
+
);
|
|
65
|
+
setItems(filtered);
|
|
66
|
+
setLoading(false);
|
|
67
|
+
}, 300);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const handleSelect = (item: SearchItem) => {
|
|
71
|
+
console.log("Selected item:", item);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
<button
|
|
77
|
+
onClick={() => setOpen(true)}
|
|
78
|
+
className="px-4 py-2 bg-blue-500 text-white rounded-lg"
|
|
79
|
+
>
|
|
80
|
+
Open Search
|
|
81
|
+
</button>
|
|
82
|
+
<SearchModal
|
|
83
|
+
{...args}
|
|
84
|
+
open={open}
|
|
85
|
+
onClose={() => setOpen(false)}
|
|
86
|
+
items={items}
|
|
87
|
+
loading={loading}
|
|
88
|
+
onSearch={handleSearch}
|
|
89
|
+
onSelect={handleSelect}
|
|
90
|
+
selectedFilter={selectedFilter}
|
|
91
|
+
onFilterChange={setSelectedFilter}
|
|
92
|
+
/>
|
|
93
|
+
</>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const Default: Story = {
|
|
98
|
+
render: (args) => <SearchModalWrapper {...args} />,
|
|
99
|
+
args: {
|
|
100
|
+
placeholder: "Search reports, chats, or issues…",
|
|
101
|
+
recentlyViewedLabel: "Recently viewed",
|
|
102
|
+
noResultsText: "No results found.",
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const WithFilters: Story = {
|
|
107
|
+
render: (args) => <SearchModalWrapper {...args} />,
|
|
108
|
+
args: {
|
|
109
|
+
placeholder: "Search reports, chats, or issues…",
|
|
110
|
+
filterOptions: ["All", "Chats", "Issues", "Resolved"],
|
|
111
|
+
recentlyViewedLabel: "Recently viewed",
|
|
112
|
+
noResultsText: "No results found.",
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const Loading: Story = {
|
|
117
|
+
args: {
|
|
118
|
+
open: true,
|
|
119
|
+
loading: true,
|
|
120
|
+
onClose: () => console.log("Closed"),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const Empty: Story = {
|
|
125
|
+
args: {
|
|
126
|
+
open: true,
|
|
127
|
+
items: [],
|
|
128
|
+
onClose: () => console.log("Closed"),
|
|
129
|
+
placeholder: "Search for something...",
|
|
130
|
+
noResultsText: "No items found. Try a different search.",
|
|
131
|
+
},
|
|
132
|
+
};
|